Skip to content
8 changes: 7 additions & 1 deletion packages/cli/src/test-utils/render.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* @license
* Copyright 2025 Google LLC
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

Expand Down Expand Up @@ -151,6 +151,12 @@ const baseMockUiState = {
activePtyId: undefined,
backgroundShells: new Map(),
backgroundShellHeight: 0,
quota: {
userTier: undefined,
stats: undefined,
proQuotaRequest: null,
validationRequest: null,
},
};

export const mockAppState: AppState = {
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/ui/App.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* @license
* Copyright 2025 Google LLC
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

Expand Down
6 changes: 3 additions & 3 deletions packages/cli/src/ui/AppContainer.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* @license
* Copyright 2025 Google LLC
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

Expand Down Expand Up @@ -951,7 +951,7 @@ describe('AppContainer State Management', () => {
});
await waitFor(() => {
// Assert that the context value is as expected
expect(capturedUIState.proQuotaRequest).toBeNull();
expect(capturedUIState.quota.proQuotaRequest).toBeNull();
});
unmount!();
});
Expand All @@ -976,7 +976,7 @@ describe('AppContainer State Management', () => {
});
await waitFor(() => {
// Assert: The mock request is correctly passed through the context
expect(capturedUIState.proQuotaRequest).toEqual(mockRequest);
expect(capturedUIState.quota.proQuotaRequest).toEqual(mockRequest);
});
unmount!();
});
Expand Down
37 changes: 33 additions & 4 deletions packages/cli/src/ui/AppContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* @license
* Copyright 2025 Google LLC
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

Expand Down Expand Up @@ -29,6 +29,7 @@ import {
AuthState,
type ConfirmationRequest,
type PermissionConfirmationRequest,
type QuotaStats,
} from './types.js';
import { checkPermissions } from './hooks/atCommandProcessor.js';
import { MessageType, StreamingState } from './types.js';
Expand Down Expand Up @@ -323,6 +324,16 @@ export const AppContainer = (props: AppContainerProps) => {
const [currentModel, setCurrentModel] = useState(config.getModel());

const [userTier, setUserTier] = useState<UserTierId | undefined>(undefined);
const [quotaStats, setQuotaStats] = useState<QuotaStats | undefined>(() => {
const remaining = config.getQuotaRemaining();
const limit = config.getQuotaLimit();
const resetTime = config.getQuotaResetTime();
return remaining !== undefined ||
limit !== undefined ||
resetTime !== undefined
? { remaining, limit, resetTime }
: undefined;
});

const [isConfigInitialized, setConfigInitialized] = useState(false);

Expand Down Expand Up @@ -425,9 +436,23 @@ export const AppContainer = (props: AppContainerProps) => {
setCurrentModel(config.getModel());
};

const handleQuotaChanged = (payload: {
remaining: number | undefined;
limit: number | undefined;
resetTime?: string;
}) => {
setQuotaStats({
remaining: payload.remaining,
limit: payload.limit,
resetTime: payload.resetTime,
});
};

coreEvents.on(CoreEvent.ModelChanged, handleModelChanged);
coreEvents.on(CoreEvent.QuotaChanged, handleQuotaChanged);
return () => {
coreEvents.off(CoreEvent.ModelChanged, handleModelChanged);
coreEvents.off(CoreEvent.QuotaChanged, handleQuotaChanged);
};
}, [config]);

Expand Down Expand Up @@ -1887,9 +1912,12 @@ Logging in with Google... Restarting Gemini CLI to continue.
queueErrorMessage,
showApprovalModeIndicator,
currentModel,
userTier,
proQuotaRequest,
validationRequest,
quota: {
userTier,
stats: quotaStats,
proQuotaRequest,
validationRequest,
},
contextFileNames,
errorCount,
availableTerminalHeight,
Expand Down Expand Up @@ -1994,6 +2022,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
queueErrorMessage,
showApprovalModeIndicator,
userTier,
quotaStats,
proQuotaRequest,
validationRequest,
contextFileNames,
Expand Down
21 changes: 20 additions & 1 deletion packages/cli/src/ui/commands/statsCommand.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* @license
* Copyright 2025 Google LLC
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

