diff --git a/packages/cli/src/ui/components/ModelDialog.test.tsx b/packages/cli/src/ui/components/ModelDialog.test.tsx index f73f1cb012..fbfddbfad1 100644 --- a/packages/cli/src/ui/components/ModelDialog.test.tsx +++ b/packages/cli/src/ui/components/ModelDialog.test.tsx @@ -47,7 +47,7 @@ describe('', () => { const mockGetHasAccessToPreviewModel = vi.fn(); interface MockConfig extends Partial { - setModel: (model: string) => void; + setModel: (model: string, isTemporary?: boolean) => void; getModel: () => string; getPreviewFeatures: () => boolean; getHasAccessToPreviewModel: () => boolean; @@ -89,9 +89,7 @@ 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('Remember model for future sessions: false'); expect(lastFrame()).toContain('Auto'); expect(lastFrame()).toContain('Manual'); }); @@ -148,7 +146,10 @@ describe('', () => { stdin.write('\r'); await waitForUpdate(); - expect(mockSetModel).toHaveBeenCalledWith(DEFAULT_GEMINI_MODEL_AUTO); + expect(mockSetModel).toHaveBeenCalledWith( + DEFAULT_GEMINI_MODEL_AUTO, + true, // Session only by default + ); expect(mockOnClose).toHaveBeenCalled(); }); @@ -165,7 +166,29 @@ describe('', () => { stdin.write('\r'); await waitForUpdate(); - expect(mockSetModel).toHaveBeenCalledWith(DEFAULT_GEMINI_MODEL); + expect(mockSetModel).toHaveBeenCalledWith(DEFAULT_GEMINI_MODEL, true); + expect(mockOnClose).toHaveBeenCalled(); + }); + + it('toggles persist mode with Tab key', async () => { + const { lastFrame, stdin } = renderComponent(); + + expect(lastFrame()).toContain('Remember model for future sessions: false'); + + // Press Tab to toggle persist mode + stdin.write('\t'); + await waitForUpdate(); + + expect(lastFrame()).toContain('Remember model for future sessions: true'); + + // Select "Auto" (index 0) + stdin.write('\r'); + await waitForUpdate(); + + expect(mockSetModel).toHaveBeenCalledWith( + DEFAULT_GEMINI_MODEL_AUTO, + false, // Persist enabled + ); expect(mockOnClose).toHaveBeenCalled(); }); diff --git a/packages/cli/src/ui/components/ModelDialog.tsx b/packages/cli/src/ui/components/ModelDialog.tsx index fa5af46390..d1c12af8ce 100644 --- a/packages/cli/src/ui/components/ModelDialog.tsx +++ b/packages/cli/src/ui/components/ModelDialog.tsx @@ -32,6 +32,7 @@ interface ModelDialogProps { export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element { const config = useContext(ConfigContext); const [view, setView] = useState<'main' | 'manual'>('main'); + const [persistMode, setPersistMode] = useState(false); // Determine the Preferred Model (read once when the dialog opens). const preferredModel = config?.getModel() || DEFAULT_GEMINI_MODEL_AUTO; @@ -62,6 +63,9 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element { onClose(); } } + if (key.name === 'tab') { + setPersistMode((prev) => !prev); + } }, { isActive: true }, ); @@ -157,13 +161,13 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element { } if (config) { - config.setModel(model); + config.setModel(model, persistMode ? false : true); const event = new ModelSlashCommandEvent(model); logModelSlashCommand(config, event); } onClose(); }, - [config, onClose], + [config, onClose, persistMode], ); let header; @@ -213,9 +217,15 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element { /> - - Applies to this session and future Gemini CLI sessions. - + + + Remember model for future sessions:{' '} + + + {persistMode ? 'true' : 'false'} + + + (Press Tab to toggle) diff --git a/packages/core/src/config/config.test.ts b/packages/core/src/config/config.test.ts index 1f389cfaa0..ea357d690a 100644 --- a/packages/core/src/config/config.test.ts +++ b/packages/core/src/config/config.test.ts @@ -1630,19 +1630,19 @@ describe('Config getHooks', () => { expect(config.getActiveModel()).toBe(originalModel); }); - it('should call onModelChange when a new model is set', () => { + it('should call onModelChange when a new model is set and should persist', () => { const onModelChange = vi.fn(); const config = new Config({ ...baseParams, onModelChange, }); - config.setModel(DEFAULT_GEMINI_MODEL); + config.setModel(DEFAULT_GEMINI_MODEL, false); expect(onModelChange).toHaveBeenCalledWith(DEFAULT_GEMINI_MODEL); }); - it('should NOT call onModelChange when a new model is set as a fallback', () => { + it('should NOT call onModelChange when a new model is temporary', () => { const onModelChange = vi.fn(); const config = new Config({ ...baseParams, diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index f295cda720..3d9aba2bb8 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -909,13 +909,13 @@ export class Config { return this.model; } - setModel(newModel: string, isFallbackModel: boolean = false): void { + setModel(newModel: string, isTemporary: boolean = true): void { if (this.model !== newModel || this._activeModel !== newModel) { this.model = newModel; // When the user explicitly sets a model, that becomes the active model. this._activeModel = newModel; coreEvents.emitModelChanged(newModel); - if (this.onModelChange && !isFallbackModel) { + if (this.onModelChange && !isTemporary) { this.onModelChange(newModel); } }