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.
@@ -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: ReqParams | undefined,
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("replace"))} ${pathDisplay}`;
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
+
@@ -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
- ? `Replacement for ${noopEdit.loc} is identical to current content:\n ${noopEdit.loc}: ${clipLine(noopEdit.currentContent)}`
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 replaced in ${path}.`;
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
  : "";
@@ -85,13 +85,13 @@ export async function clearUndo(path: string): Promise<void> {
85
85
  }
86
86
  }
87
87
 
88
- export function regReplaceUndo(pi: ExtensionAPI): void {
88
+ export function regUndo(pi: ExtensionAPI): void {
89
89
  pi.registerTool({
90
- name: "undo_last_replace",
91
- label: "Undo Last Replace",
92
- description: loadP("../prompts/undo-last-replace.md"),
93
- promptSnippet: loadP("../prompts/undo-last-replace-snippet.md"),
94
- promptGuidelines: loadGuide("../prompts/undo-last-replace-guidelines.md"),
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 replace on ${path}: the file no longer exists.`
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 replace on ${path}: the file changed after the replace. Call read() to inspect the current state.`
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 replace on ${path}.`,
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, safeSnapId } from "./file-reader";
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, writeAtomic } from "./fs-write";
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
- buildChanged,
31
- buildNoop,
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, recordServedDiffSafe } from "./served";
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(args, theme, context) {
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
- if (isPartial) {
441
- return reuseText(context, theme.fg("warning", "Editing..."));
442
- }
443
-
444
- const typedResult = result as {
445
- content?: Array<{ type: string; text?: string }>;
446
- details?: ReplaceDetails;
447
- };
448
- const renderedText = getResultText(typedResult);
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
- if (resolution) {
515
- warnings.unshift(resolution.warning);
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
- originalNormalized,
586
- originalHashes,
587
- result,
588
- resultHashes,
589
- warnings,
590
- snapshotId: updatedSnapshotId,
591
- editMeta,
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).