pi-hashline-edit-pro 6.4.4 → 6.4.5
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 +1 -1
- package/index.ts +58 -10
- package/package.json +1 -1
- package/src/auto-read-all.ts +195 -17
- package/src/constants.ts +1 -0
package/README.md
CHANGED
|
@@ -341,7 +341,7 @@ Auto-read keeps the same 50KB and 2000-line budget as `read`. Auto-read and Diff
|
|
|
341
341
|
|
|
342
342
|
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.
|
|
343
343
|
|
|
344
|
-
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; `full` also caps the budget at 2MB, while `outline` follows the context-derived budget because outlines stay compact. 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.
|
|
344
|
+
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; `full` also caps the budget at 2MB, while `outline` follows the context-derived budget because outlines stay compact. Files are ranked before the budget applies: working-tree changes first, then manifest and entry files, then the dominant source tree, then everything else, and no single top-level directory may take more than 40% of the budget in the first pass, so one large subtree cannot crowd out the rest. 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.
|
|
345
345
|
|
|
346
346
|
Each attached file starts with a header (`=== path ===` in `full` mode, `=== path (language) — lines ===` in `outline` mode) followed by its anchor rows. Edit directly from the attachment with replace and insert, so no `read` is needed. `full` attaches files whole; `outline` attaches each file's structural outline within the budget. In outline mode, multi-line symbols appear as labels with `[limit N]` and single-line symbols appear as their content.
|
|
347
347
|
|
package/index.ts
CHANGED
|
@@ -46,6 +46,11 @@ import { valAccess } from "./src/validation";
|
|
|
46
46
|
import { splitLines } from "./src/utils";
|
|
47
47
|
import { AUTO_READ_ALL_CUSTOM_TYPE } from "./src/constants";
|
|
48
48
|
|
|
49
|
+
interface PendingRegistrySession {
|
|
50
|
+
key: string | undefined;
|
|
51
|
+
alive: () => boolean;
|
|
52
|
+
}
|
|
53
|
+
|
|
49
54
|
export default function (pi: ExtensionAPI): void {
|
|
50
55
|
regRead(pi);
|
|
51
56
|
|
|
@@ -72,7 +77,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
72
77
|
let grepFlavorCwd: string | undefined;
|
|
73
78
|
const baseAnchorTools = new Set<string>();
|
|
74
79
|
let gateApplied = false;
|
|
75
|
-
const
|
|
80
|
+
const pendingRegistrySessions: PendingRegistrySession[] = [];
|
|
76
81
|
|
|
77
82
|
async function refreshEditTools(): Promise<void> {
|
|
78
83
|
try {
|
|
@@ -145,8 +150,32 @@ export default function (pi: ExtensionAPI): void {
|
|
|
145
150
|
pi.setActiveTools([...new Set([...base, ...enabled])]);
|
|
146
151
|
}
|
|
147
152
|
|
|
153
|
+
function captureRegistrySession(ctx: AnchorSessionContext | undefined): void {
|
|
154
|
+
const key = sessionKeyFor(ctx);
|
|
155
|
+
const entry: PendingRegistrySession = {
|
|
156
|
+
key,
|
|
157
|
+
alive: () => {
|
|
158
|
+
try {
|
|
159
|
+
void ctx?.sessionManager;
|
|
160
|
+
return true;
|
|
161
|
+
} catch {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
const index = key === undefined ? -1 : pendingRegistrySessions.findIndex((candidate) => candidate.key === key);
|
|
167
|
+
if (index >= 0) pendingRegistrySessions[index] = entry;
|
|
168
|
+
else pendingRegistrySessions.push(entry);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function releaseCapturedRegistrySession(key: string): void {
|
|
172
|
+
const index = pendingRegistrySessions.findIndex((entry) => entry.key === key);
|
|
173
|
+
if (index >= 0) pendingRegistrySessions.splice(index, 1);
|
|
174
|
+
releaseRegistrySession(key);
|
|
175
|
+
}
|
|
176
|
+
|
|
148
177
|
async function withRegistrySession<T>(ctx: AnchorSessionContext | undefined, fn: () => Promise<T> | T): Promise<T> {
|
|
149
|
-
|
|
178
|
+
captureRegistrySession(ctx);
|
|
150
179
|
return withAnchorSession(ctx, fn);
|
|
151
180
|
}
|
|
152
181
|
|
|
@@ -197,19 +226,38 @@ export default function (pi: ExtensionAPI): void {
|
|
|
197
226
|
}
|
|
198
227
|
}));
|
|
199
228
|
|
|
200
|
-
pi.on("session_shutdown", async () => {
|
|
229
|
+
pi.on("session_shutdown", async (_event, ctx) => {
|
|
201
230
|
try {
|
|
202
|
-
|
|
203
|
-
|
|
231
|
+
let key: string | undefined;
|
|
232
|
+
let readable = true;
|
|
233
|
+
try {
|
|
234
|
+
key = sessionKeyFor(ctx);
|
|
235
|
+
} catch {
|
|
236
|
+
readable = false;
|
|
237
|
+
}
|
|
238
|
+
if (readable && key !== undefined) {
|
|
239
|
+
releaseCapturedRegistrySession(key);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (readable) {
|
|
243
|
+
const dead = pendingRegistrySessions.findIndex((entry) => entry.key === undefined && !entry.alive());
|
|
244
|
+
const fallback = pendingRegistrySessions.findIndex((entry) => entry.key === undefined);
|
|
245
|
+
const index = dead >= 0 ? dead : fallback;
|
|
246
|
+
if (index >= 0) pendingRegistrySessions.splice(index, 1);
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const dead = pendingRegistrySessions.findIndex((entry) => !entry.alive());
|
|
250
|
+
const entry = dead >= 0 ? pendingRegistrySessions.splice(dead, 1)[0] : pendingRegistrySessions.shift();
|
|
251
|
+
if (entry?.key !== undefined) releaseRegistrySession(entry.key);
|
|
204
252
|
} catch (error) {
|
|
205
253
|
console.error("Failed to release anchor registry session:", error);
|
|
206
254
|
}
|
|
207
255
|
});
|
|
208
|
-
pi.on("model_select", async (event, ctx) =>
|
|
256
|
+
pi.on("model_select", async (event, ctx) => withRegistrySession(ctx, async () => {
|
|
209
257
|
await syncModelGate(event.model, ctx.cwd);
|
|
210
258
|
}));
|
|
211
259
|
|
|
212
|
-
pi.on("before_agent_start", async (_event, ctx) =>
|
|
260
|
+
pi.on("before_agent_start", async (_event, ctx) => withRegistrySession(ctx, async () => {
|
|
213
261
|
await syncModelGate(ctx.model, ctx.cwd);
|
|
214
262
|
if (autoReadAll === "off" || autoReadAllInjected) return;
|
|
215
263
|
if (modelDisabled(ctx.model, disableOnModels)) return;
|
|
@@ -287,12 +335,12 @@ export default function (pi: ExtensionAPI): void {
|
|
|
287
335
|
|
|
288
336
|
pi.registerCommand("clear-anchors", {
|
|
289
337
|
description: "Clear the session's anchor claims (path-free resolution state); anchors are re-claimed on the next read",
|
|
290
|
-
handler: async (_args, ctx) =>
|
|
338
|
+
handler: async (_args, ctx) => withRegistrySession(ctx, async () => {
|
|
291
339
|
clearRegistry();
|
|
292
340
|
ctx.ui.notify(`Anchor claims cleared for this session`, "info");
|
|
293
341
|
}),
|
|
294
342
|
});
|
|
295
|
-
pi.on("message_end", async (event, ctx) =>
|
|
343
|
+
pi.on("message_end", async (event, ctx) => withRegistrySession(ctx, async () => {
|
|
296
344
|
try {
|
|
297
345
|
await planAssistantMessage(event.message, ctx.cwd);
|
|
298
346
|
} catch (error) {
|
|
@@ -307,7 +355,7 @@ export default function (pi: ExtensionAPI): void {
|
|
|
307
355
|
console.error("Failed to finalize edit batch:", error);
|
|
308
356
|
}
|
|
309
357
|
});
|
|
310
|
-
pi.on("tool_result", async (event, ctx) =>
|
|
358
|
+
pi.on("tool_result", async (event, ctx) => withRegistrySession(ctx, async () => {
|
|
311
359
|
if (event.isError) return;
|
|
312
360
|
const gated = modelDisabled(ctx.model, disableOnModels);
|
|
313
361
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-hashline-edit-pro",
|
|
3
|
-
"version": "6.4.
|
|
3
|
+
"version": "6.4.5",
|
|
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
|
@@ -3,6 +3,7 @@ import { lstat, open, readdir } from "node:fs/promises";
|
|
|
3
3
|
import { join, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { formatSize } from "@earendil-works/pi-coding-agent";
|
|
5
5
|
import {
|
|
6
|
+
AUTO_READ_ALL_DIR_BUDGET_SHARE,
|
|
6
7
|
AUTO_READ_ALL_MAX_BUDGET_BYTES,
|
|
7
8
|
AUTO_READ_ALL_MAX_FILE_BYTES,
|
|
8
9
|
AUTO_READ_ALL_MAX_FILES,
|
|
@@ -108,6 +109,137 @@ function isExcludedByPattern(baseLower: string): boolean {
|
|
|
108
109
|
|| baseLower.startsWith("coreui-icons.")
|
|
109
110
|
|| baseLower === "coreui.css";
|
|
110
111
|
}
|
|
112
|
+
|
|
113
|
+
const MANIFEST_BASE_NAMES = new Set([
|
|
114
|
+
"package.json",
|
|
115
|
+
"tsconfig.json",
|
|
116
|
+
"jsconfig.json",
|
|
117
|
+
"pyproject.toml",
|
|
118
|
+
"setup.py",
|
|
119
|
+
"setup.cfg",
|
|
120
|
+
"requirements.txt",
|
|
121
|
+
"pipfile",
|
|
122
|
+
"cargo.toml",
|
|
123
|
+
"go.mod",
|
|
124
|
+
"gemfile",
|
|
125
|
+
"rakefile",
|
|
126
|
+
"makefile",
|
|
127
|
+
"gnumakefile",
|
|
128
|
+
"cmakelists.txt",
|
|
129
|
+
"meson.build",
|
|
130
|
+
"build.gradle",
|
|
131
|
+
"build.gradle.kts",
|
|
132
|
+
"settings.gradle",
|
|
133
|
+
"settings.gradle.kts",
|
|
134
|
+
"pom.xml",
|
|
135
|
+
"mix.exs",
|
|
136
|
+
"composer.json",
|
|
137
|
+
"dockerfile",
|
|
138
|
+
"docker-compose.yml",
|
|
139
|
+
"docker-compose.yaml",
|
|
140
|
+
"compose.yml",
|
|
141
|
+
"compose.yaml",
|
|
142
|
+
"readme.md",
|
|
143
|
+
"readme.rst",
|
|
144
|
+
"readme.txt",
|
|
145
|
+
"readme",
|
|
146
|
+
]);
|
|
147
|
+
|
|
148
|
+
const SOURCE_TOP_LEVEL_NAMES = new Set([
|
|
149
|
+
"src",
|
|
150
|
+
"source",
|
|
151
|
+
"lib",
|
|
152
|
+
"libs",
|
|
153
|
+
"app",
|
|
154
|
+
"apps",
|
|
155
|
+
"packages",
|
|
156
|
+
"server",
|
|
157
|
+
"client",
|
|
158
|
+
"web",
|
|
159
|
+
"api",
|
|
160
|
+
"cmd",
|
|
161
|
+
"internal",
|
|
162
|
+
"pkg",
|
|
163
|
+
"core",
|
|
164
|
+
"modules",
|
|
165
|
+
"components",
|
|
166
|
+
"services",
|
|
167
|
+
"plugins",
|
|
168
|
+
"include",
|
|
169
|
+
]);
|
|
170
|
+
|
|
171
|
+
const NON_SOURCE_TOP_LEVEL_NAMES = new Set([
|
|
172
|
+
"test",
|
|
173
|
+
"tests",
|
|
174
|
+
"spec",
|
|
175
|
+
"specs",
|
|
176
|
+
"__tests__",
|
|
177
|
+
"__mocks__",
|
|
178
|
+
"docs",
|
|
179
|
+
"doc",
|
|
180
|
+
"documentation",
|
|
181
|
+
"examples",
|
|
182
|
+
"example",
|
|
183
|
+
"samples",
|
|
184
|
+
"sample",
|
|
185
|
+
"fixtures",
|
|
186
|
+
"benchmark",
|
|
187
|
+
"benchmarks",
|
|
188
|
+
"bench",
|
|
189
|
+
"e2e",
|
|
190
|
+
"scripts",
|
|
191
|
+
"assets",
|
|
192
|
+
"static",
|
|
193
|
+
"public",
|
|
194
|
+
"coverage",
|
|
195
|
+
"migrations",
|
|
196
|
+
"locales",
|
|
197
|
+
"i18n",
|
|
198
|
+
"styles",
|
|
199
|
+
]);
|
|
200
|
+
|
|
201
|
+
function topSegmentOf(path: string): string | undefined {
|
|
202
|
+
const index = path.indexOf("/");
|
|
203
|
+
return index < 0 ? undefined : path.slice(0, index);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function detectSourceTopLevel(files: readonly string[]): Set<string> {
|
|
207
|
+
const counts = new Map<string, number>();
|
|
208
|
+
for (const file of files) {
|
|
209
|
+
const top = topSegmentOf(file)?.toLowerCase();
|
|
210
|
+
if (top === undefined || NON_SOURCE_TOP_LEVEL_NAMES.has(top)) continue;
|
|
211
|
+
counts.set(top, (counts.get(top) ?? 0) + 1);
|
|
212
|
+
}
|
|
213
|
+
let dominant: string | undefined;
|
|
214
|
+
let dominantCount = 0;
|
|
215
|
+
for (const [name, count] of counts) {
|
|
216
|
+
if (count > dominantCount) {
|
|
217
|
+
dominant = name;
|
|
218
|
+
dominantCount = count;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const source = new Set(SOURCE_TOP_LEVEL_NAMES);
|
|
222
|
+
if (dominant !== undefined) source.add(dominant);
|
|
223
|
+
return source;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function priorityOf(file: string, changed: ReadonlySet<string>, sourceTopLevel: ReadonlySet<string>): number {
|
|
227
|
+
if (changed.has(file)) return 0;
|
|
228
|
+
if (MANIFEST_BASE_NAMES.has(baseNameOf(file).toLowerCase())) return 1;
|
|
229
|
+
const top = topSegmentOf(file)?.toLowerCase();
|
|
230
|
+
if (top !== undefined && sourceTopLevel.has(top)) return 2;
|
|
231
|
+
return 3;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function rankFiles(files: readonly string[], changed: ReadonlySet<string>, sourceTopLevel: ReadonlySet<string>): string[] {
|
|
235
|
+
return [...files].sort((a, b) => {
|
|
236
|
+
const byPriority = priorityOf(a, changed, sourceTopLevel) - priorityOf(b, changed, sourceTopLevel);
|
|
237
|
+
if (byPriority !== 0) return byPriority;
|
|
238
|
+
if (a === b) return 0;
|
|
239
|
+
return a < b ? -1 : 1;
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
111
243
|
export function normalizeAutoReadAllIgnoreList(entries: readonly string[] | undefined): string[] {
|
|
112
244
|
const seen = new Set<string>();
|
|
113
245
|
const out: string[] = [];
|
|
@@ -228,6 +360,36 @@ async function listFromGit(cwd: string): Promise<string[] | undefined> {
|
|
|
228
360
|
return result.stdout.split("\0").filter((entry) => entry.length > 0);
|
|
229
361
|
}
|
|
230
362
|
|
|
363
|
+
async function listChangedFiles(cwd: string): Promise<Set<string>> {
|
|
364
|
+
const changed = new Set<string>();
|
|
365
|
+
const prefixResult = await runCommand("git", ["rev-parse", "--show-prefix"], cwd);
|
|
366
|
+
const prefix = prefixResult.code === 0 ? toPosix(prefixResult.stdout.trim()) : "";
|
|
367
|
+
const result = await runCommand("git", ["status", "--porcelain", "-z", "-uall"], cwd);
|
|
368
|
+
if (result.code !== 0) return changed;
|
|
369
|
+
const fields = result.stdout.split("\0");
|
|
370
|
+
for (let index = 0; index < fields.length; index += 1) {
|
|
371
|
+
const field = fields[index];
|
|
372
|
+
if (field === undefined || field.length < 4) continue;
|
|
373
|
+
const status = field.slice(0, 2);
|
|
374
|
+
addChangedPath(changed, field.slice(3), prefix);
|
|
375
|
+
if (status.includes("R") || status.includes("C")) {
|
|
376
|
+
index += 1;
|
|
377
|
+
const source = fields[index];
|
|
378
|
+
if (source !== undefined && source.length > 0) addChangedPath(changed, source, prefix);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return changed;
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
function addChangedPath(changed: Set<string>, path: string, prefix: string): void {
|
|
385
|
+
const posix = toPosix(path);
|
|
386
|
+
if (prefix.length > 0 && posix.startsWith(prefix)) {
|
|
387
|
+
changed.add(posix.slice(prefix.length));
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
changed.add(posix);
|
|
391
|
+
}
|
|
392
|
+
|
|
231
393
|
async function listFromRg(cwd: string): Promise<string[] | undefined> {
|
|
232
394
|
let rgPath: string;
|
|
233
395
|
try {
|
|
@@ -331,16 +493,19 @@ export async function discoverAutoReadAllFiles(cwd: string, mode: AutoReadAllMod
|
|
|
331
493
|
const unique = [...new Set(candidates.map(toPosix))].sort();
|
|
332
494
|
const includable: string[] = [];
|
|
333
495
|
let skippedByName = 0;
|
|
496
|
+
let skippedBinary = 0;
|
|
497
|
+
let skippedLarge = 0;
|
|
498
|
+
let skippedOther = 0;
|
|
334
499
|
for (const file of unique) {
|
|
335
500
|
const baseLower = baseNameOf(file).toLowerCase();
|
|
336
501
|
if (EXCLUDED_NAME_SET.has(baseLower) || isExcludedBySegment(file) || isExcludedByPattern(baseLower) || isExcludedByCustomIgnore(file, customIgnore)) skippedByName += 1;
|
|
502
|
+
else if (IMAGE_EXTENSIONS.has(extensionOf(file))) skippedBinary += 1;
|
|
337
503
|
else includable.push(file);
|
|
338
504
|
}
|
|
339
|
-
const
|
|
505
|
+
const changed = source === "git" ? await listChangedFiles(cwd) : new Set<string>();
|
|
506
|
+
const sourceTopLevel = detectSourceTopLevel(includable);
|
|
507
|
+
const scanWindow = rankFiles(includable, changed, sourceTopLevel).slice(0, AUTO_READ_ALL_MAX_FILES * SCAN_LIMIT_MULTIPLIER);
|
|
340
508
|
const sized: string[] = [];
|
|
341
|
-
let skippedBinary = 0;
|
|
342
|
-
let skippedLarge = 0;
|
|
343
|
-
let skippedOther = 0;
|
|
344
509
|
|
|
345
510
|
await forEachLimit(scanWindow, SCAN_CONCURRENCY, async (file) => {
|
|
346
511
|
let stats;
|
|
@@ -358,10 +523,6 @@ export async function discoverAutoReadAllFiles(cwd: string, mode: AutoReadAllMod
|
|
|
358
523
|
skippedLarge += 1;
|
|
359
524
|
return;
|
|
360
525
|
}
|
|
361
|
-
if (IMAGE_EXTENSIONS.has(extensionOf(file))) {
|
|
362
|
-
skippedBinary += 1;
|
|
363
|
-
return;
|
|
364
|
-
}
|
|
365
526
|
sized.push(file);
|
|
366
527
|
});
|
|
367
528
|
|
|
@@ -372,8 +533,7 @@ export async function discoverAutoReadAllFiles(cwd: string, mode: AutoReadAllMod
|
|
|
372
533
|
else if (nul) skippedBinary += 1;
|
|
373
534
|
else textual.push(file);
|
|
374
535
|
});
|
|
375
|
-
textual.
|
|
376
|
-
const files = textual.slice(0, AUTO_READ_ALL_MAX_FILES);
|
|
536
|
+
const files = rankFiles(textual, changed, sourceTopLevel).slice(0, AUTO_READ_ALL_MAX_FILES);
|
|
377
537
|
|
|
378
538
|
return { files, source, discovered: unique.length, skippedBinary, skippedLarge, skippedOther, skippedByName };
|
|
379
539
|
}
|
|
@@ -430,34 +590,52 @@ export async function buildAutoReadAllInjection(cwd: string, budgetBytes: number
|
|
|
430
590
|
const outline = isOutlineAutoReadAll(mode);
|
|
431
591
|
const sections: AutoReadAllSection[] = [];
|
|
432
592
|
const omitted: string[] = [];
|
|
593
|
+
const backlog: string[] = [];
|
|
594
|
+
const usedByDir = new Map<string, number>();
|
|
595
|
+
const dirBudget = Math.max(1, Math.floor(budgetBytes * AUTO_READ_ALL_DIR_BUDGET_SHARE));
|
|
433
596
|
let bytes = 0;
|
|
434
597
|
let completeFiles = 0;
|
|
435
|
-
|
|
598
|
+
|
|
599
|
+
const take = async (file: string, respectDirBudget: boolean): Promise<void> => {
|
|
436
600
|
if (sections.length > 0 && bytes >= budgetBytes) {
|
|
437
601
|
omitted.push(file);
|
|
438
|
-
|
|
602
|
+
return;
|
|
439
603
|
}
|
|
440
|
-
|
|
604
|
+
const group = topSegmentOf(file) ?? ".";
|
|
605
|
+
const used = usedByDir.get(group) ?? 0;
|
|
606
|
+
if (sections.length > 0) {
|
|
441
607
|
const size = await candidateFileBytes(cwd, file);
|
|
442
608
|
if (size !== undefined && bytes + size > budgetBytes) {
|
|
443
609
|
omitted.push(file);
|
|
444
|
-
|
|
610
|
+
return;
|
|
611
|
+
}
|
|
612
|
+
if (respectDirBudget && size !== undefined && used + size > dirBudget) {
|
|
613
|
+
backlog.push(file);
|
|
614
|
+
return;
|
|
445
615
|
}
|
|
446
616
|
}
|
|
447
617
|
const section = await renderFile(file, cwd, outline);
|
|
448
618
|
if (section === undefined) {
|
|
449
619
|
omitted.push(file);
|
|
450
|
-
|
|
620
|
+
return;
|
|
451
621
|
}
|
|
452
622
|
const sectionBytes = Buffer.byteLength(section.text, "utf-8") + 1;
|
|
453
623
|
if (sections.length > 0 && bytes + sectionBytes > budgetBytes) {
|
|
454
624
|
omitted.push(file);
|
|
455
|
-
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
if (sections.length > 0 && respectDirBudget && used + sectionBytes > dirBudget) {
|
|
628
|
+
backlog.push(file);
|
|
629
|
+
return;
|
|
456
630
|
}
|
|
457
631
|
sections.push(section);
|
|
458
632
|
bytes += sectionBytes;
|
|
633
|
+
usedByDir.set(group, used + sectionBytes);
|
|
459
634
|
if (section.complete) completeFiles += 1;
|
|
460
|
-
}
|
|
635
|
+
};
|
|
636
|
+
|
|
637
|
+
for (const file of discovery.files) await take(file, true);
|
|
638
|
+
for (const file of backlog) await take(file, false);
|
|
461
639
|
if (sections.length === 0) return undefined;
|
|
462
640
|
const sectionTexts = sections.map((section) => section.text);
|
|
463
641
|
const coverage = `[coverage: ${completeFiles} complete]`;
|
package/src/constants.ts
CHANGED
|
@@ -36,3 +36,4 @@ export const AUTO_READ_ALL_MAX_FILES = 500;
|
|
|
36
36
|
export const AUTO_READ_ALL_MAX_FILE_BYTES = 200_000;
|
|
37
37
|
export const AUTO_READ_ALL_MIN_BUDGET_BYTES = 200_000;
|
|
38
38
|
export const AUTO_READ_ALL_MAX_BUDGET_BYTES = 2_000_000;
|
|
39
|
+
export const AUTO_READ_ALL_DIR_BUDGET_SHARE = 0.4;
|