pi-hashline-edit-pro 2.6.5 → 2.7.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 +68 -18
- package/index.ts +9 -4
- package/package.json +2 -2
- package/prompts/grep-guidelines.md +6 -0
- package/prompts/grep-snippet.md +1 -0
- package/prompts/grep.md +1 -0
- package/prompts/insert-guidelines.md +6 -0
- package/prompts/insert-snippet.md +1 -0
- package/prompts/insert.md +1 -0
- package/prompts/read-guidelines.md +1 -1
- package/prompts/undo-last-change-guidelines.md +2 -0
- package/prompts/undo-last-change-snippet.md +1 -0
- package/prompts/undo-last-change.md +1 -0
- package/src/commit.ts +107 -0
- package/src/file-reader.ts +4 -0
- package/src/grep.ts +323 -0
- package/src/hashline/index.ts +2 -0
- package/src/hashline/resolve.ts +43 -15
- package/src/insert.ts +222 -0
- package/src/replace-diff.ts +47 -6
- package/src/replace-render.ts +117 -2
- package/src/replace-response.ts +5 -5
- package/src/replace-undo.ts +9 -9
- package/src/replace.ts +34 -240
- package/prompts/undo-last-replace-guidelines.md +0 -2
- package/prompts/undo-last-replace-snippet.md +0 -1
- package/prompts/undo-last-replace.md +0 -1
package/src/replace-render.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { Markdown, Text } from "@earendil-works/pi-tui";
|
|
1
2
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
3
|
import { normReq } from "./replace-normalize";
|
|
3
4
|
import type { ReqParams, ReplaceDetails } from "./replace";
|
|
@@ -89,17 +90,18 @@ export function fmtResult(diff: string, theme: FgT): string {
|
|
|
89
90
|
}
|
|
90
91
|
|
|
91
92
|
export function fmtCall(
|
|
92
|
-
args:
|
|
93
|
+
args: { path?: string } | undefined,
|
|
93
94
|
state: RRState,
|
|
94
95
|
expanded: boolean,
|
|
95
96
|
theme: CallT,
|
|
97
|
+
toolName = "replace",
|
|
96
98
|
): string {
|
|
97
99
|
const path = args?.path;
|
|
98
100
|
const pathDisplay =
|
|
99
101
|
typeof path === "string" && path.length > 0
|
|
100
102
|
? theme.fg("accent", path)
|
|
101
103
|
: theme.fg("toolOutput", "...");
|
|
102
|
-
let text = `${theme.fg("toolTitle", theme.bold(
|
|
104
|
+
let text = `${theme.fg("toolTitle", theme.bold(toolName))} ${pathDisplay}`;
|
|
103
105
|
|
|
104
106
|
if (!state.preview) {
|
|
105
107
|
return text;
|
|
@@ -209,3 +211,116 @@ export function mkMdTheme(theme: MdTheme) {
|
|
|
209
211
|
}),
|
|
210
212
|
};
|
|
211
213
|
}
|
|
214
|
+
|
|
215
|
+
export const PREVIEW_DEBOUNCE_MS = 150;
|
|
216
|
+
|
|
217
|
+
export function reuseText(context: any, content: string): Text {
|
|
218
|
+
const t = context.lastComponent instanceof Text
|
|
219
|
+
? context.lastComponent
|
|
220
|
+
: new Text("", 0, 0);
|
|
221
|
+
t.setText(content);
|
|
222
|
+
return t;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
export function reuseMarkdown(context: any, content: string, theme: any): Markdown {
|
|
226
|
+
const m = context.lastComponent instanceof Markdown
|
|
227
|
+
? context.lastComponent
|
|
228
|
+
: new Markdown("", 0, 0, mkMdTheme(theme));
|
|
229
|
+
m.setText(content);
|
|
230
|
+
return m;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
export function makeRenderCall(
|
|
234
|
+
preview: (args: unknown, cwd: string) => Promise<RPreview>,
|
|
235
|
+
options: { getInput?: (args: unknown) => { path?: string } | null; toolName?: string } = {},
|
|
236
|
+
) {
|
|
237
|
+
const getInput = options.getInput ?? getPreviewInput;
|
|
238
|
+
const toolName = options.toolName ?? "replace";
|
|
239
|
+
return (args: any, theme: CallT, context: any): Text => {
|
|
240
|
+
const previewInput = getInput(args);
|
|
241
|
+
const cancelPendingPreview = () => {
|
|
242
|
+
if (context.state.previewTimer) {
|
|
243
|
+
clearTimeout(context.state.previewTimer);
|
|
244
|
+
context.state.previewTimer = undefined;
|
|
245
|
+
}
|
|
246
|
+
};
|
|
247
|
+
if (context.executionStarted) {
|
|
248
|
+
cancelPendingPreview();
|
|
249
|
+
context.state.argsKey = undefined;
|
|
250
|
+
context.state.preview = undefined;
|
|
251
|
+
context.state.previewGeneration = (context.state.previewGeneration ?? 0) + 1;
|
|
252
|
+
} else if (!context.argsComplete || !previewInput) {
|
|
253
|
+
cancelPendingPreview();
|
|
254
|
+
context.state.argsKey = undefined;
|
|
255
|
+
context.state.preview = undefined;
|
|
256
|
+
context.state.previewGeneration = (context.state.previewGeneration ?? 0) + 1;
|
|
257
|
+
} else {
|
|
258
|
+
const argsKey = JSON.stringify(previewInput);
|
|
259
|
+
if (context.state.argsKey !== argsKey) {
|
|
260
|
+
cancelPendingPreview();
|
|
261
|
+
context.state.argsKey = argsKey;
|
|
262
|
+
context.state.preview = undefined;
|
|
263
|
+
const previewGeneration = (context.state.previewGeneration ?? 0) + 1;
|
|
264
|
+
context.state.previewGeneration = previewGeneration;
|
|
265
|
+
context.state.previewTimer = setTimeout(() => {
|
|
266
|
+
context.state.previewTimer = undefined;
|
|
267
|
+
preview(args, context.cwd)
|
|
268
|
+
.then((result) => {
|
|
269
|
+
if (
|
|
270
|
+
context.state.argsKey === argsKey &&
|
|
271
|
+
context.state.previewGeneration === previewGeneration
|
|
272
|
+
) {
|
|
273
|
+
context.state.preview = result;
|
|
274
|
+
context.invalidate();
|
|
275
|
+
}
|
|
276
|
+
})
|
|
277
|
+
.catch((err: unknown) => {
|
|
278
|
+
if (
|
|
279
|
+
context.state.argsKey === argsKey &&
|
|
280
|
+
context.state.previewGeneration === previewGeneration
|
|
281
|
+
) {
|
|
282
|
+
context.state.preview = {
|
|
283
|
+
error: err instanceof Error ? err.message : String(err),
|
|
284
|
+
};
|
|
285
|
+
context.invalidate();
|
|
286
|
+
}
|
|
287
|
+
});
|
|
288
|
+
}, PREVIEW_DEBOUNCE_MS);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
const text = (context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
|
292
|
+
text.setText(fmtCall(getInput(args) ?? undefined, context.state as RRState, context.expanded, theme, toolName));
|
|
293
|
+
return text;
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
export function renderEditResult(
|
|
298
|
+
result: { content?: Array<{ type: string; text?: string }>; details?: ReplaceDetails },
|
|
299
|
+
isPartial: boolean,
|
|
300
|
+
theme: FgT,
|
|
301
|
+
context: any,
|
|
302
|
+
): Text | Markdown {
|
|
303
|
+
if (isPartial) return reuseText(context, theme.fg("warning", "Editing..."));
|
|
304
|
+
const renderedText = getResultText(result);
|
|
305
|
+
const renderState = context.state as RRState | undefined;
|
|
306
|
+
if (renderState) {
|
|
307
|
+
if (renderState.previewTimer) {
|
|
308
|
+
clearTimeout(renderState.previewTimer);
|
|
309
|
+
renderState.previewTimer = undefined;
|
|
310
|
+
}
|
|
311
|
+
renderState.preview = undefined;
|
|
312
|
+
renderState.previewGeneration = (renderState.previewGeneration ?? 0) + 1;
|
|
313
|
+
}
|
|
314
|
+
if (context.isError) {
|
|
315
|
+
return renderedText
|
|
316
|
+
? reuseText(context, `\n${theme.fg("error", renderedText)}`)
|
|
317
|
+
: new Text("", 0, 0);
|
|
318
|
+
}
|
|
319
|
+
if (isApplied(result.details)) {
|
|
320
|
+
const appliedText = buildAppliedText(renderedText, result.details, theme);
|
|
321
|
+
return appliedText ? reuseText(context, appliedText) : new Text("", 0, 0);
|
|
322
|
+
}
|
|
323
|
+
if (!renderedText) return new Text("", 0, 0);
|
|
324
|
+
return reuseMarkdown(context, fmtResultMd(renderedText), theme);
|
|
325
|
+
}
|
|
326
|
+
|
package/src/replace-response.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { ReplaceDetails } from "./replace";
|
|
|
2
2
|
import { genDiff, genPatch } from "./replace-diff";
|
|
3
3
|
import { visLines, clipLine } from "./utils";
|
|
4
4
|
|
|
5
|
-
type TResult = {
|
|
5
|
+
export type TResult = {
|
|
6
6
|
content: Array<{ type: "text"; text: string }>;
|
|
7
7
|
isError?: boolean;
|
|
8
8
|
details: ReplaceDetails;
|
|
@@ -89,7 +89,7 @@ function warnBlock(warnings: string[] | undefined): string {
|
|
|
89
89
|
return warnings?.length ? `\n\nWarnings:\n${warnings.join("\n")}` : "";
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
export function buildNoop(input: NoopInput): TResult {
|
|
92
|
+
export function buildNoop(input: NoopInput, noopNoun = "Replacement"): TResult {
|
|
93
93
|
const {
|
|
94
94
|
path,
|
|
95
95
|
noopEdit,
|
|
@@ -100,7 +100,7 @@ export function buildNoop(input: NoopInput): TResult {
|
|
|
100
100
|
} = input;
|
|
101
101
|
|
|
102
102
|
const noopDetailsText = noopEdit
|
|
103
|
-
?
|
|
103
|
+
? `${noopNoun} for ${noopEdit.loc} is identical to current content:\n ${noopEdit.loc}: ${clipLine(noopEdit.currentContent)}`
|
|
104
104
|
: "The edit produced identical content.";
|
|
105
105
|
const dedupNote =
|
|
106
106
|
boundaryRemovedLines !== undefined && boundaryRemovedLines > 0
|
|
@@ -129,14 +129,14 @@ export function buildNoop(input: NoopInput): TResult {
|
|
|
129
129
|
};
|
|
130
130
|
}
|
|
131
131
|
|
|
132
|
-
export function buildChanged(input: SuccessInput): TResult {
|
|
132
|
+
export function buildChanged(input: SuccessInput, verb = "replaced"): TResult {
|
|
133
133
|
const { path, result, warnings, snapshotId, originalNormalized, originalHashes, editMeta, resultHashes } = input;
|
|
134
134
|
const resultLines = visLines(result);
|
|
135
135
|
const diffResult = genDiff(originalNormalized, result, 1, resultHashes, originalHashes);
|
|
136
136
|
const addedLines = editMeta.addedLines;
|
|
137
137
|
const removedLines = editMeta.removedLines;
|
|
138
138
|
const warningsBlock = warnBlock(warnings);
|
|
139
|
-
const successPrefix = `Successfully
|
|
139
|
+
const successPrefix = `Successfully ${verb} in ${path}.`;
|
|
140
140
|
const lineSummary = addedLines > 0 || removedLines > 0
|
|
141
141
|
? ` Added ${addedLines} line(s), removed ${removedLines} line(s).`
|
|
142
142
|
: "";
|
package/src/replace-undo.ts
CHANGED
|
@@ -85,13 +85,13 @@ export async function clearUndo(path: string): Promise<void> {
|
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
export function
|
|
88
|
+
export function regUndo(pi: ExtensionAPI): void {
|
|
89
89
|
pi.registerTool({
|
|
90
|
-
name: "
|
|
91
|
-
label: "Undo Last
|
|
92
|
-
description: loadP("../prompts/undo-last-
|
|
93
|
-
promptSnippet: loadP("../prompts/undo-last-
|
|
94
|
-
promptGuidelines: loadGuide("../prompts/undo-last-
|
|
90
|
+
name: "undo_last_change",
|
|
91
|
+
label: "Undo Last Change",
|
|
92
|
+
description: loadP("../prompts/undo-last-change.md"),
|
|
93
|
+
promptSnippet: loadP("../prompts/undo-last-change-snippet.md"),
|
|
94
|
+
promptGuidelines: loadGuide("../prompts/undo-last-change-guidelines.md"),
|
|
95
95
|
prepareArguments: makePrepareArguments(),
|
|
96
96
|
parameters: Type.Object({
|
|
97
97
|
path: Type.String({
|
|
@@ -132,7 +132,7 @@ export function regReplaceUndo(pi: ExtensionAPI): void {
|
|
|
132
132
|
content: [
|
|
133
133
|
{
|
|
134
134
|
type: "text",
|
|
135
|
-
text: `[E_UNDO_STALE] Cannot undo last
|
|
135
|
+
text: `[E_UNDO_STALE] Cannot undo last change on ${path}: the file no longer exists.`
|
|
136
136
|
},
|
|
137
137
|
],
|
|
138
138
|
isError: true,
|
|
@@ -145,7 +145,7 @@ export function regReplaceUndo(pi: ExtensionAPI): void {
|
|
|
145
145
|
content: [
|
|
146
146
|
{
|
|
147
147
|
type: "text",
|
|
148
|
-
text: `[E_UNDO_STALE] Cannot undo last
|
|
148
|
+
text: `[E_UNDO_STALE] Cannot undo last change on ${path}: the file changed after the edit. Call read() to inspect the current state.`
|
|
149
149
|
},
|
|
150
150
|
],
|
|
151
151
|
isError: true,
|
|
@@ -178,7 +178,7 @@ export function regReplaceUndo(pi: ExtensionAPI): void {
|
|
|
178
178
|
await clearUndo(mutationTargetPath);
|
|
179
179
|
|
|
180
180
|
const parts: string[] = [
|
|
181
|
-
`Undone last
|
|
181
|
+
`Undone last change on ${path}.`,
|
|
182
182
|
];
|
|
183
183
|
if (linesAddedByReplace > 0 || linesRemovedByReplace > 0) {
|
|
184
184
|
parts.push(
|
package/src/replace.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { Markdown, Text } from "@earendil-works/pi-tui";
|
|
2
1
|
import type {
|
|
3
2
|
ExtensionAPI,
|
|
4
3
|
ToolDefinition,
|
|
@@ -8,13 +7,12 @@ import { Type } from "typebox";
|
|
|
8
7
|
import { constants } from "fs";
|
|
9
8
|
import {
|
|
10
9
|
genDiff,
|
|
11
|
-
restoreEndings,
|
|
12
10
|
type LineEnding,
|
|
13
11
|
} from "./replace-diff";
|
|
14
|
-
import { readNormFile,
|
|
12
|
+
import { readNormFile, type NormFile } from "./file-reader";
|
|
15
13
|
import { normReq } from "./replace-normalize";
|
|
16
14
|
import { isRec, rejectUnknownFields, abortIf, makePrepareArguments } from "./utils";
|
|
17
|
-
import { resolveTarget
|
|
15
|
+
import { resolveTarget } from "./fs-write";
|
|
18
16
|
import { applyEdit,
|
|
19
17
|
lineHashes,
|
|
20
18
|
resEdit,
|
|
@@ -26,28 +24,19 @@ import { applyEdit,
|
|
|
26
24
|
type NEdit,
|
|
27
25
|
} from "./hashline";
|
|
28
26
|
import { toCwd } from "./paths";
|
|
27
|
+
import type { RMetrics } from "./replace-response";
|
|
29
28
|
import {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
type RMeta,
|
|
33
|
-
type RMetrics,
|
|
34
|
-
} from "./replace-response";
|
|
35
|
-
import {
|
|
36
|
-
buildAppliedText,
|
|
37
|
-
mkMdTheme,
|
|
38
|
-
fmtCall,
|
|
39
|
-
fmtResultMd,
|
|
40
|
-
getPreviewInput,
|
|
41
|
-
getResultText,
|
|
42
|
-
isApplied,
|
|
29
|
+
makeRenderCall,
|
|
30
|
+
renderEditResult,
|
|
43
31
|
type RPreview,
|
|
44
32
|
type RRState,
|
|
45
33
|
} from "./replace-render";
|
|
34
|
+
export { reuseText, reuseMarkdown } from "./replace-render";
|
|
46
35
|
import { loadP, loadGuide } from "./prompts";
|
|
47
|
-
import { saveUndo } from "./replace-undo";
|
|
48
36
|
import { loadHashStore, findSnapshotPaths, type HashStore } from "./hash-store";
|
|
49
|
-
import { getServed, recordServedSafe
|
|
37
|
+
import { getServed, recordServedSafe } from "./served";
|
|
50
38
|
import { noopPayloadKey, markBoundaryNoop, consumeBoundaryBypass, clearBoundaryBypass } from "./boundary-bypass";
|
|
39
|
+
import { commitEdit } from "./commit";
|
|
51
40
|
|
|
52
41
|
const replacementLinesSchema = Type.Array(
|
|
53
42
|
Type.String({
|
|
@@ -93,7 +82,7 @@ export type ReplaceDetails = {
|
|
|
93
82
|
metrics?: RMetrics;
|
|
94
83
|
};
|
|
95
84
|
|
|
96
|
-
interface PipelineResult {
|
|
85
|
+
export interface PipelineResult {
|
|
97
86
|
path: string;
|
|
98
87
|
originalNormalized: string;
|
|
99
88
|
result: string;
|
|
@@ -112,8 +101,6 @@ interface PipelineResult {
|
|
|
112
101
|
boundaryRemovedLines: number;
|
|
113
102
|
}
|
|
114
103
|
|
|
115
|
-
const PREVIEW_DEBOUNCE_MS = 150;
|
|
116
|
-
|
|
117
104
|
const ROOT_KS = new Set(["path", "remove_from", "remove_to", "replacement_lines"]);
|
|
118
105
|
|
|
119
106
|
export function assertReq(
|
|
@@ -183,6 +170,7 @@ export interface ExecPipelineOptions {
|
|
|
183
170
|
store?: HashStore;
|
|
184
171
|
noPersist?: boolean;
|
|
185
172
|
skipBoundaryDedup?: boolean;
|
|
173
|
+
preloadedNorm?: NormFile;
|
|
186
174
|
}
|
|
187
175
|
|
|
188
176
|
function collectRemovedHashes(
|
|
@@ -243,7 +231,7 @@ export async function execPipeline(
|
|
|
243
231
|
|
|
244
232
|
const hashStore = options?.store ?? await loadHashStore();
|
|
245
233
|
const { normalized: originalNormalized, bom, originalEnding, fileHashes: originalHashes, hadUtf8DecodeErrors, absolutePath } = await readNormFile(
|
|
246
|
-
path, cwd, { signal: options?.signal, accessMode: options?.accessMode, maxLines: MAX_HASH_LINES, store: hashStore, noPersist: options?.noPersist },
|
|
234
|
+
path, cwd, { signal: options?.signal, accessMode: options?.accessMode, maxLines: MAX_HASH_LINES, store: hashStore, noPersist: options?.noPersist, preloadedNorm: options?.preloadedNorm },
|
|
247
235
|
);
|
|
248
236
|
|
|
249
237
|
const served = await getServed(hashStore, absolutePath);
|
|
@@ -338,21 +326,6 @@ type ToolDef = ToolDefinition<
|
|
|
338
326
|
RRState
|
|
339
327
|
> & { renderShell?: "default" | "self" };
|
|
340
328
|
|
|
341
|
-
export function reuseText(context: any, content: string): Text {
|
|
342
|
-
const t = context.lastComponent instanceof Text
|
|
343
|
-
? context.lastComponent
|
|
344
|
-
: new Text("", 0, 0);
|
|
345
|
-
t.setText(content);
|
|
346
|
-
return t;
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
export function reuseMarkdown(context: any, content: string, theme: any): Markdown {
|
|
350
|
-
const m = context.lastComponent instanceof Markdown
|
|
351
|
-
? context.lastComponent
|
|
352
|
-
: new Markdown("", 0, 0, mkMdTheme(theme));
|
|
353
|
-
m.setText(content);
|
|
354
|
-
return m;
|
|
355
|
-
}
|
|
356
329
|
|
|
357
330
|
export function buildToolDef(): ToolDef {
|
|
358
331
|
const E_DESC = loadP("../prompts/replace.md");
|
|
@@ -369,107 +342,17 @@ export function buildToolDef(): ToolDef {
|
|
|
369
342
|
promptGuidelines: E_GUIDE,
|
|
370
343
|
prepareArguments: makePrepareArguments(),
|
|
371
344
|
renderShell: "default",
|
|
372
|
-
renderCall(
|
|
373
|
-
const previewInput = getPreviewInput(args);
|
|
374
|
-
const cancelPendingPreview = () => {
|
|
375
|
-
if (context.state.previewTimer) {
|
|
376
|
-
clearTimeout(context.state.previewTimer);
|
|
377
|
-
context.state.previewTimer = undefined;
|
|
378
|
-
}
|
|
379
|
-
};
|
|
380
|
-
if (context.executionStarted) {
|
|
381
|
-
cancelPendingPreview();
|
|
382
|
-
context.state.argsKey = undefined;
|
|
383
|
-
context.state.preview = undefined;
|
|
384
|
-
context.state.previewGeneration =
|
|
385
|
-
(context.state.previewGeneration ?? 0) + 1;
|
|
386
|
-
} else if (!context.argsComplete || !previewInput) {
|
|
387
|
-
cancelPendingPreview();
|
|
388
|
-
context.state.argsKey = undefined;
|
|
389
|
-
context.state.preview = undefined;
|
|
390
|
-
context.state.previewGeneration =
|
|
391
|
-
(context.state.previewGeneration ?? 0) + 1;
|
|
392
|
-
} else {
|
|
393
|
-
const argsKey = JSON.stringify(previewInput);
|
|
394
|
-
if (context.state.argsKey !== argsKey) {
|
|
395
|
-
cancelPendingPreview();
|
|
396
|
-
context.state.argsKey = argsKey;
|
|
397
|
-
context.state.preview = undefined;
|
|
398
|
-
const previewGeneration = (context.state.previewGeneration ?? 0) + 1;
|
|
399
|
-
context.state.previewGeneration = previewGeneration;
|
|
400
|
-
context.state.previewTimer = setTimeout(() => {
|
|
401
|
-
context.state.previewTimer = undefined;
|
|
402
|
-
compPreview(args, context.cwd)
|
|
403
|
-
.then((preview) => {
|
|
404
|
-
if (
|
|
405
|
-
context.state.argsKey === argsKey &&
|
|
406
|
-
context.state.previewGeneration === previewGeneration
|
|
407
|
-
) {
|
|
408
|
-
context.state.preview = preview;
|
|
409
|
-
context.invalidate();
|
|
410
|
-
}
|
|
411
|
-
})
|
|
412
|
-
.catch((err: unknown) => {
|
|
413
|
-
if (
|
|
414
|
-
context.state.argsKey === argsKey &&
|
|
415
|
-
context.state.previewGeneration === previewGeneration
|
|
416
|
-
) {
|
|
417
|
-
context.state.preview = {
|
|
418
|
-
error: err instanceof Error ? err.message : String(err),
|
|
419
|
-
};
|
|
420
|
-
context.invalidate();
|
|
421
|
-
}
|
|
422
|
-
});
|
|
423
|
-
}, PREVIEW_DEBOUNCE_MS);
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
const text =
|
|
427
|
-
(context.lastComponent as Text | undefined) ?? new Text("", 0, 0);
|
|
428
|
-
text.setText(
|
|
429
|
-
fmtCall(
|
|
430
|
-
getPreviewInput(args) ?? undefined,
|
|
431
|
-
context.state as RRState,
|
|
432
|
-
context.expanded,
|
|
433
|
-
theme,
|
|
434
|
-
),
|
|
435
|
-
);
|
|
436
|
-
return text;
|
|
437
|
-
},
|
|
438
|
-
|
|
345
|
+
renderCall: makeRenderCall(compPreview),
|
|
439
346
|
renderResult(result, { isPartial }, theme, context) {
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
const renderState = context.state as RRState | undefined;
|
|
451
|
-
if (renderState) {
|
|
452
|
-
if (renderState.previewTimer) {
|
|
453
|
-
clearTimeout(renderState.previewTimer);
|
|
454
|
-
renderState.previewTimer = undefined;
|
|
455
|
-
}
|
|
456
|
-
renderState.preview = undefined;
|
|
457
|
-
renderState.previewGeneration = (renderState.previewGeneration ?? 0) + 1;
|
|
458
|
-
}
|
|
459
|
-
|
|
460
|
-
if (context.isError) {
|
|
461
|
-
return renderedText
|
|
462
|
-
? reuseText(context, `\n${theme.fg("error", renderedText)}`)
|
|
463
|
-
: new Text("", 0, 0);
|
|
464
|
-
}
|
|
465
|
-
|
|
466
|
-
if (isApplied(typedResult.details)) {
|
|
467
|
-
const appliedText = buildAppliedText(renderedText, typedResult.details, theme);
|
|
468
|
-
return appliedText ? reuseText(context, appliedText) : new Text("", 0, 0);
|
|
469
|
-
}
|
|
470
|
-
|
|
471
|
-
if (!renderedText) return new Text("", 0, 0);
|
|
472
|
-
return reuseMarkdown(context, fmtResultMd(renderedText), theme);
|
|
347
|
+
return renderEditResult(
|
|
348
|
+
result as {
|
|
349
|
+
content?: Array<{ type: string; text?: string }>;
|
|
350
|
+
details?: ReplaceDetails;
|
|
351
|
+
},
|
|
352
|
+
isPartial,
|
|
353
|
+
theme,
|
|
354
|
+
context,
|
|
355
|
+
);
|
|
473
356
|
},
|
|
474
357
|
|
|
475
358
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
@@ -488,113 +371,24 @@ export function buildToolDef(): ToolDef {
|
|
|
488
371
|
const boundaryBypass = consumeBoundaryBypass(mutationTargetPath, noopPayload);
|
|
489
372
|
return withFileMutationQueue(mutationTargetPath, async () => {
|
|
490
373
|
abortIf(signal);
|
|
491
|
-
|
|
492
|
-
const {
|
|
493
|
-
originalNormalized,
|
|
494
|
-
originalHashes,
|
|
495
|
-
result,
|
|
496
|
-
bom,
|
|
497
|
-
originalEnding,
|
|
498
|
-
hadUtf8DecodeErrors,
|
|
499
|
-
warnings,
|
|
500
|
-
noopEdit,
|
|
501
|
-
firstChangedLine,
|
|
502
|
-
lastChangedLine,
|
|
503
|
-
resultHashes,
|
|
504
|
-
hadBoundaryDedup,
|
|
505
|
-
boundaryRemovedLines,
|
|
506
|
-
totalAddedLines,
|
|
507
|
-
totalRemovedLines,
|
|
508
|
-
} = await execPipeline(
|
|
374
|
+
const pipe = await execPipeline(
|
|
509
375
|
normalizedParams,
|
|
510
376
|
ctx.cwd,
|
|
511
377
|
{ accessMode: constants.R_OK | constants.W_OK, signal, skipBoundaryDedup: boundaryBypass },
|
|
512
378
|
);
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
if (boundaryBypass && originalNormalized !== result) {
|
|
518
|
-
warnings.push("[E_BOUNDARY_BYPASS] Boundary dedup was off for this call and is back on.");
|
|
519
|
-
}
|
|
520
|
-
|
|
521
|
-
const editsAttempted = 1;
|
|
522
|
-
if (originalNormalized === result) {
|
|
523
|
-
const noopSnapshotId = await safeSnapId(absolutePath, "noop edit");
|
|
524
|
-
if (hadBoundaryDedup) {
|
|
525
|
-
markBoundaryNoop(mutationTargetPath, noopPayload);
|
|
526
|
-
}
|
|
527
|
-
return buildNoop({
|
|
528
|
-
path,
|
|
529
|
-
noopEdit,
|
|
530
|
-
snapshotId: noopSnapshotId,
|
|
531
|
-
editMeta: {
|
|
532
|
-
editsAttempted,
|
|
533
|
-
noopEditsCount: noopEdit ? 1 : 0,
|
|
534
|
-
addedLines: 0,
|
|
535
|
-
removedLines: 0,
|
|
536
|
-
},
|
|
537
|
-
warnings,
|
|
538
|
-
boundaryRemovedLines,
|
|
539
|
-
});
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
if (hadUtf8DecodeErrors) {
|
|
543
|
-
warnings.push(
|
|
544
|
-
"Non-UTF-8 bytes were shown as U+FFFD; this edit rewrote the file as UTF-8.",
|
|
545
|
-
);
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
abortIf(signal);
|
|
549
|
-
const undo = await saveUndo(mutationTargetPath, {
|
|
550
|
-
content: originalNormalized,
|
|
551
|
-
bom,
|
|
552
|
-
originalEnding,
|
|
553
|
-
hashes: originalHashes,
|
|
554
|
-
resultContent: result,
|
|
555
|
-
});
|
|
556
|
-
if (!undo.persisted) {
|
|
557
|
-
throw new Error(
|
|
558
|
-
`[E_UNDO_UNAVAILABLE] Could not persist undo history; the edit was not applied and ${path} is unchanged.`
|
|
559
|
-
);
|
|
560
|
-
}
|
|
561
|
-
try {
|
|
562
|
-
abortIf(signal);
|
|
563
|
-
await writeAtomic(
|
|
564
|
-
absolutePath,
|
|
565
|
-
bom + restoreEndings(result, originalEnding),
|
|
566
|
-
);
|
|
567
|
-
} catch (error) {
|
|
568
|
-
await undo.restore();
|
|
569
|
-
throw error;
|
|
570
|
-
}
|
|
571
|
-
clearBoundaryBypass(mutationTargetPath);
|
|
572
|
-
const updatedSnapshotId = await safeSnapId(absolutePath, "post-edit");
|
|
573
|
-
|
|
574
|
-
const editMeta: RMeta = {
|
|
575
|
-
editsAttempted,
|
|
576
|
-
noopEditsCount: noopEdit ? 1 : 0,
|
|
577
|
-
firstChangedLine,
|
|
578
|
-
lastChangedLine,
|
|
579
|
-
addedLines: totalAddedLines,
|
|
580
|
-
removedLines: totalRemovedLines,
|
|
581
|
-
};
|
|
582
|
-
|
|
583
|
-
const successInput = {
|
|
379
|
+
const appliedWarnings = boundaryBypass
|
|
380
|
+
? ["[E_BOUNDARY_BYPASS] Boundary dedup was off for this call and is back on."]
|
|
381
|
+
: [];
|
|
382
|
+
return commitEdit(pipe, {
|
|
584
383
|
path,
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
};
|
|
593
|
-
const changed = buildChanged(successInput);
|
|
594
|
-
if (changed.details.diff) {
|
|
595
|
-
await recordServedDiffSafe(mutationTargetPath, changed.details.diff, "post-edit diff", new Set(resultHashes));
|
|
596
|
-
}
|
|
597
|
-
return changed;
|
|
384
|
+
absolutePath,
|
|
385
|
+
mutationTargetPath,
|
|
386
|
+
signal,
|
|
387
|
+
prefixWarnings: resolution ? [resolution.warning] : [],
|
|
388
|
+
appliedWarnings,
|
|
389
|
+
onApplied: () => clearBoundaryBypass(mutationTargetPath),
|
|
390
|
+
onNoopDedup: () => markBoundaryNoop(mutationTargetPath, noopPayload),
|
|
391
|
+
});
|
|
598
392
|
});
|
|
599
393
|
},
|
|
600
394
|
};
|
|
@@ -1,2 +0,0 @@
|
|
|
1
|
-
- `undo_last_replace`: reverts only the most recent replace on the file: any write to the file clears the undo history, so call it immediately after a bad replace. A replace is bad when its post-edit diff shows `-HASH│` rows for lines you meant to keep (a closing brace, import, or declaration).
|
|
2
|
-
- `undo_last_replace`: when auto-read shows the post-edit diff, its `+HASH│` and ` HASH│` rows are the fresh anchors for the restored file, so follow-up edits can anchor on the diff without re-reading.
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
Undo the last replace on a file
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
Undo the last replace on a file, reverting it to its previous state. Use when a replace produced incorrect results (e.g., wrong content, duplicated lines, broken syntax).
|