pi-hashline-edit-pro 6.1.2 → 6.3.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.
Files changed (73) hide show
  1. package/README.md +41 -23
  2. package/index.ts +20 -12
  3. package/package.json +3 -2
  4. package/src/anchor-registry.ts +22 -5
  5. package/src/auto-read-all.ts +39 -21
  6. package/src/batch.ts +127 -41
  7. package/src/commit.ts +1 -1
  8. package/src/config-ui.ts +135 -55
  9. package/src/config.ts +27 -9
  10. package/src/copy-move.ts +61 -72
  11. package/src/edit-common.ts +6 -4
  12. package/src/grep.ts +53 -121
  13. package/src/hash-store.ts +13 -12
  14. package/src/hashline/resolve.ts +13 -2
  15. package/src/insert.ts +40 -34
  16. package/src/line-endings.ts +10 -1
  17. package/src/outline/index.ts +46 -0
  18. package/src/outline/parsers.ts +42 -0
  19. package/src/outline/render.ts +170 -0
  20. package/src/outline/vendor/LICENSE +201 -0
  21. package/src/outline/vendor/README.md +17 -0
  22. package/src/outline/vendor/parsers/blade.ts +138 -0
  23. package/src/outline/vendor/parsers/index.ts +75 -0
  24. package/src/outline/vendor/parsers/java.ts +501 -0
  25. package/src/outline/vendor/parsers/manager.ts +1721 -0
  26. package/src/outline/vendor/parsers/php.ts +332 -0
  27. package/src/outline/vendor/parsers/sass.ts +416 -0
  28. package/src/outline/vendor/parsers/scss.ts +566 -0
  29. package/src/outline/vendor/types.ts +82 -0
  30. package/src/outline/vendor/wasm/LICENSES.md +37 -0
  31. package/src/outline/vendor/wasm/tree-sitter-blade.wasm +0 -0
  32. package/src/outline/vendor/wasm/tree-sitter-go.wasm +0 -0
  33. package/src/outline/vendor/wasm/tree-sitter-java.wasm +0 -0
  34. package/src/outline/vendor/wasm/tree-sitter-javascript.wasm +0 -0
  35. package/src/outline/vendor/wasm/tree-sitter-php.wasm +0 -0
  36. package/src/outline/vendor/wasm/tree-sitter-python.wasm +0 -0
  37. package/src/outline/vendor/wasm/tree-sitter-rust.wasm +0 -0
  38. package/src/outline/vendor/wasm/tree-sitter-sass.wasm +0 -0
  39. package/src/outline/vendor/wasm/tree-sitter-scss.wasm +0 -0
  40. package/src/outline/vendor/wasm/tree-sitter-solidity.wasm +0 -0
  41. package/src/outline/vendor/wasm/tree-sitter-tsx.wasm +0 -0
  42. package/src/outline/vendor/wasm/tree-sitter-typescript.wasm +0 -0
  43. package/src/read.ts +118 -41
  44. package/src/replace-match.ts +42 -41
  45. package/src/replace-undo.ts +4 -4
  46. package/src/replace.ts +30 -19
  47. package/tool-prompts/read-guidelines.md +3 -0
  48. package/{prompts → tool-prompts}/read.md +2 -0
  49. package/prompts/read-guidelines.md +0 -1
  50. package/src/auto-read-all-state.ts +0 -27
  51. /package/{prompts → tool-prompts}/copy-guidelines.md +0 -0
  52. /package/{prompts → tool-prompts}/copy-snippet.md +0 -0
  53. /package/{prompts → tool-prompts}/copy.md +0 -0
  54. /package/{prompts → tool-prompts}/grep-guidelines.md +0 -0
  55. /package/{prompts → tool-prompts}/grep-snippet.md +0 -0
  56. /package/{prompts → tool-prompts}/grep.md +0 -0
  57. /package/{prompts → tool-prompts}/insert-guidelines.md +0 -0
  58. /package/{prompts → tool-prompts}/insert-snippet.md +0 -0
  59. /package/{prompts → tool-prompts}/insert.md +0 -0
  60. /package/{prompts → tool-prompts}/move-guidelines.md +0 -0
  61. /package/{prompts → tool-prompts}/move-snippet.md +0 -0
  62. /package/{prompts → tool-prompts}/move.md +0 -0
  63. /package/{prompts → tool-prompts}/read-snippet.md +0 -0
  64. /package/{prompts → tool-prompts}/replace-guidelines.md +0 -0
  65. /package/{prompts → tool-prompts}/replace-match-guidelines.md +0 -0
  66. /package/{prompts → tool-prompts}/replace-match-snippet.md +0 -0
  67. /package/{prompts → tool-prompts}/replace-match.md +0 -0
  68. /package/{prompts → tool-prompts}/replace-snippet.md +0 -0
  69. /package/{prompts → tool-prompts}/replace.md +0 -0
  70. /package/{prompts → tool-prompts}/transfer-guidelines.md +0 -0
  71. /package/{prompts → tool-prompts}/undo-last-change-guidelines.md +0 -0
  72. /package/{prompts → tool-prompts}/undo-last-change-snippet.md +0 -0
  73. /package/{prompts → tool-prompts}/undo-last-change.md +0 -0
