pi-hashline-edit-pro 0.4.3 → 0.6.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 +19 -20
- package/index.ts +9 -3
- package/package.json +2 -2
- package/prompts/read-guidelines.md +2 -3
- package/prompts/read-snippet.md +1 -1
- package/prompts/read.md +7 -21
- package/prompts/replace-guidelines.md +3 -0
- package/prompts/replace-snippet.md +1 -0
- package/prompts/replace.md +69 -0
- package/src/hashline/apply.ts +59 -223
- package/src/hashline/hash.ts +7 -22
- package/src/hashline/resolve.ts +75 -192
- package/src/read.ts +5 -6
- package/src/{edit-normalize.ts → replace-normalize.ts} +1 -1
- package/src/{edit-render.ts → replace-render.ts} +16 -16
- package/src/{edit-response.ts → replace-response.ts} +8 -8
- package/src/{edit.ts → replace.ts} +36 -49
- package/prompts/edit-guidelines.md +0 -3
- package/prompts/edit-snippet.md +0 -1
- package/prompts/edit.md +0 -58
- /package/src/{edit-diff.ts → replace-diff.ts} +0 -0
|
@@ -14,8 +14,8 @@ import {
|
|
|
14
14
|
normalizeToLF,
|
|
15
15
|
restoreLineEndings,
|
|
16
16
|
stripBom,
|
|
17
|
-
} from "./
|
|
18
|
-
import {
|
|
17
|
+
} from "./replace-diff";
|
|
18
|
+
import { normalizeReplaceRequest } from "./replace-normalize";
|
|
19
19
|
import { isRecord, hasOwn } from "./utils";
|
|
20
20
|
import { resolveMutationTargetPath, writeFileAtomically } from "./fs-write";
|
|
21
21
|
import {
|
|
@@ -33,9 +33,9 @@ import {
|
|
|
33
33
|
buildFullResponse,
|
|
34
34
|
buildNoopResponse,
|
|
35
35
|
buildRangesResponse,
|
|
36
|
-
type
|
|
36
|
+
type ReplaceMeta,
|
|
37
37
|
type ReturnMode,
|
|
38
|
-
} from "./
|
|
38
|
+
} from "./replace-response";
|
|
39
39
|
import {
|
|
40
40
|
buildAppliedChangedResultText,
|
|
41
41
|
createRenderedEditMarkdownTheme,
|
|
@@ -44,9 +44,9 @@ import {
|
|
|
44
44
|
getRenderablePreviewInput,
|
|
45
45
|
getRenderedEditTextContent,
|
|
46
46
|
isAppliedChangedResult,
|
|
47
|
-
type
|
|
48
|
-
type
|
|
49
|
-
} from "./
|
|
47
|
+
type ReplacePreview,
|
|
48
|
+
type ReplaceRenderState,
|
|
49
|
+
} from "./replace-render";
|
|
50
50
|
|
|
51
51
|
function stringEnumSchema<const Values extends readonly string[]>(
|
|
52
52
|
values: Values,
|
|
@@ -82,29 +82,16 @@ const returnRangeSchema = Type.Object(
|
|
|
82
82
|
|
|
83
83
|
const hashlineEditItemSchema = Type.Object(
|
|
84
84
|
{
|
|
85
|
-
op: stringEnumSchema(
|
|
86
|
-
["replace", "append", "prepend"] as const,
|
|
87
|
-
{
|
|
88
|
-
description:
|
|
89
|
-
'edit operation. "replace" requires "start" + "end" (inclusive range); "append"/"prepend" take an optional "pos" anchor. Every edit must set op. The legacy "replace_text" op is no longer supported; use a hash-anchored "replace" instead.',
|
|
90
|
-
},
|
|
91
|
-
),
|
|
92
85
|
start: Type.Optional(
|
|
93
86
|
Type.String({
|
|
94
87
|
description:
|
|
95
|
-
"required range-start anchor
|
|
88
|
+
"required range-start anchor (hash anchor like \"aB3x\" copied from read output); no content may follow the anchor",
|
|
96
89
|
}),
|
|
97
90
|
),
|
|
98
91
|
end: Type.Optional(
|
|
99
92
|
Type.String({
|
|
100
93
|
description:
|
|
101
|
-
"required range-end anchor
|
|
102
|
-
}),
|
|
103
|
-
),
|
|
104
|
-
pos: Type.Optional(
|
|
105
|
-
Type.String({
|
|
106
|
-
description:
|
|
107
|
-
"anchor for op \"append\" or \"prepend\" (hash anchor like \"aB3x\"). Omit for file-boundary insertion (EOF/BOF).",
|
|
94
|
+
"required range-end anchor (hash anchor like \"aB3x\"). To replace a single line, set start = end = the line's anchor",
|
|
108
95
|
}),
|
|
109
96
|
),
|
|
110
97
|
lines: Type.Optional(hashlineEditLinesSchema),
|
|
@@ -149,14 +136,14 @@ type FullContentPreview = {
|
|
|
149
136
|
nextOffset?: number;
|
|
150
137
|
};
|
|
151
138
|
|
|
152
|
-
export type
|
|
139
|
+
export type ReplaceRequestParams = {
|
|
153
140
|
path: string;
|
|
154
141
|
returnMode?: "changed" | "full" | "ranges";
|
|
155
142
|
returnRanges?: ReturnRange[];
|
|
156
143
|
edits?: HashlineToolEdit[];
|
|
157
144
|
};
|
|
158
145
|
|
|
159
|
-
type
|
|
146
|
+
type ReplaceMetrics = {
|
|
160
147
|
edits_attempted: number;
|
|
161
148
|
edits_noop: number;
|
|
162
149
|
warnings: number;
|
|
@@ -167,7 +154,7 @@ type EditMetrics = {
|
|
|
167
154
|
removed_lines?: number;
|
|
168
155
|
};
|
|
169
156
|
|
|
170
|
-
export type
|
|
157
|
+
export type HashlineReplaceToolDetails = {
|
|
171
158
|
diff: string;
|
|
172
159
|
firstChangedLine?: number;
|
|
173
160
|
/**
|
|
@@ -185,22 +172,22 @@ export type HashlineEditToolDetails = {
|
|
|
185
172
|
* Phase 2 C — opt-in observability surface for hosts. Never echoed in text.
|
|
186
173
|
* Hosts can use it for adoption/regression dashboards.
|
|
187
174
|
*/
|
|
188
|
-
metrics?:
|
|
175
|
+
metrics?: ReplaceMetrics;
|
|
189
176
|
};
|
|
190
177
|
|
|
191
178
|
const EDIT_DESC = readFileSync(
|
|
192
|
-
new URL("../prompts/
|
|
179
|
+
new URL("../prompts/replace.md", import.meta.url),
|
|
193
180
|
"utf-8",
|
|
194
181
|
).trim();
|
|
195
182
|
|
|
196
183
|
const EDIT_PROMPT_SNIPPET = readFileSync(
|
|
197
|
-
new URL("../prompts/
|
|
184
|
+
new URL("../prompts/replace-snippet.md", import.meta.url),
|
|
198
185
|
"utf-8",
|
|
199
186
|
).trim();
|
|
200
187
|
|
|
201
188
|
|
|
202
189
|
const EDIT_PROMPT_GUIDELINES = readFileSync(
|
|
203
|
-
new URL("../prompts/
|
|
190
|
+
new URL("../prompts/replace-guidelines.md", import.meta.url),
|
|
204
191
|
"utf-8",
|
|
205
192
|
)
|
|
206
193
|
.split("\n")
|
|
@@ -209,9 +196,9 @@ const EDIT_PROMPT_GUIDELINES = readFileSync(
|
|
|
209
196
|
.map((line) => line.slice(2));
|
|
210
197
|
const ROOT_KEYS = new Set(["path", "returnMode", "returnRanges", "edits"]);
|
|
211
198
|
|
|
212
|
-
export function
|
|
199
|
+
export function assertReplaceRequest(
|
|
213
200
|
request: unknown,
|
|
214
|
-
): asserts request is
|
|
201
|
+
): asserts request is ReplaceRequestParams {
|
|
215
202
|
if (!isRecord(request)) {
|
|
216
203
|
throw new Error("[E_BAD_SHAPE] Edit request must be an object.");
|
|
217
204
|
}
|
|
@@ -219,7 +206,7 @@ export function assertEditRequest(
|
|
|
219
206
|
for (const legacyKey of ["oldText", "newText", "old_text", "new_text"]) {
|
|
220
207
|
if (hasOwn(request, legacyKey)) {
|
|
221
208
|
throw new Error(
|
|
222
|
-
`[E_LEGACY_SHAPE] "${legacyKey}" is not supported. Use {
|
|
209
|
+
`[E_LEGACY_SHAPE] "${legacyKey}" is not supported. Use {start:"<HASH>", end:"<HASH>", lines:[...]}.`
|
|
223
210
|
);
|
|
224
211
|
}
|
|
225
212
|
}
|
|
@@ -320,8 +307,8 @@ async function executeEditPipeline(
|
|
|
320
307
|
lastChangedLine?: number;
|
|
321
308
|
originalHashes?: string[];
|
|
322
309
|
}> {
|
|
323
|
-
const normalized =
|
|
324
|
-
|
|
310
|
+
const normalized = normalizeReplaceRequest(request);
|
|
311
|
+
assertReplaceRequest(normalized);
|
|
325
312
|
|
|
326
313
|
const params = normalized;
|
|
327
314
|
const path = params.path;
|
|
@@ -400,10 +387,10 @@ async function executeEditPipeline(
|
|
|
400
387
|
};
|
|
401
388
|
}
|
|
402
389
|
|
|
403
|
-
export async function
|
|
390
|
+
export async function computeReplacePreview(
|
|
404
391
|
request: unknown,
|
|
405
392
|
cwd: string,
|
|
406
|
-
): Promise<
|
|
393
|
+
): Promise<ReplacePreview> {
|
|
407
394
|
try {
|
|
408
395
|
const { path, originalNormalized, result } = await executeEditPipeline(
|
|
409
396
|
request,
|
|
@@ -425,19 +412,19 @@ export async function computeEditPreview(
|
|
|
425
412
|
|
|
426
413
|
type EditToolDefinition = ToolDefinition<
|
|
427
414
|
typeof hashlineEditToolSchema,
|
|
428
|
-
|
|
429
|
-
|
|
415
|
+
HashlineReplaceToolDetails,
|
|
416
|
+
ReplaceRenderState
|
|
430
417
|
> & { renderShell?: "default" | "self" };
|
|
431
418
|
|
|
432
419
|
const editToolDefinition: EditToolDefinition = {
|
|
433
|
-
name: "
|
|
434
|
-
label: "
|
|
420
|
+
name: "replace",
|
|
421
|
+
label: "Replace",
|
|
435
422
|
description: EDIT_DESC,
|
|
436
423
|
parameters: hashlineEditToolSchema,
|
|
437
424
|
promptSnippet: EDIT_PROMPT_SNIPPET,
|
|
438
425
|
promptGuidelines: EDIT_PROMPT_GUIDELINES,
|
|
439
426
|
prepareArguments: (args: unknown) =>
|
|
440
|
-
|
|
427
|
+
normalizeReplaceRequest(args) as ReplaceRequestParams,
|
|
441
428
|
renderShell: "default",
|
|
442
429
|
renderCall(args, theme, context) {
|
|
443
430
|
const previewInput = getRenderablePreviewInput(args);
|
|
@@ -458,7 +445,7 @@ const editToolDefinition: EditToolDefinition = {
|
|
|
458
445
|
context.state.preview = undefined;
|
|
459
446
|
const previewGeneration = (context.state.previewGeneration ?? 0) + 1;
|
|
460
447
|
context.state.previewGeneration = previewGeneration;
|
|
461
|
-
|
|
448
|
+
computeReplacePreview(previewInput, context.cwd)
|
|
462
449
|
.then((preview) => {
|
|
463
450
|
if (
|
|
464
451
|
context.state.argsKey === argsKey &&
|
|
@@ -486,7 +473,7 @@ const editToolDefinition: EditToolDefinition = {
|
|
|
486
473
|
text.setText(
|
|
487
474
|
formatEditCall(
|
|
488
475
|
getRenderablePreviewInput(args) ?? undefined,
|
|
489
|
-
context.state as
|
|
476
|
+
context.state as ReplaceRenderState,
|
|
490
477
|
context.expanded,
|
|
491
478
|
theme,
|
|
492
479
|
),
|
|
@@ -504,11 +491,11 @@ const editToolDefinition: EditToolDefinition = {
|
|
|
504
491
|
|
|
505
492
|
const typedResult = result as {
|
|
506
493
|
content?: Array<{ type: string; text?: string }>;
|
|
507
|
-
details?:
|
|
494
|
+
details?: HashlineReplaceToolDetails;
|
|
508
495
|
};
|
|
509
496
|
const renderedText = getRenderedEditTextContent(typedResult);
|
|
510
497
|
|
|
511
|
-
const renderState = context.state as
|
|
498
|
+
const renderState = context.state as ReplaceRenderState | undefined;
|
|
512
499
|
const previewBeforeResult = renderState?.preview;
|
|
513
500
|
if (renderState) {
|
|
514
501
|
renderState.preview = undefined;
|
|
@@ -558,8 +545,8 @@ const editToolDefinition: EditToolDefinition = {
|
|
|
558
545
|
},
|
|
559
546
|
|
|
560
547
|
async execute(_toolCallId, params, signal, _onUpdate, ctx) {
|
|
561
|
-
const normalized =
|
|
562
|
-
|
|
548
|
+
const normalized = normalizeReplaceRequest(params);
|
|
549
|
+
assertReplaceRequest(normalized);
|
|
563
550
|
const normalizedParams = normalized;
|
|
564
551
|
const path = normalizedParams.path;
|
|
565
552
|
const absolutePath = resolveToCwd(path, ctx.cwd);
|
|
@@ -622,7 +609,7 @@ const editToolDefinition: EditToolDefinition = {
|
|
|
622
609
|
const updatedSnapshotId = (await getFileSnapshot(absolutePath))
|
|
623
610
|
.snapshotId;
|
|
624
611
|
|
|
625
|
-
const editMeta:
|
|
612
|
+
const editMeta: ReplaceMeta = {
|
|
626
613
|
editsAttempted,
|
|
627
614
|
noopEditsCount: noopEdits?.length ?? 0,
|
|
628
615
|
firstChangedLine,
|
|
@@ -648,6 +635,6 @@ const editToolDefinition: EditToolDefinition = {
|
|
|
648
635
|
},
|
|
649
636
|
};
|
|
650
637
|
|
|
651
|
-
export function
|
|
638
|
+
export function registerReplaceTool(pi: ExtensionAPI): void {
|
|
652
639
|
pi.registerTool(editToolDefinition);
|
|
653
640
|
}
|
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
- Use edit with HASH anchors for all file changes; batch every change to one file into a single edit call.
|
|
2
|
-
- After a successful edit, the returned `--- Anchors ---` block replaces a re-read for nearby follow-up edits.
|
|
3
|
-
- On `[E_STALE_ANCHOR]`, retry with the `>>>` lines quoted in the error instead of re-reading the whole file.
|
package/prompts/edit-snippet.md
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
Edit a text file via HASH anchors from read, batching all edits to a file in one call
|
package/prompts/edit.md
DELETED
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
Patch a text file using `HASH` anchors copied verbatim from `read`.
|
|
2
|
-
|
|
3
|
-
Put all operations on one file in a single `edit` call. Stack every region into the `edits` array, even when they are far apart. Anchors within one call must all come from the same pre-edit read; the runtime applies them atomically against that one snapshot, so you do not adjust anchors for line-number shifts between edits in the same call.
|
|
4
|
-
|
|
5
|
-
Anchors are 4 characters (e.g. `aB3x`), alphabet `A-Za-z0-9-_`. The wire format for `start`/`end`/`pos` is the anchor only — no line number, no trailing content, no line content.
|
|
6
|
-
|
|
7
|
-
Ops:
|
|
8
|
-
- `replace` — replace the inclusive range `start`..`end`. Both anchors are required. Single line: `start = end`. To delete a range, use `lines: []`. Do NOT use the `pos` field on `replace`; use `start`.
|
|
9
|
-
- `append` — insert `lines` after `pos`; omit `pos` to append at EOF.
|
|
10
|
-
- `prepend` — insert `lines` before `pos`; omit `pos` to prepend at BOF. Use `prepend` at an anchor to insert a new block between line N-1 and N (anchor on the line *after* the insertion point).
|
|
11
|
-
|
|
12
|
-
Examples:
|
|
13
|
-
|
|
14
|
-
1. Single line replace:
|
|
15
|
-
```json
|
|
16
|
-
{ "path": "src/main.ts", "edits": [
|
|
17
|
-
{ "op": "replace", "start": "MQXV", "end": "MQXV", "lines": ["const x = 1;"] }
|
|
18
|
-
] }
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
2. Range replace (3 lines → 3 new lines):
|
|
22
|
-
```json
|
|
23
|
-
{ "path": "src/main.ts", "edits": [
|
|
24
|
-
{ "op": "replace", "start": "ZPMQ", "end": "VRWS", "lines": [
|
|
25
|
-
"function greet(name) {",
|
|
26
|
-
" return `Hello, ${name}`;",
|
|
27
|
-
"}"
|
|
28
|
-
}
|
|
29
|
-
] }
|
|
30
|
-
```
|
|
31
|
-
|
|
32
|
-
3. Multiple regions in one call (delete two non-adjacent ranges, insert before a third anchor):
|
|
33
|
-
```json
|
|
34
|
-
{ "path": "src/server.ts", "edits": [
|
|
35
|
-
{ "op": "replace", "start": "aB3x", "end": "xY7q", "lines": [] },
|
|
36
|
-
{ "op": "replace", "start": "MQXV", "end": "ZPMQ", "lines": [] },
|
|
37
|
-
{ "op": "prepend", "pos": "VRWS", "lines": ["// inserted before VRWS"] }
|
|
38
|
-
] }
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
Rules:
|
|
42
|
-
- `replace` requires both `start` and `end`. A single-line replace is `start=X, end=X`. To replace more than one line, set `end` to a different line's anchor.
|
|
43
|
-
- `start`, `end`, `pos` are HASH anchors only (e.g. `aB3x`). Other forms are rejected with `[E_BAD_REF]`.
|
|
44
|
-
- `lines` is literal file content. No `HASH│` prefix, no leading `+`/`-` (those are read/diff metadata, not file content). Lines starting with 4 base64 chars + `│` are checked; if detected, the edit is rejected with `[E_BARE_HASH_PREFIX]`. For `.py` files, this becomes a `[W_BARE_HASH_PREFIX]` warning instead (Python syntax like `else:`, `except:` triggers the detector).
|
|
45
|
-
- Copy anchors from the most recent `read` of the file. Do not guess or construct them.
|
|
46
|
-
- All edits in one call must be non-conflicting. The runtime rejects with `[E_EDIT_CONFLICT]` if: two `replace` ranges overlap; two `append`/`prepend` target the same insertion boundary (e.g. two EOF appends on a newline-terminated file); or an `append`/`prepend` falls inside a `replace` range in the same call. Fix: merge into one, use different boundaries, or split into a follow-up `edit` call.
|
|
47
|
-
- If `lines` matches the current content byte-for-byte, the edit is classified as `Classification: noop` (file unchanged, not an error).
|
|
48
|
-
|
|
49
|
-
On success (`changed` mode, default), the response text contains an `--- Anchors ---` block with fresh `HASH│content` for the changed region (2 lines of context, capped at ~12 lines / 50 KB). Use those for nearby follow-up edits instead of re-reading. If the response says `Anchors omitted; use read for subsequent edits`, the region was too large — call `read` again. For distant follow-ups, or on any error, call `read` again. `full` and `ranges` modes put previews in `details`; the model only needs what's in the text.
|
|
50
|
-
|
|
51
|
-
Errors are text starting with a bracketed code (e.g. `[E_BAD_SHAPE]`, `[E_STALE_ANCHOR]`, `[E_BAD_OP]`, `[E_INVALID_PATCH]`, `[E_LEGACY_SHAPE]`, `[E_EDIT_CONFLICT]`, `[E_BAD_REF]`, `[E_AMBIGUOUS_ANCHOR]`, `[E_BARE_HASH_PREFIX]`, `[E_WOULD_EMPTY]`). The message tells you what to retry; stale-anchor errors include `>>> HASH│content` lines, ready to copy.
|
|
52
|
-
|
|
53
|
-
The legacy `oldText`/`newText` shape (top-level or as `op: "replace_text"`) is rejected with `[E_LEGACY_SHAPE]`. Use hash-anchored edits instead.
|
|
54
|
-
|
|
55
|
-
Auto-read after write:
|
|
56
|
-
- After a successful `write`, the result includes a `--- Auto-read (hashline anchors) ---` block with `HASH│content` for the written file.
|
|
57
|
-
- Use those anchors directly for `edit` calls without a separate `read`.
|
|
58
|
-
- This enables a seamless write → edit workflow with no extra tool calls.
|
|
File without changes
|