Expand Down Expand Up @@ -54,6 +54,7 @@ describe('statsCommand', () => {
selectedAuthType: '',
tier: undefined,
userEmail: 'mock@example.com',
currentModel: undefined,
});
});

Expand All @@ -63,9 +64,20 @@ describe('statsCommand', () => {
const mockQuota = { buckets: [] };
const mockRefreshUserQuota = vi.fn().mockResolvedValue(mockQuota);
const mockGetUserTierName = vi.fn().mockReturnValue('Basic');
const mockGetModel = vi.fn().mockReturnValue('gemini-pro');
const mockGetQuotaRemaining = vi.fn().mockReturnValue(85);
const mockGetQuotaLimit = vi.fn().mockReturnValue(100);
const mockGetQuotaResetTime = vi
.fn()
.mockReturnValue('2025-01-01T12:00:00Z');

mockContext.services.config = {
refreshUserQuota: mockRefreshUserQuota,
getUserTierName: mockGetUserTierName,
getModel: mockGetModel,
getQuotaRemaining: mockGetQuotaRemaining,
getQuotaLimit: mockGetQuotaLimit,
getQuotaResetTime: mockGetQuotaResetTime,
} as unknown as Config;

await statsCommand.action(mockContext, '');
Expand All @@ -75,6 +87,10 @@ describe('statsCommand', () => {
expect.objectContaining({
quotas: mockQuota,
tier: 'Basic',
currentModel: 'gemini-pro',
pooledRemaining: 85,
pooledLimit: 100,
pooledResetTime: '2025-01-01T12:00:00Z',
}),
);
});
Expand All @@ -93,6 +109,9 @@ describe('statsCommand', () => {
selectedAuthType: '',
tier: undefined,
userEmail: 'mock@example.com',
currentModel: undefined,
pooledRemaining: undefined,
pooledLimit: undefined,
});
});

Expand Down
15 changes: 14 additions & 1 deletion packages/cli/src/ui/commands/statsCommand.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* @license
* Copyright 2025 Google LLC
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

Expand Down Expand Up @@ -44,19 +44,24 @@ async function defaultSessionView(context: CommandContext) {
const wallDuration = now.getTime() - sessionStartTime.getTime();

const { selectedAuthType, userEmail, tier } = getUserIdentity(context);
const currentModel = context.services.config?.getModel();

const statsItem: HistoryItemStats = {
type: MessageType.STATS,
duration: formatDuration(wallDuration),
selectedAuthType,
userEmail,
tier,
currentModel,
};

if (context.services.config) {
const quota = await context.services.config.refreshUserQuota();
if (quota) {
statsItem.quotas = quota;
statsItem.pooledRemaining = context.services.config.getQuotaRemaining();
statsItem.pooledLimit = context.services.config.getQuotaLimit();
statsItem.pooledResetTime = context.services.config.getQuotaResetTime();
}
}

Expand Down Expand Up @@ -89,11 +94,19 @@ export const statsCommand: SlashCommand = {
autoExecute: true,
action: (context: CommandContext) => {
const { selectedAuthType, userEmail, tier } = getUserIdentity(context);
const currentModel = context.services.config?.getModel();
const pooledRemaining = context.services.config?.getQuotaRemaining();
const pooledLimit = context.services.config?.getQuotaLimit();
const pooledResetTime = context.services.config?.getQuotaResetTime();
context.ui.addItem({
type: MessageType.MODEL_STATS,
selectedAuthType,
userEmail,
tier,
currentModel,
pooledRemaining,
pooledLimit,
pooledResetTime,
} as HistoryItemModelStats);
},
},
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/ui/components/AppHeader.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* @license
* Copyright 2025 Google LLC
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

Expand Down
Loading
Loading