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`] = `
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
│ │