pi-hashline-edit-pro 6.1.2 → 6.2.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 +14 -12
- package/index.ts +20 -12
- package/package.json +3 -2
- package/src/auto-read-all.ts +39 -21
- package/src/config-ui.ts +135 -55
- package/src/config.ts +27 -9
- package/src/copy-move.ts +4 -4
- package/src/grep.ts +3 -3
- package/src/insert.ts +3 -3
- package/src/outline/index.ts +45 -0
- package/src/outline/parsers.ts +42 -0
- package/src/outline/render.ts +166 -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 +108 -36
- package/src/replace-match.ts +3 -3
- package/src/replace-undo.ts +3 -3
- package/src/replace.ts +3 -3
- 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,13 +159,13 @@ 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
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=51 to continue.]`.
|
|
171
171
|
|
|
@@ -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.
|
|
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
|
|
|
@@ -483,7 +485,6 @@ Full reference:
|
|
|
483
485
|
| `[E_UNSAFE_REGEX]` | A grep regex can trigger excessive backtracking; simplify it or search with `literal: true`. |
|
|
484
486
|
| `[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. |
|
|
485
487
|
| `[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
488
|
|
|
488
489
|
## Troubleshooting
|
|
489
490
|
|
|
@@ -569,6 +570,7 @@ Set `PI_HASHLINE_DEBUG=1` to show an "active" notification at session start.
|
|
|
569
570
|
|
|
570
571
|
- [RimuruW](https://github.com/RimuruW), original `pi-hashline-edit` and the strict-semantics policy
|
|
571
572
|
- [can1357](https://github.com/can1357), original [oh-my-pi](https://github.com/can1357/oh-my-pi) implementation and the hashline concept
|
|
573
|
+
- [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
574
|
|
|
573
575
|
## License
|
|
574
576
|
|
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.2.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/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│symbol` rows with live anchors.\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
|
|