diff --git a/packages/cli/src/ui/components/StatsDisplay.test.tsx b/packages/cli/src/ui/components/StatsDisplay.test.tsx
index b34bf60298..cd98ed400d 100644
--- a/packages/cli/src/ui/components/StatsDisplay.test.tsx
+++ b/packages/cli/src/ui/components/StatsDisplay.test.tsx
@@ -9,7 +9,7 @@ import { describe, it, expect, vi } from 'vitest';
import { StatsDisplay } from './StatsDisplay.js';
import * as SessionContext from '../contexts/SessionContext.js';
import { type SessionMetrics } from '../contexts/SessionContext.js';
-import { ToolCallDecision } from '@google/gemini-cli-core';
+import { ToolCallDecision, LlmRole } from '@google/gemini-cli-core';
// Mock the context to provide controlled data for testing
vi.mock('../contexts/SessionContext.js', async (importOriginal) => {
@@ -131,6 +131,66 @@ describe('', () => {
expect(output).toMatchSnapshot();
});
+ it('renders role breakdown correctly under models', async () => {
+ const metrics = createTestMetrics({
+ models: {
+ 'gemini-2.5-flash': {
+ api: { totalRequests: 10, totalErrors: 0, totalLatencyMs: 10000 },
+ tokens: {
+ input: 1000,
+ prompt: 1200,
+ candidates: 2000,
+ total: 3200,
+ cached: 200,
+ thoughts: 0,
+ tool: 0,
+ },
+ roles: {
+ [LlmRole.MAIN]: {
+ totalRequests: 7,
+ totalErrors: 0,
+ totalLatencyMs: 7000,
+ tokens: {
+ input: 800,
+ prompt: 900,
+ candidates: 1500,
+ total: 2400,
+ cached: 100,
+ thoughts: 0,
+ tool: 0,
+ },
+ },
+ [LlmRole.UTILITY_TOOL]: {
+ totalRequests: 3,
+ totalErrors: 0,
+ totalLatencyMs: 3000,
+ tokens: {
+ input: 200,
+ prompt: 300,
+ candidates: 500,
+ total: 800,
+ cached: 100,
+ thoughts: 0,
+ tool: 0,
+ },
+ },
+ },
+ },
+ },
+ });
+
+ const { lastFrame } = await renderWithMockedStats(metrics);
+ const output = lastFrame();
+
+ expect(output).toContain('gemini-2.5-flash');
+ expect(output).toContain('10'); // Total requests
+ expect(output).toContain('↳ main');
+ expect(output).toContain('7'); // main requests
+ expect(output).toContain('↳ utility_tool');
+ expect(output).toContain('3'); // tool requests
+ expect(output).toMatchSnapshot();
+ });
+
it('renders all sections when all data is present', async () => {
const metrics = createTestMetrics({
models: {
diff --git a/packages/cli/src/ui/components/StatsDisplay.tsx b/packages/cli/src/ui/components/StatsDisplay.tsx
index 4668a7a5a7..233e9f3ed4 100644
--- a/packages/cli/src/ui/components/StatsDisplay.tsx
+++ b/packages/cli/src/ui/components/StatsDisplay.tsx
@@ -12,6 +12,7 @@ import { formatDuration } from '../utils/formatters.js';
import {
useSessionStats,
type ModelMetrics,
+ type RoleMetrics,
} from '../contexts/SessionContext.js';
import {
getStatusColor,
@@ -23,6 +24,7 @@ import {
import { computeSessionStats } from '../utils/computeStats.js';
import { useSettings } from '../contexts/SettingsContext.js';
import type { QuotaStats } from '../types.js';
+import { LlmRole } from '@google/gemini-cli-core';
// A more flexible and powerful StatRow component
interface StatRowProps {
@@ -77,6 +79,16 @@ interface ModelUsageTableProps {
models: Record;
}
+interface ModelRow {
+ name: string;
+ displayName: string;
+ requests: number | string;
+ cachedTokens: string;
+ inputTokens: string;
+ outputTokens: string;
+ isSubRow: boolean;
+}
+
const ModelUsageTable: React.FC = ({ models }) => {
const nameWidth = 28;
const requestsWidth = 8;
@@ -84,6 +96,46 @@ const ModelUsageTable: React.FC = ({ models }) => {
const cacheReadsWidth = 14;
const outputTokensWidth = 14;
+ const rows: ModelRow[] = [];
+
+ Object.entries(models).forEach(([name, metrics]) => {
+ rows.push({
+ name,
+ displayName: name,
+ requests: metrics.api.totalRequests,
+ cachedTokens: metrics.tokens.cached.toLocaleString(),
+ inputTokens: metrics.tokens.prompt.toLocaleString(),
+ outputTokens: metrics.tokens.candidates.toLocaleString(),
+ isSubRow: false,
+ });
+
+ if (metrics.roles) {
+ const roleEntries = Object.entries(metrics.roles).filter(
+ (entry): entry is [string, RoleMetrics] =>
+ entry[1] !== undefined && entry[1].totalRequests > 0,
+ );
+
+ roleEntries.sort(([a], [b]) => {
+ if (a === b) return 0;
+ if (a === LlmRole.MAIN) return -1;
+ if (b === LlmRole.MAIN) return 1;
+ return a.localeCompare(b);
+ });
+
+ roleEntries.forEach(([role, roleMetrics]) => {
+ rows.push({
+ name: `${name}-${role}`,
+ displayName: ` ↳ ${role}`,
+ requests: roleMetrics.totalRequests,
+ cachedTokens: roleMetrics.tokens.cached.toLocaleString(),
+ inputTokens: roleMetrics.tokens.prompt.toLocaleString(),
+ outputTokens: roleMetrics.tokens.candidates.toLocaleString(),
+ isSubRow: true,
+ });
+ });
+ }
+ });
+
return (
@@ -131,31 +183,42 @@ const ModelUsageTable: React.FC = ({ models }) => {
{/* Rows */}
- {Object.entries(models).map(([name, modelMetrics]) => (
-
+ {rows.map((row) => (
+
-
- {name}
+
+ {row.displayName}
-
- {modelMetrics.api.totalRequests}
+
+ {row.requests}
-
- {modelMetrics.tokens.prompt.toLocaleString()}
+
+ {row.inputTokens}
-
- {modelMetrics.tokens.cached.toLocaleString()}
+
+ {row.cachedTokens}
-
- {modelMetrics.tokens.candidates.toLocaleString()}
+
+ {row.outputTokens}
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 a06587aaaf..8a58ee3440 100644
--- a/packages/cli/src/ui/components/__snapshots__/StatsDisplay.test.tsx.snap
+++ b/packages/cli/src/ui/components/__snapshots__/StatsDisplay.test.tsx.snap
@@ -263,3 +263,32 @@ exports[` > renders only the Performance section in its zero sta
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
`;
+
+exports[` > renders role breakdown correctly under models 1`] = `
+"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
+│ │
+│ Session Stats │
+│ │
+│ Interaction Summary │
+│ Session ID: test-session-id │
+│ Tool Calls: 0 ( ✓ 0 x 0 ) │
+│ Success Rate: 0.0% │
+│ │
+│ Performance │
+│ Wall Time: 1s │
+│ Agent Active: 10.0s │
+│ » API Time: 10.0s (100.0%) │
+│ » Tool Time: 0s (0.0%) │
+│ │
+│ │
+│ Model Usage │
+│ Use /model to view model quota information │
+│ │
+│ Model Reqs Input Tokens Cache Reads Output Tokens │
+│ ────────────────────────────────────────────────────────────────────────────────────────────── │
+│ gemini-2.5-flash 10 1,200 200 2,000 │
+│ ↳ main 7 900 100 1,500 │
+│ ↳ utility_tool 3 300 100 500 │
+╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
+"
+`;
diff --git a/packages/cli/src/ui/contexts/SessionContext.tsx b/packages/cli/src/ui/contexts/SessionContext.tsx
index 5ca37a1569..7f313bb443 100644
--- a/packages/cli/src/ui/contexts/SessionContext.tsx
+++ b/packages/cli/src/ui/contexts/SessionContext.tsx
@@ -17,6 +17,7 @@ import {
import type {
SessionMetrics,
ModelMetrics,
+ RoleMetrics,
ToolCallStats,
} from '@google/gemini-cli-core';
import { uiTelemetryService, sessionId } from '@google/gemini-cli-core';
@@ -139,7 +140,7 @@ function areMetricsEqual(a: SessionMetrics, b: SessionMetrics): boolean {
return true;
}
-export type { SessionMetrics, ModelMetrics };
+export type { SessionMetrics, ModelMetrics, RoleMetrics };
export interface SessionStatsState {
sessionId: string;