/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import type React from 'react'; import { Box, Text } from 'ink'; import { ThemedGradient } from './ThemedGradient.js'; import { theme } from '../semantic-colors.js'; import { formatDuration } from '../utils/formatters.js'; import type { ModelMetrics } from '../contexts/SessionContext.js'; import { useSessionStats } from '../contexts/SessionContext.js'; import { getStatusColor, TOOL_SUCCESS_RATE_HIGH, TOOL_SUCCESS_RATE_MEDIUM, USER_AGREEMENT_RATE_HIGH, USER_AGREEMENT_RATE_MEDIUM, } from '../utils/displayUtils.js'; import { computeSessionStats } from '../utils/computeStats.js'; import type { RetrieveUserQuotaResponse } from '@google/gemini-cli-core'; // 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} {/* FIX: Wrap children in a Box that can grow to fill remaining space */} {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} {/* FIX: Apply the same flexGrow fix here */} {children} ); // A Section component to group related stats interface SectionProps { title: string; children: React.ReactNode; } const Section: React.FC = ({ title, children }) => ( {title} {children} ); const formatResetTime = (resetTime: string): string => { const diff = new Date(resetTime).getTime() - Date.now(); if (diff <= 0) return ''; const totalMinutes = Math.ceil(diff / (1000 * 60)); const hours = Math.floor(totalMinutes / 60); const minutes = totalMinutes % 60; const fmt = (val: number, unit: 'hour' | 'minute') => new Intl.NumberFormat('en', { style: 'unit', unit, unitDisplay: 'narrow', }).format(val); if (hours > 0 && minutes > 0) { return `(Resets in ${fmt(hours, 'hour')} ${fmt(minutes, 'minute')})`; } else if (hours > 0) { return `(Resets in ${fmt(hours, 'hour')})`; } return `(Resets in ${fmt(minutes, 'minute')})`; }; const ModelUsageTable: React.FC<{ models: Record; totalCachedTokens: number; cacheEfficiency: number; quotas?: RetrieveUserQuotaResponse; }> = ({ models, totalCachedTokens, cacheEfficiency, quotas }) => { const nameWidth = 25; const requestsWidth = 8; const inputTokensWidth = 15; const outputTokensWidth = 15; const usageLimitWidth = quotas ? 30 : 0; return ( {/* Header */} Model Usage Reqs Input Tokens Output Tokens {quotas && ( Usage limit remaining )} {/* Divider */} {/* Rows */} {Object.entries(models).map(([name, modelMetrics]) => { const modelName = name.replace('-001', ''); const bucket = quotas?.buckets?.find((b) => b.modelId === modelName); return ( {modelName} {modelMetrics.api.totalRequests} {modelMetrics.tokens.prompt.toLocaleString()} {modelMetrics.tokens.candidates.toLocaleString()} {bucket && bucket.remainingFraction != null && bucket.resetTime && ( {(bucket.remainingFraction * 100).toFixed(1)}%{' '} {formatResetTime(bucket.resetTime)} )} ); })} {cacheEfficiency > 0 && ( Savings Highlight:{' '} {totalCachedTokens.toLocaleString()} ({cacheEfficiency.toFixed(1)} %) of input tokens were served from the cache, reducing costs. )} {models && ( <> {`Usage limits span all sessions and reset daily.\n/auth to upgrade or switch to API key.`} » Tip: For a full token breakdown, run `/stats model`. )} ); }; interface StatsDisplayProps { duration: string; title?: string; quotas?: RetrieveUserQuotaResponse; } export const StatsDisplay: React.FC = ({ duration, title, quotas, }) => { const { stats } = useSessionStats(); const { metrics } = stats; const { models, tools, files } = metrics; const computed = computeSessionStats(metrics); 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 ); }; return ( {renderTitle()}
{stats.sessionId} {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)}%)
{Object.keys(models).length > 0 && ( )}
); };