pi-hashline-edit-pro 4.3.0 → 4.3.2
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 +7 -5
- package/index.ts +7 -6
- package/package.json +1 -1
- package/prompts/grep.md +1 -1
- package/prompts/insert-guidelines.md +0 -1
- package/prompts/insert.md +1 -1
- package/prompts/replace-guidelines.md +1 -1
- package/prompts/undo-last-change-guidelines.md +1 -1
- package/prompts/undo-last-change-snippet.md +1 -1
- package/src/auto-read-all.ts +118 -15
- package/src/config-ui.ts +1 -1
- package/src/config.ts +22 -5
- package/src/edit-common.ts +6 -0
- package/src/payload-contract.ts +1 -1
package/README.md
CHANGED
|
@@ -160,11 +160,13 @@ Auto-read keeps the same 50KB and 2000-line budget as `read`. Auto-read and Diff
|
|
|
160
160
|
|
|
161
161
|
### Auto-read all
|
|
162
162
|
|
|
163
|
-
Auto-read all is off by default
|
|
163
|
+
Auto-read all is off by default and has three modes, selected in `/hashline-config`: `off` injects nothing, `on` 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 `git` uses `git ls-files` only, injecting nothing when the working directory is not a git repository. 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.
|
|
164
164
|
|
|
165
|
-
Files are filtered before injection: symlinks, directories, image extensions, binary files (a NUL byte in the first 8KB), files over 200KB,
|
|
165
|
+
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 file whose `read` output is truncated keeps its truncation hint, so the rest can be paged in with `read`.
|
|
166
166
|
|
|
167
|
-
|
|
167
|
+
Each attached file carries a status line: `[complete, N lines; do NOT re-read]` means the section is fully attached and must be edited directly without calling `read`, while `[truncated, showing M of N lines; use read with offset=X to continue]` means only the first bytes are attached. A coverage line after the header reports complete versus truncated counts and the number of 48KB file-boundary chunks the payload splits into, so a host-side cut never tears a file in the middle.
|
|
168
|
+
|
|
169
|
+
The setting lives in `/hashline-config` as Auto-read all and in `config.json` as `autoReadAll` (`"off"`, `"on"`, or `"git"`; older configs with `true` or `false` are read as `"on"` or `"off"`).
|
|
168
170
|
|
|
169
171
|
## Tool result details
|
|
170
172
|
|
|
@@ -181,7 +183,7 @@ All five tools return machine-readable metadata in `details` alongside the model
|
|
|
181
183
|
|
|
182
184
|
| Command | Description |
|
|
183
185
|
| --- | --- |
|
|
184
|
-
| `/hashline-config` | Open the settings window: auto-read anchors, auto-read all
|
|
186
|
+
| `/hashline-config` | Open the settings window: auto-read anchors, auto-read all mode, diff context lines, `anchor_grep` tool, required `path`, strict input, and boundary dedup. Persists across sessions. |
|
|
185
187
|
| `/clear-anchors` | Clear the session's anchor claims. Anchors are re-claimed on the next `read`. |
|
|
186
188
|
|
|
187
189
|
Settings live in `~/.config/pi-hashline-edit-pro/config.json`, created when a setting is first changed in `/hashline-config`:
|
|
@@ -189,7 +191,7 @@ Settings live in `~/.config/pi-hashline-edit-pro/config.json`, created when a se
|
|
|
189
191
|
```json
|
|
190
192
|
{
|
|
191
193
|
"autoRead": true,
|
|
192
|
-
"autoReadAll":
|
|
194
|
+
"autoReadAll": "off",
|
|
193
195
|
"anchorGrepEnabled": true,
|
|
194
196
|
"requirePath": false,
|
|
195
197
|
"strictInput": false,
|
package/index.ts
CHANGED
|
@@ -11,10 +11,11 @@ import type { RMetrics } from "./src/replace-response";
|
|
|
11
11
|
import type { ReplaceDetails } from "./src/replace";
|
|
12
12
|
import { extractWarnings } from "./src/replace-render";
|
|
13
13
|
import { MAX_HASH_LINES } from "./src/hashline";
|
|
14
|
+
import type { AutoReadAllMode } from "./src/config";
|
|
14
15
|
import {
|
|
15
16
|
readConfigWithStatus,
|
|
16
17
|
toggleAutoRead,
|
|
17
|
-
|
|
18
|
+
cycleAutoReadAllMode,
|
|
18
19
|
toggleAnchorGrep,
|
|
19
20
|
toggleRequirePath,
|
|
20
21
|
toggleStrictInput,
|
|
@@ -45,7 +46,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
45
46
|
registerWriteHook(pi);
|
|
46
47
|
|
|
47
48
|
let autoRead = true;
|
|
48
|
-
let autoReadAll =
|
|
49
|
+
let autoReadAll: AutoReadAllMode = "off";
|
|
49
50
|
let autoReadAllInjected = false;
|
|
50
51
|
let grepWasActive = false;
|
|
51
52
|
|
|
@@ -80,7 +81,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
80
81
|
const { config, corrupted } = await readConfigWithStatus();
|
|
81
82
|
if (corrupted && (ctx as { hasUI?: boolean }).hasUI) ctx.ui.notify("Hashline config was corrupt and was reset to defaults", "warning");
|
|
82
83
|
autoRead = config.autoRead;
|
|
83
|
-
autoReadAll = config.autoReadAll
|
|
84
|
+
autoReadAll = config.autoReadAll ?? "off";
|
|
84
85
|
const sessionBranch = (ctx as { sessionManager?: { getBranch?: () => Array<{ type?: string; customType?: string }> } }).sessionManager?.getBranch?.() ?? [];
|
|
85
86
|
autoReadAllInjected = sessionBranch.some((entry) => entry.type === "custom_message" && entry.customType === AUTO_READ_ALL_CUSTOM_TYPE);
|
|
86
87
|
await refreshEditTools();
|
|
@@ -105,10 +106,10 @@ export default function (pi: ExtensionAPI): void {
|
|
|
105
106
|
});
|
|
106
107
|
|
|
107
108
|
pi.on("before_agent_start", async (_event, ctx) => withAnchorSession(ctx, async () => {
|
|
108
|
-
if (
|
|
109
|
+
if (autoReadAll === "off" || autoReadAllInjected) return;
|
|
109
110
|
autoReadAllInjected = true;
|
|
110
111
|
try {
|
|
111
|
-
const injection = await buildAutoReadAllInjection(ctx.cwd, autoReadAllBudget(ctx.model));
|
|
112
|
+
const injection = await buildAutoReadAllInjection(ctx.cwd, autoReadAllBudget(ctx.model), autoReadAll);
|
|
112
113
|
if (!injection) return;
|
|
113
114
|
if (ctx.hasUI) ctx.ui.notify(`Auto-read all: attached ${injection.files} file(s) with anchors`, "info");
|
|
114
115
|
return { message: { customType: AUTO_READ_ALL_CUSTOM_TYPE, content: injection.text, display: false } };
|
|
@@ -132,7 +133,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
132
133
|
done,
|
|
133
134
|
onToggle: async (key, delta) => {
|
|
134
135
|
if (key === "autoRead") autoRead = await toggleAutoRead();
|
|
135
|
-
else if (key === "autoReadAll") { autoReadAll = await
|
|
136
|
+
else if (key === "autoReadAll") { autoReadAll = await cycleAutoReadAllMode(); autoReadAllInjected = false; }
|
|
136
137
|
else if (key === "diffContextLines") await adjustDiffContextLines(delta ?? 1);
|
|
137
138
|
else if (key === "anchorGrepEnabled") {
|
|
138
139
|
const enabled = await toggleAnchorGrep();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-hashline-edit-pro",
|
|
3
|
-
"version": "4.3.
|
|
3
|
+
"version": "4.3.2",
|
|
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/prompts/grep.md
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
Search text files with ripgrep. Hits and context lines come back as `lineNumber │ anchor│content` rows, editable with replace or insert without a new read; the `=== path ===` header and line numbers locate the match. Searches respect `.gitignore`, always skip `.git`, and skip binary and image files silently. A match over 500 bytes is shown as a `...` fragment around the hit, but its anchor still covers the whole line. When the output says truncated, refine `pattern` or raise `limit
|
|
1
|
+
Search text files with ripgrep. Hits and context lines come back as `lineNumber │ anchor│content` rows, editable with replace or insert without a new read; the `=== path ===` header and line numbers locate the match. Searches respect `.gitignore`, always skip `.git`, and skip binary and image files silently. A match over 500 bytes is shown as a `...` fragment around the hit, but its anchor still covers the whole line. When the output says truncated, refine `pattern` or raise `limit` (default 100).
|
|
@@ -1,3 +1,2 @@
|
|
|
1
1
|
- `insert`: the anchor must have been shown by `read`, a post-edit diff (`+anchor│`/` anchor│`), or any served `anchor│content` row. Empty file: `read` shows one `anchor│` row — insert `after` it.
|
|
2
|
-
- `insert`: same-file calls in one message join the file's batch: earlier calls reply `In batch N`, the last call shows the combined diff.
|
|
3
2
|
- `insert`: a batch may pair one `before` and one `after` on the same anchor line; the pair composes into a single insertion. Any other same-line pair is an overlap.
|
package/prompts/insert.md
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
Insert lines after or before one existing line in a text file, addressed by a bare anchor from read output or a diff row. The anchor line is preserved: `lines` go after it with `direction: "after"` or before it with `direction: "before"`, one string per line, no anchor prefixes, no embedded newlines. Lines are added literally, even when they duplicate neighbors. Multiple `replace`/`insert` calls on the same file in one message form one batch per file
|
|
1
|
+
Insert lines after or before one existing line in a text file, addressed by a bare anchor from read output or a diff row. The anchor line is preserved: `lines` go after it with `direction: "after"` or before it with `direction: "before"`, one string per line, no anchor prefixes, no embedded newlines. Lines are added literally, even when they duplicate neighbors. Multiple `replace`/`insert` calls on the same file in one message form one batch per file: earlier calls reply `In batch N` and the last call shows the combined diff, with one undo for the whole batch.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
- `replace`: edit with `replace`/`insert`, not `sed -i` or heredocs — anchor edits are verified against what was shown and undoable.
|
|
2
2
|
- `replace`: `replacement_lines` takes bare lines without `│`; `[""]` is one blank line; pasted `anchor│` prefixes are stripped automatically (single line: same anchor for `remove_from` and `remove_to`).
|
|
3
|
-
- `replace`: post-edit diff `+anchor│`/` anchor│` rows are fresh anchors for the next edit — no new `read` needed
|
|
3
|
+
- `replace`: post-edit diff `+anchor│`/` anchor│` rows are fresh anchors for the next edit — no new `read` needed; never anchor on `-anchor│` rows, those anchors were freed by the edit. Check each batch diff before the next turn's edits on that file.
|
|
4
4
|
- `replace`: batched calls must target disjoint ranges and all be valid; an overlap or any failure aborts the whole batch with nothing applied.
|
|
5
5
|
- `replace`: if `replacement_lines` re-include the boundary line adjacent to the range, it is deduplicated automatically, shown as `dedup│content` rows in the diff (not editable, never use `dedup` as an anchor).
|
|
@@ -1 +1 @@
|
|
|
1
|
-
- `undo_last_change`: only the last `replace`/`insert` per file is undoable; a `write` clears it, so undo right after a bad diff
|
|
1
|
+
- `undo_last_change`: only the last `replace`/`insert` per file is undoable; a `write` clears it, so undo right after a bad diff — review the diff's `-anchor│` rows first to confirm what you're restoring.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
Single-level undo: reverts a file's last `replace` or `insert`
|
package/src/auto-read-all.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
AUTO_READ_ALL_MIN_BUDGET_BYTES,
|
|
10
10
|
SNIFF_BYTES,
|
|
11
11
|
} from "./constants";
|
|
12
|
+
import type { AutoReadAllMode } from "./config";
|
|
12
13
|
import { serveRows } from "./served";
|
|
13
14
|
import { readNormFile } from "./file-reader";
|
|
14
15
|
import { resolveRgPath } from "./grep";
|
|
@@ -21,9 +22,10 @@ const EXEC_MAX_BYTES = 64 * 1024 * 1024;
|
|
|
21
22
|
const SCAN_CONCURRENCY = 32;
|
|
22
23
|
const SCAN_LIMIT_MULTIPLIER = 4;
|
|
23
24
|
const MAX_REPORTED_OMISSIONS = 50;
|
|
25
|
+
export const AUTO_READ_ALL_CHUNK_BYTES = 48 * 1024;
|
|
24
26
|
|
|
25
27
|
const HEADER =
|
|
26
|
-
"[hashline auto-read-all] The content of every non-ignored project file is attached below with live hashline anchors. Each anchor│content row is already owned and served for this session, so replace and insert can target those anchors directly without calling read first. Rows with a truncation hint are only partially shown; call read with the hinted offset to see the rest.";
|
|
28
|
+
"[hashline auto-read-all] The content of every non-ignored project file is attached below with live hashline anchors. Each anchor│content row is already owned and served for this session, so replace and insert can target those anchors directly without calling read first. Sections marked [complete] are fully attached; do NOT call read for them. Only sections marked [truncated] need read. Rows with a truncation hint are only partially shown; call read with the hinted offset to see the rest.";
|
|
27
29
|
|
|
28
30
|
const IMAGE_EXTENSIONS = new Set([
|
|
29
31
|
".avif",
|
|
@@ -37,14 +39,71 @@ const IMAGE_EXTENSIONS = new Set([
|
|
|
37
39
|
".jxl",
|
|
38
40
|
".png",
|
|
39
41
|
".psd",
|
|
42
|
+
".svg",
|
|
40
43
|
".tif",
|
|
41
44
|
".tiff",
|
|
42
45
|
".webp",
|
|
43
46
|
]);
|
|
44
47
|
|
|
45
|
-
export const
|
|
46
|
-
|
|
48
|
+
export const AUTO_READ_ALL_EXCLUDED_SEGMENTS = [
|
|
49
|
+
"vendor",
|
|
50
|
+
"node_modules",
|
|
51
|
+
"bower_components",
|
|
52
|
+
"third_party",
|
|
53
|
+
"thirdparty",
|
|
54
|
+
"jspm_packages",
|
|
55
|
+
".venv",
|
|
56
|
+
"venv",
|
|
57
|
+
"site-packages",
|
|
58
|
+
"__pycache__",
|
|
59
|
+
".tox",
|
|
60
|
+
".gradle",
|
|
61
|
+
".terraform",
|
|
62
|
+
"pods",
|
|
63
|
+
"carthage",
|
|
64
|
+
"deriveddata",
|
|
65
|
+
"coreui",
|
|
66
|
+
"coreui-icons",
|
|
67
|
+
];
|
|
68
|
+
export const AUTO_READ_ALL_EXCLUDED_NAMES = [
|
|
69
|
+
"package-lock.json",
|
|
70
|
+
"yarn.lock",
|
|
71
|
+
"composer.lock",
|
|
72
|
+
"gemfile.lock",
|
|
73
|
+
"cargo.lock",
|
|
74
|
+
"poetry.lock",
|
|
75
|
+
"pipfile.lock",
|
|
76
|
+
"go.sum",
|
|
77
|
+
"flake.lock",
|
|
78
|
+
".eslintcache",
|
|
79
|
+
];
|
|
47
80
|
const EXCLUDED_NAME_SET = new Set(AUTO_READ_ALL_EXCLUDED_NAMES);
|
|
81
|
+
const EXCLUDED_SEGMENT_SET = new Set(AUTO_READ_ALL_EXCLUDED_SEGMENTS);
|
|
82
|
+
function isExcludedBySegment(path: string): boolean {
|
|
83
|
+
for (const segment of path.split("/")) {
|
|
84
|
+
if (EXCLUDED_SEGMENT_SET.has(segment.toLowerCase())) return true;
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
function isExcludedByPattern(baseLower: string): boolean {
|
|
89
|
+
if (baseLower.endsWith(".min.js") || baseLower.endsWith(".min.css") || baseLower.endsWith(".min.mjs")) return true;
|
|
90
|
+
if (baseLower.endsWith("-min.js") || baseLower.endsWith("-min.css")) return true;
|
|
91
|
+
if (baseLower.includes(".bundle.") || baseLower.includes(".chunk.")) return true;
|
|
92
|
+
if (baseLower.endsWith(".umd.js")) return true;
|
|
93
|
+
if (baseLower.endsWith(".map")) return true;
|
|
94
|
+
if (baseLower.endsWith(".lock")) return true;
|
|
95
|
+
if (baseLower.includes(".generated.") || baseLower.includes(".gen.")) return true;
|
|
96
|
+
if (baseLower.endsWith("_pb2.py")) return true;
|
|
97
|
+
if (baseLower.endsWith(".pb.go")) return true;
|
|
98
|
+
if (baseLower.endsWith(".g.dart")) return true;
|
|
99
|
+
if (baseLower.endsWith(".freezed.dart")) return true;
|
|
100
|
+
if (baseLower.endsWith(".designer.cs")) return true;
|
|
101
|
+
if (baseLower.endsWith(".g.cs")) return true;
|
|
102
|
+
if (baseLower.endsWith(".snap")) return true;
|
|
103
|
+
if (baseLower.startsWith("coreui-icons.")) return true;
|
|
104
|
+
if (baseLower === "coreui.css") return true;
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
48
107
|
|
|
49
108
|
const WALK_IGNORED_DIRS = new Set([
|
|
50
109
|
".git",
|
|
@@ -78,11 +137,22 @@ export interface AutoReadAllDiscovery {
|
|
|
78
137
|
skippedByName: number;
|
|
79
138
|
}
|
|
80
139
|
|
|
140
|
+
export interface AutoReadAllSection {
|
|
141
|
+
file: string;
|
|
142
|
+
text: string;
|
|
143
|
+
complete: boolean;
|
|
144
|
+
totalLines: number;
|
|
145
|
+
shownLines: number;
|
|
146
|
+
nextOffset?: number;
|
|
147
|
+
}
|
|
81
148
|
export interface AutoReadAllInjection {
|
|
82
149
|
text: string;
|
|
83
150
|
files: number;
|
|
84
151
|
bytes: number;
|
|
85
152
|
omitted: string[];
|
|
153
|
+
chunks: string[];
|
|
154
|
+
completeFiles: number;
|
|
155
|
+
truncatedFiles: number;
|
|
86
156
|
}
|
|
87
157
|
|
|
88
158
|
function runCommand(command: string, args: string[], cwd: string): Promise<{ stdout: string; code: number }> {
|
|
@@ -128,7 +198,7 @@ async function walkDir(dir: string, base: string, out: string[]): Promise<void>
|
|
|
128
198
|
if (entry.isSymbolicLink()) continue;
|
|
129
199
|
const full = join(dir, entry.name);
|
|
130
200
|
if (entry.isDirectory()) {
|
|
131
|
-
if (WALK_IGNORED_DIRS.has(entry.name)) continue;
|
|
201
|
+
if (WALK_IGNORED_DIRS.has(entry.name) || EXCLUDED_SEGMENT_SET.has(entry.name.toLowerCase())) continue;
|
|
132
202
|
await walkDir(full, base, out);
|
|
133
203
|
} else if (entry.isFile()) {
|
|
134
204
|
out.push(toPosix(relative(base, full)));
|
|
@@ -180,10 +250,13 @@ async function forEachLimit<T>(items: T[], limit: number, work: (item: T) => Pro
|
|
|
180
250
|
await Promise.all(workers);
|
|
181
251
|
}
|
|
182
252
|
|
|
183
|
-
export async function discoverAutoReadAllFiles(cwd: string): Promise<AutoReadAllDiscovery> {
|
|
253
|
+
export async function discoverAutoReadAllFiles(cwd: string, mode: AutoReadAllMode = "on"): Promise<AutoReadAllDiscovery> {
|
|
184
254
|
let source: AutoReadAllSource = "git";
|
|
185
255
|
let candidates = await listFromGit(cwd);
|
|
186
256
|
if (candidates === undefined) {
|
|
257
|
+
if (mode === "git") {
|
|
258
|
+
return { files: [], source: "git", discovered: 0, skippedBinary: 0, skippedLarge: 0, skippedOther: 0, skippedByName: 0 };
|
|
259
|
+
}
|
|
187
260
|
candidates = await listFromRg(cwd);
|
|
188
261
|
source = "rg";
|
|
189
262
|
}
|
|
@@ -198,7 +271,8 @@ export async function discoverAutoReadAllFiles(cwd: string): Promise<AutoReadAll
|
|
|
198
271
|
const includable: string[] = [];
|
|
199
272
|
let skippedByName = 0;
|
|
200
273
|
for (const file of unique) {
|
|
201
|
-
|
|
274
|
+
const baseLower = baseNameOf(file).toLowerCase();
|
|
275
|
+
if (EXCLUDED_NAME_SET.has(baseLower) || isExcludedBySegment(file) || isExcludedByPattern(baseLower)) skippedByName += 1;
|
|
202
276
|
else includable.push(file);
|
|
203
277
|
}
|
|
204
278
|
const scanWindow = includable.slice(0, AUTO_READ_ALL_MAX_FILES * SCAN_LIMIT_MULTIPLIER);
|
|
@@ -243,12 +317,34 @@ export async function discoverAutoReadAllFiles(cwd: string): Promise<AutoReadAll
|
|
|
243
317
|
return { files, source, discovered: unique.length, skippedBinary, skippedLarge, skippedOther, skippedByName };
|
|
244
318
|
}
|
|
245
319
|
|
|
246
|
-
|
|
320
|
+
export function chunkAutoReadAllSections(sections: string[], maxBytes: number = AUTO_READ_ALL_CHUNK_BYTES): string[] {
|
|
321
|
+
const chunks: string[] = [];
|
|
322
|
+
let current: string[] = [];
|
|
323
|
+
let currentBytes = 0;
|
|
324
|
+
for (const section of sections) {
|
|
325
|
+
const sectionBytes = Buffer.byteLength(section, "utf-8") + 2;
|
|
326
|
+
if (current.length > 0 && currentBytes + sectionBytes > maxBytes) {
|
|
327
|
+
chunks.push(current.join("\n\n"));
|
|
328
|
+
current = [];
|
|
329
|
+
currentBytes = 0;
|
|
330
|
+
}
|
|
331
|
+
current.push(section);
|
|
332
|
+
currentBytes += sectionBytes;
|
|
333
|
+
}
|
|
334
|
+
if (current.length > 0) chunks.push(current.join("\n\n"));
|
|
335
|
+
return chunks;
|
|
336
|
+
}
|
|
337
|
+
async function renderFile(file: string, cwd: string): Promise<AutoReadAllSection | undefined> {
|
|
247
338
|
try {
|
|
248
339
|
const { normalized, fileHashes, absolutePath } = await readNormFile(file, cwd, { maxLines: MAX_HASH_LINES });
|
|
249
340
|
const preview = await fmtReadPreview(normalized, {}, fileHashes, absolutePath, DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES);
|
|
250
341
|
serveRows(absolutePath, fileHashes, splitLines(normalized), preview.servedHashes);
|
|
251
|
-
|
|
342
|
+
const totalLines = fileHashes.length;
|
|
343
|
+
const shownLines = preview.servedHashes.length;
|
|
344
|
+
const complete = preview.truncation === undefined && preview.nextOffset === undefined && shownLines >= totalLines;
|
|
345
|
+
const status = complete ? `[complete, ${totalLines} lines; do NOT re-read]` : `[truncated, showing ${shownLines} of ${totalLines} lines; use read with offset=${preview.nextOffset ?? shownLines + 1} to continue]`;
|
|
346
|
+
const nextOffset = preview.nextOffset;
|
|
347
|
+
return { file, text: `=== ${file} ===\n${status}\n${preview.text}`, complete, totalLines, shownLines, ...(nextOffset !== undefined ? { nextOffset } : {}) };
|
|
252
348
|
} catch (error) {
|
|
253
349
|
console.error(`Auto-read all: skipped ${file}:`, error);
|
|
254
350
|
return undefined;
|
|
@@ -265,7 +361,7 @@ function buildFooter(attached: number, discovery: AutoReadAllDiscovery, omitted:
|
|
|
265
361
|
if (discovery.skippedBinary > 0) notes.push(`${discovery.skippedBinary} binary or image file(s) skipped`);
|
|
266
362
|
if (discovery.skippedLarge > 0) notes.push(`${discovery.skippedLarge} file(s) over ${formatSize(AUTO_READ_ALL_MAX_FILE_BYTES)} skipped`);
|
|
267
363
|
if (discovery.skippedOther > 0) notes.push(`${discovery.skippedOther} unreadable path(s) skipped`);
|
|
268
|
-
if (discovery.skippedByName > 0) notes.push(`${discovery.skippedByName} file(s) skipped by name
|
|
364
|
+
if (discovery.skippedByName > 0) notes.push(`${discovery.skippedByName} file(s) skipped by vendor/name/pattern rules`);
|
|
269
365
|
const listed = omitted.slice(0, MAX_REPORTED_OMISSIONS).join(", ");
|
|
270
366
|
const more = omitted.length > MAX_REPORTED_OMISSIONS ? `, ... (+${omitted.length - MAX_REPORTED_OMISSIONS} more)` : "";
|
|
271
367
|
const omissionNote = omitted.length > 0 ? ` Not attached: ${listed}${more}. Use read for those.` : "";
|
|
@@ -273,29 +369,36 @@ function buildFooter(attached: number, discovery: AutoReadAllDiscovery, omitted:
|
|
|
273
369
|
return `[hashline auto-read-all: ${attached} file(s) attached from ${discovery.source}; ${summary}${omissionNote}]`;
|
|
274
370
|
}
|
|
275
371
|
|
|
276
|
-
export async function buildAutoReadAllInjection(cwd: string, budgetBytes: number): Promise<AutoReadAllInjection | undefined> {
|
|
277
|
-
const discovery = await discoverAutoReadAllFiles(cwd);
|
|
372
|
+
export async function buildAutoReadAllInjection(cwd: string, budgetBytes: number, mode: AutoReadAllMode = "on"): Promise<AutoReadAllInjection | undefined> {
|
|
373
|
+
const discovery = await discoverAutoReadAllFiles(cwd, mode);
|
|
278
374
|
if (discovery.files.length === 0) return undefined;
|
|
279
|
-
const sections:
|
|
375
|
+
const sections: AutoReadAllSection[] = [];
|
|
280
376
|
const omitted: string[] = [];
|
|
281
377
|
let bytes = 0;
|
|
378
|
+
let completeFiles = 0;
|
|
379
|
+
let truncatedFiles = 0;
|
|
282
380
|
for (const file of discovery.files) {
|
|
283
381
|
const section = await renderFile(file, cwd);
|
|
284
382
|
if (section === undefined) {
|
|
285
383
|
omitted.push(file);
|
|
286
384
|
continue;
|
|
287
385
|
}
|
|
288
|
-
const sectionBytes = Buffer.byteLength(section, "utf-8") + 1;
|
|
386
|
+
const sectionBytes = Buffer.byteLength(section.text, "utf-8") + 1;
|
|
289
387
|
if (sections.length > 0 && bytes + sectionBytes > budgetBytes) {
|
|
290
388
|
omitted.push(file);
|
|
291
389
|
continue;
|
|
292
390
|
}
|
|
293
391
|
sections.push(section);
|
|
294
392
|
bytes += sectionBytes;
|
|
393
|
+
if (section.complete) completeFiles += 1;
|
|
394
|
+
else truncatedFiles += 1;
|
|
295
395
|
}
|
|
296
396
|
if (sections.length === 0) return undefined;
|
|
297
|
-
const
|
|
298
|
-
|
|
397
|
+
const sectionTexts = sections.map((section) => section.text);
|
|
398
|
+
const chunks = chunkAutoReadAllSections(sectionTexts);
|
|
399
|
+
const coverage = `[coverage: ${completeFiles} complete, ${truncatedFiles} truncated, ${chunks.length} chunk(s) x 48KB with file boundaries preserved; do NOT re-read complete files]`;
|
|
400
|
+
const text = `${HEADER}\n\n${coverage}\n\n${sectionTexts.join("\n\n")}\n\n${buildFooter(sections.length, discovery, omitted)}`;
|
|
401
|
+
return { text, files: sections.length, bytes, omitted, chunks, completeFiles, truncatedFiles };
|
|
299
402
|
}
|
|
300
403
|
|
|
301
404
|
export function autoReadAllBudget(model: { contextWindow?: number } | undefined): number {
|
package/src/config-ui.ts
CHANGED
|
@@ -18,7 +18,7 @@ export interface ConfigRow {
|
|
|
18
18
|
export function configRows(config: Config): ConfigRow[] {
|
|
19
19
|
return [
|
|
20
20
|
{ key: "autoRead", label: "Auto-read", hint: "Anchors after write + post-edit diffs", enabled: config.autoRead !== false },
|
|
21
|
-
{ key: "autoReadAll", label: "Auto-read all", hint: "Attach
|
|
21
|
+
{ key: "autoReadAll", label: "Auto-read all", hint: "Attach files on the first turn: off, on, git (git repos only)", enabled: (config.autoReadAll ?? "off") !== "off", mode: config.autoReadAll ?? "off", cycle: ["off", "on", "git"] },
|
|
22
22
|
{ key: "diffContextLines", label: "Diff context", hint: "Surrounding lines in post-edit diffs (needs Auto-read)", enabled: config.autoRead !== false, value: config.diffContextLines ?? 1, disabled: config.autoRead === false },
|
|
23
23
|
{ key: "anchorGrepEnabled", label: "Anchor grep", hint: "anchor_grep tool (builtin grep off while on)", enabled: config.anchorGrepEnabled === true },
|
|
24
24
|
{ key: "requirePath", label: "Require path", hint: "replace + insert need path (RPC visibility)", enabled: config.requirePath === true },
|
package/src/config.ts
CHANGED
|
@@ -4,6 +4,8 @@ import { configPath } from "./paths";
|
|
|
4
4
|
import { errCode, isRec } from "./utils";
|
|
5
5
|
import { writeAtomic } from "./fs-write";
|
|
6
6
|
export type BoundaryDedupMode = "on" | "off" | "strict";
|
|
7
|
+
export type AutoReadAllMode = "off" | "on" | "git";
|
|
8
|
+
const AUTO_READ_ALL_MODES: AutoReadAllMode[] = ["off", "on", "git"];
|
|
7
9
|
|
|
8
10
|
export const DEFAULT_DIFF_CONTEXT_LINES = 1;
|
|
9
11
|
export const MIN_DIFF_CONTEXT_LINES = 0;
|
|
@@ -12,7 +14,7 @@ export const MAX_DIFF_CONTEXT_LINES = 10;
|
|
|
12
14
|
export interface Config {
|
|
13
15
|
autoRead: boolean;
|
|
14
16
|
anchorGrepEnabled: boolean;
|
|
15
|
-
autoReadAll?:
|
|
17
|
+
autoReadAll?: AutoReadAllMode;
|
|
16
18
|
requirePath?: boolean;
|
|
17
19
|
strictInput?: boolean;
|
|
18
20
|
boundaryDedupMode?: BoundaryDedupMode;
|
|
@@ -22,7 +24,7 @@ export interface Config {
|
|
|
22
24
|
const DEFAULT_CONFIG: Config = {
|
|
23
25
|
autoRead: true,
|
|
24
26
|
anchorGrepEnabled: true,
|
|
25
|
-
autoReadAll:
|
|
27
|
+
autoReadAll: "off",
|
|
26
28
|
requirePath: false,
|
|
27
29
|
strictInput: false,
|
|
28
30
|
boundaryDedupMode: "on",
|
|
@@ -38,6 +40,13 @@ function parseBoundaryDedupMode(mode: unknown, legacy: unknown): BoundaryDedupMo
|
|
|
38
40
|
return DEFAULT_CONFIG.boundaryDedupMode ?? "on";
|
|
39
41
|
}
|
|
40
42
|
|
|
43
|
+
function parseAutoReadAllMode(value: unknown): AutoReadAllMode {
|
|
44
|
+
if (value === "off" || value === "on" || value === "git") return value;
|
|
45
|
+
if (value === true) return "on";
|
|
46
|
+
if (value === false) return "off";
|
|
47
|
+
return DEFAULT_CONFIG.autoReadAll ?? "off";
|
|
48
|
+
}
|
|
49
|
+
|
|
41
50
|
export function normalizeDiffContextLines(value: unknown): number {
|
|
42
51
|
if (typeof value !== "number" || !Number.isFinite(value)) return DEFAULT_DIFF_CONTEXT_LINES;
|
|
43
52
|
const floored = Math.floor(value);
|
|
@@ -62,7 +71,7 @@ function parseConfig(content: string): Config {
|
|
|
62
71
|
return {
|
|
63
72
|
autoRead: typeof autoRead === "boolean" ? autoRead : DEFAULT_CONFIG.autoRead,
|
|
64
73
|
anchorGrepEnabled: typeof anchorGrepEnabled === "boolean" ? anchorGrepEnabled : DEFAULT_CONFIG.anchorGrepEnabled,
|
|
65
|
-
autoReadAll:
|
|
74
|
+
autoReadAll: parseAutoReadAllMode(autoReadAll),
|
|
66
75
|
requirePath: typeof requirePath === "boolean" ? requirePath : DEFAULT_CONFIG.requirePath,
|
|
67
76
|
strictInput: typeof strictInput === "boolean" ? strictInput : DEFAULT_CONFIG.strictInput,
|
|
68
77
|
boundaryDedupMode: parseBoundaryDedupMode(boundaryDedupMode, legacyBoundaryDedup),
|
|
@@ -174,7 +183,7 @@ export async function writeConfig(config: Config): Promise<void> {
|
|
|
174
183
|
}
|
|
175
184
|
|
|
176
185
|
|
|
177
|
-
type ToggleKey = "autoRead" | "anchorGrepEnabled" | "
|
|
186
|
+
type ToggleKey = "autoRead" | "anchorGrepEnabled" | "requirePath" | "strictInput";
|
|
178
187
|
|
|
179
188
|
async function toggleFlag(key: ToggleKey): Promise<boolean> {
|
|
180
189
|
const config = await updateConfig((c) => { c[key] = !(c[key] === true); });
|
|
@@ -182,7 +191,15 @@ async function toggleFlag(key: ToggleKey): Promise<boolean> {
|
|
|
182
191
|
}
|
|
183
192
|
export const toggleAutoRead = (): Promise<boolean> => toggleFlag("autoRead");
|
|
184
193
|
export const toggleAnchorGrep = (): Promise<boolean> => toggleFlag("anchorGrepEnabled");
|
|
185
|
-
export
|
|
194
|
+
export async function cycleAutoReadAllMode(): Promise<AutoReadAllMode> {
|
|
195
|
+
let next: AutoReadAllMode = "off";
|
|
196
|
+
await updateConfig((c) => {
|
|
197
|
+
const current = c.autoReadAll ?? "off";
|
|
198
|
+
next = AUTO_READ_ALL_MODES[(AUTO_READ_ALL_MODES.indexOf(current) + 1) % AUTO_READ_ALL_MODES.length] ?? "off";
|
|
199
|
+
c.autoReadAll = next;
|
|
200
|
+
});
|
|
201
|
+
return next;
|
|
202
|
+
}
|
|
186
203
|
export const toggleRequirePath = (): Promise<boolean> => toggleFlag("requirePath");
|
|
187
204
|
export const toggleStrictInput = (): Promise<boolean> => toggleFlag("strictInput");
|
|
188
205
|
export async function cycleBoundaryDedupMode(): Promise<BoundaryDedupMode> {
|
package/src/edit-common.ts
CHANGED
|
@@ -45,6 +45,9 @@ export function withReplacePrompts(base: { description: string; snippet: string;
|
|
|
45
45
|
descriptionParts.push("Also give `path` matching the file the anchors were served for; it is required and must match anchor ownership.");
|
|
46
46
|
snippetParts.push("; include `path` (required)");
|
|
47
47
|
guidelines.push("`replace`: include `path` matching the file the anchors were served for; it is required.");
|
|
48
|
+
} else {
|
|
49
|
+
descriptionParts.push("Path resolution is anchor-only; do not pass `path`.");
|
|
50
|
+
guidelines.push("`replace`: path resolution is anchor-only; don't pass `path`.");
|
|
48
51
|
}
|
|
49
52
|
if (flags.strictInput) {
|
|
50
53
|
descriptionParts.push("Strict-input mode is on: auto-fixable slips are rejected instead of fixed with warnings.");
|
|
@@ -74,6 +77,9 @@ export function withInsertPrompts(base: { description: string; snippet: string;
|
|
|
74
77
|
descriptionParts.push("Also give `path` matching the file the anchor was served for; it is required and must match anchor ownership.");
|
|
75
78
|
snippetParts.push("; include `path` (required)");
|
|
76
79
|
guidelines.push("`insert`: include `path` matching the file the anchor was served for; it is required.");
|
|
80
|
+
} else {
|
|
81
|
+
descriptionParts.push("Path resolution is anchor-only; do not pass `path`.");
|
|
82
|
+
guidelines.push("`insert`: path resolution is anchor-only; don't pass `path`.");
|
|
77
83
|
}
|
|
78
84
|
if (flags.strictInput) {
|
|
79
85
|
descriptionParts.push("Strict-input mode is on: auto-fixable slips are rejected instead of fixed with warnings.");
|
package/src/payload-contract.ts
CHANGED