diff --git a/docs/cli/configuration.md b/docs/cli/configuration.md index e3c379b0..b4dd2e95 100644 --- a/docs/cli/configuration.md +++ b/docs/cli/configuration.md @@ -56,13 +56,15 @@ In addition to a project settings file, a project's `.gemini` directory can cont - **`fileFiltering`** (object): - **Description:** Controls git-aware file filtering behavior for @ commands and file discovery tools. - - **Default:** `"respectGitIgnore": true` + - **Default:** `"respectGitIgnore": true, "enableRecursiveFileSearch": true` - **Properties:** - **`respectGitIgnore`** (boolean): Whether to respect .gitignore patterns when discovering files. When set to `true`, git-ignored files (like `node_modules/`, `dist/`, `.env`) are automatically excluded from @ commands and file listing operations. + - **`enableRecursiveFileSearch`** (boolean): Whether to enable searching recursively for filenames under the current tree when completing @ prefixes in the prompt. - **Example:** ```json "fileFiltering": { - "respectGitIgnore": true + "respectGitIgnore": true, + "enableRecursiveFileSearch": false } ``` diff --git a/packages/cli/src/config/config.integration.test.ts b/packages/cli/src/config/config.integration.test.ts index de329384..868538ab 100644 --- a/packages/cli/src/config/config.integration.test.ts +++ b/packages/cli/src/config/config.integration.test.ts @@ -75,7 +75,9 @@ describe('Configuration Integration Tests', () => { sandbox: false, targetDir: tempDir, debugMode: false, - fileFilteringRespectGitIgnore: false, + fileFiltering: { + respectGitIgnore: false, + }, }; const config = new Config(configParams); @@ -109,7 +111,9 @@ describe('Configuration Integration Tests', () => { sandbox: false, targetDir: tempDir, debugMode: false, - fileFilteringRespectGitIgnore: false, + fileFiltering: { + respectGitIgnore: false, + }, }; const config = new Config(configParams); @@ -178,7 +182,9 @@ describe('Configuration Integration Tests', () => { sandbox: false, targetDir: tempDir, debugMode: false, - fileFilteringRespectGitIgnore: false, // CI might need to see all files + fileFiltering: { + respectGitIgnore: false, + }, // CI might need to see all files }; const config = new Config(configParams); diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index afc63b78..26894bc9 100644 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -228,7 +228,11 @@ export async function loadCliConfig( logPrompts: argv.telemetryLogPrompts ?? settings.telemetry?.logPrompts, }, // Git-aware file filtering settings - fileFilteringRespectGitIgnore: settings.fileFiltering?.respectGitIgnore, + fileFiltering: { + respectGitIgnore: settings.fileFiltering?.respectGitIgnore, + enableRecursiveFileSearch: + settings.fileFiltering?.enableRecursiveFileSearch, + }, checkpointing: argv.checkpointing || settings.checkpointing?.enabled, proxy: process.env.HTTPS_PROXY || diff --git a/packages/cli/src/config/settings.ts b/packages/cli/src/config/settings.ts index b63f5bb6..b149216a 100644 --- a/packages/cli/src/config/settings.ts +++ b/packages/cli/src/config/settings.ts @@ -56,6 +56,7 @@ export interface Settings { // Git-aware file filtering settings fileFiltering?: { respectGitIgnore?: boolean; + enableRecursiveFileSearch?: boolean; }; // UI setting. Does not display the ANSI-controlled terminal title. diff --git a/packages/cli/src/ui/hooks/useCompletion.integration.test.ts b/packages/cli/src/ui/hooks/useCompletion.integration.test.ts index 40af5d4f..237b6aae 100644 --- a/packages/cli/src/ui/hooks/useCompletion.integration.test.ts +++ b/packages/cli/src/ui/hooks/useCompletion.integration.test.ts @@ -47,6 +47,7 @@ describe('useCompletion git-aware filtering integration', () => { mockConfig = { getFileFilteringRespectGitIgnore: vi.fn(() => true), getFileService: vi.fn().mockReturnValue(mockFileDiscoveryService), + getEnableRecursiveFileSearch: vi.fn(() => true), }; vi.mocked(FileDiscoveryService).mockImplementation( @@ -170,6 +171,35 @@ describe('useCompletion git-aware filtering integration', () => { ); }); + it('should not perform recursive search when disabled in config', async () => { + const globResults = [`${testCwd}/data`, `${testCwd}/dist`]; + vi.mocked(glob).mockResolvedValue(globResults); + + // Disable recursive search in the mock config + const mockConfigNoRecursive = { + ...mockConfig, + getEnableRecursiveFileSearch: vi.fn(() => false), + }; + + vi.mocked(fs.readdir).mockResolvedValue([ + { name: 'data', isDirectory: () => true }, + { name: 'dist', isDirectory: () => true }, + ] as Array<{ name: string; isDirectory: () => boolean }>); + + renderHook(() => + useCompletion('@d', testCwd, true, slashCommands, mockConfigNoRecursive), + ); + + await act(async () => { + await new Promise((resolve) => setTimeout(resolve, 150)); + }); + + // `glob` should not be called because recursive search is disabled + expect(glob).not.toHaveBeenCalled(); + // `fs.readdir` should be called for the top-level directory instead + expect(fs.readdir).toHaveBeenCalledWith(testCwd, { withFileTypes: true }); + }); + it('should work without config (fallback behavior)', async () => { vi.mocked(fs.readdir).mockResolvedValue([ { name: 'src', isDirectory: () => true }, diff --git a/packages/cli/src/ui/hooks/useCompletion.ts b/packages/cli/src/ui/hooks/useCompletion.ts index 7cbe8a7e..eef54fc8 100644 --- a/packages/cli/src/ui/hooks/useCompletion.ts +++ b/packages/cli/src/ui/hooks/useCompletion.ts @@ -322,10 +322,16 @@ export function useCompletion( let fetchedSuggestions: Suggestion[] = []; const fileDiscoveryService = config ? config.getFileService() : null; + const enableRecursiveSearch = + config?.getEnableRecursiveFileSearch() ?? true; try { // If there's no slash, or it's the root, do a recursive search from cwd - if (partialPath.indexOf('/') === -1 && prefix) { + if ( + partialPath.indexOf('/') === -1 && + prefix && + enableRecursiveSearch + ) { if (fileDiscoveryService) { fetchedSuggestions = await findFilesWithGlob( prefix, diff --git a/packages/core/src/config/config.test.ts b/packages/core/src/config/config.test.ts index 8a9f038c..9b576b96 100644 --- a/packages/core/src/config/config.test.ts +++ b/packages/core/src/config/config.test.ts @@ -163,7 +163,9 @@ describe('Server Config (config.ts)', () => { it('should set custom file filtering settings when provided', () => { const paramsWithFileFiltering: ConfigParameters = { ...baseParams, - fileFilteringRespectGitIgnore: false, + fileFiltering: { + respectGitIgnore: false, + }, }; const config = new Config(paramsWithFileFiltering); expect(config.getFileFilteringRespectGitIgnore()).toBe(false); diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 514fc717..be21ac8c 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -104,7 +104,10 @@ export interface ConfigParameters { contextFileName?: string | string[]; accessibility?: AccessibilitySettings; telemetry?: TelemetrySettings; - fileFilteringRespectGitIgnore?: boolean; + fileFiltering?: { + respectGitIgnore?: boolean; + enableRecursiveFileSearch?: boolean; + }; checkpointing?: boolean; proxy?: string; cwd: string; @@ -136,7 +139,10 @@ export class Config { private readonly accessibility: AccessibilitySettings; private readonly telemetrySettings: TelemetrySettings; private geminiClient!: GeminiClient; - private readonly fileFilteringRespectGitIgnore: boolean; + private readonly fileFiltering: { + respectGitIgnore: boolean; + enableRecursiveFileSearch: boolean; + }; private fileDiscoveryService: FileDiscoveryService | null = null; private gitService: GitService | undefined = undefined; private readonly checkpointing: boolean; @@ -172,8 +178,11 @@ export class Config { logPrompts: params.telemetry?.logPrompts ?? true, }; - this.fileFilteringRespectGitIgnore = - params.fileFilteringRespectGitIgnore ?? true; + this.fileFiltering = { + respectGitIgnore: params.fileFiltering?.respectGitIgnore ?? true, + enableRecursiveFileSearch: + params.fileFiltering?.enableRecursiveFileSearch ?? true, + }; this.checkpointing = params.checkpointing ?? false; this.proxy = params.proxy; this.cwd = params.cwd ?? process.cwd(); @@ -330,8 +339,12 @@ export class Config { return getProjectTempDir(this.getProjectRoot()); } + getEnableRecursiveFileSearch(): boolean { + return this.fileFiltering.enableRecursiveFileSearch; + } + getFileFilteringRespectGitIgnore(): boolean { - return this.fileFilteringRespectGitIgnore; + return this.fileFiltering.respectGitIgnore; } getCheckpointingEnabled(): boolean {