pi-hashline-edit-pro 5.1.0 → 6.0.1
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 +47 -48
- package/index.ts +10 -10
- package/package.json +1 -1
- package/prompts/copy-guidelines.md +0 -1
- package/prompts/copy-snippet.md +1 -1
- package/prompts/copy.md +2 -2
- package/prompts/grep-guidelines.md +1 -0
- package/prompts/grep.md +1 -1
- package/prompts/insert-snippet.md +1 -1
- package/prompts/insert.md +1 -3
- package/prompts/move-guidelines.md +1 -2
- package/prompts/move-snippet.md +1 -1
- package/prompts/move.md +2 -2
- package/prompts/read-guidelines.md +0 -2
- package/prompts/read.md +1 -1
- package/prompts/replace-guidelines.md +3 -4
- package/prompts/replace-match-guidelines.md +1 -0
- package/prompts/replace-match-snippet.md +1 -0
- package/prompts/replace-match.md +3 -0
- package/prompts/replace-snippet.md +1 -1
- package/prompts/replace.md +2 -6
- package/prompts/transfer-guidelines.md +4 -0
- package/prompts/undo-last-change-guidelines.md +1 -1
- package/prompts/undo-last-change-snippet.md +1 -1
- package/prompts/undo-last-change.md +1 -1
- package/src/batch.ts +307 -58
- package/src/commit.ts +4 -2
- package/src/config-ui.ts +2 -2
- package/src/config.ts +6 -6
- package/src/constants.ts +5 -5
- package/src/copy-move.ts +303 -99
- package/src/edit-common.ts +110 -73
- package/src/edit-fidelity.ts +1 -5
- package/src/glob.ts +2 -20
- package/src/grep.ts +9 -11
- package/src/hash-store.ts +0 -3
- package/src/hashline/apply.ts +0 -9
- package/src/hashline/index.ts +0 -1
- package/src/hashline/parse.ts +1 -1
- package/src/hashline/resolve.ts +18 -19
- package/src/insert.ts +12 -12
- package/src/payload-contract.ts +60 -87
- package/src/replace-diff.ts +1 -5
- package/src/replace-match.ts +259 -0
- package/src/replace-response.ts +10 -4
- package/src/replace.ts +4 -6
- package/src/utils.ts +6 -29
- package/src/write-hook.ts +3 -5
- package/prompts/replace-within-guidelines.md +0 -3
- package/prompts/replace-within-snippet.md +0 -1
- package/prompts/replace-within.md +0 -5
- package/src/replace-within.ts +0 -224
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ HDtm│}
|
|
|
17
17
|
|
|
18
18
|
replace one line by its anchor:
|
|
19
19
|
|
|
20
|
-
{ "remove_from": "Emno", "remove_to": "Emno", "
|
|
20
|
+
{ "remove_from": "Emno", "remove_to": "Emno", "text": " console.log('hi');" }
|
|
21
21
|
|
|
22
22
|
the result is the post-edit diff with fresh anchors, so the next edit needs no re-read.
|
|
23
23
|
```
|
|
@@ -30,7 +30,7 @@ the result is the post-edit diff with fresh anchors, so the next edit needs no r
|
|
|
30
30
|
- [Tools](#tools)
|
|
31
31
|
- [read](#read)
|
|
32
32
|
- [replace](#replace)
|
|
33
|
-
- [
|
|
33
|
+
- [replace_match](#replace_match)
|
|
34
34
|
- [insert](#insert)
|
|
35
35
|
- [copy](#copy)
|
|
36
36
|
- [move](#move)
|
|
@@ -90,7 +90,7 @@ pi install /path/to/pi-hashline-edit-pro
|
|
|
90
90
|
| `edit` | disabled |
|
|
91
91
|
| `grep` | disabled while `anchor_grep` is enabled |
|
|
92
92
|
| `copy`, `move` | disabled while Copy/move is off |
|
|
93
|
-
| `
|
|
93
|
+
| `replace_match` | disabled while Replace match is off |
|
|
94
94
|
| `write` | kept; an auto-read block with fresh anchors is appended to its result |
|
|
95
95
|
| `bash` | untouched |
|
|
96
96
|
|
|
@@ -130,7 +130,7 @@ pi uninstall npm:pi-hashline-edit-pro
|
|
|
130
130
|
{
|
|
131
131
|
"remove_from": "Emno",
|
|
132
132
|
"remove_to": "Emno",
|
|
133
|
-
"
|
|
133
|
+
"text": " console.log('hi');"
|
|
134
134
|
}
|
|
135
135
|
```
|
|
136
136
|
|
|
@@ -155,7 +155,7 @@ Nothing commits until an edit call returns: the extension validates the request
|
|
|
155
155
|
|
|
156
156
|
## Tools
|
|
157
157
|
|
|
158
|
-
The extension registers eight tools: `read`, `replace`, `
|
|
158
|
+
The extension registers eight tools: `read`, `replace`, `replace_match`, `insert`, `copy`, `move`, `anchor_grep`, and `undo_last_change`. The built-in `edit` tool is disabled. `copy` and `move` are enabled by default; turn Copy/move off in `/hashline-config` to remove both. `replace_match` is enabled by default; turn Replace match off in `/hashline-config` to remove it. `replace`, `replace_match`, `insert`, `copy`, and `move` take no `path` parameter by default: the file is resolved from the anchors' session ownership alone, so an edit can only land on the file the anchors were served for. Opt in with `/hashline-config` to require `path` in `replace`, `replace_match`, `insert`, `copy`, and `move` for RPC visibility (for example pimacs.el); anchors still resolve the target and `path` must match.
|
|
159
159
|
|
|
160
160
|
### read
|
|
161
161
|
|
|
@@ -188,7 +188,7 @@ Edge cases:
|
|
|
188
188
|
| --- | --- |
|
|
189
189
|
| `remove_from` | 4-char anchor marking the FIRST line to remove (inclusive). |
|
|
190
190
|
| `remove_to` | 4-char anchor marking the LAST line to remove (inclusive). |
|
|
191
|
-
| `
|
|
191
|
+
| `text` | The exact text to write in place of the removed range, as one string: `""` deletes the range, `"\n"` is one blank line, and a trailing line break sets the last line's ending instead of adding a blank line. Embedded `\r\n`/`\r`/`\n` are preserved; JSON decoding happens once, before the tool; the tool writes the string it receives and never decodes — `\uXXXX` is the character, `\\uXXXX` the literal text. Legacy arrays are converted to text (elements joined with LF); prefer the string form. |
|
|
192
192
|
|
|
193
193
|
Example: read showed `Hasu│old` and `arvm│old2`; to replace both:
|
|
194
194
|
|
|
@@ -196,18 +196,18 @@ Example: read showed `Hasu│old` and `arvm│old2`; to replace both:
|
|
|
196
196
|
{
|
|
197
197
|
"remove_from": "Hasu",
|
|
198
198
|
"remove_to": "arvm",
|
|
199
|
-
"
|
|
199
|
+
"text": "new line 1\nnew line 2"
|
|
200
200
|
}
|
|
201
201
|
```
|
|
202
202
|
|
|
203
|
-
Single line: use the same anchor for `remove_from` and `remove_to`.
|
|
204
|
-
A deletion keeps blank lines at the edges of the removed range, so the separators around a block survive the edit; target a blank line on its own to delete it.
|
|
203
|
+
Single line: use the same anchor for `remove_from` and `remove_to`.
|
|
204
|
+
A deletion keeps blank lines at the edges of the removed range, so the separators around a block survive the edit; target a blank line on its own to delete it. Deleting every line empties the file; the result names the new empty-line anchor, so a follow-up `replace` on it can seed content without a `read`.
|
|
205
205
|
|
|
206
206
|
The extension checks the request before any file I/O, so a bad request never touches the file.
|
|
207
207
|
|
|
208
|
-
Auto-fixable slips fall into two groups. Fixed silently: a reversed range, embedded newlines, and a legacy array payload (a single-element array that holds stringified array text, even with a trailing JS method call, for example `[…].map(s => s)`, is unwrapped). Fixed with a warning: a leftover `anchor│` prefix in `
|
|
208
|
+
Auto-fixable slips fall into two groups. Fixed silently: a reversed range, embedded newlines, and a legacy array payload (a single-element array that holds stringified array text, even with a trailing JS method call, for example `[…].map(s => s)`, is unwrapped). Fixed with a warning: a leftover `anchor│` prefix in `text` or the anchor fields (a prefix of 4 to 5 letters before `│`, for example `abde│`), and diff-preview rows pasted into the replacement.
|
|
209
209
|
|
|
210
|
-
Content containing a NUL byte (`U+0000`) is rejected with `[E_BAD_SHAPE]` before any file I/O: writing it would make the file binary, so use an empty replacement to delete. This applies to `
|
|
210
|
+
Content containing a NUL byte (`U+0000`) is rejected with `[E_BAD_SHAPE]` before any file I/O: writing it would make the file binary, so use an empty replacement to delete. This applies to the `text` field of both `replace` and `insert`.
|
|
211
211
|
|
|
212
212
|
Every line in the removed range must match what was last shown to you, except that a pure deletion (an empty replacement) verifies only the first and last line of the range and removes the interior as it currently stands. The extension records the `anchor│content` rows it serves (`read` output, `anchor_grep` output, the auto-read block after `write`, the `+anchor│` and ` anchor│` rows of post-edit diffs, the current-range rows of `[E_RANGE_STALE]` feedback, and the context rows of stale-anchor feedback) and verifies the whole range against that record before writing. A line that changed on disk since it was shown, or an anchor that is not owned in this session, refuses the edit with `[E_RANGE_STALE]` or `[E_STALE_ANCHOR]` and returns the current range with fresh anchors, so the retry needs no `read`. An owned anchor enters the served record when its row is shown (after a restart, restored ownership counts as shown), so a file with no owned anchors cannot be edited by anchor at all; call `read` first. An owned line that was never shown, for example beyond an auto-read preview's truncation cap, is refused with `[E_RANGE_STALE]` and returns the current range, so the retry still needs no `read`; only lines strictly between the boundaries of a pure deletion are exempt.
|
|
213
213
|
|
|
@@ -215,13 +215,13 @@ An edit that changes neither content nor line endings reports `No changes made`
|
|
|
215
215
|
|
|
216
216
|
After a successful edit, the diff is capped at 50KB. A row over 50KB is shown as a marker that keeps the row's anchor, and only the rows shown in the capped diff are recorded as served. The same caps apply to the `insert` and `undo_last_change` diffs, to the interactive previews, and to `details.patch`.
|
|
217
217
|
|
|
218
|
-
###
|
|
218
|
+
### replace_match
|
|
219
219
|
|
|
220
|
-
`
|
|
220
|
+
`replace_match` changes part of a line (or a range of lines) without retyping the rest. `replace_from` and `replace_to` are bare anchors marking the first and last line of the range; use the same anchor for a single line. `old_string` is the exact text to find inside that range, and `new_string` replaces every occurrence of it; every other character stays untouched. That makes it the tool for a change the request quotes as a substring: a whole-line `replace` has to reproduce the rest of the line, so a slipped character becomes a wrong byte, while `replace_match` leaves everything the request did not name untouched. It is enabled by default; turn Replace match off in `/hashline-config` to remove the tool.
|
|
221
221
|
|
|
222
|
-
`
|
|
222
|
+
`old_string` is matched against the range's text (LF line breaks, no final terminator) and every non-overlapping occurrence is replaced, left to right. A missing match is refused with `[E_SUBSTRING_NOT_FOUND]` and the current `anchor│content` rows, so the retry needs no `read`. The two boundary anchors are verified against what was last shown; lines strictly inside the range are matched against the file as it currently stands on disk.
|
|
223
223
|
|
|
224
|
-
|
|
224
|
+
In a same-message batch it joins the other calls on its file: the batch validates everything against the pre-batch state and commits once, with one undo. A missing `old_string` aborts the whole batch unwritten. The post-edit diff carries fresh anchors, and the edit is undoable with `undo_last_change`.
|
|
225
225
|
|
|
226
226
|
### insert
|
|
227
227
|
|
|
@@ -231,14 +231,14 @@ A `replace_within` call is never grouped into a batch; it commits on its own lik
|
|
|
231
231
|
| --- | --- |
|
|
232
232
|
| `anchor` | 4-char anchor marking the line next to which the lines go. The anchor line is preserved. A pasted `+Hasu│x` diff row or `anchor│` prefix is stripped automatically with a warning. |
|
|
233
233
|
| `direction` | `"after"` inserts below the anchor line, `"before"` above it. |
|
|
234
|
-
| `
|
|
234
|
+
| `text` | The exact text to insert, as one string: `""` inserts one blank line (the same as `"\n"`), and a trailing line break sets the last line's ending instead of adding a blank line. Never include the anchor line. Embedded `\r\n`/`\r`/`\n` are preserved; JSON decoding happens once, before the tool; the tool writes the string it receives and never decodes — `\uXXXX` is the character, `\\uXXXX` the literal text. Legacy arrays are converted to text (elements joined with LF); prefer the string form. |
|
|
235
235
|
|
|
236
|
-
Nothing is removed and the inserted lines are written exactly as given; the anchor line and every other line stay in place. An empty `
|
|
236
|
+
Nothing is removed and the inserted lines are written exactly as given; the anchor line and every other line stay in place. An empty `text` payload inserts one blank line. To seed an empty file, read it and insert after the `anchor│` empty-line row.
|
|
237
237
|
|
|
238
238
|
Example: add a line after `Emno│`:
|
|
239
239
|
|
|
240
240
|
```json
|
|
241
|
-
{ "anchor": "Emno", "direction": "after", "
|
|
241
|
+
{ "anchor": "Emno", "direction": "after", "text": " // log the greeting" }
|
|
242
242
|
```
|
|
243
243
|
|
|
244
244
|
The same safety machinery as `replace` applies: undo is saved before the write (a failed write restores the previous undo record), and line endings and BOMs survive.
|
|
@@ -259,13 +259,13 @@ Example: read served `Hasu│old` in `a.ts` and `Qwer│top` in `b.ts`; to copy
|
|
|
259
259
|
{ "source_from": "Hasu", "source_to": "Hasu", "insert_after": "Qwer" }
|
|
260
260
|
```
|
|
261
261
|
|
|
262
|
-
The source lines stay in place and keep their anchors; the copied lines are minted fresh anchors in the destination's post-edit diff and keep their source line endings. A cross-file copy writes only the destination, so one `undo_last_change` on it reverts the copy.
|
|
262
|
+
The source lines stay in place and keep their anchors; the copied lines are minted fresh anchors in the destination's post-edit diff and keep their source line endings. A cross-file copy writes only the destination, so one `undo_last_change` on it reverts the copy. In a same-message batch, a same-file copy and a cross-file copy join the destination file's batch; the copy still duplicates the content its source anchors were served from, not the result of a sibling edit.
|
|
263
263
|
|
|
264
264
|
### move
|
|
265
265
|
|
|
266
|
-
`move` relocates a range of lines in one call: the range is removed from the source file and written after `insert_after`, which may live in a different file. It takes the same fields as `copy`, and an empty destination file is seeded with the moved lines. Within one file, `insert_after` must sit outside the source range, and moving a range to where it already sits reports `No changes made` and leaves the anchors alone. Lines between the source and the target keep their content but may be re-anchored; the moved lines keep their source line endings, and a cross-file move that removes every source line leaves the source file empty.
|
|
266
|
+
`move` relocates a range of lines in one call: the range is removed from the source file and written after `insert_after`, which may live in a different file. It takes the same fields as `copy`, and an empty destination file is seeded with the moved lines. Within one file, `insert_after` must sit outside the source range, and moving a range to where it already sits reports `No changes made` and leaves the anchors alone. A same-file `move` and a cross-file `move` join the same-message batch of the destination file; a cross-file `move` whose source file also has batched edits in the message commits on its own. A batched cross-file `move` commits its source removal with the batch but shows only the destination diff; read the source file for fresh anchors. Lines between the source and the target keep their content but may be re-anchored; the moved lines keep their source line endings, and a cross-file move that removes every source line leaves the source file empty.
|
|
267
267
|
|
|
268
|
-
The same safety machinery as `replace` applies to both tools: undo is saved before the write (a failed write restores the previous undo record), and line endings and BOMs survive. A cross-file `move` writes two files and records one undo entry per file; undo both sides to revert the whole move, because undoing one side alone leaves the moved lines duplicated or missing.
|
|
268
|
+
The same safety machinery as `replace` applies to both tools: undo is saved before the write (a failed write restores the previous undo record), and line endings and BOMs survive. A cross-file `move` writes two files and records one undo entry per file; undo both sides to revert the whole move, because undoing one side alone leaves the moved lines duplicated or missing. When the move is part of a batch, the destination side is reverted by that batch's undo and the source side by its own entry.
|
|
269
269
|
|
|
270
270
|
### anchor_grep
|
|
271
271
|
|
|
@@ -276,7 +276,7 @@ Every matching line, and each requested context line, is returned as `lineNumber
|
|
|
276
276
|
| Field | Description |
|
|
277
277
|
| --- | --- |
|
|
278
278
|
| `pattern` | Search pattern (regex, or literal text when `literal` is true). |
|
|
279
|
-
| `path` | File or directory to search (default: the current working directory).
|
|
279
|
+
| `path` | File or directory to search (default: the current working directory). |
|
|
280
280
|
| `glob` | Filter files by glob; `*` matches across directories, for example `*.ts` or `**/*.spec.ts`. A leading `/` is ignored, and the pattern may be relative to the search root or the current directory. |
|
|
281
281
|
| `ignoreCase` | Case-insensitive search (default: false). |
|
|
282
282
|
| `literal` | Treat the pattern as literal text instead of a regex (default: false). |
|
|
@@ -291,12 +291,12 @@ Output is capped at `limit` matched lines, 2000 rows, and 50KB of text, whicheve
|
|
|
291
291
|
|
|
292
292
|
### undo_last_change
|
|
293
293
|
|
|
294
|
-
`undo_last_change` reverts the most recent successful `replace`, `
|
|
294
|
+
`undo_last_change` reverts the most recent successful `replace`, `replace_match`, `insert`, `copy`, or `move` on a file, restoring the exact previous content, BOM and line endings included, plus the previous anchors.
|
|
295
295
|
|
|
296
|
-
- History is per-file and single-level: only the most recent `replace`, `
|
|
296
|
+
- History is per-file and single-level: only the most recent `replace`, `replace_match`, `insert`, `copy`, or `move` can be reverted. A same-message batch of `replace`/`insert` calls on one file counts as one entry: one undo reverts the whole batch.
|
|
297
297
|
- History is persisted and survives session restarts. A failed `write` does not clear it.
|
|
298
|
-
- Every applied `replace`, `
|
|
299
|
-
- A cross-file `move` stores one undo entry per file; `undo_last_change` reverts the file you name, so revert both sides to undo the whole move.
|
|
298
|
+
- Every applied `replace`, `replace_match`, `insert`, `copy`, or `move` is undoable; the undo record is saved before the edit is written.
|
|
299
|
+
- A cross-file `move` stores one undo entry per file; `undo_last_change` reverts the file you name, so revert both sides to undo the whole move; a batched cross-file `move` reverts its destination side with the batch's undo and its source side with its own entry.
|
|
300
300
|
- A successful `write` clears the history for that file.
|
|
301
301
|
- If the file was modified since the last edit, the undo is refused with `[E_UNDO_STALE]` rather than overwriting those changes, and the record is kept. Once the file matches the edited state again, `undo_last_change` succeeds.
|
|
302
302
|
- If the file was deleted since the last edit, `undo_last_change` restores it from the recorded pre-edit content.
|
|
@@ -305,17 +305,18 @@ Output is capped at `limit` matched lines, 2000 rows, and 50KB of text, whicheve
|
|
|
305
305
|
|
|
306
306
|
## Batching
|
|
307
307
|
|
|
308
|
-
Multiple `replace` and `
|
|
308
|
+
Multiple `replace`, `replace_match`, `insert`, `copy`, and `move` calls on the same file in one assistant message are grouped per file into one batch. A cross-file `copy` and a cross-file `move` are grouped with the edits of their destination file; a cross-file `move` whose source file also has batched edits in the same message is not grouped. The batch unit is the message, not the turn: calls from separate messages in the same turn run on their own, one after another.
|
|
309
309
|
|
|
310
310
|
- A call outside a batch commits before its result returns.
|
|
311
|
-
- A
|
|
312
|
-
- A
|
|
311
|
+
- A cross-file `move` whose source file also has batched edits in the same message is not grouped: it commits on its own, and a pending same-file batch aborts safely with `[E_OP_ABORTED]` if the file changed under it.
|
|
312
|
+
- A batched cross-file `move` displays only the destination diff: the arrival is covered by the batch's undo, and the source removal is committed with the same batch but is not shown. Read the source file for fresh anchors; it keeps its own undo entry, and `details.patch` still contains the source patch.
|
|
313
|
+
- A batch validates every call against the pre-batch state and commits once, during the batch's last call: earlier calls reply `In batch N (queued)`, and the batch's last call shows the combined diff, with one undo reverting the whole batch. A `copy` always duplicates the content its source anchors were served from: when the source file is also edited in the message, the copy reads that file's pre-batch state, even if the source batch commits before the copy runs. A batched cross-file `move` defers the source-file removal to the batch commit, so if the batch aborts, the source file is untouched.
|
|
313
314
|
- If a batch aborts, nothing is written: the failing call's error ends with `Aborts batch N.` and reports that the whole batch was discarded, and an earlier member's row renders the abort message instead of the queued placeholder. Nothing commits until the last call succeeds.
|
|
314
315
|
- A batch member accepts the same request shapes and auto-fixes as a standalone call.
|
|
315
316
|
|
|
316
|
-
Batched calls must target disjoint ranges; overlapping ranges, or any failing call, aborts the whole batch unwritten. One `insert` with `direction: "before"` and one with `direction: "after"` may target the same anchor line: the pair composes into a single insertion. A batch member that fails aborts its batch-mates with `[E_OP_ABORTED]`.
|
|
317
|
+
Batched calls must target disjoint ranges; overlapping ranges, or any failing call, aborts the whole batch unwritten. A `copy`'s `insert_after` line is the copy's destination range, so replacing or moving that same line in the batch is an overlap. One `insert` with `direction: "before"` and one with `direction: "after"` may target the same anchor line: the pair composes into a single insertion. A batch member that fails aborts its batch-mates with `[E_OP_ABORTED]`.
|
|
317
318
|
|
|
318
|
-
A call whose anchors resolve nowhere never joins a batch: it runs on its own and fails with its own error (`[E_STALE_ANCHOR]`, or `[E_BAD_SHAPE]` when its request cannot be parsed), while the same-file batch in the message still commits. Calls with one stale anchor and a valid co-anchor, or with a `requirePath` path hint, are grouped into their file's batch and abort it instead of applying partially. An error that aborts a batch ends with `Aborts batch N.`; an aborted call reads `[E_OP_ABORTED] Batch N aborted: [<kind>] Call Nr <X> errored [<code>]`, naming the failing call and its error code (or `[E_OP_ABORTED] Batch N aborted.` when the failing error carries no code). Anchor capacity is preflighted before writing; if anchor finalization fails after the write, the error states the file was written with one undo available. Verify each batch diff before the next turn's edits on that file.
|
|
319
|
+
A call whose anchors resolve nowhere never joins a batch: it runs on its own and fails with its own error (`[E_STALE_ANCHOR]`, or `[E_BAD_SHAPE]` when its request cannot be parsed), while the same-file batch in the message still commits. Calls with one stale anchor and a valid co-anchor, or with a `requirePath` path hint, are grouped into their file's batch and abort it instead of applying partially. A `copy` or `move` whose source anchors no longer resolve cannot be grouped, because the source file cannot be identified; it runs on its own and fails with its own error. A cross-file `move` whose source file has batched edits in the message is also left out of the batch and commits on its own. An error that aborts a batch ends with `Aborts batch N.`; an aborted call reads `[E_OP_ABORTED] Batch N aborted: [<kind>] Call Nr <X> errored [<code>]`, naming the failing call and its error code (or `[E_OP_ABORTED] Batch N aborted.` when the failing error carries no code). Anchor capacity is preflighted before writing; if anchor finalization fails after the write, the error states the file was written with one undo available. Verify each batch diff before the next turn's edits on that file.
|
|
319
320
|
|
|
320
321
|
The hashline tools are sequential in pi, so a message that contains one runs all of its tool calls one at a time in the order given; a `read` or shell `cat` issued before the edit commits can still observe the pre-commit state, so verify in the next message with the post-edit diff or a fresh `read`.
|
|
321
322
|
|
|
@@ -323,7 +324,7 @@ The hashline tools are sequential in pi, so a message that contains one runs all
|
|
|
323
324
|
|
|
324
325
|
Auto-read is enabled by default. After a successful `write`, the extension reads the file and appends an `--- Auto-read (hashline anchors) ---` block, so you get fresh `anchor│content` anchors without a separate `read` call.
|
|
325
326
|
|
|
326
|
-
After `replace`, `
|
|
327
|
+
After `replace`, `replace_match`, `insert`, `copy`, `move`, and `undo_last_change`, the result shows the post-edit diff. Inside a same-message batch, only the batch's last call shows the combined diff, headed by a `batch N:` line; earlier calls reply `In batch N (queued)`. The `+anchor│` and ` anchor│` rows carry the current anchors, so follow-up edits can anchor on the diff directly. The `-anchor│` rows show removed lines with their old anchors, which are stale after the edit. When the context line next to a change is blank or whitespace-only, one more context line is shown in that direction, so the change stays anchored to visible content. Call `read` when you want the full file's anchors.
|
|
327
328
|
|
|
328
329
|
An edit that changes only line endings has no content diff; the result still reports `applied`, and one `undo_last_change` reverts it.
|
|
329
330
|
|
|
@@ -345,7 +346,7 @@ The setting lives in `/hashline-config` as Auto-read all and in `config.json` as
|
|
|
345
346
|
|
|
346
347
|
| Command | Description |
|
|
347
348
|
| --- | --- |
|
|
348
|
-
| `/hashline-config` | Open the settings window: auto-read anchors, auto-read all mode, ignore folders/files, diff context lines, `anchor_grep` tool, copy/move tools,
|
|
349
|
+
| `/hashline-config` | Open the settings window: auto-read anchors, auto-read all mode, ignore folders/files, diff context lines, `anchor_grep` tool, copy/move tools, replace_match tool, required `path`, and strict input. Persists across sessions. |
|
|
349
350
|
| `/clear-anchors` | Clear the session's anchor claims. Anchors are re-claimed on the next `read`. |
|
|
350
351
|
|
|
351
352
|
Settings live in `~/.config/pi-hashline-edit-pro/config.json`, created when a setting is first changed in `/hashline-config`:
|
|
@@ -357,7 +358,7 @@ Settings live in `~/.config/pi-hashline-edit-pro/config.json`, created when a se
|
|
|
357
358
|
"autoReadAllIgnore": [],
|
|
358
359
|
"anchorGrepEnabled": true,
|
|
359
360
|
"copyMoveEnabled": true,
|
|
360
|
-
"
|
|
361
|
+
"replaceMatchEnabled": true,
|
|
361
362
|
"requirePath": false,
|
|
362
363
|
"strictInput": false,
|
|
363
364
|
"diffContextLines": 1
|
|
@@ -371,8 +372,8 @@ Settings live in `~/.config/pi-hashline-edit-pro/config.json`, created when a se
|
|
|
371
372
|
| `autoReadAllIgnore` | Ignore folders/files | `[]` | Extra folder names, file names, or globs skipped by auto-read all. |
|
|
372
373
|
| `anchorGrepEnabled` | Anchor grep | `true` | Register `anchor_grep` and disable the built-in grep while it is on. |
|
|
373
374
|
| `copyMoveEnabled` | Copy/move | `true` | Offer the `copy` and `move` tools; when off, both are removed from the active tools. |
|
|
374
|
-
| `
|
|
375
|
-
| `requirePath` | Require path | `false` | `replace`, `
|
|
375
|
+
| `replaceMatchEnabled` | Replace match | `true` | Offer the `replace_match` tool; when off, it is removed from the active tools. |
|
|
376
|
+
| `requirePath` | Require path | `false` | `replace`, `replace_match`, `insert`, `copy`, and `move` require a `path` argument that must match anchor ownership. |
|
|
376
377
|
| `strictInput` | Strict input | `false` | Reject auto-fixable slips (`[W_BAD_SHAPE]`, `[W_BAD_REF]`, `[W_INVALID_PATCH]`, `[W_BARE_HASH_PREFIX]`) with `[E_BAD_SHAPE]` instead of applying them with a warning. |
|
|
377
378
|
| `diffContextLines` | Diff context | `1` | Surrounding lines in post-edit diffs, 0-10 (needs Auto-read). |
|
|
378
379
|
|
|
@@ -384,7 +385,7 @@ When `PI_HASHLINE_DIR` is unset or empty, non-Windows platforms honor `XDG_CONFI
|
|
|
384
385
|
| --- | --- | --- |
|
|
385
386
|
| Output cap | 2000 lines and 50KB | `read`, auto-read after `write`, post-edit diffs, patches, previews, `details.patch` |
|
|
386
387
|
| Oversized row | 50KB per `anchor│content` row | replaced by an anchor-keeping marker you can still edit through |
|
|
387
|
-
| Line cap | 1,353,139 lines per file | `read`, `replace`, `
|
|
388
|
+
| Line cap | 1,353,139 lines per file | `read`, `replace`, `replace_match`, `insert`, `copy`, `move` (`[E_FILE_TOO_LARGE]`) |
|
|
388
389
|
| File size | 100MB | all tools (`[E_FILE_TOO_LARGE]`) |
|
|
389
390
|
| Hash window | first 500 bytes of a line | anchor identity for long lines |
|
|
390
391
|
| Patch guard | 1MB of pre-edit + post-edit text | patch generation is skipped and `patchTruncated` is set |
|
|
@@ -406,7 +407,7 @@ All eight tools return machine-readable metadata in `details` alongside the mode
|
|
|
406
407
|
| --- | --- |
|
|
407
408
|
| `read` | `truncation` (set when output was truncated), `snapshotId` (a `v2\|path\|ino\|mtime\|ctime\|size` fingerprint), `nextOffset` (use as the next `offset`), and `metrics` with `truncated` and `next_offset`. |
|
|
408
409
|
| `replace`, `insert` | `diff` (post-edit diff, capped, with current anchors on `+anchor│` and ` anchor│` rows; a same-message batch reports the combined diff on its last call and an empty diff on earlier calls), `patch` (a standard unified patch for external tools, capped like the diff), `patchTruncated` (true when the patch was cut or skipped for a pair over 1MB and can no longer be applied as-is), `firstChangedLine`, `snapshotId`, `classification` (`"noop"` when nothing changed), `batch` (`{ id, size, last, total }` marking same-message batch membership; earlier members also carry `aborted: true` and `abortMessage` after a batch abort), `hints` (informative `[H_*]` notices, for example literal escaped text written as sent), and `metrics`: `edits_attempted`, `edits_noop`, `warnings`, `classification` (`"applied"` or `"noop"`), `changed_lines` (`{ first, last }`), `added_lines`, `removed_lines`. |
|
|
409
|
-
| `
|
|
410
|
+
| `replace_match` | Same shape as `replace`: `diff` (post-edit diff with current anchors), `patch`, `patchTruncated`, `firstChangedLine`, `snapshotId`, `classification` (`"noop"` when nothing changed), and `metrics` with the same counters. |
|
|
410
411
|
| `copy`, `move` | Same shape as `replace`: `diff` (post-edit diff with current anchors), `patch`, `patchTruncated`, `firstChangedLine`, `snapshotId`, `classification` (`"noop"` when a move changes nothing), and `metrics` with the same counters. |
|
|
411
412
|
| `undo_last_change` | `diff` (the undo diff with restored anchors), `patch`, `patchTruncated`, and `metrics` in the same shape as `replace`. |
|
|
412
413
|
| `anchor_grep` | `metrics` with `matches` (capped at `limit`), `files`, and `truncated`; `truncation` (the standard pi truncation report) when output was cut; and `linesTruncated` (true when long lines were shown as fragments). |
|
|
@@ -431,24 +432,22 @@ Full reference:
|
|
|
431
432
|
| Code | Meaning |
|
|
432
433
|
| --- | --- |
|
|
433
434
|
| `[E_CONFIG]` | `PI_HASHLINE_DIR` is nonempty but not an absolute path. |
|
|
434
|
-
| `[E_BAD_SHAPE]` | Request envelope or edit item has unknown, missing, or wrongly-typed fields (for example `
|
|
435
|
+
| `[E_BAD_SHAPE]` | Request envelope or edit item has unknown, missing, or wrongly-typed fields (for example `text` must be a string holding the exact text), content contains a NUL byte (`U+0000`), which would make the file binary, or a grep `glob` has invalid bracket or brace syntax. |
|
|
435
436
|
| `[W_BAD_SHAPE]` | Auto-corrected request slip reported as a warning (for example legacy array text that could not be parsed and was kept as one literal line). |
|
|
436
437
|
| `[E_BAD_REF]` | An anchor in `remove_from`/`remove_to` is not a bare 4-character anchor (the anchor table is letters only). |
|
|
437
|
-
| `[E_SUBSTRING_NOT_FOUND]` | `
|
|
438
|
-
| `[E_SUBSTRING_AMBIGUOUS]` | `replace_within` found `replace_old` more than once in the selected range. Narrow `replace_from`/`replace_to` or extend `replace_old` so it matches exactly once. |
|
|
438
|
+
| `[E_SUBSTRING_NOT_FOUND]` | `replace_match` did not find `old_string` in the selected range. The current `anchor│content` rows are returned; copy `old_string` exactly from the served row and retry. |
|
|
439
439
|
| `[W_BAD_REF]` | A pasted `anchor│` or diff-preview marker was stripped from an anchor field with a warning. |
|
|
440
440
|
| `[E_STALE_ANCHOR]` | An anchor is not owned in this session (it was never shown to you, or its line was edited or the file was rewritten); call `read` for fresh anchors. |
|
|
441
|
-
| `[W_INVALID_PATCH]` | A `
|
|
442
|
-
| `[W_BARE_HASH_PREFIX]` | A `
|
|
441
|
+
| `[W_INVALID_PATCH]` | A `text` line is a diff-preview row (`+anchor│`, `-anchor│`, `- │`). The marker is stripped automatically with a warning. |
|
|
442
|
+
| `[W_BARE_HASH_PREFIX]` | A `text` line starts with an `anchor│` prefix. The prefix is stripped automatically with a warning. |
|
|
443
443
|
| `[W_ANCHOR_RECLAIMED]` | The session's anchor quota was exhausted, so all anchors of the listed files (the least recently read or edited) were freed to make room. Read those files again before editing them. |
|
|
444
|
-
| `[H_LITERAL_ESCAPE]` | A payload field contains literal escaped text such as `\uXXXX`, `\n`, `\t`, `\r`, or `\"` (one hint per distinct escape, up to three). The file receives those backslash characters as written, because JSON decoding happens once, before the tool call (`\uXXXX` → the character), so a doubled escape (`\\uXXXX`) lands literally. The hint is one line: `
|
|
444
|
+
| `[H_LITERAL_ESCAPE]` | A payload field contains literal escaped text such as `\uXXXX`, `\n`, `\t`, `\r`, or `\"` (one hint per distinct escape, up to three). The file receives those backslash characters as written, because JSON decoding happens once, before the tool call (`\uXXXX` → the character), so a doubled escape (`\\uXXXX`) lands literally. The hint is one line: `text: "\u200b" written as literal text (Kq3f│ col 31); resend with U+200B if unintended.` names the field, the escape, and up to three affected anchors with their columns, then the fix; when more than three rows carry it, it gives `on 10 rows; undo_last_change + resend with U+200B if unintended.` instead of the anchor list. |
|
|
445
445
|
| `[H_UNICODE_LOST]` | The new text is missing an invisible or look-alike character (for example `U+200B`, `U+2060`, `U+00A0`, or a smart quote) that a replaced row has, or that a context row sharing a long run with an inserted line has. The edit applied as sent; the one-line hint names the character, its column, and the reference anchor, for example `[H_UNICODE_LOST] U+2060 missing at col 31; Kq3f│ has it; resend with U+2060 if unintended.` When another look-alike character (including `U+FFFD`) takes its place, the hint reports the substitute with `[H_UNICODE_SWAPPED]` wording. |
|
|
446
446
|
| `[H_UNICODE_SWAPPED]` | The new text uses a different invisible or look-alike character than the line it matches (for example `U+200D` where that line has `U+200B`, or an ASCII stand-in such as `.` for `。` or a space for `U+00A0`). The edit applied as sent; the one-line hint names both code points, the column, and the reference anchor, for example `[H_UNICODE_SWAPPED] U+200D at col 21 where Kq3f│ has U+200B; resend with U+200B if unintended.` The substitute may also be `U+FFFD`, for example where a row has `U+00A0`. |
|
|
447
447
|
| `[H_TRAILING_WHITESPACE]` | The new text differs from the replaced line only in trailing whitespace. Anchor checksums trim trailing whitespace, so the change does not invalidate the anchor; the one-line hint names the old and new trailing-whitespace counts and the column, for example `[H_TRAILING_WHITESPACE] 1 trailing whitespace character at col 7; Kq3f│ had 0.` |
|
|
448
448
|
| `[H_INDENT_MISMATCH]` | The new line has fewer leading whitespace characters than a structurally similar row (a reference row near the anchor line for an insert, or the replaced line). The edit applied as sent; the one-line hint names both counts and the reference anchor, for example `[H_INDENT_MISMATCH] new line has 0 leading whitespace characters; Kq3f│ has 2.` Copied or moved blocks are not checked, because their indentation comes from the source lines. |
|
|
449
449
|
| `[H_SEPARATOR_MOVED]` | An insert landed its text directly against the anchor line, and the blank line that separated the anchor from its neighbor was displaced to the other side of the inserted text. The edit applied as sent; the one-line hint names the anchor and which side lost the blank line, for example `[H_SEPARATOR_MOVED] blank separator above Kq3f│ was displaced; add a blank line before Kq3f│ if unintended.` |
|
|
450
450
|
| `[H_SEPARATOR_LOST]` | A pure deletion removed a run of blank lines that sat between two content lines. The edit applied as sent; the one-line hint names the two surviving anchors and the removed count, for example `[H_SEPARATOR_LOST] deletion removed 2 blank lines between Aaaa│ and Dddd│.` |
|
|
451
|
-
| `[E_WOULD_EMPTY]` | An edit would empty a non-empty file; use `write` instead. A cross-file `move` may empty its source file. |
|
|
452
451
|
| `[E_NOT_FOUND]` | The path does not exist. |
|
|
453
452
|
| `[E_ACCESS]` | The file is not readable or writable. |
|
|
454
453
|
| `[E_NOT_TEXT]` | The path is a directory, binary file, image, or UTF-16/UTF-32 encoded text; hashline editing only supports text files. |
|
|
@@ -460,7 +459,7 @@ Full reference:
|
|
|
460
459
|
| `[E_STORE_UNAVAILABLE]` | No SQLite runtime could be loaded: the host exposes neither `node:sqlite` (Node 22.19+) nor `bun:sqlite`. The pi release binary's bundled Bun lacks `node:sqlite`; run pi under Node or a Bun build that ships SQLite. |
|
|
461
460
|
| `[E_WRITE_HASH_ECHO]` | A `write` `content` line reproduces a served row for this file (a bare `anchor│` read row, a `+anchor│`, ` anchor│`, or `-anchor│` diff row, or a `lineNumber │ anchor│content` grep row). The write is refused, file byte-identical; retry with bare content (remove the copied anchors). |
|
|
462
461
|
| `[E_PATH_CHANGED]` | A write target changed identity after it was read; the write was refused to avoid following a swapped symlink or overwriting a replacement file. |
|
|
463
|
-
| `[E_BATCH_OVERLAP]` | Batched
|
|
462
|
+
| `[E_BATCH_OVERLAP]` | Batched edit calls target overlapping ranges; the whole batch was refused. One `before` plus one `after` insert on the same anchor line is not an overlap. Retry with disjoint ranges. |
|
|
464
463
|
| `[E_OP_ABORTED]` | An edit aborted (a same-message batch member failed, or the file changed or was deleted after the edit started). Nothing was written. Fix the sibling failure and retry the batch, otherwise call `read` for fresh anchors and retry. The abort names the failing call and its error code when one is known. |
|
|
465
464
|
| `[E_UNSAFE_REGEX]` | A grep regex can trigger excessive backtracking; simplify it or search with `literal: true`. |
|
|
466
465
|
| `[E_GREP_FAILED]` | `anchor_grep` could not start ripgrep or ripgrep exited with an error (for example a pattern valid in JavaScript but unsupported by ripgrep's regex engine); the message carries ripgrep's output. Retry with `literal: true` or simplify the pattern. |
|
|
@@ -476,7 +475,7 @@ Full reference:
|
|
|
476
475
|
- Reset the anchor state. Anchors live in `~/.config/pi-hashline-edit-pro/hash-store.sqlite` (with `-wal`/`-shm` sidecars) and in per-session ownership logs under `~/.config/pi-hashline-edit-pro/sessions/`. Quit pi, delete those files, and everything is rebuilt on the next session. Anchor history is lost, but no project files are touched.
|
|
477
476
|
- Corrupt store. If the store fails its health check it is renamed to `hash-store.sqlite.corrupt-<timestamp>` and rebuilt automatically.
|
|
478
477
|
- Config directory moved. If `XDG_CONFIG_HOME` is set on a non-Windows platform, the config directory (and the anchor state inside it) lives at `$XDG_CONFIG_HOME/pi-hashline-edit-pro` instead of `~/.config/pi-hashline-edit-pro`. An existing store is not migrated automatically. To keep anchor and undo history, move the old `hash-store.sqlite` files (plus `-wal`/`-shm` sidecars) into the new directory before the first run.
|
|
479
|
-
- Windows drives in WSL. Editing a file under a Windows mount (`/mnt/c`, drvfs/9p) can fail with `EPERM` from `fchmod` because those filesystems do not store POSIX modes. Mode preservation is best-effort there, so `replace`, `
|
|
478
|
+
- Windows drives in WSL. Editing a file under a Windows mount (`/mnt/c`, drvfs/9p) can fail with `EPERM` from `fchmod` because those filesystems do not store POSIX modes. Mode preservation is best-effort there, so `replace`, `replace_match`, `insert`, and `undo_last_change` still write the edit.
|
|
480
479
|
- Not sure what the extension changed. `read` returns anchored rows and the built-in `edit` is gone; that is expected. See [What changes in your session](#what-changes-in-your-session).
|
|
481
480
|
|
|
482
481
|
## Privacy and on-disk state
|
|
@@ -503,7 +502,7 @@ Background snapshot pruning and registry sidecar GC skip `EPERM`/`EACCES` withou
|
|
|
503
502
|
|
|
504
503
|
### Allocation
|
|
505
504
|
|
|
506
|
-
Anchors are allocated, never derived. Every line that is served to you, by `read`, `anchor_grep`, the auto-read block after `write`, or a post-edit diff, gets the next free anchor from the session's pool, claimed by walking the table with a stride of 836,286 entries (coprime to the 1,353,139-entry table), so consecutively minted anchors land in unrelated regions of the table instead of sharing leading characters. Each session seeds its walk from its own offset (derived from the session key and the process id), so concurrent sessions mint different sequences instead of identical ones: an anchor minted in one session is unknown in another and is rejected with `[E_STALE_ANCHOR]` rather than resolving to a different file. Ownership is exclusive: an anchor is owned by one file's line until it is freed (the line was edited, the file was written or deleted, you ran `/clear-anchors`, or the session's quota ran out and the file was the least recently read or edited, which frees all of its anchors and reports it in `[W_ANCHOR_RECLAIMED]`). Minting prefers anchors the session has never used; when a bounded fresh-anchor probe finds nothing, freed anchors are recycled after their stale served records are purged, so an anchor is never shared by two live lines. Because ownership is exclusive, an anchor resolves to exactly one file. Two byte-identical lines never share an anchor, and that guarantee sets the file size cap: the pool is the shipped table's 1,353,139 entries (not all 52⁴ letter combinations), so a file can hold at most 1,353,139 lines, beyond which `read`, `replace`, `
|
|
505
|
+
Anchors are allocated, never derived. Every line that is served to you, by `read`, `anchor_grep`, the auto-read block after `write`, or a post-edit diff, gets the next free anchor from the session's pool, claimed by walking the table with a stride of 836,286 entries (coprime to the 1,353,139-entry table), so consecutively minted anchors land in unrelated regions of the table instead of sharing leading characters. Each session seeds its walk from its own offset (derived from the session key and the process id), so concurrent sessions mint different sequences instead of identical ones: an anchor minted in one session is unknown in another and is rejected with `[E_STALE_ANCHOR]` rather than resolving to a different file. Ownership is exclusive: an anchor is owned by one file's line until it is freed (the line was edited, the file was written or deleted, you ran `/clear-anchors`, or the session's quota ran out and the file was the least recently read or edited, which frees all of its anchors and reports it in `[W_ANCHOR_RECLAIMED]`). Minting prefers anchors the session has never used; when a bounded fresh-anchor probe finds nothing, freed anchors are recycled after their stale served records are purged, so an anchor is never shared by two live lines. Because ownership is exclusive, an anchor resolves to exactly one file. Two byte-identical lines never share an anchor, and that guarantee sets the file size cap: the pool is the shipped table's 1,353,139 entries (not all 52⁴ letter combinations), so a file can hold at most 1,353,139 lines, beyond which `read`, `replace`, `replace_match`, `insert`, `copy`, and `move` reject with `[E_FILE_TOO_LARGE]` (use `write` for very large files).
|
|
507
506
|
|
|
508
507
|
### Ownership and mapping across edits
|
|
509
508
|
|
package/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { initHasher } from "./src/hashline";
|
|
4
|
-
import {
|
|
4
|
+
import { regReplaceMatch } from "./src/replace-match";
|
|
5
5
|
import { regReplace } from "./src/replace";
|
|
6
6
|
import { regInsert } from "./src/insert";
|
|
7
7
|
import { regCopy, regMove } from "./src/copy-move";
|
|
@@ -21,7 +21,7 @@ import {
|
|
|
21
21
|
cycleAutoReadAllMode,
|
|
22
22
|
toggleAnchorGrep,
|
|
23
23
|
toggleCopyMove,
|
|
24
|
-
|
|
24
|
+
toggleReplaceMatch,
|
|
25
25
|
toggleRequirePath,
|
|
26
26
|
toggleStrictInput,
|
|
27
27
|
adjustDiffContextLines,
|
|
@@ -45,7 +45,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
45
45
|
regRead(pi);
|
|
46
46
|
|
|
47
47
|
regReplace(pi);
|
|
48
|
-
|
|
48
|
+
regReplaceMatch(pi);
|
|
49
49
|
regInsert(pi);
|
|
50
50
|
regCopy(pi);
|
|
51
51
|
regMove(pi);
|
|
@@ -64,7 +64,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
64
64
|
const flags = await currentEditFlags();
|
|
65
65
|
regRead(pi, flags);
|
|
66
66
|
regReplace(pi, flags);
|
|
67
|
-
|
|
67
|
+
regReplaceMatch(pi, flags);
|
|
68
68
|
regInsert(pi, flags);
|
|
69
69
|
regCopy(pi, flags);
|
|
70
70
|
regMove(pi, flags);
|
|
@@ -104,7 +104,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
104
104
|
pi.getActiveTools().filter((t) => {
|
|
105
105
|
if (config.anchorGrepEnabled ? t === "grep" : t === "anchor_grep") return false;
|
|
106
106
|
if (config.copyMoveEnabled === false && (t === "copy" || t === "move")) return false;
|
|
107
|
-
if (config.
|
|
107
|
+
if (config.replaceMatchEnabled === false && t === "replace_match") return false;
|
|
108
108
|
return true;
|
|
109
109
|
}),
|
|
110
110
|
);
|
|
@@ -139,7 +139,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
139
139
|
}));
|
|
140
140
|
|
|
141
141
|
pi.registerCommand("hashline-config", {
|
|
142
|
-
description: "Open the hashline settings window (auto-read, auto-read all, ignore folders/files, diff context, grep, copy/move,
|
|
142
|
+
description: "Open the hashline settings window (auto-read, auto-read all, ignore folders/files, diff context, grep, copy/move, replace_match, path, strict input)",
|
|
143
143
|
handler: async (_args, ctx) => {
|
|
144
144
|
if (!ctx.hasUI) {
|
|
145
145
|
ctx.ui.notify("/hashline-config requires interactive mode", "error");
|
|
@@ -165,10 +165,10 @@ export default function (pi: ExtensionAPI): void {
|
|
|
165
165
|
const active = pi.getActiveTools();
|
|
166
166
|
pi.setActiveTools(enabled ? [...new Set([...active, "copy", "move"])] : active.filter((t) => t !== "copy" && t !== "move"));
|
|
167
167
|
}
|
|
168
|
-
else if (key === "
|
|
169
|
-
const enabled = await
|
|
168
|
+
else if (key === "replaceMatchEnabled") {
|
|
169
|
+
const enabled = await toggleReplaceMatch();
|
|
170
170
|
const active = pi.getActiveTools();
|
|
171
|
-
pi.setActiveTools(enabled ? [...new Set([...active, "
|
|
171
|
+
pi.setActiveTools(enabled ? [...new Set([...active, "replace_match"])] : active.filter((t) => t !== "replace_match"));
|
|
172
172
|
}
|
|
173
173
|
else if (key === "requirePath") await toggleRequirePath();
|
|
174
174
|
else if (key === "strictInput") await toggleStrictInput();
|
|
@@ -262,7 +262,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
262
262
|
|
|
263
263
|
if (
|
|
264
264
|
event.toolName !== "replace" &&
|
|
265
|
-
event.toolName !== "
|
|
265
|
+
event.toolName !== "replace_match" &&
|
|
266
266
|
event.toolName !== "insert" &&
|
|
267
267
|
event.toolName !== "copy" &&
|
|
268
268
|
event.toolName !== "move" &&
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-hashline-edit-pro",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "6.0.1",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Hash-anchored read/replace/insert/grep tools for pi-coding-agent. Every line gets a unique 4-char tokenizer-friendly anchor that stays stable across edits; stale or ambiguous anchors are rejected, never fuzzy-matched. Undo persists across restarts.",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -1,2 +1 @@
|
|
|
1
1
|
- `copy`: the same anchor in `source_from` and `source_to` copies one line; copied lines get fresh anchors in the post-edit diff and the source rows keep theirs.
|
|
2
|
-
- `copy`: for a block copy, this tool is the cheap path: only the source's two boundary rows and the destination line need serving, the interior transfers verbatim, and the block lands in one commit. Rebuilding the file with shell commands costs range verification, the post-edit diff, and undo; to append at the end, use the destination's last served line as `insert_after`.
|
package/prompts/copy-snippet.md
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
Copy lines to another position by anchor: `source_from`/`source_to` mark the range, the copy lands after `insert_after`;
|
|
1
|
+
Copy lines to another position by anchor: `source_from`/`source_to` mark the range, the copy lands after `insert_after`; each anchor resolves its own file
|
package/prompts/copy.md
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
Copy a range of lines to another position, targeted by 4-character anchors from any served anchor│content row. Give `source_from` and `source_to` as bare anchors marking the first and last line to copy in the source file, and `insert_after` as the bare anchor of the destination line after which the copy goes.
|
|
1
|
+
Copy a range of lines to another position, targeted by 4-character anchors from any served anchor│content row. Give `source_from` and `source_to` as bare anchors marking the first and last line to copy in the source file, and `insert_after` as the bare anchor of the destination line after which the copy goes. The source lines stay in place, and the copied lines are written exactly as the source file holds them.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A cross-file copy joins the destination file's batch. A copy always duplicates the content its source anchors were served from, so a same-message edit to the source file does not change what is copied.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
- `anchor_grep`: prefer it over shell `rg`/`grep` for searching, so hits carry anchors and need no `read`; use `literal: true` when the pattern contains regex metacharacters.
|
package/prompts/grep.md
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
Search text files with ripgrep. Hits and context lines come back as `lineNumber │ anchor│content` rows, editable with replace, insert, copy, or move without a new read; the `=== path ===` header and line numbers locate the match. Searches respect `.gitignore`, always skip `.git`, and skip binary and image files silently. A match over 500 bytes is shown as a `...` fragment around the hit, but its anchor still covers the whole line. When cut by match limit, raise `limit` (default 100); when cut by rows/bytes, refine `pattern` / narrow path.
|
|
1
|
+
Search text files with ripgrep. Hits and context lines come back as `lineNumber │ anchor│content` rows, editable with replace, insert, copy, or move without a new read; the `=== path ===` header and line numbers locate the match. Patterns are regex by default; set `literal: true` to search exact text, `ignoreCase` for case-insensitive search, and `context` for surrounding rows. Avoid backreferences, nested quantifiers, and quantified alternation. Searches respect `.gitignore`, always skip `.git`, and skip binary and image files silently. A match over 500 bytes is shown as a `...` fragment around the hit, but its anchor still covers the whole line. When cut by match limit, raise `limit` (default 100); when cut by rows/bytes, refine `pattern` / narrow path.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
Insert a text block after or before an anchor line: the anchor line stays; `
|
|
1
|
+
Insert a text block after or before an anchor line: the anchor line stays; `text` is the exact text without anchor prefixes
|
package/prompts/insert.md
CHANGED
|
@@ -1,3 +1 @@
|
|
|
1
|
-
Insert text after or before one existing line in a text file, addressed by a bare anchor from any served anchor│content row.
|
|
2
|
-
|
|
3
|
-
Same-file calls in one message batch: earlier calls reply `In batch N (queued)` and the last call shows the combined diff, with one undo for the whole batch.
|
|
1
|
+
Insert text after or before one existing line in a text file, addressed by the `anchor` field (a bare anchor from any served anchor│content row). `text` goes after the anchor line with `direction: "after"` or before it with `direction: "before"`. `text` is one string holding the exact text to insert. A trailing line break sets the last line's ending instead of adding a blank line, so a blank line next to the anchor line must be an extra break in `text`: `"x\n"` leaves the anchor line directly after `x`, and `"x\n\n"` leaves one blank line between them (`direction: "after"` mirrors this at the start of `text`).
|
|
@@ -1,2 +1 @@
|
|
|
1
|
-
- `move`: a
|
|
2
|
-
- `move`: for a block move, this tool is the cheap path: only the source's two boundary rows and the destination line need serving, the interior moves verbatim, and rebuilding the file with shell commands costs range verification, the post-edit diff, and undo; to append at the end, use the destination's last served line as `insert_after`.
|
|
1
|
+
- `move`: moving a range to where it already sits reports `No changes made` and leaves the anchors alone.
|
package/prompts/move-snippet.md
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
Move lines to another position by anchor: `source_from`/`source_to` mark the range, it lands after `insert_after`;
|
|
1
|
+
Move lines to another position by anchor: `source_from`/`source_to` mark the range, it lands after `insert_after`; each anchor resolves its own file
|
package/prompts/move.md
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
Move a range of lines to another position, targeted by 4-character anchors from any served anchor│content row. Give `source_from` and `source_to` as bare anchors marking the first and last line to move in the source file, and `insert_after` as the bare anchor of the destination line after which the range goes. `
|
|
1
|
+
Move a range of lines to another position, targeted by 4-character anchors from any served anchor│content row. Give `source_from` and `source_to` as bare anchors marking the first and last line to move in the source file, and `insert_after` as the bare anchor of the destination line after which the range goes. `insert_after` must sit outside the source range when both anchors share a file; moving a range in one file to where it already sits reports no change.
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
A cross-file `move` whose source file also has batched edits in the message commits on its own. A batched cross-file `move` commits its source removal with the batch but shows only the destination diff; read the source file for fresh anchors.
|
|
@@ -1,3 +1 @@
|
|
|
1
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`: `E_AUTO_READ_ALL` on an attached file means its content is still exactly as it was when attached at the start of this session.
|
|
3
|
-
- `read`: `[W_ANCHOR_RECLAIMED]` means the session's anchor quota was tight and the listed files' anchors were freed; read a freed file again before editing it.
|
package/prompts/read.md
CHANGED
|
@@ -1 +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
|
|
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.
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
- `replace`: same-
|
|
2
|
-
- `replace`: a pure deletion (`
|
|
3
|
-
- `replace`:
|
|
4
|
-
- `replace`: `replacement_lines` is one string; a pasted `anchor│` prefix is stripped; single line: same anchor for `remove_from` and `remove_to`.
|
|
1
|
+
- `replace`: same-message calls must target disjoint ranges.
|
|
2
|
+
- `replace`: a pure deletion (`text: ""`) is the cheap way to clear a large range: it verifies only the first and last line against the served record and removes the interior as it currently stands.
|
|
3
|
+
- `replace`: for a single line, use the same anchor for `remove_from` and `remove_to`.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
- `replace_match`: `old_string` is usually a fragment of the line, not the whole line; every occurrence inside the range is replaced, so scope the range with anchors when only some occurrences should change.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Replace part of a line or range by anchor: every occurrence of `old_string` inside `replace_from`/`replace_to` is replaced by `new_string`, leaving the rest of the text untouched
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
Replace part of a line (or a range of lines) without retyping the rest. `replace_from` and `replace_to` are bare anchors from served `anchor│content` rows marking the first and last line of the range; use the same anchor for one line. `old_string` is the text to find inside that range, and `new_string` replaces every occurrence of it.
|
|
2
|
+
|
|
3
|
+
The two boundary anchors must still match what was last shown; lines strictly inside the range are matched against the file as it is on disk.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
Replace lines by anchor: bare anchors in `remove_from`/`remove_to`, the exact replacement text in `
|
|
1
|
+
Replace lines by anchor: bare anchors in `remove_from`/`remove_to`, the exact replacement text in `text`
|
package/prompts/replace.md
CHANGED
|
@@ -1,6 +1,2 @@
|
|
|
1
|
-
Replace a range of lines (or a single line) in a text file by anchor. `remove_from` and `remove_to` are the 4-character anchors of the first and last line to remove, and `
|
|
2
|
-
To change only part of a line without retyping the rest, use `
|
|
3
|
-
|
|
4
|
-
Same-file calls in one message batch: earlier calls reply `In batch N (queued)` and the last call shows the combined diff, with one undo for the whole batch.
|
|
5
|
-
|
|
6
|
-
Example: read served `Hasu│old` and `arvm│old2`. Call { "remove_from": "Hasu", "remove_to": "arvm", "replacement_lines": "new line 1\nnew line 2" }. The post-edit diff shows `-Hasu│old`, `-arvm│old2`, `+Qwer│new line 1`: the `-` rows are dead anchors now; the `+` and ` ` rows are live anchors for the next edit.
|
|
1
|
+
Replace a range of lines (or a single line) in a text file by anchor. `remove_from` and `remove_to` are the 4-character anchors of the first and last line to remove, and `text` is one string holding the exact replacement text. The text is written exactly as given, and nothing else in the file changes.
|
|
2
|
+
To change only part of a line without retyping the rest, use `replace_match` instead; it preserves every character the request does not name. Deleting every line empties the file; the result names the new empty-line anchor, so a follow-up `replace` on it can seed content without a `read`.
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
- `copy`/`move`: for a block transfer, only the source's two boundary rows and the destination line need serving; the interior transfers verbatim and the block lands in one commit. To append at the end, use the destination's last served line as `insert_after`.
|
|
2
|
+
- `copy`/`move`: `insert_after` must sit outside the source range; `source_to` places the block right after itself.
|
|
3
|
+
- `copy`/`move`: `source_from` and `source_to` may live in a different file than `insert_after`; an empty destination file is seeded with the copied or moved lines.
|
|
4
|
+
- `move`: a batched cross-file move does not display the source file's diff; its removal commits with the batch, and the source file needs a read for fresh anchors.
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
- `undo_last_change`:
|
|
1
|
+
- `undo_last_change`: a `write` clears the history, so undo right after a bad diff — review the diff's `-anchor│` rows first to confirm what you're restoring.
|
|
2
2
|
- `undo_last_change`: a cross-file `move` records one undo entry per file; undo both the source and the destination to revert the whole move, because undoing one side alone leaves the moved lines duplicated or missing.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
Single-level undo: reverts a file's last `replace`, `
|
|
1
|
+
Single-level undo: reverts a file's last `replace`, `replace_match`, `insert`, `copy`, or `move`
|
|
@@ -1 +1 @@
|
|
|
1
|
-
Undo the last replace,
|
|
1
|
+
Undo the last replace, replace_match, insert, copy, or move on a file, restoring the previous content, BOM, and line endings. Use it when an edit removed or changed the wrong lines. If the file changed since that edit, the undo is refused and the file is left untouched; do not force it. A file deleted since the edit is restored. If the diff is truncated, use read to see the full file.
|