@plannotator/pi-extension 0.23.0 → 0.23.1

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.
@@ -13,6 +13,7 @@
13
13
  import { homedir } from "os";
14
14
  import { isAbsolute, join, resolve, win32 } from "path";
15
15
  import { existsSync, readdirSync, type Dirent } from "fs";
16
+ import { readdir } from "node:fs/promises";
16
17
 
17
18
  const MARKDOWN_PATH_REGEX = /\.(mdx?|txt)$/i;
18
19
 
@@ -46,6 +47,23 @@ const CODE_IGNORED_DIRS = [
46
47
  ".pytest_cache/",
47
48
  ];
48
49
 
50
+ const DEFAULT_FILE_BROWSER_MAX_FILES = 5_000;
51
+
52
+ /**
53
+ * Return the shared file-traversal budget used by resolution, cache warming,
54
+ * and file-browser discovery. Invalid or non-positive overrides fall back to
55
+ * the default.
56
+ */
57
+ export function getFileBrowserMaxFiles(): number {
58
+ const value = Number.parseInt(
59
+ process.env.PLANNOTATOR_FILE_BROWSER_MAX_FILES ?? "",
60
+ 10,
61
+ );
62
+ return Number.isFinite(value) && value > 0
63
+ ? value
64
+ : DEFAULT_FILE_BROWSER_MAX_FILES;
65
+ }
66
+
49
67
  export type ResolveResult =
50
68
  | { kind: "found"; path: string }
51
69
  | { kind: "not_found"; input: string }
@@ -195,6 +213,11 @@ function fileExists(filePath: string): boolean {
195
213
  }
196
214
  }
197
215
 
216
+ type FileWalkState = {
217
+ visitedFiles: number;
218
+ readonly limit: number;
219
+ };
220
+
198
221
  /** Recursively walk a directory collecting files matching `fileMatcher`, skipping ignored dirs. */
