diff options
| author | Abhi <[email protected]> | 2025-06-29 20:44:33 -0400 |
|---|---|---|
| committer | GitHub <[email protected]> | 2025-06-30 00:44:33 +0000 |
| commit | 770f862832dfef477705bee69bd2a84397d105a8 (patch) | |
| tree | 8cb647cf789f05458ff491b461aa531a6932ad3d /packages/cli/src/ui/components/StatsDisplay.tsx | |
| parent | 0fd602eb43eea7abca980dc2ae3fd7bf2ba76a2a (diff) | |
feat: Change /stats to include more detailed breakdowns (#2615)
Diffstat (limited to 'packages/cli/src/ui/components/StatsDisplay.tsx')
| -rw-r--r-- | packages/cli/src/ui/components/StatsDisplay.tsx | 260 |
1 files changed, 200 insertions, 60 deletions
diff --git a/packages/cli/src/ui/components/StatsDisplay.tsx b/packages/cli/src/ui/components/StatsDisplay.tsx index 76d48821..249fc106 100644 --- a/packages/cli/src/ui/components/StatsDisplay.tsx +++ b/packages/cli/src/ui/components/StatsDisplay.tsx @@ -8,90 +8,230 @@ import React from 'react'; import { Box, Text } from 'ink'; import { Colors } from '../colors.js'; import { formatDuration } from '../utils/formatters.js'; -import { CumulativeStats } from '../contexts/SessionContext.js'; -import { FormattedStats, StatRow, StatsColumn } from './Stats.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'; -// --- Constants --- +// 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<StatRowProps> = ({ title, children }) => ( + <Box> + {/* Fixed width for the label creates a clean "gutter" for alignment */} + <Box width={28}> + <Text color={Colors.LightBlue}>{title}</Text> + </Box> + {children} + </Box> +); + +// A SubStatRow for indented, secondary information +interface SubStatRowProps { + title: string; + children: React.ReactNode; +} + +const SubStatRow: React.FC<SubStatRowProps> = ({ title, children }) => ( + <Box paddingLeft={2}> + {/* Adjust width for the "» " prefix */} + <Box width={26}> + <Text>» {title}</Text> + </Box> + {children} + </Box> +); + +// A Section component to group related stats +interface SectionProps { + title: string; + children: React.ReactNode; +} + +const Section: React.FC<SectionProps> = ({ title, children }) => ( + <Box flexDirection="column" width="100%" marginBottom={1}> + <Text bold>{title}</Text> + {children} + </Box> +); -const COLUMN_WIDTH = '48%'; +const ModelUsageTable: React.FC<{ + models: Record<string, ModelMetrics>; + totalCachedTokens: number; + cacheEfficiency: number; +}> = ({ models, totalCachedTokens, cacheEfficiency }) => { + const nameWidth = 25; + const requestsWidth = 8; + const inputTokensWidth = 15; + const outputTokensWidth = 15; -// --- Prop and Data Structures --- + return ( + <Box flexDirection="column" marginTop={1}> + {/* Header */} + <Box> + <Box width={nameWidth}> + <Text bold>Model Usage</Text> + </Box> + <Box width={requestsWidth} justifyContent="flex-end"> + <Text bold>Reqs</Text> + </Box> + <Box width={inputTokensWidth} justifyContent="flex-end"> + <Text bold>Input Tokens</Text> + </Box> + <Box width={outputTokensWidth} justifyContent="flex-end"> + <Text bold>Output Tokens</Text> + </Box> + </Box> + {/* Divider */} + <Box + borderStyle="round" + borderBottom={true} + borderTop={false} + borderLeft={false} + borderRight={false} + width={nameWidth + requestsWidth + inputTokensWidth + outputTokensWidth} + ></Box> + + {/* Rows */} + {Object.entries(models).map(([name, modelMetrics]) => ( + <Box key={name}> + <Box width={nameWidth}> + <Text>{name.replace('-001', '')}</Text> + </Box> + <Box width={requestsWidth} justifyContent="flex-end"> + <Text>{modelMetrics.api.totalRequests}</Text> + </Box> + <Box width={inputTokensWidth} justifyContent="flex-end"> + <Text color={Colors.AccentYellow}> + {modelMetrics.tokens.prompt.toLocaleString()} + </Text> + </Box> + <Box width={outputTokensWidth} justifyContent="flex-end"> + <Text color={Colors.AccentYellow}> + {modelMetrics.tokens.candidates.toLocaleString()} + </Text> + </Box> + </Box> + ))} + {cacheEfficiency > 0 && ( + <Box flexDirection="column" marginTop={1}> + <Text> + <Text color={Colors.AccentGreen}>Savings Highlight:</Text>{' '} + {totalCachedTokens.toLocaleString()} ({cacheEfficiency.toFixed(1)} + %) of input tokens were served from the cache, reducing costs. + </Text> + <Box height={1} /> + <Text color={Colors.Gray}> + » Tip: For a full token breakdown, run `/stats model`. + </Text> + </Box> + )} + </Box> + ); +}; interface StatsDisplayProps { - stats: CumulativeStats; - lastTurnStats: CumulativeStats; duration: string; } -// --- Main Component --- +export const StatsDisplay: React.FC<StatsDisplayProps> = ({ duration }) => { + const { stats } = useSessionStats(); + const { metrics } = stats; + const { models, tools } = metrics; + const computed = computeSessionStats(metrics); -export const StatsDisplay: React.FC<StatsDisplayProps> = ({ - stats, - lastTurnStats, - duration, -}) => { - const lastTurnFormatted: FormattedStats = { - inputTokens: lastTurnStats.promptTokenCount, - outputTokens: lastTurnStats.candidatesTokenCount, - toolUseTokens: lastTurnStats.toolUsePromptTokenCount, - thoughtsTokens: lastTurnStats.thoughtsTokenCount, - cachedTokens: lastTurnStats.cachedContentTokenCount, - totalTokens: lastTurnStats.totalTokenCount, + const successThresholds = { + green: TOOL_SUCCESS_RATE_HIGH, + yellow: TOOL_SUCCESS_RATE_MEDIUM, }; - - const cumulativeFormatted: FormattedStats = { - inputTokens: stats.promptTokenCount, - outputTokens: stats.candidatesTokenCount, - toolUseTokens: stats.toolUsePromptTokenCount, - thoughtsTokens: stats.thoughtsTokenCount, - cachedTokens: stats.cachedContentTokenCount, - totalTokens: stats.totalTokenCount, + const agreementThresholds = { + green: USER_AGREEMENT_RATE_HIGH, + yellow: USER_AGREEMENT_RATE_MEDIUM, }; + const successColor = getStatusColor(computed.successRate, successThresholds); + const agreementColor = getStatusColor( + computed.agreementRate, + agreementThresholds, + ); return ( <Box borderStyle="round" - borderColor="gray" + borderColor={Colors.Gray} flexDirection="column" paddingY={1} paddingX={2} > <Text bold color={Colors.AccentPurple}> - Stats + Session Stats </Text> + <Box height={1} /> - <Box flexDirection="row" justifyContent="space-between" marginTop={1}> - <StatsColumn - title="Last Turn" - stats={lastTurnFormatted} - width={COLUMN_WIDTH} - /> - <StatsColumn - title={`Cumulative (${stats.turnCount} Turns)`} - stats={cumulativeFormatted} - isCumulative={true} - width={COLUMN_WIDTH} - /> - </Box> + {tools.totalCalls > 0 && ( + <Section title="Interaction Summary"> + <StatRow title="Tool Calls:"> + <Text> + {tools.totalCalls} ({' '} + <Text color={Colors.AccentGreen}>✔ {tools.totalSuccess}</Text>{' '} + <Text color={Colors.AccentRed}>✖ {tools.totalFail}</Text> ) + </Text> + </StatRow> + <StatRow title="Success Rate:"> + <Text color={successColor}>{computed.successRate.toFixed(1)}%</Text> + </StatRow> + {computed.totalDecisions > 0 && ( + <StatRow title="User Agreement:"> + <Text color={agreementColor}> + {computed.agreementRate.toFixed(1)}%{' '} + <Text color={Colors.Gray}> + ({computed.totalDecisions} reviewed) + </Text> + </Text> + </StatRow> + )} + </Section> + )} - <Box flexDirection="row" justifyContent="space-between" marginTop={1}> - {/* Left column for "Last Turn" duration */} - <Box width={COLUMN_WIDTH} flexDirection="column"> - <StatRow - label="Turn Duration (API)" - value={formatDuration(lastTurnStats.apiTimeMs)} - /> - </Box> + <Section title="Performance"> + <StatRow title="Wall Time:"> + <Text>{duration}</Text> + </StatRow> + <StatRow title="Agent Active:"> + <Text>{formatDuration(computed.agentActiveTime)}</Text> + </StatRow> + <SubStatRow title="API Time:"> + <Text> + {formatDuration(computed.totalApiTime)}{' '} + <Text color={Colors.Gray}> + ({computed.apiTimePercent.toFixed(1)}%) + </Text> + </Text> + </SubStatRow> + <SubStatRow title="Tool Time:"> + <Text> + {formatDuration(computed.totalToolTime)}{' '} + <Text color={Colors.Gray}> + ({computed.toolTimePercent.toFixed(1)}%) + </Text> + </Text> + </SubStatRow> + </Section> - {/* Right column for "Cumulative" durations */} - <Box width={COLUMN_WIDTH} flexDirection="column"> - <StatRow - label="Total duration (API)" - value={formatDuration(stats.apiTimeMs)} - /> - <StatRow label="Total duration (wall)" value={duration} /> - </Box> - </Box> + {Object.keys(models).length > 0 && ( + <ModelUsageTable + models={models} + totalCachedTokens={computed.totalCachedTokens} + cacheEfficiency={computed.cacheEfficiency} + /> + )} </Box> ); }; |
