pi-hashline-edit-pro 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +143 -0
- package/index.ts +64 -0
- package/package.json +52 -0
- package/prompts/edit-snippet.md +1 -0
- package/prompts/edit.md +58 -0
- package/prompts/read-guidelines.md +3 -0
- package/prompts/read-snippet.md +1 -0
- package/prompts/read.md +28 -0
- package/src/edit-diff.ts +234 -0
- package/src/edit-normalize.ts +68 -0
- package/src/edit-render.ts +280 -0
- package/src/edit-response.ts +531 -0
- package/src/edit.ts +689 -0
- package/src/file-kind.ts +161 -0
- package/src/fs-write.ts +105 -0
- package/src/hashline/apply.ts +660 -0
- package/src/hashline/hash.ts +192 -0
- package/src/hashline/index.ts +70 -0
- package/src/hashline/parse.ts +116 -0
- package/src/hashline/resolve.ts +552 -0
- package/src/path-utils.ts +13 -0
- package/src/read.ts +256 -0
- package/src/runtime.ts +3 -0
- package/src/snapshot.ts +29 -0
- package/src/utils.ts +11 -0
|
@@ -0,0 +1,531 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Edit response builders.
|
|
3
|
+
*
|
|
4
|
+
* Pulled out of `src/edit.ts` execute() so each returnMode branch
|
|
5
|
+
* (noop / full / ranges / changed) is independently testable and the
|
|
6
|
+
* top-level execute path stays narrative.
|
|
7
|
+
*
|
|
8
|
+
* No behaviour change: outputs are byte-identical to the previous inline
|
|
9
|
+
* implementation. The only additive surface is `details.metrics` (Phase 2 C
|
|
10
|
+
* — observability for hosts; the LLM-visible text is unchanged).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { generateDiffString } from "./edit-diff";
|
|
14
|
+
import {
|
|
15
|
+
computeAffectedLineRange,
|
|
16
|
+
computeLineHashes,
|
|
17
|
+
formatHashlineRegion,
|
|
18
|
+
} from "./hashline";
|
|
19
|
+
import { formatHashlineReadPreview } from "./read";
|
|
20
|
+
|
|
21
|
+
// Local shape — pi-coding-agent does not export a public `ToolResult`. The
|
|
22
|
+
// builders return `details` as `any` so callers can keep their own per-tool
|
|
23
|
+
// details type without re-asserting it here. This file intentionally does
|
|
24
|
+
// not import the agent's tool-result type to stay decoupled from internals.
|
|
25
|
+
type ToolResult = {
|
|
26
|
+
content: Array<{ type: "text"; text: string }>;
|
|
27
|
+
isError?: boolean;
|
|
28
|
+
details: any;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const CHANGED_ANCHOR_TEXT_BUDGET_BYTES = 50 * 1024;
|
|
32
|
+
|
|
33
|
+
// ─── Public types ───────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
export type ReturnMode = "changed" | "full" | "ranges";
|
|
36
|
+
|
|
37
|
+
export type ReturnRange = {
|
|
38
|
+
start: number;
|
|
39
|
+
end?: number;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type ReturnedRangePreview = {
|
|
43
|
+
start: number;
|
|
44
|
+
end: number;
|
|
45
|
+
text: string;
|
|
46
|
+
nextOffset?: number;
|
|
47
|
+
empty?: true;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export type FullContentPreview = {
|
|
51
|
+
text: string;
|
|
52
|
+
nextOffset?: number;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Host-visible, opt-in observability surface (Phase 2 C). The LLM never sees
|
|
57
|
+
* this — it lives in `details` only. Hosts can use it for dashboards,
|
|
58
|
+
* adoption metrics, or regression alarms (e.g. "noop rate spiking").
|
|
59
|
+
*
|
|
60
|
+
* snake_case is intentional: most observability backends prefer it and
|
|
61
|
+
* avoiding camelCase saves a transform on the host side.
|
|
62
|
+
*/
|
|
63
|
+
export type EditMetrics = {
|
|
64
|
+
edits_attempted: number;
|
|
65
|
+
edits_noop: number;
|
|
66
|
+
warnings: number;
|
|
67
|
+
return_mode: ReturnMode;
|
|
68
|
+
classification: "applied" | "noop";
|
|
69
|
+
changed_lines?: { first: number; last: number };
|
|
70
|
+
added_lines?: number;
|
|
71
|
+
removed_lines?: number;
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export type ReadMetrics = {
|
|
75
|
+
truncated: boolean;
|
|
76
|
+
next_offset?: number;
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
export type EditMeta = {
|
|
80
|
+
editsAttempted: number;
|
|
81
|
+
noopEditsCount: number;
|
|
82
|
+
firstChangedLine?: number;
|
|
83
|
+
lastChangedLine?: number;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
type NoopEditEntry = {
|
|
87
|
+
editIndex: number;
|
|
88
|
+
loc: string;
|
|
89
|
+
currentContent: string;
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// ─── Builder inputs ─────────────────────────────────────────────────────
|
|
93
|
+
|
|
94
|
+
export interface NoopResponseInput {
|
|
95
|
+
path: string;
|
|
96
|
+
returnMode: ReturnMode;
|
|
97
|
+
requestedReturnRanges: ReturnRange[] | undefined;
|
|
98
|
+
noopEdits: NoopEditEntry[] | undefined;
|
|
99
|
+
originalNormalized: string;
|
|
100
|
+
snapshotId: string;
|
|
101
|
+
editMeta: EditMeta;
|
|
102
|
+
warnings: string[] | undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface SuccessResponseInput {
|
|
106
|
+
path: string;
|
|
107
|
+
returnMode: ReturnMode;
|
|
108
|
+
requestedReturnRanges: ReturnRange[] | undefined;
|
|
109
|
+
originalNormalized: string;
|
|
110
|
+
result: string;
|
|
111
|
+
/** Precomputed hashes for `result`. When omitted the response builder
|
|
112
|
+
* computes them on demand; the caller should pass them in when the same
|
|
113
|
+
* result is rendered through more than one path (full / ranges / changed)
|
|
114
|
+
* to avoid redundant work. */
|
|
115
|
+
resultHashes?: string[];
|
|
116
|
+
warnings: string[] | undefined;
|
|
117
|
+
snapshotId: string;
|
|
118
|
+
editMeta: EditMeta;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ─── Helpers ────────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
function getVisibleLines(text: string): string[] {
|
|
124
|
+
if (text.length === 0) return [];
|
|
125
|
+
const lines = text.split("\n");
|
|
126
|
+
return text.endsWith("\n") ? lines.slice(0, -1) : lines;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function countDiffLines(diff: string, marker: "+" | "-"): number {
|
|
130
|
+
if (!diff) return 0;
|
|
131
|
+
let count = 0;
|
|
132
|
+
for (const line of diff.split("\n")) {
|
|
133
|
+
if (
|
|
134
|
+
line.startsWith(marker) &&
|
|
135
|
+
!line.startsWith(`${marker}${marker}${marker}`)
|
|
136
|
+
) {
|
|
137
|
+
count += 1;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return count;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function buildMetrics(args: {
|
|
144
|
+
classification: "applied" | "noop";
|
|
145
|
+
returnMode: ReturnMode;
|
|
146
|
+
editsAttempted: number;
|
|
147
|
+
noopEditsCount: number;
|
|
148
|
+
warningsCount: number;
|
|
149
|
+
firstChangedLine?: number;
|
|
150
|
+
lastChangedLine?: number;
|
|
151
|
+
addedLines?: number;
|
|
152
|
+
removedLines?: number;
|
|
153
|
+
}): EditMetrics {
|
|
154
|
+
const metrics: EditMetrics = {
|
|
155
|
+
edits_attempted: args.editsAttempted,
|
|
156
|
+
edits_noop: args.noopEditsCount,
|
|
157
|
+
warnings: args.warningsCount,
|
|
158
|
+
return_mode: args.returnMode,
|
|
159
|
+
classification: args.classification,
|
|
160
|
+
};
|
|
161
|
+
if (
|
|
162
|
+
args.classification === "applied" &&
|
|
163
|
+
args.firstChangedLine !== undefined &&
|
|
164
|
+
args.lastChangedLine !== undefined
|
|
165
|
+
) {
|
|
166
|
+
metrics.changed_lines = {
|
|
167
|
+
first: args.firstChangedLine,
|
|
168
|
+
last: args.lastChangedLine,
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
if (args.addedLines !== undefined) metrics.added_lines = args.addedLines;
|
|
172
|
+
if (args.removedLines !== undefined)
|
|
173
|
+
metrics.removed_lines = args.removedLines;
|
|
174
|
+
return metrics;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function warningsBlockOf(warnings: string[] | undefined): string {
|
|
178
|
+
return warnings?.length ? `\n\nWarnings:\n${warnings.join("\n")}` : "";
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function outlineBlockOf(outlineText: string): string {
|
|
182
|
+
return outlineText ? `\n\n${outlineText}` : "";
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// ─── Structure outline ──────────────────────────────────────────────────
|
|
186
|
+
|
|
187
|
+
const STRUCTURE_MARKER_RE =
|
|
188
|
+
/^(#{1,6}\s+.+|(export\s+)?(async\s+)?function\s+\w+|(export\s+)?class\s+\w+|(export\s+)?interface\s+\w+|(export\s+)?type\s+\w+|(export\s+)?enum\s+\w+|(const|let|var)\s+\w+\s*=\s*(async\s*)?\()/;
|
|
189
|
+
|
|
190
|
+
function truncateOutlineEntry(text: string, max = 88): string {
|
|
191
|
+
return text.length <= max ? text : `${text.slice(0, max - 1)}…`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function collectOutlineEntries(previewText: string): string[] {
|
|
195
|
+
const structural: string[] = [];
|
|
196
|
+
for (const line of previewText.split("\n")) {
|
|
197
|
+
const match = line.match(/^\s*([A-Za-z0-9_\-]{4}):(.*)$/);
|
|
198
|
+
if (!match) continue;
|
|
199
|
+
const content = match[2]!.trim();
|
|
200
|
+
if (content.length === 0) continue;
|
|
201
|
+
if (!STRUCTURE_MARKER_RE.test(content)) continue;
|
|
202
|
+
structural.push(truncateOutlineEntry(content.replace(/\s+/g, " ")));
|
|
203
|
+
}
|
|
204
|
+
return structural.slice(0, 8);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function buildStructureOutline(
|
|
208
|
+
sections: Array<{ label?: string; previewText: string }>,
|
|
209
|
+
): { text: string; outline: string[] } {
|
|
210
|
+
const outlineLines: string[] = [];
|
|
211
|
+
const detailOutline: string[] = [];
|
|
212
|
+
const useSectionLabels = sections.length > 1;
|
|
213
|
+
|
|
214
|
+
for (const section of sections) {
|
|
215
|
+
const entries = collectOutlineEntries(section.previewText);
|
|
216
|
+
if (entries.length === 0) continue;
|
|
217
|
+
if (useSectionLabels && section.label) {
|
|
218
|
+
outlineLines.push(`- ${section.label}`);
|
|
219
|
+
}
|
|
220
|
+
for (const entry of entries) {
|
|
221
|
+
outlineLines.push(useSectionLabels ? ` - ${entry}` : `- ${entry}`);
|
|
222
|
+
detailOutline.push(section.label ? `${section.label}: ${entry}` : entry);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (outlineLines.length === 0) {
|
|
227
|
+
return { text: "", outline: [] };
|
|
228
|
+
}
|
|
229
|
+
return {
|
|
230
|
+
text: ["Structure outline:", ...outlineLines].join("\n"),
|
|
231
|
+
outline: detailOutline,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ─── Range previews ─────────────────────────────────────────────────────
|
|
236
|
+
|
|
237
|
+
function formatRequestedRangePreviews(
|
|
238
|
+
text: string,
|
|
239
|
+
ranges: ReturnRange[],
|
|
240
|
+
precomputedHashes?: string[],
|
|
241
|
+
): { text: string; returnedRanges: ReturnedRangePreview[] } {
|
|
242
|
+
const totalLines = getVisibleLines(text).length;
|
|
243
|
+
const returnedRanges = ranges.map((range) => {
|
|
244
|
+
const requestedEnd = range.end ?? range.start;
|
|
245
|
+
const preview = formatHashlineReadPreview(
|
|
246
|
+
text,
|
|
247
|
+
{
|
|
248
|
+
offset: range.start,
|
|
249
|
+
limit: requestedEnd - range.start + 1,
|
|
250
|
+
},
|
|
251
|
+
precomputedHashes,
|
|
252
|
+
);
|
|
253
|
+
const hasReturnedLines = /^\s*[A-Za-z0-9_\-]{4}:/m.test(preview.text);
|
|
254
|
+
const actualEnd = hasReturnedLines
|
|
255
|
+
? preview.nextOffset !== undefined
|
|
256
|
+
? preview.nextOffset - 1
|
|
257
|
+
: Math.min(requestedEnd, totalLines)
|
|
258
|
+
: requestedEnd;
|
|
259
|
+
return {
|
|
260
|
+
start: range.start,
|
|
261
|
+
end: hasReturnedLines ? Math.max(range.start, actualEnd) : actualEnd,
|
|
262
|
+
text: preview.text,
|
|
263
|
+
...(preview.nextOffset !== undefined
|
|
264
|
+
? { nextOffset: preview.nextOffset }
|
|
265
|
+
: {}),
|
|
266
|
+
...(!hasReturnedLines ? { empty: true as const } : {}),
|
|
267
|
+
};
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
const formatted = returnedRanges
|
|
271
|
+
.map(
|
|
272
|
+
(range, index) =>
|
|
273
|
+
`--- Range ${index + 1} ---\n${range.text}`,
|
|
274
|
+
)
|
|
275
|
+
.join("\n\n");
|
|
276
|
+
|
|
277
|
+
return {
|
|
278
|
+
text: formatted,
|
|
279
|
+
returnedRanges,
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// ─── Builders ───────────────────────────────────────────────────────────
|
|
284
|
+
|
|
285
|
+
export function buildNoopResponse(input: NoopResponseInput): ToolResult {
|
|
286
|
+
const {
|
|
287
|
+
path,
|
|
288
|
+
returnMode,
|
|
289
|
+
requestedReturnRanges,
|
|
290
|
+
noopEdits,
|
|
291
|
+
originalNormalized,
|
|
292
|
+
snapshotId,
|
|
293
|
+
editMeta,
|
|
294
|
+
warnings,
|
|
295
|
+
} = input;
|
|
296
|
+
|
|
297
|
+
const noopDetailsText = noopEdits?.length
|
|
298
|
+
? noopEdits
|
|
299
|
+
.map(
|
|
300
|
+
(edit) =>
|
|
301
|
+
`Edit ${edit.editIndex}: replacement for ${edit.loc} is identical to current content:\n ${edit.loc}: ${edit.currentContent}`,
|
|
302
|
+
)
|
|
303
|
+
.join("\n")
|
|
304
|
+
: "The edits produced identical content.";
|
|
305
|
+
|
|
306
|
+
const fullPreview =
|
|
307
|
+
returnMode === "full"
|
|
308
|
+
? formatHashlineReadPreview(originalNormalized, { offset: 1 })
|
|
309
|
+
: undefined;
|
|
310
|
+
const rangePreviews =
|
|
311
|
+
returnMode === "ranges"
|
|
312
|
+
? formatRequestedRangePreviews(
|
|
313
|
+
originalNormalized,
|
|
314
|
+
requestedReturnRanges!,
|
|
315
|
+
)
|
|
316
|
+
: undefined;
|
|
317
|
+
const outline =
|
|
318
|
+
returnMode === "full"
|
|
319
|
+
? buildStructureOutline([{ previewText: fullPreview!.text }])
|
|
320
|
+
: returnMode === "ranges"
|
|
321
|
+
? buildStructureOutline(
|
|
322
|
+
rangePreviews!.returnedRanges.map((range, index) => ({
|
|
323
|
+
label: `Range ${index + 1} (lines ${range.start}-${range.end})`,
|
|
324
|
+
previewText: range.text,
|
|
325
|
+
})),
|
|
326
|
+
)
|
|
327
|
+
: undefined;
|
|
328
|
+
|
|
329
|
+
const text =
|
|
330
|
+
returnMode === "full"
|
|
331
|
+
? `No changes made to ${path}\nClassification: noop${outlineBlockOf(outline!.text)}\n\nFull content is available in details.fullContent.`
|
|
332
|
+
: returnMode === "ranges"
|
|
333
|
+
? `No changes made to ${path}\nClassification: noop${outlineBlockOf(outline!.text)}\n\nRequested range payloads are available in details.returnedRanges.`
|
|
334
|
+
: `No changes made to ${path}\nClassification: noop\n${noopDetailsText}`;
|
|
335
|
+
|
|
336
|
+
const metrics = buildMetrics({
|
|
337
|
+
classification: "noop",
|
|
338
|
+
returnMode,
|
|
339
|
+
editsAttempted: editMeta.editsAttempted,
|
|
340
|
+
noopEditsCount: editMeta.noopEditsCount,
|
|
341
|
+
warningsCount: warnings?.length ?? 0,
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
return {
|
|
345
|
+
content: [{ type: "text", text }],
|
|
346
|
+
details: {
|
|
347
|
+
diff: "",
|
|
348
|
+
firstChangedLine: undefined,
|
|
349
|
+
snapshotId,
|
|
350
|
+
classification: "noop" as const,
|
|
351
|
+
...(fullPreview?.nextOffset !== undefined
|
|
352
|
+
? { nextOffset: fullPreview.nextOffset }
|
|
353
|
+
: {}),
|
|
354
|
+
...(fullPreview
|
|
355
|
+
? {
|
|
356
|
+
fullContent: {
|
|
357
|
+
text: fullPreview.text,
|
|
358
|
+
...(fullPreview.nextOffset !== undefined
|
|
359
|
+
? { nextOffset: fullPreview.nextOffset }
|
|
360
|
+
: {}),
|
|
361
|
+
},
|
|
362
|
+
}
|
|
363
|
+
: {}),
|
|
364
|
+
...(rangePreviews
|
|
365
|
+
? { returnedRanges: rangePreviews.returnedRanges }
|
|
366
|
+
: {}),
|
|
367
|
+
...(outline ? { structureOutline: outline.outline } : {}),
|
|
368
|
+
metrics,
|
|
369
|
+
},
|
|
370
|
+
};
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
export function buildFullResponse(input: SuccessResponseInput): ToolResult {
|
|
374
|
+
const { path, result, warnings, snapshotId, originalNormalized, editMeta } =
|
|
375
|
+
input;
|
|
376
|
+
|
|
377
|
+
const diffResult = generateDiffString(originalNormalized, result);
|
|
378
|
+
const resultHashes = input.resultHashes ?? computeLineHashes(result);
|
|
379
|
+
const fullPreview = formatHashlineReadPreview(
|
|
380
|
+
result,
|
|
381
|
+
{ offset: 1 },
|
|
382
|
+
resultHashes,
|
|
383
|
+
);
|
|
384
|
+
const outline = buildStructureOutline([{ previewText: fullPreview.text }]);
|
|
385
|
+
const text = `Updated ${path}${warningsBlockOf(warnings)}${outlineBlockOf(outline.text)}\n\nFull content is available in details.fullContent.`;
|
|
386
|
+
|
|
387
|
+
const metrics = buildMetrics({
|
|
388
|
+
classification: "applied",
|
|
389
|
+
returnMode: "full",
|
|
390
|
+
editsAttempted: editMeta.editsAttempted,
|
|
391
|
+
noopEditsCount: editMeta.noopEditsCount,
|
|
392
|
+
warningsCount: warnings?.length ?? 0,
|
|
393
|
+
firstChangedLine: editMeta.firstChangedLine,
|
|
394
|
+
lastChangedLine: editMeta.lastChangedLine,
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
return {
|
|
398
|
+
content: [{ type: "text", text }],
|
|
399
|
+
details: {
|
|
400
|
+
diff: diffResult.diff,
|
|
401
|
+
firstChangedLine:
|
|
402
|
+
editMeta.firstChangedLine ?? diffResult.firstChangedLine,
|
|
403
|
+
snapshotId,
|
|
404
|
+
...(fullPreview.nextOffset !== undefined
|
|
405
|
+
? { nextOffset: fullPreview.nextOffset }
|
|
406
|
+
: {}),
|
|
407
|
+
fullContent: {
|
|
408
|
+
text: fullPreview.text,
|
|
409
|
+
...(fullPreview.nextOffset !== undefined
|
|
410
|
+
? { nextOffset: fullPreview.nextOffset }
|
|
411
|
+
: {}),
|
|
412
|
+
},
|
|
413
|
+
structureOutline: outline.outline,
|
|
414
|
+
metrics,
|
|
415
|
+
},
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
export function buildRangesResponse(input: SuccessResponseInput): ToolResult {
|
|
420
|
+
const {
|
|
421
|
+
path,
|
|
422
|
+
result,
|
|
423
|
+
warnings,
|
|
424
|
+
snapshotId,
|
|
425
|
+
originalNormalized,
|
|
426
|
+
requestedReturnRanges,
|
|
427
|
+
editMeta,
|
|
428
|
+
} = input;
|
|
429
|
+
|
|
430
|
+
const diffResult = generateDiffString(originalNormalized, result);
|
|
431
|
+
const resultHashes = input.resultHashes ?? computeLineHashes(result);
|
|
432
|
+
const rangePreviews = formatRequestedRangePreviews(
|
|
433
|
+
result,
|
|
434
|
+
requestedReturnRanges!,
|
|
435
|
+
resultHashes,
|
|
436
|
+
);
|
|
437
|
+
const outline = buildStructureOutline(
|
|
438
|
+
rangePreviews.returnedRanges.map((range, index) => ({
|
|
439
|
+
label: `Range ${index + 1} (lines ${range.start}-${range.end})`,
|
|
440
|
+
previewText: range.text,
|
|
441
|
+
})),
|
|
442
|
+
);
|
|
443
|
+
const text = `Updated ${path}${warningsBlockOf(warnings)}${outlineBlockOf(outline.text)}\n\nRequested range payloads are available in details.returnedRanges.`;
|
|
444
|
+
|
|
445
|
+
const metrics = buildMetrics({
|
|
446
|
+
classification: "applied",
|
|
447
|
+
returnMode: "ranges",
|
|
448
|
+
editsAttempted: editMeta.editsAttempted,
|
|
449
|
+
noopEditsCount: editMeta.noopEditsCount,
|
|
450
|
+
warningsCount: warnings?.length ?? 0,
|
|
451
|
+
firstChangedLine: editMeta.firstChangedLine,
|
|
452
|
+
lastChangedLine: editMeta.lastChangedLine,
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
return {
|
|
456
|
+
content: [{ type: "text", text }],
|
|
457
|
+
details: {
|
|
458
|
+
diff: diffResult.diff,
|
|
459
|
+
firstChangedLine:
|
|
460
|
+
editMeta.firstChangedLine ?? diffResult.firstChangedLine,
|
|
461
|
+
snapshotId,
|
|
462
|
+
returnedRanges: rangePreviews.returnedRanges,
|
|
463
|
+
structureOutline: outline.outline,
|
|
464
|
+
metrics,
|
|
465
|
+
},
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
export function buildChangedResponse(input: SuccessResponseInput): ToolResult {
|
|
470
|
+
const { result, warnings, snapshotId, originalNormalized, editMeta } = input;
|
|
471
|
+
|
|
472
|
+
const diffResult = generateDiffString(originalNormalized, result);
|
|
473
|
+
const addedLines = countDiffLines(diffResult.diff, "+");
|
|
474
|
+
const removedLines = countDiffLines(diffResult.diff, "-");
|
|
475
|
+
const warningsBlock = warningsBlockOf(warnings);
|
|
476
|
+
|
|
477
|
+
const resultLines = getVisibleLines(result);
|
|
478
|
+
const resultHashes = input.resultHashes ?? computeLineHashes(result);
|
|
479
|
+
const anchorRange = computeAffectedLineRange({
|
|
480
|
+
firstChangedLine: editMeta.firstChangedLine,
|
|
481
|
+
lastChangedLine: editMeta.lastChangedLine,
|
|
482
|
+
resultLineCount: resultLines.length,
|
|
483
|
+
});
|
|
484
|
+
const anchorsBlock = anchorRange
|
|
485
|
+
? (() => {
|
|
486
|
+
const region = resultLines.slice(
|
|
487
|
+
anchorRange.start - 1,
|
|
488
|
+
anchorRange.end,
|
|
489
|
+
);
|
|
490
|
+
const regionHashes = resultHashes.slice(
|
|
491
|
+
anchorRange.start - 1,
|
|
492
|
+
anchorRange.end,
|
|
493
|
+
);
|
|
494
|
+
const formatted = formatHashlineRegion(regionHashes, region);
|
|
495
|
+
const block = `--- Anchors ---\n${formatted}`;
|
|
496
|
+
return Buffer.byteLength(block, "utf8") <=
|
|
497
|
+
CHANGED_ANCHOR_TEXT_BUDGET_BYTES
|
|
498
|
+
? block
|
|
499
|
+
: "Anchors omitted; use read for subsequent edits.";
|
|
500
|
+
})()
|
|
501
|
+
: resultLines.length === 0
|
|
502
|
+
? "File is empty. Use edit with prepend or append and omit pos to insert content."
|
|
503
|
+
: "Anchors omitted; use read for subsequent edits.";
|
|
504
|
+
|
|
505
|
+
const text = [anchorsBlock, warningsBlock.trimStart()]
|
|
506
|
+
.filter((section) => section.length > 0)
|
|
507
|
+
.join("\n\n");
|
|
508
|
+
|
|
509
|
+
const metrics = buildMetrics({
|
|
510
|
+
classification: "applied",
|
|
511
|
+
returnMode: "changed",
|
|
512
|
+
editsAttempted: editMeta.editsAttempted,
|
|
513
|
+
noopEditsCount: editMeta.noopEditsCount,
|
|
514
|
+
warningsCount: warnings?.length ?? 0,
|
|
515
|
+
firstChangedLine: editMeta.firstChangedLine,
|
|
516
|
+
lastChangedLine: editMeta.lastChangedLine,
|
|
517
|
+
addedLines,
|
|
518
|
+
removedLines,
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
return {
|
|
522
|
+
content: [{ type: "text", text }],
|
|
523
|
+
details: {
|
|
524
|
+
diff: diffResult.diff,
|
|
525
|
+
firstChangedLine:
|
|
526
|
+
editMeta.firstChangedLine ?? diffResult.firstChangedLine,
|
|
527
|
+
snapshotId,
|
|
528
|
+
metrics,
|
|
529
|
+
},
|
|
530
|
+
};
|
|
531
|
+
}
|