summaryrefslogtreecommitdiff
path: root/packages/cli/src/ui/components/StatsDisplay.tsx
diff options
context:
space:
mode:
authorAbhi <[email protected]>2025-06-29 20:44:33 -0400
committerGitHub <[email protected]>2025-06-30 00:44:33 +0000
commit770f862832dfef477705bee69bd2a84397d105a8 (patch)
tree8cb647cf789f05458ff491b461aa531a6932ad3d /packages/cli/src/ui/components/StatsDisplay.tsx
parent0fd602eb43eea7abca980dc2ae3fd7bf2ba76a2a (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.tsx260
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>
);
};