mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-26 13:04:49 -07:00
feat(cli): extract QuotaContext and resolve infinite render loop (#24959)
This commit is contained in:
@@ -201,12 +201,6 @@ const createMockUIState = (overrides: Partial<UIState> = {}): UIState =>
|
||||
isBackgroundTaskVisible: false,
|
||||
embeddedShellFocused: false,
|
||||
showIsExpandableHint: false,
|
||||
quota: {
|
||||
userTier: undefined,
|
||||
stats: undefined,
|
||||
proQuotaRequest: null,
|
||||
validationRequest: null,
|
||||
},
|
||||
...overrides,
|
||||
}) as UIState;
|
||||
|
||||
@@ -245,6 +239,7 @@ const createMockConfig = (overrides = {}): Config =>
|
||||
...overrides,
|
||||
}) as unknown as Config;
|
||||
|
||||
import { QuotaContext, type QuotaState } from '../contexts/QuotaContext.js';
|
||||
import { InputContext, type InputState } from '../contexts/InputContext.js';
|
||||
|
||||
const renderComposer = async (
|
||||
@@ -253,6 +248,7 @@ const renderComposer = async (
|
||||
config = createMockConfig(),
|
||||
uiActions = createMockUIActions(),
|
||||
inputStateOverrides: Partial<InputState> = {},
|
||||
quotaStateOverrides: Partial<QuotaState> = {},
|
||||
) => {
|
||||
const inputState = {
|
||||
buffer: { text: '' } as unknown as TextBuffer,
|
||||
@@ -266,16 +262,28 @@ const renderComposer = async (
|
||||
...inputStateOverrides,
|
||||
};
|
||||
|
||||
const quotaState: QuotaState = {
|
||||
userTier: undefined,
|
||||
stats: undefined,
|
||||
proQuotaRequest: null,
|
||||
validationRequest: null,
|
||||
overageMenuRequest: null,
|
||||
emptyWalletRequest: null,
|
||||
...quotaStateOverrides,
|
||||
};
|
||||
|
||||
const result = await render(
|
||||
<ConfigContext.Provider value={config as unknown as Config}>
|
||||
<SettingsContext.Provider value={settings as unknown as LoadedSettings}>
|
||||
<InputContext.Provider value={inputState}>
|
||||
<UIStateContext.Provider value={uiState}>
|
||||
<UIActionsContext.Provider value={uiActions}>
|
||||
<Composer isFocused={true} />
|
||||
</UIActionsContext.Provider>
|
||||
</UIStateContext.Provider>
|
||||
</InputContext.Provider>
|
||||
<QuotaContext.Provider value={quotaState}>
|
||||
<InputContext.Provider value={inputState}>
|
||||
<UIStateContext.Provider value={uiState}>
|
||||
<UIActionsContext.Provider value={uiActions}>
|
||||
<Composer isFocused={true} />
|
||||
</UIActionsContext.Provider>
|
||||
</UIStateContext.Provider>
|
||||
</InputContext.Provider>
|
||||
</QuotaContext.Provider>
|
||||
</SettingsContext.Provider>
|
||||
</ConfigContext.Provider>,
|
||||
);
|
||||
|
||||
@@ -9,6 +9,7 @@ import { DialogManager } from './DialogManager.js';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { Text } from 'ink';
|
||||
import { type UIState } from '../contexts/UIStateContext.js';
|
||||
import { type QuotaState } from '../contexts/QuotaContext.js';
|
||||
import { type RestartReason } from '../hooks/useIdeTrustListener.js';
|
||||
import { type IdeInfo } from '@google/gemini-cli-core';
|
||||
|
||||
@@ -75,14 +76,6 @@ describe('DialogManager', () => {
|
||||
terminalWidth: 80,
|
||||
confirmUpdateExtensionRequests: [],
|
||||
showIdeRestartPrompt: false,
|
||||
quota: {
|
||||
userTier: undefined,
|
||||
stats: undefined,
|
||||
proQuotaRequest: null,
|
||||
validationRequest: null,
|
||||
overageMenuRequest: null,
|
||||
emptyWalletRequest: null,
|
||||
},
|
||||
shouldShowIdePrompt: false,
|
||||
isFolderTrustDialogOpen: false,
|
||||
loopDetectionConfirmationRequest: null,
|
||||
@@ -112,7 +105,7 @@ describe('DialogManager', () => {
|
||||
unmount();
|
||||
});
|
||||
|
||||
const testCases: Array<[Partial<UIState>, string]> = [
|
||||
const testCases: Array<[Partial<UIState>, string, Partial<QuotaState>?]> = [
|
||||
[
|
||||
{
|
||||
showIdeRestartPrompt: true,
|
||||
@@ -121,23 +114,17 @@ describe('DialogManager', () => {
|
||||
'IdeTrustChangeDialog',
|
||||
],
|
||||
[
|
||||
{},
|
||||
'ProQuotaDialog',
|
||||
{
|
||||
quota: {
|
||||
userTier: undefined,
|
||||
stats: undefined,
|
||||
proQuotaRequest: {
|
||||
failedModel: 'a',
|
||||
fallbackModel: 'b',
|
||||
message: 'c',
|
||||
isTerminalQuotaError: false,
|
||||
resolve: vi.fn(),
|
||||
},
|
||||
validationRequest: null,
|
||||
overageMenuRequest: null,
|
||||
emptyWalletRequest: null,
|
||||
proQuotaRequest: {
|
||||
failedModel: 'a',
|
||||
fallbackModel: 'b',
|
||||
message: 'c',
|
||||
isTerminalQuotaError: false,
|
||||
resolve: vi.fn(),
|
||||
},
|
||||
},
|
||||
'ProQuotaDialog',
|
||||
],
|
||||
[
|
||||
{
|
||||
@@ -195,7 +182,11 @@ describe('DialogManager', () => {
|
||||
|
||||
it.each(testCases)(
|
||||
'renders %s when state is %o',
|
||||
async (uiStateOverride, expectedComponent) => {
|
||||
async (
|
||||
uiStateOverride: Partial<UIState>,
|
||||
expectedComponent: string,
|
||||
quotaStateOverride?: Partial<QuotaState>,
|
||||
) => {
|
||||
const { lastFrame, unmount } = await renderWithProviders(
|
||||
<DialogManager {...defaultProps} />,
|
||||
{
|
||||
@@ -203,6 +194,7 @@ describe('DialogManager', () => {
|
||||
...baseUiState,
|
||||
...uiStateOverride,
|
||||
} as Partial<UIState> as UIState,
|
||||
quotaState: quotaStateOverride,
|
||||
},
|
||||
);
|
||||
expect(lastFrame()).toContain(expectedComponent);
|
||||
|
||||
@@ -27,6 +27,7 @@ import { PermissionsModifyTrustDialog } from './PermissionsModifyTrustDialog.js'
|
||||
import { ModelDialog } from './ModelDialog.js';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { useUIState } from '../contexts/UIStateContext.js';
|
||||
import { useQuotaState } from '../contexts/QuotaContext.js';
|
||||
import { useUIActions } from '../contexts/UIActionsContext.js';
|
||||
import { useConfig } from '../contexts/ConfigContext.js';
|
||||
import { useSettings } from '../contexts/SettingsContext.js';
|
||||
@@ -52,6 +53,7 @@ export const DialogManager = ({
|
||||
const settings = useSettings();
|
||||
|
||||
const uiState = useUIState();
|
||||
const quotaState = useQuotaState();
|
||||
const uiActions = useUIActions();
|
||||
const {
|
||||
constrainHeight,
|
||||
@@ -74,54 +76,50 @@ export const DialogManager = ({
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (uiState.quota.proQuotaRequest) {
|
||||
if (quotaState.proQuotaRequest) {
|
||||
return (
|
||||
<ProQuotaDialog
|
||||
failedModel={uiState.quota.proQuotaRequest.failedModel}
|
||||
fallbackModel={uiState.quota.proQuotaRequest.fallbackModel}
|
||||
message={uiState.quota.proQuotaRequest.message}
|
||||
isTerminalQuotaError={
|
||||
uiState.quota.proQuotaRequest.isTerminalQuotaError
|
||||
}
|
||||
isModelNotFoundError={
|
||||
!!uiState.quota.proQuotaRequest.isModelNotFoundError
|
||||
}
|
||||
authType={uiState.quota.proQuotaRequest.authType}
|
||||
failedModel={quotaState.proQuotaRequest.failedModel}
|
||||
fallbackModel={quotaState.proQuotaRequest.fallbackModel}
|
||||
message={quotaState.proQuotaRequest.message}
|
||||
isTerminalQuotaError={quotaState.proQuotaRequest.isTerminalQuotaError}
|
||||
isModelNotFoundError={!!quotaState.proQuotaRequest.isModelNotFoundError}
|
||||
authType={quotaState.proQuotaRequest.authType}
|
||||
tierName={config?.getUserTierName()}
|
||||
onChoice={uiActions.handleProQuotaChoice}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (uiState.quota.validationRequest) {
|
||||
if (quotaState.validationRequest) {
|
||||
return (
|
||||
<ValidationDialog
|
||||
validationLink={uiState.quota.validationRequest.validationLink}
|
||||
validationLink={quotaState.validationRequest.validationLink}
|
||||
validationDescription={
|
||||
uiState.quota.validationRequest.validationDescription
|
||||
quotaState.validationRequest.validationDescription
|
||||
}
|
||||
learnMoreUrl={uiState.quota.validationRequest.learnMoreUrl}
|
||||
learnMoreUrl={quotaState.validationRequest.learnMoreUrl}
|
||||
onChoice={uiActions.handleValidationChoice}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (uiState.quota.overageMenuRequest) {
|
||||
if (quotaState.overageMenuRequest) {
|
||||
return (
|
||||
<OverageMenuDialog
|
||||
failedModel={uiState.quota.overageMenuRequest.failedModel}
|
||||
fallbackModel={uiState.quota.overageMenuRequest.fallbackModel}
|
||||
resetTime={uiState.quota.overageMenuRequest.resetTime}
|
||||
creditBalance={uiState.quota.overageMenuRequest.creditBalance}
|
||||
failedModel={quotaState.overageMenuRequest.failedModel}
|
||||
fallbackModel={quotaState.overageMenuRequest.fallbackModel}
|
||||
resetTime={quotaState.overageMenuRequest.resetTime}
|
||||
creditBalance={quotaState.overageMenuRequest.creditBalance}
|
||||
onChoice={uiActions.handleOverageMenuChoice}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (uiState.quota.emptyWalletRequest) {
|
||||
if (quotaState.emptyWalletRequest) {
|
||||
return (
|
||||
<EmptyWalletDialog
|
||||
failedModel={uiState.quota.emptyWalletRequest.failedModel}
|
||||
fallbackModel={uiState.quota.emptyWalletRequest.fallbackModel}
|
||||
resetTime={uiState.quota.emptyWalletRequest.resetTime}
|
||||
onGetCredits={uiState.quota.emptyWalletRequest.onGetCredits}
|
||||
failedModel={quotaState.emptyWalletRequest.failedModel}
|
||||
fallbackModel={quotaState.emptyWalletRequest.fallbackModel}
|
||||
resetTime={quotaState.emptyWalletRequest.resetTime}
|
||||
onGetCredits={quotaState.emptyWalletRequest.onGetCredits}
|
||||
onChoice={uiActions.handleEmptyWalletChoice}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -267,17 +267,12 @@ describe('<Footer />', () => {
|
||||
width: 120,
|
||||
uiState: {
|
||||
sessionStats: mockSessionStats,
|
||||
quota: {
|
||||
userTier: undefined,
|
||||
stats: {
|
||||
remaining: 15,
|
||||
limit: 100,
|
||||
resetTime: undefined,
|
||||
},
|
||||
proQuotaRequest: null,
|
||||
validationRequest: null,
|
||||
overageMenuRequest: null,
|
||||
emptyWalletRequest: null,
|
||||
},
|
||||
quotaState: {
|
||||
stats: {
|
||||
remaining: 15,
|
||||
limit: 100,
|
||||
resetTime: undefined,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -292,17 +287,12 @@ describe('<Footer />', () => {
|
||||
width: 120,
|
||||
uiState: {
|
||||
sessionStats: mockSessionStats,
|
||||
quota: {
|
||||
userTier: undefined,
|
||||
stats: {
|
||||
remaining: 85,
|
||||
limit: 100,
|
||||
resetTime: undefined,
|
||||
},
|
||||
proQuotaRequest: null,
|
||||
validationRequest: null,
|
||||
overageMenuRequest: null,
|
||||
emptyWalletRequest: null,
|
||||
},
|
||||
quotaState: {
|
||||
stats: {
|
||||
remaining: 85,
|
||||
limit: 100,
|
||||
resetTime: undefined,
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -317,17 +307,12 @@ describe('<Footer />', () => {
|
||||
width: 120,
|
||||
uiState: {
|
||||
sessionStats: mockSessionStats,
|
||||
quota: {
|
||||
userTier: undefined,
|
||||
stats: {
|
||||
remaining: 0,
|
||||
limit: 100,
|
||||
resetTime: undefined,
|
||||
},
|
||||
proQuotaRequest: null,
|
||||
validationRequest: null,
|
||||
overageMenuRequest: null,
|
||||
emptyWalletRequest: null,
|
||||
},
|
||||
quotaState: {
|
||||
stats: {
|
||||
remaining: 0,
|
||||
limit: 100,
|
||||
resetTime: undefined,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -23,6 +23,7 @@ import { ContextUsageDisplay } from './ContextUsageDisplay.js';
|
||||
import { QuotaDisplay } from './QuotaDisplay.js';
|
||||
import { DebugProfiler } from './DebugProfiler.js';
|
||||
import { useUIState } from '../contexts/UIStateContext.js';
|
||||
import { useQuotaState } from '../contexts/QuotaContext.js';
|
||||
import { useConfig } from '../contexts/ConfigContext.js';
|
||||
import { useSettings } from '../contexts/SettingsContext.js';
|
||||
import { useVimMode } from '../contexts/VimModeContext.js';
|
||||
@@ -174,6 +175,7 @@ interface FooterColumn {
|
||||
|
||||
export const Footer: React.FC = () => {
|
||||
const uiState = useUIState();
|
||||
const quotaState = useQuotaState();
|
||||
const { copyModeEnabled } = useInputState();
|
||||
const config = useConfig();
|
||||
const settings = useSettings();
|
||||
@@ -203,7 +205,6 @@ export const Footer: React.FC = () => {
|
||||
promptTokenCount,
|
||||
isTrustedFolder,
|
||||
terminalWidth,
|
||||
quotaStats,
|
||||
} = {
|
||||
model: uiState.currentModel,
|
||||
targetDir: config.getTargetDir(),
|
||||
@@ -216,9 +217,10 @@ export const Footer: React.FC = () => {
|
||||
promptTokenCount: uiState.sessionStats.lastPromptTokenCount,
|
||||
isTrustedFolder: uiState.isTrustedFolder,
|
||||
terminalWidth: uiState.terminalWidth,
|
||||
quotaStats: uiState.quota.stats,
|
||||
};
|
||||
|
||||
const quotaStats = quotaState.stats;
|
||||
|
||||
const isFullErrorVerbosity = settings.merged.ui.errorVerbosity === 'full';
|
||||
const showErrorSummary =
|
||||
!showErrorDetails &&
|
||||
|
||||
Reference in New Issue
Block a user