mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-06-12 12:26:57 -07:00
feat: launch Gemini 3 in Gemini CLI 🚀🚀🚀 (in main) (#13287)
Co-authored-by: Adam Weidman <65992621+adamfweidman@users.noreply.github.com> Co-authored-by: Sehoon Shon <sshon@google.com> Co-authored-by: Adib234 <30782825+Adib234@users.noreply.github.com> Co-authored-by: Sandy Tao <sandytao520@icloud.com> Co-authored-by: Abhi <43648792+abhipatel12@users.noreply.github.com> Co-authored-by: Aishanee Shah <aishaneeshah@gmail.com> Co-authored-by: gemini-cli-robot <gemini-cli-robot@google.com> Co-authored-by: Gal Zahavi <38544478+galz10@users.noreply.github.com> Co-authored-by: Jacob Richman <jacob314@gmail.com> Co-authored-by: joshualitt <joshualitt@google.com> Co-authored-by: Jenna Inouye <jinouye@google.com>
This commit is contained in:
@@ -12,6 +12,10 @@ import { Text } from 'ink';
|
||||
import { renderWithProviders } from '../../test-utils/render.js';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
|
||||
vi.mock('../utils/terminalSetup.js', () => ({
|
||||
getTerminalProgram: () => null,
|
||||
}));
|
||||
|
||||
vi.mock('../contexts/AppContext.js', () => ({
|
||||
useAppContext: () => ({
|
||||
version: '0.10.0',
|
||||
@@ -85,6 +89,11 @@ const mockConfig = {
|
||||
getTargetDir: () => '/tmp',
|
||||
getDebugMode: () => false,
|
||||
getGeminiMdFileCount: () => 0,
|
||||
getExperiments: () => ({
|
||||
flags: {},
|
||||
experimentIds: [],
|
||||
}),
|
||||
getPreviewFeatures: () => false,
|
||||
} as unknown as Config;
|
||||
|
||||
describe('AlternateBufferQuittingDisplay', () => {
|
||||
@@ -101,6 +110,10 @@ describe('AlternateBufferQuittingDisplay', () => {
|
||||
activePtyId: undefined,
|
||||
embeddedShellFocused: false,
|
||||
renderMarkdown: false,
|
||||
bannerData: {
|
||||
defaultText: '',
|
||||
warningText: '',
|
||||
},
|
||||
},
|
||||
config: mockConfig,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { renderWithProviders } from '../../test-utils/render.js';
|
||||
import { AppHeader } from './AppHeader.js';
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { makeFakeConfig } from '@google/gemini-cli-core';
|
||||
|
||||
const persistentStateMock = vi.hoisted(() => ({
|
||||
get: vi.fn(),
|
||||
set: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('../../utils/persistentState.js', () => ({
|
||||
persistentState: persistentStateMock,
|
||||
}));
|
||||
|
||||
vi.mock('../utils/terminalSetup.js', () => ({
|
||||
getTerminalProgram: () => null,
|
||||
}));
|
||||
|
||||
describe('<AppHeader />', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
persistentStateMock.get.mockReturnValue(0);
|
||||
});
|
||||
|
||||
it('should render the banner with default text', () => {
|
||||
const mockConfig = makeFakeConfig();
|
||||
const uiState = {
|
||||
bannerData: {
|
||||
defaultText: 'This is the default banner',
|
||||
warningText: '',
|
||||
},
|
||||
bannerVisible: true,
|
||||
};
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<AppHeader version="1.0.0" />,
|
||||
{ config: mockConfig, uiState },
|
||||
);
|
||||
|
||||
expect(lastFrame()).toContain('This is the default banner');
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should render the banner with warning text', () => {
|
||||
const mockConfig = makeFakeConfig();
|
||||
const uiState = {
|
||||
bannerData: {
|
||||
defaultText: 'This is the default banner',
|
||||
warningText: 'There are capacity issues',
|
||||
},
|
||||
bannerVisible: true,
|
||||
};
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<AppHeader version="1.0.0" />,
|
||||
{ config: mockConfig, uiState },
|
||||
);
|
||||
|
||||
expect(lastFrame()).toContain('There are capacity issues');
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should not render the banner when no flags are set', () => {
|
||||
const mockConfig = makeFakeConfig();
|
||||
const uiState = {
|
||||
bannerData: {
|
||||
defaultText: '',
|
||||
warningText: '',
|
||||
},
|
||||
};
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<AppHeader version="1.0.0" />,
|
||||
{ config: mockConfig, uiState },
|
||||
);
|
||||
|
||||
expect(lastFrame()).not.toContain('Banner');
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should render the banner when previewFeatures is disabled', () => {
|
||||
const mockConfig = makeFakeConfig({ previewFeatures: false });
|
||||
const uiState = {
|
||||
bannerData: {
|
||||
defaultText: 'This is the default banner',
|
||||
warningText: '',
|
||||
},
|
||||
bannerVisible: true,
|
||||
};
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<AppHeader version="1.0.0" />,
|
||||
{ config: mockConfig, uiState },
|
||||
);
|
||||
|
||||
expect(lastFrame()).toContain('This is the default banner');
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should not render the banner when previewFeatures is enabled', () => {
|
||||
const mockConfig = makeFakeConfig({ previewFeatures: true });
|
||||
const uiState = {
|
||||
bannerData: {
|
||||
defaultText: 'This is the default banner',
|
||||
warningText: '',
|
||||
},
|
||||
};
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<AppHeader version="1.0.0" />,
|
||||
{ config: mockConfig, uiState },
|
||||
);
|
||||
|
||||
expect(lastFrame()).not.toContain('This is the default banner');
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should not render the default banner if shown count is 5 or more', () => {
|
||||
persistentStateMock.get.mockReturnValue(5);
|
||||
const mockConfig = makeFakeConfig();
|
||||
const uiState = {
|
||||
bannerData: {
|
||||
defaultText: 'This is the default banner',
|
||||
warningText: '',
|
||||
},
|
||||
};
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<AppHeader version="1.0.0" />,
|
||||
{ config: mockConfig, uiState },
|
||||
);
|
||||
|
||||
expect(lastFrame()).not.toContain('This is the default banner');
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should increment the shown count when default banner is displayed', () => {
|
||||
persistentStateMock.get.mockReturnValue(0);
|
||||
const mockConfig = makeFakeConfig();
|
||||
const uiState = {
|
||||
bannerData: {
|
||||
defaultText: 'This is the default banner',
|
||||
warningText: '',
|
||||
},
|
||||
};
|
||||
|
||||
const { unmount } = renderWithProviders(<AppHeader version="1.0.0" />, {
|
||||
config: mockConfig,
|
||||
uiState,
|
||||
});
|
||||
|
||||
expect(persistentStateMock.set).toHaveBeenCalledWith(
|
||||
'defaultBannerShownCount',
|
||||
1,
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should render banner text with unescaped newlines', () => {
|
||||
const mockConfig = makeFakeConfig();
|
||||
const uiState = {
|
||||
bannerData: {
|
||||
defaultText: 'First line\\nSecond line',
|
||||
warningText: '',
|
||||
},
|
||||
bannerVisible: true,
|
||||
};
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<AppHeader version="1.0.0" />,
|
||||
{ config: mockConfig, uiState },
|
||||
);
|
||||
|
||||
expect(lastFrame()).not.toContain('First line\\nSecond line');
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
@@ -10,6 +10,11 @@ import { Tips } from './Tips.js';
|
||||
import { useSettings } from '../contexts/SettingsContext.js';
|
||||
import { useConfig } from '../contexts/ConfigContext.js';
|
||||
import { useUIState } from '../contexts/UIStateContext.js';
|
||||
import { Banner } from './Banner.js';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { Colors } from '../colors.js';
|
||||
import { persistentState } from '../../utils/persistentState.js';
|
||||
import { useState, useEffect, useRef } from 'react';
|
||||
|
||||
interface AppHeaderProps {
|
||||
version: string;
|
||||
@@ -18,12 +23,46 @@ interface AppHeaderProps {
|
||||
export const AppHeader = ({ version }: AppHeaderProps) => {
|
||||
const settings = useSettings();
|
||||
const config = useConfig();
|
||||
const { nightly } = useUIState();
|
||||
const { nightly, mainAreaWidth, bannerData, bannerVisible } = useUIState();
|
||||
|
||||
const [defaultBannerShownCount] = useState(
|
||||
() => persistentState.get('defaultBannerShownCount') || 0,
|
||||
);
|
||||
|
||||
const { defaultText, warningText } = bannerData;
|
||||
|
||||
const showDefaultBanner =
|
||||
warningText === '' &&
|
||||
!config.getPreviewFeatures() &&
|
||||
defaultBannerShownCount < 5;
|
||||
const bannerText = showDefaultBanner ? defaultText : warningText;
|
||||
const unescapedBannerText = bannerText.replace(/\\n/g, '\n');
|
||||
|
||||
const defaultColor = Colors.AccentBlue;
|
||||
const fontColor = warningText === '' ? defaultColor : theme.status.warning;
|
||||
|
||||
const hasIncrementedRef = useRef(false);
|
||||
useEffect(() => {
|
||||
if (showDefaultBanner && defaultText && !hasIncrementedRef.current) {
|
||||
hasIncrementedRef.current = true;
|
||||
const current = persistentState.get('defaultBannerShownCount') || 0;
|
||||
persistentState.set('defaultBannerShownCount', current + 1);
|
||||
}
|
||||
}, [showDefaultBanner, defaultText]);
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
{!(settings.merged.ui?.hideBanner || config.getScreenReader()) && (
|
||||
<Header version={version} nightly={nightly} />
|
||||
<>
|
||||
<Header version={version} nightly={nightly} />
|
||||
{bannerVisible && unescapedBannerText && (
|
||||
<Banner
|
||||
width={mainAreaWidth}
|
||||
bannerText={unescapedBannerText}
|
||||
color={fontColor}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!(settings.merged.ui?.hideTips || config.getScreenReader()) && (
|
||||
<Tips config={config} />
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { Box, Text } from 'ink';
|
||||
import Gradient from 'ink-gradient';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
|
||||
interface BannerProps {
|
||||
bannerText: string;
|
||||
color: string;
|
||||
width: number;
|
||||
}
|
||||
|
||||
export const Banner = ({ bannerText, color, width }: BannerProps) => {
|
||||
const gradient = theme.ui.gradient;
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
borderStyle="round"
|
||||
borderColor={color}
|
||||
width={width}
|
||||
paddingLeft={1}
|
||||
paddingRight={1}
|
||||
>
|
||||
<Gradient colors={gradient}>
|
||||
<Text>{bannerText}</Text>
|
||||
</Gradient>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -157,6 +157,7 @@ export const Composer = () => {
|
||||
suggestionsWidth={uiState.suggestionsWidth}
|
||||
onSubmit={uiActions.handleFinalSubmit}
|
||||
userMessages={uiState.userMessages}
|
||||
setBannerVisible={uiActions.setBannerVisible}
|
||||
onClearScreen={uiActions.handleClearScreen}
|
||||
config={config}
|
||||
slashCommands={uiState.slashCommands || []}
|
||||
|
||||
@@ -53,8 +53,13 @@ export const DialogManager = ({
|
||||
if (uiState.proQuotaRequest) {
|
||||
return (
|
||||
<ProQuotaDialog
|
||||
failedModel={uiState.proQuotaRequest.failedModel}
|
||||
fallbackModel={uiState.proQuotaRequest.fallbackModel}
|
||||
message={uiState.proQuotaRequest.message}
|
||||
isTerminalQuotaError={uiState.proQuotaRequest.isTerminalQuotaError}
|
||||
isModelNotFoundError={!!uiState.proQuotaRequest.isModelNotFoundError}
|
||||
onChoice={uiActions.handleProQuotaChoice}
|
||||
userTier={uiState.userTier}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import { useState } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import {
|
||||
EDITOR_DISPLAY_NAMES,
|
||||
editorSettingsManager,
|
||||
type EditorDisplay,
|
||||
} from '../editors/editorSettingsManager.js';
|
||||
@@ -19,8 +18,11 @@ import type {
|
||||
LoadedSettings,
|
||||
} from '../../config/settings.js';
|
||||
import { SettingScope } from '../../config/settings.js';
|
||||
import type { EditorType } from '@google/gemini-cli-core';
|
||||
import { isEditorAvailable } from '@google/gemini-cli-core';
|
||||
import {
|
||||
type EditorType,
|
||||
isEditorAvailable,
|
||||
EDITOR_DISPLAY_NAMES,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { useKeypress } from '../hooks/useKeypress.js';
|
||||
|
||||
interface EditorDialogProps {
|
||||
|
||||
@@ -235,6 +235,7 @@ describe('InputPrompt', () => {
|
||||
focus: true,
|
||||
setQueueErrorMessage: vi.fn(),
|
||||
streamingState: StreamingState.Idle,
|
||||
setBannerVisible: vi.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
@@ -812,6 +813,19 @@ describe('InputPrompt', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should call setBannerVisible(false) when clear screen key is pressed', async () => {
|
||||
const { stdin, unmount } = renderWithProviders(<InputPrompt {...props} />);
|
||||
|
||||
await act(async () => {
|
||||
stdin.write('\x0C'); // Ctrl+L
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(props.setBannerVisible).toHaveBeenCalledWith(false);
|
||||
});
|
||||
unmount();
|
||||
});
|
||||
|
||||
describe('cursor-based completion trigger', () => {
|
||||
it.each([
|
||||
{
|
||||
|
||||
@@ -80,6 +80,7 @@ export interface InputPromptProps {
|
||||
streamingState: StreamingState;
|
||||
popAllMessages?: (onPop: (messages: string | undefined) => void) => void;
|
||||
suggestionsPosition?: 'above' | 'below';
|
||||
setBannerVisible: (visible: boolean) => void;
|
||||
}
|
||||
|
||||
// The input content, input container, and input suggestions list may have different widths
|
||||
@@ -121,6 +122,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
streamingState,
|
||||
popAllMessages,
|
||||
suggestionsPosition = 'below',
|
||||
setBannerVisible,
|
||||
}) => {
|
||||
const kittyProtocol = useKittyKeyboardProtocol();
|
||||
const isShellFocused = useShellFocusState();
|
||||
@@ -525,6 +527,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
}
|
||||
|
||||
if (keyMatchers[Command.CLEAR_SCREEN](key)) {
|
||||
setBannerVisible(false);
|
||||
onClearScreen();
|
||||
return;
|
||||
}
|
||||
@@ -819,6 +822,7 @@ export const InputPrompt: React.FC<InputPromptProps> = ({
|
||||
commandSearchCompletion,
|
||||
kittyProtocol.supported,
|
||||
tryLoadQueuedMessages,
|
||||
setBannerVisible,
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -8,9 +8,9 @@ import { render } from '../../test-utils/render.js';
|
||||
import { cleanup } from 'ink-testing-library';
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import {
|
||||
DEFAULT_GEMINI_FLASH_LITE_MODEL,
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
DEFAULT_GEMINI_MODEL,
|
||||
GEMINI_MODEL_ALIAS_FLASH_LITE,
|
||||
GEMINI_MODEL_ALIAS_FLASH,
|
||||
GEMINI_MODEL_ALIAS_PRO,
|
||||
DEFAULT_GEMINI_MODEL_AUTO,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { ModelDialog } from './ModelDialog.js';
|
||||
@@ -43,6 +43,7 @@ const renderComponent = (
|
||||
// --- Functions used by ModelDialog ---
|
||||
getModel: vi.fn(() => DEFAULT_GEMINI_MODEL_AUTO),
|
||||
setModel: vi.fn(),
|
||||
getPreviewFeatures: vi.fn(() => false),
|
||||
|
||||
// --- Functions used by ClearcutLogger ---
|
||||
getUsageStatisticsEnabled: vi.fn(() => true),
|
||||
@@ -86,7 +87,7 @@ describe('<ModelDialog />', () => {
|
||||
expect(lastFrame()).toContain('Select Model');
|
||||
expect(lastFrame()).toContain('(Press Esc to close)');
|
||||
expect(lastFrame()).toContain(
|
||||
'> To use a specific Gemini model on startup, use the --model flag.',
|
||||
'To use a specific Gemini model on startup, use the --model flag.',
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
@@ -98,15 +99,15 @@ describe('<ModelDialog />', () => {
|
||||
const props = mockedSelect.mock.calls[0][0];
|
||||
expect(props.items).toHaveLength(4);
|
||||
expect(props.items[0].value).toBe(DEFAULT_GEMINI_MODEL_AUTO);
|
||||
expect(props.items[1].value).toBe(DEFAULT_GEMINI_MODEL);
|
||||
expect(props.items[2].value).toBe(DEFAULT_GEMINI_FLASH_MODEL);
|
||||
expect(props.items[3].value).toBe(DEFAULT_GEMINI_FLASH_LITE_MODEL);
|
||||
expect(props.items[1].value).toBe(GEMINI_MODEL_ALIAS_PRO);
|
||||
expect(props.items[2].value).toBe(GEMINI_MODEL_ALIAS_FLASH);
|
||||
expect(props.items[3].value).toBe(GEMINI_MODEL_ALIAS_FLASH_LITE);
|
||||
expect(props.showNumbers).toBe(true);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('initializes with the model from ConfigContext', () => {
|
||||
const mockGetModel = vi.fn(() => DEFAULT_GEMINI_FLASH_MODEL);
|
||||
const mockGetModel = vi.fn(() => GEMINI_MODEL_ALIAS_FLASH);
|
||||
const { unmount } = renderComponent({}, { getModel: mockGetModel });
|
||||
|
||||
expect(mockGetModel).toHaveBeenCalled();
|
||||
@@ -157,10 +158,10 @@ describe('<ModelDialog />', () => {
|
||||
const childOnSelect = mockedSelect.mock.calls[0][0].onSelect;
|
||||
expect(childOnSelect).toBeDefined();
|
||||
|
||||
childOnSelect(DEFAULT_GEMINI_MODEL);
|
||||
childOnSelect(GEMINI_MODEL_ALIAS_PRO);
|
||||
|
||||
// Assert against the default mock provided by renderComponent
|
||||
expect(mockConfig?.setModel).toHaveBeenCalledWith(DEFAULT_GEMINI_MODEL);
|
||||
expect(mockConfig?.setModel).toHaveBeenCalledWith(GEMINI_MODEL_ALIAS_PRO);
|
||||
expect(props.onClose).toHaveBeenCalledTimes(1);
|
||||
unmount();
|
||||
});
|
||||
@@ -209,18 +210,23 @@ describe('<ModelDialog />', () => {
|
||||
|
||||
it('updates initialIndex when config context changes', () => {
|
||||
const mockGetModel = vi.fn(() => DEFAULT_GEMINI_MODEL_AUTO);
|
||||
const oldMockConfig = {
|
||||
getModel: mockGetModel,
|
||||
getPreviewFeatures: vi.fn(() => false),
|
||||
} as unknown as Config;
|
||||
const { rerender, unmount } = render(
|
||||
<ConfigContext.Provider
|
||||
value={{ getModel: mockGetModel } as unknown as Config}
|
||||
>
|
||||
<ConfigContext.Provider value={oldMockConfig}>
|
||||
<ModelDialog onClose={vi.fn()} />
|
||||
</ConfigContext.Provider>,
|
||||
);
|
||||
|
||||
expect(mockedSelect.mock.calls[0][0].initialIndex).toBe(0);
|
||||
|
||||
mockGetModel.mockReturnValue(DEFAULT_GEMINI_FLASH_LITE_MODEL);
|
||||
const newMockConfig = { getModel: mockGetModel } as unknown as Config;
|
||||
mockGetModel.mockReturnValue(GEMINI_MODEL_ALIAS_FLASH_LITE);
|
||||
const newMockConfig = {
|
||||
getModel: mockGetModel,
|
||||
getPreviewFeatures: vi.fn(() => false),
|
||||
} as unknown as Config;
|
||||
|
||||
rerender(
|
||||
<ConfigContext.Provider value={newMockConfig}>
|
||||
|
||||
@@ -8,10 +8,14 @@ import type React from 'react';
|
||||
import { useCallback, useContext, useMemo } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import {
|
||||
DEFAULT_GEMINI_FLASH_LITE_MODEL,
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
DEFAULT_GEMINI_MODEL,
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
DEFAULT_GEMINI_FLASH_LITE_MODEL,
|
||||
DEFAULT_GEMINI_MODEL_AUTO,
|
||||
GEMINI_MODEL_ALIAS_FLASH,
|
||||
GEMINI_MODEL_ALIAS_FLASH_LITE,
|
||||
GEMINI_MODEL_ALIAS_PRO,
|
||||
ModelSlashCommandEvent,
|
||||
logModelSlashCommand,
|
||||
} from '@google/gemini-cli-core';
|
||||
@@ -19,38 +23,12 @@ import { useKeypress } from '../hooks/useKeypress.js';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { DescriptiveRadioButtonSelect } from './shared/DescriptiveRadioButtonSelect.js';
|
||||
import { ConfigContext } from '../contexts/ConfigContext.js';
|
||||
import Gradient from 'ink-gradient';
|
||||
|
||||
interface ModelDialogProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const MODEL_OPTIONS = [
|
||||
{
|
||||
value: DEFAULT_GEMINI_MODEL_AUTO,
|
||||
title: 'Auto (recommended)',
|
||||
description: 'Let the system choose the best model for your task',
|
||||
key: DEFAULT_GEMINI_MODEL_AUTO,
|
||||
},
|
||||
{
|
||||
value: DEFAULT_GEMINI_MODEL,
|
||||
title: 'Pro',
|
||||
description: 'For complex tasks that require deep reasoning and creativity',
|
||||
key: DEFAULT_GEMINI_MODEL,
|
||||
},
|
||||
{
|
||||
value: DEFAULT_GEMINI_FLASH_MODEL,
|
||||
title: 'Flash',
|
||||
description: 'For tasks that need a balance of speed and reasoning',
|
||||
key: DEFAULT_GEMINI_FLASH_MODEL,
|
||||
},
|
||||
{
|
||||
value: DEFAULT_GEMINI_FLASH_LITE_MODEL,
|
||||
title: 'Flash-Lite',
|
||||
description: 'For simple tasks that need to be done quickly',
|
||||
key: DEFAULT_GEMINI_FLASH_LITE_MODEL,
|
||||
},
|
||||
];
|
||||
|
||||
export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
||||
const config = useContext(ConfigContext);
|
||||
|
||||
@@ -66,10 +44,43 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
||||
{ isActive: true },
|
||||
);
|
||||
|
||||
const options = useMemo(
|
||||
() => [
|
||||
{
|
||||
value: DEFAULT_GEMINI_MODEL_AUTO,
|
||||
title: 'Auto',
|
||||
description: 'Let the system choose the best model for your task.',
|
||||
key: DEFAULT_GEMINI_MODEL_AUTO,
|
||||
},
|
||||
{
|
||||
value: GEMINI_MODEL_ALIAS_PRO,
|
||||
title: config?.getPreviewFeatures()
|
||||
? `Pro (${PREVIEW_GEMINI_MODEL}, ${DEFAULT_GEMINI_MODEL})`
|
||||
: `Pro (${DEFAULT_GEMINI_MODEL})`,
|
||||
description:
|
||||
'For complex tasks that require deep reasoning and creativity',
|
||||
key: GEMINI_MODEL_ALIAS_PRO,
|
||||
},
|
||||
{
|
||||
value: GEMINI_MODEL_ALIAS_FLASH,
|
||||
title: `Flash (${DEFAULT_GEMINI_FLASH_MODEL})`,
|
||||
description: 'For tasks that need a balance of speed and reasoning',
|
||||
key: GEMINI_MODEL_ALIAS_FLASH,
|
||||
},
|
||||
{
|
||||
value: GEMINI_MODEL_ALIAS_FLASH_LITE,
|
||||
title: `Flash-Lite (${DEFAULT_GEMINI_FLASH_LITE_MODEL})`,
|
||||
description: 'For simple tasks that need to be done quickly',
|
||||
key: GEMINI_MODEL_ALIAS_FLASH_LITE,
|
||||
},
|
||||
],
|
||||
[config],
|
||||
);
|
||||
|
||||
// Calculate the initial index based on the preferred model.
|
||||
const initialIndex = useMemo(
|
||||
() => MODEL_OPTIONS.findIndex((option) => option.value === preferredModel),
|
||||
[preferredModel],
|
||||
() => options.findIndex((option) => option.value === preferredModel),
|
||||
[preferredModel, options],
|
||||
);
|
||||
|
||||
// Handle selection internally (Autonomous Dialog).
|
||||
@@ -85,6 +96,14 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
||||
[config, onClose],
|
||||
);
|
||||
|
||||
const header = config?.getPreviewFeatures()
|
||||
? 'Gemini 3 is now enabled.'
|
||||
: 'Gemini 3 is now available.';
|
||||
|
||||
const subheader = config?.getPreviewFeatures()
|
||||
? `To disable Gemini 3, disable "Preview features" in /settings.\nLearn more at https://goo.gle/enable-preview-features\n\nWhen you select Auto or Pro, Gemini CLI will attempt to use ${PREVIEW_GEMINI_MODEL} first, before falling back to ${DEFAULT_GEMINI_MODEL}.`
|
||||
: `To use Gemini 3, enable "Preview features" in /settings.\nLearn more at https://goo.gle/enable-preview-features`;
|
||||
|
||||
return (
|
||||
<Box
|
||||
borderStyle="round"
|
||||
@@ -94,17 +113,25 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element {
|
||||
width="100%"
|
||||
>
|
||||
<Text bold>Select Model</Text>
|
||||
|
||||
<Box marginTop={1} marginBottom={1} flexDirection="column">
|
||||
<Gradient colors={theme.ui.gradient}>
|
||||
<Text>{header}</Text>
|
||||
</Gradient>
|
||||
<Text>{subheader}</Text>
|
||||
</Box>
|
||||
|
||||
<Box marginTop={1}>
|
||||
<DescriptiveRadioButtonSelect
|
||||
items={MODEL_OPTIONS}
|
||||
items={options}
|
||||
onSelect={handleSelect}
|
||||
initialIndex={initialIndex}
|
||||
showNumbers={true}
|
||||
/>
|
||||
</Box>
|
||||
<Box flexDirection="column">
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
<Text color={theme.text.secondary}>
|
||||
{'> To use a specific Gemini model on startup, use the --model flag.'}
|
||||
{'To use a specific Gemini model on startup, use the --model flag.'}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box marginTop={1} flexDirection="column">
|
||||
|
||||
@@ -10,86 +10,297 @@ import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
|
||||
import { ProQuotaDialog } from './ProQuotaDialog.js';
|
||||
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
|
||||
|
||||
import {
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
UserTierId,
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
} from '@google/gemini-cli-core';
|
||||
|
||||
// Mock the child component to make it easier to test the parent
|
||||
vi.mock('./shared/RadioButtonSelect.js', () => ({
|
||||
RadioButtonSelect: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('ProQuotaDialog', () => {
|
||||
const mockOnChoice = vi.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should render with correct title and options', () => {
|
||||
const { lastFrame, unmount } = render(
|
||||
<ProQuotaDialog fallbackModel="gemini-2.5-flash" onChoice={() => {}} />,
|
||||
);
|
||||
describe('for flash model failures', () => {
|
||||
it('should render "Keep trying" and "Stop" options', () => {
|
||||
const { unmount } = render(
|
||||
<ProQuotaDialog
|
||||
failedModel={DEFAULT_GEMINI_FLASH_MODEL}
|
||||
fallbackModel="gemini-2.5-pro"
|
||||
message="flash error"
|
||||
isTerminalQuotaError={true} // should not matter
|
||||
onChoice={mockOnChoice}
|
||||
userTier={UserTierId.FREE}
|
||||
/>,
|
||||
);
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain(
|
||||
'Note: You can always use /model to select a different option.',
|
||||
);
|
||||
|
||||
// Check that RadioButtonSelect was called with the correct items
|
||||
expect(RadioButtonSelect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
items: [
|
||||
{
|
||||
label: 'Try again later',
|
||||
value: 'retry_later' as const,
|
||||
key: 'retry_later',
|
||||
},
|
||||
{
|
||||
label: `Switch to gemini-2.5-flash for the rest of this session`,
|
||||
value: 'retry' as const,
|
||||
key: 'retry',
|
||||
},
|
||||
],
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
unmount();
|
||||
expect(RadioButtonSelect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
items: [
|
||||
{
|
||||
label: 'Keep trying',
|
||||
value: 'retry_once',
|
||||
key: 'retry_once',
|
||||
},
|
||||
{
|
||||
label: 'Stop',
|
||||
value: 'retry_later',
|
||||
key: 'retry_later',
|
||||
},
|
||||
],
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
it('should call onChoice with "auth" when "Change auth" is selected', () => {
|
||||
const mockOnChoice = vi.fn();
|
||||
const { unmount } = render(
|
||||
<ProQuotaDialog
|
||||
fallbackModel="gemini-2.5-flash"
|
||||
onChoice={mockOnChoice}
|
||||
/>,
|
||||
);
|
||||
describe('for non-flash model failures', () => {
|
||||
describe('when it is a terminal quota error', () => {
|
||||
it('should render switch and stop options for paid tiers', () => {
|
||||
const { unmount } = render(
|
||||
<ProQuotaDialog
|
||||
failedModel="gemini-2.5-pro"
|
||||
fallbackModel="gemini-2.5-flash"
|
||||
message="paid tier quota error"
|
||||
isTerminalQuotaError={true}
|
||||
isModelNotFoundError={false}
|
||||
onChoice={mockOnChoice}
|
||||
userTier={UserTierId.LEGACY}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Get the onSelect function passed to RadioButtonSelect
|
||||
const onSelect = (RadioButtonSelect as Mock).mock.calls[0][0].onSelect;
|
||||
expect(RadioButtonSelect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
items: [
|
||||
{
|
||||
label: 'Switch to gemini-2.5-flash',
|
||||
value: 'retry_always',
|
||||
key: 'retry_always',
|
||||
},
|
||||
{
|
||||
label: 'Stop',
|
||||
value: 'retry_later',
|
||||
key: 'retry_later',
|
||||
},
|
||||
],
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
|
||||
// Simulate the selection
|
||||
act(() => {
|
||||
onSelect('auth');
|
||||
it('should render switch, upgrade, and stop options for free tier', () => {
|
||||
const { unmount } = render(
|
||||
<ProQuotaDialog
|
||||
failedModel="gemini-2.5-pro"
|
||||
fallbackModel="gemini-2.5-flash"
|
||||
message="free tier quota error"
|
||||
isTerminalQuotaError={true}
|
||||
isModelNotFoundError={false}
|
||||
onChoice={mockOnChoice}
|
||||
userTier={UserTierId.FREE}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(RadioButtonSelect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
items: [
|
||||
{
|
||||
label: 'Switch to gemini-2.5-flash',
|
||||
value: 'retry_always',
|
||||
key: 'retry_always',
|
||||
},
|
||||
{
|
||||
label: 'Upgrade for higher limits',
|
||||
value: 'upgrade',
|
||||
key: 'upgrade',
|
||||
},
|
||||
{
|
||||
label: 'Stop',
|
||||
value: 'retry_later',
|
||||
key: 'retry_later',
|
||||
},
|
||||
],
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
expect(mockOnChoice).toHaveBeenCalledWith('auth');
|
||||
unmount();
|
||||
describe('when it is a capacity error', () => {
|
||||
it('should render keep trying, switch, and stop options', () => {
|
||||
const { unmount } = render(
|
||||
<ProQuotaDialog
|
||||
failedModel="gemini-2.5-pro"
|
||||
fallbackModel="gemini-2.5-flash"
|
||||
message="capacity error"
|
||||
isTerminalQuotaError={false}
|
||||
isModelNotFoundError={false}
|
||||
onChoice={mockOnChoice}
|
||||
userTier={UserTierId.FREE}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(RadioButtonSelect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
items: [
|
||||
{
|
||||
label: 'Keep trying',
|
||||
value: 'retry_once',
|
||||
key: 'retry_once',
|
||||
},
|
||||
{
|
||||
label: 'Switch to gemini-2.5-flash',
|
||||
value: 'retry_always',
|
||||
key: 'retry_always',
|
||||
},
|
||||
{ label: 'Stop', value: 'retry_later', key: 'retry_later' },
|
||||
],
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when it is a model not found error', () => {
|
||||
it('should render switch and stop options regardless of tier', () => {
|
||||
const { unmount } = render(
|
||||
<ProQuotaDialog
|
||||
failedModel="gemini-3-pro-preview"
|
||||
fallbackModel="gemini-2.5-pro"
|
||||
message="You don't have access to gemini-3-pro-preview yet."
|
||||
isTerminalQuotaError={false}
|
||||
isModelNotFoundError={true}
|
||||
onChoice={mockOnChoice}
|
||||
userTier={UserTierId.FREE}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(RadioButtonSelect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
items: [
|
||||
{
|
||||
label: 'Switch to gemini-2.5-pro',
|
||||
value: 'retry_always',
|
||||
key: 'retry_always',
|
||||
},
|
||||
{
|
||||
label: 'Stop',
|
||||
value: 'retry_later',
|
||||
key: 'retry_later',
|
||||
},
|
||||
],
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('should render switch and stop options for paid tier as well', () => {
|
||||
const { unmount } = render(
|
||||
<ProQuotaDialog
|
||||
failedModel="gemini-3-pro-preview"
|
||||
fallbackModel="gemini-2.5-pro"
|
||||
message="You don't have access to gemini-3-pro-preview yet."
|
||||
isTerminalQuotaError={false}
|
||||
isModelNotFoundError={true}
|
||||
onChoice={mockOnChoice}
|
||||
userTier={UserTierId.LEGACY}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(RadioButtonSelect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
items: [
|
||||
{
|
||||
label: 'Switch to gemini-2.5-pro',
|
||||
value: 'retry_always',
|
||||
key: 'retry_always',
|
||||
},
|
||||
{
|
||||
label: 'Stop',
|
||||
value: 'retry_later',
|
||||
key: 'retry_later',
|
||||
},
|
||||
],
|
||||
}),
|
||||
undefined,
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should call onChoice with "continue" when "Continue with flash" is selected', () => {
|
||||
const mockOnChoice = vi.fn();
|
||||
const { unmount } = render(
|
||||
<ProQuotaDialog
|
||||
fallbackModel="gemini-2.5-flash"
|
||||
onChoice={mockOnChoice}
|
||||
/>,
|
||||
);
|
||||
describe('onChoice handling', () => {
|
||||
it('should call onChoice with the selected value', () => {
|
||||
const { unmount } = render(
|
||||
<ProQuotaDialog
|
||||
failedModel="gemini-2.5-pro"
|
||||
fallbackModel="gemini-2.5-flash"
|
||||
message=""
|
||||
isTerminalQuotaError={false}
|
||||
onChoice={mockOnChoice}
|
||||
userTier={UserTierId.FREE}
|
||||
/>,
|
||||
);
|
||||
|
||||
// Get the onSelect function passed to RadioButtonSelect
|
||||
const onSelect = (RadioButtonSelect as Mock).mock.calls[0][0].onSelect;
|
||||
const onSelect = (RadioButtonSelect as Mock).mock.calls[0][0].onSelect;
|
||||
act(() => {
|
||||
onSelect('retry_always');
|
||||
});
|
||||
|
||||
// Simulate the selection
|
||||
act(() => {
|
||||
onSelect('retry');
|
||||
expect(mockOnChoice).toHaveBeenCalledWith('retry_always');
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
describe('footer note', () => {
|
||||
it('should show a special note for PREVIEW_GEMINI_MODEL', () => {
|
||||
const { lastFrame, unmount } = render(
|
||||
<ProQuotaDialog
|
||||
failedModel={PREVIEW_GEMINI_MODEL}
|
||||
fallbackModel="gemini-2.5-pro"
|
||||
message=""
|
||||
isTerminalQuotaError={false}
|
||||
onChoice={mockOnChoice}
|
||||
userTier={UserTierId.FREE}
|
||||
/>,
|
||||
);
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain(
|
||||
'Note: We will periodically retry Preview Model to see if congestion has cleared.',
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
|
||||
expect(mockOnChoice).toHaveBeenCalledWith('retry');
|
||||
unmount();
|
||||
it('should show the default note for other models', () => {
|
||||
const { lastFrame, unmount } = render(
|
||||
<ProQuotaDialog
|
||||
failedModel="gemini-2.5-pro"
|
||||
fallbackModel="gemini-2.5-flash"
|
||||
message=""
|
||||
isTerminalQuotaError={false}
|
||||
onChoice={mockOnChoice}
|
||||
userTier={UserTierId.FREE}
|
||||
/>,
|
||||
);
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain(
|
||||
'Note: You can always use /model to select a different option.',
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -9,43 +9,127 @@ import { Box, Text } from 'ink';
|
||||
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
|
||||
import {
|
||||
DEFAULT_GEMINI_FLASH_LITE_MODEL,
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
UserTierId,
|
||||
} from '@google/gemini-cli-core';
|
||||
|
||||
interface ProQuotaDialogProps {
|
||||
failedModel: string;
|
||||
fallbackModel: string;
|
||||
onChoice: (choice: 'retry_later' | 'retry') => void;
|
||||
message: string;
|
||||
isTerminalQuotaError: boolean;
|
||||
isModelNotFoundError?: boolean;
|
||||
onChoice: (
|
||||
choice: 'retry_later' | 'retry_once' | 'retry_always' | 'upgrade',
|
||||
) => void;
|
||||
userTier: UserTierId | undefined;
|
||||
}
|
||||
|
||||
export function ProQuotaDialog({
|
||||
failedModel,
|
||||
fallbackModel,
|
||||
message,
|
||||
isTerminalQuotaError,
|
||||
isModelNotFoundError,
|
||||
onChoice,
|
||||
userTier,
|
||||
}: ProQuotaDialogProps): React.JSX.Element {
|
||||
const items = [
|
||||
{
|
||||
label: 'Try again later',
|
||||
value: 'retry_later' as const,
|
||||
key: 'retry_later',
|
||||
},
|
||||
{
|
||||
label: `Switch to ${fallbackModel} for the rest of this session`,
|
||||
value: 'retry' as const,
|
||||
key: 'retry',
|
||||
},
|
||||
];
|
||||
// Use actual user tier if available; otherwise, default to FREE tier behavior (safe default)
|
||||
const isPaidTier =
|
||||
userTier === UserTierId.LEGACY || userTier === UserTierId.STANDARD;
|
||||
let items;
|
||||
// flash and flash lite don't have options to switch or upgrade.
|
||||
if (
|
||||
failedModel === DEFAULT_GEMINI_FLASH_MODEL ||
|
||||
failedModel === DEFAULT_GEMINI_FLASH_LITE_MODEL
|
||||
) {
|
||||
items = [
|
||||
{
|
||||
label: 'Keep trying',
|
||||
value: 'retry_once' as const,
|
||||
key: 'retry_once',
|
||||
},
|
||||
{
|
||||
label: 'Stop',
|
||||
value: 'retry_later' as const,
|
||||
key: 'retry_later',
|
||||
},
|
||||
];
|
||||
} else if (isModelNotFoundError || (isTerminalQuotaError && isPaidTier)) {
|
||||
// out of quota
|
||||
items = [
|
||||
{
|
||||
label: `Switch to ${fallbackModel}`,
|
||||
value: 'retry_always' as const,
|
||||
key: 'retry_always',
|
||||
},
|
||||
{
|
||||
label: `Stop`,
|
||||
value: 'retry_later' as const,
|
||||
key: 'retry_later',
|
||||
},
|
||||
];
|
||||
} else if (isTerminalQuotaError && !isPaidTier) {
|
||||
// free user gets an option to upgrade
|
||||
items = [
|
||||
{
|
||||
label: `Switch to ${fallbackModel}`,
|
||||
value: 'retry_always' as const,
|
||||
key: 'retry_always',
|
||||
},
|
||||
{
|
||||
label: 'Upgrade for higher limits',
|
||||
value: 'upgrade' as const,
|
||||
key: 'upgrade',
|
||||
},
|
||||
{
|
||||
label: `Stop`,
|
||||
value: 'retry_later' as const,
|
||||
key: 'retry_later',
|
||||
},
|
||||
];
|
||||
} else {
|
||||
// capacity error
|
||||
items = [
|
||||
{
|
||||
label: 'Keep trying',
|
||||
value: 'retry_once' as const,
|
||||
key: 'retry_once',
|
||||
},
|
||||
{
|
||||
label: `Switch to ${fallbackModel}`,
|
||||
value: 'retry_always' as const,
|
||||
key: 'retry_always',
|
||||
},
|
||||
{
|
||||
label: 'Stop',
|
||||
value: 'retry_later' as const,
|
||||
key: 'retry_later',
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
const handleSelect = (choice: 'retry_later' | 'retry') => {
|
||||
const handleSelect = (
|
||||
choice: 'retry_later' | 'retry_once' | 'retry_always' | 'upgrade',
|
||||
) => {
|
||||
onChoice(choice);
|
||||
};
|
||||
|
||||
return (
|
||||
<Box borderStyle="round" flexDirection="column" paddingX={1}>
|
||||
<Box borderStyle="round" flexDirection="column" padding={1}>
|
||||
<Box marginBottom={1}>
|
||||
<Text>{message}</Text>
|
||||
</Box>
|
||||
<Box marginTop={1} marginBottom={1}>
|
||||
<RadioButtonSelect
|
||||
items={items}
|
||||
initialIndex={1}
|
||||
onSelect={handleSelect}
|
||||
/>
|
||||
<RadioButtonSelect items={items} onSelect={handleSelect} />
|
||||
</Box>
|
||||
<Text color={theme.text.primary}>
|
||||
Note: You can always use /model to select a different option.
|
||||
{failedModel === PREVIEW_GEMINI_MODEL && !isModelNotFoundError
|
||||
? 'Note: We will periodically retry Preview Model to see if congestion has cleared.'
|
||||
: 'Note: You can always use /model to select a different option.'}
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -367,17 +367,17 @@ describe('SettingsDialog', () => {
|
||||
|
||||
const { stdin, unmount, lastFrame } = renderDialog(settings, onSelect);
|
||||
|
||||
// Wait for initial render and verify we're on Vim Mode (first setting)
|
||||
// Wait for initial render and verify we're on Preview Features (first setting)
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toContain('Vim Mode');
|
||||
expect(lastFrame()).toContain('Preview Features (e.g., models)');
|
||||
});
|
||||
|
||||
// Navigate to Disable Auto Update setting and verify we're there
|
||||
// Navigate to Vim Mode setting and verify we're there
|
||||
act(() => {
|
||||
stdin.write(TerminalKeys.DOWN_ARROW as string);
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toContain('Disable Auto Update');
|
||||
expect(lastFrame()).toContain('Vim Mode');
|
||||
});
|
||||
|
||||
// Toggle the setting
|
||||
@@ -397,10 +397,10 @@ describe('SettingsDialog', () => {
|
||||
});
|
||||
|
||||
expect(vi.mocked(saveModifiedSettings)).toHaveBeenCalledWith(
|
||||
new Set<string>(['general.disableAutoUpdate']),
|
||||
new Set<string>(['general.vimMode']),
|
||||
expect.objectContaining({
|
||||
general: expect.objectContaining({
|
||||
disableAutoUpdate: true,
|
||||
vimMode: true,
|
||||
}),
|
||||
}),
|
||||
expect.any(LoadedSettings),
|
||||
@@ -571,7 +571,7 @@ describe('SettingsDialog', () => {
|
||||
|
||||
// Wait for initial render
|
||||
await waitFor(() => {
|
||||
expect(lastFrame()).toContain('Hide Window Title');
|
||||
expect(lastFrame()).toContain('Vim Mode');
|
||||
});
|
||||
|
||||
// Verify the dialog is rendered properly
|
||||
|
||||
@@ -0,0 +1,118 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`<AppHeader /> > should not render the banner when no flags are set 1`] = `
|
||||
"
|
||||
███ █████████
|
||||
░░░███ ███░░░░░███
|
||||
░░░███ ███ ░░░
|
||||
░░░███░███
|
||||
███░ ░███ █████
|
||||
███░ ░░███ ░░███
|
||||
███░ ░░█████████
|
||||
░░░ ░░░░░░░░░
|
||||
|
||||
Tips for getting started:
|
||||
1. Ask questions, edit files, or run commands.
|
||||
2. Be specific for the best results.
|
||||
3. Create GEMINI.md files to customize your interactions with Gemini.
|
||||
4. /help for more information."
|
||||
`;
|
||||
|
||||
exports[`<AppHeader /> > should not render the banner when previewFeatures is enabled 1`] = `
|
||||
"
|
||||
███ █████████
|
||||
░░░███ ███░░░░░███
|
||||
░░░███ ███ ░░░
|
||||
░░░███░███
|
||||
███░ ░███ █████
|
||||
███░ ░░███ ░░███
|
||||
███░ ░░█████████
|
||||
░░░ ░░░░░░░░░
|
||||
|
||||
Tips for getting started:
|
||||
1. Ask questions, edit files, or run commands.
|
||||
2. Be specific for the best results.
|
||||
3. Create GEMINI.md files to customize your interactions with Gemini.
|
||||
4. /help for more information."
|
||||
`;
|
||||
|
||||
exports[`<AppHeader /> > should not render the default banner if shown count is 5 or more 1`] = `
|
||||
"
|
||||
███ █████████
|
||||
░░░███ ███░░░░░███
|
||||
░░░███ ███ ░░░
|
||||
░░░███░███
|
||||
███░ ░███ █████
|
||||
███░ ░░███ ░░███
|
||||
███░ ░░█████████
|
||||
░░░ ░░░░░░░░░
|
||||
|
||||
Tips for getting started:
|
||||
1. Ask questions, edit files, or run commands.
|
||||
2. Be specific for the best results.
|
||||
3. Create GEMINI.md files to customize your interactions with Gemini.
|
||||
4. /help for more information."
|
||||
`;
|
||||
|
||||
exports[`<AppHeader /> > should render the banner when previewFeatures is disabled 1`] = `
|
||||
"
|
||||
███ █████████
|
||||
░░░███ ███░░░░░███
|
||||
░░░███ ███ ░░░
|
||||
░░░███░███
|
||||
███░ ░███ █████
|
||||
███░ ░░███ ░░███
|
||||
███░ ░░█████████
|
||||
░░░ ░░░░░░░░░
|
||||
|
||||
╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ This is the default banner │
|
||||
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
Tips for getting started:
|
||||
1. Ask questions, edit files, or run commands.
|
||||
2. Be specific for the best results.
|
||||
3. Create GEMINI.md files to customize your interactions with Gemini.
|
||||
4. /help for more information."
|
||||
`;
|
||||
|
||||
exports[`<AppHeader /> > should render the banner with default text 1`] = `
|
||||
"
|
||||
███ █████████
|
||||
░░░███ ███░░░░░███
|
||||
░░░███ ███ ░░░
|
||||
░░░███░███
|
||||
███░ ░███ █████
|
||||
███░ ░░███ ░░███
|
||||
███░ ░░█████████
|
||||
░░░ ░░░░░░░░░
|
||||
|
||||
╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ This is the default banner │
|
||||
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
Tips for getting started:
|
||||
1. Ask questions, edit files, or run commands.
|
||||
2. Be specific for the best results.
|
||||
3. Create GEMINI.md files to customize your interactions with Gemini.
|
||||
4. /help for more information."
|
||||
`;
|
||||
|
||||
exports[`<AppHeader /> > should render the banner with warning text 1`] = `
|
||||
"
|
||||
███ █████████
|
||||
░░░███ ███░░░░░███
|
||||
░░░███ ███ ░░░
|
||||
░░░███░███
|
||||
███░ ░███ █████
|
||||
███░ ░░███ ░░███
|
||||
███░ ░░█████████
|
||||
░░░ ░░░░░░░░░
|
||||
|
||||
╭─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ There are capacity issues │
|
||||
╰─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
Tips for getting started:
|
||||
1. Ask questions, edit files, or run commands.
|
||||
2. Be specific for the best results.
|
||||
3. Create GEMINI.md files to customize your interactions with Gemini.
|
||||
4. /help for more information."
|
||||
`;
|
||||
@@ -6,7 +6,9 @@ exports[`SettingsDialog > Initial Rendering > should render settings list with v
|
||||
│ > Settings │
|
||||
│ │
|
||||
│ ▲ │
|
||||
│ ● Vim Mode false │
|
||||
│ ● Preview Features (e.g., models) false │
|
||||
│ │
|
||||
│ Vim Mode false │
|
||||
│ │
|
||||
│ Disable Auto Update false │
|
||||
│ │
|
||||
@@ -14,8 +16,6 @@ exports[`SettingsDialog > Initial Rendering > should render settings list with v
|
||||
│ │
|
||||
│ Debug Keystroke Logging false │
|
||||
│ │
|
||||
│ Session Retention undefined │
|
||||
│ │
|
||||
│ Enable Session Cleanup false │
|
||||
│ │
|
||||
│ Output Format Text │
|
||||
@@ -41,7 +41,9 @@ exports[`SettingsDialog > Snapshot Tests > should render 'accessibility settings
|
||||
│ > Settings │
|
||||
│ │
|
||||
│ ▲ │
|
||||
│ ● Vim Mode true* │
|
||||
│ ● Preview Features (e.g., models) false │
|
||||
│ │
|
||||
│ Vim Mode true* │
|
||||
│ │
|
||||
│ Disable Auto Update false │
|
||||
│ │
|
||||
@@ -49,8 +51,6 @@ exports[`SettingsDialog > Snapshot Tests > should render 'accessibility settings
|
||||
│ │
|
||||
│ Debug Keystroke Logging false │
|
||||
│ │
|
||||
│ Session Retention undefined │
|
||||
│ │
|
||||
│ Enable Session Cleanup false │
|
||||
│ │
|
||||
│ Output Format Text │
|
||||
@@ -76,7 +76,9 @@ exports[`SettingsDialog > Snapshot Tests > should render 'all boolean settings d
|
||||
│ > Settings │
|
||||
│ │
|
||||
│ ▲ │
|
||||
│ ● Vim Mode false* │
|
||||
│ ● Preview Features (e.g., models) false │
|
||||
│ │
|
||||
│ Vim Mode false* │
|
||||
│ │
|
||||
│ Disable Auto Update false* │
|
||||
│ │
|
||||
@@ -84,8 +86,6 @@ exports[`SettingsDialog > Snapshot Tests > should render 'all boolean settings d
|
||||
│ │
|
||||
│ Debug Keystroke Logging false* │
|
||||
│ │
|
||||
│ Session Retention undefined │
|
||||
│ │
|
||||
│ Enable Session Cleanup false │
|
||||
│ │
|
||||
│ Output Format Text │
|
||||
@@ -111,7 +111,9 @@ exports[`SettingsDialog > Snapshot Tests > should render 'default state' correct
|
||||
│ > Settings │
|
||||
│ │
|
||||
│ ▲ │
|
||||
│ ● Vim Mode false │
|
||||
│ ● Preview Features (e.g., models) false │
|
||||
│ │
|
||||
│ Vim Mode false │
|
||||
│ │
|
||||
│ Disable Auto Update false │
|
||||
│ │
|
||||
@@ -119,8 +121,6 @@ exports[`SettingsDialog > Snapshot Tests > should render 'default state' correct
|
||||
│ │
|
||||
│ Debug Keystroke Logging false │
|
||||
│ │
|
||||
│ Session Retention undefined │
|
||||
│ │
|
||||
│ Enable Session Cleanup false │
|
||||
│ │
|
||||
│ Output Format Text │
|
||||
@@ -146,7 +146,9 @@ exports[`SettingsDialog > Snapshot Tests > should render 'file filtering setting
|
||||
│ > Settings │
|
||||
│ │
|
||||
│ ▲ │
|
||||
│ ● Vim Mode false │
|
||||
│ ● Preview Features (e.g., models) false │
|
||||
│ │
|
||||
│ Vim Mode false │
|
||||
│ │
|
||||
│ Disable Auto Update false │
|
||||
│ │
|
||||
@@ -154,8 +156,6 @@ exports[`SettingsDialog > Snapshot Tests > should render 'file filtering setting
|
||||
│ │
|
||||
│ Debug Keystroke Logging false │
|
||||
│ │
|
||||
│ Session Retention undefined │
|
||||
│ │
|
||||
│ Enable Session Cleanup false │
|
||||
│ │
|
||||
│ Output Format Text │
|
||||
@@ -181,6 +181,8 @@ exports[`SettingsDialog > Snapshot Tests > should render 'focused on scope selec
|
||||
│ Settings │
|
||||
│ │
|
||||
│ ▲ │
|
||||
│ Preview Features (e.g., models) false │
|
||||
│ │
|
||||
│ Vim Mode false │
|
||||
│ │
|
||||
│ Disable Auto Update false │
|
||||
@@ -189,8 +191,6 @@ exports[`SettingsDialog > Snapshot Tests > should render 'focused on scope selec
|
||||
│ │
|
||||
│ Debug Keystroke Logging false │
|
||||
│ │
|
||||
│ Session Retention undefined │
|
||||
│ │
|
||||
│ Enable Session Cleanup false │
|
||||
│ │
|
||||
│ Output Format Text │
|
||||
@@ -216,7 +216,9 @@ exports[`SettingsDialog > Snapshot Tests > should render 'mixed boolean and numb
|
||||
│ > Settings │
|
||||
│ │
|
||||
│ ▲ │
|
||||
│ ● Vim Mode false* │
|
||||
│ ● Preview Features (e.g., models) false │
|
||||
│ │
|
||||
│ Vim Mode false* │
|
||||
│ │
|
||||
│ Disable Auto Update true* │
|
||||
│ │
|
||||
@@ -224,8 +226,6 @@ exports[`SettingsDialog > Snapshot Tests > should render 'mixed boolean and numb
|
||||
│ │
|
||||
│ Debug Keystroke Logging false │
|
||||
│ │
|
||||
│ Session Retention undefined │
|
||||
│ │
|
||||
│ Enable Session Cleanup false │
|
||||
│ │
|
||||
│ Output Format Text │
|
||||
@@ -251,7 +251,9 @@ exports[`SettingsDialog > Snapshot Tests > should render 'tools and security set
|
||||
│ > Settings │
|
||||
│ │
|
||||
│ ▲ │
|
||||
│ ● Vim Mode false │
|
||||
│ ● Preview Features (e.g., models) false │
|
||||
│ │
|
||||
│ Vim Mode false │
|
||||
│ │
|
||||
│ Disable Auto Update false │
|
||||
│ │
|
||||
@@ -259,8 +261,6 @@ exports[`SettingsDialog > Snapshot Tests > should render 'tools and security set
|
||||
│ │
|
||||
│ Debug Keystroke Logging false │
|
||||
│ │
|
||||
│ Session Retention undefined │
|
||||
│ │
|
||||
│ Enable Session Cleanup false │
|
||||
│ │
|
||||
│ Output Format Text │
|
||||
@@ -286,7 +286,9 @@ exports[`SettingsDialog > Snapshot Tests > should render 'various boolean settin
|
||||
│ > Settings │
|
||||
│ │
|
||||
│ ▲ │
|
||||
│ ● Vim Mode true* │
|
||||
│ ● Preview Features (e.g., models) false │
|
||||
│ │
|
||||
│ Vim Mode true* │
|
||||
│ │
|
||||
│ Disable Auto Update true* │
|
||||
│ │
|
||||
@@ -294,8 +296,6 @@ exports[`SettingsDialog > Snapshot Tests > should render 'various boolean settin
|
||||
│ │
|
||||
│ Debug Keystroke Logging true* │
|
||||
│ │
|
||||
│ Session Retention undefined │
|
||||
│ │
|
||||
│ Enable Session Cleanup false │
|
||||
│ │
|
||||
│ Output Format Text │
|
||||
|
||||
Reference in New Issue
Block a user