/** * @license * Copyright 2026 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import type React from 'react'; import { Box, Text, useStdout } from 'ink'; import { ThemedGradient } from './ThemedGradient.js'; import { theme } from '../semantic-colors.js'; import { formatDuration, formatResetTime } from '../utils/formatters.js'; import { useSessionStats, type ModelMetrics, } from '../contexts/SessionContext.js'; import { getStatusColor, TOOL_SUCCESS_RATE_HIGH, TOOL_SUCCESS_RATE_MEDIUM, USER_AGREEMENT_RATE_HIGH, USER_AGREEMENT_RATE_MEDIUM, CACHE_EFFICIENCY_HIGH, CACHE_EFFICIENCY_MEDIUM, getUsedStatusColor, QUOTA_USED_WARNING_THRESHOLD, QUOTA_USED_CRITICAL_THRESHOLD, } from '../utils/displayUtils.js'; import { computeSessionStats } from '../utils/computeStats.js'; import { type RetrieveUserQuotaResponse, isActiveModel, getDisplayString, isAutoModel, AuthType, } from '@google/gemini-cli-core'; import { useSettings } from '../contexts/SettingsContext.js'; import { useConfig } from '../contexts/ConfigContext.js'; import type { QuotaStats } from '../types.js'; import { QuotaStatsInfo } from './QuotaStatsInfo.js'; // A more flexible and powerful StatRow component interface StatRowProps { title: string; children: React.ReactNode; // Use children to allow for complex, colored values } const StatRow: React.FC = ({ title, children }) => ( {/* Fixed width for the label creates a clean "gutter" for alignment */} {title} {children} ); // A SubStatRow for indented, secondary information interface SubStatRowProps { title: string; children: React.ReactNode; } const SubStatRow: React.FC = ({ title, children }) => ( {/* Adjust width for the "» " prefix */} » {title} {children} ); // A Section component to group related stats interface SectionProps { title: string; children: React.ReactNode; } const Section: React.FC = ({ title, children }) => ( {title} {children} ); // Logic for building the unified list of table rows const buildModelRows = ( models: Record, quotas?: RetrieveUserQuotaResponse, useGemini3_1 = false, useCustomToolModel = false, ) => { const getBaseModelName = (name: string) => name.replace('-001', ''); const usedModelNames = new Set( Object.keys(models).map(getBaseModelName).map(getDisplayString), ); // 1. Models with active usage const activeRows = Object.entries(models).map(([name, metrics]) => { const modelName = getBaseModelName(name); const cachedTokens = metrics.tokens.cached; const inputTokens = metrics.tokens.input; return { key: name, modelName: getDisplayString(modelName), requests: metrics.api.totalRequests, cachedTokens: cachedTokens.toLocaleString(), inputTokens: inputTokens.toLocaleString(), outputTokens: metrics.tokens.candidates.toLocaleString(), bucket: quotas?.buckets?.find((b) => b.modelId === modelName), isActive: true, }; }); // 2. Models with quota only const quotaRows = quotas?.buckets ?.filter( (b) => b.modelId && isActiveModel(b.modelId, useGemini3_1, useCustomToolModel) && !usedModelNames.has(getDisplayString(b.modelId)), ) .map((bucket) => ({ key: bucket.modelId!, modelName: getDisplayString(bucket.modelId!), requests: '-', cachedTokens: '-', inputTokens: '-', outputTokens: '-', bucket, isActive: false, })) || []; return [...activeRows, ...quotaRows]; }; const ModelUsageTable: React.FC<{ models: Record; quotas?: RetrieveUserQuotaResponse; cacheEfficiency: number; totalCachedTokens: number; currentModel?: string; pooledRemaining?: number; pooledLimit?: number; pooledResetTime?: string; useGemini3_1?: boolean; useCustomToolModel?: boolean; }> = ({ models, quotas, cacheEfficiency, totalCachedTokens, currentModel, pooledRemaining, pooledLimit, pooledResetTime, useGemini3_1, useCustomToolModel, }) => { const { stdout } = useStdout(); const terminalWidth = stdout?.columns ?? 84; const rows = buildModelRows(models, quotas, useGemini3_1, useCustomToolModel); if (rows.length === 0) { return null; } const showQuotaColumn = !!quotas && rows.some((row) => !!row.bucket); const nameWidth = 23; const requestsWidth = 5; const uncachedWidth = 15; const cachedWidth = 14; const outputTokensWidth = 15; const percentageWidth = showQuotaColumn ? 6 : 0; const resetWidth = 22; // Total width of other columns (including parent box paddingX={2}) const fixedWidth = nameWidth + requestsWidth + percentageWidth + resetWidth; const outerPadding = 4; const availableForUsage = terminalWidth - outerPadding - fixedWidth; const usageLimitWidth = showQuotaColumn ? Math.max(10, Math.min(24, availableForUsage)) : 0; const progressBarWidth = Math.max(2, usageLimitWidth - 4); const renderProgressBar = ( usedFraction: number, color: string, totalSteps = 20, ) => { let filledSteps = Math.round(usedFraction * totalSteps); // If something is used (fraction > 0) but rounds to 0, show 1 tick. // If < 100% (fraction < 1) but rounds to 20, show 19 ticks. if (usedFraction > 0 && usedFraction < 1) { filledSteps = Math.min(Math.max(filledSteps, 1), totalSteps - 1); } const emptySteps = Math.max(0, totalSteps - filledSteps); return ( {'▬'.repeat(filledSteps)} {'▬'.repeat(emptySteps)} ); }; const cacheEfficiencyColor = getStatusColor(cacheEfficiency, { green: CACHE_EFFICIENCY_HIGH, yellow: CACHE_EFFICIENCY_MEDIUM, }); const totalWidth = nameWidth + requestsWidth + (showQuotaColumn ? usageLimitWidth + percentageWidth + resetWidth : uncachedWidth + cachedWidth + outputTokensWidth); const isAuto = currentModel && isAutoModel(currentModel); return ( {isAuto && showQuotaColumn && pooledRemaining !== undefined && pooledLimit !== undefined && pooledLimit > 0 && ( For a full token breakdown, run `/stats model`. )} Model Reqs {!showQuotaColumn && ( <> Input Tokens Cache Reads Output Tokens )} {showQuotaColumn && ( <> Model usage Usage resets )} {/* Divider */} {rows.map((row) => { let effectiveUsedFraction = 0; let usedPercentage = 0; let statusColor = theme.ui.comment; let percentageText = ''; if (row.bucket && row.bucket.remainingFraction != null) { const actualUsedFraction = 1 - row.bucket.remainingFraction; effectiveUsedFraction = actualUsedFraction === 0 && row.isActive ? 0.001 : actualUsedFraction; usedPercentage = effectiveUsedFraction * 100; statusColor = getUsedStatusColor(usedPercentage, { warning: QUOTA_USED_WARNING_THRESHOLD, critical: QUOTA_USED_CRITICAL_THRESHOLD, }) ?? (row.isActive ? theme.text.primary : theme.ui.comment); percentageText = usedPercentage > 0 && usedPercentage < 1 ? `${usedPercentage.toFixed(1)}%` : `${usedPercentage.toFixed(0)}%`; } return ( {row.modelName} {row.requests} {!showQuotaColumn && ( <> {row.inputTokens} {row.cachedTokens} {row.outputTokens} )} {showQuotaColumn && ( <> {row.bucket && row.bucket.remainingFraction != null && ( {renderProgressBar( effectiveUsedFraction, statusColor, progressBarWidth, )} )} {row.bucket && row.bucket.remainingFraction != null && ( {row.bucket.remainingFraction === 0 ? ( Limit ) : ( {percentageText} )} )} {row.bucket?.resetTime && formatResetTime(row.bucket.resetTime, 'column') ? formatResetTime(row.bucket.resetTime, 'column') : ''} )} ); })} {cacheEfficiency > 0 && !showQuotaColumn && ( Savings Highlight:{' '} {totalCachedTokens.toLocaleString()} ( {cacheEfficiency.toFixed(1)}% ) of input tokens were served from the cache, reducing costs. )} ); }; interface StatsDisplayProps { duration: string; title?: string; quotas?: RetrieveUserQuotaResponse; footer?: string; selectedAuthType?: string; userEmail?: string; tier?: string; currentModel?: string; quotaStats?: QuotaStats; creditBalance?: number; } export const StatsDisplay: React.FC = ({ duration, title, quotas, footer, selectedAuthType, userEmail, tier, currentModel, quotaStats, creditBalance, }) => { const { stats } = useSessionStats(); const { metrics } = stats; const { models, tools, files } = metrics; const computed = computeSessionStats(metrics); const settings = useSettings(); const config = useConfig(); const useGemini3_1 = config.getGemini31LaunchedSync?.() ?? false; const useCustomToolModel = useGemini3_1 && config.getContentGeneratorConfig().authType === AuthType.USE_GEMINI; const pooledRemaining = quotaStats?.remaining; const pooledLimit = quotaStats?.limit; const pooledResetTime = quotaStats?.resetTime; const showUserIdentity = settings.merged.ui.showUserIdentity; const successThresholds = { green: TOOL_SUCCESS_RATE_HIGH, yellow: TOOL_SUCCESS_RATE_MEDIUM, }; const agreementThresholds = { green: USER_AGREEMENT_RATE_HIGH, yellow: USER_AGREEMENT_RATE_MEDIUM, }; const successColor = getStatusColor(computed.successRate, successThresholds); const agreementColor = getStatusColor( computed.agreementRate, agreementThresholds, ); const renderTitle = () => { if (title) { return {title}; } return ( Session Stats ); }; const renderFooter = () => { if (!footer) { return null; } return {footer}; }; return ( {renderTitle()}
{stats.sessionId} {showUserIdentity && selectedAuthType && ( {selectedAuthType.startsWith('oauth') ? userEmail ? `Signed in with Google (${userEmail})` : 'Signed in with Google' : selectedAuthType} )} {showUserIdentity && tier && ( {tier} )} {showUserIdentity && creditBalance != null && creditBalance >= 0 && ( 0 ? theme.text.primary : theme.text.secondary } > {creditBalance.toLocaleString()} )} {tools.totalCalls} ({' '} ✓ {tools.totalSuccess}{' '} x {tools.totalFail} ) {computed.successRate.toFixed(1)}% {computed.totalDecisions > 0 && ( {computed.agreementRate.toFixed(1)}%{' '} ({computed.totalDecisions} reviewed) )} {files && (files.totalLinesAdded > 0 || files.totalLinesRemoved > 0) && ( +{files.totalLinesAdded} {' '} -{files.totalLinesRemoved} )}
{duration} {formatDuration(computed.agentActiveTime)} {formatDuration(computed.totalApiTime)}{' '} ({computed.apiTimePercent.toFixed(1)}%) {formatDuration(computed.totalToolTime)}{' '} ({computed.toolTimePercent.toFixed(1)}%)
{renderFooter()}
); };