feat(patch): added compact hashline diff preview for patch responses

- Added buildCompactHashlineDiffPreview() function to generate collapsed diff previews for tool responses.
- Enhanced EditTool response to include diff summary with added/removed line counts and compact preview.
- Added project-level discovery for .agent/ and .agents/ directories walking up to repository root.
- Implemented helper functions for truncating long diff runs (collapseFromStart, collapseFromEnd, collapseFromMiddle).
This commit is contained in:
can1357
2026-03-04 22:39:39 +01:00
parent f59b1b9fbf
commit 631021aa67
4 changed files with 174 additions and 3 deletions
+113
View File
@@ -710,3 +710,116 @@ export function applyHashlineEdits(
}
}
}
export interface CompactHashlineDiffPreview {
preview: string;
addedLines: number;
removedLines: number;
}
export interface CompactHashlineDiffOptions {
maxUnchangedRun?: number;
maxAdditionRun?: number;
maxDeletionRun?: number;
maxOutputLines?: number;
}
const NUMBERED_DIFF_LINE_RE = /^([ +-])(\d+)\|(.*)$/;
type DiffRunKind = " " | "+" | "-" | "meta";
type DiffRun = { kind: DiffRunKind; lines: string[] };
function collapseFromStart(lines: string[], maxLines: number, label: string): string[] {
if (lines.length <= maxLines) return lines;
const hidden = lines.length - maxLines;
return [...lines.slice(0, maxLines), ` ... ${hidden} more ${label} lines`];
}
function collapseFromEnd(lines: string[], maxLines: number, label: string): string[] {
if (lines.length <= maxLines) return lines;
const hidden = lines.length - maxLines;
return [` ... ${hidden} more ${label} lines`, ...lines.slice(-maxLines)];
}
function collapseFromMiddle(lines: string[], maxLines: number, label: string): string[] {
if (lines.length <= maxLines * 2) return lines;
const hidden = lines.length - maxLines * 2;
return [...lines.slice(0, maxLines), ` ... ${hidden} more ${label} lines`, ...lines.slice(-maxLines)];
}
function splitDiffRuns(lines: string[]): DiffRun[] {
const runs: DiffRun[] = [];
for (const line of lines) {
const match = NUMBERED_DIFF_LINE_RE.exec(line);
const kind = (match?.[1] as " " | "+" | "-" | undefined) ?? "meta";
const prev = runs[runs.length - 1];
if (prev && prev.kind === kind) {
prev.lines.push(line);
continue;
}
runs.push({ kind, lines: [line] });
}
return runs;
}
/**
* Build a compact diff preview suitable for model-visible tool responses.
*
* Collapses long unchanged runs and long consecutive additions/removals so the
* model sees the shape of edits without replaying full file content.
*/
export function buildCompactHashlineDiffPreview(
diff: string,
options: CompactHashlineDiffOptions = {},
): CompactHashlineDiffPreview {
const maxUnchangedRun = options.maxUnchangedRun ?? 2;
const maxAdditionRun = options.maxAdditionRun ?? 2;
const maxDeletionRun = options.maxDeletionRun ?? 2;
const maxOutputLines = options.maxOutputLines ?? 16;
const inputLines = diff.length === 0 ? [] : diff.split("\n");
const runs = splitDiffRuns(inputLines);
const out: string[] = [];
let addedLines = 0;
let removedLines = 0;
for (let runIndex = 0; runIndex < runs.length; runIndex++) {
const run = runs[runIndex];
switch (run.kind) {
case "meta":
out.push(...run.lines);
break;
case "+":
addedLines += run.lines.length;
out.push(...collapseFromStart(run.lines, maxAdditionRun, "added"));
break;
case "-":
removedLines += run.lines.length;
out.push(...collapseFromStart(run.lines, maxDeletionRun, "removed"));
break;
case " ":
if (runIndex === 0) {
out.push(...collapseFromEnd(run.lines, maxUnchangedRun, "unchanged"));
break;
}
if (runIndex === runs.length - 1) {
out.push(...collapseFromStart(run.lines, maxUnchangedRun, "unchanged"));
break;
}
out.push(...collapseFromMiddle(run.lines, maxUnchangedRun, "unchanged"));
break;
}
}
if (out.length > maxOutputLines) {
const hidden = out.length - maxOutputLines;
return {
preview: [...out.slice(0, maxOutputLines), ` ... ${hidden} more preview lines`].join("\n"),
addedLines,
removedLines,
};
}
return { preview: out.join("\n"), addedLines, removedLines };
}
+13 -2
View File
@@ -34,7 +34,14 @@ import { enforcePlanModeWrite, resolvePlanPath } from "../tools/plan-mode-guard"
import { applyPatch } from "./applicator";
import { generateDiffString, generateUnifiedDiffString, replaceText } from "./diff";
import { findMatch } from "./fuzzy";
import { type Anchor, applyHashlineEdits, computeLineHash, type HashlineEdit, parseTag } from "./hashline";
import {
type Anchor,
applyHashlineEdits,
buildCompactHashlineDiffPreview,
computeLineHash,
type HashlineEdit,
parseTag,
} from "./hashline";
import { detectLineEnding, normalizeToLF, restoreLineEndings, stripBom } from "./normalize";
import { type EditToolDetails, getLspBatchRequest } from "./shared";
// Internal imports
@@ -597,11 +604,15 @@ export class EditTool implements AgentTool<TInput> {
.get();
const resultText = move ? `Moved ${path} to ${move}` : `Updated ${path}`;
const preview = buildCompactHashlineDiffPreview(diffResult.diff);
const summaryLine = `Changes: +${preview.addedLines} -${preview.removedLines}${preview.preview ? "" : " (no textual diff preview)"}`;
const warningsBlock = result.warnings?.length ? `\n\nWarnings:\n${result.warnings.join("\n")}` : "";
const previewBlock = preview.preview ? `\n\nDiff preview:\n${preview.preview}` : "";
return {
content: [
{
type: "text",
text: `${resultText}${result.warnings?.length ? `\n\nWarnings:\n${result.warnings.join("\n")}` : ""}`,
text: `${resultText}\n${summaryLine}${previewBlock}${warningsBlock}`,
},
],
details: {