feat: promote Gemini 3.1 Flash Lite to GA and support Gemini 3.5 Flash

This commit is contained in:
Sri Pasumarthi
2026-06-05 11:30:34 -07:00
parent a57f33acbc
commit eb42ff6086
78 changed files with 2658 additions and 705 deletions
+6 -11
View File
@@ -19,6 +19,7 @@ import type * as acp from '@agentclientprotocol/sdk';
import {
AuthType,
type Config,
GEMINI_MODEL_ALIAS_AUTO,
type MessageBus,
type Storage,
} from '@google/gemini-cli-core';
@@ -208,20 +209,19 @@ describe('AcpSessionManager', () => {
expect(response.models?.availableModels).toEqual(
expect.arrayContaining([
expect.objectContaining({
modelId: 'auto-gemini-3',
modelId: GEMINI_MODEL_ALIAS_AUTO,
name: expect.stringContaining('Auto'),
}),
]),
);
});
it('should include gemini-3.1-flash-lite when useGemini31FlashLite is true', async () => {
it('should NOT include retired preview models (none) in available models', async () => {
mockConfig.getContentGeneratorConfig = vi.fn().mockReturnValue({
apiKey: 'test-key',
});
mockConfig.getHasAccessToPreviewModel = vi.fn().mockReturnValue(true);
mockConfig.getGemini31LaunchedSync = vi.fn().mockReturnValue(true);
mockConfig.getGemini31FlashLiteLaunchedSync = vi.fn().mockReturnValue(true);
const response = await manager.newSession(
{
@@ -231,14 +231,9 @@ describe('AcpSessionManager', () => {
{},
);
expect(response.models?.availableModels).toEqual(
expect.arrayContaining([
expect.objectContaining({
modelId: 'gemini-3.1-flash-lite',
name: 'gemini-3.1-flash-lite',
}),
]),
);
const modelIds =
response.models?.availableModels?.map((m) => m.modelId) ?? [];
expect(modelIds).not.toContain('none');
});
it('should return modes with plan mode when plan is enabled', async () => {
+28 -31
View File
@@ -10,8 +10,7 @@ import {
type ToolCallConfirmationDetails,
Kind,
ApprovalMode,
DEFAULT_GEMINI_MODEL_AUTO,
PREVIEW_GEMINI_MODEL_AUTO,
GEMINI_MODEL_ALIAS_AUTO,
DEFAULT_GEMINI_MODEL,
DEFAULT_GEMINI_FLASH_MODEL,
DEFAULT_GEMINI_FLASH_LITE_MODEL,
@@ -19,10 +18,11 @@ import {
PREVIEW_GEMINI_MODEL,
PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL,
PREVIEW_GEMINI_FLASH_MODEL,
GEMINI_3_1_FLASH_LITE_MODEL,
PREVIEW_GEMINI_FLASH_LITE_MODEL,
getDisplayString,
AuthType,
ToolConfirmationOutcome,
getAutoModelDescription,
} from '@google/gemini-cli-core';
import type * as acp from '@agentclientprotocol/sdk';
import { z } from 'zod';
@@ -262,11 +262,10 @@ export function buildAvailableModels(
}>;
currentModelId: string;
} {
const preferredModel = config.getModel() || DEFAULT_GEMINI_MODEL_AUTO;
const preferredModel = config.getModel() || GEMINI_MODEL_ALIAS_AUTO;
const shouldShowPreviewModels = config.getHasAccessToPreviewModel();
const useGemini31 = config.getGemini31LaunchedSync?.() ?? false;
const useGemini31FlashLite =
config.getGemini31FlashLiteLaunchedSync?.() ?? false;
const useGemini3_5Flash = config.hasGemini35FlashGAAccess?.() ?? false;
const selectedAuthType = settings.merged.security.auth.selectedType;
const useCustomToolModel =
useGemini31 && selectedAuthType === AuthType.USE_GEMINI;
@@ -278,7 +277,7 @@ export function buildAvailableModels(
) {
const options = config.getModelConfigService().getAvailableModelOptions({
useGemini3_1: useGemini31,
useGemini3_1FlashLite: useGemini31FlashLite,
useGemini3_5Flash,
useCustomTools: useCustomToolModel,
hasAccessToPreview: shouldShowPreviewModels,
});
@@ -290,37 +289,35 @@ export function buildAvailableModels(
}
// --- LEGACY PATH ---
const defaultFlashModel =
config.getDefaultGeminiFlashModel?.() ?? DEFAULT_GEMINI_FLASH_MODEL;
const previewFlashModel =
config.getPreviewGeminiFlashModel?.() ?? PREVIEW_GEMINI_FLASH_MODEL;
const mainOptions = [
{
value: DEFAULT_GEMINI_MODEL_AUTO,
title: getDisplayString(DEFAULT_GEMINI_MODEL_AUTO),
description:
'Let Gemini CLI decide the best model for the task: gemini-2.5-pro, gemini-2.5-flash',
value: GEMINI_MODEL_ALIAS_AUTO,
title: getDisplayString(GEMINI_MODEL_ALIAS_AUTO, config),
description: getAutoModelDescription(
shouldShowPreviewModels,
useGemini31,
useGemini3_5Flash,
),
},
];
if (shouldShowPreviewModels) {
mainOptions.unshift({
value: PREVIEW_GEMINI_MODEL_AUTO,
title: getDisplayString(PREVIEW_GEMINI_MODEL_AUTO),
description: useGemini31
? 'Let Gemini CLI decide the best model for the task: gemini-3.1-pro, gemini-3-flash'
: 'Let Gemini CLI decide the best model for the task: gemini-3-pro, gemini-3-flash',
});
}
const manualOptions = [
{
value: DEFAULT_GEMINI_MODEL,
title: getDisplayString(DEFAULT_GEMINI_MODEL),
title: getDisplayString(DEFAULT_GEMINI_MODEL, config),
},
{
value: DEFAULT_GEMINI_FLASH_MODEL,
title: getDisplayString(DEFAULT_GEMINI_FLASH_MODEL),
value: defaultFlashModel,
title: getDisplayString(defaultFlashModel, config),
},
{
value: DEFAULT_GEMINI_FLASH_LITE_MODEL,
title: getDisplayString(DEFAULT_GEMINI_FLASH_LITE_MODEL),
title: getDisplayString(DEFAULT_GEMINI_FLASH_LITE_MODEL, config),
},
];
@@ -336,18 +333,18 @@ export function buildAvailableModels(
const previewOptions = [
{
value: previewProValue,
title: getDisplayString(previewProModel),
title: getDisplayString(previewProModel, config),
},
{
value: PREVIEW_GEMINI_FLASH_MODEL,
title: getDisplayString(PREVIEW_GEMINI_FLASH_MODEL),
value: previewFlashModel,
title: getDisplayString(previewFlashModel, config),
},
];
if (useGemini31FlashLite) {
if (PREVIEW_GEMINI_FLASH_LITE_MODEL !== 'none') {
previewOptions.push({
value: GEMINI_3_1_FLASH_LITE_MODEL,
title: getDisplayString(GEMINI_3_1_FLASH_LITE_MODEL),
value: PREVIEW_GEMINI_FLASH_LITE_MODEL,
title: getDisplayString(PREVIEW_GEMINI_FLASH_LITE_MODEL, config),
});
}
@@ -12,14 +12,14 @@ import { waitFor } from '../../test-utils/async.js';
import { createMockSettings } from '../../test-utils/settings.js';
import {
DEFAULT_GEMINI_MODEL,
DEFAULT_GEMINI_MODEL_AUTO,
GEMINI_MODEL_ALIAS_AUTO,
DEFAULT_GEMINI_FLASH_MODEL,
DEFAULT_GEMINI_FLASH_LITE_MODEL,
PREVIEW_GEMINI_MODEL,
PREVIEW_GEMINI_3_1_MODEL,
PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL,
PREVIEW_GEMINI_FLASH_MODEL,
GEMINI_3_1_FLASH_LITE_MODEL,
PREVIEW_GEMINI_FLASH_LITE_MODEL,
AuthType,
} from '@google/gemini-cli-core';
import type { Config, ModelSlashCommandEvent } from '@google/gemini-cli-core';
@@ -34,6 +34,11 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
getAutoModelDescription: (
hasAccessToPreview: boolean,
useGemini3_1?: boolean,
) =>
`Auto Model Description (preview: ${hasAccessToPreview}, 3.1: ${useGemini3_1})`,
getDisplayString: (val: string) => mockGetDisplayString(val),
logModelSlashCommand: (config: Config, event: ModelSlashCommandEvent) =>
mockLogModelSlashCommand(config, event),
@@ -42,8 +47,7 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
mockModelSlashCommandEvent(model);
}
},
PREVIEW_GEMINI_3_1_FLASH_LITE_MODEL: 'gemini-3.1-flash-lite-preview',
GEMINI_3_1_FLASH_LITE_MODEL: 'gemini-3.1-flash-lite',
PREVIEW_GEMINI_FLASH_LITE_MODEL: 'none',
};
});
@@ -63,7 +67,6 @@ describe('<ModelDialog />', () => {
getHasAccessToPreviewModel: () => boolean;
getIdeMode: () => boolean;
getGemini31LaunchedSync: () => boolean;
getGemini31FlashLiteLaunchedSync: () => boolean;
getProModelNoAccess: () => Promise<boolean>;
getProModelNoAccessSync: () => boolean;
getExperimentalGemma: () => boolean;
@@ -84,7 +87,6 @@ describe('<ModelDialog />', () => {
getHasAccessToPreviewModel: mockGetHasAccessToPreviewModel,
getIdeMode: () => false,
getGemini31LaunchedSync: mockGetGemini31LaunchedSync,
getGemini31FlashLiteLaunchedSync: mockGetGemini31FlashLiteLaunchedSync,
getProModelNoAccess: mockGetProModelNoAccess,
getProModelNoAccessSync: mockGetProModelNoAccessSync,
getExperimentalGemma: () => false,
@@ -94,17 +96,15 @@ describe('<ModelDialog />', () => {
beforeEach(() => {
vi.resetAllMocks();
mockGetModel.mockReturnValue(DEFAULT_GEMINI_MODEL_AUTO);
mockGetModel.mockReturnValue(GEMINI_MODEL_ALIAS_AUTO);
mockGetHasAccessToPreviewModel.mockReturnValue(false);
mockGetGemini31LaunchedSync.mockReturnValue(false);
mockGetGemini31FlashLiteLaunchedSync.mockReturnValue(false);
mockGetProModelNoAccess.mockResolvedValue(false);
mockGetProModelNoAccessSync.mockReturnValue(false);
// Default implementation for getDisplayString
mockGetDisplayString.mockImplementation((val: string) => {
if (val === 'auto-gemini-2.5') return 'Auto (Gemini 2.5)';
if (val === 'auto-gemini-3') return 'Auto (Preview)';
if (val === 'auto') return 'Auto';
return val;
});
});
@@ -154,15 +154,13 @@ describe('<ModelDialog />', () => {
expect(output).not.toContain(DEFAULT_GEMINI_MODEL);
expect(output).not.toContain(PREVIEW_GEMINI_MODEL);
// Verify order: Flash Preview -> Flash Lite Preview -> Flash -> Flash Lite
// Verify order: Flash Preview -> Flash Lite (Preview/Default) -> Flash
const flashPreviewIdx = output.indexOf(PREVIEW_GEMINI_FLASH_MODEL);
const flashLitePreviewIdx = output.indexOf(GEMINI_3_1_FLASH_LITE_MODEL);
const flashIdx = output.indexOf(DEFAULT_GEMINI_FLASH_MODEL);
const flashLiteIdx = output.indexOf(DEFAULT_GEMINI_FLASH_LITE_MODEL);
const flashIdx = output.indexOf(DEFAULT_GEMINI_FLASH_MODEL);
expect(flashPreviewIdx).toBeLessThan(flashLitePreviewIdx);
expect(flashLitePreviewIdx).toBeLessThan(flashIdx);
expect(flashIdx).toBeLessThan(flashLiteIdx);
expect(flashPreviewIdx).toBeLessThan(flashLiteIdx);
expect(flashLiteIdx).toBeLessThan(flashIdx);
expect(output).not.toContain('Auto');
unmount();
@@ -233,7 +231,7 @@ describe('<ModelDialog />', () => {
await waitFor(() => {
expect(mockSetModel).toHaveBeenCalledWith(
DEFAULT_GEMINI_MODEL_AUTO,
GEMINI_MODEL_ALIAS_AUTO,
true, // Session only by default
);
expect(mockOnClose).toHaveBeenCalled();
@@ -291,7 +289,7 @@ describe('<ModelDialog />', () => {
await waitFor(() => {
expect(mockSetModel).toHaveBeenCalledWith(
DEFAULT_GEMINI_MODEL_AUTO,
GEMINI_MODEL_ALIAS_AUTO,
false, // Persist enabled
);
expect(mockOnClose).toHaveBeenCalled();
@@ -354,7 +352,7 @@ describe('<ModelDialog />', () => {
mockGetModel.mockReturnValue(DEFAULT_GEMINI_MODEL);
mockGetDisplayString.mockImplementation((val: string) => {
if (val === DEFAULT_GEMINI_MODEL) return 'My Custom Model Display';
if (val === 'auto-gemini-2.5') return 'Auto (Gemini 2.5)';
if (val === 'auto') return 'Auto';
return val;
});
const { lastFrame, unmount } = await renderComponent();
@@ -368,9 +366,9 @@ describe('<ModelDialog />', () => {
mockGetHasAccessToPreviewModel.mockReturnValue(true);
});
it('shows Auto (Preview) in main view when access is granted', async () => {
it('shows Auto in main view when access is granted', async () => {
const { lastFrame, unmount } = await renderComponent();
expect(lastFrame()).toContain('Auto (Preview)');
expect(lastFrame()).toContain('Auto');
unmount();
});
@@ -448,7 +446,7 @@ describe('<ModelDialog />', () => {
unmount();
});
it('shows Flash Lite Preview model regardless of tier when flag is enabled', async () => {
it('does not show Flash Lite Preview model when it is retired (none) even if flag is enabled', async () => {
mockGetProModelNoAccessSync.mockReturnValue(false);
mockGetProModelNoAccess.mockResolvedValue(false);
mockGetHasAccessToPreviewModel.mockReturnValue(true);
@@ -467,7 +465,8 @@ describe('<ModelDialog />', () => {
await waitUntilReady();
const output = lastFrame();
expect(output).toContain(GEMINI_3_1_FLASH_LITE_MODEL);
expect(output).not.toContain(PREVIEW_GEMINI_FLASH_LITE_MODEL);
expect(output).toContain(DEFAULT_GEMINI_FLASH_LITE_MODEL);
unmount();
});
});
+68 -51
View File
@@ -13,13 +13,11 @@ import {
PREVIEW_GEMINI_MODEL,
PREVIEW_GEMINI_3_1_MODEL,
PREVIEW_GEMINI_FLASH_MODEL,
PREVIEW_GEMINI_3_1_FLASH_LITE_MODEL,
PREVIEW_GEMINI_MODEL_AUTO,
GEMINI_3_1_FLASH_LITE_MODEL,
PREVIEW_GEMINI_FLASH_LITE_MODEL,
DEFAULT_GEMINI_MODEL,
DEFAULT_GEMINI_FLASH_MODEL,
DEFAULT_GEMINI_FLASH_LITE_MODEL,
DEFAULT_GEMINI_MODEL_AUTO,
GEMINI_MODEL_ALIAS_AUTO,
GEMMA_4_31B_IT_MODEL,
GEMMA_4_26B_A4B_IT_MODEL,
ModelSlashCommandEvent,
@@ -28,6 +26,7 @@ import {
AuthType,
PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL,
isProModel,
getAutoModelDescription,
} from '@google/gemini-cli-core';
import { useKeypress } from '../hooks/useKeypress.js';
import { theme } from '../semantic-colors.js';
@@ -64,12 +63,11 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
}, [config]);
// Determine the Preferred Model (read once when the dialog opens).
const preferredModel = config?.getModel() || DEFAULT_GEMINI_MODEL_AUTO;
const preferredModel = config?.getModel() || GEMINI_MODEL_ALIAS_AUTO;
const shouldShowPreviewModels = config?.getHasAccessToPreviewModel();
const shouldShowPreviewModels = config?.getHasAccessToPreviewModel() ?? false;
const useGemini31 = config?.getGemini31LaunchedSync?.() ?? false;
const useGemini31FlashLite =
config?.getGemini31FlashLiteLaunchedSync?.() ?? false;
const useGemini3_5Flash = config?.hasGemini35FlashGAAccess?.() ?? false;
const selectedAuthType = settings.merged.security.auth.selectedType;
const useCustomToolModel =
useGemini31 && selectedAuthType === AuthType.USE_GEMINI;
@@ -88,17 +86,21 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
: '';
}
const defaultFlashModel =
config?.getDefaultGeminiFlashModel?.() ?? DEFAULT_GEMINI_FLASH_MODEL;
const previewFlashModel =
config?.getPreviewGeminiFlashModel?.() ?? PREVIEW_GEMINI_FLASH_MODEL;
const manualModels = [
DEFAULT_GEMINI_MODEL,
DEFAULT_GEMINI_FLASH_MODEL,
defaultFlashModel,
DEFAULT_GEMINI_FLASH_LITE_MODEL,
PREVIEW_GEMINI_MODEL,
PREVIEW_GEMINI_3_1_MODEL,
PREVIEW_GEMINI_3_1_CUSTOM_TOOLS_MODEL,
PREVIEW_GEMINI_3_1_FLASH_LITE_MODEL,
GEMINI_3_1_FLASH_LITE_MODEL,
PREVIEW_GEMINI_FLASH_MODEL,
];
PREVIEW_GEMINI_FLASH_LITE_MODEL,
previewFlashModel,
].filter((m) => m !== 'none');
if (manualModels.includes(preferredModel)) {
return preferredModel;
}
@@ -123,7 +125,6 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
},
{ isActive: true },
);
const mainOptions = useMemo(() => {
// --- DYNAMIC PATH ---
if (
@@ -134,7 +135,7 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
.getModelConfigService()
.getAvailableModelOptions({
useGemini3_1: useGemini31,
useGemini3_1FlashLite: useGemini31FlashLite,
useGemini3_5Flash,
useCustomTools: useCustomToolModel,
hasAccessToPreview: shouldShowPreviewModels,
hasAccessToProModel,
@@ -163,11 +164,14 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
// --- LEGACY PATH ---
const list = [
{
value: DEFAULT_GEMINI_MODEL_AUTO,
title: getDisplayString(DEFAULT_GEMINI_MODEL_AUTO),
description:
'Let Gemini CLI decide the best model for the task: gemini-2.5-pro, gemini-2.5-flash',
key: DEFAULT_GEMINI_MODEL_AUTO,
value: GEMINI_MODEL_ALIAS_AUTO,
title: getDisplayString(GEMINI_MODEL_ALIAS_AUTO),
description: getAutoModelDescription(
shouldShowPreviewModels,
useGemini31,
useGemini3_5Flash,
),
key: GEMINI_MODEL_ALIAS_AUTO,
},
{
value: 'Manual',
@@ -179,23 +183,13 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
},
];
if (shouldShowPreviewModels) {
list.unshift({
value: PREVIEW_GEMINI_MODEL_AUTO,
title: getDisplayString(PREVIEW_GEMINI_MODEL_AUTO),
description: useGemini31
? 'Let Gemini CLI decide the best model for the task: gemini-3.1-pro, gemini-3-flash'
: 'Let Gemini CLI decide the best model for the task: gemini-3-pro, gemini-3-flash',
key: PREVIEW_GEMINI_MODEL_AUTO,
});
}
return list;
}, [
config,
shouldShowPreviewModels,
manualModelSelected,
useGemini31,
useGemini31FlashLite,
useGemini3_5Flash,
useCustomToolModel,
hasAccessToProModel,
]);
@@ -210,7 +204,7 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
.getModelConfigService()
.getAvailableModelOptions({
useGemini3_1: useGemini31,
useGemini3_1FlashLite: useGemini31FlashLite,
useGemini3_5Flash,
useCustomTools: useCustomToolModel,
hasAccessToPreview: shouldShowPreviewModels,
hasAccessToProModel,
@@ -227,22 +221,29 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
// --- LEGACY PATH ---
const showGemmaModels = config?.getExperimentalGemma() ?? false;
const defaultFlashModel =
config?.getDefaultGeminiFlashModel?.() ?? DEFAULT_GEMINI_FLASH_MODEL;
const previewFlashModel =
config?.getPreviewGeminiFlashModel?.() ?? PREVIEW_GEMINI_FLASH_MODEL;
const options = [
{
value: DEFAULT_GEMINI_MODEL,
title: getDisplayString(DEFAULT_GEMINI_MODEL),
title: getDisplayString(DEFAULT_GEMINI_MODEL, config ?? undefined),
key: DEFAULT_GEMINI_MODEL,
},
{
value: DEFAULT_GEMINI_FLASH_MODEL,
title: getDisplayString(DEFAULT_GEMINI_FLASH_MODEL),
key: DEFAULT_GEMINI_FLASH_MODEL,
value: DEFAULT_GEMINI_FLASH_LITE_MODEL,
title: getDisplayString(
DEFAULT_GEMINI_FLASH_LITE_MODEL,
config ?? undefined,
),
key: DEFAULT_GEMINI_FLASH_LITE_MODEL,
},
{
value: DEFAULT_GEMINI_FLASH_LITE_MODEL,
title: getDisplayString(DEFAULT_GEMINI_FLASH_LITE_MODEL),
key: DEFAULT_GEMINI_FLASH_LITE_MODEL,
value: defaultFlashModel,
title: getDisplayString(defaultFlashModel, config ?? undefined),
key: defaultFlashModel,
},
];
@@ -250,12 +251,15 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
options.push(
{
value: GEMMA_4_31B_IT_MODEL,
title: getDisplayString(GEMMA_4_31B_IT_MODEL),
title: getDisplayString(GEMMA_4_31B_IT_MODEL, config ?? undefined),
key: GEMMA_4_31B_IT_MODEL,
},
{
value: GEMMA_4_26B_A4B_IT_MODEL,
title: getDisplayString(GEMMA_4_26B_A4B_IT_MODEL),
title: getDisplayString(
GEMMA_4_26B_A4B_IT_MODEL,
config ?? undefined,
),
key: GEMMA_4_26B_A4B_IT_MODEL,
},
);
@@ -273,21 +277,24 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
const previewOptions = [
{
value: previewProValue,
title: getDisplayString(previewProModel),
title: getDisplayString(previewProModel, config ?? undefined),
key: previewProModel,
},
{
value: PREVIEW_GEMINI_FLASH_MODEL,
title: getDisplayString(PREVIEW_GEMINI_FLASH_MODEL),
key: PREVIEW_GEMINI_FLASH_MODEL,
value: previewFlashModel,
title: getDisplayString(previewFlashModel, config ?? undefined),
key: previewFlashModel,
},
];
if (useGemini31FlashLite) {
if (PREVIEW_GEMINI_FLASH_LITE_MODEL !== 'none') {
previewOptions.push({
value: GEMINI_3_1_FLASH_LITE_MODEL,
title: getDisplayString(GEMINI_3_1_FLASH_LITE_MODEL),
key: GEMINI_3_1_FLASH_LITE_MODEL,
value: PREVIEW_GEMINI_FLASH_LITE_MODEL,
title: getDisplayString(
PREVIEW_GEMINI_FLASH_LITE_MODEL,
config ?? undefined,
),
key: PREVIEW_GEMINI_FLASH_LITE_MODEL,
});
}
@@ -303,13 +310,23 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
}, [
shouldShowPreviewModels,
useGemini31,
useGemini31FlashLite,
useGemini3_5Flash,
useCustomToolModel,
hasAccessToProModel,
config,
]);
const options = view === 'main' ? mainOptions : manualOptions;
const options = useMemo(() => {
const rawOptions = view === 'main' ? mainOptions : manualOptions;
const seen = new Set<string>();
return rawOptions.filter((option) => {
if (seen.has(option.value)) {
return false;
}
seen.add(option.value);
return true;
});
}, [view, mainOptions, manualOptions]);
// Calculate the initial index based on the preferred model.
const initialIndex = useMemo(() => {
@@ -353,6 +353,49 @@ describe('<ModelStatsDisplay />', () => {
unmount();
});
it('should resolve gemini-3-flash to gemini-3.5-flash via getDisplayString', async () => {
const { lastFrame, unmount } = await renderWithMockedStats({
models: {
'gemini-3-flash': {
api: { totalRequests: 1, totalErrors: 0, totalLatencyMs: 100 },
tokens: {
input: 5,
prompt: 10,
candidates: 20,
total: 30,
cached: 5,
thoughts: 2,
tool: 1,
},
roles: {},
},
},
tools: {
totalCalls: 0,
totalSuccess: 0,
totalFail: 0,
totalDurationMs: 0,
totalDecisions: {
accept: 0,
reject: 0,
modify: 0,
[ToolCallDecision.AUTO_ACCEPT]: 0,
},
byName: {},
},
files: {
totalLinesAdded: 0,
totalLinesRemoved: 0,
},
});
const output = lastFrame();
expect(output).toContain('gemini-3.5-flash');
expect(output).not.toContain('gemini-3-flash');
expect(output).toMatchSnapshot();
unmount();
});
it('should handle models with long names (gemini-3-*-preview) without layout breaking', async () => {
const { lastFrame, unmount } = await renderWithMockedStats(
{
@@ -299,7 +299,7 @@ export const ModelStatsDisplay: React.FC<ModelStatsDisplayProps> = ({
},
...modelNames.map((name) => ({
key: name,
header: name,
header: getDisplayString(name),
flexGrow: 1,
renderCell: (row: StatRowData) => {
// Don't render anything for section headers in model columns
@@ -6,6 +6,12 @@ Sort: s Reverse: r First/Last: g/G
"
`;
exports[`SessionBrowser Search and Navigation Components > NavigationHelp renders correctly 2`] = `
"Navigate: ↑/↓ Resume: Enter Search: / Delete: x Quit: q
Sort: s Reverse: r First/Last: g/G
"
`;
exports[`SessionBrowser Search and Navigation Components > NoResultsDisplay renders correctly 1`] = `
"
No sessions found matching 'no match'.
@@ -6,6 +6,12 @@ exports[`SessionBrowser UI States > SessionBrowserEmpty renders correctly 1`] =
"
`;
exports[`SessionBrowser UI States > SessionBrowserEmpty renders correctly 2`] = `
" No auto-saved conversations found.
Press q to exit
"
`;
exports[`SessionBrowser UI States > SessionBrowserError renders correctly 1`] = `
" Error: Test error message
Press q to exit
@@ -131,6 +131,33 @@ describe('<StatsDisplay />', () => {
expect(output).toMatchSnapshot();
});
it('resolves gemini-3-flash to gemini-3.5-flash in the model usage table', async () => {
const metrics = createTestMetrics({
models: {
'gemini-3-flash': {
api: { totalRequests: 5, totalErrors: 0, totalLatencyMs: 3000 },
tokens: {
input: 1000,
prompt: 2000,
candidates: 3000,
total: 5000,
cached: 500,
thoughts: 100,
tool: 50,
},
roles: {},
},
},
});
const { lastFrame } = await renderWithMockedStats(metrics);
const output = lastFrame();
expect(output).toContain('gemini-3.5-flash');
expect(output).not.toContain('gemini-3-flash\u0020'); // Avoid matching parts of substrings if not intended
expect(output).toMatchSnapshot();
});
it('renders role breakdown correctly under models', async () => {
const metrics = createTestMetrics({
models: {
@@ -24,7 +24,7 @@ import {
import { computeSessionStats } from '../utils/computeStats.js';
import { useSettings } from '../contexts/SettingsContext.js';
import type { QuotaStats } from '../types.js';
import { LlmRole } from '@google/gemini-cli-core';
import { LlmRole, getDisplayString } from '@google/gemini-cli-core';
// A more flexible and powerful StatRow component
interface StatRowProps {
@@ -101,7 +101,7 @@ const ModelUsageTable: React.FC<ModelUsageTableProps> = ({ models }) => {
Object.entries(models).forEach(([name, metrics]) => {
rows.push({
name,
displayName: name,
displayName: getDisplayString(name),
requests: metrics.api.totalRequests,
cachedTokens: metrics.tokens.cached.toLocaleString(),
inputTokens: metrics.tokens.prompt.toLocaleString(),
@@ -84,6 +84,33 @@ exports[`AlternateBufferQuittingDisplay > renders with history but no pending it
Tips for getting started:
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results
╭──────────────────────────────────────────────────────────────────────────╮
│ ✓ tool1 Description for tool 1 │
│ │
╰──────────────────────────────────────────────────────────────────────────╯
╭──────────────────────────────────────────────────────────────────────────╮
│ ✓ tool2 Description for tool 2 │
│ │
╰──────────────────────────────────────────────────────────────────────────╯
"
`;
exports[`AlternateBufferQuittingDisplay > renders with pending items but no history > with_history_no_pending 1`] = `
"
▝▜▄ ▗█▀▀▜▙▝█▛▀▀▌▜██▖▟██▘▜█▘▜██▖▝█▛▝█▛
▝▜▄ █▌ █▙▟ ▐█▝█▛▐█ ▐█ ▐█▝█▖█▌ █▌
▗▟▀ ▜▙ ▝█▛ █▌▝ ▖▐█ ▐█ ▐█ ▐█ ▝██▌ █▌
▝▀ ▀▀▀▀▘▝▀▀▀▀▘▀▀▘ ▀▀▘▀▀▘▀▀▘ ▝▀▀▝▀▀
Gemini CLI v0.10.0
Tips for getting started:
1. Create GEMINI.md files to customize your interactions
2. /help for more information
@@ -123,6 +150,29 @@ Tips for getting started:
"
`;
exports[`AlternateBufferQuittingDisplay > renders with user and gemini messages > with_confirming_tool 1`] = `
"
▝▜▄ ▗█▀▀▜▙▝█▛▀▀▌▜██▖▟██▘▜█▘▜██▖▝█▛▝█▛
▝▜▄ █▌ █▙▟ ▐█▝█▛▐█ ▐█ ▐█▝█▖█▌ █▌
▗▟▀ ▜▙ ▝█▛ █▌▝ ▖▐█ ▐█ ▐█ ▐█ ▝██▌ █▌
▝▀ ▀▀▀▀▘▝▀▀▀▀▘▀▀▘ ▀▀▘▀▀▘▀▀▘ ▝▀▀▝▀▀
Gemini CLI v0.10.0
Tips for getting started:
1. Create GEMINI.md files to customize your interactions
2. /help for more information
3. Ask coding questions, edit code or run commands
4. Be specific for the best results
Action Required (was prompted):
? confirming_tool Confirming tool description
"
`;
exports[`AlternateBufferQuittingDisplay > renders with user and gemini messages > with_user_gemini_messages 1`] = `
"
▝▜▄ ▗█▀▀▜▙▝█▛▀▀▌▜██▖▟██▘▜█▘▜██▖▝█▛▝█▛
@@ -0,0 +1,20 @@
<svg xmlns="http://www.w3.org/2000/svg" width="920" height="88" viewBox="0 0 920 88">
<style>
text { font-family: Consolas, "Courier New", monospace; font-size: 14px; dominant-baseline: text-before-edge; white-space: pre; }
</style>
<rect width="920" height="88" fill="#000000" />
<g transform="translate(10, 10)">
<text x="0" y="2" fill="#333333" textLength="720" lengthAdjust="spacingAndGlyphs">╭──────────────────────────────────────────────────────────────────────────────╮</text>
<text x="0" y="19" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="19" fill="#4796e4" textLength="9" lengthAdjust="spacingAndGlyphs">L</text>
<text x="27" y="19" fill="#6688d9" textLength="9" lengthAdjust="spacingAndGlyphs">i</text>
<text x="36" y="19" fill="#847ace" textLength="9" lengthAdjust="spacingAndGlyphs">n</text>
<text x="45" y="19" fill="#9974b4" textLength="9" lengthAdjust="spacingAndGlyphs">e</text>
<text x="63" y="19" fill="#c3677f" textLength="9" lengthAdjust="spacingAndGlyphs">1</text>
<text x="711" y="19" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="18" y="36" fill="#ffffff" textLength="54" lengthAdjust="spacingAndGlyphs">Line 2</text>
<text x="711" y="36" fill="#333333" textLength="9" lengthAdjust="spacingAndGlyphs"></text>
<text x="0" y="53" fill="#333333" textLength="720" lengthAdjust="spacingAndGlyphs">╰──────────────────────────────────────────────────────────────────────────────╯</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.9 KiB

@@ -7,6 +7,13 @@ exports[`Banner > handles newlines in text 1`] = `
╰──────────────────────────────────────────────────────────────────────────────╯"
`;
exports[`Banner > handles newlines in text 2`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
│ Line 1 │
│ Line 2 │
╰──────────────────────────────────────────────────────────────────────────────╯"
`;
exports[`Banner > renders in info mode 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
│ Info Message │
@@ -11,6 +11,17 @@ exports[`<Checklist /> > renders expanded view correctly 1`] = `
"
`;
exports[`<Checklist /> > renders expanded view correctly 2`] = `
"────────────────────────────────────────────────────────────────────────────────────────────────────
Test List 1/3 completed (toggle me)
✓ Task 1
» Task 2
☐ Task 3
✗ Task 4
"
`;
exports[`<Checklist /> > renders summary view correctly (collapsed) 1`] = `
"────────────────────────────────────────────────────────────────────────────────────────────────────
Test List 1/3 completed (toggle me) » Task 2
@@ -5,6 +5,11 @@ exports[`<ContextSummaryDisplay /> > should not render empty parts 1`] = `
"
`;
exports[`<ContextSummaryDisplay /> > should not render empty parts 2`] = `
" 1 open file (F4 to view)
"
`;
exports[`<ContextSummaryDisplay /> > should render on a single line on a wide screen 1`] = `
" 1 open file (F4 to view) · 1 GEMINI.md file · 1 MCP server · 1 skill
"
@@ -1,5 +1,18 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`LoopDetectionConfirmation > contains the expected options 1`] = `
" ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ ? A potential loop was detected │
│ │
│ This can happen due to repetitive tool calls or other model behavior. Do you want to keep loop │
│ detection enabled or disable it for this session? │
│ │
│ ● 1. Keep loop detection enabled (esc) │
│ 2. Disable loop detection for this session │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
`;
exports[`LoopDetectionConfirmation > renders correctly 1`] = `
" ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ ? A potential loop was detected │
@@ -215,3 +215,26 @@ exports[`<ModelStatsDisplay /> > should render "no API calls" message when there
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
`;
exports[`<ModelStatsDisplay /> > should resolve gemini-3-flash to gemini-3.5-flash via getDisplayString 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ │
│ Model Stats For Nerds │
│ │
│ │
│ Metric gemini-3.5-flash │
│ ────────────────────────────────────────────────────────────────────────────────────────────── │
│ API │
│ Requests 1 │
│ Errors 0 (0.0%) │
│ Avg Latency 100ms │
│ Tokens │
│ Total 30 │
│ ↳ Input 5 │
│ ↳ Cache Reads 5 (50.0%) │
│ ↳ Thoughts 2 │
│ ↳ Tool 1 │
│ ↳ Output 20 │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
`;
@@ -16,6 +16,22 @@ exports[`ShortcutsHelp > renders correctly in 'narrow' mode on 'linux' 1`] = `
"
`;
exports[`ShortcutsHelp > renders correctly in 'narrow' mode on 'linux' 2`] = `
"────────────────────────────────────────
Shortcuts See /help for more
! shell mode
@ select file or folder
Double Esc clear & rewind
Tab focus UI
Ctrl+Y YOLO mode
Shift+Tab cycle mode
Ctrl+V paste images
Alt+M raw markdown mode
Ctrl+R reverse-search history
Ctrl+G open external editor
"
`;
exports[`ShortcutsHelp > renders correctly in 'narrow' mode on 'mac' 1`] = `
"────────────────────────────────────────
Shortcuts See /help for more
@@ -292,3 +292,30 @@ exports[`<StatsDisplay /> > renders role breakdown correctly under models 1`] =
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
`;
exports[`<StatsDisplay /> > resolves gemini-3-flash to gemini-3.5-flash in the model usage table 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ │
│ Session Stats │
│ │
│ Interaction Summary │
│ Session ID: test-session-id │
│ Tool Calls: 0 ( ✓ 0 x 0 ) │
│ Success Rate: 0.0% │
│ │
│ Performance │
│ Wall Time: 1s │
│ Agent Active: 3.0s │
│ » API Time: 3.0s (100.0%) │
│ » Tool Time: 0s (0.0%) │
│ │
│ │
│ Model Usage │
│ Use /model to view model quota information │
│ │
│ Model Reqs Input Tokens Cache Reads Output Tokens │
│ ────────────────────────────────────────────────────────────────────────────────────────────── │
│ gemini-3.5-flash 5 2,000 500 3,000 │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
`;
@@ -25,6 +25,31 @@ exports[`Initial Theme Selection > should default to a dark theme when terminal
"
`;
exports[`Initial Theme Selection > should default to a dark theme when terminal background is dark and no theme is set 2`] = `
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ │
│ > Select Theme Preview │
│ ▲ ┌─────────────────────────────────────────────────┐ │
│ 1. ANSI Dark │ │ │
│ 2. Atom One Dark │ 1 # function │ │
│ 3. Ayu Dark │ 2 def fibonacci(n): │ │
│ ● 4. Default Dark (Matches terminal) │ 3 a, b = 0, 1 │ │
│ 5. Dracula Dark │ 4 for _ in range(n): │ │
│ 6. GitHub Dark │ 5 a, b = b, a + b │ │
│ 7. GitHub Dark Colorblind Dark │ 6 return a │ │
│ 8. Holiday Dark │ │ │
│ 9. Shades Of Purple Dark │ 1 - print("Hello, " + name) │ │
│ 10. Solarized Dark │ 1 + print(f"Hello, {name}!") │ │
│ 11. Tokyo Night Dark │ │ │
│ 12. ANSI Light (Incompatible) └─────────────────────────────────────────────────┘ │
│ ▼ │
│ │
│ (Use Enter to select, Tab to configure scope, Esc to close) │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
`;
exports[`Initial Theme Selection > should default to a light theme when terminal background is light and no theme is set 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ │
@@ -89,6 +114,20 @@ exports[`ThemeDialog Snapshots > should render correctly in scope selector mode
"
`;
exports[`ThemeDialog Snapshots > should render correctly in scope selector mode 2`] = `
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ │
│ > Apply To │
│ ● 1. User Settings │
│ 2. Workspace Settings │
│ 3. System Settings │
│ │
│ (Use Enter to apply scope, Tab to select theme, Esc to close) │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
`;
exports[`ThemeDialog Snapshots > should render correctly in theme selection mode (isDevelopment: false) 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ │
@@ -18,3 +18,12 @@ Tips for getting started:
3. Be specific for the best results
"
`;
exports[`Tips > 'renders fewer tips when GEMINI.md exi…' 2`] = `
"
Tips for getting started:
1. /help for more information
2. Ask coding questions, edit code or run commands
3. Be specific for the best results
"
`;
@@ -41,6 +41,27 @@ exports[`<ToolStatsDisplay /> > should display stats for multiple tools correctl
"
`;
exports[`<ToolStatsDisplay /> > should display stats for multiple tools correctly 2`] = `
"╭────────────────────────────────────────────────────────────────────╮
│ │
│ Tool Stats For Nerds │
│ │
│ Tool Name Calls Success Rate Avg Duration │
│ ──────────────────────────────────────────────────────────────── │
│ tool-a 2 50.0% 100ms │
│ tool-b 1 100.0% 100ms │
│ │
│ User Decision Summary │
│ Total Reviewed Suggestions: 3 │
│ » Accepted: 1 │
│ » Rejected: 1 │
│ » Modified: 1 │
│ ──────────────────────────────────────────────────────────────── │
│ Overall Agreement Rate: 33.3% │
╰────────────────────────────────────────────────────────────────────╯
"
`;
exports[`<ToolStatsDisplay /> > should handle large values without wrapping or overlapping 1`] = `
"╭────────────────────────────────────────────────────────────────────╮
│ │
@@ -6,6 +6,12 @@ exports[`ErrorMessage > renders multiline error messages 1`] = `
"
`;
exports[`ErrorMessage > renders multiline error messages 2`] = `
"✕ Error line 1
Error line 2
"
`;
exports[`ErrorMessage > renders with the correct prefix and text 1`] = `
"✕ Something went wrong
"
@@ -16,6 +16,13 @@ exports[`<GeminiMessage /> - Raw Markdown Display Snapshots > renders pending st
"
`;
exports[`<GeminiMessage /> - Raw Markdown Display Snapshots > renders pending state with renderMarkdown=true 2`] = `
"✦ Test bold and code markdown
1 const x = 1;
"
`;
exports[`<GeminiMessage /> - Raw Markdown Display Snapshots > renders with renderMarkdown=false '(raw markdown with syntax highlightin…' 1`] = `
"✦ Test **bold** and \`code\` markdown
@@ -40,3 +47,12 @@ exports[`<GeminiMessage /> - Raw Markdown Display Snapshots > wraps long lines c
truncation
"
`;
exports[`<GeminiMessage /> - Raw Markdown Display Snapshots > wraps long lines correctly in raw markdown mode 2`] = `
"✦ This is a long
line that should
wrap correctly
without
truncation
"
`;
@@ -26,6 +26,12 @@ exports[`ToolGroupMessage Compact Rendering > does not add an extra empty line i
"
`;
exports[`ToolGroupMessage Compact Rendering > does not add an extra empty line if a compact tool has a dense payload 2`] = `
" ✓ ReadFolder Listing files → file1.txt
✓ ReadFile Reading file → read file
"
`;
exports[`ToolGroupMessage Compact Rendering > renders consecutive compact tools without empty lines between them 1`] = `
" ✓ ReadFolder Listing files → file1.txt file2.txt
✓ ReadFolder Listing files → file3.txt
@@ -16,6 +16,14 @@ exports[`<ToolMessage /> - Raw Markdown Display Snapshots > renders with renderM
"
`;
exports[`<ToolMessage /> - Raw Markdown Display Snapshots > renders with renderMarkdown=false, useAlternateBuffer=true '(raw markdown, alternate buffer)' 2`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
│ ✓ test-tool A tool for testing │
│ │
│ Test **bold** and \`code\` markdown │
"
`;
exports[`<ToolMessage /> - Raw Markdown Display Snapshots > renders with renderMarkdown=true, useAlternateBuffer=false '(constrained height, regular buffer -…' 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
│ ✓ test-tool A tool for testing │
@@ -8,6 +8,14 @@ exports[`UserMessage > renders multiline user message 1`] = `
"
`;
exports[`UserMessage > renders multiline user message 2`] = `
"▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
> Line 1
Line 2
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
"
`;
exports[`UserMessage > renders normal user message with correct prefix 1`] = `
"▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
> Hello Gemini
@@ -7,6 +7,13 @@ exports[`WarningMessage > renders multiline warning messages 1`] = `
"
`;
exports[`WarningMessage > renders multiline warning messages 2`] = `
"
⚠ Warning line 1
Warning line 2
"
`;
exports[`WarningMessage > renders with the correct prefix and text 1`] = `
"
⚠ Watch out!
@@ -9,6 +9,15 @@ Note: Newest last, oldest first
"
`;
exports[`<ChatList /> > handles invalid date formats gracefully 2`] = `
"List of saved conversations:
- bad-date-chat (Invalid Date)
Note: Newest last, oldest first
"
`;
exports[`<ChatList /> > renders correctly with a list of chats 1`] = `
"List of saved conversations:
@@ -30,6 +30,20 @@ BackgroundTaskDisplay
Notifications
CopyModeWarning
Composer
ExitWarning
"
`;
exports[`<DefaultAppLayout /> > shows BackgroundTaskDisplay when StreamingState is NOT WaitingForConfirmation 2`] = `
"MainContent
BackgroundTaskDisplay
Notifications
CopyModeWarning
Composer