mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-27 05:24:34 -07:00
feat(cli): extract QuotaContext and resolve infinite render loop (#24959)
This commit is contained in:
@@ -602,6 +602,7 @@ const mockUIActions: UIActions = {
|
|||||||
|
|
||||||
import { type TextBuffer } from '../ui/components/shared/text-buffer.js';
|
import { type TextBuffer } from '../ui/components/shared/text-buffer.js';
|
||||||
import { InputContext, type InputState } from '../ui/contexts/InputContext.js';
|
import { InputContext, type InputState } from '../ui/contexts/InputContext.js';
|
||||||
|
import { QuotaContext, type QuotaState } from '../ui/contexts/QuotaContext.js';
|
||||||
|
|
||||||
let capturedOverflowState: OverflowState | undefined;
|
let capturedOverflowState: OverflowState | undefined;
|
||||||
let capturedOverflowActions: OverflowActions | undefined;
|
let capturedOverflowActions: OverflowActions | undefined;
|
||||||
@@ -619,6 +620,7 @@ export const renderWithProviders = async (
|
|||||||
shellFocus = true,
|
shellFocus = true,
|
||||||
settings = mockSettings,
|
settings = mockSettings,
|
||||||
uiState: providedUiState,
|
uiState: providedUiState,
|
||||||
|
quotaState: providedQuotaState,
|
||||||
inputState: providedInputState,
|
inputState: providedInputState,
|
||||||
width,
|
width,
|
||||||
mouseEventsEnabled = false,
|
mouseEventsEnabled = false,
|
||||||
@@ -631,6 +633,7 @@ export const renderWithProviders = async (
|
|||||||
shellFocus?: boolean;
|
shellFocus?: boolean;
|
||||||
settings?: LoadedSettings;
|
settings?: LoadedSettings;
|
||||||
uiState?: Partial<UIState>;
|
uiState?: Partial<UIState>;
|
||||||
|
quotaState?: Partial<QuotaState>;
|
||||||
inputState?: Partial<InputState>;
|
inputState?: Partial<InputState>;
|
||||||
width?: number;
|
width?: number;
|
||||||
mouseEventsEnabled?: boolean;
|
mouseEventsEnabled?: boolean;
|
||||||
@@ -666,6 +669,16 @@ export const renderWithProviders = async (
|
|||||||
},
|
},
|
||||||
) as UIState;
|
) as UIState;
|
||||||
|
|
||||||
|
const quotaState: QuotaState = {
|
||||||
|
userTier: undefined,
|
||||||
|
stats: undefined,
|
||||||
|
proQuotaRequest: null,
|
||||||
|
validationRequest: null,
|
||||||
|
overageMenuRequest: null,
|
||||||
|
emptyWalletRequest: null,
|
||||||
|
...providedQuotaState,
|
||||||
|
};
|
||||||
|
|
||||||
const inputState = {
|
const inputState = {
|
||||||
buffer: { text: '' } as unknown as TextBuffer,
|
buffer: { text: '' } as unknown as TextBuffer,
|
||||||
userMessages: [],
|
userMessages: [],
|
||||||
@@ -727,65 +740,67 @@ export const renderWithProviders = async (
|
|||||||
<AppContext.Provider value={appState}>
|
<AppContext.Provider value={appState}>
|
||||||
<ConfigContext.Provider value={config}>
|
<ConfigContext.Provider value={config}>
|
||||||
<SettingsContext.Provider value={settings}>
|
<SettingsContext.Provider value={settings}>
|
||||||
<InputContext.Provider value={inputState}>
|
<QuotaContext.Provider value={quotaState}>
|
||||||
<UIStateContext.Provider value={finalUiState}>
|
<InputContext.Provider value={inputState}>
|
||||||
<VimModeProvider>
|
<UIStateContext.Provider value={finalUiState}>
|
||||||
<ShellFocusContext.Provider value={shellFocus}>
|
<VimModeProvider>
|
||||||
<SessionStatsProvider sessionId={config.getSessionId()}>
|
<ShellFocusContext.Provider value={shellFocus}>
|
||||||
<StreamingContext.Provider
|
<SessionStatsProvider sessionId={config.getSessionId()}>
|
||||||
value={finalUiState.streamingState}
|
<StreamingContext.Provider
|
||||||
>
|
value={finalUiState.streamingState}
|
||||||
<UIActionsContext.Provider value={finalUIActions}>
|
>
|
||||||
<OverflowProvider>
|
<UIActionsContext.Provider value={finalUIActions}>
|
||||||
<ToolActionsProvider
|
<OverflowProvider>
|
||||||
config={config}
|
<ToolActionsProvider
|
||||||
toolCalls={allToolCalls}
|
config={config}
|
||||||
isExpanded={
|
toolCalls={allToolCalls}
|
||||||
toolActions?.isExpanded ??
|
isExpanded={
|
||||||
vi.fn().mockReturnValue(false)
|
toolActions?.isExpanded ??
|
||||||
}
|
vi.fn().mockReturnValue(false)
|
||||||
toggleExpansion={
|
}
|
||||||
toolActions?.toggleExpansion ?? vi.fn()
|
toggleExpansion={
|
||||||
}
|
toolActions?.toggleExpansion ?? vi.fn()
|
||||||
toggleAllExpansion={
|
}
|
||||||
toolActions?.toggleAllExpansion ?? vi.fn()
|
toggleAllExpansion={
|
||||||
}
|
toolActions?.toggleAllExpansion ?? vi.fn()
|
||||||
>
|
}
|
||||||
<AskUserActionsProvider
|
|
||||||
request={null}
|
|
||||||
onSubmit={vi.fn()}
|
|
||||||
onCancel={vi.fn()}
|
|
||||||
>
|
>
|
||||||
<KeypressProvider>
|
<AskUserActionsProvider
|
||||||
<MouseProvider
|
request={null}
|
||||||
mouseEventsEnabled={mouseEventsEnabled}
|
onSubmit={vi.fn()}
|
||||||
>
|
onCancel={vi.fn()}
|
||||||
<TerminalProvider>
|
>
|
||||||
<ScrollProvider>
|
<KeypressProvider>
|
||||||
<ContextCapture>
|
<MouseProvider
|
||||||
<Box
|
mouseEventsEnabled={mouseEventsEnabled}
|
||||||
width={terminalWidth}
|
>
|
||||||
flexShrink={0}
|
<TerminalProvider>
|
||||||
flexGrow={0}
|
<ScrollProvider>
|
||||||
flexDirection="column"
|
<ContextCapture>
|
||||||
>
|
<Box
|
||||||
{comp}
|
width={terminalWidth}
|
||||||
</Box>
|
flexShrink={0}
|
||||||
</ContextCapture>
|
flexGrow={0}
|
||||||
</ScrollProvider>
|
flexDirection="column"
|
||||||
</TerminalProvider>
|
>
|
||||||
</MouseProvider>
|
{comp}
|
||||||
</KeypressProvider>
|
</Box>
|
||||||
</AskUserActionsProvider>
|
</ContextCapture>
|
||||||
</ToolActionsProvider>
|
</ScrollProvider>
|
||||||
</OverflowProvider>
|
</TerminalProvider>
|
||||||
</UIActionsContext.Provider>
|
</MouseProvider>
|
||||||
</StreamingContext.Provider>
|
</KeypressProvider>
|
||||||
</SessionStatsProvider>
|
</AskUserActionsProvider>
|
||||||
</ShellFocusContext.Provider>
|
</ToolActionsProvider>
|
||||||
</VimModeProvider>
|
</OverflowProvider>
|
||||||
</UIStateContext.Provider>
|
</UIActionsContext.Provider>
|
||||||
</InputContext.Provider>
|
</StreamingContext.Provider>
|
||||||
|
</SessionStatsProvider>
|
||||||
|
</ShellFocusContext.Provider>
|
||||||
|
</VimModeProvider>
|
||||||
|
</UIStateContext.Provider>
|
||||||
|
</InputContext.Provider>
|
||||||
|
</QuotaContext.Provider>
|
||||||
</SettingsContext.Provider>
|
</SettingsContext.Provider>
|
||||||
</ConfigContext.Provider>
|
</ConfigContext.Provider>
|
||||||
</AppContext.Provider>
|
</AppContext.Provider>
|
||||||
|
|||||||
@@ -123,16 +123,19 @@ vi.mock('ink', async (importOriginal) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
import { InputContext, type InputState } from './contexts/InputContext.js';
|
import { InputContext, type InputState } from './contexts/InputContext.js';
|
||||||
|
import { QuotaContext, type QuotaState } from './contexts/QuotaContext.js';
|
||||||
|
|
||||||
// Helper component will read the context values provided by AppContainer
|
// Helper component will read the context values provided by AppContainer
|
||||||
// so we can assert against them in our tests.
|
// so we can assert against them in our tests.
|
||||||
let capturedUIState: UIState;
|
let capturedUIState: UIState;
|
||||||
let capturedInputState: InputState;
|
let capturedInputState: InputState;
|
||||||
|
let capturedQuotaState: QuotaState;
|
||||||
let capturedUIActions: UIActions;
|
let capturedUIActions: UIActions;
|
||||||
let capturedOverflowActions: OverflowActions;
|
let capturedOverflowActions: OverflowActions;
|
||||||
function TestContextConsumer() {
|
function TestContextConsumer() {
|
||||||
capturedUIState = useContext(UIStateContext)!;
|
capturedUIState = useContext(UIStateContext)!;
|
||||||
capturedInputState = useContext(InputContext)!;
|
capturedInputState = useContext(InputContext)!;
|
||||||
|
capturedQuotaState = useContext(QuotaContext)!;
|
||||||
capturedUIActions = useContext(UIActionsContext)!;
|
capturedUIActions = useContext(UIActionsContext)!;
|
||||||
capturedOverflowActions = useOverflowActions()!;
|
capturedOverflowActions = useOverflowActions()!;
|
||||||
return null;
|
return null;
|
||||||
@@ -1309,15 +1312,15 @@ describe('AppContainer State Management', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('Quota and Fallback Integration', () => {
|
describe('Quota and Fallback Integration', () => {
|
||||||
it('passes a null proQuotaRequest to UIStateContext by default', async () => {
|
it('passes a null proQuotaRequest to QuotaContext by default', async () => {
|
||||||
// The default mock from beforeEach already sets proQuotaRequest to null
|
// The default mock from beforeEach already sets proQuotaRequest to null
|
||||||
const { unmount } = await act(async () => renderAppContainer());
|
const { unmount } = await act(async () => renderAppContainer());
|
||||||
// Assert that the context value is as expected
|
// Assert that the context value is as expected
|
||||||
expect(capturedUIState.quota.proQuotaRequest).toBeNull();
|
expect(capturedQuotaState.proQuotaRequest).toBeNull();
|
||||||
unmount();
|
unmount();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('passes a valid proQuotaRequest to UIStateContext when provided by the hook', async () => {
|
it('passes a valid proQuotaRequest to QuotaContext when provided by the hook', async () => {
|
||||||
// Arrange: Create a mock request object that a UI dialog would receive
|
// Arrange: Create a mock request object that a UI dialog would receive
|
||||||
const mockRequest = {
|
const mockRequest = {
|
||||||
failedModel: 'gemini-pro',
|
failedModel: 'gemini-pro',
|
||||||
@@ -1332,7 +1335,7 @@ describe('AppContainer State Management', () => {
|
|||||||
// Act: Render the container
|
// Act: Render the container
|
||||||
const { unmount } = await act(async () => renderAppContainer());
|
const { unmount } = await act(async () => renderAppContainer());
|
||||||
// Assert: The mock request is correctly passed through the context
|
// Assert: The mock request is correctly passed through the context
|
||||||
expect(capturedUIState.quota.proQuotaRequest).toEqual(mockRequest);
|
expect(capturedQuotaState.proQuotaRequest).toEqual(mockRequest);
|
||||||
unmount();
|
unmount();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
import { App } from './App.js';
|
import { App } from './App.js';
|
||||||
import { AppContext } from './contexts/AppContext.js';
|
import { AppContext } from './contexts/AppContext.js';
|
||||||
import { UIStateContext, type UIState } from './contexts/UIStateContext.js';
|
import { UIStateContext, type UIState } from './contexts/UIStateContext.js';
|
||||||
|
import { QuotaContext } from './contexts/QuotaContext.js';
|
||||||
import {
|
import {
|
||||||
UIActionsContext,
|
UIActionsContext,
|
||||||
type UIActions,
|
type UIActions,
|
||||||
@@ -2401,6 +2402,26 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
|||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const quotaState = useMemo(
|
||||||
|
() => ({
|
||||||
|
userTier,
|
||||||
|
stats: quotaStats,
|
||||||
|
proQuotaRequest,
|
||||||
|
validationRequest,
|
||||||
|
// G1 AI Credits dialog state
|
||||||
|
overageMenuRequest,
|
||||||
|
emptyWalletRequest,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
userTier,
|
||||||
|
quotaStats,
|
||||||
|
proQuotaRequest,
|
||||||
|
validationRequest,
|
||||||
|
overageMenuRequest,
|
||||||
|
emptyWalletRequest,
|
||||||
|
],
|
||||||
|
);
|
||||||
|
|
||||||
const uiState: UIState = useMemo(
|
const uiState: UIState = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
history: historyManager.history,
|
history: historyManager.history,
|
||||||
@@ -2473,15 +2494,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
|||||||
showApprovalModeIndicator,
|
showApprovalModeIndicator,
|
||||||
allowPlanMode,
|
allowPlanMode,
|
||||||
currentModel,
|
currentModel,
|
||||||
quota: {
|
|
||||||
userTier,
|
|
||||||
stats: quotaStats,
|
|
||||||
proQuotaRequest,
|
|
||||||
validationRequest,
|
|
||||||
// G1 AI Credits dialog state
|
|
||||||
overageMenuRequest,
|
|
||||||
emptyWalletRequest,
|
|
||||||
},
|
|
||||||
contextFileNames,
|
contextFileNames,
|
||||||
errorCount,
|
errorCount,
|
||||||
availableTerminalHeight,
|
availableTerminalHeight,
|
||||||
@@ -2592,12 +2604,6 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
|||||||
queueErrorMessage,
|
queueErrorMessage,
|
||||||
showApprovalModeIndicator,
|
showApprovalModeIndicator,
|
||||||
allowPlanMode,
|
allowPlanMode,
|
||||||
userTier,
|
|
||||||
quotaStats,
|
|
||||||
proQuotaRequest,
|
|
||||||
validationRequest,
|
|
||||||
overageMenuRequest,
|
|
||||||
emptyWalletRequest,
|
|
||||||
contextFileNames,
|
contextFileNames,
|
||||||
errorCount,
|
errorCount,
|
||||||
availableTerminalHeight,
|
availableTerminalHeight,
|
||||||
@@ -2816,34 +2822,36 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<UIStateContext.Provider value={uiState}>
|
<UIStateContext.Provider value={uiState}>
|
||||||
<InputContext.Provider value={inputState}>
|
<QuotaContext.Provider value={quotaState}>
|
||||||
<UIActionsContext.Provider value={uiActions}>
|
<InputContext.Provider value={inputState}>
|
||||||
<ConfigContext.Provider value={config}>
|
<UIActionsContext.Provider value={uiActions}>
|
||||||
<AppContext.Provider
|
<ConfigContext.Provider value={config}>
|
||||||
value={{
|
<AppContext.Provider
|
||||||
version: props.version,
|
value={{
|
||||||
startupWarnings: props.startupWarnings || [],
|
version: props.version,
|
||||||
}}
|
startupWarnings: props.startupWarnings || [],
|
||||||
>
|
}}
|
||||||
<ToolActionsProvider
|
|
||||||
config={config}
|
|
||||||
toolCalls={allToolCalls}
|
|
||||||
isExpanded={isExpanded}
|
|
||||||
toggleExpansion={toggleExpansion}
|
|
||||||
toggleAllExpansion={toggleAllExpansion}
|
|
||||||
>
|
>
|
||||||
<ShellFocusContext.Provider value={isFocused}>
|
<ToolActionsProvider
|
||||||
<MouseProvider mouseEventsEnabled={mouseMode}>
|
config={config}
|
||||||
<ScrollProvider>
|
toolCalls={allToolCalls}
|
||||||
<App key={`app-${forceRerenderKey}`} />
|
isExpanded={isExpanded}
|
||||||
</ScrollProvider>
|
toggleExpansion={toggleExpansion}
|
||||||
</MouseProvider>
|
toggleAllExpansion={toggleAllExpansion}
|
||||||
</ShellFocusContext.Provider>
|
>
|
||||||
</ToolActionsProvider>
|
<ShellFocusContext.Provider value={isFocused}>
|
||||||
</AppContext.Provider>
|
<MouseProvider mouseEventsEnabled={mouseMode}>
|
||||||
</ConfigContext.Provider>
|
<ScrollProvider>
|
||||||
</UIActionsContext.Provider>
|
<App key={`app-${forceRerenderKey}`} />
|
||||||
</InputContext.Provider>
|
</ScrollProvider>
|
||||||
|
</MouseProvider>
|
||||||
|
</ShellFocusContext.Provider>
|
||||||
|
</ToolActionsProvider>
|
||||||
|
</AppContext.Provider>
|
||||||
|
</ConfigContext.Provider>
|
||||||
|
</UIActionsContext.Provider>
|
||||||
|
</InputContext.Provider>
|
||||||
|
</QuotaContext.Provider>
|
||||||
</UIStateContext.Provider>
|
</UIStateContext.Provider>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ import {
|
|||||||
CoreToolCallStatus,
|
CoreToolCallStatus,
|
||||||
ApprovalMode,
|
ApprovalMode,
|
||||||
makeFakeConfig,
|
makeFakeConfig,
|
||||||
|
type SerializableConfirmationDetails,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import { type UIState } from './contexts/UIStateContext.js';
|
import { type UIState } from './contexts/UIStateContext.js';
|
||||||
import type { SerializableConfirmationDetails } from '@google/gemini-cli-core';
|
|
||||||
import { act } from 'react';
|
import { act } from 'react';
|
||||||
import { StreamingState } from './types.js';
|
import { StreamingState } from './types.js';
|
||||||
|
|
||||||
@@ -107,15 +107,6 @@ describe('Full Terminal Tool Confirmation Snapshot', () => {
|
|||||||
constrainHeight: true,
|
constrainHeight: true,
|
||||||
isConfigInitialized: true,
|
isConfigInitialized: true,
|
||||||
cleanUiDetailsVisible: true,
|
cleanUiDetailsVisible: true,
|
||||||
quota: {
|
|
||||||
userTier: 'PRO',
|
|
||||||
stats: {
|
|
||||||
limits: {},
|
|
||||||
usage: {},
|
|
||||||
},
|
|
||||||
proQuotaRequest: null,
|
|
||||||
validationRequest: null,
|
|
||||||
},
|
|
||||||
pendingHistoryItems: [
|
pendingHistoryItems: [
|
||||||
{
|
{
|
||||||
id: 2,
|
id: 2,
|
||||||
@@ -145,6 +136,13 @@ describe('Full Terminal Tool Confirmation Snapshot', () => {
|
|||||||
const { waitUntilReady, lastFrame, generateSvg, unmount } =
|
const { waitUntilReady, lastFrame, generateSvg, unmount } =
|
||||||
await renderWithProviders(<App />, {
|
await renderWithProviders(<App />, {
|
||||||
uiState: mockUIState,
|
uiState: mockUIState,
|
||||||
|
quotaState: {
|
||||||
|
userTier: 'PRO',
|
||||||
|
stats: {
|
||||||
|
remaining: 100,
|
||||||
|
limit: 1000,
|
||||||
|
},
|
||||||
|
},
|
||||||
config: mockConfig,
|
config: mockConfig,
|
||||||
settings: createMockSettings({
|
settings: createMockSettings({
|
||||||
merged: {
|
merged: {
|
||||||
|
|||||||
@@ -201,12 +201,6 @@ const createMockUIState = (overrides: Partial<UIState> = {}): UIState =>
|
|||||||
isBackgroundTaskVisible: false,
|
isBackgroundTaskVisible: false,
|
||||||
embeddedShellFocused: false,
|
embeddedShellFocused: false,
|
||||||
showIsExpandableHint: false,
|
showIsExpandableHint: false,
|
||||||
quota: {
|
|
||||||
userTier: undefined,
|
|
||||||
stats: undefined,
|
|
||||||
proQuotaRequest: null,
|
|
||||||
validationRequest: null,
|
|
||||||
},
|
|
||||||
...overrides,
|
...overrides,
|
||||||
}) as UIState;
|
}) as UIState;
|
||||||
|
|
||||||
@@ -245,6 +239,7 @@ const createMockConfig = (overrides = {}): Config =>
|
|||||||
...overrides,
|
...overrides,
|
||||||
}) as unknown as Config;
|
}) as unknown as Config;
|
||||||
|
|
||||||
|
import { QuotaContext, type QuotaState } from '../contexts/QuotaContext.js';
|
||||||
import { InputContext, type InputState } from '../contexts/InputContext.js';
|
import { InputContext, type InputState } from '../contexts/InputContext.js';
|
||||||
|
|
||||||
const renderComposer = async (
|
const renderComposer = async (
|
||||||
@@ -253,6 +248,7 @@ const renderComposer = async (
|
|||||||
config = createMockConfig(),
|
config = createMockConfig(),
|
||||||
uiActions = createMockUIActions(),
|
uiActions = createMockUIActions(),
|
||||||
inputStateOverrides: Partial<InputState> = {},
|
inputStateOverrides: Partial<InputState> = {},
|
||||||
|
quotaStateOverrides: Partial<QuotaState> = {},
|
||||||
) => {
|
) => {
|
||||||
const inputState = {
|
const inputState = {
|
||||||
buffer: { text: '' } as unknown as TextBuffer,
|
buffer: { text: '' } as unknown as TextBuffer,
|
||||||
@@ -266,16 +262,28 @@ const renderComposer = async (
|
|||||||
...inputStateOverrides,
|
...inputStateOverrides,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const quotaState: QuotaState = {
|
||||||
|
userTier: undefined,
|
||||||
|
stats: undefined,
|
||||||
|
proQuotaRequest: null,
|
||||||
|
validationRequest: null,
|
||||||
|
overageMenuRequest: null,
|
||||||
|
emptyWalletRequest: null,
|
||||||
|
...quotaStateOverrides,
|
||||||
|
};
|
||||||
|
|
||||||
const result = await render(
|
const result = await render(
|
||||||
<ConfigContext.Provider value={config as unknown as Config}>
|
<ConfigContext.Provider value={config as unknown as Config}>
|
||||||
<SettingsContext.Provider value={settings as unknown as LoadedSettings}>
|
<SettingsContext.Provider value={settings as unknown as LoadedSettings}>
|
||||||
<InputContext.Provider value={inputState}>
|
<QuotaContext.Provider value={quotaState}>
|
||||||
<UIStateContext.Provider value={uiState}>
|
<InputContext.Provider value={inputState}>
|
||||||
<UIActionsContext.Provider value={uiActions}>
|
<UIStateContext.Provider value={uiState}>
|
||||||
<Composer isFocused={true} />
|
<UIActionsContext.Provider value={uiActions}>
|
||||||
</UIActionsContext.Provider>
|
<Composer isFocused={true} />
|
||||||
</UIStateContext.Provider>
|
</UIActionsContext.Provider>
|
||||||
</InputContext.Provider>
|
</UIStateContext.Provider>
|
||||||
|
</InputContext.Provider>
|
||||||
|
</QuotaContext.Provider>
|
||||||
</SettingsContext.Provider>
|
</SettingsContext.Provider>
|
||||||
</ConfigContext.Provider>,
|
</ConfigContext.Provider>,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { DialogManager } from './DialogManager.js';
|
|||||||
import { describe, it, expect, vi } from 'vitest';
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
import { Text } from 'ink';
|
import { Text } from 'ink';
|
||||||
import { type UIState } from '../contexts/UIStateContext.js';
|
import { type UIState } from '../contexts/UIStateContext.js';
|
||||||
|
import { type QuotaState } from '../contexts/QuotaContext.js';
|
||||||
import { type RestartReason } from '../hooks/useIdeTrustListener.js';
|
import { type RestartReason } from '../hooks/useIdeTrustListener.js';
|
||||||
import { type IdeInfo } from '@google/gemini-cli-core';
|
import { type IdeInfo } from '@google/gemini-cli-core';
|
||||||
|
|
||||||
@@ -75,14 +76,6 @@ describe('DialogManager', () => {
|
|||||||
terminalWidth: 80,
|
terminalWidth: 80,
|
||||||
confirmUpdateExtensionRequests: [],
|
confirmUpdateExtensionRequests: [],
|
||||||
showIdeRestartPrompt: false,
|
showIdeRestartPrompt: false,
|
||||||
quota: {
|
|
||||||
userTier: undefined,
|
|
||||||
stats: undefined,
|
|
||||||
proQuotaRequest: null,
|
|
||||||
validationRequest: null,
|
|
||||||
overageMenuRequest: null,
|
|
||||||
emptyWalletRequest: null,
|
|
||||||
},
|
|
||||||
shouldShowIdePrompt: false,
|
shouldShowIdePrompt: false,
|
||||||
isFolderTrustDialogOpen: false,
|
isFolderTrustDialogOpen: false,
|
||||||
loopDetectionConfirmationRequest: null,
|
loopDetectionConfirmationRequest: null,
|
||||||
@@ -112,7 +105,7 @@ describe('DialogManager', () => {
|
|||||||
unmount();
|
unmount();
|
||||||
});
|
});
|
||||||
|
|
||||||
const testCases: Array<[Partial<UIState>, string]> = [
|
const testCases: Array<[Partial<UIState>, string, Partial<QuotaState>?]> = [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
showIdeRestartPrompt: true,
|
showIdeRestartPrompt: true,
|
||||||
@@ -121,23 +114,17 @@ describe('DialogManager', () => {
|
|||||||
'IdeTrustChangeDialog',
|
'IdeTrustChangeDialog',
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
|
{},
|
||||||
|
'ProQuotaDialog',
|
||||||
{
|
{
|
||||||
quota: {
|
proQuotaRequest: {
|
||||||
userTier: undefined,
|
failedModel: 'a',
|
||||||
stats: undefined,
|
fallbackModel: 'b',
|
||||||
proQuotaRequest: {
|
message: 'c',
|
||||||
failedModel: 'a',
|
isTerminalQuotaError: false,
|
||||||
fallbackModel: 'b',
|
resolve: vi.fn(),
|
||||||
message: 'c',
|
|
||||||
isTerminalQuotaError: false,
|
|
||||||
resolve: vi.fn(),
|
|
||||||
},
|
|
||||||
validationRequest: null,
|
|
||||||
overageMenuRequest: null,
|
|
||||||
emptyWalletRequest: null,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
'ProQuotaDialog',
|
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
@@ -195,7 +182,11 @@ describe('DialogManager', () => {
|
|||||||
|
|
||||||
it.each(testCases)(
|
it.each(testCases)(
|
||||||
'renders %s when state is %o',
|
'renders %s when state is %o',
|
||||||
async (uiStateOverride, expectedComponent) => {
|
async (
|
||||||
|
uiStateOverride: Partial<UIState>,
|
||||||
|
expectedComponent: string,
|
||||||
|
quotaStateOverride?: Partial<QuotaState>,
|
||||||
|
) => {
|
||||||
const { lastFrame, unmount } = await renderWithProviders(
|
const { lastFrame, unmount } = await renderWithProviders(
|
||||||
<DialogManager {...defaultProps} />,
|
<DialogManager {...defaultProps} />,
|
||||||
{
|
{
|
||||||
@@ -203,6 +194,7 @@ describe('DialogManager', () => {
|
|||||||
...baseUiState,
|
...baseUiState,
|
||||||
...uiStateOverride,
|
...uiStateOverride,
|
||||||
} as Partial<UIState> as UIState,
|
} as Partial<UIState> as UIState,
|
||||||
|
quotaState: quotaStateOverride,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
expect(lastFrame()).toContain(expectedComponent);
|
expect(lastFrame()).toContain(expectedComponent);
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import { PermissionsModifyTrustDialog } from './PermissionsModifyTrustDialog.js'
|
|||||||
import { ModelDialog } from './ModelDialog.js';
|
import { ModelDialog } from './ModelDialog.js';
|
||||||
import { theme } from '../semantic-colors.js';
|
import { theme } from '../semantic-colors.js';
|
||||||
import { useUIState } from '../contexts/UIStateContext.js';
|
import { useUIState } from '../contexts/UIStateContext.js';
|
||||||
|
import { useQuotaState } from '../contexts/QuotaContext.js';
|
||||||
import { useUIActions } from '../contexts/UIActionsContext.js';
|
import { useUIActions } from '../contexts/UIActionsContext.js';
|
||||||
import { useConfig } from '../contexts/ConfigContext.js';
|
import { useConfig } from '../contexts/ConfigContext.js';
|
||||||
import { useSettings } from '../contexts/SettingsContext.js';
|
import { useSettings } from '../contexts/SettingsContext.js';
|
||||||
@@ -52,6 +53,7 @@ export const DialogManager = ({
|
|||||||
const settings = useSettings();
|
const settings = useSettings();
|
||||||
|
|
||||||
const uiState = useUIState();
|
const uiState = useUIState();
|
||||||
|
const quotaState = useQuotaState();
|
||||||
const uiActions = useUIActions();
|
const uiActions = useUIActions();
|
||||||
const {
|
const {
|
||||||
constrainHeight,
|
constrainHeight,
|
||||||
@@ -74,54 +76,50 @@ export const DialogManager = ({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (uiState.quota.proQuotaRequest) {
|
if (quotaState.proQuotaRequest) {
|
||||||
return (
|
return (
|
||||||
<ProQuotaDialog
|
<ProQuotaDialog
|
||||||
failedModel={uiState.quota.proQuotaRequest.failedModel}
|
failedModel={quotaState.proQuotaRequest.failedModel}
|
||||||
fallbackModel={uiState.quota.proQuotaRequest.fallbackModel}
|
fallbackModel={quotaState.proQuotaRequest.fallbackModel}
|
||||||
message={uiState.quota.proQuotaRequest.message}
|
message={quotaState.proQuotaRequest.message}
|
||||||
isTerminalQuotaError={
|
isTerminalQuotaError={quotaState.proQuotaRequest.isTerminalQuotaError}
|
||||||
uiState.quota.proQuotaRequest.isTerminalQuotaError
|
isModelNotFoundError={!!quotaState.proQuotaRequest.isModelNotFoundError}
|
||||||
}
|
authType={quotaState.proQuotaRequest.authType}
|
||||||
isModelNotFoundError={
|
|
||||||
!!uiState.quota.proQuotaRequest.isModelNotFoundError
|
|
||||||
}
|
|
||||||
authType={uiState.quota.proQuotaRequest.authType}
|
|
||||||
tierName={config?.getUserTierName()}
|
tierName={config?.getUserTierName()}
|
||||||
onChoice={uiActions.handleProQuotaChoice}
|
onChoice={uiActions.handleProQuotaChoice}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (uiState.quota.validationRequest) {
|
if (quotaState.validationRequest) {
|
||||||
return (
|
return (
|
||||||
<ValidationDialog
|
<ValidationDialog
|
||||||
validationLink={uiState.quota.validationRequest.validationLink}
|
validationLink={quotaState.validationRequest.validationLink}
|
||||||
validationDescription={
|
validationDescription={
|
||||||
uiState.quota.validationRequest.validationDescription
|
quotaState.validationRequest.validationDescription
|
||||||
}
|
}
|
||||||
learnMoreUrl={uiState.quota.validationRequest.learnMoreUrl}
|
learnMoreUrl={quotaState.validationRequest.learnMoreUrl}
|
||||||
onChoice={uiActions.handleValidationChoice}
|
onChoice={uiActions.handleValidationChoice}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (uiState.quota.overageMenuRequest) {
|
if (quotaState.overageMenuRequest) {
|
||||||
return (
|
return (
|
||||||
<OverageMenuDialog
|
<OverageMenuDialog
|
||||||
failedModel={uiState.quota.overageMenuRequest.failedModel}
|
failedModel={quotaState.overageMenuRequest.failedModel}
|
||||||
fallbackModel={uiState.quota.overageMenuRequest.fallbackModel}
|
fallbackModel={quotaState.overageMenuRequest.fallbackModel}
|
||||||
resetTime={uiState.quota.overageMenuRequest.resetTime}
|
resetTime={quotaState.overageMenuRequest.resetTime}
|
||||||
creditBalance={uiState.quota.overageMenuRequest.creditBalance}
|
creditBalance={quotaState.overageMenuRequest.creditBalance}
|
||||||
onChoice={uiActions.handleOverageMenuChoice}
|
onChoice={uiActions.handleOverageMenuChoice}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (uiState.quota.emptyWalletRequest) {
|
if (quotaState.emptyWalletRequest) {
|
||||||
return (
|
return (
|
||||||
<EmptyWalletDialog
|
<EmptyWalletDialog
|
||||||
failedModel={uiState.quota.emptyWalletRequest.failedModel}
|
failedModel={quotaState.emptyWalletRequest.failedModel}
|
||||||
fallbackModel={uiState.quota.emptyWalletRequest.fallbackModel}
|
fallbackModel={quotaState.emptyWalletRequest.fallbackModel}
|
||||||
resetTime={uiState.quota.emptyWalletRequest.resetTime}
|
resetTime={quotaState.emptyWalletRequest.resetTime}
|
||||||
onGetCredits={uiState.quota.emptyWalletRequest.onGetCredits}
|
onGetCredits={quotaState.emptyWalletRequest.onGetCredits}
|
||||||
onChoice={uiActions.handleEmptyWalletChoice}
|
onChoice={uiActions.handleEmptyWalletChoice}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -267,17 +267,12 @@ describe('<Footer />', () => {
|
|||||||
width: 120,
|
width: 120,
|
||||||
uiState: {
|
uiState: {
|
||||||
sessionStats: mockSessionStats,
|
sessionStats: mockSessionStats,
|
||||||
quota: {
|
},
|
||||||
userTier: undefined,
|
quotaState: {
|
||||||
stats: {
|
stats: {
|
||||||
remaining: 15,
|
remaining: 15,
|
||||||
limit: 100,
|
limit: 100,
|
||||||
resetTime: undefined,
|
resetTime: undefined,
|
||||||
},
|
|
||||||
proQuotaRequest: null,
|
|
||||||
validationRequest: null,
|
|
||||||
overageMenuRequest: null,
|
|
||||||
emptyWalletRequest: null,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -292,17 +287,12 @@ describe('<Footer />', () => {
|
|||||||
width: 120,
|
width: 120,
|
||||||
uiState: {
|
uiState: {
|
||||||
sessionStats: mockSessionStats,
|
sessionStats: mockSessionStats,
|
||||||
quota: {
|
},
|
||||||
userTier: undefined,
|
quotaState: {
|
||||||
stats: {
|
stats: {
|
||||||
remaining: 85,
|
remaining: 85,
|
||||||
limit: 100,
|
limit: 100,
|
||||||
resetTime: undefined,
|
resetTime: undefined,
|
||||||
},
|
|
||||||
proQuotaRequest: null,
|
|
||||||
validationRequest: null,
|
|
||||||
overageMenuRequest: null,
|
|
||||||
emptyWalletRequest: null,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -317,17 +307,12 @@ describe('<Footer />', () => {
|
|||||||
width: 120,
|
width: 120,
|
||||||
uiState: {
|
uiState: {
|
||||||
sessionStats: mockSessionStats,
|
sessionStats: mockSessionStats,
|
||||||
quota: {
|
},
|
||||||
userTier: undefined,
|
quotaState: {
|
||||||
stats: {
|
stats: {
|
||||||
remaining: 0,
|
remaining: 0,
|
||||||
limit: 100,
|
limit: 100,
|
||||||
resetTime: undefined,
|
resetTime: undefined,
|
||||||
},
|
|
||||||
proQuotaRequest: null,
|
|
||||||
validationRequest: null,
|
|
||||||
overageMenuRequest: null,
|
|
||||||
emptyWalletRequest: null,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import { ContextUsageDisplay } from './ContextUsageDisplay.js';
|
|||||||
import { QuotaDisplay } from './QuotaDisplay.js';
|
import { QuotaDisplay } from './QuotaDisplay.js';
|
||||||
import { DebugProfiler } from './DebugProfiler.js';
|
import { DebugProfiler } from './DebugProfiler.js';
|
||||||
import { useUIState } from '../contexts/UIStateContext.js';
|
import { useUIState } from '../contexts/UIStateContext.js';
|
||||||
|
import { useQuotaState } from '../contexts/QuotaContext.js';
|
||||||
import { useConfig } from '../contexts/ConfigContext.js';
|
import { useConfig } from '../contexts/ConfigContext.js';
|
||||||
import { useSettings } from '../contexts/SettingsContext.js';
|
import { useSettings } from '../contexts/SettingsContext.js';
|
||||||
import { useVimMode } from '../contexts/VimModeContext.js';
|
import { useVimMode } from '../contexts/VimModeContext.js';
|
||||||
@@ -174,6 +175,7 @@ interface FooterColumn {
|
|||||||
|
|
||||||
export const Footer: React.FC = () => {
|
export const Footer: React.FC = () => {
|
||||||
const uiState = useUIState();
|
const uiState = useUIState();
|
||||||
|
const quotaState = useQuotaState();
|
||||||
const { copyModeEnabled } = useInputState();
|
const { copyModeEnabled } = useInputState();
|
||||||
const config = useConfig();
|
const config = useConfig();
|
||||||
const settings = useSettings();
|
const settings = useSettings();
|
||||||
@@ -203,7 +205,6 @@ export const Footer: React.FC = () => {
|
|||||||
promptTokenCount,
|
promptTokenCount,
|
||||||
isTrustedFolder,
|
isTrustedFolder,
|
||||||
terminalWidth,
|
terminalWidth,
|
||||||
quotaStats,
|
|
||||||
} = {
|
} = {
|
||||||
model: uiState.currentModel,
|
model: uiState.currentModel,
|
||||||
targetDir: config.getTargetDir(),
|
targetDir: config.getTargetDir(),
|
||||||
@@ -216,9 +217,10 @@ export const Footer: React.FC = () => {
|
|||||||
promptTokenCount: uiState.sessionStats.lastPromptTokenCount,
|
promptTokenCount: uiState.sessionStats.lastPromptTokenCount,
|
||||||
isTrustedFolder: uiState.isTrustedFolder,
|
isTrustedFolder: uiState.isTrustedFolder,
|
||||||
terminalWidth: uiState.terminalWidth,
|
terminalWidth: uiState.terminalWidth,
|
||||||
quotaStats: uiState.quota.stats,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const quotaStats = quotaState.stats;
|
||||||
|
|
||||||
const isFullErrorVerbosity = settings.merged.ui.errorVerbosity === 'full';
|
const isFullErrorVerbosity = settings.merged.ui.errorVerbosity === 'full';
|
||||||
const showErrorSummary =
|
const showErrorSummary =
|
||||||
!showErrorDetails &&
|
!showErrorDetails &&
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
/**
|
||||||
|
* @license
|
||||||
|
* Copyright 2026 Google LLC
|
||||||
|
* SPDX-License-Identifier: Apache-2.0
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { createContext, useContext } from 'react';
|
||||||
|
import type { QuotaStats } from '../types.js';
|
||||||
|
import type { UserTierId } from '@google/gemini-cli-core';
|
||||||
|
import type {
|
||||||
|
ProQuotaDialogRequest,
|
||||||
|
ValidationDialogRequest,
|
||||||
|
OverageMenuDialogRequest,
|
||||||
|
EmptyWalletDialogRequest,
|
||||||
|
} from './UIStateContext.js';
|
||||||
|
|
||||||
|
export interface QuotaState {
|
||||||
|
userTier?: UserTierId;
|
||||||
|
stats?: QuotaStats;
|
||||||
|
proQuotaRequest?: ProQuotaDialogRequest | null;
|
||||||
|
validationRequest?: ValidationDialogRequest | null;
|
||||||
|
overageMenuRequest?: OverageMenuDialogRequest | null;
|
||||||
|
emptyWalletRequest?: EmptyWalletDialogRequest | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const QuotaContext = createContext<QuotaState | null>(null);
|
||||||
|
|
||||||
|
export const useQuotaState = () => {
|
||||||
|
const context = useContext(QuotaContext);
|
||||||
|
if (!context) {
|
||||||
|
throw new Error('useQuotaState must be used within a QuotaProvider');
|
||||||
|
}
|
||||||
|
return context;
|
||||||
|
};
|
||||||
@@ -9,7 +9,6 @@ import type {
|
|||||||
HistoryItem,
|
HistoryItem,
|
||||||
ThoughtSummary,
|
ThoughtSummary,
|
||||||
ConfirmationRequest,
|
ConfirmationRequest,
|
||||||
QuotaStats,
|
|
||||||
LoopDetectionConfirmationRequest,
|
LoopDetectionConfirmationRequest,
|
||||||
HistoryItemWithoutId,
|
HistoryItemWithoutId,
|
||||||
StreamingState,
|
StreamingState,
|
||||||
@@ -21,7 +20,6 @@ import type { CommandContext, SlashCommand } from '../commands/types.js';
|
|||||||
import type {
|
import type {
|
||||||
IdeContext,
|
IdeContext,
|
||||||
ApprovalMode,
|
ApprovalMode,
|
||||||
UserTierId,
|
|
||||||
IdeInfo,
|
IdeInfo,
|
||||||
AuthType,
|
AuthType,
|
||||||
FallbackIntent,
|
FallbackIntent,
|
||||||
@@ -86,16 +84,6 @@ import { type RestartReason } from '../hooks/useIdeTrustListener.js';
|
|||||||
import type { TerminalBackgroundColor } from '../utils/terminalCapabilityManager.js';
|
import type { TerminalBackgroundColor } from '../utils/terminalCapabilityManager.js';
|
||||||
import type { BackgroundTask } from '../hooks/useExecutionLifecycle.js';
|
import type { BackgroundTask } from '../hooks/useExecutionLifecycle.js';
|
||||||
|
|
||||||
export interface QuotaState {
|
|
||||||
userTier: UserTierId | undefined;
|
|
||||||
stats: QuotaStats | undefined;
|
|
||||||
proQuotaRequest: ProQuotaDialogRequest | null;
|
|
||||||
validationRequest: ValidationDialogRequest | null;
|
|
||||||
// G1 AI Credits overage flow
|
|
||||||
overageMenuRequest: OverageMenuDialogRequest | null;
|
|
||||||
emptyWalletRequest: EmptyWalletDialogRequest | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AccountSuspensionInfo {
|
export interface AccountSuspensionInfo {
|
||||||
message: string;
|
message: string;
|
||||||
appealUrl?: string;
|
appealUrl?: string;
|
||||||
@@ -169,8 +157,6 @@ export interface UIState {
|
|||||||
queueErrorMessage: string | null;
|
queueErrorMessage: string | null;
|
||||||
showApprovalModeIndicator: ApprovalMode;
|
showApprovalModeIndicator: ApprovalMode;
|
||||||
allowPlanMode: boolean;
|
allowPlanMode: boolean;
|
||||||
// Quota-related state
|
|
||||||
quota: QuotaState;
|
|
||||||
currentModel: string;
|
currentModel: string;
|
||||||
contextFileNames: string[];
|
contextFileNames: string[];
|
||||||
errorCount: number;
|
errorCount: number;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
import { useMemo } from 'react';
|
import { useMemo } from 'react';
|
||||||
import { useUIState } from '../contexts/UIStateContext.js';
|
import { useUIState } from '../contexts/UIStateContext.js';
|
||||||
|
import { useQuotaState } from '../contexts/QuotaContext.js';
|
||||||
import { useSettings } from '../contexts/SettingsContext.js';
|
import { useSettings } from '../contexts/SettingsContext.js';
|
||||||
import { CoreToolCallStatus, ApprovalMode } from '@google/gemini-cli-core';
|
import { CoreToolCallStatus, ApprovalMode } from '@google/gemini-cli-core';
|
||||||
import { type HistoryItemToolGroup, StreamingState } from '../types.js';
|
import { type HistoryItemToolGroup, StreamingState } from '../types.js';
|
||||||
@@ -18,6 +19,7 @@ import { theme } from '../semantic-colors.js';
|
|||||||
*/
|
*/
|
||||||
export const useComposerStatus = () => {
|
export const useComposerStatus = () => {
|
||||||
const uiState = useUIState();
|
const uiState = useUIState();
|
||||||
|
const quotaState = useQuotaState();
|
||||||
const settings = useSettings();
|
const settings = useSettings();
|
||||||
|
|
||||||
const hasPendingToolConfirmation = useMemo(
|
const hasPendingToolConfirmation = useMemo(
|
||||||
@@ -40,8 +42,8 @@ export const useComposerStatus = () => {
|
|||||||
Boolean(uiState.authConsentRequest) ||
|
Boolean(uiState.authConsentRequest) ||
|
||||||
(uiState.confirmUpdateExtensionRequests?.length ?? 0) > 0 ||
|
(uiState.confirmUpdateExtensionRequests?.length ?? 0) > 0 ||
|
||||||
Boolean(uiState.loopDetectionConfirmationRequest) ||
|
Boolean(uiState.loopDetectionConfirmationRequest) ||
|
||||||
Boolean(uiState.quota.proQuotaRequest) ||
|
Boolean(quotaState.proQuotaRequest) ||
|
||||||
Boolean(uiState.quota.validationRequest) ||
|
Boolean(quotaState.validationRequest) ||
|
||||||
Boolean(uiState.customDialog);
|
Boolean(uiState.customDialog);
|
||||||
|
|
||||||
const isInteractiveShellWaiting = Boolean(
|
const isInteractiveShellWaiting = Boolean(
|
||||||
|
|||||||
Reference in New Issue
Block a user