pi-hashline-edit-pro 4.3.1 → 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 +3 -1
- package/package.json +1 -1
- package/src/auto-read-all.ts +111 -12
package/README.md
CHANGED
|
@@ -162,7 +162,9 @@ Auto-read keeps the same 50KB and 2000-line budget as `read`. Auto-read and Diff
|
|
|
162
162
|
|
|
163
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
|
+
|
|
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.
|
|
166
168
|
|
|
167
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
|
|
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/src/auto-read-all.ts
CHANGED
|
@@ -22,9 +22,10 @@ const EXEC_MAX_BYTES = 64 * 1024 * 1024;
|
|
|
22
22
|
const SCAN_CONCURRENCY = 32;
|
|
23
23
|
const SCAN_LIMIT_MULTIPLIER = 4;
|
|
24
24
|
const MAX_REPORTED_OMISSIONS = 50;
|
|
25
|
+
export const AUTO_READ_ALL_CHUNK_BYTES = 48 * 1024;
|
|
25
26
|
|
|
26
27
|
const HEADER =
|
|
27
|
-
"[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.";
|
|
28
29
|
|
|
29
30
|
const IMAGE_EXTENSIONS = new Set([
|
|
30
31
|
".avif",
|
|
@@ -38,14 +39,71 @@ const IMAGE_EXTENSIONS = new Set([
|
|
|
38
39
|
".jxl",
|
|
39
40
|
".png",
|
|
40
41
|
".psd",
|
|
42
|
+
".svg",
|
|
41
43
|
".tif",
|
|
42
44
|
".tiff",
|
|
43
45
|
".webp",
|
|
44
46
|
]);
|
|
45
47
|
|
|
46
|
-
export const
|
|
47
|
-
|
|
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
|
+
];
|
|
48
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
|
+
}
|
|
49
107
|
|
|
50
108
|
const WALK_IGNORED_DIRS = new Set([
|
|
51
109
|
".git",
|
|
@@ -79,11 +137,22 @@ export interface AutoReadAllDiscovery {
|
|
|
79
137
|
skippedByName: number;
|
|
80
138
|
}
|
|
81
139
|
|
|
140
|
+
export interface AutoReadAllSection {
|
|
141
|
+
file: string;
|
|
142
|
+
text: string;
|
|
143
|
+
complete: boolean;
|
|
144
|
+
totalLines: number;
|
|
145
|
+
shownLines: number;
|
|
146
|
+
nextOffset?: number;
|
|
147
|
+
}
|
|
82
148
|
export interface AutoReadAllInjection {
|
|
83
149
|
text: string;
|
|
84
150
|
files: number;
|
|
85
151
|
bytes: number;
|
|
86
152
|
omitted: string[];
|
|
153
|
+
chunks: string[];
|
|
154
|
+
completeFiles: number;
|
|
155
|
+
truncatedFiles: number;
|
|
87
156
|
}
|
|
88
157
|
|
|
89
158
|
function runCommand(command: string, args: string[], cwd: string): Promise<{ stdout: string; code: number }> {
|
|
@@ -129,7 +198,7 @@ async function walkDir(dir: string, base: string, out: string[]): Promise<void>
|
|
|
129
198
|
if (entry.isSymbolicLink()) continue;
|
|
130
199
|
const full = join(dir, entry.name);
|
|
131
200
|
if (entry.isDirectory()) {
|
|
132
|
-
if (WALK_IGNORED_DIRS.has(entry.name)) continue;
|
|
201
|
+
if (WALK_IGNORED_DIRS.has(entry.name) || EXCLUDED_SEGMENT_SET.has(entry.name.toLowerCase())) continue;
|
|
133
202
|
await walkDir(full, base, out);
|
|
134
203
|
} else if (entry.isFile()) {
|
|
135
204
|
out.push(toPosix(relative(base, full)));
|
|
@@ -202,7 +271,8 @@ export async function discoverAutoReadAllFiles(cwd: string, mode: AutoReadAllMod
|
|
|
202
271
|
const includable: string[] = [];
|
|
203
272
|
let skippedByName = 0;
|
|
204
273
|
for (const file of unique) {
|
|
205
|
-
|
|
274
|
+
const baseLower = baseNameOf(file).toLowerCase();
|
|
275
|
+
if (EXCLUDED_NAME_SET.has(baseLower) || isExcludedBySegment(file) || isExcludedByPattern(baseLower)) skippedByName += 1;
|
|
206
276
|
else includable.push(file);
|
|
207
277
|
}
|
|
208
278
|
const scanWindow = includable.slice(0, AUTO_READ_ALL_MAX_FILES * SCAN_LIMIT_MULTIPLIER);
|
|
@@ -247,12 +317,34 @@ export async function discoverAutoReadAllFiles(cwd: string, mode: AutoReadAllMod
|
|
|
247
317
|
return { files, source, discovered: unique.length, skippedBinary, skippedLarge, skippedOther, skippedByName };
|
|
248
318
|
}
|
|
249
319
|
|
|
250
|
-
|
|
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> {
|
|
251
338
|
try {
|
|
252
339
|
const { normalized, fileHashes, absolutePath } = await readNormFile(file, cwd, { maxLines: MAX_HASH_LINES });
|
|
253
340
|
const preview = await fmtReadPreview(normalized, {}, fileHashes, absolutePath, DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES);
|
|
254
341
|
serveRows(absolutePath, fileHashes, splitLines(normalized), preview.servedHashes);
|
|
255
|
-
|
|
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 } : {}) };
|
|
256
348
|
} catch (error) {
|
|
257
349
|
console.error(`Auto-read all: skipped ${file}:`, error);
|
|
258
350
|
return undefined;
|
|
@@ -269,7 +361,7 @@ function buildFooter(attached: number, discovery: AutoReadAllDiscovery, omitted:
|
|
|
269
361
|
if (discovery.skippedBinary > 0) notes.push(`${discovery.skippedBinary} binary or image file(s) skipped`);
|
|
270
362
|
if (discovery.skippedLarge > 0) notes.push(`${discovery.skippedLarge} file(s) over ${formatSize(AUTO_READ_ALL_MAX_FILE_BYTES)} skipped`);
|
|
271
363
|
if (discovery.skippedOther > 0) notes.push(`${discovery.skippedOther} unreadable path(s) skipped`);
|
|
272
|
-
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`);
|
|
273
365
|
const listed = omitted.slice(0, MAX_REPORTED_OMISSIONS).join(", ");
|
|
274
366
|
const more = omitted.length > MAX_REPORTED_OMISSIONS ? `, ... (+${omitted.length - MAX_REPORTED_OMISSIONS} more)` : "";
|
|
275
367
|
const omissionNote = omitted.length > 0 ? ` Not attached: ${listed}${more}. Use read for those.` : "";
|
|
@@ -280,26 +372,33 @@ function buildFooter(attached: number, discovery: AutoReadAllDiscovery, omitted:
|
|
|
280
372
|
export async function buildAutoReadAllInjection(cwd: string, budgetBytes: number, mode: AutoReadAllMode = "on"): Promise<AutoReadAllInjection | undefined> {
|
|
281
373
|
const discovery = await discoverAutoReadAllFiles(cwd, mode);
|
|
282
374
|
if (discovery.files.length === 0) return undefined;
|
|
283
|
-
const sections:
|
|
375
|
+
const sections: AutoReadAllSection[] = [];
|
|
284
376
|
const omitted: string[] = [];
|
|
285
377
|
let bytes = 0;
|
|
378
|
+
let completeFiles = 0;
|
|
379
|
+
let truncatedFiles = 0;
|
|
286
380
|
for (const file of discovery.files) {
|
|
287
381
|
const section = await renderFile(file, cwd);
|
|
288
382
|
if (section === undefined) {
|
|
289
383
|
omitted.push(file);
|
|
290
384
|
continue;
|
|
291
385
|
}
|
|
292
|
-
const sectionBytes = Buffer.byteLength(section, "utf-8") + 1;
|
|
386
|
+
const sectionBytes = Buffer.byteLength(section.text, "utf-8") + 1;
|
|
293
387
|
if (sections.length > 0 && bytes + sectionBytes > budgetBytes) {
|
|
294
388
|
omitted.push(file);
|
|
295
389
|
continue;
|
|
296
390
|
}
|
|
297
391
|
sections.push(section);
|
|
298
392
|
bytes += sectionBytes;
|
|
393
|
+
if (section.complete) completeFiles += 1;
|
|
394
|
+
else truncatedFiles += 1;
|
|
299
395
|
}
|
|
300
396
|
if (sections.length === 0) return undefined;
|
|
301
|
-
const
|
|
302
|
-
|
|
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 };
|
|
303
402
|
}
|
|
304
403
|
|
|
305
404
|
export function autoReadAllBudget(model: { contextWindow?: number } | undefined): number {
|