diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts index f002fd84..08a85e4d 100644 --- a/packages/cli/src/config/config.test.ts +++ b/packages/cli/src/config/config.test.ts @@ -6,7 +6,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import * as os from 'os'; -import { loadCliConfig } from './config.js'; +import { loadCliConfig, parseArguments } from './config.js'; import { Settings } from './settings.js'; import { Extension } from './extension.js'; import * as ServerConfig from '@google/gemini-cli-core'; @@ -46,6 +46,100 @@ vi.mock('@google/gemini-cli-core', async () => { }; }); +describe('parseArguments', () => { + const originalArgv = process.argv; + + afterEach(() => { + process.argv = originalArgv; + }); + + it('should throw an error when both --prompt and --prompt-interactive are used together', async () => { + process.argv = [ + 'node', + 'script.js', + '--prompt', + 'test prompt', + '--prompt-interactive', + 'interactive prompt', + ]; + + const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => { + throw new Error('process.exit called'); + }); + + const mockConsoleError = vi + .spyOn(console, 'error') + .mockImplementation(() => {}); + + await expect(parseArguments()).rejects.toThrow('process.exit called'); + + expect(mockConsoleError).toHaveBeenCalledWith( + expect.stringContaining( + 'Cannot use both --prompt (-p) and --prompt-interactive (-i) together', + ), + ); + + mockExit.mockRestore(); + mockConsoleError.mockRestore(); + }); + + it('should throw an error when using short flags -p and -i together', async () => { + process.argv = [ + 'node', + 'script.js', + '-p', + 'test prompt', + '-i', + 'interactive prompt', + ]; + + const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => { + throw new Error('process.exit called'); + }); + + const mockConsoleError = vi + .spyOn(console, 'error') + .mockImplementation(() => {}); + + await expect(parseArguments()).rejects.toThrow('process.exit called'); + + expect(mockConsoleError).toHaveBeenCalledWith( + expect.stringContaining( + 'Cannot use both --prompt (-p) and --prompt-interactive (-i) together', + ), + ); + + mockExit.mockRestore(); + mockConsoleError.mockRestore(); + }); + + it('should allow --prompt without --prompt-interactive', async () => { + process.argv = ['node', 'script.js', '--prompt', 'test prompt']; + const argv = await parseArguments(); + expect(argv.prompt).toBe('test prompt'); + expect(argv.promptInteractive).toBeUndefined(); + }); + + it('should allow --prompt-interactive without --prompt', async () => { + process.argv = [ + 'node', + 'script.js', + '--prompt-interactive', + 'interactive prompt', + ]; + const argv = await parseArguments(); + expect(argv.promptInteractive).toBe('interactive prompt'); + expect(argv.prompt).toBeUndefined(); + }); + + it('should allow -i flag as alias for --prompt-interactive', async () => { + process.argv = ['node', 'script.js', '-i', 'interactive prompt']; + const argv = await parseArguments(); + expect(argv.promptInteractive).toBe('interactive prompt'); + expect(argv.prompt).toBeUndefined(); + }); +}); + describe('loadCliConfig', () => { const originalArgv = process.argv; const originalEnv = { ...process.env }; @@ -64,29 +158,33 @@ describe('loadCliConfig', () => { it('should set showMemoryUsage to true when --show-memory-usage flag is present', async () => { process.argv = ['node', 'script.js', '--show-memory-usage']; + const argv = await parseArguments(); const settings: Settings = {}; - const config = await loadCliConfig(settings, [], 'test-session'); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getShowMemoryUsage()).toBe(true); }); it('should set showMemoryUsage to false when --memory flag is not present', async () => { process.argv = ['node', 'script.js']; + const argv = await parseArguments(); const settings: Settings = {}; - const config = await loadCliConfig(settings, [], 'test-session'); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getShowMemoryUsage()).toBe(false); }); it('should set showMemoryUsage to false by default from settings if CLI flag is not present', async () => { process.argv = ['node', 'script.js']; + const argv = await parseArguments(); const settings: Settings = { showMemoryUsage: false }; - const config = await loadCliConfig(settings, [], 'test-session'); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getShowMemoryUsage()).toBe(false); }); it('should prioritize CLI flag over settings for showMemoryUsage (CLI true, settings false)', async () => { process.argv = ['node', 'script.js', '--show-memory-usage']; + const argv = await parseArguments(); const settings: Settings = { showMemoryUsage: false }; - const config = await loadCliConfig(settings, [], 'test-session'); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getShowMemoryUsage()).toBe(true); }); }); @@ -109,59 +207,67 @@ describe('loadCliConfig telemetry', () => { it('should set telemetry to false by default when no flag or setting is present', async () => { process.argv = ['node', 'script.js']; + const argv = await parseArguments(); const settings: Settings = {}; - const config = await loadCliConfig(settings, [], 'test-session'); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryEnabled()).toBe(false); }); it('should set telemetry to true when --telemetry flag is present', async () => { process.argv = ['node', 'script.js', '--telemetry']; + const argv = await parseArguments(); const settings: Settings = {}; - const config = await loadCliConfig(settings, [], 'test-session'); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryEnabled()).toBe(true); }); it('should set telemetry to false when --no-telemetry flag is present', async () => { process.argv = ['node', 'script.js', '--no-telemetry']; + const argv = await parseArguments(); const settings: Settings = {}; - const config = await loadCliConfig(settings, [], 'test-session'); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryEnabled()).toBe(false); }); it('should use telemetry value from settings if CLI flag is not present (settings true)', async () => { process.argv = ['node', 'script.js']; + const argv = await parseArguments(); const settings: Settings = { telemetry: { enabled: true } }; - const config = await loadCliConfig(settings, [], 'test-session'); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryEnabled()).toBe(true); }); it('should use telemetry value from settings if CLI flag is not present (settings false)', async () => { process.argv = ['node', 'script.js']; + const argv = await parseArguments(); const settings: Settings = { telemetry: { enabled: false } }; - const config = await loadCliConfig(settings, [], 'test-session'); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryEnabled()).toBe(false); }); it('should prioritize --telemetry CLI flag (true) over settings (false)', async () => { process.argv = ['node', 'script.js', '--telemetry']; + const argv = await parseArguments(); const settings: Settings = { telemetry: { enabled: false } }; - const config = await loadCliConfig(settings, [], 'test-session'); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryEnabled()).toBe(true); }); it('should prioritize --no-telemetry CLI flag (false) over settings (true)', async () => { process.argv = ['node', 'script.js', '--no-telemetry']; + const argv = await parseArguments(); const settings: Settings = { telemetry: { enabled: true } }; - const config = await loadCliConfig(settings, [], 'test-session'); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryEnabled()).toBe(false); }); it('should use telemetry OTLP endpoint from settings if CLI flag is not present', async () => { process.argv = ['node', 'script.js']; + const argv = await parseArguments(); const settings: Settings = { telemetry: { otlpEndpoint: 'http://settings.example.com' }, }; - const config = await loadCliConfig(settings, [], 'test-session'); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryOtlpEndpoint()).toBe( 'http://settings.example.com', ); @@ -174,26 +280,29 @@ describe('loadCliConfig telemetry', () => { '--telemetry-otlp-endpoint', 'http://cli.example.com', ]; + const argv = await parseArguments(); const settings: Settings = { telemetry: { otlpEndpoint: 'http://settings.example.com' }, }; - const config = await loadCliConfig(settings, [], 'test-session'); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryOtlpEndpoint()).toBe('http://cli.example.com'); }); it('should use default endpoint if no OTLP endpoint is provided via CLI or settings', async () => { process.argv = ['node', 'script.js']; + const argv = await parseArguments(); const settings: Settings = { telemetry: { enabled: true } }; - const config = await loadCliConfig(settings, [], 'test-session'); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryOtlpEndpoint()).toBe('http://localhost:4317'); }); it('should use telemetry target from settings if CLI flag is not present', async () => { process.argv = ['node', 'script.js']; + const argv = await parseArguments(); const settings: Settings = { telemetry: { target: ServerConfig.DEFAULT_TELEMETRY_TARGET }, }; - const config = await loadCliConfig(settings, [], 'test-session'); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryTarget()).toBe( ServerConfig.DEFAULT_TELEMETRY_TARGET, ); @@ -201,17 +310,19 @@ describe('loadCliConfig telemetry', () => { it('should prioritize --telemetry-target CLI flag over settings', async () => { process.argv = ['node', 'script.js', '--telemetry-target', 'gcp']; + const argv = await parseArguments(); const settings: Settings = { telemetry: { target: ServerConfig.DEFAULT_TELEMETRY_TARGET }, }; - const config = await loadCliConfig(settings, [], 'test-session'); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryTarget()).toBe('gcp'); }); it('should use default target if no target is provided via CLI or settings', async () => { process.argv = ['node', 'script.js']; + const argv = await parseArguments(); const settings: Settings = { telemetry: { enabled: true } }; - const config = await loadCliConfig(settings, [], 'test-session'); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryTarget()).toBe( ServerConfig.DEFAULT_TELEMETRY_TARGET, ); @@ -219,29 +330,33 @@ describe('loadCliConfig telemetry', () => { it('should use telemetry log prompts from settings if CLI flag is not present', async () => { process.argv = ['node', 'script.js']; + const argv = await parseArguments(); const settings: Settings = { telemetry: { logPrompts: false } }; - const config = await loadCliConfig(settings, [], 'test-session'); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryLogPromptsEnabled()).toBe(false); }); it('should prioritize --telemetry-log-prompts CLI flag (true) over settings (false)', async () => { process.argv = ['node', 'script.js', '--telemetry-log-prompts']; + const argv = await parseArguments(); const settings: Settings = { telemetry: { logPrompts: false } }; - const config = await loadCliConfig(settings, [], 'test-session'); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryLogPromptsEnabled()).toBe(true); }); it('should prioritize --no-telemetry-log-prompts CLI flag (false) over settings (true)', async () => { process.argv = ['node', 'script.js', '--no-telemetry-log-prompts']; + const argv = await parseArguments(); const settings: Settings = { telemetry: { logPrompts: true } }; - const config = await loadCliConfig(settings, [], 'test-session'); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryLogPromptsEnabled()).toBe(false); }); it('should use default log prompts (true) if no value is provided via CLI or settings', async () => { process.argv = ['node', 'script.js']; + const argv = await parseArguments(); const settings: Settings = { telemetry: { enabled: true } }; - const config = await loadCliConfig(settings, [], 'test-session'); + const config = await loadCliConfig(settings, [], 'test-session', argv); expect(config.getTelemetryLogPromptsEnabled()).toBe(true); }); }); @@ -286,7 +401,8 @@ describe('Hierarchical Memory Loading (config.ts) - Placeholder Suite', () => { ], }, ]; - await loadCliConfig(settings, extensions, 'session-id'); + const argv = await parseArguments(); + await loadCliConfig(settings, extensions, 'session-id', argv); expect(ServerConfig.loadServerHierarchicalMemory).toHaveBeenCalledWith( expect.any(String), false, @@ -346,7 +462,9 @@ describe('mergeMcpServers', () => { }, ]; const originalSettings = JSON.parse(JSON.stringify(settings)); - await loadCliConfig(settings, extensions, 'test-session'); + process.argv = ['node', 'script.js']; + const argv = await parseArguments(); + await loadCliConfig(settings, extensions, 'test-session', argv); expect(settings).toEqual(originalSettings); }); }); @@ -372,7 +490,14 @@ describe('mergeExcludeTools', () => { contextFiles: [], }, ]; - const config = await loadCliConfig(settings, extensions, 'test-session'); + process.argv = ['node', 'script.js']; + const argv = await parseArguments(); + const config = await loadCliConfig( + settings, + extensions, + 'test-session', + argv, + ); expect(config.getExcludeTools()).toEqual( expect.arrayContaining(['tool1', 'tool2', 'tool3', 'tool4', 'tool5']), ); @@ -391,7 +516,14 @@ describe('mergeExcludeTools', () => { contextFiles: [], }, ]; - const config = await loadCliConfig(settings, extensions, 'test-session'); + process.argv = ['node', 'script.js']; + const argv = await parseArguments(); + const config = await loadCliConfig( + settings, + extensions, + 'test-session', + argv, + ); expect(config.getExcludeTools()).toEqual( expect.arrayContaining(['tool1', 'tool2', 'tool3']), ); @@ -418,7 +550,14 @@ describe('mergeExcludeTools', () => { contextFiles: [], }, ]; - const config = await loadCliConfig(settings, extensions, 'test-session'); + process.argv = ['node', 'script.js']; + const argv = await parseArguments(); + const config = await loadCliConfig( + settings, + extensions, + 'test-session', + argv, + ); expect(config.getExcludeTools()).toEqual( expect.arrayContaining(['tool1', 'tool2', 'tool3', 'tool4']), ); @@ -428,14 +567,28 @@ describe('mergeExcludeTools', () => { it('should return an empty array when no excludeTools are specified', async () => { const settings: Settings = {}; const extensions: Extension[] = []; - const config = await loadCliConfig(settings, extensions, 'test-session'); + process.argv = ['node', 'script.js']; + const argv = await parseArguments(); + const config = await loadCliConfig( + settings, + extensions, + 'test-session', + argv, + ); expect(config.getExcludeTools()).toEqual([]); }); it('should handle settings with excludeTools but no extensions', async () => { + process.argv = ['node', 'script.js']; + const argv = await parseArguments(); const settings: Settings = { excludeTools: ['tool1', 'tool2'] }; const extensions: Extension[] = []; - const config = await loadCliConfig(settings, extensions, 'test-session'); + const config = await loadCliConfig( + settings, + extensions, + 'test-session', + argv, + ); expect(config.getExcludeTools()).toEqual( expect.arrayContaining(['tool1', 'tool2']), ); @@ -454,7 +607,14 @@ describe('mergeExcludeTools', () => { contextFiles: [], }, ]; - const config = await loadCliConfig(settings, extensions, 'test-session'); + process.argv = ['node', 'script.js']; + const argv = await parseArguments(); + const config = await loadCliConfig( + settings, + extensions, + 'test-session', + argv, + ); expect(config.getExcludeTools()).toEqual( expect.arrayContaining(['tool1', 'tool2']), ); @@ -474,7 +634,9 @@ describe('mergeExcludeTools', () => { }, ]; const originalSettings = JSON.parse(JSON.stringify(settings)); - await loadCliConfig(settings, extensions, 'test-session'); + process.argv = ['node', 'script.js']; + const argv = await parseArguments(); + await loadCliConfig(settings, extensions, 'test-session', argv); expect(settings).toEqual(originalSettings); }); }); @@ -505,7 +667,8 @@ describe('loadCliConfig with allowed-mcp-server-names', () => { it('should allow all MCP servers if the flag is not provided', async () => { process.argv = ['node', 'script.js']; - const config = await loadCliConfig(baseSettings, [], 'test-session'); + const argv = await parseArguments(); + const config = await loadCliConfig(baseSettings, [], 'test-session', argv); expect(config.getMcpServers()).toEqual(baseSettings.mcpServers); }); @@ -516,7 +679,8 @@ describe('loadCliConfig with allowed-mcp-server-names', () => { '--allowed-mcp-server-names', 'server1', ]; - const config = await loadCliConfig(baseSettings, [], 'test-session'); + const argv = await parseArguments(); + const config = await loadCliConfig(baseSettings, [], 'test-session', argv); expect(config.getMcpServers()).toEqual({ server1: { url: 'http://localhost:8080' }, }); @@ -531,7 +695,8 @@ describe('loadCliConfig with allowed-mcp-server-names', () => { '--allowed-mcp-server-names', 'server3', ]; - const config = await loadCliConfig(baseSettings, [], 'test-session'); + const argv = await parseArguments(); + const config = await loadCliConfig(baseSettings, [], 'test-session', argv); expect(config.getMcpServers()).toEqual({ server1: { url: 'http://localhost:8080' }, server3: { url: 'http://localhost:8082' }, @@ -547,7 +712,8 @@ describe('loadCliConfig with allowed-mcp-server-names', () => { '--allowed-mcp-server-names', 'server4', ]; - const config = await loadCliConfig(baseSettings, [], 'test-session'); + const argv = await parseArguments(); + const config = await loadCliConfig(baseSettings, [], 'test-session', argv); expect(config.getMcpServers()).toEqual({ server1: { url: 'http://localhost:8080' }, }); @@ -555,7 +721,8 @@ describe('loadCliConfig with allowed-mcp-server-names', () => { it('should allow no MCP servers if the flag is provided but empty', async () => { process.argv = ['node', 'script.js', '--allowed-mcp-server-names', '']; - const config = await loadCliConfig(baseSettings, [], 'test-session'); + const argv = await parseArguments(); + const config = await loadCliConfig(baseSettings, [], 'test-session', argv); expect(config.getMcpServers()).toEqual({}); }); }); @@ -574,11 +741,13 @@ describe('loadCliConfig extensions', () => { it('should not filter extensions if --extensions flag is not used', async () => { process.argv = ['node', 'script.js']; + const argv = await parseArguments(); const settings: Settings = {}; const config = await loadCliConfig( settings, mockExtensions, 'test-session', + argv, ); expect(config.getExtensionContextFilePaths()).toEqual([ '/path/to/ext1.md', @@ -588,11 +757,13 @@ describe('loadCliConfig extensions', () => { it('should filter extensions if --extensions flag is used', async () => { process.argv = ['node', 'script.js', '--extensions', 'ext1']; + const argv = await parseArguments(); const settings: Settings = {}; const config = await loadCliConfig( settings, mockExtensions, 'test-session', + argv, ); expect(config.getExtensionContextFilePaths()).toEqual(['/path/to/ext1.md']); }); diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index b685f090..1c1f0746 100644 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -34,12 +34,13 @@ const logger = { error: (...args: any[]) => console.error('[ERROR]', ...args), }; -interface CliArgs { +export interface CliArgs { model: string | undefined; sandbox: boolean | string | undefined; sandboxImage: string | undefined; debug: boolean | undefined; prompt: string | undefined; + promptInteractive: string | undefined; allFiles: boolean | undefined; all_files: boolean | undefined; showMemoryUsage: boolean | undefined; @@ -55,7 +56,7 @@ interface CliArgs { listExtensions: boolean | undefined; } -async function parseArguments(): Promise { +export async function parseArguments(): Promise { const yargsInstance = yargs(hideBin(process.argv)) .scriptName('gemini') .usage( @@ -73,6 +74,12 @@ async function parseArguments(): Promise { type: 'string', description: 'Prompt. Appended to input on stdin (if any).', }) + .option('prompt-interactive', { + alias: 'i', + type: 'string', + description: + 'Execute the provided prompt and continue in interactive mode', + }) .option('sandbox', { alias: 's', type: 'boolean', @@ -173,10 +180,17 @@ async function parseArguments(): Promise { .alias('v', 'version') .help() .alias('h', 'help') - .strict(); + .strict() + .check((argv) => { + if (argv.prompt && argv.promptInteractive) { + throw new Error( + 'Cannot use both --prompt (-p) and --prompt-interactive (-i) together', + ); + } + return true; + }); yargsInstance.wrap(yargsInstance.terminalWidth()); - return yargsInstance.argv; } @@ -208,8 +222,8 @@ export async function loadCliConfig( settings: Settings, extensions: Extension[], sessionId: string, + argv: CliArgs, ): Promise { - const argv = await parseArguments(); const debugMode = argv.debug || [process.env.DEBUG, process.env.DEBUG_MODE].some( @@ -267,7 +281,7 @@ export async function loadCliConfig( sandbox: sandboxConfig, targetDir: process.cwd(), debugMode, - question: argv.prompt || '', + question: argv.promptInteractive || argv.prompt || '', fullContext: argv.allFiles || argv.all_files || false, coreTools: settings.coreTools || undefined, excludeTools, diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx index 3c0b151b..b2f84c80 100644 --- a/packages/cli/src/gemini.tsx +++ b/packages/cli/src/gemini.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { render } from 'ink'; import { AppWrapper } from './ui/App.js'; -import { loadCliConfig } from './config/config.js'; +import { loadCliConfig, parseArguments, CliArgs } from './config/config.js'; import { readStdin } from './utils/readStdin.js'; import { basename } from 'node:path'; import v8 from 'node:v8'; @@ -102,8 +102,21 @@ export async function main() { process.exit(1); } + const argv = await parseArguments(); const extensions = loadExtensions(workspaceRoot); - const config = await loadCliConfig(settings.merged, extensions, sessionId); + const config = await loadCliConfig( + settings.merged, + extensions, + sessionId, + argv, + ); + + if (argv.promptInteractive && !process.stdin.isTTY) { + console.error( + 'Error: The --prompt-interactive flag is not supported when piping input from stdin.', + ); + process.exit(1); + } if (config.getListExtensions()) { console.log('Installed extensions:'); @@ -182,8 +195,11 @@ export async function main() { ...(await getUserStartupWarnings(workspaceRoot)), ]; + const shouldBeInteractive = + !!argv.promptInteractive || (process.stdin.isTTY && input?.length === 0); + // Render UI, passing necessary config values. Check that there is no command line question. - if (process.stdin.isTTY && input?.length === 0) { + if (shouldBeInteractive) { const version = await getCliVersion(); setWindowTitle(basename(workspaceRoot), settings); render( @@ -224,6 +240,7 @@ export async function main() { config, extensions, settings, + argv, ); await runNonInteractive(nonInteractiveConfig, input, prompt_id); @@ -264,6 +281,7 @@ async function loadNonInteractiveConfig( config: Config, extensions: Extension[], settings: LoadedSettings, + argv: CliArgs, ) { let finalConfig = config; if (config.getApprovalMode() !== ApprovalMode.YOLO) { @@ -287,6 +305,7 @@ async function loadNonInteractiveConfig( nonInteractiveSettings, extensions, config.getSessionId(), + argv, ); await finalConfig.initialize(); } diff --git a/packages/cli/src/ui/App.test.tsx b/packages/cli/src/ui/App.test.tsx index fffea64d..c64b526e 100644 --- a/packages/cli/src/ui/App.test.tsx +++ b/packages/cli/src/ui/App.test.tsx @@ -14,9 +14,12 @@ import { ToolRegistry, AccessibilitySettings, SandboxConfig, + GeminiClient, } from '@google/gemini-cli-core'; import { LoadedSettings, SettingsFile, Settings } from '../config/settings.js'; import process from 'node:process'; +import { useGeminiStream } from './hooks/useGeminiStream.js'; +import { StreamingState } from './types.js'; import { Tips } from './components/Tips.js'; // Define a more complete mock server config based on actual Config @@ -67,6 +70,7 @@ interface MockServerConfig { getAccessibility: Mock<() => AccessibilitySettings>; getProjectRoot: Mock<() => string | undefined>; getAllGeminiMdFilenames: Mock<() => string[]>; + getGeminiClient: Mock<() => GeminiClient | undefined>; getUserTier: Mock<() => Promise>; } @@ -124,7 +128,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => { getVertexAI: vi.fn(() => opts.vertexai), getShowMemoryUsage: vi.fn(() => opts.showMemoryUsage ?? false), getAccessibility: vi.fn(() => opts.accessibility ?? {}), - getProjectRoot: vi.fn(() => opts.projectRoot), + getProjectRoot: vi.fn(() => opts.targetDir), getGeminiClient: vi.fn(() => ({})), getCheckpointingEnabled: vi.fn(() => opts.checkpointing ?? true), getAllGeminiMdFilenames: vi.fn(() => ['GEMINI.md']), @@ -508,4 +512,48 @@ describe('App UI', () => { expect(lastFrame()).not.toContain('Select Theme'); }); }); + + describe('with initial prompt from --prompt-interactive', () => { + it('should submit the initial prompt automatically', async () => { + const mockSubmitQuery = vi.fn(); + + mockConfig.getQuestion = vi.fn(() => 'hello from prompt-interactive'); + + vi.mocked(useGeminiStream).mockReturnValue({ + streamingState: StreamingState.Idle, + submitQuery: mockSubmitQuery, + initError: null, + pendingHistoryItems: [], + thought: null, + }); + + mockConfig.getGeminiClient.mockReturnValue({ + isInitialized: vi.fn(() => true), + } as unknown as GeminiClient); + + const { unmount, rerender } = render( + , + ); + currentUnmount = unmount; + + // Force a re-render to trigger useEffect + rerender( + , + ); + + await new Promise((resolve) => setTimeout(resolve, 0)); + + expect(mockSubmitQuery).toHaveBeenCalledWith( + 'hello from prompt-interactive', + ); + }); + }); }); diff --git a/packages/cli/src/ui/App.tsx b/packages/cli/src/ui/App.tsx index f2324b28..98f7689c 100644 --- a/packages/cli/src/ui/App.tsx +++ b/packages/cli/src/ui/App.tsx @@ -148,6 +148,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { const openPrivacyNotice = useCallback(() => { setShowPrivacyNotice(true); }, []); + const initialPromptSubmitted = useRef(false); const errorCount = useMemo( () => consoleMessages.filter((msg) => msg.type === 'error').length, @@ -637,6 +638,34 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => { return getAllGeminiMdFilenames(); }, [settings.merged.contextFileName]); + const initialPrompt = useMemo(() => config.getQuestion(), [config]); + const geminiClient = config.getGeminiClient(); + + useEffect(() => { + if ( + initialPrompt && + !initialPromptSubmitted.current && + !isAuthenticating && + !isAuthDialogOpen && + !isThemeDialogOpen && + !isEditorDialogOpen && + !showPrivacyNotice && + geminiClient?.isInitialized?.() + ) { + submitQuery(initialPrompt); + initialPromptSubmitted.current = true; + } + }, [ + initialPrompt, + submitQuery, + isAuthenticating, + isAuthDialogOpen, + isThemeDialogOpen, + isEditorDialogOpen, + showPrivacyNotice, + geminiClient, + ]); + if (quittingMessages) { return ( diff --git a/packages/core/src/core/client.ts b/packages/core/src/core/client.ts index 747b9165..ed903788 100644 --- a/packages/core/src/core/client.ts +++ b/packages/core/src/core/client.ts @@ -134,6 +134,10 @@ export class GeminiClient { return this.chat; } + isInitialized(): boolean { + return this.chat !== undefined && this.contentGenerator !== undefined; + } + getHistory(): Content[] { return this.getChat().getHistory(); }