/** * @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, CACHE_EFFICIENCY_HIGH, CACHE_EFFICIENCY_MEDIUM, } from '../utils/displayUtils.js'; import { computeSessionStats } from '../utils/computeStats.js'; import { getBorderStyle } from '../contexts/UnicodeRenderingContext.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} ); const getInputTokens = ( inputTokens: number | undefined, promptTokens: number, cachedTokens: number, ) => inputTokens ?? Math.max(0, promptTokens - cachedTokens); const hasCodeChanges = ( files: | { totalLinesAdded: number; totalLinesRemoved: number; } | undefined, ) => files !== undefined && (files.totalLinesAdded > 0 || files.totalLinesRemoved > 0); // Logic for building the unified list of table rows const buildModelRows = (models: Record) => { const getBaseModelName = (name: string) => name.replace('-001', ''); // Models with active usage const activeRows = Object.entries(models).map( ([name, metrics]: [string, ModelMetrics]) => { const modelName = getBaseModelName(name); const cachedTokens = metrics.tokens.cached; const promptTokens = metrics.tokens.prompt; // Use input if available, otherwise compute from prompt - cached const inputTokens = getInputTokens( metrics.tokens.input, promptTokens, cachedTokens, ); const avgLatency = metrics.api.totalRequests > 0 ? metrics.api.totalLatencyMs / metrics.api.totalRequests : 0; return { key: name, modelName, requests: metrics.api.totalRequests, errors: metrics.api.totalErrors, cachedTokens: cachedTokens.toLocaleString(), inputTokens: inputTokens.toLocaleString(), outputTokens: metrics.tokens.candidates.toLocaleString(), totalLatency: formatDuration(metrics.api.totalLatencyMs), avgLatency: formatDuration(avgLatency), }; }, ); return activeRows; }; const MODEL_TABLE_WIDTHS = { name: 25, requests: 7, uncached: 15, cached: 14, output: 15, } as const; const ModelTableHeader: React.FC = () => ( Model Usage Reqs Input Tokens Cache Reads Output Tokens ); interface ModelRowData { key: string; modelName: string; requests: number; errors: number; cachedTokens: string; inputTokens: string; outputTokens: string; totalLatency: string; avgLatency: string; } const ModelTableRow: React.FC<{ row: ModelRowData }> = ({ row }) => ( {row.modelName} {row.requests} {row.inputTokens} {row.cachedTokens} {row.outputTokens} Latency: {row.avgLatency} avg / {row.totalLatency} total {row.errors > 0 ? ` (${row.errors} errors)` : ''} ); const CacheSavingsHighlight: React.FC<{ cacheEfficiency: number; totalCachedTokens: number; }> = ({ cacheEfficiency, totalCachedTokens }) => { if (cacheEfficiency <= 0) return null; const cacheEfficiencyColor = getStatusColor(cacheEfficiency, { green: CACHE_EFFICIENCY_HIGH, yellow: CACHE_EFFICIENCY_MEDIUM, }); return ( Savings Highlight:{' '} {totalCachedTokens.toLocaleString()} ( {cacheEfficiency.toFixed(1)}%) of input tokens were served from the cache, reducing costs. ); }; const ModelUsageTable: React.FC<{ models: Record; cacheEfficiency: number; totalCachedTokens: number; }> = ({ models, cacheEfficiency, totalCachedTokens }) => { const rows = buildModelRows(models); if (rows.length === 0) return null; const totalWidth = MODEL_TABLE_WIDTHS.name + MODEL_TABLE_WIDTHS.requests + MODEL_TABLE_WIDTHS.uncached + MODEL_TABLE_WIDTHS.cached + MODEL_TABLE_WIDTHS.output; return ( {rows.map((row) => ( ))} ); }; interface InteractionSummaryProps { sessionId: string; tools: { totalCalls: number; totalSuccess: number; totalFail: number; totalCancelled: number; totalDurationMs: number; }; files: { totalLinesAdded: number; totalLinesRemoved: number } | undefined; successRate: number; agreementRate: number; totalDecisions: number; } const InteractionSummary: React.FC = ({ sessionId, tools, files, successRate, agreementRate, totalDecisions, }) => { const successColor = getStatusColor(successRate, { green: TOOL_SUCCESS_RATE_HIGH, yellow: TOOL_SUCCESS_RATE_MEDIUM, }); const agreementColor = getStatusColor(agreementRate, { green: USER_AGREEMENT_RATE_HIGH, yellow: USER_AGREEMENT_RATE_MEDIUM, }); return (
{sessionId} {tools.totalCalls} ({' '} OK {tools.totalSuccess}{' '} ERR {tools.totalFail} {tools.totalCancelled > 0 && ( <> {' '} CNL {tools.totalCancelled} )} {' )'} {tools.totalDurationMs > 0 && ( {formatDuration(tools.totalDurationMs)} )} {successRate.toFixed(1)}% {totalDecisions > 0 && ( {agreementRate.toFixed(1)}%{' '} ({totalDecisions} reviewed) )} {hasCodeChanges(files) && ( +{files!.totalLinesAdded}{' '} -{files!.totalLinesRemoved} )}
); }; interface PerformanceSectionProps { duration: string; totalApiTime: number; apiTimePercent: number; totalToolTime: number; toolTimePercent: number; agentActiveTime: number; accumulatedWorkMs: number; tokensPerMinute: number; lastRequestTpm: number; timeToFirstToken: number | null; weightedAvgTtftMs: number | null; outputGenerationTps: number; lastOutputGenerationTps: number; effectiveInputTps: number; lastEffectiveInputTps: number; uncachedInputTps: number | null; } function formatThroughput(value: number): string { return value < 1000 ? `${value.toFixed(2)} TPM` : `${(value / 1000).toFixed(2)}k TPM`; } const ThroughputRows: React.FC<{ tokensPerMinute: number; lastRequestTpm: number; }> = ({ tokensPerMinute, lastRequestTpm }) => ( <> {Number.isFinite(tokensPerMinute) && tokensPerMinute > 0 && ( {formatThroughput(tokensPerMinute)} (session weighted) )} {Number.isFinite(lastRequestTpm) && lastRequestTpm > 0 && ( {formatThroughput(lastRequestTpm)} )} ); const LatencyRows: React.FC<{ timeToFirstToken: number | null; weightedAvgTtftMs: number | null; }> = ({ timeToFirstToken, weightedAvgTtftMs }) => { if (timeToFirstToken === null || !Number.isFinite(timeToFirstToken)) { return null; } return ( {timeToFirstToken.toFixed(0)}ms {weightedAvgTtftMs !== null && Number.isFinite(weightedAvgTtftMs) && ( {' '} (avg: {weightedAvgTtftMs.toFixed(0)}ms) )} ); }; const RateRows: React.FC<{ outputGenerationTps: number; lastOutputGenerationTps: number; effectiveInputTps: number; lastEffectiveInputTps: number; uncachedInputTps: number | null; }> = ({ outputGenerationTps, lastOutputGenerationTps, effectiveInputTps, lastEffectiveInputTps, uncachedInputTps, }) => ( <> {Number.isFinite(outputGenerationTps) && outputGenerationTps > 0 && ( {outputGenerationTps.toFixed(2)} tok/s (session weighted) {lastOutputGenerationTps > 0 && ( {' '} (last: {lastOutputGenerationTps.toFixed(2)}) )} )} {Number.isFinite(effectiveInputTps) && effectiveInputTps > 0 && ( {effectiveInputTps.toFixed(2)} tok/s (ΣP/ΣTTFT) {lastEffectiveInputTps > 0 && ( {' '} (last: {lastEffectiveInputTps.toFixed(2)}) )} )} {uncachedInputTps !== null && Number.isFinite(uncachedInputTps) && ( {uncachedInputTps.toFixed(2)} tok/s (Σmax(0,P-C)/ΣTTFT) )} ); const PerformanceSection: React.FC = ({ duration, totalApiTime, apiTimePercent, totalToolTime, toolTimePercent, agentActiveTime, accumulatedWorkMs, tokensPerMinute, lastRequestTpm, timeToFirstToken, weightedAvgTtftMs, outputGenerationTps, lastOutputGenerationTps, effectiveInputTps, lastEffectiveInputTps, uncachedInputTps, }) => (
{duration} {agentActiveTime > 0 && ( {formatDuration(agentActiveTime)} (interval union) )} {accumulatedWorkMs > 0 && ( {formatDuration(accumulatedWorkMs)} (API+Tool) )} {formatDuration(totalApiTime)}{' '} ({apiTimePercent.toFixed(1)}%) {formatDuration(totalToolTime)}{' '} ({toolTimePercent.toFixed(1)}%)
); const QuotaInfo: React.FC<{ quotaLines: string[] }> = ({ quotaLines }) => ( Quota Information {quotaLines.map((line, index) => ( {line} ))} ); interface StatsDisplayProps { duration: string; title?: string; quotaLines?: string[]; } const StatsTitle: React.FC<{ title?: string }> = ({ title }) => title ? ( {title} ) : ( Session Stats ); function sumModelApi( models: Record, field: 'totalRequests' | 'totalErrors' | 'totalLatencyMs', ): number { return Object.values(models).reduce((acc, m) => acc + m.api[field], 0); } const SessionApiSection: React.FC<{ totalRequests: number; totalErrors: number; avgLatency: number; }> = ({ totalRequests, totalErrors, avgLatency }) => { if (totalRequests <= 0) return null; return (
{totalRequests.toLocaleString()} {totalErrors.toLocaleString()} {formatDuration(avgLatency)}
); }; export const StatsDisplay: React.FC = ({ duration, title, quotaLines, }) => { const { stats } = useSessionStats(); const { metrics } = stats; const { models, tools, files } = metrics; const computed = computeSessionStats(metrics); const totalSessionRequests = sumModelApi(models, 'totalRequests'); const totalSessionErrors = sumModelApi(models, 'totalErrors'); const totalSessionLatency = sumModelApi(models, 'totalLatencyMs'); const avgLatency = totalSessionRequests > 0 ? totalSessionLatency / totalSessionRequests : 0; return ( {tools.totalCalls > 0 && ( )} {quotaLines && quotaLines.length > 0 && ( )} ); };