From 2a4945ad0b9e75eebb88a4b8a563543beabfdfba Mon Sep 17 00:00:00 2001 From: Yurui Zhou Date: Tue, 28 Apr 2026 20:43:38 +0800 Subject: [PATCH] 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) --- .../app/src/hooks/use-agent-autocomplete.ts | 19 ++++++++++-- .../src/utils/directory-suggestions.test.ts | 30 +++++++++++++++++++ .../server/src/utils/directory-suggestions.ts | 11 ++++++- 3 files changed, 56 insertions(+), 4 deletions(-) diff --git a/packages/app/src/hooks/use-agent-autocomplete.ts b/packages/app/src/hooks/use-agent-autocomplete.ts index 4657f1ca0..5c27c966f 100644 --- a/packages/app/src/hooks/use-agent-autocomplete.ts +++ b/packages/app/src/hooks/use-agent-autocomplete.ts @@ -1,4 +1,4 @@ -import { useCallback, useMemo } from "react"; +import { useCallback, useEffect, useMemo, useState } from "react"; import { keepPreviousData, useQuery } from "@tanstack/react-query"; import type { AutocompleteOption } from "@/components/ui/autocomplete"; import { useAgentCommandsQuery, type DraftCommandConfig } from "./use-agent-commands-query"; @@ -185,6 +185,12 @@ export function useAgentAutocomplete(input: UseAgentAutocompleteInput): AgentAut ); const showFileAutocomplete = activeFileMention !== null; const fileFilterQuery = activeFileMention?.query ?? ""; + const [debouncedFileFilterQuery, setDebouncedFileFilterQuery] = useState(fileFilterQuery); + + useEffect(() => { + const timer = setTimeout(() => setDebouncedFileFilterQuery(fileFilterQuery), 180); + return () => clearTimeout(timer); + }, [fileFilterQuery]); const normalizedDraftConfig = useMemo( () => normalizeDraftCommandConfig(draftConfig), @@ -229,14 +235,21 @@ export function useAgentAutocomplete(input: UseAgentAutocompleteInput): AgentAut }); const fileSuggestionsQuery = useQuery({ - queryKey: ["directorySuggestions", serverId, autocompleteCwd, fileFilterQuery, true, true], + queryKey: [ + "directorySuggestions", + serverId, + autocompleteCwd, + debouncedFileFilterQuery, + true, + true, + ], queryFn: async (): Promise => { if (!client) { throw new Error("Daemon client unavailable"); } const response = await client.getDirectorySuggestions({ cwd: autocompleteCwd, - query: fileFilterQuery, + query: debouncedFileFilterQuery, limit: 50, includeFiles: true, includeDirectories: true, diff --git a/packages/server/src/utils/directory-suggestions.test.ts b/packages/server/src/utils/directory-suggestions.test.ts index 9eb01e864..88c05a388 100644 --- a/packages/server/src/utils/directory-suggestions.test.ts +++ b/packages/server/src/utils/directory-suggestions.test.ts @@ -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); + } + }); }); diff --git a/packages/server/src/utils/directory-suggestions.ts b/packages/server/src/utils/directory-suggestions.ts index 17c8d3aae..08b180fa3 100644 --- a/packages/server/src/utils/directory-suggestions.ts +++ b/packages/server/src/utils/directory-suggestions.ts @@ -73,7 +73,16 @@ const directoryListCache = new Map(); const workspaceEntryListCache = new Map(); 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,