2a367bf043
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.
88 lines
2.6 KiB
TypeScript
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");
|
|
}
|