package/src/read.ts CHANGED
@@ -1,6 +1,8 @@
1
+ import { realpathSync } from "node:fs";
1
2
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
3
  import {
3
4
  createReadTool,
5
+ createReadToolDefinition,
4
6
  formatSize,
5
7
  truncateHead,
6
8
  DEFAULT_MAX_BYTES,
@@ -11,23 +13,21 @@ import { Type } from "typebox";
11
13
  import { loadFileKindAndText } from "./file-kind";
12
14
  import { MAX_OVERSIZED_WARNING_LINES } from "./constants";
13
15
  import { readNormFile, safeSnapId } from "./file-reader";
14
- import { lineHashes, fmtRegion, fmtRow, HASH_SEP, MAX_HASH_LINES } from "./hashline";
16
+ import { lineHashes, fmtRegion, fmtRow, HASH_SEP, MAX_HASH_LINES, parseHashRef, resolveAnchorLine, AnchorMismatchError } from "./hashline";
15
17
  import { toCwd } from "./paths";
16
- import { abortIf, makePrepareArguments, numberedRead, visLines, splitLines } from "./utils";
18
+ import { abortIf, isRec, makePrepareArguments, numberedRead, visLines, splitLines } from "./utils";
17
19
  import { loadP, loadGuide } from "./prompts";
18
20
  import { withReadPrompts, DEFAULT_EDIT_FLAGS, type EditToolFlags } from "./edit-common";
19
21
  import { valAccess } from "./validation";
20
- import { readConfig } from "./config";
21
- import { resolveTarget } from "./fs-write";
22
- import { withAnchorSession, servedForPath, sessionKeyFor, formatAnchorReclaimNotice, takeReclaimedPaths } from "./anchor-registry";
22
+ import { withAnchorSession, adoptAnchors, ownerOf, ownersDifferingOnlyByCase, formatAnchorReclaimNotice, takeReclaimedPaths } from "./anchor-registry";
23
23
  import { serveRows } from "./served";
24
- import { getAutoReadAllSnapshot } from "./auto-read-all-state";
24
+ import { snapshotCache } from "./hash-store/cache";
25
25
  import { Text } from "@earendil-works/pi-tui";
26
26
  import { anchoredLine, readResultSchema, withStructuredErrors, type AnchoredLine, type ReadResult } from "./structured";
27
- const R_DESC = loadP("../prompts/read.md");
28
- const R_SNIPPET = loadP("../prompts/read-snippet.md");
27
+ const R_DESC = loadP("../tool-prompts/read.md");
28
+ const R_SNIPPET = loadP("../tool-prompts/read-snippet.md");
29
29
  function readGuide(): string[] {
30
- return loadGuide("../prompts/read-guidelines.md");
30
+ return loadGuide("../tool-prompts/read-guidelines.md");
31
31
  }
32
32
  function normPosInt(
33
33
  value: number | undefined,
@@ -49,10 +49,11 @@ export function formatPaginationHint(
49
49
  endLine: number,
50
50
  totalLines: number,
51
51
  nextOffset: number,
52
+ lastAnchor?: string,
52
53
  byteLimit?: number,
53
54
  ): string {
54
55
  const sizeSuffix = byteLimit !== undefined ? ` (${formatSize(byteLimit)} limit)` : "";
55
- return `[Showing lines ${startLine}-${endLine} of ${totalLines}${sizeSuffix}. Use offset=${nextOffset} to continue.]`;
56
+ return `[Showing lines ${startLine}-${endLine} of ${totalLines}${sizeSuffix}. Use offset=${lastAnchor ?? nextOffset} to continue.]`;
56
57
  }
57
58
 
58
59
  export async function fmtReadPreview(
@@ -147,7 +148,7 @@ export async function fmtReadPreview(
147
148
  let nextOffset: number | undefined;
148
149
  if (shownRowCount > 0 && (skippedTruncation.truncated || lastShownLine < totalLines)) {
149
150
  nextOffset = lastShownLine + 1;
150
- preview += `\n\n${warning}\n${formatPaginationHint(startLine, lastShownLine, totalLines, nextOffset, skippedTruncation.truncated ? skippedTruncation.maxBytes : undefined)}`;
151
+ preview += `\n\n${warning}\n${formatPaginationHint(startLine, lastShownLine, totalLines, nextOffset, servedHashes.at(-1), skippedTruncation.truncated ? skippedTruncation.maxBytes : undefined)}`;
151
152
  } else {
152
153
  preview += `\n\n${warning}`;
153
154
  }
@@ -175,13 +176,13 @@ export async function fmtReadPreview(
175
176
  const endLineDisplay = startLine + truncation.outputLines - 1;
176
177
  nextOffset = endLineDisplay + 1;
177
178
  if (truncation.truncatedBy === "lines") {
178
- preview += `\n\n${formatPaginationHint(startLine, endLineDisplay, totalLines, nextOffset)}`;
179
+ preview += `\n\n${formatPaginationHint(startLine, endLineDisplay, totalLines, nextOffset, servedHashes.at(-1))}`;
179
180
  } else {
180
- preview += `\n\n${formatPaginationHint(startLine, endLineDisplay, totalLines, nextOffset, truncation.maxBytes)}`;
181
+ preview += `\n\n${formatPaginationHint(startLine, endLineDisplay, totalLines, nextOffset, servedHashes.at(-1), truncation.maxBytes)}`;
181
182
  }
182
183
  } else if (endIdx < totalLines) {
183
184
  nextOffset = endIdx + 1;
184
- preview += `\n\n${formatPaginationHint(startLine, endIdx, totalLines, nextOffset)}`;
185
+ preview += `\n\n${formatPaginationHint(startLine, endIdx, totalLines, nextOffset, servedHashes.at(-1))}`;
185
186
  }
186
187
 
187
188
  return {
@@ -196,6 +197,94 @@ export async function fmtReadPreview(
196
197
  };
197
198
  }
198
199
 
200
+ export function resolveReadOffset(
201
+ offset: number | string | undefined,
202
+ fileLines: string[],
203
+ fileHashes: string[],
204
+ resolvedPath: string,
205
+ ): number | undefined {
206
+ if (offset === undefined || typeof offset === "number") return offset;
207
+ if (/^\d+$/.test(offset)) return Number(offset);
208
+ const ref = parseHashRef(offset);
209
+ const owner = ownerOf(ref.hash);
210
+ if (owner === undefined) {
211
+ const folded = ownersDifferingOnlyByCase(ref.hash);
212
+ const hint = folded.length > 0 ? ` Anchors are case-sensitive; ${folded.map((match) => `"${match.anchor}"`).join(", ")} differs only in case.` : "";
213
+ throw new Error(`[E_STALE_ANCHOR] "${ref.hash}" is not owned in this session.${hint} Use a line number, or read again for fresh anchors.`);
214
+ }
215
+ if (owner.path !== resolvedPath) {
216
+ throw new Error(`[E_STALE_ANCHOR] "${ref.hash}" is owned by ${owner.path}. Use a line number, or read ${owner.path} to use that anchor.`);
217
+ }
218
+ try {
219
+ return resolveAnchorLine(ref, fileLines, fileHashes, resolvedPath);
220
+ } catch (error) {
221
+ if (error instanceof AnchorMismatchError) adoptAnchors(resolvedPath, error.feedbackMap);
222
+ throw error;
223
+ }
224
+ }
225
+
226
+ const builtinReadRenderCall = createReadToolDefinition(process.cwd()).renderCall as
227
+ (args: unknown, theme: any, context: any) => Text;
228
+
229
+ function normalizeReadCallArgs(args: unknown): unknown {
230
+ if (!isRec(args)) return args;
231
+ const normalized: Record<string, unknown> = { ...args };
232
+ for (const key of ["offset", "limit"]) {
233
+ const value = normalized[key];
234
+ if (typeof value === "string" && /^\d+$/.test(value)) normalized[key] = Number(value);
235
+ }
236
+ return normalized;
237
+ }
238
+
239
+ function anchorRefOf(args: unknown): { anchor: string; limit: number | undefined; path: string | undefined } | undefined {
240
+ if (!isRec(args)) return undefined;
241
+ const { offset, limit, path } = args;
242
+ if (typeof offset !== "string" || offset.length === 0 || /^\d+$/.test(offset)) return undefined;
243
+ const validLimit = typeof limit === "number" && Number.isInteger(limit) && limit >= 1 ? limit : undefined;
244
+ if (limit !== undefined && validLimit === undefined) return undefined;
245
+ return { anchor: offset, limit: validLimit, path: typeof path === "string" ? path : undefined };
246
+ }
247
+
248
+ function cachedHashes(path: string): string[] | undefined {
249
+ const cached = snapshotCache.get(path);
250
+ if (cached !== undefined) return cached.hashes;
251
+ try {
252
+ return snapshotCache.get(realpathSync(path))?.hashes;
253
+ } catch {
254
+ return undefined;
255
+ }
256
+ }
257
+
258
+ function anchorLineOf(anchor: string, path: string | undefined, cwd: unknown): number | undefined {
259
+ if (path === undefined || typeof cwd !== "string") return undefined;
260
+ const hashes = cachedHashes(toCwd(path, cwd));
261
+ if (hashes === undefined) return undefined;
262
+ const index = hashes.indexOf(anchor);
263
+ return index < 0 ? undefined : index + 1;
264
+ }
265
+
266
+ function renderReadCall(args: unknown, theme: any, context: any): Text {
267
+ const normalized = normalizeReadCallArgs(args);
268
+ const rendered = builtinReadRenderCall(normalized, theme, context);
269
+ const ref = anchorRefOf(normalized);
270
+ if (ref === undefined) return rendered;
271
+ const marker = `:${ref.anchor}`;
272
+ const text = (rendered as unknown as { text: string }).text;
273
+ const index = text.lastIndexOf(marker);
274
+ if (index < 0) return rendered;
275
+ const line = anchorLineOf(ref.anchor, ref.path, context?.cwd);
276
+ if (line === undefined) {
277
+ if (ref.limit === undefined) return rendered;
278
+ const suffix = theme.fg("warning", ` +${ref.limit}`);
279
+ rendered.setText(`${text.slice(0, index + marker.length)}${suffix}${text.slice(index + marker.length)}`);
280
+ return rendered;
281
+ }
282
+ const range = ref.limit === undefined ? `${line}` : `${line}-${line + ref.limit - 1}`;
283
+ const replacement = theme.fg("warning", `:${range} (${ref.anchor})`);
284
+ rendered.setText(`${text.slice(0, index)}${replacement}${text.slice(index + marker.length)}`);
285
+ return rendered;
286
+ }
287
+
199
288
  export function regRead(pi: ExtensionAPI, flags: EditToolFlags = DEFAULT_EDIT_FLAGS): void {
200
289
  const prompted = withReadPrompts({ description: R_DESC, snippet: R_SNIPPET, guidelines: readGuide() }, flags);
201
290
  pi.registerTool({
@@ -210,20 +299,26 @@ export function regRead(pi: ExtensionAPI, flags: EditToolFlags = DEFAULT_EDIT_FL
210
299
  description: "Path to the file to read (relative or absolute)",
211
300
  }),
212
301
  offset: Type.Optional(
213
- Type.Integer({
214
- minimum: 1,
215
- description: "Line number to start reading from (1-indexed)",
216
- }),
302
+ Type.Union([
303
+ Type.Integer({
304
+ minimum: 1,
305
+ description: "Line number to start reading from (1-indexed)",
306
+ }),
307
+ Type.String({
308
+ description: "4-char anchor of a served line to start reading from",
309
+ }),
310
+ ], { description: "Line number (1-indexed) or a served anchor to start reading from" }),
217
311
  ),
218
312
  limit: Type.Optional(
219
313
  Type.Integer({
220
314
  minimum: 1,
221
- description: "Maximum number of lines to read",
315
+ description: "Maximum number of lines to read from offset",
222
316
  }),
223
317
  ),
224
318
  }),
225
319
  outputSchema: readResultSchema,
226
320
  executionMode: "sequential",
321
+ renderCall: renderReadCall,
227
322
  renderResult(result, { isPartial, expanded }, theme, context) {
228
323
  if (isPartial) return new Text((theme as unknown as { fg: (a:string,b:string)=>string }).fg("warning", "Reading..."), 0, 0);
229
324
  const raw = (result.content?.[0] as { text?: string } | undefined)?.text;
@@ -243,22 +338,6 @@ export function regRead(pi: ExtensionAPI, flags: EditToolFlags = DEFAULT_EDIT_FL
243
338
 
244
339
  abortIf(signal);
245
340
  await valAccess(absolutePath, rawPath);
246
- const autoReadAllMode = (await readConfig()).autoReadAll ?? "off";
247
- if (autoReadAllMode !== "off") {
248
- const canonical = await resolveTarget(absolutePath).catch(() => undefined);
249
- if (canonical !== undefined) {
250
- const stored = getAutoReadAllSnapshot(sessionKeyFor(ctx), canonical);
251
- if (stored !== undefined) {
252
- const current = await safeSnapId(canonical, "auto-read-all guard");
253
- if (current !== undefined && current === stored) {
254
- const served = servedForPath(canonical);
255
- if (served !== undefined && served.size > 0) {
256
- throw new Error(`[E_AUTO_READ_ALL] ${rawPath} is unchanged since this session's start-of-session auto-read, so the attached content is still exact. Read succeeds on files that have changed since the full auto read.`);
257
- }
258
- }
259
- }
260
- }
261
- }
262
341
 
263
342
  abortIf(signal);
264
343
  const file = await loadFileKindAndText(absolutePath, { maxLines: MAX_HASH_LINES, displayPath: rawPath });
@@ -279,12 +358,10 @@ export function regRead(pi: ExtensionAPI, flags: EditToolFlags = DEFAULT_EDIT_FL
279
358
  rawPath, ctx.cwd, { signal, preloadedFile: file, maxLines: MAX_HASH_LINES },
280
359
  );
281
360
  const fileLines = splitLines(normalized);
361
+ const offset = resolveReadOffset(params.offset, fileLines, fileHashes, resolvedPath);
282
362
  const preview = await fmtReadPreview(
283
363
  normalized,
284
- {
285
- offset: params.offset,
286
- limit: params.limit,
287
- },
364
+ { offset, limit: params.limit },
288
365
  fileHashes,
289
366
  resolvedPath,
290
367
  );
@@ -315,12 +392,12 @@ export function regRead(pi: ExtensionAPI, flags: EditToolFlags = DEFAULT_EDIT_FL
315
392
  details: {
316
393
  truncation: preview.truncation,
317
394
  snapshotId,
318
- offset: params.offset ?? 1,
395
+ offset: preview.startLine,
319
396
  ...(preview.nextOffset !== undefined
320
397
  ? { nextOffset: preview.nextOffset }
321
398
  : {}),
322
399
  metrics: {
323
- truncated: !!preview.truncation,
400
+ truncated: preview.truncation !== undefined,
324
401
  ...(preview.nextOffset !== undefined
325
402
  ? { next_offset: preview.nextOffset }
326
403
  : {}),
@@ -3,9 +3,9 @@ import { constants } from "node:fs";
3
3
  import { execPipeline, noteAnchorError, previewFromPipe, previewError, type ReplaceDetails } from "./replace";
4
4
  import { commitEdit } from "./commit";
5
5
  import { readNormFile, type NormFile } from "./file-reader";
6
- import { fmtRegion, MAX_HASH_LINES, parseHashRef, resEdit, resolveAnchorLine, stripAnchorRow, type Anchor, type HEdit } from "./hashline";
6
+ import { fmtRegion, MAX_HASH_LINES, parseHashRef, planEdit, preserveDeletionSeparators, resEdit, resolveAnchorLine, stripAnchorRow, type Anchor } from "./hashline";
7
7
  import { servedForPath, withAnchorSession } from "./anchor-registry";
8
- import { batchMemberFor, batchServedFor, ensureBatchBase, executeBatchMember, noteBatchFailure } from "./batch";
8
+ import { batchMemberFor, executeBatchMember, noteBatchFailure, registerBatchPlanner, type BatchMemberPlan, type BatchPlannerInput } from "./batch";
9
9
  import { loadP, loadGuide } from "./prompts";
10
10
  import {
11
11
  assertReplaceMatchReq,
@@ -109,6 +109,33 @@ export function buildReplaceMatchEdit(
109
109
  };
110
110
  }
111
111
 
112
+ function planReplaceMatchBatchMember(input: BatchPlannerInput): BatchMemberPlan {
113
+ const req = normReq(input.member.args, "replace");
114
+ assertReplaceMatchReq(req);
115
+ const { refs, warnings } = parseMatchAnchors(req);
116
+ const hints = [...literalEscapeHints([req.old_string], "old_string"), ...literalEscapeHints([req.new_string], "new_string")];
117
+ const preload = { normalized: input.base.content, fileHashes: input.base.hashes } as NormFile;
118
+ const matchPlan = buildReplaceMatchEdit(req, refs, preload, input.displayPath, input.served);
119
+ const resWarnings: string[] = [];
120
+ const hedit = resEdit(matchPlan.editParams, resWarnings);
121
+ const effectiveHedit = preserveDeletionSeparators(hedit, input.base.baseLines, input.base.hashes);
122
+ const planned = planEdit(input.base.content, effectiveHedit, input.base.hashes, {
123
+ filePath: input.displayPath,
124
+ servedHashes: matchPlan.servedOverride ?? input.served,
125
+ signal: input.signal,
126
+ baseFileLines: input.base.baseLines,
127
+ });
128
+ return {
129
+ hedit,
130
+ warnings: [...warnings, ...hints, ...resWarnings],
131
+ planned,
132
+ foldedLines: 0,
133
+ ...(matchPlan.servedOverride !== undefined ? { servedOverride: matchPlan.servedOverride } : {}),
134
+ };
135
+ }
136
+
137
+ registerBatchPlanner("replace_match", planReplaceMatchBatchMember);
138
+
112
139
  export async function replaceMatchPreview(request: unknown, cwd: string, signal?: AbortSignal): Promise<RPreview> {
113
140
  try {
114
141
  const normalized = normReq(request, "replace");
@@ -148,9 +175,9 @@ type ReplaceMatchToolDef = ToolDefinition<any, ReplaceDetails, RRState> & { rend
148
175
 
149
176
  export function buildReplaceMatchToolDef(flags: EditToolFlags = DEFAULT_EDIT_FLAGS): ReplaceMatchToolDef {
150
177
  const prompted = withReplaceMatchPrompts({
151
- description: loadP("../prompts/replace-match.md"),
152
- snippet: loadP("../prompts/replace-match-snippet.md"),
153
- guidelines: loadGuide("../prompts/replace-match-guidelines.md"),
178
+ description: loadP("../tool-prompts/replace-match.md"),
179
+ snippet: loadP("../tool-prompts/replace-match-snippet.md"),
180
+ guidelines: loadGuide("../tool-prompts/replace-match-guidelines.md"),
154
181
  }, flags);
155
182
  return {
156
183
  name: "replace_match",
@@ -173,13 +200,13 @@ export function buildReplaceMatchToolDef(flags: EditToolFlags = DEFAULT_EDIT_FLA
173
200
  const normalized = normReq(params, "replace");
174
201
  assertReplaceMatchReq(normalized);
175
202
  const req = normalized;
203
+ const member = batchMemberFor(_toolCallId);
176
204
  let refs: MatchRefs;
177
205
  let warnings: string[];
178
206
  try {
179
207
  ({ refs, warnings } = parseMatchAnchors(req));
180
- await throwIfStrictInput(warnings);
208
+ if (member === undefined) await throwIfStrictInput(warnings);
181
209
  } catch (error) {
182
- const member = batchMemberFor(_toolCallId);
183
210
  if (member) noteBatchFailure(member, error);
184
211
  throw error;
185
212
  }
@@ -189,53 +216,27 @@ export function buildReplaceMatchToolDef(flags: EditToolFlags = DEFAULT_EDIT_FLA
189
216
  removeTo: req.replace_to,
190
217
  providedPath: req.path,
191
218
  cwd: ctx.cwd,
219
+ anchorTarget: member?.target,
192
220
  }).catch((error: unknown) => {
193
- const member = batchMemberFor(_toolCallId);
194
221
  if (member) noteBatchFailure(member, error);
195
222
  throw error;
196
223
  });
197
224
  return queuedEdit(targetPath, ctx.cwd, signal, async (absolutePath, mutationTargetPath) => {
198
- const member = batchMemberFor(_toolCallId);
199
- let preload: NormFile;
200
225
  if (member) {
201
- const base = await ensureBatchBase({ member, targetPath, mutationTargetPath, cwd: ctx.cwd, signal });
202
- preload = { normalized: base.content, fileHashes: base.hashes } as NormFile;
203
- } else {
204
- preload = await readNormFile(targetPath, ctx.cwd, {
205
- signal,
206
- accessMode: constants.R_OK | constants.W_OK,
207
- maxLines: MAX_HASH_LINES,
208
- });
226
+ return executeBatchMember({ member, targetPath, mutationTargetPath, cwd: ctx.cwd, signal });
209
227
  }
228
+ const preload = await readNormFile(targetPath, ctx.cwd, {
229
+ signal,
230
+ accessMode: constants.R_OK | constants.W_OK,
231
+ maxLines: MAX_HASH_LINES,
232
+ });
210
233
  let plan: MatchPlan;
211
234
  try {
212
- plan = buildReplaceMatchEdit(req, refs, preload, targetPath, member ? batchServedFor(member) : servedForPath(preload.absolutePath));
235
+ plan = buildReplaceMatchEdit(req, refs, preload, targetPath, servedForPath(preload.absolutePath));
213
236
  } catch (error) {
214
237
  await noteAnchorError(mutationTargetPath, error);
215
- if (member) noteBatchFailure(member, error);
216
238
  throw error;
217
239
  }
218
- if (member) {
219
- let hedit: HEdit;
220
- const resWarnings: string[] = [];
221
- try {
222
- hedit = resEdit(plan.editParams, resWarnings);
223
- } catch (error) {
224
- noteBatchFailure(member, error);
225
- throw error;
226
- }
227
- return executeBatchMember({
228
- kind: "replace_match",
229
- member,
230
- targetPath,
231
- mutationTargetPath,
232
- cwd: ctx.cwd,
233
- signal,
234
- hedit,
235
- extraWarnings: [...warnings, ...hints, ...resWarnings],
236
- ...(plan.servedOverride !== undefined ? { servedOverride: plan.servedOverride } : {}),
237
- });
238
- }
239
240
  const pipe = await execPipeline(targetPath, plan.editParams, ctx.cwd, {
240
241
  accessMode: constants.R_OK | constants.W_OK,
241
242
  signal,
@@ -113,9 +113,9 @@ function fallbackFileMode(): number {
113
113
 
114
114
  export function regUndo(pi: ExtensionAPI, flags: EditToolFlags = DEFAULT_EDIT_FLAGS): void {
115
115
  const prompted = withUndoPrompts({
116
- description: loadP("../prompts/undo-last-change.md"),
117
- snippet: loadP("../prompts/undo-last-change-snippet.md"),
118
- guidelines: loadGuide("../prompts/undo-last-change-guidelines.md"),
116
+ description: loadP("../tool-prompts/undo-last-change.md"),
117
+ snippet: loadP("../tool-prompts/undo-last-change-snippet.md"),
118
+ guidelines: loadGuide("../tool-prompts/undo-last-change-guidelines.md"),
119
119
  }, flags);
120
120
  pi.registerTool({
121
121
  name: "undo_last_change",
@@ -184,7 +184,7 @@ export function regUndo(pi: ExtensionAPI, flags: EditToolFlags = DEFAULT_EDIT_FL
184
184
  );
185
185
 
186
186
  const currentNormalized = currentRaw === undefined ? "" : toLF(stripBOM(currentRaw).text);
187
- const currentHashes = await lineHashes(currentNormalized, mutationTargetPath);
187
+ const currentHashes = await lineHashes(currentNormalized, mutationTargetPath, undefined, undefined, false, true);
188
188
  const diffResult = genDiff(undo.content, undo.resultContent, 0, undefined, undo.hashes, { unlimited: true });
189
189
  const linesAddedByReplace = cntDiff(diffResult.diff, "+");
190
190
  const linesRemovedByReplace = cntDiff(diffResult.diff, "-");
package/src/replace.ts CHANGED
@@ -16,6 +16,7 @@ import { loadP, loadGuide } from "./prompts";
16
16
  import { type FileIdentity } from "./fs-write";
17
17
  import { applyEdit,
18
18
  lineHashes,
19
+ planEdit,
19
20
  resEdit,
20
21
  preserveDeletionSeparators,
21
22
  MAX_HASH_LINES,
@@ -36,7 +37,7 @@ import { resolveTarget } from "./fs-write";
36
37
  import { toCwd, toDisplayPath } from "./paths";
37
38
  import { queuedEdit, editToolBase, editRenderCallWrapper, editRenderResultWrapper, resolveEditTargetWithRequirement, throwIfStrictInput, withReplacePrompts, DEFAULT_EDIT_FLAGS, type EditToolFlags } from "./edit-common";
38
39
  import { commitEdit } from "./commit";
39
- import { batchMemberFor, executeBatchMember, noteBatchFailure } from "./batch";
40
+ import { batchMemberFor, executeBatchMember, noteBatchFailure, registerBatchPlanner, type BatchMemberPlan, type BatchPlannerInput } from "./batch";
40
41
 
41
42
  export { editToolSchema, type ReqParams, assertReq };
42
43
 
@@ -126,12 +127,32 @@ function countLineChanges(
126
127
  export function buildReplaceHEdit(params: RawReqParams): { edit: HEdit; warnings: string[] } {
127
128
  const editWarnings: string[] = [];
128
129
  const anchors = { remove_from: params.remove_from, remove_to: params.remove_to };
129
- const edit = typeof params.text === "string"
130
- ? resEdit({ ...anchors, text: params.text }, editWarnings)
131
- : resEdit({ ...anchors, text: params.text }, editWarnings);
130
+ const edit = resEdit({ ...anchors, text: params.text }, editWarnings);
132
131
  return { edit, warnings: editWarnings };
133
132
  }
134
133
 
134
+ function planReplaceBatchMember(input: BatchPlannerInput): BatchMemberPlan {
135
+ const req = normReq(input.member.args, "remove");
136
+ assertReq(req);
137
+ const literalEscapes = literalEscapeHints([req.text], "text");
138
+ const built = buildReplaceHEdit(req);
139
+ const effectiveHedit = preserveDeletionSeparators(built.edit, input.base.baseLines, input.base.hashes);
140
+ const planned = planEdit(input.base.content, effectiveHedit, input.base.hashes, {
141
+ filePath: input.displayPath,
142
+ servedHashes: input.served,
143
+ signal: input.signal,
144
+ baseFileLines: input.base.baseLines,
145
+ });
146
+ return {
147
+ hedit: built.edit,
148
+ warnings: [...literalEscapes, ...built.warnings],
149
+ planned,
150
+ foldedLines: 0,
151
+ };
152
+ }
153
+
154
+ registerBatchPlanner("replace", planReplaceBatchMember);
155
+
135
156
  function withEndingOverrides(edit: HEdit, overrides: (LineEnding | undefined)[] | undefined): HEdit {
136
157
  if (overrides === undefined) return edit;
137
158
  const length = Math.max(edit.content_separators?.length ?? 0, overrides.length);
@@ -269,9 +290,9 @@ type ToolDef = ToolDefinition<
269
290
 
270
291
  export function buildToolDef(flags: EditToolFlags = DEFAULT_EDIT_FLAGS): ToolDef {
271
292
  const prompted = withReplacePrompts({
272
- description: loadP("../prompts/replace.md"),
273
- snippet: loadP("../prompts/replace-snippet.md"),
274
- guidelines: loadGuide("../prompts/replace-guidelines.md"),
293
+ description: loadP("../tool-prompts/replace.md"),
294
+ snippet: loadP("../tool-prompts/replace-snippet.md"),
295
+ guidelines: loadGuide("../tool-prompts/replace-guidelines.md"),
275
296
  }, flags);
276
297
  const parameters = buildEditToolSchema(flags.requirePath);
277
298
  return {
@@ -292,18 +313,18 @@ export function buildToolDef(flags: EditToolFlags = DEFAULT_EDIT_FLAGS): ToolDef
292
313
  assertReq(canonical);
293
314
  const normalizedParams = canonical;
294
315
  const literalEscapes = literalEscapeHints([normalizedParams.text], "text");
316
+ const member = batchMemberFor(_toolCallId);
295
317
  const targetPath = await resolveEditTargetWithRequirement({
296
318
  removeFrom: normalizedParams.remove_from,
297
319
  removeTo: normalizedParams.remove_to,
298
320
  providedPath: normalizedParams.path,
299
321
  cwd: ctx.cwd,
322
+ anchorTarget: member?.target,
300
323
  }).catch((error: unknown) => {
301
- const member = batchMemberFor(_toolCallId);
302
324
  if (member) noteBatchFailure(member, error);
303
325
  throw error;
304
326
  });
305
327
  return queuedEdit(targetPath, ctx.cwd, signal, async (absolutePath, mutationTargetPath) => {
306
- const member = batchMemberFor(_toolCallId);
307
328
  if (!member) {
308
329
  const pipe = await execPipeline(
309
330
  targetPath,
@@ -320,22 +341,12 @@ export function buildToolDef(flags: EditToolFlags = DEFAULT_EDIT_FLAGS): ToolDef
320
341
  signal,
321
342
  });
322
343
  }
323
- let built: { edit: HEdit; warnings: string[] };
324
- try {
325
- built = buildReplaceHEdit(normalizedParams);
326
- } catch (error) {
327
- noteBatchFailure(member, error);
328
- throw error;
329
- }
330
344
  return executeBatchMember({
331
- kind: "replace",
332
345
  member,
333
346
  targetPath,
334
347
  mutationTargetPath,
335
348
  cwd: ctx.cwd,
336
349
  signal,
337
- hedit: built.edit,
338
- extraWarnings: [...literalEscapes, ...built.warnings],
339
350
  });
340
351
  });
341
352
  }));
@@ -0,0 +1,3 @@
1
+ - `read`: call again after an edit when you need anchors you lack — post-edit diff `+anchor│`/` anchor│` rows and any served `anchor│content` rows already carry fresh anchors for the changed range.
2
+ - `read`: pass a served anchor as `offset` with `limit` to read from it without line-number math; an outline row's `[limit N]` is the span to request, and a row without one is a single line shown as its content.
3
+ - `read`: an outline row shows a symbol label, or the line content for a single-line symbol; edit by anchor, and read the anchor (or copy `old_string` from a served row) before `replace_match`.
@@ -1 +1,3 @@
1
1
  Read a text file and return it as `anchor│content` rows, one per line: a 4-character anchor (letters only, case-sensitive), the `│` separator, then the line content. Target lines in edits by anchor, never by content or line number. Page long files with `offset` and `limit`; when the output says truncated, continue with the hinted `offset/limit`. Images attach visually; binary, directory, and UTF-16/UTF-32 text are rejected; an empty file returns one empty row you can replace to seed content.
2
+
3
+ `offset` takes a 1-indexed line number or a served anchor; with an anchor, `limit` counts the lines to return after it. A stale anchor is refused with the current context rows and fresh anchors.
@@ -1 +0,0 @@
1
- - `read`: call again after an edit when you need anchors you lack — post-edit diff `+anchor│`/` anchor│` rows and any served `anchor│content` rows already carry fresh anchors for the changed range.
@@ -1,27 +0,0 @@
1
- const completeSnapshots = new Map<string, Map<string, string>>();
2
-
3
- function scopeFor(sessionKey: string | undefined): string {
4
- return sessionKey ?? "";
5
- }
6
-
7
- export function recordAutoReadAllComplete(sessionKey: string | undefined, absolutePath: string, snapshotId: string): void {
8
- const scope = scopeFor(sessionKey);
9
- let snapshots = completeSnapshots.get(scope);
10
- if (!snapshots) {
11
- snapshots = new Map();
12
- completeSnapshots.set(scope, snapshots);
13
- }
14
- snapshots.set(absolutePath, snapshotId);
15
- }
16
-
17
- export function getAutoReadAllSnapshot(sessionKey: string | undefined, absolutePath: string): string | undefined {
18
- return completeSnapshots.get(scopeFor(sessionKey))?.get(absolutePath);
19
- }
20
-
21
- export function clearAutoReadAllComplete(sessionKey: string | undefined): void {
22
- completeSnapshots.delete(scopeFor(sessionKey));
23
- }
24
-
25
- export function clearAllAutoReadAllComplete(): void {
26
- completeSnapshots.clear();
27
- }
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes