diff options
Diffstat (limited to 'packages/cli/src')
9 files changed, 123 insertions, 424 deletions
diff --git a/packages/cli/src/ui/components/SessionSummaryDisplay.test.tsx b/packages/cli/src/ui/components/SessionSummaryDisplay.test.tsx index afb822e5..f3c0764e 100644 --- a/packages/cli/src/ui/components/SessionSummaryDisplay.test.tsx +++ b/packages/cli/src/ui/components/SessionSummaryDisplay.test.tsx @@ -33,7 +33,7 @@ const renderWithMockedStats = (metrics: SessionMetrics) => { }; describe('<SessionSummaryDisplay />', () => { - it('correctly sums and displays stats from multiple models', () => { + it('renders the summary display with a title', () => { const metrics: SessionMetrics = { models: { 'gemini-2.5-pro': { @@ -47,17 +47,6 @@ describe('<SessionSummaryDisplay />', () => { tool: 200, }, }, - 'gemini-2.5-flash': { - api: { totalRequests: 5, totalErrors: 0, totalLatencyMs: 12345 }, - tokens: { - prompt: 500, - candidates: 1000, - total: 1500, - cached: 100, - thoughts: 50, - tool: 20, - }, - }, }, tools: { totalCalls: 0, @@ -72,25 +61,7 @@ describe('<SessionSummaryDisplay />', () => { const { lastFrame } = renderWithMockedStats(metrics); const output = lastFrame(); - // Verify totals are summed correctly - expect(output).toContain('Cumulative Stats (15 API calls)'); + expect(output).toContain('Agent powering down. Goodbye!'); expect(output).toMatchSnapshot(); }); - - it('renders zero state correctly', () => { - const zeroMetrics: SessionMetrics = { - models: {}, - tools: { - totalCalls: 0, - totalSuccess: 0, - totalFail: 0, - totalDurationMs: 0, - totalDecisions: { accept: 0, reject: 0, modify: 0 }, - byName: {}, - }, - }; - - const { lastFrame } = renderWithMockedStats(zeroMetrics); - expect(lastFrame()).toMatchSnapshot(); - }); }); diff --git a/packages/cli/src/ui/components/SessionSummaryDisplay.tsx b/packages/cli/src/ui/components/SessionSummaryDisplay.tsx index a009f3d8..34e3cc72 100644 --- a/packages/cli/src/ui/components/SessionSummaryDisplay.tsx +++ b/packages/cli/src/ui/components/SessionSummaryDisplay.tsx @@ -5,101 +5,14 @@ */ 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 } from '../contexts/SessionContext.js'; -import { computeSessionStats } from '../utils/computeStats.js'; -import { FormattedStats, StatRow, StatsColumn } from './Stats.js'; - -// --- Prop and Data Structures --- +import { StatsDisplay } from './StatsDisplay.js'; interface SessionSummaryDisplayProps { duration: string; } -// --- Main Component --- - export const SessionSummaryDisplay: React.FC<SessionSummaryDisplayProps> = ({ duration, -}) => { - const { stats } = useSessionStats(); - const { metrics } = stats; - const computed = computeSessionStats(metrics); - - const cumulativeFormatted: FormattedStats = { - inputTokens: Object.values(metrics.models).reduce( - (acc, model) => acc + model.tokens.prompt, - 0, - ), - outputTokens: Object.values(metrics.models).reduce( - (acc, model) => acc + model.tokens.candidates, - 0, - ), - toolUseTokens: Object.values(metrics.models).reduce( - (acc, model) => acc + model.tokens.tool, - 0, - ), - thoughtsTokens: Object.values(metrics.models).reduce( - (acc, model) => acc + model.tokens.thoughts, - 0, - ), - cachedTokens: Object.values(metrics.models).reduce( - (acc, model) => acc + model.tokens.cached, - 0, - ), - totalTokens: Object.values(metrics.models).reduce( - (acc, model) => acc + model.tokens.total, - 0, - ), - }; - - const totalRequests = Object.values(metrics.models).reduce( - (acc, model) => acc + model.api.totalRequests, - 0, - ); - - const title = 'Agent powering down. Goodbye!'; - - return ( - <Box - borderStyle="round" - borderColor="gray" - flexDirection="column" - paddingY={1} - paddingX={2} - alignSelf="flex-start" - > - <Box marginBottom={1} flexDirection="column"> - {Colors.GradientColors ? ( - <Gradient colors={Colors.GradientColors}> - <Text bold>{title}</Text> - </Gradient> - ) : ( - <Text bold>{title}</Text> - )} - </Box> - - <Box marginTop={1}> - <StatsColumn - title={`Cumulative Stats (${totalRequests} API calls)`} - stats={cumulativeFormatted} - isCumulative={true} - > - <Box marginTop={1} flexDirection="column"> - <StatRow - label="Total duration (API)" - value={formatDuration(computed.totalApiTime)} - /> - <StatRow - label="Total duration (Tools)" - value={formatDuration(computed.totalToolTime)} - /> - <StatRow label="Total duration (wall)" value={duration} /> - </Box> - </StatsColumn> - </Box> - </Box> - ); -}; +}) => ( + <StatsDisplay title="Agent powering down. Goodbye!" duration={duration} /> +); diff --git a/packages/cli/src/ui/components/Stats.test.tsx b/packages/cli/src/ui/components/Stats.test.tsx deleted file mode 100644 index 27c7d64e..00000000 --- a/packages/cli/src/ui/components/Stats.test.tsx +++ /dev/null @@ -1,89 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import { render } from 'ink-testing-library'; -import { describe, it, expect } from 'vitest'; -import { - StatRow, - StatsColumn, - DurationColumn, - FormattedStats, -} from './Stats.js'; -import { Colors } from '../colors.js'; - -describe('<StatRow />', () => { - it('renders a label and value', () => { - const { lastFrame } = render( - <StatRow label="Test Label" value="Test Value" />, - ); - expect(lastFrame()).toMatchSnapshot(); - }); - - it('renders with a specific value color', () => { - const { lastFrame } = render( - <StatRow - label="Test Label" - value="Test Value" - valueColor={Colors.AccentGreen} - />, - ); - expect(lastFrame()).toMatchSnapshot(); - }); -}); - -describe('<StatsColumn />', () => { - const mockStats: FormattedStats = { - inputTokens: 100, - outputTokens: 200, - toolUseTokens: 50, - thoughtsTokens: 25, - cachedTokens: 10, - totalTokens: 385, - }; - - it('renders a stats column with children', () => { - const { lastFrame } = render( - <StatsColumn title="Test Stats" stats={mockStats}> - <StatRow label="Child Prop" value="Child Value" /> - </StatsColumn>, - ); - expect(lastFrame()).toMatchSnapshot(); - }); - - it('renders a stats column with a specific width', () => { - const { lastFrame } = render( - <StatsColumn title="Test Stats" stats={mockStats} width="50%" />, - ); - expect(lastFrame()).toMatchSnapshot(); - }); - - it('renders a cumulative stats column with percentages', () => { - const { lastFrame } = render( - <StatsColumn title="Cumulative Stats" stats={mockStats} isCumulative />, - ); - expect(lastFrame()).toMatchSnapshot(); - }); - - it('hides the tool use row when there are no tool use tokens', () => { - const statsWithNoToolUse: FormattedStats = { - ...mockStats, - toolUseTokens: 0, - }; - const { lastFrame } = render( - <StatsColumn title="Test Stats" stats={statsWithNoToolUse} />, - ); - expect(lastFrame()).not.toContain('Tool Use Tokens'); - }); -}); - -describe('<DurationColumn />', () => { - it('renders a duration column', () => { - const { lastFrame } = render( - <DurationColumn apiTime="5s" wallTime="10s" />, - ); - expect(lastFrame()).toMatchSnapshot(); - }); -}); diff --git a/packages/cli/src/ui/components/Stats.tsx b/packages/cli/src/ui/components/Stats.tsx deleted file mode 100644 index d620416e..00000000 --- a/packages/cli/src/ui/components/Stats.tsx +++ /dev/null @@ -1,118 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import React from 'react'; -import { Box, Text } from 'ink'; -import { Colors } from '../colors.js'; - -// --- Prop and Data Structures --- - -export interface FormattedStats { - inputTokens: number; - outputTokens: number; - toolUseTokens: number; - thoughtsTokens: number; - cachedTokens: number; - totalTokens: number; -} - -// --- Helper Components --- - -/** - * Renders a single row with a colored label on the left and a value on the right. - */ -export const StatRow: React.FC<{ - label: string; - value: string | number; - valueColor?: string; -}> = ({ label, value, valueColor }) => ( - <Box justifyContent="space-between" gap={2}> - <Text color={Colors.LightBlue}>{label}</Text> - <Text color={valueColor}>{value}</Text> - </Box> -); - -/** - * Renders a full column for either "Last Turn" or "Cumulative" stats. - */ -export const StatsColumn: React.FC<{ - title: string; - stats: FormattedStats; - isCumulative?: boolean; - width?: string | number; - children?: React.ReactNode; -}> = ({ title, stats, isCumulative = false, width, children }) => { - const cachedDisplay = - isCumulative && stats.totalTokens > 0 - ? `${stats.cachedTokens.toLocaleString()} (${((stats.cachedTokens / stats.totalTokens) * 100).toFixed(1)}%)` - : stats.cachedTokens.toLocaleString(); - - const cachedColor = - isCumulative && stats.cachedTokens > 0 ? Colors.AccentGreen : undefined; - - return ( - <Box flexDirection="column" width={width}> - <Text bold>{title}</Text> - <Box marginTop={1} flexDirection="column"> - {/* All StatRows below will now inherit the gap */} - <StatRow - label="Input Tokens" - value={stats.inputTokens.toLocaleString()} - /> - <StatRow - label="Output Tokens" - value={stats.outputTokens.toLocaleString()} - /> - {stats.toolUseTokens > 0 && ( - <StatRow - label="Tool Use Tokens" - value={stats.toolUseTokens.toLocaleString()} - /> - )} - <StatRow - label="Thoughts Tokens" - value={stats.thoughtsTokens.toLocaleString()} - /> - {stats.cachedTokens > 0 && ( - <StatRow - label="Cached Tokens" - value={cachedDisplay} - valueColor={cachedColor} - /> - )} - {/* Divider Line */} - <Box - borderTop={true} - borderLeft={false} - borderRight={false} - borderBottom={false} - borderStyle="single" - /> - <StatRow - label="Total Tokens" - value={stats.totalTokens.toLocaleString()} - /> - {children} - </Box> - </Box> - ); -}; - -/** - * Renders a column for displaying duration information. - */ -export const DurationColumn: React.FC<{ - apiTime: string; - wallTime: string; -}> = ({ apiTime, wallTime }) => ( - <Box flexDirection="column" width={'48%'}> - <Text bold>Duration</Text> - <Box marginTop={1} flexDirection="column"> - <StatRow label="API Time" value={apiTime} /> - <StatRow label="Wall Time" value={wallTime} /> - </Box> - </Box> -); diff --git a/packages/cli/src/ui/components/StatsDisplay.test.tsx b/packages/cli/src/ui/components/StatsDisplay.test.tsx index 29f322f4..a62815d9 100644 --- a/packages/cli/src/ui/components/StatsDisplay.test.tsx +++ b/packages/cli/src/ui/components/StatsDisplay.test.tsx @@ -260,4 +260,44 @@ describe('<StatsDisplay />', () => { expect(lastFrame()).toMatchSnapshot(); }); }); + + describe('Title Rendering', () => { + const zeroMetrics: SessionMetrics = { + models: {}, + tools: { + totalCalls: 0, + totalSuccess: 0, + totalFail: 0, + totalDurationMs: 0, + totalDecisions: { accept: 0, reject: 0, modify: 0 }, + byName: {}, + }, + }; + + it('renders the default title when no title prop is provided', () => { + const { lastFrame } = renderWithMockedStats(zeroMetrics); + const output = lastFrame(); + expect(output).toContain('Session Stats'); + expect(output).not.toContain('Agent powering down'); + expect(output).toMatchSnapshot(); + }); + + it('renders the custom title when a title prop is provided', () => { + useSessionStatsMock.mockReturnValue({ + stats: { + sessionStartTime: new Date(), + metrics: zeroMetrics, + lastPromptTokenCount: 0, + }, + }); + + const { lastFrame } = render( + <StatsDisplay duration="1s" title="Agent powering down. Goodbye!" />, + ); + const output = lastFrame(); + expect(output).toContain('Agent powering down. Goodbye!'); + expect(output).not.toContain('Session Stats'); + expect(output).toMatchSnapshot(); + }); + }); }); diff --git a/packages/cli/src/ui/components/StatsDisplay.tsx b/packages/cli/src/ui/components/StatsDisplay.tsx index 249fc106..014026ff 100644 --- a/packages/cli/src/ui/components/StatsDisplay.tsx +++ b/packages/cli/src/ui/components/StatsDisplay.tsx @@ -6,6 +6,7 @@ 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'; @@ -140,9 +141,13 @@ const ModelUsageTable: React.FC<{ interface StatsDisplayProps { duration: string; + title?: string; } -export const StatsDisplay: React.FC<StatsDisplayProps> = ({ duration }) => { +export const StatsDisplay: React.FC<StatsDisplayProps> = ({ + duration, + title, +}) => { const { stats } = useSessionStats(); const { metrics } = stats; const { models, tools } = metrics; @@ -162,6 +167,25 @@ export const StatsDisplay: React.FC<StatsDisplayProps> = ({ duration }) => { agreementThresholds, ); + const renderTitle = () => { + if (title) { + return Colors.GradientColors && Colors.GradientColors.length > 0 ? ( + <Gradient colors={Colors.GradientColors}> + <Text bold>{title}</Text> + </Gradient> + ) : ( + <Text bold color={Colors.AccentPurple}> + {title} + </Text> + ); + } + return ( + <Text bold color={Colors.AccentPurple}> + Session Stats + </Text> + ); + }; + return ( <Box borderStyle="round" @@ -170,9 +194,7 @@ export const StatsDisplay: React.FC<StatsDisplayProps> = ({ duration }) => { paddingY={1} paddingX={2} > - <Text bold color={Colors.AccentPurple}> - Session Stats - </Text> + {renderTitle()} <Box height={1} /> {tools.totalCalls > 0 && ( diff --git a/packages/cli/src/ui/components/__snapshots__/SessionSummaryDisplay.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/SessionSummaryDisplay.test.tsx.snap index 06dc2116..c9b2bd64 100644 --- a/packages/cli/src/ui/components/__snapshots__/SessionSummaryDisplay.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/SessionSummaryDisplay.test.tsx.snap @@ -1,45 +1,24 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`<SessionSummaryDisplay /> > correctly sums and displays stats from multiple models 1`] = ` -"╭─────────────────────────────────────╮ -│ │ -│ Agent powering down. Goodbye! │ -│ │ -│ │ -│ Cumulative Stats (15 API calls) │ -│ │ -│ Input Tokens 1,500 │ -│ Output Tokens 3,000 │ -│ Tool Use Tokens 220 │ -│ Thoughts Tokens 350 │ -│ Cached Tokens 600 (12.0%) │ -│ ───────────────────────────────── │ -│ Total Tokens 5,000 │ -│ │ -│ Total duration (API) 1m 2s │ -│ Total duration (Tools) 0s │ -│ Total duration (wall) 1h 23m 45s │ -│ │ -╰─────────────────────────────────────╯" -`; - -exports[`<SessionSummaryDisplay /> > renders zero state correctly 1`] = ` -"╭─────────────────────────────────────╮ -│ │ -│ Agent powering down. Goodbye! │ -│ │ -│ │ -│ Cumulative Stats (0 API calls) │ -│ │ -│ Input Tokens 0 │ -│ Output Tokens 0 │ -│ Thoughts Tokens 0 │ -│ ───────────────────────────────── │ -│ Total Tokens 0 │ -│ │ -│ Total duration (API) 0s │ -│ Total duration (Tools) 0s │ -│ Total duration (wall) 1h 23m 45s │ -│ │ -╰─────────────────────────────────────╯" +exports[`<SessionSummaryDisplay /> > renders the summary display with a title 1`] = ` +"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ Agent powering down. Goodbye! │ +│ │ +│ Performance │ +│ Wall Time: 1h 23m 45s │ +│ Agent Active: 50.2s │ +│ » API Time: 50.2s (100.0%) │ +│ » Tool Time: 0s (0.0%) │ +│ │ +│ │ +│ Model Usage Reqs Input Tokens Output Tokens │ +│ ─────────────────────────────────────────────────────────────── │ +│ gemini-2.5-pro 10 1,000 2,000 │ +│ │ +│ Savings Highlight: 500 (50.0%) of input tokens were served from the cache, reducing costs. │ +│ │ +│ » Tip: For a full token breakdown, run \`/stats model\`. │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" `; diff --git a/packages/cli/src/ui/components/__snapshots__/Stats.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/Stats.test.tsx.snap deleted file mode 100644 index 9b003891..00000000 --- a/packages/cli/src/ui/components/__snapshots__/Stats.test.tsx.snap +++ /dev/null @@ -1,49 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`<DurationColumn /> > renders a duration column 1`] = ` -"Duration - -API Time 5s -Wall Time 10s" -`; - -exports[`<StatRow /> > renders a label and value 1`] = `"Test Label Test Value"`; - -exports[`<StatRow /> > renders with a specific value color 1`] = `"Test Label Test Value"`; - -exports[`<StatsColumn /> > renders a cumulative stats column with percentages 1`] = ` -"Cumulative Stats - -Input Tokens 100 -Output Tokens 200 -Tool Use Tokens 50 -Thoughts Tokens 25 -Cached Tokens 10 (2.6%) -──────────────────────────────────────────────────────────────────────────────────────────────────── -Total Tokens 385" -`; - -exports[`<StatsColumn /> > renders a stats column with a specific width 1`] = ` -"Test Stats - -Input Tokens 100 -Output Tokens 200 -Tool Use Tokens 50 -Thoughts Tokens 25 -Cached Tokens 10 -────────────────────────────────────────────────── -Total Tokens 385" -`; - -exports[`<StatsColumn /> > renders a stats column with children 1`] = ` -"Test Stats - -Input Tokens 100 -Output Tokens 200 -Tool Use Tokens 50 -Thoughts Tokens 25 -Cached Tokens 10 -──────────────────────────────────────────────────────────────────────────────────────────────────── -Total Tokens 385 -Child Prop Child Value" -`; diff --git a/packages/cli/src/ui/components/__snapshots__/StatsDisplay.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/StatsDisplay.test.tsx.snap index 6fc2565e..c7c2ec59 100644 --- a/packages/cli/src/ui/components/__snapshots__/StatsDisplay.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/StatsDisplay.test.tsx.snap @@ -95,6 +95,36 @@ exports[`<StatsDisplay /> > Conditional Rendering Tests > hides User Agreement w ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" `; +exports[`<StatsDisplay /> > Title Rendering > renders the custom title when a title prop is provided 1`] = ` +"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ Agent powering down. Goodbye! │ +│ │ +│ Performance │ +│ Wall Time: 1s │ +│ Agent Active: 0s │ +│ » API Time: 0s (0.0%) │ +│ » Tool Time: 0s (0.0%) │ +│ │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + +exports[`<StatsDisplay /> > Title Rendering > renders the default title when no title prop is provided 1`] = ` +"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ Session Stats │ +│ │ +│ Performance │ +│ Wall Time: 1s │ +│ Agent Active: 0s │ +│ » API Time: 0s (0.0%) │ +│ » Tool Time: 0s (0.0%) │ +│ │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + exports[`<StatsDisplay /> > renders a table with two models correctly 1`] = ` "╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │ │ |
