feat: add Pro Quota Dialog (#7094)

Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Jacob Richman <jacob314@gmail.com>
This commit is contained in:
JAYADITYA
2025-08-29 04:19:43 +05:30
committed by GitHub
parent a0fbe000ee
commit a63e67823d
3 changed files with 218 additions and 3 deletions
+77 -3
View File
@@ -70,6 +70,7 @@ import {
isProQuotaExceededError, isProQuotaExceededError,
isGenericQuotaExceededError, isGenericQuotaExceededError,
UserTierId, UserTierId,
DEFAULT_GEMINI_FLASH_MODEL,
} from '@google/gemini-cli-core'; } from '@google/gemini-cli-core';
import type { IdeIntegrationNudgeResult } from './IdeIntegrationNudge.js'; import type { IdeIntegrationNudgeResult } from './IdeIntegrationNudge.js';
import { IdeIntegrationNudge } from './IdeIntegrationNudge.js'; import { IdeIntegrationNudge } from './IdeIntegrationNudge.js';
@@ -100,6 +101,7 @@ import { ShowMoreLines } from './components/ShowMoreLines.js';
import { PrivacyNotice } from './privacy/PrivacyNotice.js'; import { PrivacyNotice } from './privacy/PrivacyNotice.js';
import { useSettingsCommand } from './hooks/useSettingsCommand.js'; import { useSettingsCommand } from './hooks/useSettingsCommand.js';
import { SettingsDialog } from './components/SettingsDialog.js'; import { SettingsDialog } from './components/SettingsDialog.js';
import { ProQuotaDialog } from './components/ProQuotaDialog.js';
import { setUpdateHandler } from '../utils/handleAutoUpdate.js'; import { setUpdateHandler } from '../utils/handleAutoUpdate.js';
import { appEvents, AppEvent } from '../utils/events.js'; import { appEvents, AppEvent } from '../utils/events.js';
import { isNarrowWidth } from './utils/isNarrowWidth.js'; import { isNarrowWidth } from './utils/isNarrowWidth.js';
@@ -227,6 +229,7 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
>(); >();
const [showEscapePrompt, setShowEscapePrompt] = useState(false); const [showEscapePrompt, setShowEscapePrompt] = useState(false);
const [isProcessing, setIsProcessing] = useState<boolean>(false); const [isProcessing, setIsProcessing] = useState<boolean>(false);
const { const {
showWorkspaceMigrationDialog, showWorkspaceMigrationDialog,
workspaceExtensions, workspaceExtensions,
@@ -234,6 +237,11 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
onWorkspaceMigrationDialogClose, onWorkspaceMigrationDialogClose,
} = useWorkspaceMigration(settings); } = useWorkspaceMigration(settings);
const [isProQuotaDialogOpen, setIsProQuotaDialogOpen] = useState(false);
const [proQuotaDialogResolver, setProQuotaDialogResolver] = useState<
((value: boolean) => void) | null
>(null);
useEffect(() => { useEffect(() => {
const unsubscribe = ideContext.subscribeToIdeContext(setIdeContextState); const unsubscribe = ideContext.subscribeToIdeContext(setIdeContextState);
// Set the initial value // Set the initial value
@@ -415,6 +423,12 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
fallbackModel: string, fallbackModel: string,
error?: unknown, error?: unknown,
): Promise<boolean> => { ): Promise<boolean> => {
// Check if we've already switched to the fallback model
if (config.isInFallbackMode()) {
// If we're already in fallback mode, don't show the dialog again
return false;
}
let message: string; let message: string;
if ( if (
@@ -429,11 +443,11 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
if (error && isProQuotaExceededError(error)) { if (error && isProQuotaExceededError(error)) {
if (isPaidTier) { if (isPaidTier) {
message = `⚡ You have reached your daily ${currentModel} quota limit. message = `⚡ You have reached your daily ${currentModel} quota limit.
Automatically switching from ${currentModel} to ${fallbackModel} for the remainder of this session. You can choose to authenticate with a paid API key or continue with the fallback model.
⚡ To continue accessing the ${currentModel} model today, consider using /auth to switch to using a paid API key from AI Studio at https://aistudio.google.com/apikey`; ⚡ To continue accessing the ${currentModel} model today, consider using /auth to switch to using a paid API key from AI Studio at https://aistudio.google.com/apikey`;
} else { } else {
message = `⚡ You have reached your daily ${currentModel} quota limit. message = `⚡ You have reached your daily ${currentModel} quota limit.
Automatically switching from ${currentModel} to ${fallbackModel} for the remainder of this session. You can choose to authenticate with a paid API key or continue with the fallback model.
⚡ To increase your limits, upgrade to a Gemini Code Assist Standard or Enterprise plan with higher limits at https://goo.gle/set-up-gemini-code-assist ⚡ To increase your limits, upgrade to a Gemini Code Assist Standard or Enterprise plan with higher limits at https://goo.gle/set-up-gemini-code-assist
⚡ Or you can utilize a Gemini API Key. See: https://goo.gle/gemini-cli-docs-auth#gemini-api-key ⚡ Or you can utilize a Gemini API Key. See: https://goo.gle/gemini-cli-docs-auth#gemini-api-key
⚡ You can switch authentication methods by typing /auth`; ⚡ You can switch authentication methods by typing /auth`;
@@ -475,6 +489,40 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
Date.now(), Date.now(),
); );
// For Pro quota errors, show the dialog and wait for user's choice
if (error && isProQuotaExceededError(error)) {
// Set the flag to prevent tool continuation
setModelSwitchedFromQuotaError(true);
// Set global quota error flag to prevent Flash model calls
config.setQuotaErrorOccurred(true);
// Show the ProQuotaDialog and wait for user's choice
const shouldContinueWithFallback = await new Promise<boolean>(
(resolve) => {
setIsProQuotaDialogOpen(true);
setProQuotaDialogResolver(() => resolve);
},
);
// If user chose to continue with fallback, we don't need to stop the current prompt
if (shouldContinueWithFallback) {
// Switch to fallback model for future use
config.setModel(fallbackModel);
config.setFallbackMode(true);
logFlashFallback(
config,
new FlashFallbackEvent(
config.getContentGeneratorConfig().authType!,
),
);
return true; // Continue with current prompt using fallback model
}
// If user chose to authenticate, stop current prompt
return false;
}
// For other quota errors, automatically switch to fallback model
// Set the flag to prevent tool continuation // Set the flag to prevent tool continuation
setModelSwitchedFromQuotaError(true); setModelSwitchedFromQuotaError(true);
// Set global quota error flag to prevent Flash model calls // Set global quota error flag to prevent Flash model calls
@@ -831,7 +879,8 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
(streamingState === StreamingState.Idle || (streamingState === StreamingState.Idle ||
streamingState === StreamingState.Responding) && streamingState === StreamingState.Responding) &&
!initError && !initError &&
!isProcessing; !isProcessing &&
!isProQuotaDialogOpen;
const handleClearScreen = useCallback(() => { const handleClearScreen = useCallback(() => {
clearItems(); clearItems();
@@ -1044,6 +1093,31 @@ const App = ({ config, settings, startupWarnings = [], version }: AppProps) => {
ide={currentIDE} ide={currentIDE}
onComplete={handleIdePromptComplete} onComplete={handleIdePromptComplete}
/> />
) : isProQuotaDialogOpen ? (
<ProQuotaDialog
currentModel={config.getModel()}
fallbackModel={DEFAULT_GEMINI_FLASH_MODEL}
onChoice={(choice) => {
setIsProQuotaDialogOpen(false);
if (!proQuotaDialogResolver) return;
const resolveValue = choice !== 'auth';
proQuotaDialogResolver(resolveValue);
setProQuotaDialogResolver(null);
if (choice === 'auth') {
openAuthDialog();
} else {
addItem(
{
type: MessageType.INFO,
text: 'Switched to fallback model. Tip: Press Ctrl+P to recall your previous prompt and submit it again if you wish.',
},
Date.now(),
);
}
}}
/>
) : isFolderTrustDialogOpen ? ( ) : isFolderTrustDialogOpen ? (
<FolderTrustDialog <FolderTrustDialog
onSelect={handleFolderTrustSelect} onSelect={handleFolderTrustSelect}
@@ -0,0 +1,89 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from 'ink-testing-library';
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
import { ProQuotaDialog } from './ProQuotaDialog.js';
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
// Mock the child component to make it easier to test the parent
vi.mock('./shared/RadioButtonSelect.js', () => ({
RadioButtonSelect: vi.fn(),
}));
describe('ProQuotaDialog', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('should render with correct title and options', () => {
const { lastFrame } = render(
<ProQuotaDialog
currentModel="gemini-2.5-pro"
fallbackModel="gemini-2.5-flash"
onChoice={() => {}}
/>,
);
const output = lastFrame();
expect(output).toContain('Pro quota limit reached for gemini-2.5-pro.');
// Check that RadioButtonSelect was called with the correct items
expect(RadioButtonSelect).toHaveBeenCalledWith(
expect.objectContaining({
items: [
{
label: 'Change auth (executes the /auth command)',
value: 'auth',
},
{
label: `Continue with gemini-2.5-flash`,
value: 'continue',
},
],
}),
undefined,
);
});
it('should call onChoice with "auth" when "Change auth" is selected', () => {
const mockOnChoice = vi.fn();
render(
<ProQuotaDialog
currentModel="gemini-2.5-pro"
fallbackModel="gemini-2.5-flash"
onChoice={mockOnChoice}
/>,
);
// Get the onSelect function passed to RadioButtonSelect
const onSelect = (RadioButtonSelect as Mock).mock.calls[0][0].onSelect;
// Simulate the selection
onSelect('auth');
expect(mockOnChoice).toHaveBeenCalledWith('auth');
});
it('should call onChoice with "continue" when "Continue with flash" is selected', () => {
const mockOnChoice = vi.fn();
render(
<ProQuotaDialog
currentModel="gemini-2.5-pro"
fallbackModel="gemini-2.5-flash"
onChoice={mockOnChoice}
/>,
);
// Get the onSelect function passed to RadioButtonSelect
const onSelect = (RadioButtonSelect as Mock).mock.calls[0][0].onSelect;
// Simulate the selection
onSelect('continue');
expect(mockOnChoice).toHaveBeenCalledWith('continue');
});
});
@@ -0,0 +1,52 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import { Box, Text } from 'ink';
import { RadioButtonSelect } from './shared/RadioButtonSelect.js';
import { Colors } from '../colors.js';
interface ProQuotaDialogProps {
currentModel: string;
fallbackModel: string;
onChoice: (choice: 'auth' | 'continue') => void;
}
export function ProQuotaDialog({
currentModel,
fallbackModel,
onChoice,
}: ProQuotaDialogProps): React.JSX.Element {
const items = [
{
label: 'Change auth (executes the /auth command)',
value: 'auth' as const,
},
{
label: `Continue with ${fallbackModel}`,
value: 'continue' as const,
},
];
const handleSelect = (choice: 'auth' | 'continue') => {
onChoice(choice);
};
return (
<Box borderStyle="round" flexDirection="column" paddingX={1}>
<Text bold color={Colors.AccentYellow}>
Pro quota limit reached for {currentModel}.
</Text>
<Box marginTop={1}>
<RadioButtonSelect
items={items}
initialIndex={1}
onSelect={handleSelect}
/>
</Box>
</Box>
);
}