From 6be034392f3f4753c7a49105bfc6266d69698b21 Mon Sep 17 00:00:00 2001 From: Niyas Hameed Date: Tue, 23 Dec 2025 19:53:43 +0530 Subject: [PATCH] feat: automatic `/model` persistence across Gemini CLI sessions (#13199) Co-authored-by: Jack Wotherspoon --- packages/cli/src/config/config.test.ts | 6 ++---- packages/cli/src/config/config.ts | 4 ++++ packages/cli/src/config/settings.ts | 15 +++++++++++++++ .../cli/src/ui/components/ModelDialog.test.tsx | 3 +++ packages/cli/src/ui/components/ModelDialog.tsx | 8 ++++++-- packages/core/src/config/config.test.ts | 12 ++++++++++++ packages/core/src/config/config.ts | 6 ++++++ 7 files changed, 48 insertions(+), 6 deletions(-) diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts index ff8be73536..7748c1869a 100644 --- a/packages/cli/src/config/config.test.ts +++ b/packages/cli/src/config/config.test.ts @@ -25,13 +25,11 @@ import { ExtensionManager } from './extension-manager.js'; import { RESUME_LATEST } from '../utils/sessionUtils.js'; vi.mock('./trustedFolders.js', () => ({ - isWorkspaceTrusted: vi - .fn() - .mockReturnValue({ isTrusted: true, source: 'file' }), // Default to trusted + isWorkspaceTrusted: vi.fn(() => ({ isTrusted: true, source: 'file' })), // Default to trusted })); vi.mock('./sandboxConfig.js', () => ({ - loadSandboxConfig: vi.fn().mockResolvedValue(undefined), + loadSandboxConfig: vi.fn(async () => undefined), })); vi.mock('fs', async (importOriginal) => { diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index 8b0f3aeeca..3cb133fb0d 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -35,6 +35,7 @@ import { PREVIEW_GEMINI_MODEL_AUTO, } from '@google/gemini-cli-core'; import type { Settings } from './settings.js'; +import { saveModelChange, loadSettings } from './settings.js'; import { loadSandboxConfig } from './sandboxConfig.js'; import { resolvePath } from '../utils/resolvePath.js'; @@ -387,6 +388,8 @@ export async function loadCliConfig( ): Promise { const debugMode = isDebugMode(argv); + const loadedSettings = loadSettings(cwd); + if (argv.sandbox) { process.env['GEMINI_SANDBOX'] = 'true'; } @@ -693,6 +696,7 @@ export async function loadCliConfig( // TODO: loading of hooks based on workspace trust enableHooks: settings.tools?.enableHooks ?? false, hooks: settings.hooks || {}, + onModelChange: (model: string) => saveModelChange(loadedSettings, model), }); } diff --git a/packages/cli/src/config/settings.ts b/packages/cli/src/config/settings.ts index 3e41fbe87e..5506ff8dba 100644 --- a/packages/cli/src/config/settings.ts +++ b/packages/cli/src/config/settings.ts @@ -874,3 +874,18 @@ export function saveSettings(settingsFile: SettingsFile): void { ); } } + +export function saveModelChange( + loadedSettings: LoadedSettings, + model: string, +): void { + try { + loadedSettings.setValue(SettingScope.User, 'model.name', model); + } catch (error) { + coreEvents.emitFeedback( + 'error', + 'There was an error saving your preferred model.', + error, + ); + } +} diff --git a/packages/cli/src/ui/components/ModelDialog.test.tsx b/packages/cli/src/ui/components/ModelDialog.test.tsx index 390be01b74..f73f1cb012 100644 --- a/packages/cli/src/ui/components/ModelDialog.test.tsx +++ b/packages/cli/src/ui/components/ModelDialog.test.tsx @@ -89,6 +89,9 @@ describe('', () => { it('renders the initial "main" view correctly', () => { const { lastFrame } = renderComponent(); expect(lastFrame()).toContain('Select Model'); + expect(lastFrame()).toContain( + 'Applies to this session and future Gemini CLI sessions.', + ); expect(lastFrame()).toContain('Auto'); expect(lastFrame()).toContain('Manual'); }); diff --git a/packages/cli/src/ui/components/ModelDialog.tsx b/packages/cli/src/ui/components/ModelDialog.tsx index 5be22beda8..fa5af46390 100644 --- a/packages/cli/src/ui/components/ModelDialog.tsx +++ b/packages/cli/src/ui/components/ModelDialog.tsx @@ -204,7 +204,6 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element { )} {subheader && {subheader}} - - {'To use a specific Gemini model on startup, use the --model flag.'} + Applies to this session and future Gemini CLI sessions. + + + + + {'> To use a specific Gemini model on startup, use the --model flag.'} diff --git a/packages/core/src/config/config.test.ts b/packages/core/src/config/config.test.ts index ebe8295b69..03414a3da2 100644 --- a/packages/core/src/config/config.test.ts +++ b/packages/core/src/config/config.test.ts @@ -1629,6 +1629,18 @@ describe('Config getHooks', () => { expect(config.getModel()).toBe(originalModel); expect(config.getActiveModel()).toBe(originalModel); }); + + it('should call onModelChange when a new model is set', () => { + const onModelChange = vi.fn(); + const config = new Config({ + ...baseParams, + onModelChange, + }); + + config.setModel(DEFAULT_GEMINI_MODEL); + + expect(onModelChange).toHaveBeenCalledWith(DEFAULT_GEMINI_MODEL); + }); }); }); diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index fcc16c1e65..b29b54476c 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -337,6 +337,7 @@ export interface ConfigParameters { previewFeatures?: boolean; enableAgents?: boolean; experimentalJitContext?: boolean; + onModelChange?: (model: string) => void; } export class Config { @@ -457,6 +458,7 @@ export class Config { private experiments: Experiments | undefined; private experimentsPromise: Promise | undefined; private hookSystem?: HookSystem; + private readonly onModelChange: ((model: string) => void) | undefined; private readonly enableAgents: boolean; @@ -623,6 +625,7 @@ export class Config { this.disableYoloMode = params.disableYoloMode ?? false; this.hooks = params.hooks; this.experiments = params.experiments; + this.onModelChange = params.onModelChange; if (params.contextFileName) { setGeminiMdFilename(params.contextFileName); @@ -879,6 +882,9 @@ export class Config { // When the user explicitly sets a model, that becomes the active model. this._activeModel = newModel; coreEvents.emitModelChanged(newModel); + if (this.onModelChange) { + this.onModelChange(newModel); + } } this.modelAvailabilityService.reset(); }