pi-hashline-edit-pro 0.15.4 → 0.15.5
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/package.json +1 -1
- package/src/replace-flat.ts +7 -303
- package/src/replace.ts +77 -20
package/package.json
CHANGED
package/src/replace-flat.ts
CHANGED
|
@@ -1,313 +1,17 @@
|
|
|
1
|
-
import { Markdown, Text } from "@earendil-works/pi-tui";
|
|
2
1
|
import type {
|
|
3
2
|
ExtensionAPI,
|
|
4
|
-
ToolDefinition,
|
|
5
3
|
} from "@earendil-works/pi-coding-agent";
|
|
6
|
-
import { withFileMutationQueue } from "@earendil-works/pi-coding-agent";
|
|
7
|
-
import { Type } from "typebox";
|
|
8
|
-
import { constants } from "fs";
|
|
9
4
|
import {
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
} from "./replace
|
|
13
|
-
import { readNormFile } from "./file-reader";
|
|
14
|
-
import { normReq } from "./replace-normalize";
|
|
15
|
-
import { isRec } from "./utils";
|
|
16
|
-
import { MAX_HASH_LINES } from "./constants";
|
|
17
|
-
import { resolveTarget, writeAtomic } from "./fs-write";
|
|
18
|
-
import {
|
|
19
|
-
applyEdits,
|
|
20
|
-
resEdits,
|
|
21
|
-
type HTEdit,
|
|
22
|
-
} from "./hashline";
|
|
23
|
-
import { toCwd } from "./path-utils";
|
|
24
|
-
import { abortIf } from "./runtime";
|
|
25
|
-
import { fileSnap } from "./snapshot";
|
|
26
|
-
import {
|
|
27
|
-
buildChanged,
|
|
28
|
-
buildNoop,
|
|
29
|
-
type RMeta,
|
|
30
|
-
type RMetrics,
|
|
31
|
-
} from "./replace-response";
|
|
32
|
-
import {
|
|
33
|
-
buildAppliedText,
|
|
34
|
-
mkMdTheme,
|
|
35
|
-
fmtCall,
|
|
36
|
-
fmtResultMd,
|
|
37
|
-
getPreviewInput,
|
|
38
|
-
getResultText,
|
|
39
|
-
isApplied,
|
|
40
|
-
type RPreview,
|
|
41
|
-
type RRState,
|
|
42
|
-
} from "./replace-render";
|
|
43
|
-
import { loadP, loadGuide } from "./prompts";
|
|
44
|
-
import { execPipeline, compPreview, type ReplaceDetails } from "./replace";
|
|
45
|
-
import { readAutoReadSync } from "./config";
|
|
46
|
-
|
|
47
|
-
const contentLinesSchema = Type.Array(Type.String(), {
|
|
48
|
-
description:
|
|
49
|
-
"literal replacement file content, one string per line. Must not include the HASH│ prefix from read output.",
|
|
50
|
-
});
|
|
51
|
-
|
|
52
|
-
const hashRangeInclSchema = Type.Array(
|
|
53
|
-
Type.String({ description: "anchor (3-char HASH)" }),
|
|
54
|
-
{
|
|
55
|
-
description: "inclusive hash range to replace [start_hash, end_hash]. Each element must be the 3-character hash anchor only; do not include the │ separator or line content.",
|
|
56
|
-
minItems: 2,
|
|
57
|
-
maxItems: 2,
|
|
58
|
-
},
|
|
59
|
-
);
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Flat-mode schema: hash_range_inclusive and content_lines are at the top
|
|
63
|
-
* level instead of inside a "changes" array. Only a single edit is supported
|
|
64
|
-
* per call (no bulk changes).
|
|
65
|
-
*/
|
|
66
|
-
export const flatEditToolSchema = Type.Object(
|
|
67
|
-
{
|
|
68
|
-
path: Type.String({ description: "path" }),
|
|
69
|
-
hash_range_inclusive: hashRangeInclSchema,
|
|
70
|
-
content_lines: contentLinesSchema,
|
|
71
|
-
},
|
|
72
|
-
{ additionalProperties: false },
|
|
73
|
-
);
|
|
74
|
-
|
|
75
|
-
type ToolDef = ToolDefinition<
|
|
76
|
-
typeof flatEditToolSchema,
|
|
77
|
-
ReplaceDetails,
|
|
78
|
-
RRState
|
|
79
|
-
> & { renderShell?: "default" | "self" };
|
|
80
|
-
|
|
81
|
-
function reuseText(context: any, content: string): Text {
|
|
82
|
-
const t = context.lastComponent instanceof Text
|
|
83
|
-
? context.lastComponent
|
|
84
|
-
: new Text("", 0, 0);
|
|
85
|
-
t.setText(content);
|
|
86
|
-
return t;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function reuseMarkdown(context: any, content: string, theme: any): Markdown {
|
|
90
|
-
const m = context.lastComponent instanceof Markdown
|
|
91
|
-
? context.lastComponent
|
|
92
|
-
: new Markdown("", 0, 0, mkMdTheme(theme));
|
|
93
|
-
m.setText(content);
|
|
94
|
-
return m;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
export function buildToolDef(): ToolDef {
|
|
98
|
-
const autoRead = readAutoReadSync();
|
|
99
|
-
const readGuidance = autoRead
|
|
100
|
-
? "Anchors are provided automatically after write operations when auto-read is enabled."
|
|
101
|
-
: "Call `read` to get fresh anchors for follow-up edits.";
|
|
102
|
-
|
|
103
|
-
const E_DESC = loadP("../prompts/replace-flat.md", {
|
|
104
|
-
AUTO_READ_GUIDANCE: readGuidance,
|
|
105
|
-
});
|
|
106
|
-
const E_SNIPPET = loadP("../prompts/replace-flat-snippet.md");
|
|
107
|
-
const E_GUIDE = loadGuide("../prompts/replace-flat-guidelines.md", {
|
|
108
|
-
AUTO_READ_GUIDANCE: readGuidance,
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
return {
|
|
112
|
-
name: "replace",
|
|
113
|
-
label: "Replace",
|
|
114
|
-
description: E_DESC,
|
|
115
|
-
parameters: flatEditToolSchema,
|
|
116
|
-
promptSnippet: E_SNIPPET,
|
|
117
|
-
promptGuidelines: E_GUIDE,
|
|
118
|
-
prepareArguments: (args: unknown) => {
|
|
119
|
-
// Minimal normalization: file_path → path, JSON string parsing.
|
|
120
|
-
// The flat-to-canonical conversion happens in execute().
|
|
121
|
-
if (!isRec(args)) return args as any;
|
|
122
|
-
const record = { ...args };
|
|
123
|
-
if (typeof record.path !== "string" && typeof record.file_path === "string") {
|
|
124
|
-
record.path = record.file_path;
|
|
125
|
-
delete record.file_path;
|
|
126
|
-
}
|
|
127
|
-
if (typeof record.hash_range_inclusive === "string") {
|
|
128
|
-
try { record.hash_range_inclusive = JSON.parse(record.hash_range_inclusive as string); } catch { /* keep as-is */ }
|
|
129
|
-
}
|
|
130
|
-
if (typeof record.content_lines === "string") {
|
|
131
|
-
try { record.content_lines = JSON.parse(record.content_lines as string); } catch { /* keep as-is */ }
|
|
132
|
-
}
|
|
133
|
-
return record as any;
|
|
134
|
-
},
|
|
135
|
-
renderShell: "default",
|
|
136
|
-
renderCall(args, theme, context) {
|
|
137
|
-
const previewInput = getPreviewInput(args);
|
|
138
|
-
if (context.executionStarted) {
|
|
139
|
-
context.state.argsKey = undefined;
|
|
140
|
-
context.state.preview = undefined;
|
|
141
|
-
context.state.previewGeneration =
|
|
142
|
-
(context.state.previewGeneration ?? 0) + 1;
|
|
143
|
-
} else if (!context.argsComplete || !previewInput) {
|
|
144
|
-
context.state.argsKey = undefined;
|
|
145
|
-
context.state.preview = undefined;
|
|
146
|
-
context.state.previewGeneration =
|
|
147
|
-
(context.state.previewGeneration ?? 0) + 1;
|
|
148
|
-
} else {
|
|
149
|
-
const argsKey = JSON.stringify(previewInput);
|
|
150
|
-
if (context.state.argsKey !== argsKey) {
|
|
151
|
-
context.state.argsKey = argsKey;
|
|
152
|
-
context.state.preview = undefined;
|
|
153
|
-
const previewGeneration = (context.state.previewGeneration ?? 0) + 1;
|
|
154
|
-
context.state.previewGeneration = previewGeneration;
|
|
155
|
-
compPreview(previewInput, context.cwd)
|
|
156
|
-
.then((preview) => {
|
|
157
|
-
if (
|
|
158
|
-
context.state.argsKey === argsKey &&
|
|
159
|
-
context.state.previewGeneration === previewGeneration
|
|
160
|
-
) {
|
|
161
|
-
context.state.preview = preview;
|
|
162
|
-
context.invalidate();
|
|
163
|
-
}
|
|
164
|
-
})
|
|
165
|
-
.catch((err: unknown) => {
|
|
166
|
-
if (
|
|
167
|
-
context.state.argsKey === argsKey &&
|
|
168
|
-
context.state.previewGeneration === previewGeneration
|
|
169
|
-
) {
|
|
170
|
-
context.state.preview = {
|
|
171
|
-
error: err instanceof Error ? err.message : String(err),
|
|
172
|
-
};
|
|
173
|
-
context.invalidate();
|
|
174
|
-
}
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
const text =
|
|
179
|
-
(context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
|
180
|
-
text.setText(
|
|
181
|
-
fmtCall(
|
|
182
|
-
getPreviewInput(args) ?? undefined,
|
|
183
|
-
context.state as RRState,
|
|
184
|
-
context.expanded,
|
|
185
|
-
theme,
|
|
186
|
-
),
|
|
187
|
-
);
|
|
188
|
-
return text;
|
|
189
|
-
},
|
|
190
|
-
|
|
191
|
-
renderResult(result, { isPartial }, theme, context) {
|
|
192
|
-
if (isPartial) {
|
|
193
|
-
return reuseText(context, theme.fg("warning", "Editing..."));
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
const typedResult = result as {
|
|
197
|
-
content?: Array<{ type: string; text?: string }>;
|
|
198
|
-
details?: ReplaceDetails;
|
|
199
|
-
};
|
|
200
|
-
const renderedText = getResultText(typedResult);
|
|
201
|
-
|
|
202
|
-
const renderState = context.state as RRState | undefined;
|
|
203
|
-
if (renderState) {
|
|
204
|
-
renderState.preview = undefined;
|
|
205
|
-
renderState.previewGeneration = (renderState.previewGeneration ?? 0) + 1;
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
if (context.isError) {
|
|
209
|
-
return renderedText
|
|
210
|
-
? reuseText(context, `\n${theme.fg("error", renderedText)}`)
|
|
211
|
-
: new Text("", 0, 0);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
if (isApplied(typedResult.details)) {
|
|
215
|
-
const appliedText = buildAppliedText(renderedText, typedResult.details, theme);
|
|
216
|
-
return appliedText ? reuseText(context, appliedText) : new Text("", 0, 0);
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
if (!renderedText) return new Text("", 0, 0);
|
|
220
|
-
return reuseMarkdown(context, fmtResultMd(renderedText), theme);
|
|
221
|
-
},
|
|
222
|
-
|
|
223
|
-
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
224
|
-
// Wrap flat params into canonical shape for the pipeline
|
|
225
|
-
const canonical = normReq({
|
|
226
|
-
path: params.path,
|
|
227
|
-
changes: [{
|
|
228
|
-
hash_range_inclusive: params.hash_range_inclusive,
|
|
229
|
-
content_lines: params.content_lines,
|
|
230
|
-
}],
|
|
231
|
-
});
|
|
232
|
-
const normalizedParams = canonical as { path: string; changes: HTEdit[] };
|
|
233
|
-
const path = normalizedParams.path;
|
|
234
|
-
const absolutePath = toCwd(path, ctx.cwd);
|
|
235
|
-
const mutationTargetPath = await resolveTarget(absolutePath);
|
|
236
|
-
return withFileMutationQueue(mutationTargetPath, async () => {
|
|
237
|
-
abortIf(signal);
|
|
238
|
-
|
|
239
|
-
const {
|
|
240
|
-
originalNormalized,
|
|
241
|
-
originalHashes,
|
|
242
|
-
result,
|
|
243
|
-
bom,
|
|
244
|
-
originalEnding,
|
|
245
|
-
hadUtf8DecodeErrors,
|
|
246
|
-
warnings,
|
|
247
|
-
noopEdits,
|
|
248
|
-
firstChangedLine,
|
|
249
|
-
lastChangedLine,
|
|
250
|
-
resultHashes,
|
|
251
|
-
} = await execPipeline(
|
|
252
|
-
normalizedParams,
|
|
253
|
-
ctx.cwd,
|
|
254
|
-
constants.R_OK | constants.W_OK,
|
|
255
|
-
signal,
|
|
256
|
-
);
|
|
257
|
-
|
|
258
|
-
const editsAttempted = 1; // flat mode: exactly one edit per call
|
|
259
|
-
|
|
260
|
-
if (originalNormalized === result) {
|
|
261
|
-
const noopSnapshotId = (await fileSnap(absolutePath)).snapshotId;
|
|
262
|
-
return buildNoop({
|
|
263
|
-
path,
|
|
264
|
-
noopEdits,
|
|
265
|
-
snapshotId: noopSnapshotId,
|
|
266
|
-
editMeta: {
|
|
267
|
-
editsAttempted,
|
|
268
|
-
noopEditsCount: noopEdits?.length ?? 0,
|
|
269
|
-
},
|
|
270
|
-
warnings,
|
|
271
|
-
});
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
if (hadUtf8DecodeErrors) {
|
|
275
|
-
warnings.push(
|
|
276
|
-
"Non-UTF-8 bytes were shown as U+FFFD; this edit rewrote the file as UTF-8.",
|
|
277
|
-
);
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
abortIf(signal);
|
|
281
|
-
await writeAtomic(
|
|
282
|
-
absolutePath,
|
|
283
|
-
bom + restoreEndings(result, originalEnding),
|
|
284
|
-
);
|
|
285
|
-
const updatedSnapshotId = (await fileSnap(absolutePath))
|
|
286
|
-
.snapshotId;
|
|
5
|
+
buildToolDef,
|
|
6
|
+
flatEditToolSchema,
|
|
7
|
+
} from "./replace";
|
|
287
8
|
|
|
288
|
-
|
|
289
|
-
editsAttempted,
|
|
290
|
-
noopEditsCount: noopEdits?.length ?? 0,
|
|
291
|
-
firstChangedLine,
|
|
292
|
-
lastChangedLine,
|
|
293
|
-
};
|
|
9
|
+
export { flatEditToolSchema };
|
|
294
10
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
originalNormalized,
|
|
298
|
-
originalHashes,
|
|
299
|
-
result,
|
|
300
|
-
resultHashes,
|
|
301
|
-
warnings,
|
|
302
|
-
snapshotId: updatedSnapshotId,
|
|
303
|
-
editMeta,
|
|
304
|
-
};
|
|
305
|
-
return buildChanged(successInput);
|
|
306
|
-
});
|
|
307
|
-
},
|
|
308
|
-
};
|
|
11
|
+
export function buildToolDefFlat() {
|
|
12
|
+
return buildToolDef({ flat: true });
|
|
309
13
|
}
|
|
310
14
|
|
|
311
15
|
export function regReplaceFlat(pi: ExtensionAPI): void {
|
|
312
|
-
pi.registerTool(buildToolDef());
|
|
16
|
+
pi.registerTool(buildToolDef({ flat: true }));
|
|
313
17
|
}
|
package/src/replace.ts
CHANGED
|
@@ -75,6 +75,20 @@ export const editToolSchema = Type.Object(
|
|
|
75
75
|
{ additionalProperties: false },
|
|
76
76
|
);
|
|
77
77
|
|
|
78
|
+
/**
|
|
79
|
+
* Flat-mode schema: hash_range_inclusive and content_lines are at the top
|
|
80
|
+
* level instead of inside a "changes" array. Only a single edit is supported
|
|
81
|
+
* per call (no bulk changes).
|
|
82
|
+
*/
|
|
83
|
+
export const flatEditToolSchema = Type.Object(
|
|
84
|
+
{
|
|
85
|
+
path: Type.String({ description: "path" }),
|
|
86
|
+
hash_range_inclusive: hashRangeInclSchema,
|
|
87
|
+
content_lines: contentLinesSchema,
|
|
88
|
+
},
|
|
89
|
+
{ additionalProperties: false },
|
|
90
|
+
);
|
|
91
|
+
|
|
78
92
|
export type ReqParams = {
|
|
79
93
|
path: string;
|
|
80
94
|
changes: HTEdit[];
|
|
@@ -165,11 +179,23 @@ export async function execPipeline(
|
|
|
165
179
|
|
|
166
180
|
const result = anchorResult.content;
|
|
167
181
|
|
|
168
|
-
// Collect hashes targeted by the edit for hash-aware diff disambiguation
|
|
182
|
+
// Collect hashes targeted by the edit for hash-aware diff disambiguation.
|
|
183
|
+
// Include every hash in the replaced range, not just the boundary anchors,
|
|
184
|
+
// so that a surviving line whose content is identical to an interior line
|
|
185
|
+
// of the edit is not matched to a removed hash.
|
|
186
|
+
// Use originalHashes.indexOf to find line numbers since resEdits returns
|
|
187
|
+
// HEdit[] (Anchor = { hash }) not RHEdit[] (RAnchor = { line, hash }).
|
|
169
188
|
const removedHashes = new Set<string>();
|
|
170
189
|
for (const edit of resolved) {
|
|
171
|
-
|
|
172
|
-
|
|
190
|
+
const startHash = edit.hash_range_inclusive[0].hash;
|
|
191
|
+
const endHash = edit.hash_range_inclusive[1].hash;
|
|
192
|
+
const startLine = originalHashes.indexOf(startHash);
|
|
193
|
+
const endLine = originalHashes.indexOf(endHash);
|
|
194
|
+
if (startLine >= 0 && endLine >= 0) {
|
|
195
|
+
for (let i = startLine; i <= endLine; i++) {
|
|
196
|
+
removedHashes.add(originalHashes[i]!);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
173
199
|
}
|
|
174
200
|
|
|
175
201
|
// Compute stable result hashes using hash-aware preservation
|
|
@@ -247,12 +273,12 @@ export async function compPreview(
|
|
|
247
273
|
}
|
|
248
274
|
|
|
249
275
|
type ToolDef = ToolDefinition<
|
|
250
|
-
|
|
276
|
+
any,
|
|
251
277
|
ReplaceDetails,
|
|
252
278
|
RRState
|
|
253
279
|
> & { renderShell?: "default" | "self" };
|
|
254
280
|
|
|
255
|
-
function reuseText(context: any, content: string): Text {
|
|
281
|
+
export function reuseText(context: any, content: string): Text {
|
|
256
282
|
const t = context.lastComponent instanceof Text
|
|
257
283
|
? context.lastComponent
|
|
258
284
|
: new Text("", 0, 0);
|
|
@@ -260,7 +286,7 @@ function reuseText(context: any, content: string): Text {
|
|
|
260
286
|
return t;
|
|
261
287
|
}
|
|
262
288
|
|
|
263
|
-
function reuseMarkdown(context: any, content: string, theme: any): Markdown {
|
|
289
|
+
export function reuseMarkdown(context: any, content: string, theme: any): Markdown {
|
|
264
290
|
const m = context.lastComponent instanceof Markdown
|
|
265
291
|
? context.lastComponent
|
|
266
292
|
: new Markdown("", 0, 0, mkMdTheme(theme));
|
|
@@ -268,29 +294,49 @@ function reuseMarkdown(context: any, content: string, theme: any): Markdown {
|
|
|
268
294
|
return m;
|
|
269
295
|
}
|
|
270
296
|
|
|
271
|
-
export function buildToolDef(): ToolDef {
|
|
297
|
+
export function buildToolDef(opts: { flat: boolean }): ToolDef {
|
|
272
298
|
const autoRead = readAutoReadSync();
|
|
273
299
|
const readGuidance = autoRead
|
|
274
300
|
? "Anchors are provided automatically after write operations when auto-read is enabled."
|
|
275
301
|
: "Call `read` to get fresh anchors for follow-up edits.";
|
|
276
302
|
|
|
277
|
-
const E_DESC = loadP("../prompts/replace-bulk.md", {
|
|
303
|
+
const E_DESC = loadP(opts.flat ? "../prompts/replace-flat.md" : "../prompts/replace-bulk.md", {
|
|
278
304
|
AUTO_READ_GUIDANCE: readGuidance,
|
|
279
305
|
});
|
|
280
|
-
const E_SNIPPET = loadP("../prompts/replace-bulk-snippet.md");
|
|
281
|
-
const E_GUIDE = loadGuide("../prompts/replace-bulk-guidelines.md", {
|
|
306
|
+
const E_SNIPPET = loadP(opts.flat ? "../prompts/replace-flat-snippet.md" : "../prompts/replace-bulk-snippet.md");
|
|
307
|
+
const E_GUIDE = loadGuide(opts.flat ? "../prompts/replace-flat-guidelines.md" : "../prompts/replace-bulk-guidelines.md", {
|
|
282
308
|
AUTO_READ_GUIDANCE: readGuidance,
|
|
283
309
|
});
|
|
284
310
|
|
|
311
|
+
const parameters = opts.flat ? flatEditToolSchema : editToolSchema;
|
|
312
|
+
|
|
285
313
|
return {
|
|
286
314
|
name: "replace",
|
|
287
315
|
label: "Replace",
|
|
288
316
|
description: E_DESC,
|
|
289
|
-
parameters
|
|
317
|
+
parameters,
|
|
290
318
|
promptSnippet: E_SNIPPET,
|
|
291
319
|
promptGuidelines: E_GUIDE,
|
|
292
|
-
prepareArguments:
|
|
293
|
-
|
|
320
|
+
prepareArguments: opts.flat
|
|
321
|
+
? (args: unknown) => {
|
|
322
|
+
// Minimal normalization: file_path → path, JSON string parsing.
|
|
323
|
+
// The flat-to-canonical conversion happens in execute().
|
|
324
|
+
if (!isRec(args)) return args as any;
|
|
325
|
+
const record = { ...args };
|
|
326
|
+
if (typeof record.path !== "string" && typeof record.file_path === "string") {
|
|
327
|
+
record.path = record.file_path;
|
|
328
|
+
delete record.file_path;
|
|
329
|
+
}
|
|
330
|
+
if (typeof record.hash_range_inclusive === "string") {
|
|
331
|
+
try { record.hash_range_inclusive = JSON.parse(record.hash_range_inclusive as string); } catch { /* keep as-is */ }
|
|
332
|
+
}
|
|
333
|
+
if (typeof record.content_lines === "string") {
|
|
334
|
+
try { record.content_lines = JSON.parse(record.content_lines as string); } catch { /* keep as-is */ }
|
|
335
|
+
}
|
|
336
|
+
return record as any;
|
|
337
|
+
}
|
|
338
|
+
: (args: unknown) =>
|
|
339
|
+
normReq(args) as ReqParams,
|
|
294
340
|
renderShell: "default",
|
|
295
341
|
renderCall(args, theme, context) {
|
|
296
342
|
const previewInput = getPreviewInput(args);
|
|
@@ -380,9 +426,18 @@ export function buildToolDef(): ToolDef {
|
|
|
380
426
|
},
|
|
381
427
|
|
|
382
428
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
const
|
|
429
|
+
// Flat mode: wrap top-level fields into a single-element changes array.
|
|
430
|
+
// Bulk mode: use params as-is after normReq normalization.
|
|
431
|
+
const canonical = opts.flat
|
|
432
|
+
? normReq({
|
|
433
|
+
path: (params as any).path,
|
|
434
|
+
changes: [{
|
|
435
|
+
hash_range_inclusive: (params as any).hash_range_inclusive,
|
|
436
|
+
content_lines: (params as any).content_lines,
|
|
437
|
+
}],
|
|
438
|
+
})
|
|
439
|
+
: normReq(params);
|
|
440
|
+
const normalizedParams = canonical as { path: string; changes: HTEdit[] };
|
|
386
441
|
const path = normalizedParams.path;
|
|
387
442
|
const absolutePath = toCwd(path, ctx.cwd);
|
|
388
443
|
const mutationTargetPath = await resolveTarget(absolutePath);
|
|
@@ -408,9 +463,11 @@ export function buildToolDef(): ToolDef {
|
|
|
408
463
|
signal,
|
|
409
464
|
);
|
|
410
465
|
|
|
411
|
-
const editsAttempted =
|
|
412
|
-
?
|
|
413
|
-
:
|
|
466
|
+
const editsAttempted = opts.flat
|
|
467
|
+
? 1
|
|
468
|
+
: Array.isArray(normalizedParams.changes)
|
|
469
|
+
? normalizedParams.changes.length
|
|
470
|
+
: 0;
|
|
414
471
|
|
|
415
472
|
if (originalNormalized === result) {
|
|
416
473
|
const noopSnapshotId = (await fileSnap(absolutePath)).snapshotId;
|
|
@@ -464,5 +521,5 @@ export function buildToolDef(): ToolDef {
|
|
|
464
521
|
}
|
|
465
522
|
|
|
466
523
|
export function regReplace(pi: ExtensionAPI): void {
|
|
467
|
-
pi.registerTool(buildToolDef());
|
|
524
|
+
pi.registerTool(buildToolDef({ flat: false }));
|
|
468
525
|
}
|