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 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, and any file whose name is in the built-in skip list (currently `package-lock.json`, matched by file name anywhere in the tree) are skipped. The attachment stops at 500 files or at a byte budget derived from the model's 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`.
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.1",
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",
@@ -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 AUTO_READ_ALL_EXCLUDED_NAMES = ["package-lock.json"];
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
- if (EXCLUDED_NAME_SET.has(baseNameOf(file).toLowerCase())) skippedByName += 1;
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
- async function renderFile(file: string, cwd: string): Promise<string | undefined> {
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
- return `=== ${file} ===\n${preview.text}`;
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 (${AUTO_READ_ALL_EXCLUDED_NAMES.join(", ")})`);
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: string[] = [];
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 text = `${HEADER}\n\n${sections.join("\n\n")}\n\n${buildFooter(sections.length, discovery, omitted)}`;
302
- return { text, files: sections.length, bytes, omitted };
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 {