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:
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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: {
|
||||
|
||||
Reference in New Issue
Block a user