fix: keep @ file mention responsive on very large projects (#600)

Typing @ in the chat input to reference a file froze the app on huge
workspaces. Two compounding causes:

1. The autocomplete hook fed fileFilterQuery straight into the React
   Query key, so every keystroke fired a new directory_suggestions
   request and a fresh BFS scan on the daemon. Added a 180 ms debounce
   that mirrors the existing pattern in new-workspace-screen so rapid
   typing coalesces into one request.

2. searchWorkspaceEntries only ignored node_modules, so dist, build,
   target, out, coverage, vendor and __pycache__ each consumed the
   5,000-entry scan budget before any real source file was reached.
   Extended WORKSPACE_IGNORED_DIRECTORY_NAMES with those names and
   added a regression test that proves a deep source file is still
   reachable when those directories are full of bait entries.

Refs #599

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yurui Zhou
2026-04-28 20:43:38 +08:00
committed by GitHub
co-authored by Claude Opus 4.7
parent 3ba4d09294
commit 2a4945ad0b
3 changed files with 56 additions and 4 deletions
@@ -265,4 +265,34 @@ describe("searchWorkspaceEntries", () => {
});
expect(results.some((entry) => entry.path.startsWith("node_modules/"))).toBe(false);
});
it("ignores common build/cache directories so large generated trees do not exhaust scan budget", async () => {
mkdirSync(path.join(workspaceDir, "packages", "app", "src"), { recursive: true });
writeFileSync(path.join(workspaceDir, "packages", "app", "src", "needle.ts"), "");
const heavyDirs = ["dist", "build", "target", "out", "coverage", "vendor", "__pycache__"];
for (const heavyDir of heavyDirs) {
for (let index = 0; index < 30; index += 1) {
mkdirSync(path.join(workspaceDir, heavyDir, `bundle-${index}`), { recursive: true });
writeFileSync(path.join(workspaceDir, heavyDir, `bundle-${index}`, "needle.ts"), "");
}
}
const results = await searchWorkspaceEntries({
cwd: workspaceDir,
query: "needle.ts",
limit: 20,
includeFiles: true,
includeDirectories: true,
maxEntriesScanned: 80,
});
expect(results).toContainEqual({
path: "packages/app/src/needle.ts",
kind: "file",
});
for (const heavyDir of heavyDirs) {
expect(results.some((entry) => entry.path.startsWith(`${heavyDir}/`))).toBe(false);
}
});
});
@@ -73,7 +73,16 @@ const directoryListCache = new Map<string, DirectoryListCacheEntry>();
const workspaceEntryListCache = new Map<string, WorkspaceEntryListCacheEntry>();
const NO_SEGMENT_INDEX = Number.MAX_SAFE_INTEGER;
const NO_MATCH_OFFSET = Number.MAX_SAFE_INTEGER;
const WORKSPACE_IGNORED_DIRECTORY_NAMES = new Set(["node_modules"]);
const WORKSPACE_IGNORED_DIRECTORY_NAMES = new Set([
"node_modules",
"dist",
"build",
"target",
"out",
"coverage",
"vendor",
"__pycache__",
]);
export async function searchHomeDirectories(
options: SearchHomeDirectoriesOptions,