Opt-in to persist model from /model (#15820)

This commit is contained in:
Sehoon Shon
2026-01-05 15:31:13 -05:00
committed by GitHub
parent b13c6b57ae
commit e5d183031a
4 changed files with 49 additions and 16 deletions

View File

@@ -47,7 +47,7 @@ describe('<ModelDialog />', () => {
const mockGetHasAccessToPreviewModel = vi.fn();
interface MockConfig extends Partial<Config> {
setModel: (model: string) => void;
setModel: (model: string, isTemporary?: boolean) => void;
getModel: () => string;
getPreviewFeatures: () => boolean;
getHasAccessToPreviewModel: () => boolean;
@@ -89,9 +89,7 @@ describe('<ModelDialog />', () => {
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('<ModelDialog />', () => {
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('<ModelDialog />', () => {
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();
});

View File

@@ -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 {
/>
</Box>
<Box marginTop={1} flexDirection="column">
<Text color={theme.text.secondary}>
Applies to this session and future Gemini CLI sessions.
</Text>
<Box>
<Text color={theme.text.primary}>
Remember model for future sessions:{' '}
</Text>
<Text color={theme.text.secondary}>
{persistMode ? 'true' : 'false'}
</Text>
</Box>
<Text color={theme.text.secondary}>(Press Tab to toggle)</Text>
</Box>
<Box marginTop={1} flexDirection="column">
<Text color={theme.text.secondary}>

View File

@@ -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,

View File

@@ -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);
}
}