diff --git a/extensions/pretty.ts b/extensions/pretty.ts index 06f82b4..29268d7 100644 --- a/extensions/pretty.ts +++ b/extensions/pretty.ts @@ -369,6 +369,20 @@ export default function pretty(pi: ExtensionAPI) { return diff ? renderPreviewDiff(theme, diff, args?.path) : null; } + /** + * pi's edit tool details provide both `patch` (standard unified diff with + * hunk headers) and `diff` (pre-formatted line-numbered diff). Prefer the + * standard patch so hunk-based word emphasis and side-by-side split work as + * designed, falling back to diff. + */ + function extractEditDiff(result: unknown): string { + if (!isRecord(result) || !isRecord(result.details)) return ""; + const { patch, diff } = result.details; + if (typeof patch === "string" && patch) return patch; + if (typeof diff === "string") return diff; + return ""; + } + /** Renderers per tool; delegate execution to the original untouched. */ function renderersFor(tool: PrettyTool): Record { switch (tool) { @@ -451,10 +465,7 @@ export default function pretty(pi: ExtensionAPI) { if (isFailed(result)) { return new Text(theme.fg("error", failureLine(textContent(result).split("\n")[0] || "Edit failed", clipWidth())), 0, 0); } - const diff = - isRecord(result) && isRecord(result.details) && typeof result.details.diff === "string" - ? result.details.diff - : ""; + const diff = extractEditDiff(result); const stats = statsLabel(theme, diffStats(diff), settings.diffStatMeter); if (!options.expanded) return new Text(stats, 0, 0); const body = preview(diff, true, diffLimits()); diff --git a/test/wire.test.ts b/test/wire.test.ts index 89014df..9170ece 100644 --- a/test/wire.test.ts +++ b/test/wire.test.ts @@ -241,3 +241,62 @@ test("f090: syntaxHighlight:false leaves the read body plain", async () => { assert.ok(out.includes("const x = 1;"), "content is still shown, just unhighlighted"); }); }); + +// ── edit diff source (details.patch vs details.diff) ───────────────── + +test("edit result prefers details.patch over details.diff", async () => { + await withTmp(async (cwd) => { + mkdirSync(join(cwd, ".pi")); + writeFileSync(join(cwd, ".pi", "pretty.json"), JSON.stringify({ diffSplit: true })); + const env = makeEnv(cwd, () => BUILTIN_BASH); + await env.sessionStart(); + const edit = env.tools.get("edit")!.renderResult as ( + r: unknown, + o: unknown, + t: unknown, + c: unknown, + ) => unknown; + + const EDIT_RESULT = { + content: [{ type: "text", text: "Successfully replaced 1 block(s)." }], + details: { + diff: " 1 alpha\n-2 bravo\n+2 bravo-mod\n 3 charlie", + patch: + "===================================================================\n--- a.ts\n+++ a.ts\n@@ -1,3 +1,3 @@\n alpha\n-bravo\n+bravo-mod\n charlie", + }, + }; + + // Fake terminal columns so splitFits succeeds in headless test + const origCols = process.stdout.columns; + try { + Object.defineProperty(process.stdout, "columns", { value: 140, configurable: true }); + const out = textOf(edit(EDIT_RESULT, { expanded: true }, IDENTITY_THEME, { args: { path: "a.ts" } })); + assert.ok(out.includes("│"), "split view separator present when details.patch is consumed"); + } finally { + Object.defineProperty(process.stdout, "columns", { value: origCols, configurable: true }); + } + }); +}); + +test("edit result falls back to details.diff when details.patch is absent", async () => { + await withTmp(async (cwd) => { + const env = makeEnv(cwd, () => BUILTIN_BASH); + await env.sessionStart(); + const edit = env.tools.get("edit")!.renderResult as ( + r: unknown, + o: unknown, + t: unknown, + c: unknown, + ) => unknown; + + const EDIT_RESULT_NO_PATCH = { + content: [{ type: "text", text: "Successfully replaced 1 block(s)." }], + details: { + diff: " 1 alpha\n-2 bravo\n+2 bravo-mod\n 3 charlie", + }, + }; + + const out = textOf(edit(EDIT_RESULT_NO_PATCH, { expanded: true }, IDENTITY_THEME, { args: { path: "a.ts" } })); + assert.ok(out.includes("bravo-mod"), "falls back to details.diff content"); + }); +});