perf(coding-agent): bound completed diff rendering

This commit is contained in:
Aleksandr Khaustov
2026-08-15 20:39:36 +04:00
parent ffd53ff92a
commit 006c5371ea
5 changed files with 171 additions and 25 deletions
+4
View File
@@ -2,6 +2,10 @@
## [Unreleased]
### Changed
- Kept completed edit previews within their collapsed line limit for oversized single hunks and cached header change statistics across terminal repaints.
## [17.3.4] - 2026-08-14
### Changed
+6 -1
View File
@@ -864,6 +864,7 @@ function renderSingleFileResult(
let diffSectionRenderDiffFn: ((t: string, o?: { filePath?: string }) => string) | undefined;
const diffSectionCache = createRenderedStringCache();
const statsSuffixCache = createRenderedStringCache();
return framedBlock(uiTheme, width => {
const { expanded, renderContext } = options;
@@ -887,7 +888,11 @@ function renderSingleFileResult(
// Change stats ride inline on the header bar next to the path.
const previewDiff = editDiffPreview && !("error" in editDiffPreview) ? editDiffPreview.diff : undefined;
const headerDiff = isError ? undefined : details?.diff || previewDiff;
const statsSuffix = headerDiff ? formatDiffStatsSuffix(headerDiff, uiTheme) : "";
const statsSuffix = headerDiff
? cachedRenderedString(statsSuffixCache, uiTheme, false, "", headerDiff, () =>
formatDiffStatsSuffix(headerDiff, uiTheme),
)
: "";
const header = renderEditHeader(width, uiTheme, {
icon: isError ? "error" : "success",
iconOverride: !isError && !options.isPartial ? uiTheme.styledSymbol("tool.edit", "accent") : undefined,
+55 -23
View File
@@ -599,79 +599,111 @@ export function truncateDiffByHunk(
let keptHunks = 0;
for (const seg of segments) {
if (seg.isChange) {
keptHunks++;
if (keptHunks > maxHunks) break;
}
kept.push(...seg.lines);
if (kept.length >= maxLines) break;
if (seg.isChange) {
if (keptHunks >= maxHunks) break;
keptHunks++;
}
const take = Math.min(seg.lines.length, maxLines - kept.length);
for (let i = 0; i < take; i++) {
kept.push(seg.lines[i]!);
}
}
const keptStats = getDiffStats(kept.join("\n"));
return {
text: kept.join("\n"),
hiddenHunks: Math.max(0, totalStats.hunks - keptStats.hunks),
hiddenHunks: Math.max(0, totalStats.hunks - keptHunks),
hiddenLines: Math.max(0, lines.length - kept.length),
};
}
const contextBudget = maxLines - changeLineCount;
const contextSegments = segments.filter(s => !s.isChange && !s.isEllipsis);
const contextSegments = segments.filter(s => !s.isChange);
const totalContextLines = contextSegments.reduce((sum, s) => sum + s.lines.length, 0);
const kept: string[] = [];
let keptHunks = 0;
let keptSourceLines = 0;
if (totalContextLines <= contextBudget) {
for (const seg of segments) {
if (seg.isChange) {
if (keptHunks >= maxHunks) break;
keptHunks++;
if (keptHunks > maxHunks) break;
}
kept.push(...seg.lines);
keptSourceLines += seg.lines.length;
}
} else {
const contextRatio = contextSegments.length > 0 ? contextBudget / totalContextLines : 0;
const contextRatio = totalContextLines > 0 ? contextBudget / totalContextLines : 0;
let remainingContextBudget = contextBudget;
for (let i = 0; i < segments.length; i++) {
const seg = segments[i];
if (seg.isChange) {
if (keptHunks >= maxHunks) break;
keptHunks++;
if (keptHunks > maxHunks) break;
kept.push(...seg.lines);
} else if (seg.isEllipsis) {
kept.push(...seg.lines);
keptSourceLines += seg.lines.length;
continue;
}
if (remainingContextBudget <= 0) continue;
const allowedLines = Math.min(
remainingContextBudget,
Math.max(1, Math.floor(seg.lines.length * contextRatio)),
);
const outputStart = kept.length;
let sourceLinesAdded = 0;
if (seg.isEllipsis || seg.lines.length <= allowedLines) {
for (let j = 0; j < allowedLines; j++) {
kept.push(seg.lines[j]!);
}
sourceLinesAdded = allowedLines;
} else {
const allowedLines = Math.max(1, Math.floor(seg.lines.length * contextRatio));
const isBeforeChange = segments[i + 1]?.isChange;
const isAfterChange = segments[i - 1]?.isChange;
if (isBeforeChange && isAfterChange) {
const half = Math.ceil(allowedLines / 2);
if (seg.lines.length > allowedLines) {
kept.push(...seg.lines.slice(0, half));
if (allowedLines >= 3) {
const sourceBudget = allowedLines - 1;
const firstCount = Math.ceil(sourceBudget / 2);
const lastCount = sourceBudget - firstCount;
kept.push(...seg.lines.slice(0, firstCount));
kept.push("");
kept.push(...seg.lines.slice(-half));
if (lastCount > 0) kept.push(...seg.lines.slice(-lastCount));
sourceLinesAdded = sourceBudget;
} else {
kept.push(...seg.lines);
const firstCount = Math.ceil(allowedLines / 2);
const lastCount = allowedLines - firstCount;
kept.push(...seg.lines.slice(0, firstCount));
if (lastCount > 0) kept.push(...seg.lines.slice(-lastCount));
sourceLinesAdded = allowedLines;
}
} else if (isBeforeChange) {
kept.push(...seg.lines.slice(-allowedLines));
sourceLinesAdded = allowedLines;
} else if (isAfterChange) {
kept.push(...seg.lines.slice(0, allowedLines));
sourceLinesAdded = allowedLines;
} else {
kept.push(...seg.lines.slice(0, Math.min(allowedLines, 2)));
const take = Math.min(allowedLines, 2);
kept.push(...seg.lines.slice(0, take));
sourceLinesAdded = take;
}
}
keptSourceLines += sourceLinesAdded;
remainingContextBudget -= kept.length - outputStart;
}
}
const keptStats = getDiffStats(kept.join("\n"));
return {
text: kept.join("\n"),
hiddenHunks: Math.max(0, totalStats.hunks - keptStats.hunks),
hiddenLines: Math.max(0, lines.length - kept.length),
hiddenHunks: Math.max(0, totalStats.hunks - keptHunks),
hiddenLines: Math.max(0, lines.length - keptSourceLines),
};
}
@@ -255,6 +255,19 @@ describe("editToolRenderer", () => {
it("caches completed diff rendering across stable frame renders", async () => {
const uiTheme = await getUiTheme();
let renderDiffCalls = 0;
let statsColorCalls = 0;
const countingTheme = new Proxy(uiTheme, {
get(target, property) {
if (property === "fg") {
return (color: Parameters<themeModule.Theme["fg"]>[0], text: string): string => {
if (color === "toolDiffAdded" && text === "+1") statsColorCalls++;
return target.fg(color, text);
};
}
const value = Reflect.get(target, property, target) as unknown;
return typeof value === "function" ? value.bind(target) : value;
},
});
const options = {
expanded: false,
isPartial: false,
@@ -275,17 +288,19 @@ describe("editToolRenderer", () => {
},
},
options,
uiTheme,
countingTheme,
{ file_path: "src/example.ts" },
);
component.render(160);
component.render(120);
expect(renderDiffCalls).toBe(1);
expect(statsColorCalls).toBe(1);
options.expanded = true;
component.render(120);
expect(renderDiffCalls).toBe(2);
expect(statsColorCalls).toBe(1);
});
it("computes the hashline preview diff once a single-line edit finishes streaming", async () => {
@@ -420,6 +435,27 @@ describe("editToolRenderer", () => {
expect(lines.filter(line => line.includes("+2/-1"))).toHaveLength(1);
});
it("bounds a completed diff that contains one oversized change hunk", async () => {
const uiTheme = await getUiTheme();
const diff = Array.from({ length: 1_000 }, (_, i) => `+${i + 1}│line ${i}`).join("\n");
const component = editToolRenderer.renderResult(
{
content: [{ type: "text", text: "Updated demo.ts" }],
details: { diff, op: "update" },
},
{ expanded: false, isPartial: false, renderContext: { editMode: "hashline" } },
uiTheme,
{ file_path: "demo.ts" },
);
const lines = component.render(160).map(line => Bun.stripANSI(line));
const rendered = lines.join("\n");
expect(lines.filter(line => line.includes("│line "))).toHaveLength(40);
expect(rendered).toContain("+40│line 39");
expect(rendered).not.toContain("+41│line 40");
expect(rendered).toContain("960 more lines");
});
it("renders completed edit gutters without inherited frame padding", async () => {
const uiTheme = await getUiTheme();
const component = editToolRenderer.renderResult(
@@ -286,6 +286,75 @@ describe("truncateDiffByHunk", () => {
expect(idxOld).toBeLessThan(idxNew);
expect(idxNew).toBeLessThan(idxTrailing);
});
it("caps one oversized change hunk at the line budget", () => {
const diff = makeHunk("+", 0, 1_000).join("\n");
const head = truncateDiffByHunk(diff, 4, 32);
const tail = truncateDiffByHunk(diff, 4, 32, { fromTail: true });
expect(head.text.split("\n")).toHaveLength(32);
expect(head.text).toStartWith("+ new 0\n");
expect(head.text).toEndWith("+ new 31");
expect(head.hiddenLines).toBe(968);
expect(head.hiddenHunks).toBe(0);
expect(tail.text.split("\n")).toHaveLength(32);
expect(tail.text).toStartWith("+ new 968\n");
expect(tail.text).toEndWith("+ new 999");
expect(tail.hiddenLines).toBe(968);
expect(tail.hiddenHunks).toBe(0);
});
it("keeps an exact-size change hunk unchanged", () => {
const diff = makeHunk("-", 0, 32).join("\n");
expect(truncateDiffByHunk(diff, 4, 32)).toEqual({
text: diff,
hiddenHunks: 0,
hiddenLines: 0,
});
});
it("drops surrounding context when changes exactly fill the line budget", () => {
const diff = [" leading context", ...makeHunk("+", 0, 32), " trailing context"].join("\n");
for (const result of [truncateDiffByHunk(diff, 4, 32), truncateDiffByHunk(diff, 4, 32, { fromTail: true })]) {
expect(result.text.split("\n")).toHaveLength(32);
expect(result.text).not.toContain("context");
expect(result.hiddenLines).toBe(2);
expect(result.hiddenHunks).toBe(0);
}
});
it("does not exceed the line budget when context rounding spans multiple hunks", () => {
const diff = [
" leading context",
...makeHunk("+", 0, 15),
" middle context a",
" middle context b",
...makeHunk("-", 100, 15),
" trailing context",
].join("\n");
for (const result of [truncateDiffByHunk(diff, 4, 32), truncateDiffByHunk(diff, 4, 32, { fromTail: true })]) {
expect(result.text.split("\n")).toHaveLength(32);
expect(result.hiddenLines).toBe(2);
expect(result.hiddenHunks).toBe(0);
}
});
it("does not count a removed separator as a hidden hunk", () => {
const diff = [...makeHunk("+", 0, 16), " hunk separator", ...makeHunk("-", 100, 16)].join("\n");
for (const result of [truncateDiffByHunk(diff, 4, 32), truncateDiffByHunk(diff, 4, 32, { fromTail: true })]) {
expect(result.text.split("\n")).toHaveLength(32);
expect(result.hiddenLines).toBe(1);
expect(result.hiddenHunks).toBe(0);
}
});
it("reports every hunk excluded by the hunk limit", () => {
const diff = buildDiff(6, 1);
for (const result of [truncateDiffByHunk(diff, 2, 100), truncateDiffByHunk(diff, 2, 100, { fromTail: true })]) {
expect(result.hiddenHunks).toBe(4);
}
});
});
describe("formatErrorMessage (F4 sanitization)", () => {