diff options
Diffstat (limited to 'packages/cli/src')
17 files changed, 320 insertions, 133 deletions
diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 27e3ec09..1dd8519c 100644 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -61,6 +61,7 @@ export interface CliArgs { listExtensions: boolean | undefined; ideMode: boolean | undefined; proxy: string | undefined; + includeDirectories: string[] | undefined; } export async function parseArguments(): Promise<CliArgs> { @@ -199,6 +200,15 @@ export async function parseArguments(): Promise<CliArgs> { description: 'Proxy for gemini client, like schema://user:password@host:port', }) + .option('include-directories', { + type: 'array', + string: true, + description: + 'Additional directories to include in the workspace (comma-separated or multiple --include-directories)', + coerce: (dirs: string[]) => + // Handle comma-separated values + dirs.flatMap((dir) => dir.split(',').map((d) => d.trim())), + }) .version(await getCliVersion()) // This will enable the --version flag based on package.json .alias('v', 'version') .help() @@ -366,6 +376,7 @@ export async function loadCliConfig( embeddingModel: DEFAULT_GEMINI_EMBEDDING_MODEL, sandbox: sandboxConfig, targetDir: process.cwd(), + includeDirectories: argv.includeDirectories, debugMode, question: argv.promptInteractive || argv.prompt || '', fullContext: argv.allFiles || argv.all_files || false, diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx index a31c4b2f..b4b70b61 100644 --- a/packages/cli/src/gemini.tsx +++ b/packages/cli/src/gemini.tsx @@ -199,7 +199,7 @@ export async function main() { process.exit(1); } } - await start_sandbox(sandboxConfig, memoryArgs); + await start_sandbox(sandboxConfig, memoryArgs, config); process.exit(0); } else { // Not in a sandbox and not entering one, so relaunch with additional diff --git a/packages/cli/src/ui/App.test.tsx b/packages/cli/src/ui/App.test.tsx index fef4106a..13ddb77d 100644 --- a/packages/cli/src/ui/App.test.tsx +++ b/packages/cli/src/ui/App.test.tsx @@ -152,6 +152,9 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => { getSessionId: vi.fn(() => 'test-session-id'), getUserTier: vi.fn().mockResolvedValue(undefined), getIdeMode: vi.fn(() => false), + getWorkspaceContext: vi.fn(() => ({ + getDirectories: vi.fn(() => []), + })), }; }); @@ -292,6 +295,13 @@ describe('App UI', () => { // Ensure a theme is set so the theme dialog does not appear. mockSettings = createMockSettings({ workspace: { theme: 'Default' } }); + + // Ensure getWorkspaceContext is available if not added by the constructor + if (!mockConfig.getWorkspaceContext) { + mockConfig.getWorkspaceContext = vi.fn(() => ({ + getDirectories: vi.fn(() => ['/test/dir']), + })); + } vi.mocked(ideContext.getIdeContext).mockReturnValue(undefined); }); diff --git a/packages/cli/src/ui/commands/aboutCommand.test.ts b/packages/cli/src/ui/commands/aboutCommand.test.ts index 48dd6db3..43cd59ec 100644 --- a/packages/cli/src/ui/commands/aboutCommand.test.ts +++ b/packages/cli/src/ui/commands/aboutCommand.test.ts @@ -62,6 +62,7 @@ describe('aboutCommand', () => { }); it('should call addItem with all version info', async () => { + process.env.SANDBOX = ''; if (!aboutCommand.action) { throw new Error('The about command must have an action.'); } diff --git a/packages/cli/src/ui/components/InputPrompt.test.tsx b/packages/cli/src/ui/components/InputPrompt.test.tsx index 1d0b868f..e0d967da 100644 --- a/packages/cli/src/ui/components/InputPrompt.test.tsx +++ b/packages/cli/src/ui/components/InputPrompt.test.tsx @@ -172,6 +172,9 @@ describe('InputPrompt', () => { getProjectRoot: () => path.join('test', 'project'), getTargetDir: () => path.join('test', 'project', 'src'), getVimMode: () => false, + getWorkspaceContext: () => ({ + getDirectories: () => ['/test/project/src'], + }), } as unknown as Config, slashCommands: mockSlashCommands, commandContext: mockCommandContext, @@ -731,6 +734,7 @@ describe('InputPrompt', () => { // Verify useCompletion was called with correct signature expect(mockedUseCompletion).toHaveBeenCalledWith( mockBuffer, + ['/test/project/src'], path.join('test', 'project', 'src'), mockSlashCommands, mockCommandContext, @@ -756,6 +760,7 @@ describe('InputPrompt', () => { expect(mockedUseCompletion).toHaveBeenCalledWith( mockBuffer, + ['/test/project/src'], path.join('test', 'project', 'src'), mockSlashCommands, mockCommandContext, @@ -781,6 +786,7 @@ describe('InputPrompt', () => { expect(mockedUseCompletion).toHaveBeenCalledWith( mockBuffer, + ['/test/project/src'], path.join('test', 'project', 'src'), mockSlashCommands, mockCommandContext, @@ -806,6 +812,7 @@ describe('InputPrompt', () => { expect(mockedUseCompletion).toHaveBeenCalledWith( mockBuffer, + ['/test/project/src'], path.join('test', 'project', 'src'), mockSlashCommands, mockCommandContext, @@ -831,6 +838,7 @@ describe('InputPrompt', () => { expect(mockedUseCompletion).toHaveBeenCalledWith( mockBuffer, + ['/test/project/src'], path.join('test', 'project', 'src'), mockSlashCommands, mockCommandContext, @@ -857,6 +865,7 @@ describe('InputPrompt', () => { // Verify useCompletion was called with the buffer expect(mockedUseCompletion).toHaveBeenCalledWith( mockBuffer, + ['/test/project/src'], path.join('test', 'project', 'src'), mockSlashCommands, mockCommandContext, @@ -882,6 +891,7 @@ describe('InputPrompt', () => { expect(mockedUseCompletion).toHaveBeenCalledWith( mockBuffer, + ['/test/project/src'], path.join('test', 'project', 'src'), mockSlashCommands, mockCommandContext, @@ -908,6 +918,7 @@ describe('InputPrompt', () => { expect(mockedUseCompletion).toHaveBeenCalledWith( mockBuffer, + ['/test/project/src'], path.join('test', 'project', 'src'), mockSlashCommands, mockCommandContext, @@ -934,6 +945,7 @@ describe('InputPrompt', () => { expect(mockedUseCompletion).toHaveBeenCalledWith( mockBuffer, + ['/test/project/src'], path.join('test', 'project', 'src'), mockSlashCommands, mockCommandContext, @@ -960,6 +972,7 @@ describe('InputPrompt', () => { expect(mockedUseCompletion).toHaveBeenCalledWith( mockBuffer, + ['/test/project/src'], path.join('test', 'project', 'src'), mockSlashCommands, mockCommandContext, @@ -986,6 +999,7 @@ describe('InputPrompt', () => { expect(mockedUseCompletion).toHaveBeenCalledWith( mockBuffer, + ['/test/project/src'], path.join('test', 'project', 'src'), mockSlashCommands, mockCommandContext, @@ -1014,6 +1028,7 @@ describe('InputPrompt', () => { expect(mockedUseCompletion).toHaveBeenCalledWith( mockBuffer, + ['/test/project/src'], path.join('test', 'project', 'src'), mockSlashCommands, mockCommandContext, @@ -1040,6 +1055,7 @@ describe('InputPrompt', () => { expect(mockedUseCompletion).toHaveBeenCalledWith( mockBuffer, + ['/test/project/src'], path.join('test', 'project', 'src'), mockSlashCommands, mockCommandContext, @@ -1068,6 +1084,7 @@ describe('InputPrompt', () => { expect(mockedUseCompletion).toHaveBeenCalledWith( mockBuffer, + ['/test/project/src'], path.join('test', 'project', 'src'), mockSlashCommands, mockCommandContext, diff --git a/packages/cli/src/ui/components/InputPrompt.tsx b/packages/cli/src/ui/components/InputPrompt.tsx index 17b7694e..5a7b6353 100644 --- a/packages/cli/src/ui/components/InputPrompt.tsx +++ b/packages/cli/src/ui/components/InputPrompt.tsx @@ -60,8 +60,19 @@ export const InputPrompt: React.FC<InputPromptProps> = ({ }) => { const [justNavigatedHistory, setJustNavigatedHistory] = useState(false); + const [dirs, setDirs] = useState<readonly string[]>( + config.getWorkspaceContext().getDirectories(), + ); + const dirsChanged = config.getWorkspaceContext().getDirectories(); + useEffect(() => { + if (dirs.length !== dirsChanged.length) { + setDirs(dirsChanged); + } + }, [dirs.length, dirsChanged]); + const completion = useCompletion( buffer, + dirs, config.getTargetDir(), slashCommands, commandContext, diff --git a/packages/cli/src/ui/hooks/atCommandProcessor.test.ts b/packages/cli/src/ui/hooks/atCommandProcessor.test.ts index de05667e..2b4c81a3 100644 --- a/packages/cli/src/ui/hooks/atCommandProcessor.test.ts +++ b/packages/cli/src/ui/hooks/atCommandProcessor.test.ts @@ -57,6 +57,10 @@ describe('handleAtCommand', () => { respectGeminiIgnore: true, }), getEnableRecursiveFileSearch: vi.fn(() => true), + getWorkspaceContext: () => ({ + isPathWithinWorkspace: () => true, + getDirectories: () => [testRootDir], + }), } as unknown as Config; const registry = new ToolRegistry(mockConfig); diff --git a/packages/cli/src/ui/hooks/atCommandProcessor.ts b/packages/cli/src/ui/hooks/atCommandProcessor.ts index 237d983f..7b9005fa 100644 --- a/packages/cli/src/ui/hooks/atCommandProcessor.ts +++ b/packages/cli/src/ui/hooks/atCommandProcessor.ts @@ -188,6 +188,14 @@ export async function handleAtCommand({ // Check if path should be ignored based on filtering options + const workspaceContext = config.getWorkspaceContext(); + if (!workspaceContext.isPathWithinWorkspace(pathName)) { + onDebugMessage( + `Path ${pathName} is not in the workspace and will be skipped.`, + ); + continue; + } + const gitIgnored = respectFileIgnore.respectGitIgnore && fileDiscovery.shouldIgnoreFile(pathName, { @@ -215,90 +223,88 @@ export async function handleAtCommand({ continue; } - let currentPathSpec = pathName; - let resolvedSuccessfully = false; - - try { - const absolutePath = path.resolve(config.getTargetDir(), pathName); - const stats = await fs.stat(absolutePath); - if (stats.isDirectory()) { - currentPathSpec = - pathName + (pathName.endsWith(path.sep) ? `**` : `/**`); - onDebugMessage( - `Path ${pathName} resolved to directory, using glob: ${currentPathSpec}`, - ); - } else { - onDebugMessage(`Path ${pathName} resolved to file: ${absolutePath}`); - } - resolvedSuccessfully = true; - } catch (error) { - if (isNodeError(error) && error.code === 'ENOENT') { - if (config.getEnableRecursiveFileSearch() && globTool) { + for (const dir of config.getWorkspaceContext().getDirectories()) { + let currentPathSpec = pathName; + let resolvedSuccessfully = false; + try { + const absolutePath = path.resolve(dir, pathName); + const stats = await fs.stat(absolutePath); + if (stats.isDirectory()) { + currentPathSpec = + pathName + (pathName.endsWith(path.sep) ? `**` : `/**`); onDebugMessage( - `Path ${pathName} not found directly, attempting glob search.`, + `Path ${pathName} resolved to directory, using glob: ${currentPathSpec}`, ); - try { - const globResult = await globTool.execute( - { - pattern: `**/*${pathName}*`, - path: config.getTargetDir(), - }, - signal, + } else { + onDebugMessage(`Path ${pathName} resolved to file: ${absolutePath}`); + } + resolvedSuccessfully = true; + } catch (error) { + if (isNodeError(error) && error.code === 'ENOENT') { + if (config.getEnableRecursiveFileSearch() && globTool) { + onDebugMessage( + `Path ${pathName} not found directly, attempting glob search.`, ); - if ( - globResult.llmContent && - typeof globResult.llmContent === 'string' && - !globResult.llmContent.startsWith('No files found') && - !globResult.llmContent.startsWith('Error:') - ) { - const lines = globResult.llmContent.split('\n'); - if (lines.length > 1 && lines[1]) { - const firstMatchAbsolute = lines[1].trim(); - currentPathSpec = path.relative( - config.getTargetDir(), - firstMatchAbsolute, - ); - onDebugMessage( - `Glob search for ${pathName} found ${firstMatchAbsolute}, using relative path: ${currentPathSpec}`, - ); - resolvedSuccessfully = true; + try { + const globResult = await globTool.execute( + { + pattern: `**/*${pathName}*`, + path: dir, + }, + signal, + ); + if ( + globResult.llmContent && + typeof globResult.llmContent === 'string' && + !globResult.llmContent.startsWith('No files found') && + !globResult.llmContent.startsWith('Error:') + ) { + const lines = globResult.llmContent.split('\n'); + if (lines.length > 1 && lines[1]) { + const firstMatchAbsolute = lines[1].trim(); + currentPathSpec = path.relative(dir, firstMatchAbsolute); + onDebugMessage( + `Glob search for ${pathName} found ${firstMatchAbsolute}, using relative path: ${currentPathSpec}`, + ); + resolvedSuccessfully = true; + } else { + onDebugMessage( + `Glob search for '**/*${pathName}*' did not return a usable path. Path ${pathName} will be skipped.`, + ); + } } else { onDebugMessage( - `Glob search for '**/*${pathName}*' did not return a usable path. Path ${pathName} will be skipped.`, + `Glob search for '**/*${pathName}*' found no files or an error. Path ${pathName} will be skipped.`, ); } - } else { + } catch (globError) { + console.error( + `Error during glob search for ${pathName}: ${getErrorMessage(globError)}`, + ); onDebugMessage( - `Glob search for '**/*${pathName}*' found no files or an error. Path ${pathName} will be skipped.`, + `Error during glob search for ${pathName}. Path ${pathName} will be skipped.`, ); } - } catch (globError) { - console.error( - `Error during glob search for ${pathName}: ${getErrorMessage(globError)}`, - ); + } else { onDebugMessage( - `Error during glob search for ${pathName}. Path ${pathName} will be skipped.`, + `Glob tool not found. Path ${pathName} will be skipped.`, ); } } else { + console.error( + `Error stating path ${pathName}: ${getErrorMessage(error)}`, + ); onDebugMessage( - `Glob tool not found. Path ${pathName} will be skipped.`, + `Error stating path ${pathName}. Path ${pathName} will be skipped.`, ); } - } else { - console.error( - `Error stating path ${pathName}: ${getErrorMessage(error)}`, - ); - onDebugMessage( - `Error stating path ${pathName}. Path ${pathName} will be skipped.`, - ); } - } - - if (resolvedSuccessfully) { - pathSpecsToRead.push(currentPathSpec); - atPathToResolvedSpecMap.set(originalAtPath, currentPathSpec); - contentLabelsForDisplay.push(pathName); + if (resolvedSuccessfully) { + pathSpecsToRead.push(currentPathSpec); + atPathToResolvedSpecMap.set(originalAtPath, currentPathSpec); + contentLabelsForDisplay.push(pathName); + break; + } } } diff --git a/packages/cli/src/ui/hooks/useCompletion.test.ts b/packages/cli/src/ui/hooks/useCompletion.test.ts index da6a7ab3..f876eea1 100644 --- a/packages/cli/src/ui/hooks/useCompletion.test.ts +++ b/packages/cli/src/ui/hooks/useCompletion.test.ts @@ -22,6 +22,7 @@ describe('useCompletion', () => { // A minimal mock is sufficient for these tests. const mockCommandContext = {} as CommandContext; + let testDirs: string[]; async function createEmptyDir(...pathSegments: string[]) { const fullPath = path.join(testRootDir, ...pathSegments); @@ -51,8 +52,12 @@ describe('useCompletion', () => { testRootDir = await fs.mkdtemp( path.join(os.tmpdir(), 'completion-unit-test-'), ); + testDirs = [testRootDir]; mockConfig = { getTargetDir: () => testRootDir, + getWorkspaceContext: () => ({ + getDirectories: () => testDirs, + }), getProjectRoot: () => testRootDir, getFileFilteringOptions: vi.fn(() => ({ respectGitIgnore: true, @@ -79,6 +84,7 @@ describe('useCompletion', () => { const { result } = renderHook(() => useCompletion( useTextBufferForTest(''), + testDirs, testRootDir, slashCommands, mockCommandContext, @@ -108,6 +114,7 @@ describe('useCompletion', () => { const textBuffer = useTextBufferForTest(text); return useCompletion( textBuffer, + testDirs, testRootDir, slashCommands, mockCommandContext, @@ -138,6 +145,7 @@ describe('useCompletion', () => { const { result } = renderHook(() => useCompletion( useTextBufferForTest('/help'), + testDirs, testRootDir, slashCommands, mockCommandContext, @@ -170,6 +178,7 @@ describe('useCompletion', () => { const { result } = renderHook(() => useCompletion( useTextBufferForTest(''), + testDirs, testRootDir, slashCommands, mockCommandContext, @@ -191,6 +200,7 @@ describe('useCompletion', () => { const { result } = renderHook(() => useCompletion( useTextBufferForTest(''), + testDirs, testRootDir, slashCommands, mockCommandContext, @@ -215,6 +225,7 @@ describe('useCompletion', () => { const { result } = renderHook(() => useCompletion( useTextBufferForTest('/h'), + testDirs, testRootDir, slashCommands, mockCommandContext, @@ -242,6 +253,7 @@ describe('useCompletion', () => { const { result } = renderHook(() => useCompletion( useTextBufferForTest('/h'), + testDirs, testRootDir, slashCommands, mockCommandContext, @@ -270,6 +282,7 @@ describe('useCompletion', () => { const { result } = renderHook(() => useCompletion( useTextBufferForTest('/'), + testDirs, testRootDir, slashCommands, mockCommandContext, @@ -315,6 +328,7 @@ describe('useCompletion', () => { const { result } = renderHook(() => useCompletion( useTextBufferForTest('/command'), + testDirs, testRootDir, largeMockCommands, mockCommandContext, @@ -372,6 +386,7 @@ describe('useCompletion', () => { const { result } = renderHook(() => useCompletion( useTextBufferForTest('/'), + testDirs, testRootDir, slashCommands, mockCommandContext, @@ -394,6 +409,7 @@ describe('useCompletion', () => { const { result } = renderHook(() => useCompletion( useTextBufferForTest('/mem'), + testDirs, testRootDir, slashCommands, mockCommandContext, @@ -417,6 +433,7 @@ describe('useCompletion', () => { const { result } = renderHook(() => useCompletion( useTextBufferForTest('/usag'), // part of the word "usage" + testDirs, testRootDir, slashCommands, mockCommandContext, @@ -443,6 +460,7 @@ describe('useCompletion', () => { const { result } = renderHook(() => useCompletion( useTextBufferForTest('/clear'), // No trailing space + testDirs, testRootDir, slashCommands, mockCommandContext, @@ -474,6 +492,7 @@ describe('useCompletion', () => { const { result } = renderHook(() => useCompletion( useTextBufferForTest(query), + testDirs, testRootDir, mockSlashCommands, mockCommandContext, @@ -494,6 +513,7 @@ describe('useCompletion', () => { const { result } = renderHook(() => useCompletion( useTextBufferForTest('/clear '), + testDirs, testRootDir, slashCommands, mockCommandContext, @@ -514,6 +534,7 @@ describe('useCompletion', () => { const { result } = renderHook(() => useCompletion( useTextBufferForTest('/unknown-command'), + testDirs, testRootDir, slashCommands, mockCommandContext, @@ -547,6 +568,7 @@ describe('useCompletion', () => { const { result } = renderHook(() => useCompletion( useTextBufferForTest('/memory'), // Note: no trailing space + testDirs, testRootDir, slashCommands, mockCommandContext, @@ -584,6 +606,7 @@ describe('useCompletion', () => { const { result } = renderHook(() => useCompletion( useTextBufferForTest('/memory'), + testDirs, testRootDir, slashCommands, mockCommandContext, @@ -619,6 +642,7 @@ describe('useCompletion', () => { const { result } = renderHook(() => useCompletion( useTextBufferForTest('/memory a'), + testDirs, testRootDir, slashCommands, mockCommandContext, @@ -650,6 +674,7 @@ describe('useCompletion', () => { const { result } = renderHook(() => useCompletion( useTextBufferForTest('/memory dothisnow'), + testDirs, testRootDir, slashCommands, mockCommandContext, @@ -692,6 +717,7 @@ describe('useCompletion', () => { const { result } = renderHook(() => useCompletion( useTextBufferForTest('/chat resume my-ch'), + testDirs, testRootDir, slashCommands, mockCommandContext, @@ -735,6 +761,7 @@ describe('useCompletion', () => { const { result } = renderHook(() => useCompletion( useTextBufferForTest('/chat resume '), + testDirs, testRootDir, slashCommands, mockCommandContext, @@ -769,6 +796,7 @@ describe('useCompletion', () => { const { result } = renderHook(() => useCompletion( useTextBufferForTest('/chat resume '), + testDirs, testRootDir, slashCommands, mockCommandContext, @@ -796,6 +824,7 @@ describe('useCompletion', () => { const { result } = renderHook(() => useCompletion( useTextBufferForTest('@s'), + testDirs, testRootDir, [], mockCommandContext, @@ -829,6 +858,7 @@ describe('useCompletion', () => { const { result } = renderHook(() => useCompletion( useTextBufferForTest('@src/comp'), + testDirs, testRootDir, [], mockCommandContext, @@ -854,6 +884,7 @@ describe('useCompletion', () => { const { result } = renderHook(() => useCompletion( useTextBufferForTest('@.'), + testDirs, testRootDir, [], mockCommandContext, @@ -885,6 +916,7 @@ describe('useCompletion', () => { const { result } = renderHook(() => useCompletion( useTextBufferForTest('@d'), + testDirs, testRootDir, [], mockCommandContext, @@ -910,6 +942,7 @@ describe('useCompletion', () => { const { result } = renderHook(() => useCompletion( useTextBufferForTest('@'), + testDirs, testRootDir, [], mockCommandContext, @@ -944,6 +977,7 @@ describe('useCompletion', () => { const { result } = renderHook(() => useCompletion( useTextBufferForTest('@'), + testDirs, testRootDir, [], mockCommandContext, @@ -974,6 +1008,7 @@ describe('useCompletion', () => { const { result } = renderHook(() => useCompletion( useTextBufferForTest('@d'), + testDirs, testRootDir, [], mockCommandContext, @@ -1007,6 +1042,7 @@ describe('useCompletion', () => { const { result } = renderHook(() => useCompletion( useTextBufferForTest('@'), + testDirs, testRootDir, [], mockCommandContext, @@ -1039,6 +1075,7 @@ describe('useCompletion', () => { const { result } = renderHook(() => useCompletion( useTextBufferForTest('@t'), + testDirs, testRootDir, [], mockCommandContext, @@ -1085,6 +1122,7 @@ describe('useCompletion', () => { const { result } = renderHook(() => useCompletion( mockBuffer, + testDirs, testRootDir, slashCommands, mockCommandContext, @@ -1128,6 +1166,7 @@ describe('useCompletion', () => { const { result } = renderHook(() => useCompletion( mockBuffer, + testDirs, testRootDir, slashCommands, mockCommandContext, @@ -1173,6 +1212,7 @@ describe('useCompletion', () => { const { result } = renderHook(() => useCompletion( mockBuffer, + testDirs, testRootDir, slashCommands, mockCommandContext, @@ -1221,6 +1261,7 @@ describe('useCompletion', () => { const { result } = renderHook(() => useCompletion( mockBuffer, + testDirs, testRootDir, slashCommands, mockCommandContext, diff --git a/packages/cli/src/ui/hooks/useCompletion.ts b/packages/cli/src/ui/hooks/useCompletion.ts index 10724c21..4b106c1b 100644 --- a/packages/cli/src/ui/hooks/useCompletion.ts +++ b/packages/cli/src/ui/hooks/useCompletion.ts @@ -43,6 +43,7 @@ export interface UseCompletionReturn { export function useCompletion( buffer: TextBuffer, + dirs: readonly string[], cwd: string, slashCommands: readonly SlashCommand[], commandContext: CommandContext, @@ -328,8 +329,6 @@ export function useCompletion( : partialPath.substring(lastSlashIndex + 1), ); - const baseDirAbsolute = path.resolve(cwd, baseDirRelative); - let isMounted = true; const findFilesRecursively = async ( @@ -358,7 +357,7 @@ export function useCompletion( const entryPathRelative = path.join(currentRelativePath, entry.name); const entryPathFromRoot = path.relative( - cwd, + startDir, path.join(startDir, entry.name), ); @@ -417,29 +416,31 @@ export function useCompletion( respectGitIgnore?: boolean; respectGeminiIgnore?: boolean; }, + searchDir: string, maxResults = 50, ): Promise<Suggestion[]> => { const globPattern = `**/${searchPrefix}*`; const files = await glob(globPattern, { - cwd, + cwd: searchDir, dot: searchPrefix.startsWith('.'), nocase: true, }); const suggestions: Suggestion[] = files - .map((file: string) => ({ - label: file, - value: escapePath(file), - })) - .filter((s) => { + .filter((file) => { if (fileDiscoveryService) { - return !fileDiscoveryService.shouldIgnoreFile( - s.label, - filterOptions, - ); // relative path + return !fileDiscoveryService.shouldIgnoreFile(file, filterOptions); } return true; }) + .map((file: string) => { + const absolutePath = path.resolve(searchDir, file); + const label = path.relative(cwd, absolutePath); + return { + label, + value: escapePath(label), + }; + }) .slice(0, maxResults); return suggestions; @@ -456,63 +457,78 @@ export function useCompletion( config?.getFileFilteringOptions() ?? DEFAULT_FILE_FILTERING_OPTIONS; try { - // If there's no slash, or it's the root, do a recursive search from cwd - if ( - partialPath.indexOf('/') === -1 && - prefix && - enableRecursiveSearch - ) { - if (fileDiscoveryService) { - fetchedSuggestions = await findFilesWithGlob( - prefix, - fileDiscoveryService, - filterOptions, - ); + // If there's no slash, or it's the root, do a recursive search from workspace directories + for (const dir of dirs) { + let fetchedSuggestionsPerDir: Suggestion[] = []; + if ( + partialPath.indexOf('/') === -1 && + prefix && + enableRecursiveSearch + ) { + if (fileDiscoveryService) { + fetchedSuggestionsPerDir = await findFilesWithGlob( + prefix, + fileDiscoveryService, + filterOptions, + dir, + ); + } else { + fetchedSuggestionsPerDir = await findFilesRecursively( + dir, + prefix, + null, + filterOptions, + ); + } } else { - fetchedSuggestions = await findFilesRecursively( - cwd, - prefix, - null, - filterOptions, - ); - } - } else { - // Original behavior: list files in the specific directory - const lowerPrefix = prefix.toLowerCase(); - const entries = await fs.readdir(baseDirAbsolute, { - withFileTypes: true, - }); + // Original behavior: list files in the specific directory + const lowerPrefix = prefix.toLowerCase(); + const baseDirAbsolute = path.resolve(dir, baseDirRelative); + const entries = await fs.readdir(baseDirAbsolute, { + withFileTypes: true, + }); - // Filter entries using git-aware filtering - const filteredEntries = []; - for (const entry of entries) { - // Conditionally ignore dotfiles - if (!prefix.startsWith('.') && entry.name.startsWith('.')) { - continue; - } - if (!entry.name.toLowerCase().startsWith(lowerPrefix)) continue; + // Filter entries using git-aware filtering + const filteredEntries = []; + for (const entry of entries) { + // Conditionally ignore dotfiles + if (!prefix.startsWith('.') && entry.name.startsWith('.')) { + continue; + } + if (!entry.name.toLowerCase().startsWith(lowerPrefix)) continue; - const relativePath = path.relative( - cwd, - path.join(baseDirAbsolute, entry.name), - ); - if ( - fileDiscoveryService && - fileDiscoveryService.shouldIgnoreFile(relativePath, filterOptions) - ) { - continue; + const relativePath = path.relative( + dir, + path.join(baseDirAbsolute, entry.name), + ); + if ( + fileDiscoveryService && + fileDiscoveryService.shouldIgnoreFile( + relativePath, + filterOptions, + ) + ) { + continue; + } + + filteredEntries.push(entry); } - filteredEntries.push(entry); + fetchedSuggestionsPerDir = filteredEntries.map((entry) => { + const absolutePath = path.resolve(baseDirAbsolute, entry.name); + const label = + cwd === dir ? entry.name : path.relative(cwd, absolutePath); + const suggestionLabel = entry.isDirectory() ? label + '/' : label; + return { + label: suggestionLabel, + value: escapePath(suggestionLabel), + }; + }); } - - fetchedSuggestions = filteredEntries.map((entry) => { - const label = entry.isDirectory() ? entry.name + '/' : entry.name; - return { - label, - value: escapePath(label), // Value for completion should be just the name part - }; - }); + fetchedSuggestions = [ + ...fetchedSuggestions, + ...fetchedSuggestionsPerDir, + ]; } // Like glob, we always return forwardslashes, even in windows. @@ -585,6 +601,7 @@ export function useCompletion( }; }, [ buffer.text, + dirs, cwd, isActive, resetCompletionState, diff --git a/packages/cli/src/utils/sandbox-macos-permissive-closed.sb b/packages/cli/src/utils/sandbox-macos-permissive-closed.sb index 36d88995..cf64da94 100644 --- a/packages/cli/src/utils/sandbox-macos-permissive-closed.sb +++ b/packages/cli/src/utils/sandbox-macos-permissive-closed.sb @@ -13,6 +13,12 @@ (subpath (string-append (param "HOME_DIR") "/.npm")) (subpath (string-append (param "HOME_DIR") "/.cache")) (subpath (string-append (param "HOME_DIR") "/.gitconfig")) + ;; Allow writes to included directories from --include-directories + (subpath (param "INCLUDE_DIR_0")) + (subpath (param "INCLUDE_DIR_1")) + (subpath (param "INCLUDE_DIR_2")) + (subpath (param "INCLUDE_DIR_3")) + (subpath (param "INCLUDE_DIR_4")) (literal "/dev/stdout") (literal "/dev/stderr") (literal "/dev/null") diff --git a/packages/cli/src/utils/sandbox-macos-permissive-open.sb b/packages/cli/src/utils/sandbox-macos-permissive-open.sb index 552efcd4..50d21a1f 100644 --- a/packages/cli/src/utils/sandbox-macos-permissive-open.sb +++ b/packages/cli/src/utils/sandbox-macos-permissive-open.sb @@ -13,6 +13,12 @@ (subpath (string-append (param "HOME_DIR") "/.npm")) (subpath (string-append (param "HOME_DIR") "/.cache")) (subpath (string-append (param "HOME_DIR") "/.gitconfig")) + ;; Allow writes to included directories from --include-directories + (subpath (param "INCLUDE_DIR_0")) + (subpath (param "INCLUDE_DIR_1")) + (subpath (param "INCLUDE_DIR_2")) + (subpath (param "INCLUDE_DIR_3")) + (subpath (param "INCLUDE_DIR_4")) (literal "/dev/stdout") (literal "/dev/stderr") (literal "/dev/null") diff --git a/packages/cli/src/utils/sandbox-macos-permissive-proxied.sb b/packages/cli/src/utils/sandbox-macos-permissive-proxied.sb index 4410776b..8becc8cb 100644 --- a/packages/cli/src/utils/sandbox-macos-permissive-proxied.sb +++ b/packages/cli/src/utils/sandbox-macos-permissive-proxied.sb @@ -13,6 +13,12 @@ (subpath (string-append (param "HOME_DIR") "/.npm")) (subpath (string-append (param "HOME_DIR") "/.cache")) (subpath (string-append (param "HOME_DIR") "/.gitconfig")) + ;; Allow writes to included directories from --include-directories + (subpath (param "INCLUDE_DIR_0")) + (subpath (param "INCLUDE_DIR_1")) + (subpath (param "INCLUDE_DIR_2")) + (subpath (param "INCLUDE_DIR_3")) + (subpath (param "INCLUDE_DIR_4")) (literal "/dev/stdout") (literal "/dev/stderr") (literal "/dev/null") diff --git a/packages/cli/src/utils/sandbox-macos-restrictive-closed.sb b/packages/cli/src/utils/sandbox-macos-restrictive-closed.sb index 9ce68e9d..17d0c073 100644 --- a/packages/cli/src/utils/sandbox-macos-restrictive-closed.sb +++ b/packages/cli/src/utils/sandbox-macos-restrictive-closed.sb @@ -71,6 +71,12 @@ (subpath (string-append (param "HOME_DIR") "/.npm")) (subpath (string-append (param "HOME_DIR") "/.cache")) (subpath (string-append (param "HOME_DIR") "/.gitconfig")) + ;; Allow writes to included directories from --include-directories + (subpath (param "INCLUDE_DIR_0")) + (subpath (param "INCLUDE_DIR_1")) + (subpath (param "INCLUDE_DIR_2")) + (subpath (param "INCLUDE_DIR_3")) + (subpath (param "INCLUDE_DIR_4")) (literal "/dev/stdout") (literal "/dev/stderr") (literal "/dev/null") diff --git a/packages/cli/src/utils/sandbox-macos-restrictive-open.sb b/packages/cli/src/utils/sandbox-macos-restrictive-open.sb index e89b8090..17f27224 100644 --- a/packages/cli/src/utils/sandbox-macos-restrictive-open.sb +++ b/packages/cli/src/utils/sandbox-macos-restrictive-open.sb @@ -71,6 +71,12 @@ (subpath (string-append (param "HOME_DIR") "/.npm")) (subpath (string-append (param "HOME_DIR") "/.cache")) (subpath (string-append (param "HOME_DIR") "/.gitconfig")) + ;; Allow writes to included directories from --include-directories + (subpath (param "INCLUDE_DIR_0")) + (subpath (param "INCLUDE_DIR_1")) + (subpath (param "INCLUDE_DIR_2")) + (subpath (param "INCLUDE_DIR_3")) + (subpath (param "INCLUDE_DIR_4")) (literal "/dev/stdout") (literal "/dev/stderr") (literal "/dev/null") diff --git a/packages/cli/src/utils/sandbox-macos-restrictive-proxied.sb b/packages/cli/src/utils/sandbox-macos-restrictive-proxied.sb index a49712a3..c07c1496 100644 --- a/packages/cli/src/utils/sandbox-macos-restrictive-proxied.sb +++ b/packages/cli/src/utils/sandbox-macos-restrictive-proxied.sb @@ -71,6 +71,12 @@ (subpath (string-append (param "HOME_DIR") "/.npm")) (subpath (string-append (param "HOME_DIR") "/.cache")) (subpath (string-append (param "HOME_DIR") "/.gitconfig")) + ;; Allow writes to included directories from --include-directories + (subpath (param "INCLUDE_DIR_0")) + (subpath (param "INCLUDE_DIR_1")) + (subpath (param "INCLUDE_DIR_2")) + (subpath (param "INCLUDE_DIR_3")) + (subpath (param "INCLUDE_DIR_4")) (literal "/dev/stdout") (literal "/dev/stderr") (literal "/dev/null") diff --git a/packages/cli/src/utils/sandbox.ts b/packages/cli/src/utils/sandbox.ts index 84fdc8f7..72b5e56b 100644 --- a/packages/cli/src/utils/sandbox.ts +++ b/packages/cli/src/utils/sandbox.ts @@ -15,7 +15,7 @@ import { SETTINGS_DIRECTORY_NAME, } from '../config/settings.js'; import { promisify } from 'util'; -import { SandboxConfig } from '@google/gemini-cli-core'; +import { Config, SandboxConfig } from '@google/gemini-cli-core'; const execAsync = promisify(exec); @@ -183,6 +183,7 @@ function entrypoint(workdir: string): string[] { export async function start_sandbox( config: SandboxConfig, nodeArgs: string[] = [], + cliConfig?: Config, ) { if (config.command === 'sandbox-exec') { // disallow BUILD_SANDBOX @@ -223,6 +224,38 @@ export async function start_sandbox( `HOME_DIR=${fs.realpathSync(os.homedir())}`, '-D', `CACHE_DIR=${fs.realpathSync(execSync(`getconf DARWIN_USER_CACHE_DIR`).toString().trim())}`, + ]; + + // Add included directories from the workspace context + // Always add 5 INCLUDE_DIR parameters to ensure .sb files can reference them + const MAX_INCLUDE_DIRS = 5; + const targetDir = fs.realpathSync(cliConfig?.getTargetDir() || ''); + const includedDirs: string[] = []; + + if (cliConfig) { + const workspaceContext = cliConfig.getWorkspaceContext(); + const directories = workspaceContext.getDirectories(); + + // Filter out TARGET_DIR + for (const dir of directories) { + const realDir = fs.realpathSync(dir); + if (realDir !== targetDir) { + includedDirs.push(realDir); + } + } + } + + for (let i = 0; i < MAX_INCLUDE_DIRS; i++) { + let dirPath = '/dev/null'; // Default to a safe path that won't cause issues + + if (i < includedDirs.length) { + dirPath = includedDirs[i]; + } + + args.push('-D', `INCLUDE_DIR_${i}=${dirPath}`); + } + + args.push( '-f', profileFile, 'sh', @@ -232,7 +265,7 @@ export async function start_sandbox( `NODE_OPTIONS="${nodeOptions}"`, ...process.argv.map((arg) => quote([arg])), ].join(' '), - ]; + ); // start and set up proxy if GEMINI_SANDBOX_PROXY_COMMAND is set const proxyCommand = process.env.GEMINI_SANDBOX_PROXY_COMMAND; let proxyProcess: ChildProcess | undefined = undefined; |
