pi-hashline-edit-pro 6.1.2 → 6.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -23
- package/index.ts +20 -12
- package/package.json +3 -2
- package/src/anchor-registry.ts +22 -5
- package/src/auto-read-all.ts +39 -21
- package/src/batch.ts +127 -41
- package/src/commit.ts +1 -1
- package/src/config-ui.ts +135 -55
- package/src/config.ts +27 -9
- package/src/copy-move.ts +61 -72
- package/src/edit-common.ts +6 -4
- package/src/grep.ts +53 -121
- package/src/hash-store.ts +13 -12
- package/src/hashline/resolve.ts +13 -2
- package/src/insert.ts +40 -34
- package/src/line-endings.ts +10 -1
- package/src/outline/index.ts +46 -0
- package/src/outline/parsers.ts +42 -0
- package/src/outline/render.ts +170 -0
- package/src/outline/vendor/LICENSE +201 -0
- package/src/outline/vendor/README.md +17 -0
- package/src/outline/vendor/parsers/blade.ts +138 -0
- package/src/outline/vendor/parsers/index.ts +75 -0
- package/src/outline/vendor/parsers/java.ts +501 -0
- package/src/outline/vendor/parsers/manager.ts +1721 -0
- package/src/outline/vendor/parsers/php.ts +332 -0
- package/src/outline/vendor/parsers/sass.ts +416 -0
- package/src/outline/vendor/parsers/scss.ts +566 -0
- package/src/outline/vendor/types.ts +82 -0
- package/src/outline/vendor/wasm/LICENSES.md +37 -0
- package/src/outline/vendor/wasm/tree-sitter-blade.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-go.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-java.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-javascript.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-php.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-python.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-rust.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-sass.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-scss.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-solidity.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-tsx.wasm +0 -0
- package/src/outline/vendor/wasm/tree-sitter-typescript.wasm +0 -0
- package/src/read.ts +118 -41
- package/src/replace-match.ts +42 -41
- package/src/replace-undo.ts +4 -4
- package/src/replace.ts +30 -19
- package/tool-prompts/read-guidelines.md +3 -0
- package/{prompts → tool-prompts}/read.md +2 -0
- package/prompts/read-guidelines.md +0 -1
- package/src/auto-read-all-state.ts +0 -27
- /package/{prompts → tool-prompts}/copy-guidelines.md +0 -0
- /package/{prompts → tool-prompts}/copy-snippet.md +0 -0
- /package/{prompts → tool-prompts}/copy.md +0 -0
- /package/{prompts → tool-prompts}/grep-guidelines.md +0 -0
- /package/{prompts → tool-prompts}/grep-snippet.md +0 -0
- /package/{prompts → tool-prompts}/grep.md +0 -0
- /package/{prompts → tool-prompts}/insert-guidelines.md +0 -0
- /package/{prompts → tool-prompts}/insert-snippet.md +0 -0
- /package/{prompts → tool-prompts}/insert.md +0 -0
- /package/{prompts → tool-prompts}/move-guidelines.md +0 -0
- /package/{prompts → tool-prompts}/move-snippet.md +0 -0
- /package/{prompts → tool-prompts}/move.md +0 -0
- /package/{prompts → tool-prompts}/read-snippet.md +0 -0
- /package/{prompts → tool-prompts}/replace-guidelines.md +0 -0
- /package/{prompts → tool-prompts}/replace-match-guidelines.md +0 -0
- /package/{prompts → tool-prompts}/replace-match-snippet.md +0 -0
- /package/{prompts → tool-prompts}/replace-match.md +0 -0
- /package/{prompts → tool-prompts}/replace-snippet.md +0 -0
- /package/{prompts → tool-prompts}/replace.md +0 -0
- /package/{prompts → tool-prompts}/transfer-guidelines.md +0 -0
- /package/{prompts → tool-prompts}/undo-last-change-guidelines.md +0 -0
- /package/{prompts → tool-prompts}/undo-last-change-snippet.md +0 -0
- /package/{prompts → tool-prompts}/undo-last-change.md +0 -0
package/README.md
CHANGED
|
@@ -159,15 +159,15 @@ The extension registers eight tools: `read`, `replace`, `replace_match`, `insert
|
|
|
159
159
|
|
|
160
160
|
### read
|
|
161
161
|
|
|
162
|
-
`read` returns a text file with every line prefixed by `anchor│content`. The anchor is the line's address.
|
|
162
|
+
`read` returns a text file with every line prefixed by `anchor│content`. The anchor is the line's address. `offset` accepts a 1-indexed line number or a served anchor; with an anchor, `limit` counts the lines to return after it. Anchor offsets are stable across edits and return the same editable rows.
|
|
163
163
|
|
|
164
164
|
| Parameter | Description |
|
|
165
165
|
| --- | --- |
|
|
166
166
|
| `path` | Path to the file (relative or absolute). |
|
|
167
|
-
| `offset` | Line number to start reading from
|
|
168
|
-
| `limit` | Maximum number of lines to return
|
|
167
|
+
| `offset` | Line number (1-indexed) or a served anchor to start reading from. |
|
|
168
|
+
| `limit` | Maximum number of lines to return from `offset`. |
|
|
169
169
|
|
|
170
|
-
Output is capped at 2000 lines and 50KB. Paged output ends with a continuation hint, for example `[Showing lines 1-50 of 120. Use offset=
|
|
170
|
+
Output is capped at 2000 lines and 50KB. Paged output ends with a continuation hint, for example `[Showing lines 1-50 of 120. Use offset=Emno to continue.]`.
|
|
171
171
|
|
|
172
172
|
A line whose `anchor│content` row exceeds 50KB is replaced by a marker that keeps the line's anchor: `anchor│[Line N is 2.2MB, exceeds 50.0KB; content not shown. Use bash: sed -n 'Np' <path> | head -c 51200]`. The marker is served like a normal row, so the whole line can still be replaced through it.
|
|
173
173
|
|
|
@@ -225,7 +225,7 @@ After a successful edit, the diff is capped at 50KB. A row over 50KB is shown as
|
|
|
225
225
|
|
|
226
226
|
`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.
|
|
227
227
|
|
|
228
|
-
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`
|
|
228
|
+
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` fails that call on its own and is left out of the batch; the rest of the batch still commits. The post-edit diff carries fresh anchors, and the edit is undoable with `undo_last_change`.
|
|
229
229
|
|
|
230
230
|
### insert
|
|
231
231
|
|
|
@@ -289,7 +289,7 @@ Every matching line, and each requested context line, is returned as `lineNumber
|
|
|
289
289
|
|
|
290
290
|
Directory searches respect `.gitignore` (including parent directories); `.git` is always skipped, and hidden files are searched. `node_modules`, `.tmp`, and `coverage` are skipped only when a `.gitignore` lists them. Binary, image, and oversized files are skipped silently.
|
|
291
291
|
|
|
292
|
-
|
|
292
|
+
Patterns are evaluated by ripgrep's linear-time regex engine, so shapes that stall a backtracking engine (nested quantifiers, several `.*` terms, huge repetitions) are safe. Pattern syntax is ripgrep's Rust regex syntax; backreferences and look-around are rejected with `[E_GREP_FAILED]`. Use `literal: true` when regex behavior is unnecessary.
|
|
293
293
|
|
|
294
294
|
Output is capped at `limit` matched lines, 2000 rows, and 50KB of text, whichever comes first, with a note naming the caps that cut the results (the exact one is in `details.truncation`). A matched line whose `anchor│content` row exceeds 500 bytes is shown as a fragment around the match, with `...` marking the truncated sides; a context row over 500 bytes is shown as its head with a trailing `...`. Fragments keep the line's anchor (long lines are hashed from their first 500 bytes) and are served like full rows, so a fragmented match is still editable, and `replace` always replaces the whole line.
|
|
295
295
|
|
|
@@ -314,13 +314,13 @@ Multiple `replace`, `replace_match`, `insert`, `copy`, and `move` calls on the s
|
|
|
314
314
|
- A call outside a batch commits before its result returns.
|
|
315
315
|
- 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.
|
|
316
316
|
- 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.
|
|
317
|
-
- 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.
|
|
318
|
-
-
|
|
317
|
+
- A batch validates every call against the pre-batch state and commits once, during the batch's last applied call: earlier applied calls reply `In batch N (queued)`, and the batch's last applied 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.
|
|
318
|
+
- 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
319
|
- A batch member accepts the same request shapes and auto-fixes as a standalone call.
|
|
320
320
|
|
|
321
|
-
Batched calls must target disjoint ranges; overlapping ranges
|
|
321
|
+
Batched calls must target disjoint ranges; overlapping ranges abort 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 a batch-level step aborts its batch-mates with `[E_OP_ABORTED]`; a call left out of the batch does not.
|
|
322
322
|
|
|
323
|
-
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
|
|
323
|
+
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
324
|
|
|
325
325
|
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`.
|
|
326
326
|
|
|
@@ -328,7 +328,7 @@ The hashline tools are sequential in pi, so a message that contains one runs all
|
|
|
328
328
|
|
|
329
329
|
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. A model matched by `disableOnModels` skips the block, and the post-edit diff substitution below is skipped with it.
|
|
330
330
|
|
|
331
|
-
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.
|
|
331
|
+
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 applied call shows the combined diff, headed by a `batch N:` line; earlier applied 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.
|
|
332
332
|
|
|
333
333
|
An edit that changes only line endings has no content diff; the result still reports `applied`, and one `undo_last_change` reverts it.
|
|
334
334
|
|
|
@@ -336,21 +336,21 @@ Auto-read keeps the same 50KB and 2000-line budget as `read`. Auto-read and Diff
|
|
|
336
336
|
|
|
337
337
|
## Auto-read all
|
|
338
338
|
|
|
339
|
-
Auto-read all is off by default and has three modes, selected in `/hashline-config`: `off` injects nothing
|
|
339
|
+
Auto-read all is off by default and has three modes, selected in `/hashline-config`: `off` injects nothing; `outline` attaches an anchor-stamped structural outline per file; and `full` discovers every file in the working directory that is not git-ignored (`git ls-files`, falling back to `ripgrep`, then to a directory walk) and attaches full contents. `Git repos only` is on by default and keeps the whole injection inside a git repository; turn it off to attach files in any directory. On the first turn of a session, the extension discovers the files, reads each one, and attaches the resulting `anchor│content` rows to the conversation as one extension message before the model answers. Those anchors are served exactly like `read` output, so the model can `replace` and `insert` immediately without calling `read` first. The message is injected once per session; resumed, forked, and cloned sessions that already contain it skip the injection.
|
|
340
340
|
|
|
341
|
-
Files are filtered before injection: symlinks, directories, image extensions (including SVG), binary files (a NUL byte in the first 8KB), files over 200KB, any path with a vendored segment (vendor, node_modules, bower_components, third_party, thirdparty, jspm_packages, .venv, venv, site-packages, __pycache__, .tox, .gradle, .terraform, Pods, Carthage, DerivedData, coreui, coreui-icons, case-insensitive), and vendored or generated names and patterns (*.min.js, *.min.css, *.min.mjs, *-min.js, *-min.css, *.bundle.*, *.chunk.*, *.umd.js, *.map, *.lock, package-lock.json, yarn.lock, composer.lock, Gemfile.lock, Cargo.lock, poetry.lock, Pipfile.lock, go.sum, flake.lock, *.generated.*, *.gen.*, *_pb2.py, *.pb.go, *.g.dart, *.freezed.dart, *.designer.cs, *.g.cs, *.snap, .eslintcache, coreui-icons.*, coreui.css) are skipped. The attachment stops at 500 files or at a byte budget derived from the model context window (200KB floor, 2MB ceiling), and it never drops below one file. Skipped and not-attached files are named at the end of the message so the model can `read` them on demand. A model matched by `disableOnModels` skips the injection entirely.
|
|
341
|
+
Files are filtered before injection: symlinks, directories, image extensions (including SVG), binary files (a NUL byte in the first 8KB), files over 200KB, any path with a vendored segment (vendor, node_modules, bower_components, third_party, thirdparty, jspm_packages, .venv, venv, site-packages, __pycache__, .tox, .gradle, .terraform, Pods, Carthage, DerivedData, coreui, coreui-icons, case-insensitive; `resources/views/vendor` is kept so Laravel view overrides stay attached), and vendored or generated names and patterns (*.min.js, *.min.css, *.min.mjs, *-min.js, *-min.css, *.bundle.*, *.chunk.*, *.umd.js, *.map, *.lock, package-lock.json, yarn.lock, composer.lock, Gemfile.lock, Cargo.lock, poetry.lock, Pipfile.lock, go.sum, flake.lock, *.generated.*, *.gen.*, *_pb2.py, *.pb.go, *.g.dart, *.freezed.dart, *.designer.cs, *.g.cs, *.snap, _ide_helper.php, _ide_helper_models.php, .phpstorm.meta.php, .eslintcache, coreui-icons.*, coreui.css) are skipped. The attachment stops at 500 files or at a byte budget derived from the model context window (200KB floor, 2MB ceiling), and it never drops below one file. Skipped and not-attached files are named at the end of the message so the model can `read` them on demand. A model matched by `disableOnModels` skips the injection entirely.
|
|
342
342
|
|
|
343
|
-
Each attached file
|
|
343
|
+
Each attached file starts with a header (`=== path ===` in `full` mode, `=== path (language) — lines ===` in `outline` mode) followed by its anchor rows. Edit directly from the attachment with replace and insert, so no `read` is needed. `full` attaches files whole; `outline` attaches each file's structural outline within the budget. In outline mode, multi-line symbols appear as labels with `[limit N]` and single-line symbols appear as their content.
|
|
344
344
|
|
|
345
|
-
A coverage line after the header reports
|
|
345
|
+
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
346
|
|
|
347
|
-
The setting lives in `/hashline-config` as Auto-read all and in `config.json` as `autoReadAll` (`"off"`, `"
|
|
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 `anchor_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
348
|
|
|
349
349
|
## Configuration
|
|
350
350
|
|
|
351
351
|
| Command | Description |
|
|
352
352
|
| --- | --- |
|
|
353
|
-
| `/hashline-config` | Open the settings window: auto-read anchors, auto-read all mode, ignore folders/files, disable on models, read on disabled models, diff context lines, `anchor_grep` tool, copy/move tools, replace_match tool, required `path`, and strict input. Persists across sessions. |
|
|
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, `anchor_grep` tool, copy/move tools, replace_match tool, required `path`, and strict input. Persists across sessions. |
|
|
354
354
|
| `/clear-anchors` | Clear the session's anchor claims. Anchors are re-claimed on the next `read`. |
|
|
355
355
|
|
|
356
356
|
Settings live in `~/.config/pi-hashline-edit-pro/config.json`, created when a setting is first changed in `/hashline-config`:
|
|
@@ -359,6 +359,7 @@ Settings live in `~/.config/pi-hashline-edit-pro/config.json`, created when a se
|
|
|
359
359
|
{
|
|
360
360
|
"autoRead": true,
|
|
361
361
|
"autoReadAll": "off",
|
|
362
|
+
"autoReadAllRequireGit": true,
|
|
362
363
|
"autoReadAllIgnore": [],
|
|
363
364
|
"anchorGrepEnabled": true,
|
|
364
365
|
"copyMoveEnabled": true,
|
|
@@ -374,7 +375,8 @@ Settings live in `~/.config/pi-hashline-edit-pro/config.json`, created when a se
|
|
|
374
375
|
| Key | `/hashline-config` label | Default | Effect |
|
|
375
376
|
| --- | --- | --- | --- |
|
|
376
377
|
| `autoRead` | Auto-read | `true` | Append the auto-read block after `write` and show post-edit diffs. |
|
|
377
|
-
| `autoReadAll` | Auto-read all | `"off"` | Attachment mode: `"off"`, `"
|
|
378
|
+
| `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
|
+
| `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. |
|
|
378
380
|
| `autoReadAllIgnore` | Ignore folders/files | `[]` | Extra folder names, file names, or globs skipped by auto-read all. |
|
|
379
381
|
| `anchorGrepEnabled` | Anchor grep | `true` | Register `anchor_grep` and disable the built-in grep while it is on. |
|
|
380
382
|
| `copyMoveEnabled` | Copy/move | `true` | Offer the `copy` and `move` tools; when off, both are removed from the active tools. |
|
|
@@ -434,7 +436,7 @@ All eight tools also declare an `outputSchema` and return `structuredContent`, s
|
|
|
434
436
|
|
|
435
437
|
## Error, warning, and hint codes
|
|
436
438
|
|
|
437
|
-
Codes starting with `E_` are errors: nothing was written, with one exception. `File was written; anchor finalization failed` means the file was written and one undo reverts it. Codes starting with `W_` are warnings: the call succeeded with an auto-fix notice or an anchor-reclaim notice; check `classification` (`applied` vs `noop`) in `details.metrics` to tell whether bytes changed. Codes starting with `H_` are hints: the call succeeded and the file holds exactly what was requested, so the notice is informational, never blocks an edit (not even in strict-input mode), and is reported in `details.hints` instead of `details.warnings`.
|
|
439
|
+
Codes starting with `E_` are errors: nothing was written, with one exception. `File was written; anchor finalization failed` means the file was written and one undo reverts it. Codes starting with `W_` are warnings: the call succeeded with an auto-fix notice or an anchor-reclaim notice; check `classification` (`applied` vs `noop`) in `details.metrics` to tell whether bytes changed. Codes starting with `H_` are hints: the call succeeded and the file holds exactly what was requested, so the notice is informational, never blocks an edit (not even in strict-input mode), and is reported in `details.hints` instead of `details.warnings`.
|
|
438
440
|
|
|
439
441
|
Most common, with the fix:
|
|
440
442
|
|
|
@@ -479,11 +481,9 @@ Full reference:
|
|
|
479
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 │ anchor│content` grep row). The write is refused, file byte-identical; retry with bare content (remove the copied anchors). |
|
|
480
482
|
| `[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. |
|
|
481
483
|
| `[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. |
|
|
482
|
-
| `[E_OP_ABORTED]` |
|
|
483
|
-
| `[
|
|
484
|
-
| `[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. |
|
|
484
|
+
| `[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]` | `anchor_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. |
|
|
485
486
|
| `[E_GREP_TIMEOUT]` | `anchor_grep` timed out after 10 seconds; narrow `path` or simplify `pattern` and retry. |
|
|
486
|
-
| `[E_AUTO_READ_ALL]` | File already attached and unchanged since this session's start; the attached content is still exact. |
|
|
487
487
|
|
|
488
488
|
## Troubleshooting
|
|
489
489
|
|
|
@@ -517,8 +517,25 @@ Config, SQLite (including WAL/SHM and undo history), registry sidecars, and the
|
|
|
517
517
|
|
|
518
518
|
Background snapshot pruning and registry sidecar GC skip `EPERM`/`EACCES` without deleting records or logging each inaccessible path. Unexpected errors remain visible; tool file-access failures and SQLite errors are not silenced.
|
|
519
519
|
|
|
520
|
+
### Environment variables
|
|
521
|
+
|
|
522
|
+
| Variable | Effect |
|
|
523
|
+
| --- | --- |
|
|
524
|
+
| `PI_HASHLINE_DIR` | Absolute path overriding this extension's state directory on all platforms; an unset or empty value keeps the XDG/home defaults and a relative value is rejected with `[E_CONFIG]`. |
|
|
525
|
+
| `PI_HASHLINE_DEBUG` | Set to `1` or `true` to show the "Hashline Edit mode active" notification at session start. |
|
|
526
|
+
|
|
520
527
|
## How anchors work
|
|
521
528
|
|
|
529
|
+
### Request flow
|
|
530
|
+
|
|
531
|
+
```text
|
|
532
|
+
read / anchor_grep / auto-read-all ──► anchor registry (ownership + served records)
|
|
533
|
+
│
|
|
534
|
+
edit call ──► resolve anchors ──► apply ──► commit ──► post-edit diff with fresh anchors
|
|
535
|
+
│
|
|
536
|
+
hash store (SQLite snapshots + undo)
|
|
537
|
+
```
|
|
538
|
+
|
|
522
539
|
### Allocation
|
|
523
540
|
|
|
524
541
|
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).
|
|
@@ -569,6 +586,7 @@ Set `PI_HASHLINE_DEBUG=1` to show an "active" notification at session start.
|
|
|
569
586
|
|
|
570
587
|
- [RimuruW](https://github.com/RimuruW), original `pi-hashline-edit` and the strict-semantics policy
|
|
571
588
|
- [can1357](https://github.com/can1357), original [oh-my-pi](https://github.com/can1357/oh-my-pi) implementation and the hashline concept
|
|
589
|
+
- [HanzCEO](https://github.com/HanzCEO), [pi-codebase-reader](https://github.com/HanzCEO/pi-codebase-reader): vendored tree-sitter parsers and outline extraction (Apache-2.0), exposed through the auto-read-all `outline` and `outline(git)` modes
|
|
572
590
|
|
|
573
591
|
## License
|
|
574
592
|
|
package/index.ts
CHANGED
|
@@ -9,8 +9,7 @@ import { regGrep } 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";
|
|
12
|
-
import { buildAutoReadAllInjection, autoReadAllBudget } from "./src/auto-read-all";
|
|
13
|
-
import { clearAutoReadAllComplete } from "./src/auto-read-all-state";
|
|
12
|
+
import { buildAutoReadAllInjection, autoReadAllBudget, isOutlineAutoReadAll } from "./src/auto-read-all";
|
|
14
13
|
import type { RMetrics } from "./src/replace-response";
|
|
15
14
|
import type { ReplaceDetails } from "./src/replace";
|
|
16
15
|
import { extractHints, extractWarnings } from "./src/replace-render";
|
|
@@ -22,6 +21,7 @@ import {
|
|
|
22
21
|
readConfigWithStatus,
|
|
23
22
|
toggleAutoRead,
|
|
24
23
|
cycleAutoReadAllMode,
|
|
24
|
+
toggleAutoReadAllRequireGit,
|
|
25
25
|
toggleAnchorGrep,
|
|
26
26
|
toggleCopyMove,
|
|
27
27
|
toggleReplaceMatch,
|
|
@@ -61,6 +61,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
61
61
|
let autoRead = true;
|
|
62
62
|
let autoReadAll: AutoReadAllMode = "off";
|
|
63
63
|
let autoReadAllIgnore: string[] = [];
|
|
64
|
+
let autoReadAllRequireGit = true;
|
|
64
65
|
let disableOnModels: string[] = [];
|
|
65
66
|
let readOnDisabledModels: ReadOnDisabledModels = "vanilla";
|
|
66
67
|
let readFlavor: "anchored" | "vanilla" = "anchored";
|
|
@@ -100,26 +101,31 @@ export default function (pi: ExtensionAPI): void {
|
|
|
100
101
|
await refreshEditTools();
|
|
101
102
|
}
|
|
102
103
|
if (gated) {
|
|
103
|
-
const active = pi.getActiveTools();
|
|
104
|
+
const active = pi.getActiveTools().filter((tool) => tool !== "edit");
|
|
104
105
|
const gatedTools = readOnDisabledModels === "vanilla" ? ANCHOR_TOOL_NAMES.filter((tool) => tool !== "read") : ANCHOR_TOOL_NAMES;
|
|
105
106
|
const next = active.filter((tool) => !gatedTools.includes(tool));
|
|
106
|
-
if (readOnDisabledModels === "vanilla" && baseAnchorTools.has("read") && !next.includes("read")) next.push("read");
|
|
107
107
|
if (grepWasActive && !next.includes("grep")) next.push("grep");
|
|
108
108
|
pi.setActiveTools(next);
|
|
109
109
|
gateApplied = true;
|
|
110
110
|
return;
|
|
111
111
|
}
|
|
112
|
-
if (!gateApplied)
|
|
112
|
+
if (!gateApplied) {
|
|
113
|
+
const current = pi.getActiveTools();
|
|
114
|
+
const withoutEdit = current.filter((tool) => tool !== "edit");
|
|
115
|
+
if (withoutEdit.length !== current.length) pi.setActiveTools(withoutEdit);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
113
118
|
gateApplied = false;
|
|
114
119
|
const config = await readConfig();
|
|
115
120
|
const enabled = ANCHOR_TOOL_NAMES.filter((tool) => {
|
|
121
|
+
if (tool === "read") return true;
|
|
116
122
|
if (!baseAnchorTools.has(tool)) return false;
|
|
117
123
|
if (tool === "replace_match") return config.replaceMatchEnabled !== false;
|
|
118
124
|
if (tool === "copy" || tool === "move") return config.copyMoveEnabled !== false;
|
|
119
125
|
if (tool === "anchor_grep") return config.anchorGrepEnabled === true;
|
|
120
126
|
return true;
|
|
121
127
|
});
|
|
122
|
-
let next = [...new Set([...pi.getActiveTools(), ...enabled])];
|
|
128
|
+
let next = [...new Set([...pi.getActiveTools().filter((tool) => tool !== "edit"), ...enabled])];
|
|
123
129
|
if (enabled.includes("anchor_grep") && grepWasActive) next = next.filter((tool) => tool !== "grep");
|
|
124
130
|
pi.setActiveTools(next);
|
|
125
131
|
}
|
|
@@ -149,6 +155,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
149
155
|
if (corrupted && (ctx as { hasUI?: boolean }).hasUI) ctx.ui.notify("Hashline config was corrupt and was reset to defaults", "warning");
|
|
150
156
|
autoRead = config.autoRead;
|
|
151
157
|
autoReadAll = config.autoReadAll ?? "off";
|
|
158
|
+
autoReadAllRequireGit = config.autoReadAllRequireGit ?? true;
|
|
152
159
|
autoReadAllIgnore = config.autoReadAllIgnore ?? [];
|
|
153
160
|
disableOnModels = config.disableOnModels ?? [];
|
|
154
161
|
readOnDisabledModels = config.readOnDisabledModels ?? "vanilla";
|
|
@@ -173,7 +180,6 @@ export default function (pi: ExtensionAPI): void {
|
|
|
173
180
|
pi.on("session_shutdown", async (_event, ctx) => {
|
|
174
181
|
try {
|
|
175
182
|
const key = sessionKeyFor(ctx);
|
|
176
|
-
clearAutoReadAllComplete(key);
|
|
177
183
|
if (key !== undefined) releaseRegistrySession(key);
|
|
178
184
|
} catch (error) {
|
|
179
185
|
console.error("Failed to release anchor registry session:", error);
|
|
@@ -189,9 +195,9 @@ export default function (pi: ExtensionAPI): void {
|
|
|
189
195
|
if (modelDisabled(ctx.model, disableOnModels)) return;
|
|
190
196
|
autoReadAllInjected = true;
|
|
191
197
|
try {
|
|
192
|
-
const injection = await buildAutoReadAllInjection(ctx.cwd, autoReadAllBudget(ctx.model), autoReadAll, autoReadAllIgnore,
|
|
198
|
+
const injection = await buildAutoReadAllInjection(ctx.cwd, autoReadAllBudget(ctx.model), autoReadAll, autoReadAllIgnore, autoReadAllRequireGit);
|
|
193
199
|
if (!injection) return;
|
|
194
|
-
if (ctx.hasUI) ctx.ui.notify(`Auto-read all: attached ${injection.files} file(s)
|
|
200
|
+
if (ctx.hasUI) ctx.ui.notify(`Auto-read all: ${isOutlineAutoReadAll(autoReadAll) ? "outlined" : "attached"} ${injection.files} file(s)`, "info");
|
|
195
201
|
return { message: { customType: AUTO_READ_ALL_CUSTOM_TYPE, content: injection.text, display: false } };
|
|
196
202
|
} catch (error) {
|
|
197
203
|
console.error("Auto-read all failed:", error);
|
|
@@ -200,7 +206,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
200
206
|
}));
|
|
201
207
|
|
|
202
208
|
pi.registerCommand("hashline-config", {
|
|
203
|
-
description: "Open the hashline settings window (auto-read, auto-read all, ignore folders/files,
|
|
209
|
+
description: "Open the hashline settings window (auto-read, diff context, auto-read all, git repos only, ignore folders/files, grep, copy/move, replace_match, path, strict input, disable on models, read on disabled models)",
|
|
204
210
|
handler: async (_args, ctx) => {
|
|
205
211
|
if (!ctx.hasUI) {
|
|
206
212
|
ctx.ui.notify("/hashline-config requires interactive mode", "error");
|
|
@@ -209,14 +215,16 @@ export default function (pi: ExtensionAPI): void {
|
|
|
209
215
|
await ctx.ui.custom<void>(async (tui, theme, _keybindings, done) => {
|
|
210
216
|
const overlay = new HashlineConfigOverlay({
|
|
211
217
|
tui,
|
|
218
|
+
maxHeight: () => tui.terminal ? Math.max(6, Math.floor(tui.terminal.rows * 0.9)) : undefined,
|
|
212
219
|
theme,
|
|
213
220
|
done,
|
|
214
221
|
onToggle: async (key, delta, value) => {
|
|
215
222
|
if (key === "autoRead") autoRead = await toggleAutoRead();
|
|
216
|
-
else if (key === "autoReadAll") { autoReadAll = await cycleAutoReadAllMode(); autoReadAllInjected = false; }
|
|
223
|
+
else if (key === "autoReadAll") { autoReadAll = await cycleAutoReadAllMode(delta); autoReadAllInjected = false; }
|
|
224
|
+
else if (key === "autoReadAllRequireGit") autoReadAllRequireGit = await toggleAutoReadAllRequireGit();
|
|
217
225
|
else if (key === "autoReadAllIgnore") autoReadAllIgnore = await setAutoReadAllIgnoreFromText(value ?? "");
|
|
218
226
|
else if (key === "disableOnModels") disableOnModels = await setDisableOnModelsFromText(value ?? "");
|
|
219
|
-
else if (key === "readOnDisabledModels") readOnDisabledModels = await cycleReadOnDisabledModels();
|
|
227
|
+
else if (key === "readOnDisabledModels") readOnDisabledModels = await cycleReadOnDisabledModels(delta);
|
|
220
228
|
else if (key === "diffContextLines") await adjustDiffContextLines(delta ?? 1);
|
|
221
229
|
else if (key === "anchorGrepEnabled") {
|
|
222
230
|
const enabled = await toggleAnchorGrep();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-hashline-edit-pro",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.3.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",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"files": [
|
|
30
30
|
"index.ts",
|
|
31
31
|
"src",
|
|
32
|
-
"prompts",
|
|
32
|
+
"tool-prompts",
|
|
33
33
|
"README.md",
|
|
34
34
|
"LICENSE"
|
|
35
35
|
],
|
|
@@ -41,6 +41,7 @@
|
|
|
41
41
|
"dependencies": {
|
|
42
42
|
"diff": "^9.0.0",
|
|
43
43
|
"file-type": "^22.0.2",
|
|
44
|
+
"web-tree-sitter": "^0.27.0",
|
|
44
45
|
"xxhash-wasm": "^1.1.0"
|
|
45
46
|
},
|
|
46
47
|
"peerDependencies": {
|
package/src/anchor-registry.ts
CHANGED
|
@@ -7,7 +7,7 @@ import { sessionClaimsDir } from "./paths";
|
|
|
7
7
|
import { contentChecksum } from "./hashline/hasher";
|
|
8
8
|
import { ANCHOR_COUNT, anchorAt } from "./hashline/alphabet";
|
|
9
9
|
import { HASH_PROBE_STRIDE } from "./hashline/hash";
|
|
10
|
-
import { errCode, splitLines } from "./utils";
|
|
10
|
+
import { errCode, isRec, splitLines } from "./utils";
|
|
11
11
|
import * as Diff from "diff";
|
|
12
12
|
import { lineChecksum } from "./hashline";
|
|
13
13
|
import { getAllocatedState, persistSnapshot, type HashStore } from "./hash-store";
|
|
@@ -197,6 +197,7 @@ function seedServedFromOwned(state: SessionState): void {
|
|
|
197
197
|
export function foldRegistryEvents(events: RegistryEvent[], seed?: string): SessionState {
|
|
198
198
|
const state = newSessionState(seed);
|
|
199
199
|
for (const event of events) {
|
|
200
|
+
if (!isRegistryEvent(event)) continue;
|
|
200
201
|
if (event.kind === "clear") {
|
|
201
202
|
state.owned.clear();
|
|
202
203
|
state.lastUsedAt.clear();
|
|
@@ -232,15 +233,31 @@ export function foldRegistryEvents(events: RegistryEvent[], seed?: string): Sess
|
|
|
232
233
|
return state;
|
|
233
234
|
}
|
|
234
235
|
|
|
236
|
+
function isStringArray(value: unknown): value is string[] {
|
|
237
|
+
return Array.isArray(value) && value.every((entry) => typeof entry === "string");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function isRegistryEvent(value: unknown): value is RegistryEvent {
|
|
241
|
+
if (!isRec(value) || typeof value.kind !== "string") return false;
|
|
242
|
+
if (value.kind === "session") return typeof value.sessionFile === "string";
|
|
243
|
+
if (value.kind === "clear") return true;
|
|
244
|
+
if (value.kind === "allocate") {
|
|
245
|
+
return typeof value.path === "string"
|
|
246
|
+
&& Array.isArray(value.rows)
|
|
247
|
+
&& value.rows.every((row) => Array.isArray(row) && row.length === 2 && typeof row[0] === "string" && typeof row[1] === "string");
|
|
248
|
+
}
|
|
249
|
+
if (value.kind === "free") return typeof value.path === "string" && (value.anchors === undefined || isStringArray(value.anchors));
|
|
250
|
+
if (value.kind === "minted") return isStringArray(value.anchors);
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
|
|
235
254
|
export function parseRegistryLog(raw: string): RegistryEvent[] {
|
|
236
255
|
const events: RegistryEvent[] = [];
|
|
237
256
|
for (const line of raw.split("\n")) {
|
|
238
257
|
if (!line.trim()) continue;
|
|
239
258
|
try {
|
|
240
|
-
const parsed = JSON.parse(line)
|
|
241
|
-
if (
|
|
242
|
-
events.push(parsed);
|
|
243
|
-
}
|
|
259
|
+
const parsed: unknown = JSON.parse(line);
|
|
260
|
+
if (isRegistryEvent(parsed)) events.push(parsed);
|
|
244
261
|
} catch {
|
|
245
262
|
continue;
|
|
246
263
|
}
|
package/src/auto-read-all.ts
CHANGED
|
@@ -12,13 +12,13 @@ import {
|
|
|
12
12
|
import { normalizeAutoReadAllIgnoreEntry, type AutoReadAllMode } from "./config";
|
|
13
13
|
import { serveRows } from "./served";
|
|
14
14
|
import { formatAnchorReclaimNotice, takeReclaimedPaths } from "./anchor-registry";
|
|
15
|
-
import { readNormFile
|
|
15
|
+
import { readNormFile } from "./file-reader";
|
|
16
16
|
import { resolveRgPath } from "./grep";
|
|
17
17
|
import { globToRegex } from "./glob";
|
|
18
18
|
import { MAX_HASH_LINES } from "./hashline";
|
|
19
19
|
import { fmtReadPreview } from "./read";
|
|
20
|
+
import { buildFileOutline } from "./outline";
|
|
20
21
|
import { splitLines } from "./utils";
|
|
21
|
-
import { recordAutoReadAllComplete } from "./auto-read-all-state";
|
|
22
22
|
|
|
23
23
|
const EXEC_TIMEOUT_MS = 20_000;
|
|
24
24
|
const EXEC_MAX_BYTES = 64 * 1024 * 1024;
|
|
@@ -27,7 +27,10 @@ const SCAN_LIMIT_MULTIPLIER = 4;
|
|
|
27
27
|
const MAX_REPORTED_OMISSIONS = 50;
|
|
28
28
|
|
|
29
29
|
const HEADER =
|
|
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
|
|
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
|
+
|
|
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`; 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
34
|
|
|
32
35
|
const IMAGE_EXTENSIONS = new Set([
|
|
33
36
|
".avif",
|
|
@@ -78,12 +81,18 @@ export const AUTO_READ_ALL_EXCLUDED_NAMES = [
|
|
|
78
81
|
"go.sum",
|
|
79
82
|
"flake.lock",
|
|
80
83
|
".eslintcache",
|
|
84
|
+
"_ide_helper.php",
|
|
85
|
+
"_ide_helper_models.php",
|
|
86
|
+
".phpstorm.meta.php",
|
|
81
87
|
];
|
|
82
88
|
const EXCLUDED_NAME_SET = new Set(AUTO_READ_ALL_EXCLUDED_NAMES);
|
|
83
89
|
const EXCLUDED_SEGMENT_SET = new Set(AUTO_READ_ALL_EXCLUDED_SEGMENTS);
|
|
84
90
|
function isExcludedBySegment(path: string): boolean {
|
|
85
|
-
|
|
86
|
-
|
|
91
|
+
const lower = path.toLowerCase();
|
|
92
|
+
const vendorViews = lower.startsWith("resources/views/vendor/");
|
|
93
|
+
for (const segment of lower.split("/")) {
|
|
94
|
+
if (vendorViews && segment === "vendor") continue;
|
|
95
|
+
if (EXCLUDED_SEGMENT_SET.has(segment)) return true;
|
|
87
96
|
}
|
|
88
97
|
return false;
|
|
89
98
|
}
|
|
@@ -190,6 +199,7 @@ export interface AutoReadAllSection {
|
|
|
190
199
|
text: string;
|
|
191
200
|
totalLines: number;
|
|
192
201
|
absolutePath: string;
|
|
202
|
+
complete: boolean;
|
|
193
203
|
}
|
|
194
204
|
export interface AutoReadAllInjection {
|
|
195
205
|
text: string;
|
|
@@ -295,12 +305,17 @@ async function forEachLimit<T>(items: T[], limit: number, work: (item: T) => Pro
|
|
|
295
305
|
await Promise.all(workers);
|
|
296
306
|
}
|
|
297
307
|
|
|
298
|
-
export
|
|
308
|
+
export function isOutlineAutoReadAll(mode: AutoReadAllMode): boolean {
|
|
309
|
+
return mode === "outline";
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export async function discoverAutoReadAllFiles(cwd: string, mode: AutoReadAllMode = "off", ignoreDirs: readonly string[] = [], requireGit = true): Promise<AutoReadAllDiscovery> {
|
|
313
|
+
if (mode === "off") return { files: [], source: "git", discovered: 0, skippedBinary: 0, skippedLarge: 0, skippedOther: 0, skippedByName: 0 };
|
|
299
314
|
const customIgnore = normalizeAutoReadAllIgnoreList(ignoreDirs);
|
|
300
315
|
let source: AutoReadAllSource = "git";
|
|
301
316
|
let candidates = await listFromGit(cwd);
|
|
302
317
|
if (candidates === undefined) {
|
|
303
|
-
if (
|
|
318
|
+
if (requireGit) {
|
|
304
319
|
return { files: [], source: "git", discovered: 0, skippedBinary: 0, skippedLarge: 0, skippedOther: 0, skippedByName: 0 };
|
|
305
320
|
}
|
|
306
321
|
candidates = await listFromRg(cwd);
|
|
@@ -372,13 +387,18 @@ async function candidateFileBytes(cwd: string, file: string): Promise<number | u
|
|
|
372
387
|
}
|
|
373
388
|
}
|
|
374
389
|
|
|
375
|
-
async function renderFile(file: string, cwd: string): Promise<AutoReadAllSection | undefined> {
|
|
390
|
+
async function renderFile(file: string, cwd: string, outline: boolean): Promise<AutoReadAllSection | undefined> {
|
|
376
391
|
try {
|
|
377
392
|
const { normalized, fileHashes, absolutePath } = await readNormFile(file, cwd, { maxLines: MAX_HASH_LINES });
|
|
393
|
+
const fileLines = splitLines(normalized);
|
|
394
|
+
if (outline) {
|
|
395
|
+
const section = await buildFileOutline({ displayPath: file, content: normalized, hashes: fileHashes });
|
|
396
|
+
serveRows(absolutePath, fileHashes, fileLines, section.servedHashes);
|
|
397
|
+
return { file, text: section.text, totalLines: fileHashes.length, absolutePath, complete: !section.truncated };
|
|
398
|
+
}
|
|
378
399
|
const preview = await fmtReadPreview(normalized, {}, fileHashes, absolutePath, AUTO_READ_ALL_MAX_BUDGET_BYTES, MAX_HASH_LINES);
|
|
379
|
-
serveRows(absolutePath, fileHashes,
|
|
380
|
-
|
|
381
|
-
return { file, text: `=== ${file} ===\n${preview.text}`, totalLines, absolutePath };
|
|
400
|
+
serveRows(absolutePath, fileHashes, fileLines, preview.servedHashes);
|
|
401
|
+
return { file, text: `=== ${file} ===\n${preview.text}`, totalLines: fileHashes.length, absolutePath, complete: preview.truncation === undefined && !preview.blockedByLongLine };
|
|
382
402
|
} catch (error) {
|
|
383
403
|
console.error(`Auto-read all: skipped ${file}:`, error);
|
|
384
404
|
return undefined;
|
|
@@ -403,22 +423,24 @@ function buildFooter(attached: number, discovery: AutoReadAllDiscovery, omitted:
|
|
|
403
423
|
return `[hashline auto-read-all: ${attached} file(s) attached from ${discovery.source}; ${summary}${omissionNote}]`;
|
|
404
424
|
}
|
|
405
425
|
|
|
406
|
-
export async function buildAutoReadAllInjection(cwd: string, budgetBytes: number, mode: AutoReadAllMode = "
|
|
407
|
-
|
|
426
|
+
export async function buildAutoReadAllInjection(cwd: string, budgetBytes: number, mode: AutoReadAllMode = "off", ignoreDirs: readonly string[] = [], requireGit = true): Promise<AutoReadAllInjection | undefined> {
|
|
427
|
+
if (mode === "off") return undefined;
|
|
428
|
+
const discovery = await discoverAutoReadAllFiles(cwd, mode, ignoreDirs, requireGit);
|
|
408
429
|
if (discovery.files.length === 0) return undefined;
|
|
430
|
+
const outline = isOutlineAutoReadAll(mode);
|
|
409
431
|
const sections: AutoReadAllSection[] = [];
|
|
410
432
|
const omitted: string[] = [];
|
|
411
433
|
let bytes = 0;
|
|
412
434
|
let completeFiles = 0;
|
|
413
435
|
for (const file of discovery.files) {
|
|
414
|
-
if (sections.length > 0) {
|
|
436
|
+
if (!outline && sections.length > 0) {
|
|
415
437
|
const size = await candidateFileBytes(cwd, file);
|
|
416
438
|
if (size !== undefined && bytes + size > budgetBytes) {
|
|
417
439
|
omitted.push(file);
|
|
418
440
|
continue;
|
|
419
441
|
}
|
|
420
442
|
}
|
|
421
|
-
const section = await renderFile(file, cwd);
|
|
443
|
+
const section = await renderFile(file, cwd, outline);
|
|
422
444
|
if (section === undefined) {
|
|
423
445
|
omitted.push(file);
|
|
424
446
|
continue;
|
|
@@ -429,18 +451,14 @@ export async function buildAutoReadAllInjection(cwd: string, budgetBytes: number
|
|
|
429
451
|
continue;
|
|
430
452
|
}
|
|
431
453
|
sections.push(section);
|
|
432
|
-
const snapshotId = await safeSnapId(section.absolutePath, "auto-read-all");
|
|
433
|
-
if (snapshotId !== undefined) recordAutoReadAllComplete(sessionKey, section.absolutePath, snapshotId);
|
|
434
454
|
bytes += sectionBytes;
|
|
435
|
-
completeFiles += 1;
|
|
455
|
+
if (section.complete) completeFiles += 1;
|
|
436
456
|
}
|
|
437
457
|
if (sections.length === 0) return undefined;
|
|
438
458
|
const sectionTexts = sections.map((section) => section.text);
|
|
439
459
|
const coverage = `[coverage: ${completeFiles} complete]`;
|
|
440
|
-
const completeNames = sections.map((section) => section.file);
|
|
441
|
-
const machineList = `[files complete: ${JSON.stringify(completeNames)} omitted: ${JSON.stringify(omitted)}]`;
|
|
442
460
|
const reclaimNotice = formatAnchorReclaimNotice(takeReclaimedPaths());
|
|
443
|
-
const text = `${HEADER}\n\n${coverage}\n
|
|
461
|
+
const text = `${outline ? OUTLINE_HEADER : HEADER}\n\n${coverage}\n\n${sectionTexts.join("\n\n")}\n\n${buildFooter(sections.length, discovery, omitted)}${reclaimNotice !== undefined ? `\n${reclaimNotice}` : ""}`;
|
|
444
462
|
return { text, files: sections.length, bytes, omitted, completeFiles };
|
|
445
463
|
}
|
|
446
464
|
|