Files
oh-my-pi/packages/coding-agent/src/edit/apply-patch/index.ts
T
Hans Josephsen 2a367bf043 feat(coding-agent/edit): add codex apply_patch as a new edit mode
Slots a new "apply_patch" variant alongside the existing edit modes
(replace, patch, hashline, chunk, vim). The mode accepts a single input
string containing a Codex *** Begin Patch / *** End Patch envelope,
parses it with a new lenient parser (heredoc-tolerant), and fans each
file-op out to the existing executePatchSingle so LSP writethrough,
plan-mode guards, fs-cache invalidation and diagnostics are shared
with the patch mode.

Exposes both tool shapes from the spec: the JSON function-tool variant
(§1.2, {input: string}) and the OpenAI custom-tool / Lark-grammar
"freeform" variant (§1.1, raw patch string). The edit tool advertises
a Lark grammar via customFormat and a wire name via customWireName;
openai-responses emits it as a grammar-constrained custom tool when a
model opts in with applyPatchToolType: "freeform" in models.json.
custom_tool_call / custom_tool_call_output are plumbed end-to-end
through the shared responses code (emission, streaming, history
replay), and the agent-loop dispatcher matches tool calls by either
name or customWireName so returned calls route correctly.

Also threads preview/diff rendering for apply_patch through the TUI
(tool-execution + edit renderer) so streaming patches show per-file
diffs like the other edit modes.

Default edit mode is unchanged (hashline); opt in via edit.mode or
PI_EDIT_VARIANT=apply_patch.
2026-04-24 00:15:17 +02:00

88 lines
2.6 KiB
TypeScript

/**
* Multi-file orchestrator for the Codex `apply_patch` envelope.
*
* Decoupled from tool-registration: takes raw patch text + options, parses
* it, and applies each hunk via the existing single-file `applyPatch` in
* `../modes/patch.ts`. A future OpenAI freeform/grammar tool variant can
* call this directly with the raw grammar output.
*
* Per spec §6.1, hunks are applied in order and NOT atomically — if hunk
* N fails, hunks `0..N-1` are already on disk. We surface that by
* returning the per-file results alongside the error when it happens.
*/
import { ApplyPatchError } from "../diff";
import { type ApplyPatchOptions, type ApplyPatchResult, applyPatch, type PatchInput } from "../modes/patch";
import { parseApplyPatch } from "./parser";
export { parseApplyPatch } from "./parser";
export interface ApplyCodexPatchResult {
/** Single-file apply results in the order they were attempted. */
results: ApplyPatchResult[];
/** Affected file paths grouped by operation, for the §9.1 summary. */
affected: {
added: string[];
modified: string[];
deleted: string[];
};
}
/**
* Apply a full Codex `*** Begin Patch` envelope.
*
* Note: renames are reported under `modified` with the original path (spec
* §9.1), not as a delete + add.
*/
export async function applyCodexPatch(patchText: string, options: ApplyPatchOptions): Promise<ApplyCodexPatchResult> {
const hunks = parseApplyPatch(patchText);
if (hunks.length === 0) {
throw new ApplyPatchError("No files were modified.");
}
const results: ApplyPatchResult[] = [];
const affected = {
added: [] as string[],
modified: [] as string[],
deleted: [] as string[],
};
for (const hunk of hunks) {
const result = await applyPatch(hunk, options);
results.push(result);
recordAffected(affected, hunk, result);
}
return { results, affected };
}
function recordAffected(
affected: ApplyCodexPatchResult["affected"],
hunk: PatchInput,
_result: ApplyPatchResult,
): void {
switch (hunk.op) {
case "create":
affected.added.push(hunk.path);
break;
case "delete":
affected.deleted.push(hunk.path);
break;
case "update":
affected.modified.push(hunk.path);
break;
}
}
/**
* Format the A/M/D summary described in spec §9.1.
*/
export function formatApplyCodexPatchSummary(affected: ApplyCodexPatchResult["affected"]): string {
const lines = ["Success. Updated the following files:"];
for (const p of affected.added) lines.push(`A ${p}`);
for (const p of affected.modified) lines.push(`M ${p}`);
for (const p of affected.deleted) lines.push(`D ${p}`);
return lines.join("\n");
}