mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-21 18:44:30 -07:00
FEAT: Add availabilityService (#81)
* auto and fallback work * test fixes * fixes * Show model dialog even if there's no more fallback option * fix tests * fix failing test * disable showInDialog for model in settings * revert package-lock.json * remove dup line --------- Co-authored-by: Sehoon Shon <sshon@google.com>
This commit is contained in:
committed by
Tommaso Sciortino
parent
ad994cfe8b
commit
48ad6983a3
@@ -17,7 +17,7 @@ import {
|
||||
import { render } from '../test-utils/render.js';
|
||||
import { waitFor } from '../test-utils/async.js';
|
||||
import { cleanup } from 'ink-testing-library';
|
||||
import { act, useContext } from 'react';
|
||||
import { act, useContext, type ReactElement } from 'react';
|
||||
import { AppContainer } from './AppContainer.js';
|
||||
import { SettingsContext } from './contexts/SettingsContext.js';
|
||||
import {
|
||||
@@ -71,6 +71,14 @@ vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
})),
|
||||
enableMouseEvents: vi.fn(),
|
||||
disableMouseEvents: vi.fn(),
|
||||
FileDiscoveryService: vi.fn().mockImplementation(() => ({
|
||||
initialize: vi.fn(),
|
||||
})),
|
||||
startupProfiler: {
|
||||
flush: vi.fn(),
|
||||
start: vi.fn(),
|
||||
end: vi.fn(),
|
||||
},
|
||||
};
|
||||
});
|
||||
import ansiEscapes from 'ansi-escapes';
|
||||
@@ -344,7 +352,7 @@ describe('AppContainer State Management', () => {
|
||||
// Add other properties if AppContainer uses them
|
||||
});
|
||||
mockedUseLogger.mockReturnValue({
|
||||
getPreviousUserMessages: vi.fn().mockReturnValue(new Promise(() => {})),
|
||||
getPreviousUserMessages: vi.fn().mockResolvedValue([]),
|
||||
});
|
||||
mockedUseInputHistoryStore.mockReturnValue({
|
||||
inputHistory: [],
|
||||
@@ -361,6 +369,8 @@ describe('AppContainer State Management', () => {
|
||||
|
||||
// Mock config's getTargetDir to return consistent workspace directory
|
||||
vi.spyOn(mockConfig, 'getTargetDir').mockReturnValue('/test/workspace');
|
||||
vi.spyOn(mockConfig, 'initialize').mockResolvedValue(undefined);
|
||||
vi.spyOn(mockConfig, 'getDebugMode').mockReturnValue(false);
|
||||
|
||||
mockExtensionManager = vi.mockObject({
|
||||
getExtensions: vi.fn().mockReturnValue([]),
|
||||
@@ -403,17 +413,25 @@ describe('AppContainer State Management', () => {
|
||||
|
||||
describe('Basic Rendering', () => {
|
||||
it('renders without crashing with minimal props', async () => {
|
||||
const { unmount } = renderAppContainer();
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer();
|
||||
unmount = result.unmount;
|
||||
});
|
||||
await waitFor(() => expect(capturedUIState).toBeTruthy());
|
||||
unmount();
|
||||
unmount!();
|
||||
});
|
||||
|
||||
it('renders with startup warnings', async () => {
|
||||
const startupWarnings = ['Warning 1', 'Warning 2'];
|
||||
|
||||
const { unmount } = renderAppContainer({ startupWarnings });
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer({ startupWarnings });
|
||||
unmount = result.unmount;
|
||||
});
|
||||
await waitFor(() => expect(capturedUIState).toBeTruthy());
|
||||
unmount();
|
||||
unmount!();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -424,11 +442,15 @@ describe('AppContainer State Management', () => {
|
||||
themeError: 'Failed to load theme',
|
||||
};
|
||||
|
||||
const { unmount } = renderAppContainer({
|
||||
initResult: initResultWithError,
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer({
|
||||
initResult: initResultWithError,
|
||||
});
|
||||
unmount = result.unmount;
|
||||
});
|
||||
await waitFor(() => expect(capturedUIState).toBeTruthy());
|
||||
unmount();
|
||||
unmount!();
|
||||
});
|
||||
|
||||
it('handles debug mode state', () => {
|
||||
@@ -443,29 +465,45 @@ describe('AppContainer State Management', () => {
|
||||
|
||||
describe('Context Providers', () => {
|
||||
it('provides AppContext with correct values', async () => {
|
||||
const { unmount } = renderAppContainer({ version: '2.0.0' });
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer({ version: '2.0.0' });
|
||||
unmount = result.unmount;
|
||||
});
|
||||
await waitFor(() => expect(capturedUIState).toBeTruthy());
|
||||
|
||||
// Should render and unmount cleanly
|
||||
expect(() => unmount()).not.toThrow();
|
||||
expect(() => unmount!()).not.toThrow();
|
||||
});
|
||||
|
||||
it('provides UIStateContext with state management', async () => {
|
||||
const { unmount } = renderAppContainer();
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer();
|
||||
unmount = result.unmount;
|
||||
});
|
||||
await waitFor(() => expect(capturedUIState).toBeTruthy());
|
||||
unmount();
|
||||
unmount!();
|
||||
});
|
||||
|
||||
it('provides UIActionsContext with action handlers', async () => {
|
||||
const { unmount } = renderAppContainer();
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer();
|
||||
unmount = result.unmount;
|
||||
});
|
||||
await waitFor(() => expect(capturedUIState).toBeTruthy());
|
||||
unmount();
|
||||
unmount!();
|
||||
});
|
||||
|
||||
it('provides ConfigContext with config object', async () => {
|
||||
const { unmount } = renderAppContainer();
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer();
|
||||
unmount = result.unmount;
|
||||
});
|
||||
await waitFor(() => expect(capturedUIState).toBeTruthy());
|
||||
unmount();
|
||||
unmount!();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -480,9 +518,13 @@ describe('AppContainer State Management', () => {
|
||||
},
|
||||
} as unknown as LoadedSettings;
|
||||
|
||||
const { unmount } = renderAppContainer({ settings: settingsAllHidden });
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer({ settings: settingsAllHidden });
|
||||
unmount = result.unmount;
|
||||
});
|
||||
await waitFor(() => expect(capturedUIState).toBeTruthy());
|
||||
unmount();
|
||||
unmount!();
|
||||
});
|
||||
|
||||
it('handles settings with memory usage enabled', async () => {
|
||||
@@ -495,9 +537,13 @@ describe('AppContainer State Management', () => {
|
||||
},
|
||||
} as unknown as LoadedSettings;
|
||||
|
||||
const { unmount } = renderAppContainer({ settings: settingsWithMemory });
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer({ settings: settingsWithMemory });
|
||||
unmount = result.unmount;
|
||||
});
|
||||
await waitFor(() => expect(capturedUIState).toBeTruthy());
|
||||
unmount();
|
||||
unmount!();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -505,9 +551,13 @@ describe('AppContainer State Management', () => {
|
||||
it.each(['1.0.0', '2.1.3-beta', '3.0.0-nightly'])(
|
||||
'handles version format: %s',
|
||||
async (version) => {
|
||||
const { unmount } = renderAppContainer({ version });
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer({ version });
|
||||
unmount = result.unmount;
|
||||
});
|
||||
await waitFor(() => expect(capturedUIState).toBeTruthy());
|
||||
unmount();
|
||||
unmount!();
|
||||
},
|
||||
);
|
||||
});
|
||||
@@ -529,9 +579,13 @@ describe('AppContainer State Management', () => {
|
||||
merged: {},
|
||||
} as LoadedSettings;
|
||||
|
||||
const { unmount } = renderAppContainer({ settings: undefinedSettings });
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer({ settings: undefinedSettings });
|
||||
unmount = result.unmount;
|
||||
});
|
||||
await waitFor(() => expect(capturedUIState).toBeTruthy());
|
||||
unmount();
|
||||
unmount!();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -860,12 +914,16 @@ describe('AppContainer State Management', () => {
|
||||
describe('Quota and Fallback Integration', () => {
|
||||
it('passes a null proQuotaRequest to UIStateContext by default', async () => {
|
||||
// The default mock from beforeEach already sets proQuotaRequest to null
|
||||
const { unmount } = renderAppContainer();
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer();
|
||||
unmount = result.unmount;
|
||||
});
|
||||
await waitFor(() => {
|
||||
// Assert that the context value is as expected
|
||||
expect(capturedUIState.proQuotaRequest).toBeNull();
|
||||
});
|
||||
unmount();
|
||||
unmount!();
|
||||
});
|
||||
|
||||
it('passes a valid proQuotaRequest to UIStateContext when provided by the hook', async () => {
|
||||
@@ -881,12 +939,16 @@ describe('AppContainer State Management', () => {
|
||||
});
|
||||
|
||||
// Act: Render the container
|
||||
const { unmount } = renderAppContainer();
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer();
|
||||
unmount = result.unmount;
|
||||
});
|
||||
await waitFor(() => {
|
||||
// Assert: The mock request is correctly passed through the context
|
||||
expect(capturedUIState.proQuotaRequest).toEqual(mockRequest);
|
||||
});
|
||||
unmount();
|
||||
unmount!();
|
||||
});
|
||||
|
||||
it('passes the handleProQuotaChoice function to UIActionsContext', async () => {
|
||||
@@ -898,7 +960,11 @@ describe('AppContainer State Management', () => {
|
||||
});
|
||||
|
||||
// Act: Render the container
|
||||
const { unmount } = renderAppContainer();
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer();
|
||||
unmount = result.unmount;
|
||||
});
|
||||
await waitFor(() => {
|
||||
// Assert: The action in the context is the mock handler we provided
|
||||
expect(capturedUIActions.handleProQuotaChoice).toBe(mockHandler);
|
||||
@@ -909,7 +975,7 @@ describe('AppContainer State Management', () => {
|
||||
capturedUIActions.handleProQuotaChoice('retry_later');
|
||||
});
|
||||
expect(mockHandler).toHaveBeenCalledWith('retry_later');
|
||||
unmount();
|
||||
unmount!();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1327,13 +1393,17 @@ describe('AppContainer State Management', () => {
|
||||
activePtyId: 'some-id',
|
||||
});
|
||||
|
||||
const { unmount } = renderAppContainer();
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer();
|
||||
unmount = result.unmount;
|
||||
});
|
||||
await waitFor(() => expect(resizePtySpy).toHaveBeenCalled());
|
||||
const lastCall =
|
||||
resizePtySpy.mock.calls[resizePtySpy.mock.calls.length - 1];
|
||||
// Check the height argument specifically
|
||||
expect(lastCall[2]).toBe(1);
|
||||
unmount();
|
||||
unmount!();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1672,11 +1742,15 @@ describe('AppContainer State Management', () => {
|
||||
closeModelDialog: vi.fn(),
|
||||
});
|
||||
|
||||
const { unmount } = renderAppContainer();
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer();
|
||||
unmount = result.unmount;
|
||||
});
|
||||
await waitFor(() => expect(capturedUIState).toBeTruthy());
|
||||
|
||||
expect(capturedUIState.isModelDialogOpen).toBe(true);
|
||||
unmount();
|
||||
unmount!();
|
||||
});
|
||||
|
||||
it('should provide model dialog actions in the UIActionsContext', async () => {
|
||||
@@ -1688,7 +1762,11 @@ describe('AppContainer State Management', () => {
|
||||
closeModelDialog: mockCloseModelDialog,
|
||||
});
|
||||
|
||||
const { unmount } = renderAppContainer();
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer();
|
||||
unmount = result.unmount;
|
||||
});
|
||||
await waitFor(() => expect(capturedUIState).toBeTruthy());
|
||||
|
||||
// Verify that the actions are correctly passed through context
|
||||
@@ -1696,13 +1774,17 @@ describe('AppContainer State Management', () => {
|
||||
capturedUIActions.closeModelDialog();
|
||||
});
|
||||
expect(mockCloseModelDialog).toHaveBeenCalled();
|
||||
unmount();
|
||||
unmount!();
|
||||
});
|
||||
});
|
||||
|
||||
describe('CoreEvents Integration', () => {
|
||||
it('subscribes to UserFeedback and drains backlog on mount', async () => {
|
||||
const { unmount } = renderAppContainer();
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer();
|
||||
unmount = result.unmount;
|
||||
});
|
||||
await waitFor(() => expect(capturedUIState).toBeTruthy());
|
||||
|
||||
expect(mockCoreEvents.on).toHaveBeenCalledWith(
|
||||
@@ -1710,14 +1792,18 @@ describe('AppContainer State Management', () => {
|
||||
expect.any(Function),
|
||||
);
|
||||
expect(mockCoreEvents.drainBacklogs).toHaveBeenCalledTimes(1);
|
||||
unmount();
|
||||
unmount!();
|
||||
});
|
||||
|
||||
it('unsubscribes from UserFeedback on unmount', async () => {
|
||||
const { unmount } = renderAppContainer();
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer();
|
||||
unmount = result.unmount;
|
||||
});
|
||||
await waitFor(() => expect(capturedUIState).toBeTruthy());
|
||||
|
||||
unmount();
|
||||
unmount!();
|
||||
|
||||
expect(mockCoreEvents.off).toHaveBeenCalledWith(
|
||||
CoreEvent.UserFeedback,
|
||||
@@ -1726,7 +1812,11 @@ describe('AppContainer State Management', () => {
|
||||
});
|
||||
|
||||
it('adds history item when UserFeedback event is received', async () => {
|
||||
const { unmount } = renderAppContainer();
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer();
|
||||
unmount = result.unmount;
|
||||
});
|
||||
await waitFor(() => expect(capturedUIState).toBeTruthy());
|
||||
|
||||
// Get the registered handler
|
||||
@@ -1751,14 +1841,18 @@ describe('AppContainer State Management', () => {
|
||||
}),
|
||||
expect.any(Number),
|
||||
);
|
||||
unmount();
|
||||
unmount!();
|
||||
});
|
||||
|
||||
it('updates currentModel when ModelChanged event is received', async () => {
|
||||
// Arrange: Mock initial model
|
||||
vi.spyOn(mockConfig, 'getModel').mockReturnValue('initial-model');
|
||||
|
||||
const { unmount } = renderAppContainer();
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer();
|
||||
unmount = result.unmount;
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(capturedUIState?.currentModel).toBe('initial-model');
|
||||
});
|
||||
@@ -1770,13 +1864,15 @@ describe('AppContainer State Management', () => {
|
||||
expect(handler).toBeDefined();
|
||||
|
||||
// Act: Simulate ModelChanged event
|
||||
// Update config mock to return new model since the handler reads from config
|
||||
vi.spyOn(mockConfig, 'getModel').mockReturnValue('new-model');
|
||||
act(() => {
|
||||
handler({ model: 'new-model' });
|
||||
});
|
||||
|
||||
// Assert: Verify model is updated
|
||||
expect(capturedUIState.currentModel).toBe('new-model');
|
||||
unmount();
|
||||
unmount!();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1799,10 +1895,14 @@ describe('AppContainer State Management', () => {
|
||||
});
|
||||
|
||||
// The main assertion is that the render does not throw.
|
||||
const { unmount } = renderAppContainer();
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer();
|
||||
unmount = result.unmount;
|
||||
});
|
||||
|
||||
await waitFor(() => expect(resizePtySpy).toHaveBeenCalled());
|
||||
unmount();
|
||||
unmount!();
|
||||
});
|
||||
});
|
||||
describe('Banner Text', () => {
|
||||
@@ -1812,10 +1912,14 @@ describe('AppContainer State Management', () => {
|
||||
authType: AuthType.USE_GEMINI,
|
||||
apiKey: 'fake-key',
|
||||
});
|
||||
const { unmount } = renderAppContainer();
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer();
|
||||
unmount = result.unmount;
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(capturedUIState.bannerData.defaultText).toBeDefined();
|
||||
unmount();
|
||||
unmount!();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1838,7 +1942,11 @@ describe('AppContainer State Management', () => {
|
||||
});
|
||||
|
||||
it('clears the prompt when onCancelSubmit is called with shouldRestorePrompt=false', async () => {
|
||||
const { unmount } = renderAppContainer();
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer();
|
||||
unmount = result.unmount;
|
||||
});
|
||||
await waitFor(() => expect(capturedUIState).toBeTruthy());
|
||||
|
||||
const { onCancelSubmit } = extractUseGeminiStreamArgs(
|
||||
@@ -1851,7 +1959,7 @@ describe('AppContainer State Management', () => {
|
||||
|
||||
expect(mockSetText).toHaveBeenCalledWith('');
|
||||
|
||||
unmount();
|
||||
unmount!();
|
||||
});
|
||||
|
||||
it('restores the prompt when onCancelSubmit is called with shouldRestorePrompt=true (or undefined)', async () => {
|
||||
@@ -1862,7 +1970,11 @@ describe('AppContainer State Management', () => {
|
||||
initializeFromLogger: vi.fn(),
|
||||
});
|
||||
|
||||
const { unmount } = renderAppContainer();
|
||||
let unmount: () => void;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer();
|
||||
unmount = result.unmount;
|
||||
});
|
||||
await waitFor(() =>
|
||||
expect(capturedUIState.userMessages).toContain('previous message'),
|
||||
);
|
||||
@@ -1877,7 +1989,7 @@ describe('AppContainer State Management', () => {
|
||||
|
||||
expect(mockSetText).toHaveBeenCalledWith('previous message');
|
||||
|
||||
unmount();
|
||||
unmount!();
|
||||
});
|
||||
|
||||
it('input history is independent from conversation history (survives /clear)', async () => {
|
||||
@@ -1890,7 +2002,13 @@ describe('AppContainer State Management', () => {
|
||||
initializeFromLogger: vi.fn(),
|
||||
});
|
||||
|
||||
const { unmount } = renderAppContainer();
|
||||
let rerender: (tree: ReactElement) => void;
|
||||
let unmount;
|
||||
await act(async () => {
|
||||
const result = renderAppContainer();
|
||||
rerender = result.rerender;
|
||||
unmount = result.unmount;
|
||||
});
|
||||
|
||||
// Verify userMessages is populated from inputHistory
|
||||
await waitFor(() =>
|
||||
@@ -1908,12 +2026,17 @@ describe('AppContainer State Management', () => {
|
||||
loadHistory: vi.fn(),
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
// Rerender to apply the new mock.
|
||||
rerender(getAppContainer());
|
||||
});
|
||||
|
||||
// Verify that userMessages still contains the input history
|
||||
// (it should not be affected by clearing conversation history)
|
||||
expect(capturedUIState.userMessages).toContain('first prompt');
|
||||
expect(capturedUIState.userMessages).toContain('second prompt');
|
||||
|
||||
unmount();
|
||||
unmount!();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1928,7 +2051,11 @@ describe('AppContainer State Management', () => {
|
||||
// Clear previous calls
|
||||
mocks.mockStdout.write.mockClear();
|
||||
|
||||
const { unmount } = renderAppContainer();
|
||||
let compUnmount: () => void = () => {};
|
||||
await act(async () => {
|
||||
const { unmount } = renderAppContainer();
|
||||
compUnmount = unmount;
|
||||
});
|
||||
|
||||
// Allow async effects to run
|
||||
await waitFor(() => expect(capturedUIState).toBeTruthy());
|
||||
@@ -1944,7 +2071,7 @@ describe('AppContainer State Management', () => {
|
||||
);
|
||||
|
||||
expect(clearTerminalCalls).toHaveLength(0);
|
||||
unmount();
|
||||
compUnmount();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -35,7 +35,6 @@ import {
|
||||
type IdeContext,
|
||||
type UserTierId,
|
||||
type UserFeedbackPayload,
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
IdeClient,
|
||||
ideContextStore,
|
||||
getErrorMessage,
|
||||
@@ -50,7 +49,6 @@ import {
|
||||
coreEvents,
|
||||
CoreEvent,
|
||||
refreshServerHierarchicalMemory,
|
||||
type ModelChangedPayload,
|
||||
type MemoryChangedPayload,
|
||||
writeToStdout,
|
||||
disableMouseEvents,
|
||||
@@ -256,12 +254,7 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
);
|
||||
|
||||
// Helper to determine the effective model, considering the fallback state.
|
||||
const getEffectiveModel = useCallback(() => {
|
||||
if (config.isInFallbackMode()) {
|
||||
return DEFAULT_GEMINI_FLASH_MODEL;
|
||||
}
|
||||
return config.getModel();
|
||||
}, [config]);
|
||||
const getEffectiveModel = useCallback(() => config.getModel(), [config]);
|
||||
|
||||
const [currentModel, setCurrentModel] = useState(getEffectiveModel());
|
||||
|
||||
@@ -340,22 +333,15 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
|
||||
// Subscribe to fallback mode and model changes from core
|
||||
useEffect(() => {
|
||||
const handleFallbackModeChanged = () => {
|
||||
const effectiveModel = getEffectiveModel();
|
||||
setCurrentModel(effectiveModel);
|
||||
const handleModelChanged = () => {
|
||||
setCurrentModel(config.getModel());
|
||||
};
|
||||
|
||||
const handleModelChanged = (payload: ModelChangedPayload) => {
|
||||
setCurrentModel(payload.model);
|
||||
};
|
||||
|
||||
coreEvents.on(CoreEvent.FallbackModeChanged, handleFallbackModeChanged);
|
||||
coreEvents.on(CoreEvent.ModelChanged, handleModelChanged);
|
||||
return () => {
|
||||
coreEvents.off(CoreEvent.FallbackModeChanged, handleFallbackModeChanged);
|
||||
coreEvents.off(CoreEvent.ModelChanged, handleModelChanged);
|
||||
};
|
||||
}, [getEffectiveModel]);
|
||||
}, [getEffectiveModel, config]);
|
||||
|
||||
const { consoleMessages, clearConsoleMessages: clearConsoleMessagesState } =
|
||||
useConsoleMessages();
|
||||
|
||||
@@ -7,7 +7,11 @@
|
||||
import type React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { shortenPath, tildeifyPath } from '@google/gemini-cli-core';
|
||||
import {
|
||||
shortenPath,
|
||||
tildeifyPath,
|
||||
getDisplayString,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { ConsoleSummaryDisplay } from './ConsoleSummaryDisplay.js';
|
||||
import process from 'node:process';
|
||||
import { ThemedGradient } from './ThemedGradient.js';
|
||||
@@ -145,7 +149,7 @@ export const Footer: React.FC = () => {
|
||||
<Box alignItems="center" justifyContent="flex-end">
|
||||
<Box alignItems="center">
|
||||
<Text color={theme.text.accent}>
|
||||
{model}
|
||||
{getDisplayString(model)}
|
||||
{!hideContextPercentage && (
|
||||
<>
|
||||
{' '}
|
||||
|
||||
@@ -13,6 +13,8 @@ import {
|
||||
DEFAULT_GEMINI_FLASH_LITE_MODEL,
|
||||
DEFAULT_GEMINI_FLASH_MODEL,
|
||||
DEFAULT_GEMINI_MODEL,
|
||||
FLASH_PREVIEW_MODEL_REVERT_BEFORE_MERGE,
|
||||
PREVIEW_GEMINI_FLASH_MODEL,
|
||||
UserTierId,
|
||||
} from '@google/gemini-cli-core';
|
||||
|
||||
@@ -44,7 +46,9 @@ export function ProQuotaDialog({
|
||||
// flash and flash lite don't have options to switch or upgrade.
|
||||
if (
|
||||
failedModel === DEFAULT_GEMINI_FLASH_MODEL ||
|
||||
failedModel === DEFAULT_GEMINI_FLASH_LITE_MODEL
|
||||
failedModel === DEFAULT_GEMINI_FLASH_LITE_MODEL ||
|
||||
failedModel === PREVIEW_GEMINI_FLASH_MODEL ||
|
||||
failedModel === FLASH_PREVIEW_MODEL_REVERT_BEFORE_MERGE
|
||||
) {
|
||||
items = [
|
||||
{
|
||||
|
||||
@@ -283,7 +283,7 @@ describe('useQuotaAndFallback', () => {
|
||||
const lastCall = (mockHistoryManager.addItem as Mock).mock
|
||||
.calls[0][0];
|
||||
expect(lastCall.type).toBe(MessageType.INFO);
|
||||
expect(lastCall.text).toContain('Switched to fallback model.');
|
||||
expect(lastCall.text).toContain('Switched to fallback model model-B');
|
||||
});
|
||||
}
|
||||
|
||||
@@ -316,9 +316,9 @@ describe('useQuotaAndFallback', () => {
|
||||
|
||||
const message = request!.message;
|
||||
expect(message).toBe(
|
||||
`It seems like you don't have access to Gemini 3.
|
||||
`It seems like you don't have access to gemini-3-pro-preview.
|
||||
Learn more at https://goo.gle/enable-preview-features
|
||||
To disable Gemini 3, disable "Preview features" in /settings.`,
|
||||
To disable gemini-3-pro-preview, disable "Preview features" in /settings.`,
|
||||
);
|
||||
|
||||
// Simulate the user choosing to switch
|
||||
@@ -415,7 +415,9 @@ To disable Gemini 3, disable "Preview features" in /settings.`,
|
||||
expect(mockHistoryManager.addItem).toHaveBeenCalledTimes(1);
|
||||
const lastCall = (mockHistoryManager.addItem as Mock).mock.calls[0][0];
|
||||
expect(lastCall.type).toBe(MessageType.INFO);
|
||||
expect(lastCall.text).toContain('Switched to fallback model.');
|
||||
expect(lastCall.text).toContain(
|
||||
'Switched to fallback model gemini-flash',
|
||||
);
|
||||
});
|
||||
|
||||
it('should show a special message when falling back from the preview model', async () => {
|
||||
@@ -449,7 +451,7 @@ To disable Gemini 3, disable "Preview features" in /settings.`,
|
||||
const lastCall = (mockHistoryManager.addItem as Mock).mock.calls[0][0];
|
||||
expect(lastCall.type).toBe(MessageType.INFO);
|
||||
expect(lastCall.text).toContain(
|
||||
`Switched to fallback model gemini-2.5-pro. We will periodically check if ${PREVIEW_GEMINI_MODEL} is available again.`,
|
||||
`Switched to fallback model gemini-2.5-pro`,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -484,7 +486,7 @@ To disable Gemini 3, disable "Preview features" in /settings.`,
|
||||
const lastCall = (mockHistoryManager.addItem as Mock).mock.calls[0][0];
|
||||
expect(lastCall.type).toBe(MessageType.INFO);
|
||||
expect(lastCall.text).toContain(
|
||||
`Switched to fallback model gemini-2.5-flash.`,
|
||||
`Switched to fallback model gemini-2.5-flash`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
type UserTierId,
|
||||
PREVIEW_GEMINI_MODEL,
|
||||
DEFAULT_GEMINI_MODEL,
|
||||
VALID_GEMINI_MODELS,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { type UseHistoryManagerReturn } from './useHistoryManager.js';
|
||||
@@ -71,12 +72,15 @@ export function useQuotaAndFallback({
|
||||
`/auth to switch to API key.`,
|
||||
].filter(Boolean);
|
||||
message = messageLines.join('\n');
|
||||
} else if (error instanceof ModelNotFoundError) {
|
||||
} else if (
|
||||
error instanceof ModelNotFoundError &&
|
||||
VALID_GEMINI_MODELS.has(failedModel)
|
||||
) {
|
||||
isModelNotFoundError = true;
|
||||
const messageLines = [
|
||||
`It seems like you don't have access to Gemini 3.`,
|
||||
`It seems like you don't have access to ${failedModel}.`,
|
||||
`Learn more at https://goo.gle/enable-preview-features`,
|
||||
`To disable Gemini 3, disable "Preview features" in /settings.`,
|
||||
`To disable ${failedModel}, disable "Preview features" in /settings.`,
|
||||
];
|
||||
message = messageLines.join('\n');
|
||||
} else {
|
||||
@@ -120,30 +124,20 @@ export function useQuotaAndFallback({
|
||||
isDialogPending.current = false; // Reset the flag here
|
||||
|
||||
if (choice === 'retry_always') {
|
||||
// If we were recovering from a Preview Model failure, show a specific message.
|
||||
if (proQuotaRequest.failedModel === PREVIEW_GEMINI_MODEL) {
|
||||
const showPeriodicalCheckMessage =
|
||||
!proQuotaRequest.isModelNotFoundError &&
|
||||
proQuotaRequest.fallbackModel === DEFAULT_GEMINI_MODEL;
|
||||
historyManager.addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: `Switched to fallback model ${proQuotaRequest.fallbackModel}. ${showPeriodicalCheckMessage ? `We will periodically check if ${PREVIEW_GEMINI_MODEL} is available again.` : ''}`,
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
} else {
|
||||
historyManager.addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: 'Switched to fallback model.',
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
}
|
||||
// Explicitly set the model to the fallback model to persist the user's choice.
|
||||
// This ensures the Footer updates and future turns use this model.
|
||||
config.setModel(proQuotaRequest.fallbackModel);
|
||||
|
||||
historyManager.addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: `Switched to fallback model ${proQuotaRequest.fallbackModel}`,
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
}
|
||||
},
|
||||
[proQuotaRequest, historyManager],
|
||||
[proQuotaRequest, historyManager, config],
|
||||
);
|
||||
|
||||
return {
|
||||
|
||||
Reference in New Issue
Block a user