feat(sessions): add resuming to geminiChat and add CLI flags for session management (#10719)

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
This commit is contained in:
bl-ue
2025-11-10 18:31:00 -07:00
committed by GitHub
parent 51f952e700
commit 6893d27441
21 changed files with 2578 additions and 11 deletions
+30
View File
@@ -451,6 +451,36 @@ describe('parseArguments', () => {
mockConsoleError.mockRestore();
});
it('should throw an error when resuming a session without prompt in non-interactive mode', async () => {
const originalIsTTY = process.stdin.isTTY;
process.stdin.isTTY = false;
process.argv = ['node', 'script.js', '--resume', 'session-id'];
const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => {
throw new Error('process.exit called');
});
const mockConsoleError = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
try {
await expect(parseArguments({} as Settings)).rejects.toThrow(
'process.exit called',
);
expect(mockConsoleError).toHaveBeenCalledWith(
expect.stringContaining(
'When resuming a session, you must provide a message via --prompt (-p) or stdin',
),
);
} finally {
mockExit.mockRestore();
mockConsoleError.mockRestore();
process.stdin.isTTY = originalIsTTY;
}
});
it('should support comma-separated values for --allowed-tools', async () => {
process.argv = [
'node',
+39
View File
@@ -61,6 +61,9 @@ export interface CliArgs {
experimentalAcp: boolean | undefined;
extensions: string[] | undefined;
listExtensions: boolean | undefined;
resume: string | 'latest' | undefined;
listSessions: boolean | undefined;
deleteSession: string | undefined;
includeDirectories: string[] | undefined;
screenReader: boolean | undefined;
useSmartEdit: boolean | undefined;
@@ -172,6 +175,35 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
type: 'boolean',
description: 'List all available extensions and exit.',
})
.option('resume', {
alias: 'r',
type: 'string',
// `skipValidation` so that we can distinguish between it being passed with a value, without
// one, and not being passed at all.
skipValidation: true,
description:
'Resume a previous session. Use "latest" for most recent or index number (e.g. --resume 5)',
coerce: (value: string): string => {
// When --resume passed with a value (`gemini --resume 123`): value = "123" (string)
// When --resume passed without a value (`gemini --resume`): value = "" (string)
// When --resume not passed at all: this `coerce` function is not called at all, and
// `yargsInstance.argv.resume` is undefined.
if (value === '') {
return 'latest';
}
return value;
},
})
.option('list-sessions', {
type: 'boolean',
description:
'List available sessions for the current project and exit.',
})
.option('delete-session', {
type: 'string',
description:
'Delete a session by index number (use --list-sessions to see available sessions).',
})
.option('include-directories', {
type: 'array',
string: true,
@@ -227,6 +259,11 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
if (argv['prompt'] && argv['promptInteractive']) {
return 'Cannot use both --prompt (-p) and --prompt-interactive (-i) together';
}
if (argv.resume && !argv.prompt && !process.stdin.isTTY) {
throw new Error(
'When resuming a session, you must provide a message via --prompt (-p) or stdin',
);
}
if (argv.yolo && argv['approvalMode']) {
return 'Cannot use both --yolo (-y) and --approval-mode together. Use --approval-mode=yolo instead.';
}
@@ -585,6 +622,8 @@ export async function loadCliConfig(
maxSessionTurns: settings.model?.maxSessionTurns ?? -1,
experimentalZedIntegration: argv.experimentalAcp || false,
listExtensions: argv.listExtensions || false,
listSessions: argv.listSessions || false,
deleteSession: argv.deleteSession,
enabledExtensions: argv.extensions,
extensionLoader: extensionManager,
enableExtensionReloading: settings.experimental?.extensionReloading,