From beacc4f6fd102fd969239ffddb6c092631cd0481 Mon Sep 17 00:00:00 2001 From: Ishaan Gupta Date: Fri, 23 Jan 2026 05:08:53 +0530 Subject: [PATCH] fix(cli)!: Default to interactive mode for positional arguments (#16329) Co-authored-by: Allen Hutchison --- packages/cli/src/config/config.test.ts | 46 ++++++++++++++++++-------- packages/cli/src/config/config.ts | 25 ++++++++------ packages/cli/src/gemini.tsx | 6 ++++ 3 files changed, 53 insertions(+), 24 deletions(-) diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts index 73b0c45ccb..193914ef88 100644 --- a/packages/cli/src/config/config.test.ts +++ b/packages/cli/src/config/config.test.ts @@ -371,6 +371,21 @@ describe('parseArguments', () => { } }, ); + + it('should include a startup message when converting positional query to interactive prompt', async () => { + const originalIsTTY = process.stdin.isTTY; + process.stdin.isTTY = true; + process.argv = ['node', 'script.js', 'hello']; + + try { + const argv = await parseArguments(createTestMergedSettings()); + expect(argv.startupMessages).toContain( + 'Positional arguments now default to interactive mode. To run in non-interactive mode, use the --prompt (-p) flag.', + ); + } finally { + process.stdin.isTTY = originalIsTTY; + } + }); }); it.each([ @@ -1953,7 +1968,7 @@ describe('loadCliConfig interactive', () => { expect(config.isInteractive()).toBe(false); }); - it('should not be interactive if positional prompt words are provided with other flags', async () => { + it('should be interactive if positional prompt words are provided with other flags', async () => { process.stdin.isTTY = true; process.argv = ['node', 'script.js', '--model', 'gemini-2.5-pro', 'Hello']; const argv = await parseArguments(createTestMergedSettings()); @@ -1962,10 +1977,10 @@ describe('loadCliConfig interactive', () => { 'test-session', argv, ); - expect(config.isInteractive()).toBe(false); + expect(config.isInteractive()).toBe(true); }); - it('should not be interactive if positional prompt words are provided with multiple flags', async () => { + it('should be interactive if positional prompt words are provided with multiple flags', async () => { process.stdin.isTTY = true; process.argv = [ 'node', @@ -1981,13 +1996,13 @@ describe('loadCliConfig interactive', () => { 'test-session', argv, ); - expect(config.isInteractive()).toBe(false); + expect(config.isInteractive()).toBe(true); // Verify the question is preserved for one-shot execution - expect(argv.prompt).toBe('Hello world'); - expect(argv.promptInteractive).toBeUndefined(); + expect(argv.prompt).toBeUndefined(); + expect(argv.promptInteractive).toBe('Hello world'); }); - it('should not be interactive if positional prompt words are provided with extensions flag', async () => { + it('should be interactive if positional prompt words are provided with extensions flag', async () => { process.stdin.isTTY = true; process.argv = ['node', 'script.js', '-e', 'none', 'hello']; const argv = await parseArguments(createTestMergedSettings()); @@ -1996,8 +2011,9 @@ describe('loadCliConfig interactive', () => { 'test-session', argv, ); - expect(config.isInteractive()).toBe(false); + expect(config.isInteractive()).toBe(true); expect(argv.query).toBe('hello'); + expect(argv.promptInteractive).toBe('hello'); expect(argv.extensions).toEqual(['none']); }); @@ -2010,9 +2026,9 @@ describe('loadCliConfig interactive', () => { 'test-session', argv, ); - expect(config.isInteractive()).toBe(false); + expect(config.isInteractive()).toBe(true); expect(argv.query).toBe('hello world how are you'); - expect(argv.prompt).toBe('hello world how are you'); + expect(argv.promptInteractive).toBe('hello world how are you'); }); it('should handle multiple positional words with flags', async () => { @@ -2035,8 +2051,9 @@ describe('loadCliConfig interactive', () => { 'test-session', argv, ); - expect(config.isInteractive()).toBe(false); + expect(config.isInteractive()).toBe(true); expect(argv.query).toBe('write a function to sort array'); + expect(argv.promptInteractive).toBe('write a function to sort array'); expect(argv.model).toBe('gemini-2.5-pro'); }); @@ -2072,8 +2089,9 @@ describe('loadCliConfig interactive', () => { 'test-session', argv, ); - expect(config.isInteractive()).toBe(false); + expect(config.isInteractive()).toBe(true); expect(argv.query).toBe('hello world how are you'); + expect(argv.promptInteractive).toBe('hello world how are you'); expect(argv.extensions).toEqual(['none']); }); @@ -2708,9 +2726,9 @@ describe('PolicyEngine nonInteractive wiring', () => { vi.restoreAllMocks(); }); - it('should set nonInteractive to true in one-shot mode', async () => { + it('should set nonInteractive to true when -p flag is used', async () => { process.stdin.isTTY = true; - process.argv = ['node', 'script.js', 'echo hello']; // Positional query makes it one-shot + process.argv = ['node', 'script.js', '-p', 'echo hello']; const argv = await parseArguments(createTestMergedSettings()); const config = await loadCliConfig( createTestMergedSettings(), diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index d6ac10bc77..cba5824da4 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -84,6 +84,7 @@ export interface CliArgs { outputFormat: string | undefined; fakeResponses: string | undefined; recordResponses: string | undefined; + startupMessages?: string[]; rawOutput: boolean | undefined; acceptRawOutputRisk: boolean | undefined; isCommand: boolean | undefined; @@ -93,11 +94,12 @@ export async function parseArguments( settings: MergedSettings, ): Promise { const rawArgv = hideBin(process.argv); + const startupMessages: string[] = []; const yargsInstance = yargs(rawArgv) .locale('en') .scriptName('gemini') .usage( - 'Usage: gemini [options] [command]\n\nGemini CLI - Launch an interactive CLI, use -p/--prompt for non-interactive mode', + 'Usage: gemini [options] [command]\n\nGemini CLI - Defaults to interactive mode. Use -p/--prompt for non-interactive (headless) mode.', ) .option('debug', { alias: 'd', @@ -109,7 +111,7 @@ export async function parseArguments( yargsInstance .positional('query', { description: - 'Positional prompt. Defaults to one-shot; use -i/--prompt-interactive for interactive.', + 'Initial prompt. Runs in interactive mode by default; use -p/--prompt for non-interactive.', }) .option('model', { alias: 'm', @@ -121,7 +123,8 @@ export async function parseArguments( alias: 'p', type: 'string', nargs: 1, - description: 'Prompt. Appended to input on stdin (if any).', + description: + 'Run in non-interactive (headless) mode with the given prompt. Appended to input on stdin (if any).', }) .option('prompt-interactive', { alias: 'i', @@ -342,11 +345,12 @@ export async function parseArguments( ? queryArg.join(' ') : queryArg; - // Route positional args: explicit -i flag -> interactive; else -> one-shot (even for @commands) + // -p/--prompt forces non-interactive mode; positional args default to interactive in TTY if (q && !result['prompt']) { - const hasExplicitInteractive = - result['promptInteractive'] === '' || !!result['promptInteractive']; - if (hasExplicitInteractive) { + if (process.stdin.isTTY) { + startupMessages.push( + 'Positional arguments now default to interactive mode. To run in non-interactive mode, use the --prompt (-p) flag.', + ); result['promptInteractive'] = q; } else { result['prompt'] = q; @@ -355,6 +359,7 @@ export async function parseArguments( // Keep CliArgs.query as a string for downstream typing (result as Record)['query'] = q || undefined; + (result as Record)['startupMessages'] = startupMessages; // The import format is now only controlled by settings.memoryImportFormat // We no longer accept it as a CLI argument @@ -573,12 +578,12 @@ export async function loadCliConfig( throw err; } - // Interactive mode: explicit -i flag or (TTY + no args + no -p flag) - const hasQuery = !!argv.query; + // -p/--prompt forces non-interactive (headless) mode + // -i/--prompt-interactive forces interactive mode with an initial prompt const interactive = !!argv.promptInteractive || !!argv.experimentalAcp || - (process.stdin.isTTY && !hasQuery && !argv.prompt && !argv.isCommand); + (process.stdin.isTTY && !argv.query && !argv.prompt && !argv.isCommand); const allowedTools = argv.allowedTools || settings.tools?.allowed || []; const allowedToolsSet = new Set(allowedTools); diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx index aad7956142..ff73dcfdfa 100644 --- a/packages/cli/src/gemini.tsx +++ b/packages/cli/src/gemini.tsx @@ -324,6 +324,12 @@ export async function main() { const argv = await parseArguments(settings.merged); parseArgsHandle?.end(); + if (argv.startupMessages) { + argv.startupMessages.forEach((msg) => { + coreEvents.emitFeedback('info', msg); + }); + } + // Check for invalid input combinations early to prevent crashes if (argv.promptInteractive && !process.stdin.isTTY) { writeToStderr(