199
222
  function walkFiles(
200
223
  dir: string,
@@ -202,28 +225,48 @@ function walkFiles(
202
225
  results: string[],
203
226
  ignoredDirs: string[],
204
227
  fileMatcher: (name: string) => boolean,
228
+ state: FileWalkState,
205
229
  ): void {
230
+ if (state.visitedFiles >= state.limit) return;
206
231
  const entries = readdirSync(dir, { withFileTypes: true }) as Dirent[];
207
232
  for (const entry of entries) {
233
+ if (state.visitedFiles >= state.limit) return;
208
234
  if (entry.isDirectory()) {
209
235
  if (ignoredDirs.some((d) => d === entry.name + "/")) continue;
210
236
  try {
211
- walkFiles(join(dir, entry.name), root, results, ignoredDirs, fileMatcher);
237
+ walkFiles(
238
+ join(dir, entry.name),
239
+ root,
240
+ results,
241
+ ignoredDirs,
242
+ fileMatcher,
243
+ state,
244
+ );
212
245
  } catch {
213
246
  /* skip dirs we can't read */
214
247
  }
215
- } else if (entry.isFile() && fileMatcher(entry.name)) {
216
- const relative = join(dir, entry.name)
217
- .slice(root.length + 1)
218
- .replace(/\\/g, "/");
219
- results.push(relative);
248
+ } else if (entry.isFile()) {
249
+ state.visitedFiles += 1;
250
+ if (fileMatcher(entry.name)) {
251
+ const relative = join(dir, entry.name)
252
+ .slice(root.length + 1)
253
+ .replace(/\\/g, "/");
254
+ results.push(relative);
255
+ }
220
256
  }
221
257
  }
222
258
  }
223
259
 
224
260
  function walkMarkdownFiles(dir: string, root: string, results: string[], ignoredDirs: string[]): void {
225
261
  try {
226
- walkFiles(dir, root, results, ignoredDirs, (name) => /\.(mdx?|txt)$/i.test(name));
262
+ walkFiles(
263
+ dir,
264
+ root,
265
+ results,
266
+ ignoredDirs,
267
+ (name) => /\.(mdx?|txt)$/i.test(name),
268
+ { visitedFiles: 0, limit: getFileBrowserMaxFiles() },
269
+ );
227
270
  } catch {
228
271
  /* fail soft for markdown — preserves existing behavior */
229
272
  }
@@ -241,18 +284,46 @@ function fileListCacheKey(projectRoot: string, kind: string): string {
241
284
  return `${projectRoot}|${kind}`;
242
285
  }
243
286
 
244
- function startCodeWalk(projectRoot: string): Promise<string[] | null> {
245
- return Promise.resolve().then(() => {
246
- try {
247
- const results: string[] = [];
248
- walkFiles(projectRoot, projectRoot, results, CODE_IGNORED_DIRS, (name) =>
249
- CODE_FILE_BASENAME_REGEX.test(name),
250
- );
251
- return results;
252
- } catch {
253
- return null;
287
+ async function walkCodeFiles(
288
+ dir: string,
289
+ root: string,
290
+ results: string[],
291
+ state: FileWalkState,
292
+ ): Promise<void> {
293
+ if (state.visitedFiles >= state.limit) return;
294
+ const entries = await readdir(dir, { withFileTypes: true });
295
+ for (const entry of entries) {
296
+ if (state.visitedFiles >= state.limit) return;
297
+ if (entry.isDirectory()) {
298
+ if (CODE_IGNORED_DIRS.some((d) => d === entry.name + "/")) continue;
299
+ try {
300
+ await walkCodeFiles(join(dir, entry.name), root, results, state);
301
+ } catch {
302
+ /* skip dirs we can't read */
303
+ }
304
+ } else if (entry.isFile()) {
305
+ state.visitedFiles += 1;
306
+ if (CODE_FILE_BASENAME_REGEX.test(entry.name)) {
307
+ const relative = join(dir, entry.name)
308
+ .slice(root.length + 1)
309
+ .replace(/\\/g, "/");
310
+ results.push(relative);
311
+ }
254
312
  }
255
- });
313
+ }
314
+ }
315
+
316
+ async function startCodeWalk(projectRoot: string): Promise<string[] | null> {
317
+ try {
318
+ const results: string[] = [];
319
+ await walkCodeFiles(projectRoot, projectRoot, results, {
320
+ visitedFiles: 0,
321
+ limit: getFileBrowserMaxFiles(),
322
+ });
323
+ return results;
324
+ } catch {
325
+ return null;
326
+ }
256
327
  }
257
328
 
258
329
  /**
@@ -489,7 +560,13 @@ export function hasMarkdownFiles(
489
560
  excludedDirs: string[] = IGNORED_DIRS,
490
561
  extensions: RegExp = /\.mdx?$/i,
491
562
  ): boolean {
563
+ const state: FileWalkState = {
564
+ visitedFiles: 0,
565
+ limit: getFileBrowserMaxFiles(),
566
+ };
567
+
492
568
  function walk(dir: string): boolean {
569
+ if (state.visitedFiles >= state.limit) return false;
493
570
  let entries;
494
571
  try {
495
572
  entries = readdirSync(dir, { withFileTypes: true });
@@ -497,11 +574,13 @@ export function hasMarkdownFiles(
497
574
  return false;
498
575
  }
499
576
  for (const entry of entries) {
577
+ if (state.visitedFiles >= state.limit) return false;
500
578
  if (entry.isDirectory()) {
501
579
  if (excludedDirs.some((d) => d === entry.name + "/")) continue;
502
580
  if (walk(join(dir, entry.name))) return true;
503
- } else if (entry.isFile() && extensions.test(entry.name)) {
504
- return true;
581
+ } else if (entry.isFile()) {
582
+ state.visitedFiles += 1;
583
+ if (extensions.test(entry.name)) return true;
505
584
  }
506
585
  }
507
586
  return false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plannotator/pi-extension",
3
- "version": "0.23.0",
3
+ "version": "0.23.1",
4
4
  "type": "module",
5
5
  "description": "Plannotator Pi extension - interactive plan review with annotations, annotate agent messages, and review code/PRs",
6
6
  "author": "backnotprop",