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('', () => { - 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('', () => { 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('', () => { 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 = ({ 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 ( - - - {Colors.GradientColors ? ( - - {title} - - ) : ( - {title} - )} - - - - - - - - - - - - - ); -}; +}) => ( + +); 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('', () => { - it('renders a label and value', () => { - const { lastFrame } = render( - , - ); - expect(lastFrame()).toMatchSnapshot(); - }); - - it('renders with a specific value color', () => { - const { lastFrame } = render( - , - ); - expect(lastFrame()).toMatchSnapshot(); - }); -}); - -describe('', () => { - 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( - - - , - ); - expect(lastFrame()).toMatchSnapshot(); - }); - - it('renders a stats column with a specific width', () => { - const { lastFrame } = render( - , - ); - expect(lastFrame()).toMatchSnapshot(); - }); - - it('renders a cumulative stats column with percentages', () => { - const { lastFrame } = render( - , - ); - 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( - , - ); - expect(lastFrame()).not.toContain('Tool Use Tokens'); - }); -}); - -describe('', () => { - it('renders a duration column', () => { - const { lastFrame } = render( - , - ); - 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 }) => ( - - {label} - {value} - -); - -/** - * 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 ( - - {title} - - {/* All StatRows below will now inherit the gap */} - - - {stats.toolUseTokens > 0 && ( - - )} - - {stats.cachedTokens > 0 && ( - - )} - {/* Divider Line */} - - - {children} - - - ); -}; - -/** - * Renders a column for displaying duration information. - */ -export const DurationColumn: React.FC<{ - apiTime: string; - wallTime: string; -}> = ({ apiTime, wallTime }) => ( - - Duration - - - - - -); 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('', () => { 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( + , + ); + 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 = ({ duration }) => { +export const StatsDisplay: React.FC = ({ + duration, + title, +}) => { const { stats } = useSessionStats(); const { metrics } = stats; const { models, tools } = metrics; @@ -162,6 +167,25 @@ export const StatsDisplay: React.FC = ({ duration }) => { agreementThresholds, ); + const renderTitle = () => { + if (title) { + return Colors.GradientColors && Colors.GradientColors.length > 0 ? ( + + {title} + + ) : ( + + {title} + + ); + } + return ( + + Session Stats + + ); + }; + return ( = ({ duration }) => { paddingY={1} paddingX={2} > - - Session Stats - + {renderTitle()} {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[` > 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[` > 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[` > 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[` > renders a duration column 1`] = ` -"Duration - -API Time 5s -Wall Time 10s" -`; - -exports[` > renders a label and value 1`] = `"Test Label Test Value"`; - -exports[` > renders with a specific value color 1`] = `"Test Label Test Value"`; - -exports[` > 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[` > 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[` > 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[` > Conditional Rendering Tests > hides User Agreement w ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" `; +exports[` > 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[` > 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[` > renders a table with two models correctly 1`] = ` "╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │ │