pi-hashline-edit-pro 0.5.1 → 0.6.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/README.md +15 -15
- package/index.ts +9 -3
- package/package.json +2 -2
- package/prompts/read-guidelines.md +2 -3
- package/prompts/read-snippet.md +1 -1
- package/prompts/read.md +7 -21
- package/prompts/replace-guidelines.md +3 -0
- package/prompts/replace-snippet.md +1 -0
- package/prompts/replace.md +69 -0
- package/src/hashline/apply.ts +59 -223
- package/src/hashline/resolve.ts +75 -192
- package/src/read.ts +5 -6
- package/src/{edit-normalize.ts → replace-normalize.ts} +1 -1
- package/src/{edit-render.ts → replace-render.ts} +16 -16
- package/src/{edit-response.ts → replace-response.ts} +8 -8
- package/src/{edit.ts → replace.ts} +36 -49
- package/prompts/edit-guidelines.md +0 -3
- package/prompts/edit-snippet.md +0 -1
- package/prompts/edit.md +0 -59
- /package/src/{edit-diff.ts → replace-diff.ts} +0 -0
package/src/hashline/resolve.ts
CHANGED
|
@@ -10,21 +10,12 @@ export type ResolvedAnchor = {
|
|
|
10
10
|
hashMatched: boolean;
|
|
11
11
|
};
|
|
12
12
|
|
|
13
|
-
export type HashlineEdit =
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
| {
|
|
20
|
-
op: "replace";
|
|
21
|
-
start: ResolvedAnchor;
|
|
22
|
-
end: ResolvedAnchor;
|
|
23
|
-
lines: string[];
|
|
24
|
-
}
|
|
25
|
-
| { op: "append"; pos?: ResolvedAnchor; lines: string[] }
|
|
26
|
-
| { op: "prepend"; pos?: ResolvedAnchor; lines: string[] };
|
|
27
|
-
|
|
13
|
+
export type HashlineEdit = { start: Anchor; end: Anchor; lines: string[] };
|
|
14
|
+
export type ResolvedHashlineEdit = {
|
|
15
|
+
start: ResolvedAnchor;
|
|
16
|
+
end: ResolvedAnchor;
|
|
17
|
+
lines: string[];
|
|
18
|
+
};
|
|
28
19
|
interface HashMismatch {
|
|
29
20
|
ref: Anchor;
|
|
30
21
|
kind: "not_found" | "ambiguous";
|
|
@@ -38,8 +29,6 @@ export interface NoopEdit {
|
|
|
38
29
|
}
|
|
39
30
|
|
|
40
31
|
export type HashlineToolEdit = {
|
|
41
|
-
op: string;
|
|
42
|
-
pos?: string;
|
|
43
32
|
start?: string;
|
|
44
33
|
end?: string;
|
|
45
34
|
lines?: string[];
|
|
@@ -130,7 +119,7 @@ export function formatMismatchError(
|
|
|
130
119
|
}
|
|
131
120
|
|
|
132
121
|
|
|
133
|
-
const ITEM_KEYS = new Set(["
|
|
122
|
+
const ITEM_KEYS = new Set(["start", "end", "lines"]);
|
|
134
123
|
function isStringArray(value: unknown): value is string[] {
|
|
135
124
|
return (
|
|
136
125
|
Array.isArray(value) && value.every((item) => typeof item === "string")
|
|
@@ -145,23 +134,6 @@ function assertEditItem(edit: Record<string, unknown>, index: number): void {
|
|
|
145
134
|
);
|
|
146
135
|
}
|
|
147
136
|
|
|
148
|
-
if (typeof edit.op !== "string") {
|
|
149
|
-
throw new Error(`[E_BAD_SHAPE] Edit ${index} requires an "op" string.`);
|
|
150
|
-
}
|
|
151
|
-
if (
|
|
152
|
-
edit.op !== "replace" &&
|
|
153
|
-
edit.op !== "append" &&
|
|
154
|
-
edit.op !== "prepend"
|
|
155
|
-
) {
|
|
156
|
-
throw new Error(
|
|
157
|
-
`[E_BAD_OP] Edit ${index} uses unknown op "${edit.op}". Expected "replace", "append", or "prepend".`,
|
|
158
|
-
);
|
|
159
|
-
}
|
|
160
|
-
if ("pos" in edit && typeof edit.pos !== "string" && edit.op !== "replace") {
|
|
161
|
-
throw new Error(
|
|
162
|
-
`[E_BAD_SHAPE] Edit ${index} field "pos" must be a string when provided.`,
|
|
163
|
-
);
|
|
164
|
-
}
|
|
165
137
|
if ("start" in edit && typeof edit.start !== "string") {
|
|
166
138
|
throw new Error(
|
|
167
139
|
`[E_BAD_SHAPE] Edit ${index} field "start" must be a string when provided.`,
|
|
@@ -176,29 +148,17 @@ function assertEditItem(edit: Record<string, unknown>, index: number): void {
|
|
|
176
148
|
if ("lines" in edit && !isStringArray(edit.lines)) {
|
|
177
149
|
throw new Error(`[E_BAD_SHAPE] Edit ${index} field "lines" must be a string array.`);
|
|
178
150
|
}
|
|
179
|
-
if (edit.
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
);
|
|
184
|
-
}
|
|
185
|
-
if (typeof edit.start !== "string") {
|
|
186
|
-
throw new Error(
|
|
187
|
-
`[E_BAD_OP] Edit ${index} with op "replace" requires a "start" anchor string.`,
|
|
188
|
-
);
|
|
189
|
-
}
|
|
190
|
-
if (typeof edit.end !== "string") {
|
|
191
|
-
throw new Error(
|
|
192
|
-
`[E_BAD_OP] Edit ${index} with op "replace" requires an "end" anchor string.`,
|
|
193
|
-
);
|
|
194
|
-
}
|
|
151
|
+
if (typeof edit.start !== "string") {
|
|
152
|
+
throw new Error(
|
|
153
|
+
`[E_BAD_OP] Edit ${index} requires a "start" anchor string.`,
|
|
154
|
+
);
|
|
195
155
|
}
|
|
196
|
-
|
|
197
|
-
if ((edit.op === "append" || edit.op === "prepend") && "end" in edit) {
|
|
156
|
+
if (typeof edit.end !== "string") {
|
|
198
157
|
throw new Error(
|
|
199
|
-
`[E_BAD_OP] Edit ${index}
|
|
158
|
+
`[E_BAD_OP] Edit ${index} requires an "end" anchor string.`,
|
|
200
159
|
);
|
|
201
160
|
}
|
|
161
|
+
|
|
202
162
|
}
|
|
203
163
|
|
|
204
164
|
export function resolveEditAnchors(edits: HashlineToolEdit[]): HashlineEdit[] {
|
|
@@ -206,45 +166,21 @@ export function resolveEditAnchors(edits: HashlineToolEdit[]): HashlineEdit[] {
|
|
|
206
166
|
for (const [index, edit] of edits.entries()) {
|
|
207
167
|
assertEditItem(edit as Record<string, unknown>, index);
|
|
208
168
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
start: parseHashRef(edit.start!),
|
|
221
|
-
end: parseHashRef(edit.end!),
|
|
222
|
-
lines: normalizedLines,
|
|
223
|
-
});
|
|
224
|
-
break;
|
|
225
|
-
}
|
|
226
|
-
case "append": {
|
|
227
|
-
result.push({
|
|
228
|
-
op: "append",
|
|
229
|
-
...(edit.pos ? { pos: parseHashRef(edit.pos) } : {}),
|
|
230
|
-
lines: hashlineParseText(edit.lines ?? null),
|
|
231
|
-
});
|
|
232
|
-
break;
|
|
233
|
-
}
|
|
234
|
-
case "prepend": {
|
|
235
|
-
result.push({
|
|
236
|
-
op: "prepend",
|
|
237
|
-
...(edit.pos ? { pos: parseHashRef(edit.pos) } : {}),
|
|
238
|
-
lines: hashlineParseText(edit.lines ?? null),
|
|
239
|
-
});
|
|
240
|
-
break;
|
|
241
|
-
}
|
|
242
|
-
}
|
|
169
|
+
// Normalize lines: [""] to lines: [] for deletion.
|
|
170
|
+
const replaceLines = hashlineParseText(edit.lines ?? null);
|
|
171
|
+
const normalizedLines =
|
|
172
|
+
replaceLines.length === 1 && replaceLines[0] === ""
|
|
173
|
+
? []
|
|
174
|
+
: replaceLines;
|
|
175
|
+
result.push({
|
|
176
|
+
start: parseHashRef(edit.start!),
|
|
177
|
+
end: parseHashRef(edit.end!),
|
|
178
|
+
lines: normalizedLines,
|
|
179
|
+
});
|
|
243
180
|
}
|
|
244
181
|
return result;
|
|
245
182
|
}
|
|
246
183
|
|
|
247
|
-
|
|
248
184
|
function maybeWarnSuspiciousUnicodeEscapePlaceholder(
|
|
249
185
|
edits: HashlineEdit[],
|
|
250
186
|
warnings: string[],
|
|
@@ -308,14 +244,7 @@ export function assertNoBareHashPrefixLines(
|
|
|
308
244
|
* Human-readable label for a resolved edit (used in warnings and conflict errors).
|
|
309
245
|
*/
|
|
310
246
|
export function describeEdit(edit: ResolvedHashlineEdit): string {
|
|
311
|
-
|
|
312
|
-
case "replace":
|
|
313
|
-
return `replace ${edit.start.hash}-${edit.end.hash}`;
|
|
314
|
-
case "append":
|
|
315
|
-
return edit.pos ? `append after ${edit.pos.hash}` : "append at EOF";
|
|
316
|
-
case "prepend":
|
|
317
|
-
return edit.pos ? `prepend before ${edit.pos.hash}` : "prepend at BOF";
|
|
318
|
-
}
|
|
247
|
+
return `replace ${edit.start.hash}-${edit.end.hash}`;
|
|
319
248
|
}
|
|
320
249
|
|
|
321
250
|
export function validateAnchorEdits(
|
|
@@ -345,102 +274,56 @@ export function validateAnchorEdits(
|
|
|
345
274
|
|
|
346
275
|
for (const edit of edits) {
|
|
347
276
|
throwIfAborted(signal);
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
};
|
|
393
|
-
warnings.push(
|
|
394
|
-
`Potential boundary duplication before ${describeEdit(resolvedEdit)}: the replacement starts with a line that matches the preceding surviving line after trim.`,
|
|
395
|
-
);
|
|
396
|
-
}
|
|
397
|
-
resolved.push({
|
|
398
|
-
op: "replace",
|
|
399
|
-
start: startResolved,
|
|
400
|
-
end: endResolved,
|
|
401
|
-
lines: edit.lines,
|
|
402
|
-
});
|
|
403
|
-
break;
|
|
404
|
-
}
|
|
405
|
-
case "append": {
|
|
406
|
-
let posResolved: ResolvedAnchor | undefined;
|
|
407
|
-
if (edit.pos) {
|
|
408
|
-
const r = tryResolve(edit.pos);
|
|
409
|
-
if (!r) continue;
|
|
410
|
-
posResolved = r;
|
|
411
|
-
}
|
|
412
|
-
if (edit.lines.length === 0) {
|
|
413
|
-
throw new Error(
|
|
414
|
-
"[E_BAD_OP] Append with empty lines payload. Provide content to insert or remove the edit.",
|
|
415
|
-
);
|
|
416
|
-
}
|
|
417
|
-
resolved.push({
|
|
418
|
-
op: "append",
|
|
419
|
-
...(posResolved ? { pos: posResolved } : {}),
|
|
420
|
-
lines: edit.lines,
|
|
421
|
-
});
|
|
422
|
-
break;
|
|
423
|
-
}
|
|
424
|
-
case "prepend": {
|
|
425
|
-
let posResolved: ResolvedAnchor | undefined;
|
|
426
|
-
if (edit.pos) {
|
|
427
|
-
const r = tryResolve(edit.pos);
|
|
428
|
-
if (!r) continue;
|
|
429
|
-
posResolved = r;
|
|
430
|
-
}
|
|
431
|
-
if (edit.lines.length === 0) {
|
|
432
|
-
throw new Error(
|
|
433
|
-
"[E_BAD_OP] Prepend with empty lines payload. Provide content to insert or remove the edit.",
|
|
434
|
-
);
|
|
435
|
-
}
|
|
436
|
-
resolved.push({
|
|
437
|
-
op: "prepend",
|
|
438
|
-
...(posResolved ? { pos: posResolved } : {}),
|
|
439
|
-
lines: edit.lines,
|
|
440
|
-
});
|
|
441
|
-
break;
|
|
442
|
-
}
|
|
277
|
+
const startResolved = tryResolve(edit.start);
|
|
278
|
+
const endResolved = tryResolve(edit.end);
|
|
279
|
+
if (!startResolved || !endResolved) {
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
282
|
+
if (startResolved.line > endResolved.line) {
|
|
283
|
+
throw new Error(
|
|
284
|
+
`[E_BAD_OP] Range start line ${startResolved.line} must be <= end line ${endResolved.line} (anchors ${edit.start.hash} and ${edit.end.hash}).`,
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
const endLine = endResolved.line;
|
|
288
|
+
const nextLine = fileLines[endLine];
|
|
289
|
+
const replacementLastLine = edit.lines.at(-1)?.trim();
|
|
290
|
+
if (
|
|
291
|
+
nextLine !== undefined &&
|
|
292
|
+
replacementLastLine &&
|
|
293
|
+
/[\p{L}\p{N}]/u.test(replacementLastLine) &&
|
|
294
|
+
replacementLastLine === nextLine.trim()
|
|
295
|
+
) {
|
|
296
|
+
const resolvedEdit: ResolvedHashlineEdit = {
|
|
297
|
+
start: startResolved,
|
|
298
|
+
end: endResolved,
|
|
299
|
+
lines: edit.lines,
|
|
300
|
+
};
|
|
301
|
+
warnings.push(
|
|
302
|
+
`Potential boundary duplication after ${describeEdit(resolvedEdit)}: the replacement ends with a line that matches the next surviving line after trim.`,
|
|
303
|
+
);
|
|
304
|
+
}
|
|
305
|
+
const prevLine = fileLines[startResolved.line - 2];
|
|
306
|
+
const replacementFirstLine = edit.lines[0]?.trim();
|
|
307
|
+
if (
|
|
308
|
+
prevLine !== undefined &&
|
|
309
|
+
replacementFirstLine &&
|
|
310
|
+
/[\p{L}\p{N}]/u.test(replacementFirstLine) &&
|
|
311
|
+
replacementFirstLine === prevLine.trim()
|
|
312
|
+
) {
|
|
313
|
+
const resolvedEdit: ResolvedHashlineEdit = {
|
|
314
|
+
start: startResolved,
|
|
315
|
+
end: endResolved,
|
|
316
|
+
lines: edit.lines,
|
|
317
|
+
};
|
|
318
|
+
warnings.push(
|
|
319
|
+
`Potential boundary duplication before ${describeEdit(resolvedEdit)}: the replacement starts with a line that matches the preceding surviving line after trim.`,
|
|
320
|
+
);
|
|
443
321
|
}
|
|
322
|
+
resolved.push({
|
|
323
|
+
start: startResolved,
|
|
324
|
+
end: endResolved,
|
|
325
|
+
lines: edit.lines,
|
|
326
|
+
});
|
|
444
327
|
}
|
|
445
328
|
|
|
446
329
|
return { resolved, mismatches };
|
package/src/read.ts
CHANGED
|
@@ -11,7 +11,7 @@ import { Type } from "typebox";
|
|
|
11
11
|
import { readFileSync } from "fs";
|
|
12
12
|
import { access as fsAccess } from "fs/promises";
|
|
13
13
|
import { constants } from "fs";
|
|
14
|
-
import { normalizeToLF, stripBom } from "./
|
|
14
|
+
import { normalizeToLF, stripBom } from "./replace-diff";
|
|
15
15
|
import { loadFileKindAndText } from "./file-kind";
|
|
16
16
|
import { computeLineHashes, formatHashlineRegion } from "./hashline";
|
|
17
17
|
import { resolveToCwd } from "./path-utils";
|
|
@@ -68,15 +68,14 @@ export function formatHashlineReadPreview(
|
|
|
68
68
|
if (totalLines === 0) {
|
|
69
69
|
if (startLine === 1) {
|
|
70
70
|
return {
|
|
71
|
-
text: "File is empty. Use edit
|
|
71
|
+
text: "File is empty. Use edit to insert content.",
|
|
72
72
|
};
|
|
73
73
|
}
|
|
74
|
-
|
|
75
74
|
return {
|
|
76
|
-
text: `Offset ${startLine} is beyond end of file (0 lines total). The file is empty. Use edit
|
|
75
|
+
text: `Offset ${startLine} is beyond end of file (0 lines total). The file is empty. Use edit to insert content.`,
|
|
77
76
|
};
|
|
78
|
-
}
|
|
79
77
|
|
|
78
|
+
}
|
|
80
79
|
if (startLine > totalLines) {
|
|
81
80
|
return {
|
|
82
81
|
text: `Offset ${startLine} is beyond end of file (${totalLines} lines total). Use offset=1 to read from the start, or offset=${totalLines} to read the last line.`,
|
|
@@ -197,7 +196,7 @@ export function registerReadTool(pi: ExtensionAPI): void {
|
|
|
197
196
|
throwIfAborted(signal);
|
|
198
197
|
const normalized = normalizeToLF(stripBom(file.text).text);
|
|
199
198
|
// Compute hashes once for the whole file so the per-line anchors the
|
|
200
|
-
// model sees here are byte-identical to what the
|
|
199
|
+
// model sees here are byte-identical to what the replace tool will
|
|
201
200
|
// compute when it later validates against this file.
|
|
202
201
|
const fileHashes = computeLineHashes(normalized);
|
|
203
202
|
const preview = formatHashlineReadPreview(
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* TUI rendering helpers for the
|
|
2
|
+
* TUI rendering helpers for the replace tool.
|
|
3
3
|
*
|
|
4
|
-
* Extracted from `src/
|
|
4
|
+
* Extracted from `src/replace.ts` to separate presentation (color themes, diff
|
|
5
5
|
* formatting, Markdown rendering) from tool execution logic.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
9
|
-
import {
|
|
10
|
-
import type {
|
|
9
|
+
import { normalizeReplaceRequest } from "./replace-normalize";
|
|
10
|
+
import type { ReplaceRequestParams, HashlineReplaceToolDetails } from "./replace";
|
|
11
11
|
import { isRecord } from "./utils";
|
|
12
12
|
|
|
13
13
|
// ─── Theme type aliases ─────────────────────────────────────────────────
|
|
@@ -21,11 +21,11 @@ export type RenderedMarkdownTheme = Pick<
|
|
|
21
21
|
|
|
22
22
|
// ─── Render state ───────────────────────────────────────────────────────
|
|
23
23
|
|
|
24
|
-
export type
|
|
24
|
+
export type ReplacePreview = { diff: string } | { error: string };
|
|
25
25
|
|
|
26
|
-
export type
|
|
26
|
+
export type ReplaceRenderState = {
|
|
27
27
|
argsKey?: string;
|
|
28
|
-
preview?:
|
|
28
|
+
preview?: ReplacePreview;
|
|
29
29
|
previewGeneration?: number;
|
|
30
30
|
};
|
|
31
31
|
|
|
@@ -34,10 +34,10 @@ export type EditRenderState = {
|
|
|
34
34
|
|
|
35
35
|
export function getRenderablePreviewInput(
|
|
36
36
|
args: unknown,
|
|
37
|
-
):
|
|
37
|
+
): ReplaceRequestParams | null {
|
|
38
38
|
let normalized: unknown;
|
|
39
39
|
try {
|
|
40
|
-
normalized =
|
|
40
|
+
normalized = normalizeReplaceRequest(args);
|
|
41
41
|
} catch {
|
|
42
42
|
return null;
|
|
43
43
|
}
|
|
@@ -45,7 +45,7 @@ export function getRenderablePreviewInput(
|
|
|
45
45
|
return null;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
const request:
|
|
48
|
+
const request: ReplaceRequestParams = { path: normalized.path };
|
|
49
49
|
if (Array.isArray(normalized.edits)) {
|
|
50
50
|
request.edits = normalized.edits as any;
|
|
51
51
|
}
|
|
@@ -91,8 +91,8 @@ export function formatResultDiff(diff: string, theme: FgTheme): string {
|
|
|
91
91
|
// ─── Edit call formatting ───────────────────────────────────────────────
|
|
92
92
|
|
|
93
93
|
export function formatEditCall(
|
|
94
|
-
args:
|
|
95
|
-
state:
|
|
94
|
+
args: ReplaceRequestParams | undefined,
|
|
95
|
+
state: ReplaceRenderState,
|
|
96
96
|
expanded: boolean,
|
|
97
97
|
theme: CallTheme,
|
|
98
98
|
): string {
|
|
@@ -101,7 +101,7 @@ export function formatEditCall(
|
|
|
101
101
|
typeof path === "string" && path.length > 0
|
|
102
102
|
? theme.fg("accent", path)
|
|
103
103
|
: theme.fg("toolOutput", "...");
|
|
104
|
-
let text = `${theme.fg("toolTitle", theme.bold("
|
|
104
|
+
let text = `${theme.fg("toolTitle", theme.bold("replace"))} ${pathDisplay}`;
|
|
105
105
|
|
|
106
106
|
if (!state.preview) {
|
|
107
107
|
return text;
|
|
@@ -139,7 +139,7 @@ export function extractRenderedWarnings(
|
|
|
139
139
|
// ─── Result classification ──────────────────────────────────────────────
|
|
140
140
|
|
|
141
141
|
export function isAppliedChangedResult(
|
|
142
|
-
details:
|
|
142
|
+
details: HashlineReplaceToolDetails | undefined,
|
|
143
143
|
): boolean {
|
|
144
144
|
const metrics = details?.metrics;
|
|
145
145
|
return (
|
|
@@ -152,8 +152,8 @@ export function isAppliedChangedResult(
|
|
|
152
152
|
|
|
153
153
|
export function buildAppliedChangedResultText(
|
|
154
154
|
text: string | undefined,
|
|
155
|
-
details:
|
|
156
|
-
preview:
|
|
155
|
+
details: HashlineReplaceToolDetails | undefined,
|
|
156
|
+
preview: ReplacePreview | undefined,
|
|
157
157
|
theme: FgTheme,
|
|
158
158
|
): string | undefined {
|
|
159
159
|
const previewDiff =
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
|
|
2
|
-
import { generateDiffString } from "./
|
|
2
|
+
import { generateDiffString } from "./replace-diff";
|
|
3
3
|
import {
|
|
4
4
|
computeAffectedLineRange,
|
|
5
5
|
computeLineHashes,
|
|
@@ -37,7 +37,7 @@ export type FullContentPreview = {
|
|
|
37
37
|
nextOffset?: number;
|
|
38
38
|
};
|
|
39
39
|
|
|
40
|
-
export type
|
|
40
|
+
export type ReplaceMetrics = {
|
|
41
41
|
edits_attempted: number;
|
|
42
42
|
edits_noop: number;
|
|
43
43
|
warnings: number;
|
|
@@ -53,7 +53,7 @@ export type ReadMetrics = {
|
|
|
53
53
|
next_offset?: number;
|
|
54
54
|
};
|
|
55
55
|
|
|
56
|
-
export type
|
|
56
|
+
export type ReplaceMeta = {
|
|
57
57
|
editsAttempted: number;
|
|
58
58
|
noopEditsCount: number;
|
|
59
59
|
firstChangedLine?: number;
|
|
@@ -75,7 +75,7 @@ export interface NoopResponseInput {
|
|
|
75
75
|
noopEdits: NoopEditEntry[] | undefined;
|
|
76
76
|
originalNormalized: string;
|
|
77
77
|
snapshotId: string;
|
|
78
|
-
editMeta:
|
|
78
|
+
editMeta: ReplaceMeta;
|
|
79
79
|
warnings: string[] | undefined;
|
|
80
80
|
}
|
|
81
81
|
|
|
@@ -92,7 +92,7 @@ export interface SuccessResponseInput {
|
|
|
92
92
|
resultHashes?: string[];
|
|
93
93
|
warnings: string[] | undefined;
|
|
94
94
|
snapshotId: string;
|
|
95
|
-
editMeta:
|
|
95
|
+
editMeta: ReplaceMeta;
|
|
96
96
|
}
|
|
97
97
|
|
|
98
98
|
// ─── Helpers ────────────────────────────────────────────────────────────
|
|
@@ -122,8 +122,8 @@ function buildMetrics(args: {
|
|
|
122
122
|
lastChangedLine?: number;
|
|
123
123
|
addedLines?: number;
|
|
124
124
|
removedLines?: number;
|
|
125
|
-
}):
|
|
126
|
-
const metrics:
|
|
125
|
+
}): ReplaceMetrics {
|
|
126
|
+
const metrics: ReplaceMetrics = {
|
|
127
127
|
edits_attempted: args.editsAttempted,
|
|
128
128
|
edits_noop: args.noopEditsCount,
|
|
129
129
|
warnings: args.warningsCount,
|
|
@@ -471,7 +471,7 @@ export function buildChangedResponse(input: SuccessResponseInput): ToolResult {
|
|
|
471
471
|
: "Anchors omitted; use read for subsequent edits.";
|
|
472
472
|
})()
|
|
473
473
|
: resultLines.length === 0
|
|
474
|
-
? "File is empty. Use edit
|
|
474
|
+
? "File is empty. Use edit to insert content."
|
|
475
475
|
: "Anchors omitted; use read for subsequent edits.";
|
|
476
476
|
|
|
477
477
|
const text = [anchorsBlock, warningsBlock.trimStart()]
|