pi-hashline-edit-pro 6.3.0 → 6.4.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 +32 -31
- package/index.ts +32 -16
- package/package.json +1 -1
- package/src/auto-read-all.ts +1 -1
- package/src/batch.ts +6 -4
- package/src/config-ui.ts +1 -1
- package/src/constants.ts +5 -2
- package/src/copy-move.ts +90 -65
- package/src/edit-common.ts +93 -51
- package/src/edit-fidelity.ts +16 -6
- package/src/grep.ts +22 -11
- package/src/insert.ts +5 -4
- package/src/model-gate.ts +1 -1
- package/src/payload-contract.ts +13 -9
- package/src/prompts.ts +32 -0
- package/src/read.ts +5 -10
- package/src/replace-match.ts +2 -2
- package/src/replace-render.ts +4 -4
- package/src/replace-response.ts +1 -0
- package/src/replace-undo.ts +2 -2
- package/src/replace.ts +2 -2
- package/src/write-hook.ts +1 -1
- package/tool-prompts/copy-snippet.md +1 -1
- package/tool-prompts/copy.md +2 -2
- package/tool-prompts/grep-guidelines.md +2 -1
- package/tool-prompts/grep-snippet.md +1 -1
- package/tool-prompts/grep.md +1 -1
- package/tool-prompts/insert-guidelines.md +1 -1
- package/tool-prompts/insert.md +1 -1
- package/tool-prompts/move-snippet.md +1 -1
- package/tool-prompts/move.md +2 -2
- package/tool-prompts/read-guidelines.md +3 -3
- package/tool-prompts/read.md +1 -3
- package/tool-prompts/replace-match-guidelines.md +1 -0
- package/tool-prompts/replace-match-snippet.md +1 -1
- package/tool-prompts/replace-match.md +1 -1
- package/tool-prompts/replace.md +3 -2
- package/tool-prompts/transfer-guidelines.md +3 -3
- package/tool-prompts/undo-last-change-guidelines.md +1 -1
- package/tool-prompts/undo-last-change-snippet.md +1 -1
- package/tool-prompts/undo-last-change.md +1 -1
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ the result is the post-edit diff with fresh anchors, so the next edit needs no r
|
|
|
34
34
|
- [insert](#insert)
|
|
35
35
|
- [copy](#copy)
|
|
36
36
|
- [move](#move)
|
|
37
|
-
- [
|
|
37
|
+
- [grep](#grep)
|
|
38
38
|
- [undo_last_change](#undo_last_change)
|
|
39
39
|
- [Batching](#batching)
|
|
40
40
|
- [Auto-read](#auto-read)
|
|
@@ -88,7 +88,7 @@ pi install /path/to/pi-hashline-edit-pro
|
|
|
88
88
|
| --- | --- |
|
|
89
89
|
| `read` | overridden, returns `anchor│content` rows |
|
|
90
90
|
| `edit` | disabled |
|
|
91
|
-
| `grep` |
|
|
91
|
+
| `grep` | replaced by an anchored `grep`; pi's own `grep` returns when Anchor grep is off |
|
|
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 |
|
|
@@ -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`, `replace_match`, `insert`, `copy`, `move`, `
|
|
158
|
+
The extension registers eight tools: `read`, `replace`, `replace_match`, `insert`, `copy`, `move`, `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` do not need a `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. A `path` passed anyway is checked against anchor ownership: a matching path is accepted, a mismatch is rejected. 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
|
-
| `text` | The exact text to write in place of the removed range, as one string: `""` deletes the range
|
|
191
|
+
| `text` | The exact text to write in place of the removed range, as one string: `""` deletes the range. A trailing line break sets the newline of the last line written instead of adding a blank line; a lone newline writes one blank line. Embedded `\r\n`/`\r`/`\n` are preserved; escapes in the tool call are decoded once when the call is parsed, and the tool then writes the string it receives as-is, never decoding again — `\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
|
`replace_from` and `replace_to` are accepted as aliases for `remove_from` and `remove_to`.
|
|
194
194
|
|
|
@@ -211,7 +211,7 @@ Auto-fixable slips fall into two groups. Fixed silently: a reversed range, embed
|
|
|
211
211
|
|
|
212
212
|
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`.
|
|
213
213
|
|
|
214
|
-
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, `
|
|
214
|
+
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, `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.
|
|
215
215
|
|
|
216
216
|
An edit that changes neither content nor line endings reports `No changes made` and leaves the anchors alone.
|
|
217
217
|
|
|
@@ -229,13 +229,13 @@ In a same-message batch it joins the other calls on its file: the batch validate
|
|
|
229
229
|
|
|
230
230
|
### insert
|
|
231
231
|
|
|
232
|
-
`insert` adds lines after or before an existing line without removing anything. Like `replace`,
|
|
232
|
+
`insert` adds lines after or before an existing line without removing anything. Like `replace`, `path` is unnecessary and must match anchor ownership when passed.
|
|
233
233
|
|
|
234
234
|
| Field | Description |
|
|
235
235
|
| --- | --- |
|
|
236
236
|
| `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. |
|
|
237
237
|
| `direction` | `"after"` inserts below the anchor line, `"before"` above it. |
|
|
238
|
-
| `text` | The exact text to insert, as one string: `""` inserts one blank line (the same as
|
|
238
|
+
| `text` | The exact text to insert, as one string: `""` inserts one blank line (the same as a lone newline). A trailing line break sets the newline of the last line written instead of adding a blank line. Never include the anchor line. Embedded `\r\n`/`\r`/`\n` are preserved; escapes in the tool call are decoded once when the call is parsed, and the tool then writes the string it receives as-is, never decoding again — `\uXXXX` is the character, `\\uXXXX` the literal text. Legacy arrays are converted to text (elements joined with LF); prefer the string form. |
|
|
239
239
|
|
|
240
240
|
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.
|
|
241
241
|
|
|
@@ -249,33 +249,34 @@ The same safety machinery as `replace` applies: undo is saved before the write (
|
|
|
249
249
|
|
|
250
250
|
### copy
|
|
251
251
|
|
|
252
|
-
`copy` duplicates a range of lines to another position without removing the source. `source_from` and `source_to` select lines in the source file; `
|
|
252
|
+
`copy` duplicates a range of lines to another position without removing the source. `source_from` and `source_to` select lines in the source file; `anchor` and `direction` (`"before"` or `"after"`) select the destination line and side, and the anchor may live in a different file. An empty destination file is seeded with the copied lines. It is a served-anchor edit like `replace` and `insert`: the source's first and last lines and the destination anchor line must come from rows you were shown, and the request is refused if they changed on disk or were never served; the interior of the range is transferred verbatim and does not need to have been shown. With `requirePath` on, `path` must match the source or the destination file.
|
|
253
253
|
|
|
254
254
|
| Field | Description |
|
|
255
255
|
| --- | --- |
|
|
256
256
|
| `source_from` | 4-char anchor marking the FIRST source line to copy (inclusive). |
|
|
257
257
|
| `source_to` | 4-char anchor marking the LAST source line to copy (inclusive). |
|
|
258
|
-
| `
|
|
258
|
+
| `anchor` | 4-char anchor of the destination line the block goes before or after, resolved in the destination file. |
|
|
259
|
+
| `direction` | `"after"` places the block below the anchor line, `"before"` above it. Within one file the insertion point must fall before the source range's first line or after its last. |
|
|
259
260
|
|
|
260
261
|
Example: read served `Hasu│old` in `a.ts` and `Qwer│top` in `b.ts`; to copy `old` into `b.ts` below `top`:
|
|
261
262
|
|
|
262
263
|
```json
|
|
263
|
-
{ "source_from": "Hasu", "source_to": "Hasu", "
|
|
264
|
+
{ "source_from": "Hasu", "source_to": "Hasu", "anchor": "Qwer", "direction": "after" }
|
|
264
265
|
```
|
|
265
266
|
|
|
266
|
-
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.
|
|
267
|
+
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. Anchoring the block's last line with `direction: "after"` (or its first line with `direction: "before"`) duplicates the block right beside itself.
|
|
267
268
|
|
|
268
269
|
### move
|
|
269
270
|
|
|
270
|
-
`move` relocates a range of lines in one call: the range is removed from the source file and written after `
|
|
271
|
+
`move` relocates a range of lines in one call: the range is removed from the source file and written before or after `anchor`, 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, the destination position 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.
|
|
271
272
|
|
|
272
273
|
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.
|
|
273
274
|
|
|
274
|
-
###
|
|
275
|
+
### grep
|
|
275
276
|
|
|
276
|
-
`
|
|
277
|
+
`grep` is an anchored search backed by ripgrep, registered under pi's own `grep` name. It is enabled by default; turn Anchor grep off in `/hashline-config` (or set `anchorGrepEnabled` to `false` in the config file) to register pi's plain `grep` in its place.
|
|
277
278
|
|
|
278
|
-
Every matching line
|
|
279
|
+
Every matching line is returned as `lineNumber:│ anchor│content` and each requested context line as `lineNumber-│ anchor│content`, so the `:`/`-` gutter always tells a hit from a context row. The `anchor│content` part is served exactly like `read` output, so you can target it with `replace` or `insert` without a separate `read`; the line-number gutter and `=== path ===` header give filename and line for navigation.
|
|
279
280
|
|
|
280
281
|
| Field | Description |
|
|
281
282
|
| --- | --- |
|
|
@@ -318,7 +319,7 @@ Multiple `replace`, `replace_match`, `insert`, `copy`, and `move` calls on the s
|
|
|
318
319
|
- Before committing, the batch validates every call against the pre-batch state. A call whose own request is invalid (`[E_BAD_SHAPE]`, `[E_BAD_REF]`, `[E_STALE_ANCHOR]`, `[E_RANGE_STALE]`, `[E_SUBSTRING_NOT_FOUND]`, or a strict-input rejection) is left out: it fails on its own call without an `Aborts batch N.` suffix, and the remaining calls still commit as one batch with one undo. A failure of the file, the commit, or a cross-file member aborts the whole batch instead: nothing is written, the failing call's error ends with `Aborts batch N.`, and an earlier member's row renders the abort message instead of the queued placeholder.
|
|
319
320
|
- A batch member accepts the same request shapes and auto-fixes as a standalone call.
|
|
320
321
|
|
|
321
|
-
Batched calls must target disjoint ranges; overlapping ranges abort the whole batch unwritten. A `copy`'s `
|
|
322
|
+
Batched calls must target disjoint ranges; overlapping ranges abort the whole batch unwritten. A `copy`'s `anchor` line is the copy's destination range, so replacing or moving that same line in the batch is an overlap. One `insert`, `copy`, or cross-file `move` 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 a batch-level step aborts its batch-mates with `[E_OP_ABORTED]`; a call left out of the batch does not.
|
|
322
323
|
|
|
323
324
|
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; when their own request then fails validation against the pre-batch state, they are left out of the batch and fail on their own 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.
|
|
324
325
|
|
|
@@ -344,13 +345,13 @@ Each attached file starts with a header (`=== path ===` in `full` mode, `=== pat
|
|
|
344
345
|
|
|
345
346
|
A coverage line after the header reports how many attached files were served in full; a section truncated by the per-file budget is not counted as complete. Each attached file begins with a `=== path ===` header, and files that were not attached are named in the footer.
|
|
346
347
|
|
|
347
|
-
The setting lives in `/hashline-config` as Auto-read all and in `config.json` as `autoReadAll` (`"off"`, `"outline"`, or `"full"`; older configs with `"on"` or `true` are read as `"full"`, `"git"` as `"full"`, `"outline(git)"` as `"outline"` — both with Git repos only on — and `false` as `"off"`), and as `autoReadAllRequireGit`, the Git repos only switch (on by default). Extra folders and files are ignored via `/hashline-config` as Ignore folders/files and via `config.json` as `autoReadAllIgnore` (array of folder names, file names, or globs, for example `["docs", "scratch.md", "*.test.ts"]`). A single-segment entry skips any folder or file with that exact name (case-insensitive); an entry containing glob characters (`*`, `?`, `[`, `]`, `{`, `}`) is matched as a glob against the file name, or against the whole relative path when it also contains a slash, with the same syntax as `
|
|
348
|
+
The setting lives in `/hashline-config` as Auto-read all and in `config.json` as `autoReadAll` (`"off"`, `"outline"`, or `"full"`; older configs with `"on"` or `true` are read as `"full"`, `"git"` as `"full"`, `"outline(git)"` as `"outline"` — both with Git repos only on — and `false` as `"off"`), and as `autoReadAllRequireGit`, the Git repos only switch (on by default). Extra folders and files are ignored via `/hashline-config` as Ignore folders/files and via `config.json` as `autoReadAllIgnore` (array of folder names, file names, or globs, for example `["docs", "scratch.md", "*.test.ts"]`). A single-segment entry skips any folder or file with that exact name (case-insensitive); an entry containing glob characters (`*`, `?`, `[`, `]`, `{`, `}`) is matched as a glob against the file name, or against the whole relative path when it also contains a slash, with the same syntax as `grep`'s `glob`; any other entry with a slash (for example `"src/tmp"`) skips that path. Custom ignores are counted with the vendor/name/pattern skips in the footer.
|
|
348
349
|
|
|
349
350
|
## Configuration
|
|
350
351
|
|
|
351
352
|
| Command | Description |
|
|
352
353
|
| --- | --- |
|
|
353
|
-
| `/hashline-config` | Open the settings window: auto-read anchors, auto-read all mode, git repos only, ignore folders/files, disable on models, read on disabled models, diff context lines, `
|
|
354
|
+
| `/hashline-config` | Open the settings window: auto-read anchors, auto-read all mode, git repos only, ignore folders/files, disable on models, read on disabled models, diff context lines, `grep` tool, copy/move tools, replace_match tool, required `path`, and strict input. Persists across sessions. |
|
|
354
355
|
| `/clear-anchors` | Clear the session's anchor claims. Anchors are re-claimed on the next `read`. |
|
|
355
356
|
|
|
356
357
|
Settings live in `~/.config/pi-hashline-edit-pro/config.json`, created when a setting is first changed in `/hashline-config`:
|
|
@@ -378,7 +379,7 @@ Settings live in `~/.config/pi-hashline-edit-pro/config.json`, created when a se
|
|
|
378
379
|
| `autoReadAll` | Auto-read all | `"off"` | Attachment mode: `"off"`, `"outline"` (anchor-stamped per-file outlines), or `"full"` (full contents). Legacy `"git"`/`"outline(git)"` read as `"full"`/`"outline"` with Git repos only on; `"on"`/`true` read as `"full"`. |
|
|
379
380
|
| `autoReadAllRequireGit` | Git repos only | `true` | Run the injection only when the working directory is inside a git repository; when off, auto-read all also attaches files discovered by `ripgrep` or the directory walk. |
|
|
380
381
|
| `autoReadAllIgnore` | Ignore folders/files | `[]` | Extra folder names, file names, or globs skipped by auto-read all. |
|
|
381
|
-
| `anchorGrepEnabled` | Anchor grep | `true` | Register `
|
|
382
|
+
| `anchorGrepEnabled` | Anchor grep | `true` | Register an anchored `grep` in place of pi's own `grep` while it is on. |
|
|
382
383
|
| `copyMoveEnabled` | Copy/move | `true` | Offer the `copy` and `move` tools; when off, both are removed from the active tools. |
|
|
383
384
|
| `replaceMatchEnabled` | Replace match | `true` | Offer the `replace_match` tool; when off, it is removed from the active tools. |
|
|
384
385
|
| `requirePath` | Require path | `false` | `replace`, `replace_match`, `insert`, `copy`, and `move` require a `path` argument that must match anchor ownership. |
|
|
@@ -387,7 +388,7 @@ Settings live in `~/.config/pi-hashline-edit-pro/config.json`, created when a se
|
|
|
387
388
|
| `disableOnModels` | Disable on models | `[]` | Model globs matched case-insensitively against `provider/id`, the bare `id`, and `api`, with `*` and `?` wildcards. A matching model gets no anchored edit tools, no auto-read, and no write-echo refusal; `read` stays available as pi's ordinary read unless Read on disabled models is `remove`. |
|
|
388
389
|
| `readOnDisabledModels` | Read on disabled models | `"vanilla"` | `read` for a model matched by `disableOnModels`: `"vanilla"` keeps pi's ordinary `read` active while the anchored edit tools stay off; `"remove"` disables `read` with the rest of the anchored tools. |
|
|
389
390
|
|
|
390
|
-
`disableOnModels` turns off the whole anchored surface for the models you name, so another edit tool (`apply_patch`, a shell read) can own the session without competing instructions. A match removes `replace`, `replace_match`, `insert`, `copy`, `move`, `
|
|
391
|
+
`disableOnModels` turns off the whole anchored surface for the models you name, so another edit tool (`apply_patch`, a shell read) can own the session without competing instructions. A match removes `replace`, `replace_match`, `insert`, `copy`, `move`, `grep`, and `undo_last_change` from the active tools, and also `read` when Read on disabled models is `remove`, registering pi's plain `grep` for those models, and it also skips the auto-read-all injection, the auto-read block after `write`, the post-edit diff substitution, and the `write` hook that refuses a `write` echoing a served anchor. Because the extension's `read` replaces the built-in one, a matched model has no `read` at all under `remove` unless another extension provides one; the default `vanilla` gives those models pi's ordinary `read` while the anchored edit tools stay off. The list is checked on `session_start`, on `model_select`, and again on `before_agent_start`, so a model change inside a session switches the surface immediately and another extension cannot re-add the tools mid-session. The default `[]` leaves every model unchanged.
|
|
391
392
|
|
|
392
393
|
When `PI_HASHLINE_DIR` is unset or empty, non-Windows platforms honor `XDG_CONFIG_HOME` when set (falling back to `~/.config`); on Windows the directory always uses `~/.config`, where `~` is `%USERPROFILE%`. To move the directory explicitly, see [Isolated state](#isolated-state).
|
|
393
394
|
|
|
@@ -401,15 +402,15 @@ When `PI_HASHLINE_DIR` is unset or empty, non-Windows platforms honor `XDG_CONFI
|
|
|
401
402
|
| File size | 100MB | all tools (`[E_FILE_TOO_LARGE]`) |
|
|
402
403
|
| Hash window | first 500 bytes of a line | anchor identity for long lines |
|
|
403
404
|
| Patch guard | 1MB of pre-edit + post-edit text | patch generation is skipped and `patchTruncated` is set |
|
|
404
|
-
| Grep matches | 100 matched lines by default (`limit`) | `
|
|
405
|
-
| Grep output | 2000 rows and 50KB | `
|
|
405
|
+
| Grep matches | 100 matched lines by default (`limit`) | `grep` |
|
|
406
|
+
| Grep output | 2000 rows and 50KB | `grep` |
|
|
406
407
|
| Grep row fragment | 500 bytes | long match and context rows shown as `...` fragments |
|
|
407
408
|
| Auto-read all files | 500 files, 200KB per file | files larger than 200KB and files past the cap are omitted |
|
|
408
409
|
| Auto-read all budget | 200KB floor, 2MB ceiling | total injection size, derived from the model context window |
|
|
409
410
|
| Stale-range feedback | first 100 lines | rows returned with `[E_RANGE_STALE]` |
|
|
410
411
|
| Session anchors | 1,353,139 anchors in use | all tools; the least recently read or edited files are freed when the quota is exhausted (`[W_ANCHOR_RECLAIMED]`) |
|
|
411
412
|
|
|
412
|
-
`
|
|
413
|
+
`grep` uses the same 100MB file-size cutoff as `read`. Files over the line cap are skipped silently in directory searches.
|
|
413
414
|
|
|
414
415
|
## Tool result details
|
|
415
416
|
|
|
@@ -421,7 +422,7 @@ All eight tools also declare an `outputSchema` and return `structuredContent`, s
|
|
|
421
422
|
|
|
422
423
|
`replace`, `replace_match`, `insert`, `copy`, `move`, and `undo_last_change` return `{ kind: "edit", verb, classification, path, text, diff, warnings, hints, firstChangedLine, anchors, anchorsOmitted }`, where `verb` is `"replaced"`, `"inserted"`, `"copied"`, `"moved"`, or `"undone"`, `anchors` are the live rows of the post-edit diff, and `anchorsOmitted` is true when the diff was truncated or carried no rows, meaning a fresh read is needed. A cross-file move reports the destination file and its anchors.
|
|
423
424
|
|
|
424
|
-
`
|
|
425
|
+
`grep` returns `{ kind: "grep", text, matches, files, truncated, results }`, where each entry of `results` is `{ path, matchLines, lines, hadUtf8DecodeErrors }` and `lines` uses the same `{ line, text, anchor, rendered }` shape as `read`.
|
|
425
426
|
|
|
426
427
|
| Tool | `details` |
|
|
427
428
|
| --- | --- |
|
|
@@ -430,7 +431,7 @@ All eight tools also declare an `outputSchema` and return `structuredContent`, s
|
|
|
430
431
|
| `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. |
|
|
431
432
|
| `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. |
|
|
432
433
|
| `undo_last_change` | `diff` (the undo diff with restored anchors), `patch`, `patchTruncated`, and `metrics` in the same shape as `replace`. |
|
|
433
|
-
| `
|
|
434
|
+
| `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). |
|
|
434
435
|
|
|
435
436
|
`snapshotId`, `firstChangedLine`, and `metrics.warnings` are the three fields external consumers most often read; `snapshotId` is a string fingerprint, `firstChangedLine` is a 1-based line number on the result file, and `metrics.warnings` counts the `[W_*]` notices in `details.warnings`; hint `[H_*]` notices live in `details.hints` and are not counted.
|
|
436
437
|
|
|
@@ -461,7 +462,7 @@ Full reference:
|
|
|
461
462
|
| `[W_INVALID_PATCH]` | A `text` line is a diff-preview row (`+anchor│`, `-anchor│`, `- │`). The marker is stripped automatically with a warning. |
|
|
462
463
|
| `[W_BARE_HASH_PREFIX]` | A `text` line starts with an `anchor│` prefix. The prefix is stripped automatically with a warning. |
|
|
463
464
|
| `[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. |
|
|
464
|
-
| `[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
|
|
465
|
+
| `[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 escapes in the tool call are decoded once when the call is parsed (`\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. |
|
|
465
466
|
| `[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. |
|
|
466
467
|
| `[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`. |
|
|
467
468
|
| `[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.` |
|
|
@@ -478,12 +479,12 @@ Full reference:
|
|
|
478
479
|
| `[E_FILE_TOO_LARGE]` | The file exceeds the 1,353,139-line hashline limit or the 100MB size limit. |
|
|
479
480
|
| `[E_REGISTRY]` | The anchor registry was not initialized; a serve or edit ran outside an initialized session. |
|
|
480
481
|
| `[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. |
|
|
481
|
-
| `[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
|
|
482
|
+
| `[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). |
|
|
482
483
|
| `[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. |
|
|
483
484
|
| `[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. |
|
|
484
485
|
| `[E_OP_ABORTED]` | A batch-level failure aborted the edit before writing (the base read failed, a cross-file member could not be prepared, the file changed or was deleted during the batch, or the composed result exceeded a limit). Nothing was written; a call whose own request is invalid is left out of the batch instead. Fix the cause and retry, otherwise call `read` for fresh anchors and retry. The abort names the failing call and its error code when one is known. |
|
|
485
|
-
| `[E_GREP_FAILED]` | `
|
|
486
|
-
| `[E_GREP_TIMEOUT]` | `
|
|
486
|
+
| `[E_GREP_FAILED]` | `grep` could not start ripgrep or ripgrep exited with an error (for example a pattern with backreferences or look-around, which ripgrep's regex engine does not support); the message carries ripgrep's output. Retry with `literal: true` or simplify the pattern. |
|
|
487
|
+
| `[E_GREP_TIMEOUT]` | `grep` timed out after 10 seconds; narrow `path` or simplify `pattern` and retry. |
|
|
487
488
|
|
|
488
489
|
## Troubleshooting
|
|
489
490
|
|
|
@@ -529,7 +530,7 @@ Background snapshot pruning and registry sidecar GC skip `EPERM`/`EACCES` withou
|
|
|
529
530
|
### Request flow
|
|
530
531
|
|
|
531
532
|
```text
|
|
532
|
-
read /
|
|
533
|
+
read / grep / auto-read-all ──► anchor registry (ownership + served records)
|
|
533
534
|
│
|
|
534
535
|
edit call ──► resolve anchors ──► apply ──► commit ──► post-edit diff with fresh anchors
|
|
535
536
|
│
|
|
@@ -538,7 +539,7 @@ edit call ──► resolve anchors ──► apply ──► commit ──► p
|
|
|
538
539
|
|
|
539
540
|
### Allocation
|
|
540
541
|
|
|
541
|
-
Anchors are allocated, never derived. Every line that is served to you, by `read`, `
|
|
542
|
+
Anchors are allocated, never derived. Every line that is served to you, by `read`, `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).
|
|
542
543
|
|
|
543
544
|
### Ownership and mapping across edits
|
|
544
545
|
|
package/index.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
-
import { createReadToolDefinition, DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { createReadToolDefinition, createGrepToolDefinition, DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { initHasher } from "./src/hashline";
|
|
4
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";
|
|
8
|
-
import { regGrep } from "./src/grep";
|
|
8
|
+
import { regGrep, regVanillaGrep } from "./src/grep";
|
|
9
9
|
import { regUndo, clearUndo } from "./src/replace-undo";
|
|
10
10
|
import { regRead, fmtReadPreview } from "./src/read";
|
|
11
11
|
import { ANCHOR_TOOL_NAMES, modelDisabled, type ModelLike } from "./src/model-gate";
|
|
@@ -67,7 +67,9 @@ export default function (pi: ExtensionAPI): void {
|
|
|
67
67
|
let readFlavor: "anchored" | "vanilla" = "anchored";
|
|
68
68
|
let readFlavorCwd: string | undefined;
|
|
69
69
|
let autoReadAllInjected = false;
|
|
70
|
-
let
|
|
70
|
+
let grepEnabled = true;
|
|
71
|
+
let grepFlavor: "anchored" | "vanilla" = "anchored";
|
|
72
|
+
let grepFlavorCwd: string | undefined;
|
|
71
73
|
const baseAnchorTools = new Set<string>();
|
|
72
74
|
let gateApplied = false;
|
|
73
75
|
|
|
@@ -80,7 +82,8 @@ export default function (pi: ExtensionAPI): void {
|
|
|
80
82
|
regInsert(pi, flags);
|
|
81
83
|
regCopy(pi, flags);
|
|
82
84
|
regMove(pi, flags);
|
|
83
|
-
|
|
85
|
+
if (grepFlavor === "vanilla" && grepFlavorCwd !== undefined) regVanillaGrep(pi, grepFlavorCwd);
|
|
86
|
+
else regGrep(pi, flags);
|
|
84
87
|
regUndo(pi, flags);
|
|
85
88
|
} catch (error) {
|
|
86
89
|
console.error("Failed to refresh edit tools:", error);
|
|
@@ -100,11 +103,22 @@ export default function (pi: ExtensionAPI): void {
|
|
|
100
103
|
readFlavorCwd = undefined;
|
|
101
104
|
await refreshEditTools();
|
|
102
105
|
}
|
|
106
|
+
const grepFlavorWanted = grepEnabled && !gated ? "anchored" : "vanilla";
|
|
107
|
+
if (grepFlavorWanted === "vanilla") {
|
|
108
|
+
if (grepFlavor !== "vanilla" || grepFlavorCwd !== cwd) {
|
|
109
|
+
pi.registerTool(createGrepToolDefinition(cwd));
|
|
110
|
+
grepFlavor = "vanilla";
|
|
111
|
+
grepFlavorCwd = cwd;
|
|
112
|
+
}
|
|
113
|
+
} else if (grepFlavor === "vanilla") {
|
|
114
|
+
grepFlavor = "anchored";
|
|
115
|
+
grepFlavorCwd = undefined;
|
|
116
|
+
await refreshEditTools();
|
|
117
|
+
}
|
|
103
118
|
if (gated) {
|
|
104
119
|
const active = pi.getActiveTools().filter((tool) => tool !== "edit");
|
|
105
|
-
const gatedTools =
|
|
120
|
+
const gatedTools = ANCHOR_TOOL_NAMES.filter((tool) => tool !== "grep" && (readOnDisabledModels !== "vanilla" || tool !== "read"));
|
|
106
121
|
const next = active.filter((tool) => !gatedTools.includes(tool));
|
|
107
|
-
if (grepWasActive && !next.includes("grep")) next.push("grep");
|
|
108
122
|
pi.setActiveTools(next);
|
|
109
123
|
gateApplied = true;
|
|
110
124
|
return;
|
|
@@ -122,17 +136,16 @@ export default function (pi: ExtensionAPI): void {
|
|
|
122
136
|
if (!baseAnchorTools.has(tool)) return false;
|
|
123
137
|
if (tool === "replace_match") return config.replaceMatchEnabled !== false;
|
|
124
138
|
if (tool === "copy" || tool === "move") return config.copyMoveEnabled !== false;
|
|
125
|
-
if (tool === "
|
|
139
|
+
if (tool === "grep") return config.anchorGrepEnabled === true;
|
|
126
140
|
return true;
|
|
127
141
|
});
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
pi.setActiveTools(
|
|
142
|
+
const active = pi.getActiveTools().filter((tool) => tool !== "edit");
|
|
143
|
+
const base = enabled.includes("grep") ? active.filter((tool) => tool !== "grep") : active;
|
|
144
|
+
pi.setActiveTools([...new Set([...base, ...enabled])]);
|
|
131
145
|
}
|
|
132
146
|
|
|
133
147
|
pi.on("session_start", async (_event, ctx) => withAnchorSession(ctx, async () => {
|
|
134
148
|
const active = pi.getActiveTools();
|
|
135
|
-
grepWasActive = active.includes("grep");
|
|
136
149
|
baseAnchorTools.clear();
|
|
137
150
|
for (const tool of ANCHOR_TOOL_NAMES) {
|
|
138
151
|
if (active.includes(tool)) baseAnchorTools.add(tool);
|
|
@@ -159,12 +172,14 @@ export default function (pi: ExtensionAPI): void {
|
|
|
159
172
|
autoReadAllIgnore = config.autoReadAllIgnore ?? [];
|
|
160
173
|
disableOnModels = config.disableOnModels ?? [];
|
|
161
174
|
readOnDisabledModels = config.readOnDisabledModels ?? "vanilla";
|
|
175
|
+
grepEnabled = config.anchorGrepEnabled === true;
|
|
176
|
+
grepFlavor = grepEnabled ? "anchored" : "vanilla";
|
|
177
|
+
grepFlavorCwd = grepEnabled ? undefined : ctx.cwd;
|
|
162
178
|
const sessionBranch = (ctx as { sessionManager?: { getBranch?: () => Array<{ type?: string; customType?: string }> } }).sessionManager?.getBranch?.() ?? [];
|
|
163
179
|
autoReadAllInjected = sessionBranch.some((entry) => entry.type === "custom_message" && entry.customType === AUTO_READ_ALL_CUSTOM_TYPE);
|
|
164
180
|
await refreshEditTools();
|
|
165
181
|
pi.setActiveTools(
|
|
166
182
|
pi.getActiveTools().filter((t) => {
|
|
167
|
-
if (config.anchorGrepEnabled ? t === "grep" : t === "anchor_grep") return false;
|
|
168
183
|
if (config.copyMoveEnabled === false && (t === "copy" || t === "move")) return false;
|
|
169
184
|
if (config.replaceMatchEnabled === false && t === "replace_match") return false;
|
|
170
185
|
return true;
|
|
@@ -228,10 +243,11 @@ export default function (pi: ExtensionAPI): void {
|
|
|
228
243
|
else if (key === "diffContextLines") await adjustDiffContextLines(delta ?? 1);
|
|
229
244
|
else if (key === "anchorGrepEnabled") {
|
|
230
245
|
const enabled = await toggleAnchorGrep();
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
246
|
+
grepEnabled = enabled;
|
|
247
|
+
grepFlavor = enabled ? "anchored" : "vanilla";
|
|
248
|
+
grepFlavorCwd = enabled ? undefined : ctx.cwd;
|
|
249
|
+
if (enabled) baseAnchorTools.add("grep");
|
|
250
|
+
else baseAnchorTools.delete("grep");
|
|
235
251
|
}
|
|
236
252
|
else if (key === "copyMoveEnabled") {
|
|
237
253
|
const enabled = await toggleCopyMove();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-hashline-edit-pro",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.4.0",
|
|
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",
|
package/src/auto-read-all.ts
CHANGED
|
@@ -30,7 +30,7 @@ const HEADER =
|
|
|
30
30
|
"[hashline auto-read-all] Every non-ignored project file is attached below as `=== path ===` then `anchor│content` rows with live anchors.\nEdit directly from the attachment with replace and insert; do not call read for attached files.\nFiles listed as not attached can be read normally.\nAnchors are case-sensitive and stay valid until their line is edited.";
|
|
31
31
|
|
|
32
32
|
const OUTLINE_HEADER =
|
|
33
|
-
"[hashline auto-read-all] Every non-ignored project file is outlined below as `=== path (language) — lines ===` then `anchor│...` rows with live anchors: multi-line symbols show a label with `[limit N]`, the span to request when reading from the anchor, and single-line symbols show the line content.\nEdit a row's anchor with replace or insert, or read from it with `offset` set to that anchor and the row's `limit
|
|
33
|
+
"[hashline auto-read-all] Every non-ignored project file is outlined below as `=== path (language) — lines ===` then `anchor│...` rows with live anchors: multi-line symbols show a label with `[limit N]`, the span to request when reading from the anchor, and single-line symbols show the line content.\nEdit a row's anchor with replace or insert, or read from it with `offset` set to that anchor (and the row's `limit` when it shows one); do not call read for attached files.\nFiles listed as not attached can be read normally.\nAnchors are case-sensitive and stay valid until their line is edited.";
|
|
34
34
|
|
|
35
35
|
const IMAGE_EXTENSIONS = new Set([
|
|
36
36
|
".avif",
|
package/src/batch.ts
CHANGED
|
@@ -189,11 +189,12 @@ type NormalizedEditArgs =
|
|
|
189
189
|
| { kind: "replace"; removeFrom: string; removeTo?: string; path?: string }
|
|
190
190
|
| { kind: "insert"; anchor: string; path?: string }
|
|
191
191
|
| { kind: "replace_match"; replaceFrom: string; replaceTo?: string; path?: string }
|
|
192
|
-
| { kind: "copy" | "move"; sourceFrom: string; sourceTo?: string;
|
|
192
|
+
| { kind: "copy" | "move"; sourceFrom: string; sourceTo?: string; anchor?: string; direction?: string; path?: string };
|
|
193
193
|
|
|
194
194
|
const MAX_TRACKED_BATCHES = 256;
|
|
195
195
|
const BATCH_DISCARDED_NOTE = "Nothing was written; the whole batch was discarded.";
|
|
196
196
|
const BATCHABLE_CALLS = new Set(["replace", "insert", "replace_match", "copy", "move"]);
|
|
197
|
+
const MERGEABLE_PIECE_KINDS = new Set<BatchKind>(["insert", "copy", "move"]);
|
|
197
198
|
|
|
198
199
|
const plan = new Map<string, PlannedMember>();
|
|
199
200
|
const batches = new Map<number, BatchState>();
|
|
@@ -267,7 +268,8 @@ function normalizeEditArgs(name: string, args: unknown): NormalizedEditArgs | un
|
|
|
267
268
|
kind: name,
|
|
268
269
|
sourceFrom: normalized.source_from,
|
|
269
270
|
...(typeof normalized.source_to === "string" ? { sourceTo: normalized.source_to } : {}),
|
|
270
|
-
...(typeof normalized.
|
|
271
|
+
...(typeof normalized.anchor === "string" ? { anchor: normalized.anchor } : {}),
|
|
272
|
+
...(typeof normalized.direction === "string" ? { direction: normalized.direction } : {}),
|
|
271
273
|
...(path ? { path } : {}),
|
|
272
274
|
};
|
|
273
275
|
}
|
|
@@ -332,7 +334,7 @@ async function resolveBatchCallTarget(
|
|
|
332
334
|
const sourceTarget = normalized.sourceTo !== undefined
|
|
333
335
|
? tryResolveEditTarget(normalized.sourceFrom, normalized.sourceTo)
|
|
334
336
|
: tryResolveEditTarget(normalized.sourceFrom);
|
|
335
|
-
const destinationTarget = normalized.
|
|
337
|
+
const destinationTarget = normalized.anchor !== undefined ? tryResolveEditTarget(normalized.anchor) : undefined;
|
|
336
338
|
if (sourceTarget === undefined) return undefined;
|
|
337
339
|
if (normalized.kind === "move") {
|
|
338
340
|
if (sourceTarget === destinationTarget) return { target: sourceTarget, sourceTarget };
|
|
@@ -811,7 +813,7 @@ function changesEnding(piece: BatchPiece, baseSeparators: LineEnding[]): boolean
|
|
|
811
813
|
function mergeInsertPairs(pieces: BatchPiece[]): BatchPiece[] {
|
|
812
814
|
const byAnchor = new Map<number, BatchPiece[]>();
|
|
813
815
|
for (const piece of pieces) {
|
|
814
|
-
if (piece.kind
|
|
816
|
+
if (!MERGEABLE_PIECE_KINDS.has(piece.kind) || piece.start !== piece.end || piece.foldedLines === 0) continue;
|
|
815
817
|
const group = byAnchor.get(piece.start) ?? [];
|
|
816
818
|
group.push(piece);
|
|
817
819
|
byAnchor.set(piece.start, group);
|
package/src/config-ui.ts
CHANGED
|
@@ -33,7 +33,7 @@ export function configRows(config: Config): ConfigRow[] {
|
|
|
33
33
|
{ key: "autoReadAll", group: "Auto-read all", depth: 0, label: "Auto-read all", hint: "Attach project files when a session starts", enabled: (config.autoReadAll ?? "off") !== "off", mode: config.autoReadAll ?? "off", cycle: ["off", "outline", "full"] },
|
|
34
34
|
{ key: "autoReadAllRequireGit", group: "Auto-read all", depth: 1, dependsOn: "autoReadAll", gated: true, label: "Git repos only", hint: "Attach only inside a git repository", enabled: config.autoReadAllRequireGit !== false },
|
|
35
35
|
{ key: "autoReadAllIgnore", group: "Auto-read all", depth: 1, dependsOn: "autoReadAll", label: "Ignore folders/files", hint: "Folders, files, or globs that auto-read all skips (comma-separated)", enabled: (config.autoReadAllIgnore ?? []).length > 0, entries: config.autoReadAllIgnore ?? [] },
|
|
36
|
-
{ key: "anchorGrepEnabled", group: "Tools", depth: 0, label: "Anchor grep", hint: "Use
|
|
36
|
+
{ key: "anchorGrepEnabled", group: "Tools", depth: 0, label: "Anchor grep", hint: "Use the anchored grep in place of pi's own grep", enabled: config.anchorGrepEnabled === true },
|
|
37
37
|
{ key: "copyMoveEnabled", group: "Tools", depth: 0, label: "Copy/move", hint: "Enable the copy and move tools", enabled: config.copyMoveEnabled !== false },
|
|
38
38
|
{ key: "replaceMatchEnabled", group: "Tools", depth: 0, label: "Replace match", hint: "Enable the replace_match tool", enabled: config.replaceMatchEnabled !== false },
|
|
39
39
|
{ key: "requirePath", group: "Edit behavior", depth: 0, label: "Require path", hint: "Edit tools must also send a matching path", enabled: config.requirePath === true },
|
package/src/constants.ts
CHANGED
|
@@ -8,16 +8,19 @@ export const MAX_GREP_LINE_BYTES = 500;
|
|
|
8
8
|
|
|
9
9
|
export const MAX_DIFF_INPUT_BYTES = 1024 * 1024;
|
|
10
10
|
|
|
11
|
+
export const TEXT_ENDING_NOTE =
|
|
12
|
+
"A trailing line break sets the newline of the last line written instead of adding a blank line; a lone newline writes one blank line.";
|
|
13
|
+
|
|
11
14
|
export const HASH_STORE_BUSY_TIMEOUT = 1000;
|
|
12
15
|
export const HASH_STORE_VERSION = 9;
|
|
13
16
|
export const NEW_CONTENT_NOT_ARRAY_MSG =
|
|
14
17
|
`[E_BAD_SHAPE] "text" must be an array of strings, one per line (use [] to delete).`;
|
|
15
18
|
|
|
16
19
|
export const NEW_CONTENT_NOT_STRING_MSG =
|
|
17
|
-
`[E_BAD_SHAPE] "text" must be a string holding the exact text to write. Use "" to delete the range and
|
|
20
|
+
`[E_BAD_SHAPE] "text" must be a string holding the exact text to write. Use "" to delete the range and a lone newline for one blank line; line breaks inside the string separate lines.`;
|
|
18
21
|
|
|
19
22
|
export const TEXT_NOT_STRING_MSG =
|
|
20
|
-
`[E_BAD_SHAPE] "text" must be a string holding the exact text to insert. Use
|
|
23
|
+
`[E_BAD_SHAPE] "text" must be a string holding the exact text to insert. Use a lone newline for one blank line; line breaks inside the string separate lines.`;
|
|
21
24
|
|
|
22
25
|
export const NUL_CONTENT_MSG =
|
|
23
26
|
`[E_BAD_SHAPE] Content contains a NUL byte (U+0000); a text file cannot contain NUL, and writing it would break further reads and edits. Remove the NUL byte and retry.`;
|