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.
- package/README.md +41 -23
- package/index.ts +20 -12
- package/package.json +3 -2
- package/src/anchor-registry.ts +22 -5
- package/src/auto-read-all.ts +39 -21
- package/src/batch.ts +127 -41
- package/src/commit.ts +1 -1
- package/src/config-ui.ts +135 -55
- package/src/config.ts +27 -9
- package/src/copy-move.ts +61 -72
- package/src/edit-common.ts +6 -4
- package/src/grep.ts +53 -121
- package/src/hash-store.ts +13 -12
- package/src/hashline/resolve.ts +13 -2
- package/src/insert.ts +40 -34
- package/src/line-endings.ts +10 -1
- package/src/outline/index.ts +46 -0
- package/src/outline/parsers.ts +42 -0
- package/src/outline/render.ts +170 -0
- package/src/outline/vendor/LICENSE +201 -0
- package/src/outline/vendor/README.md +17 -0
- package/src/outline/vendor/parsers/blade.ts +138 -0
- package/src/outline/vendor/parsers/index.ts +75 -0
- package/src/outline/vendor/parsers/java.ts +501 -0
- package/src/outline/vendor/parsers/manager.ts +1721 -0
- package/src/outline/vendor/parsers/php.ts +332 -0
- package/src/outline/vendor/parsers/sass.ts +416 -0
- package/src/outline/vendor/parsers/scss.ts +566 -0
- package/src/outline/vendor/types.ts +82 -0
- package/src/outline/vendor/wasm/LICENSES.md +37 -0
- package/src/outline/vendor/wasm/tree-sitter-blade.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-go.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-java.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-javascript.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-php.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-python.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-rust.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-sass.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-scss.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-solidity.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-tsx.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-typescript.wasm +0 -0
- package/src/read.ts +118 -41
- package/src/replace-match.ts +42 -41
- package/src/replace-undo.ts +4 -4
- package/src/replace.ts +30 -19
- package/tool-prompts/read-guidelines.md +3 -0
- package/{prompts → tool-prompts}/read.md +2 -0
- package/prompts/read-guidelines.md +0 -1
- package/src/auto-read-all-state.ts +0 -27
- /package/{prompts → tool-prompts}/copy-guidelines.md +0 -0
- /package/{prompts → tool-prompts}/copy-snippet.md +0 -0
- /package/{prompts → tool-prompts}/copy.md +0 -0
- /package/{prompts → tool-prompts}/grep-guidelines.md +0 -0
- /package/{prompts → tool-prompts}/grep-snippet.md +0 -0
- /package/{prompts → tool-prompts}/grep.md +0 -0
- /package/{prompts → tool-prompts}/insert-guidelines.md +0 -0
- /package/{prompts → tool-prompts}/insert-snippet.md +0 -0
- /package/{prompts → tool-prompts}/insert.md +0 -0
- /package/{prompts → tool-prompts}/move-guidelines.md +0 -0
- /package/{prompts → tool-prompts}/move-snippet.md +0 -0
- /package/{prompts → tool-prompts}/move.md +0 -0
- /package/{prompts → tool-prompts}/read-snippet.md +0 -0
- /package/{prompts → tool-prompts}/replace-guidelines.md +0 -0
- /package/{prompts → tool-prompts}/replace-match-guidelines.md +0 -0
- /package/{prompts → tool-prompts}/replace-match-snippet.md +0 -0
- /package/{prompts → tool-prompts}/replace-match.md +0 -0
- /package/{prompts → tool-prompts}/replace-snippet.md +0 -0
- /package/{prompts → tool-prompts}/replace.md +0 -0
- /package/{prompts → tool-prompts}/transfer-guidelines.md +0 -0
- /package/{prompts → tool-prompts}/undo-last-change-guidelines.md +0 -0
- /package/{prompts → tool-prompts}/undo-last-change-snippet.md +0 -0
- /package/{prompts → tool-prompts}/undo-last-change.md +0 -0
|
Binary file
|
|
Binary file
|
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 {
|
|
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 {
|
|
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.
|
|
214
|
-
|
|
215
|
-
|
|
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:
|
|
395
|
+
offset: preview.startLine,
|
|
319
396
|
...(preview.nextOffset !== undefined
|
|
320
397
|
? { nextOffset: preview.nextOffset }
|
|
321
398
|
: {}),
|
|
322
399
|
metrics: {
|
|
323
|
-
truncated:
|
|
400
|
+
truncated: preview.truncation !== undefined,
|
|
324
401
|
...(preview.nextOffset !== undefined
|
|
325
402
|
? { next_offset: preview.nextOffset }
|
|
326
403
|
: {}),
|
package/src/replace-match.ts
CHANGED
|
@@ -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
|
|
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,
|
|
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
|
-
|
|
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,
|
|
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,
|
package/src/replace-undo.ts
CHANGED
|
@@ -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 =
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|