/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import React from 'react'; import { Box, Text } from 'ink'; import Gradient from 'ink-gradient'; import { Colors } from '../colors.js'; import { formatDuration } from '../utils/formatters.js'; import { useSessionStats, ModelMetrics } 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'; // 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 ModelUsageTable: React.FC<{ models: Record; totalCachedTokens: number; cacheEfficiency: number; }> = ({ models, totalCachedTokens, cacheEfficiency }) => { const nameWidth = 25; const requestsWidth = 8; const inputTokensWidth = 15; const outputTokensWidth = 15; return ( {/* Header */} Model Usage Reqs Input Tokens Output Tokens {/* Divider */} {/* Rows */} {Object.entries(models).map(([name, modelMetrics]) => ( {name.replace('-001', '')} {modelMetrics.api.totalRequests} {modelMetrics.tokens.prompt.toLocaleString()} {modelMetrics.tokens.candidates.toLocaleString()} ))} {cacheEfficiency > 0 && ( Savings Highlight:{' '} {totalCachedTokens.toLocaleString()} ({cacheEfficiency.toFixed(1)} %) of input tokens were served from the cache, reducing costs. » Tip: For a full token breakdown, run `/stats model`. )} ); }; interface StatsDisplayProps { duration: string; title?: string; } export const StatsDisplay: React.FC = ({ duration, title, }) => { const { stats } = useSessionStats(); const { metrics } = stats; const { models, tools } = 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 Colors.GradientColors && Colors.GradientColors.length > 0 ? ( {title} ) : ( {title} ); } return ( Session Stats ); }; return ( {renderTitle()}
{stats.sessionId} {tools.totalCalls} ({' '} ✔ {tools.totalSuccess}{' '} ✖ {tools.totalFail} ) {computed.successRate.toFixed(1)}% {computed.totalDecisions > 0 && ( {computed.agreementRate.toFixed(1)}%{' '} ({computed.totalDecisions} reviewed) )}
{duration} {formatDuration(computed.agentActiveTime)} {formatDuration(computed.totalApiTime)}{' '} ({computed.apiTimePercent.toFixed(1)}%) {formatDuration(computed.totalToolTime)}{' '} ({computed.toolTimePercent.toFixed(1)}%)
{Object.keys(models).length > 0 && ( )}
); };