fix(cli)!: Default to interactive mode for positional arguments (#16329)

Co-authored-by: Allen Hutchison <adh@google.com>
This commit is contained in:
Ishaan Gupta
2026-01-23 05:08:53 +05:30
committed by GitHub
parent a060e6149a
commit beacc4f6fd
3 changed files with 53 additions and 24 deletions
+32 -14
View File
@@ -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([ it.each([
@@ -1953,7 +1968,7 @@ describe('loadCliConfig interactive', () => {
expect(config.isInteractive()).toBe(false); 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.stdin.isTTY = true;
process.argv = ['node', 'script.js', '--model', 'gemini-2.5-pro', 'Hello']; process.argv = ['node', 'script.js', '--model', 'gemini-2.5-pro', 'Hello'];
const argv = await parseArguments(createTestMergedSettings()); const argv = await parseArguments(createTestMergedSettings());
@@ -1962,10 +1977,10 @@ describe('loadCliConfig interactive', () => {
'test-session', 'test-session',
argv, 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.stdin.isTTY = true;
process.argv = [ process.argv = [
'node', 'node',
@@ -1981,13 +1996,13 @@ describe('loadCliConfig interactive', () => {
'test-session', 'test-session',
argv, argv,
); );
expect(config.isInteractive()).toBe(false); expect(config.isInteractive()).toBe(true);
// Verify the question is preserved for one-shot execution // Verify the question is preserved for one-shot execution
expect(argv.prompt).toBe('Hello world'); expect(argv.prompt).toBeUndefined();
expect(argv.promptInteractive).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.stdin.isTTY = true;
process.argv = ['node', 'script.js', '-e', 'none', 'hello']; process.argv = ['node', 'script.js', '-e', 'none', 'hello'];
const argv = await parseArguments(createTestMergedSettings()); const argv = await parseArguments(createTestMergedSettings());
@@ -1996,8 +2011,9 @@ describe('loadCliConfig interactive', () => {
'test-session', 'test-session',
argv, argv,
); );
expect(config.isInteractive()).toBe(false); expect(config.isInteractive()).toBe(true);
expect(argv.query).toBe('hello'); expect(argv.query).toBe('hello');
expect(argv.promptInteractive).toBe('hello');
expect(argv.extensions).toEqual(['none']); expect(argv.extensions).toEqual(['none']);
}); });
@@ -2010,9 +2026,9 @@ describe('loadCliConfig interactive', () => {
'test-session', 'test-session',
argv, argv,
); );
expect(config.isInteractive()).toBe(false); expect(config.isInteractive()).toBe(true);
expect(argv.query).toBe('hello world how are you'); 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 () => { it('should handle multiple positional words with flags', async () => {
@@ -2035,8 +2051,9 @@ describe('loadCliConfig interactive', () => {
'test-session', 'test-session',
argv, argv,
); );
expect(config.isInteractive()).toBe(false); expect(config.isInteractive()).toBe(true);
expect(argv.query).toBe('write a function to sort array'); 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'); expect(argv.model).toBe('gemini-2.5-pro');
}); });
@@ -2072,8 +2089,9 @@ describe('loadCliConfig interactive', () => {
'test-session', 'test-session',
argv, argv,
); );
expect(config.isInteractive()).toBe(false); expect(config.isInteractive()).toBe(true);
expect(argv.query).toBe('hello world how are you'); expect(argv.query).toBe('hello world how are you');
expect(argv.promptInteractive).toBe('hello world how are you');
expect(argv.extensions).toEqual(['none']); expect(argv.extensions).toEqual(['none']);
}); });
@@ -2708,9 +2726,9 @@ describe('PolicyEngine nonInteractive wiring', () => {
vi.restoreAllMocks(); 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.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 argv = await parseArguments(createTestMergedSettings());
const config = await loadCliConfig( const config = await loadCliConfig(
createTestMergedSettings(), createTestMergedSettings(),
+15 -10
View File
@@ -84,6 +84,7 @@ export interface CliArgs {
outputFormat: string | undefined; outputFormat: string | undefined;
fakeResponses: string | undefined; fakeResponses: string | undefined;
recordResponses: string | undefined; recordResponses: string | undefined;
startupMessages?: string[];
rawOutput: boolean | undefined; rawOutput: boolean | undefined;
acceptRawOutputRisk: boolean | undefined; acceptRawOutputRisk: boolean | undefined;
isCommand: boolean | undefined; isCommand: boolean | undefined;
@@ -93,11 +94,12 @@ export async function parseArguments(
settings: MergedSettings, settings: MergedSettings,
): Promise<CliArgs> { ): Promise<CliArgs> {
const rawArgv = hideBin(process.argv); const rawArgv = hideBin(process.argv);
const startupMessages: string[] = [];
const yargsInstance = yargs(rawArgv) const yargsInstance = yargs(rawArgv)
.locale('en') .locale('en')
.scriptName('gemini') .scriptName('gemini')
.usage( .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', { .option('debug', {
alias: 'd', alias: 'd',
@@ -109,7 +111,7 @@ export async function parseArguments(
yargsInstance yargsInstance
.positional('query', { .positional('query', {
description: 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', { .option('model', {
alias: 'm', alias: 'm',
@@ -121,7 +123,8 @@ export async function parseArguments(
alias: 'p', alias: 'p',
type: 'string', type: 'string',
nargs: 1, 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', { .option('prompt-interactive', {
alias: 'i', alias: 'i',
@@ -342,11 +345,12 @@ export async function parseArguments(
? queryArg.join(' ') ? queryArg.join(' ')
: queryArg; : 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']) { if (q && !result['prompt']) {
const hasExplicitInteractive = if (process.stdin.isTTY) {
result['promptInteractive'] === '' || !!result['promptInteractive']; startupMessages.push(
if (hasExplicitInteractive) { 'Positional arguments now default to interactive mode. To run in non-interactive mode, use the --prompt (-p) flag.',
);
result['promptInteractive'] = q; result['promptInteractive'] = q;
} else { } else {
result['prompt'] = q; result['prompt'] = q;
@@ -355,6 +359,7 @@ export async function parseArguments(
// Keep CliArgs.query as a string for downstream typing // Keep CliArgs.query as a string for downstream typing
(result as Record<string, unknown>)['query'] = q || undefined; (result as Record<string, unknown>)['query'] = q || undefined;
(result as Record<string, unknown>)['startupMessages'] = startupMessages;
// The import format is now only controlled by settings.memoryImportFormat // The import format is now only controlled by settings.memoryImportFormat
// We no longer accept it as a CLI argument // We no longer accept it as a CLI argument
@@ -573,12 +578,12 @@ export async function loadCliConfig(
throw err; throw err;
} }
// Interactive mode: explicit -i flag or (TTY + no args + no -p flag) // -p/--prompt forces non-interactive (headless) mode
const hasQuery = !!argv.query; // -i/--prompt-interactive forces interactive mode with an initial prompt
const interactive = const interactive =
!!argv.promptInteractive || !!argv.promptInteractive ||
!!argv.experimentalAcp || !!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 allowedTools = argv.allowedTools || settings.tools?.allowed || [];
const allowedToolsSet = new Set(allowedTools); const allowedToolsSet = new Set(allowedTools);
+6
View File
@@ -324,6 +324,12 @@ export async function main() {
const argv = await parseArguments(settings.merged); const argv = await parseArguments(settings.merged);
parseArgsHandle?.end(); parseArgsHandle?.end();
if (argv.startupMessages) {
argv.startupMessages.forEach((msg) => {
coreEvents.emitFeedback('info', msg);
});
}
// Check for invalid input combinations early to prevent crashes // Check for invalid input combinations early to prevent crashes
if (argv.promptInteractive && !process.stdin.isTTY) { if (argv.promptInteractive && !process.stdin.isTTY) {
writeToStderr( writeToStderr(