feat(cli): add user identity info to stats command (#17612)

This commit is contained in:
Sehoon Shon
2026-01-28 16:26:33 -05:00
committed by GitHub
parent 30065c51fb
commit bee1267e2a
15 changed files with 429 additions and 112 deletions

View File

@@ -57,6 +57,7 @@ they appear in the UI.
| Show Line Numbers | `ui.showLineNumbers` | Show line numbers in the chat. | `true` |
| Show Citations | `ui.showCitations` | Show citations for generated text in the chat. | `false` |
| Show Model Info In Chat | `ui.showModelInfoInChat` | Show the model name in the chat for each model turn. | `false` |
| Show User Identity | `ui.showUserIdentity` | Show the logged-in user's identity (e.g. email) in the UI. | `true` |
| Use Alternate Screen Buffer | `ui.useAlternateBuffer` | Use an alternate screen buffer for the UI, preserving shell history. | `false` |
| Use Background Color | `ui.useBackgroundColor` | Whether to use background colors in the UI. | `true` |
| Incremental Rendering | `ui.incrementalRendering` | Enable incremental rendering for the UI. This option will reduce flickering but may cause rendering artifacts. Only supported when useAlternateBuffer is enabled. | `true` |

View File

@@ -244,6 +244,10 @@ their corresponding top-level category object in your `settings.json` file.
- **Description:** Show the model name in the chat for each model turn.
- **Default:** `false`
- **`ui.showUserIdentity`** (boolean):
- **Description:** Show the logged-in user's identity (e.g. email) in the UI.
- **Default:** `true`
- **`ui.useAlternateBuffer`** (boolean):
- **Description:** Use an alternate screen buffer for the UI, preserving shell
history.

View File

@@ -524,6 +524,16 @@ const SETTINGS_SCHEMA = {
description: 'Show the model name in the chat for each model turn.',
showInDialog: true,
},
showUserIdentity: {
type: 'boolean',
label: 'Show User Identity',
category: 'UI',
requiresRestart: false,
default: true,
description:
"Show the logged-in user's identity (e.g. email) in the UI.",
showInDialog: true,
},
useAlternateBuffer: {
type: 'boolean',
label: 'Use Alternate Screen Buffer',

View File

@@ -12,6 +12,17 @@ import { MessageType } from '../types.js';
import { formatDuration } from '../utils/formatters.js';
import type { Config } from '@google/gemini-cli-core';
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const actual =
await importOriginal<typeof import('@google/gemini-cli-core')>();
return {
...actual,
UserAccountManager: vi.fn().mockImplementation(() => ({
getCachedGoogleAccount: vi.fn().mockReturnValue('mock@example.com'),
})),
};
});
describe('statsCommand', () => {
let mockContext: CommandContext;
const startTime = new Date('2025-07-14T10:00:00.000Z');
@@ -40,6 +51,9 @@ describe('statsCommand', () => {
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
type: MessageType.STATS,
duration: expectedDuration,
selectedAuthType: '',
tier: undefined,
userEmail: 'mock@example.com',
});
});
@@ -48,8 +62,10 @@ describe('statsCommand', () => {
const mockQuota = { buckets: [] };
const mockRefreshUserQuota = vi.fn().mockResolvedValue(mockQuota);
const mockGetUserTierName = vi.fn().mockReturnValue('Basic');
mockContext.services.config = {
refreshUserQuota: mockRefreshUserQuota,
getUserTierName: mockGetUserTierName,
} as unknown as Config;
await statsCommand.action(mockContext, '');
@@ -58,6 +74,7 @@ describe('statsCommand', () => {
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
expect.objectContaining({
quotas: mockQuota,
tier: 'Basic',
}),
);
});
@@ -73,6 +90,9 @@ describe('statsCommand', () => {
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
type: MessageType.MODEL_STATS,
selectedAuthType: '',
tier: undefined,
userEmail: 'mock@example.com',
});
});

View File

@@ -4,15 +4,33 @@
* SPDX-License-Identifier: Apache-2.0
*/
import type { HistoryItemStats } from '../types.js';
import type {
HistoryItemStats,
HistoryItemModelStats,
HistoryItemToolStats,
} from '../types.js';
import { MessageType } from '../types.js';
import { formatDuration } from '../utils/formatters.js';
import { UserAccountManager } from '@google/gemini-cli-core';
import {
type CommandContext,
type SlashCommand,
CommandKind,
} from './types.js';
function getUserIdentity(context: CommandContext) {
const selectedAuthType =
context.services.settings.merged.security.auth.selectedType || '';
const userAccountManager = new UserAccountManager();
const cachedAccount = userAccountManager.getCachedGoogleAccount();
const userEmail = cachedAccount ?? undefined;
const tier = context.services.config?.getUserTierName();
return { selectedAuthType, userEmail, tier };
}
async function defaultSessionView(context: CommandContext) {
const now = new Date();
const { sessionStartTime } = context.session.stats;
@@ -25,9 +43,14 @@ async function defaultSessionView(context: CommandContext) {
}
const wallDuration = now.getTime() - sessionStartTime.getTime();
const { selectedAuthType, userEmail, tier } = getUserIdentity(context);
const statsItem: HistoryItemStats = {
type: MessageType.STATS,
duration: formatDuration(wallDuration),
selectedAuthType,
userEmail,
tier,
};
if (context.services.config) {
@@ -65,9 +88,13 @@ export const statsCommand: SlashCommand = {
kind: CommandKind.BUILT_IN,
autoExecute: true,
action: (context: CommandContext) => {
const { selectedAuthType, userEmail, tier } = getUserIdentity(context);
context.ui.addItem({
type: MessageType.MODEL_STATS,
});
selectedAuthType,
userEmail,
tier,
} as HistoryItemModelStats);
},
},
{
@@ -78,7 +105,7 @@ export const statsCommand: SlashCommand = {
action: (context: CommandContext) => {
context.ui.addItem({
type: MessageType.TOOL_STATS,
});
} as HistoryItemToolStats);
},
},
],

View File

@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from '../../test-utils/render.js';
import { renderWithProviders } from '../../test-utils/render.js';
import { AboutBox } from './AboutBox.js';
import { describe, it, expect, vi } from 'vitest';
@@ -25,7 +25,7 @@ describe('AboutBox', () => {
};
it('renders with required props', () => {
const { lastFrame } = render(<AboutBox {...defaultProps} />);
const { lastFrame } = renderWithProviders(<AboutBox {...defaultProps} />);
const output = lastFrame();
expect(output).toContain('About Gemini CLI');
expect(output).toContain('1.0.0');
@@ -42,7 +42,7 @@ describe('AboutBox', () => {
['tier', 'Enterprise', 'Tier'],
])('renders optional prop %s', (prop, value, label) => {
const props = { ...defaultProps, [prop]: value };
const { lastFrame } = render(<AboutBox {...props} />);
const { lastFrame } = renderWithProviders(<AboutBox {...props} />);
const output = lastFrame();
expect(output).toContain(label);
expect(output).toContain(value);
@@ -50,14 +50,14 @@ describe('AboutBox', () => {
it('renders Auth Method with email when userEmail is provided', () => {
const props = { ...defaultProps, userEmail: 'test@example.com' };
const { lastFrame } = render(<AboutBox {...props} />);
const { lastFrame } = renderWithProviders(<AboutBox {...props} />);
const output = lastFrame();
expect(output).toContain('Logged in with Google (test@example.com)');
});
it('renders Auth Method correctly when not oauth', () => {
const props = { ...defaultProps, selectedAuthType: 'api-key' };
const { lastFrame } = render(<AboutBox {...props} />);
const { lastFrame } = renderWithProviders(<AboutBox {...props} />);
const output = lastFrame();
expect(output).toContain('api-key');
});

View File

@@ -8,6 +8,7 @@ import type React from 'react';
import { Box, Text } from 'ink';
import { theme } from '../semantic-colors.js';
import { GIT_COMMIT_INFO } from '../../generated/git-commit.js';
import { useSettings } from '../contexts/SettingsContext.js';
interface AboutBoxProps {
cliVersion: string;
@@ -31,123 +32,130 @@ export const AboutBox: React.FC<AboutBoxProps> = ({
ideClient,
userEmail,
tier,
}) => (
<Box
borderStyle="round"
borderColor={theme.border.default}
flexDirection="column"
padding={1}
marginY={1}
width="100%"
>
<Box marginBottom={1}>
<Text bold color={theme.text.accent}>
About Gemini CLI
</Text>
</Box>
<Box flexDirection="row">
<Box width="35%">
<Text bold color={theme.text.link}>
CLI Version
}) => {
const settings = useSettings();
const showUserIdentity = settings.merged.ui.showUserIdentity;
return (
<Box
borderStyle="round"
borderColor={theme.border.default}
flexDirection="column"
padding={1}
marginY={1}
width="100%"
>
<Box marginBottom={1}>
<Text bold color={theme.text.accent}>
About Gemini CLI
</Text>
</Box>
<Box>
<Text color={theme.text.primary}>{cliVersion}</Text>
</Box>
</Box>
{GIT_COMMIT_INFO && !['N/A'].includes(GIT_COMMIT_INFO) && (
<Box flexDirection="row">
<Box width="35%">
<Text bold color={theme.text.link}>
Git Commit
CLI Version
</Text>
</Box>
<Box>
<Text color={theme.text.primary}>{GIT_COMMIT_INFO}</Text>
<Text color={theme.text.primary}>{cliVersion}</Text>
</Box>
</Box>
)}
<Box flexDirection="row">
<Box width="35%">
<Text bold color={theme.text.link}>
Model
</Text>
</Box>
<Box>
<Text color={theme.text.primary}>{modelVersion}</Text>
</Box>
</Box>
<Box flexDirection="row">
<Box width="35%">
<Text bold color={theme.text.link}>
Sandbox
</Text>
</Box>
<Box>
<Text color={theme.text.primary}>{sandboxEnv}</Text>
</Box>
</Box>
<Box flexDirection="row">
<Box width="35%">
<Text bold color={theme.text.link}>
OS
</Text>
</Box>
<Box>
<Text color={theme.text.primary}>{osVersion}</Text>
</Box>
</Box>
<Box flexDirection="row">
<Box width="35%">
<Text bold color={theme.text.link}>
Auth Method
</Text>
</Box>
<Box>
<Text color={theme.text.primary}>
{selectedAuthType.startsWith('oauth')
? userEmail
? `Logged in with Google (${userEmail})`
: 'Logged in with Google'
: selectedAuthType}
</Text>
</Box>
</Box>
{tier && (
{GIT_COMMIT_INFO && !['N/A'].includes(GIT_COMMIT_INFO) && (
<Box flexDirection="row">
<Box width="35%">
<Text bold color={theme.text.link}>
Git Commit
</Text>
</Box>
<Box>
<Text color={theme.text.primary}>{GIT_COMMIT_INFO}</Text>
</Box>
</Box>
)}
<Box flexDirection="row">
<Box width="35%">
<Text bold color={theme.text.link}>
Tier
Model
</Text>
</Box>
<Box>
<Text color={theme.text.primary}>{tier}</Text>
<Text color={theme.text.primary}>{modelVersion}</Text>
</Box>
</Box>
)}
{gcpProject && (
<Box flexDirection="row">
<Box width="35%">
<Text bold color={theme.text.link}>
GCP Project
Sandbox
</Text>
</Box>
<Box>
<Text color={theme.text.primary}>{gcpProject}</Text>
<Text color={theme.text.primary}>{sandboxEnv}</Text>
</Box>
</Box>
)}
{ideClient && (
<Box flexDirection="row">
<Box width="35%">
<Text bold color={theme.text.link}>
IDE Client
OS
</Text>
</Box>
<Box>
<Text color={theme.text.primary}>{ideClient}</Text>
<Text color={theme.text.primary}>{osVersion}</Text>
</Box>
</Box>
)}
</Box>
);
{showUserIdentity && (
<Box flexDirection="row">
<Box width="35%">
<Text bold color={theme.text.link}>
Auth Method
</Text>
</Box>
<Box>
<Text color={theme.text.primary}>
{selectedAuthType.startsWith('oauth')
? userEmail
? `Logged in with Google (${userEmail})`
: 'Logged in with Google'
: selectedAuthType}
</Text>
</Box>
</Box>
)}
{showUserIdentity && tier && (
<Box flexDirection="row">
<Box width="35%">
<Text bold color={theme.text.link}>
Tier
</Text>
</Box>
<Box>
<Text color={theme.text.primary}>{tier}</Text>
</Box>
</Box>
)}
{gcpProject && (
<Box flexDirection="row">
<Box width="35%">
<Text bold color={theme.text.link}>
GCP Project
</Text>
</Box>
<Box>
<Text color={theme.text.primary}>{gcpProject}</Text>
</Box>
</Box>
)}
{ideClient && (
<Box flexDirection="row">
<Box width="35%">
<Text bold color={theme.text.link}>
IDE Client
</Text>
</Box>
<Box>
<Text color={theme.text.primary}>{ideClient}</Text>
</Box>
</Box>
)}
</Box>
);
};

View File

@@ -122,9 +122,18 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
<StatsDisplay
duration={itemForDisplay.duration}
quotas={itemForDisplay.quotas}
selectedAuthType={itemForDisplay.selectedAuthType}
userEmail={itemForDisplay.userEmail}
tier={itemForDisplay.tier}
/>
)}
{itemForDisplay.type === 'model_stats' && (
<ModelStatsDisplay
selectedAuthType={itemForDisplay.selectedAuthType}
userEmail={itemForDisplay.userEmail}
tier={itemForDisplay.tier}
/>
)}
{itemForDisplay.type === 'model_stats' && <ModelStatsDisplay />}
{itemForDisplay.type === 'tool_stats' && <ToolStatsDisplay />}
{itemForDisplay.type === 'model' && (
<ModelMessage model={itemForDisplay.model} />

View File

@@ -8,6 +8,8 @@ import { render } from '../../test-utils/render.js';
import { describe, it, expect, vi, beforeAll, afterAll } from 'vitest';
import { ModelStatsDisplay } from './ModelStatsDisplay.js';
import * as SessionContext from '../contexts/SessionContext.js';
import * as SettingsContext from '../contexts/SettingsContext.js';
import type { LoadedSettings } from '../../config/settings.js';
import type { SessionMetrics } from '../contexts/SessionContext.js';
import { ToolCallDecision } from '@google/gemini-cli-core';
@@ -20,7 +22,16 @@ vi.mock('../contexts/SessionContext.js', async (importOriginal) => {
};
});
vi.mock('../contexts/SettingsContext.js', async (importOriginal) => {
const actual = await importOriginal<typeof SettingsContext>();
return {
...actual,
useSettings: vi.fn(),
};
});
const useSessionStatsMock = vi.mocked(SessionContext.useSessionStats);
const useSettingsMock = vi.mocked(SettingsContext.useSettings);
const renderWithMockedStats = (metrics: SessionMetrics, width?: number) => {
useSessionStatsMock.mockReturnValue({
@@ -36,6 +47,14 @@ const renderWithMockedStats = (metrics: SessionMetrics, width?: number) => {
startNewPrompt: vi.fn(),
});
useSettingsMock.mockReturnValue({
merged: {
ui: {
showUserIdentity: true,
},
},
} as unknown as LoadedSettings);
return render(<ModelStatsDisplay />, width);
};
@@ -368,4 +387,74 @@ describe('<ModelStatsDisplay />', () => {
expect(output).toContain('gemini-3-flash-');
expect(output).toMatchSnapshot();
});
it('should render user identity information when provided', () => {
useSettingsMock.mockReturnValue({
merged: {
ui: {
showUserIdentity: true,
},
},
} as unknown as LoadedSettings);
const { lastFrame } = render(
<ModelStatsDisplay
selectedAuthType="oauth"
userEmail="test@example.com"
tier="Pro"
/>,
);
useSessionStatsMock.mockReturnValue({
stats: {
sessionId: 'test-session',
sessionStartTime: new Date(),
metrics: {
models: {
'gemini-2.5-pro': {
api: { totalRequests: 1, totalErrors: 0, totalLatencyMs: 100 },
tokens: {
input: 10,
prompt: 10,
candidates: 20,
total: 30,
cached: 0,
thoughts: 0,
tool: 0,
},
},
},
tools: {
totalCalls: 0,
totalSuccess: 0,
totalFail: 0,
totalDurationMs: 0,
totalDecisions: {
accept: 0,
reject: 0,
modify: 0,
[ToolCallDecision.AUTO_ACCEPT]: 0,
},
byName: {},
},
files: {
totalLinesAdded: 0,
totalLinesRemoved: 0,
},
},
lastPromptTokenCount: 0,
promptCount: 5,
},
getPromptCount: () => 5,
startNewPrompt: vi.fn(),
});
const output = lastFrame();
expect(output).toContain('Auth Method:');
expect(output).toContain('Logged in with Google');
expect(output).toContain('(test@example.com)');
expect(output).toContain('Tier:');
expect(output).toContain('Pro');
});
});

View File

@@ -15,6 +15,7 @@ import {
} from '../utils/computeStats.js';
import { useSessionStats } from '../contexts/SessionContext.js';
import { Table, type Column } from './Table.js';
import { useSettings } from '../contexts/SettingsContext.js';
interface StatRowData {
metric: string;
@@ -24,9 +25,21 @@ interface StatRowData {
[key: string]: string | React.ReactNode | boolean | undefined;
}
export const ModelStatsDisplay: React.FC = () => {
interface ModelStatsDisplayProps {
selectedAuthType?: string;
userEmail?: string;
tier?: string;
}
export const ModelStatsDisplay: React.FC<ModelStatsDisplayProps> = ({
selectedAuthType,
userEmail,
tier,
}) => {
const { stats } = useSessionStats();
const { models } = stats.metrics;
const settings = useSettings();
const showUserIdentity = settings.merged.ui.showUserIdentity;
const activeModels = Object.entries(models).filter(
([, metrics]) => metrics.api.totalRequests > 0,
);
@@ -75,10 +88,12 @@ export const ModelStatsDisplay: React.FC = () => {
return row;
};
const rows: StatRowData[] = [
// API Section
{ metric: 'API', isSection: true },
createRow('Requests', (m) => m.api.totalRequests.toLocaleString()),
const rows: StatRowData[] = [];
// API Section
rows.push({ metric: 'API', isSection: true });
rows.push(createRow('Requests', (m) => m.api.totalRequests.toLocaleString()));
rows.push(
createRow('Errors', (m) => {
const errorRate = calculateErrorRate(m);
return (
@@ -91,18 +106,24 @@ export const ModelStatsDisplay: React.FC = () => {
</Text>
);
}),
);
rows.push(
createRow('Avg Latency', (m) => formatDuration(calculateAverageLatency(m))),
);
// Spacer
{ metric: '' },
// Spacer
rows.push({ metric: '' });
// Tokens Section
{ metric: 'Tokens', isSection: true },
// Tokens Section
rows.push({ metric: 'Tokens', isSection: true });
rows.push(
createRow('Total', (m) => (
<Text color={theme.text.secondary}>
{m.tokens.total.toLocaleString()}
</Text>
)),
);
rows.push(
createRow(
'Input',
(m) => (
@@ -112,7 +133,7 @@ export const ModelStatsDisplay: React.FC = () => {
),
{ isSubtle: true },
),
];
);
if (hasCached) {
rows.push(
@@ -214,6 +235,31 @@ export const ModelStatsDisplay: React.FC = () => {
Model Stats For Nerds
</Text>
<Box height={1} />
{showUserIdentity && selectedAuthType && (
<Box>
<Box width={28}>
<Text color={theme.text.link}>Auth Method:</Text>
</Box>
<Text color={theme.text.primary}>
{selectedAuthType.startsWith('oauth')
? userEmail
? `Logged in with Google (${userEmail})`
: 'Logged in with Google'
: selectedAuthType}
</Text>
</Box>
)}
{showUserIdentity && tier && (
<Box>
<Box width={28}>
<Text color={theme.text.link}>Tier:</Text>
</Box>
<Text color={theme.text.primary}>{tier}</Text>
</Box>
)}
{showUserIdentity && (selectedAuthType || tier) && <Box height={1} />}
<Table data={rows} columns={columns} />
</Box>
);

View File

@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from '../../test-utils/render.js';
import { renderWithProviders } from '../../test-utils/render.js';
import { describe, it, expect, vi } from 'vitest';
import { SessionSummaryDisplay } from './SessionSummaryDisplay.js';
import * as SessionContext from '../contexts/SessionContext.js';
@@ -35,7 +35,9 @@ const renderWithMockedStats = (metrics: SessionMetrics) => {
startNewPrompt: vi.fn(),
});
return render(<SessionSummaryDisplay duration="1h 23m 45s" />);
return renderWithProviders(<SessionSummaryDisplay duration="1h 23m 45s" />, {
width: 100,
});
};
describe('<SessionSummaryDisplay />', () => {

View File

@@ -4,7 +4,7 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { render } from '../../test-utils/render.js';
import { renderWithProviders } from '../../test-utils/render.js';
import { describe, it, expect, vi } from 'vitest';
import { StatsDisplay } from './StatsDisplay.js';
import * as SessionContext from '../contexts/SessionContext.js';
@@ -39,7 +39,7 @@ const renderWithMockedStats = (metrics: SessionMetrics) => {
startNewPrompt: vi.fn(),
});
return render(<StatsDisplay duration="1s" />);
return renderWithProviders(<StatsDisplay duration="1s" />, { width: 100 });
};
// Helper to create metrics with default zero values
@@ -381,8 +381,9 @@ describe('<StatsDisplay />', () => {
startNewPrompt: vi.fn(),
});
const { lastFrame } = render(
const { lastFrame } = renderWithProviders(
<StatsDisplay duration="1s" title="Agent powering down. Goodbye!" />,
{ width: 100 },
);
const output = lastFrame();
expect(output).toContain('Agent powering down. Goodbye!');
@@ -439,8 +440,9 @@ describe('<StatsDisplay />', () => {
startNewPrompt: vi.fn(),
});
const { lastFrame } = render(
const { lastFrame } = renderWithProviders(
<StatsDisplay duration="1s" quotas={quotas} />,
{ width: 100 },
);
const output = lastFrame();
@@ -484,8 +486,9 @@ describe('<StatsDisplay />', () => {
startNewPrompt: vi.fn(),
});
const { lastFrame } = render(
const { lastFrame } = renderWithProviders(
<StatsDisplay duration="1s" quotas={quotas} />,
{ width: 100 },
);
const output = lastFrame();
@@ -498,4 +501,64 @@ describe('<StatsDisplay />', () => {
vi.useRealTimers();
});
});
describe('User Identity Display', () => {
it('renders User row with Auth Method and Tier', () => {
const metrics = createTestMetrics();
useSessionStatsMock.mockReturnValue({
stats: {
sessionId: 'test-session-id',
sessionStartTime: new Date(),
metrics,
lastPromptTokenCount: 0,
promptCount: 5,
},
getPromptCount: () => 5,
startNewPrompt: vi.fn(),
});
const { lastFrame } = renderWithProviders(
<StatsDisplay
duration="1s"
selectedAuthType="oauth"
userEmail="test@example.com"
tier="Pro"
/>,
{ width: 100 },
);
const output = lastFrame();
expect(output).toContain('Auth Method:');
expect(output).toContain('Logged in with Google (test@example.com)');
expect(output).toContain('Tier:');
expect(output).toContain('Pro');
});
it('renders User row with API Key and no Tier', () => {
const metrics = createTestMetrics();
useSessionStatsMock.mockReturnValue({
stats: {
sessionId: 'test-session-id',
sessionStartTime: new Date(),
metrics,
lastPromptTokenCount: 0,
promptCount: 5,
},
getPromptCount: () => 5,
startNewPrompt: vi.fn(),
});
const { lastFrame } = renderWithProviders(
<StatsDisplay duration="1s" selectedAuthType="Google API Key" />,
{ width: 100 },
);
const output = lastFrame();
expect(output).toContain('Auth Method:');
expect(output).toContain('Google API Key');
expect(output).not.toContain('Tier:');
});
});
});

View File

@@ -25,6 +25,7 @@ import {
type RetrieveUserQuotaResponse,
VALID_GEMINI_MODELS,
} from '@google/gemini-cli-core';
import { useSettings } from '../contexts/SettingsContext.js';
// A more flexible and powerful StatRow component
interface StatRowProps {
@@ -364,17 +365,25 @@ interface StatsDisplayProps {
duration: string;
title?: string;
quotas?: RetrieveUserQuotaResponse;
selectedAuthType?: string;
userEmail?: string;
tier?: string;
}
export const StatsDisplay: React.FC<StatsDisplayProps> = ({
duration,
title,
quotas,
selectedAuthType,
userEmail,
tier,
}) => {
const { stats } = useSessionStats();
const { metrics } = stats;
const { models, tools, files } = metrics;
const computed = computeSessionStats(metrics);
const settings = useSettings();
const showUserIdentity = settings.merged.ui.showUserIdentity;
const successThresholds = {
green: TOOL_SUCCESS_RATE_HIGH,
@@ -417,6 +426,22 @@ export const StatsDisplay: React.FC<StatsDisplayProps> = ({
<StatRow title="Session ID:">
<Text color={theme.text.primary}>{stats.sessionId}</Text>
</StatRow>
{showUserIdentity && selectedAuthType && (
<StatRow title="Auth Method:">
<Text color={theme.text.primary}>
{selectedAuthType.startsWith('oauth')
? userEmail
? `Logged in with Google (${userEmail})`
: 'Logged in with Google'
: selectedAuthType}
</Text>
</StatRow>
)}
{showUserIdentity && tier && (
<StatRow title="Tier:">
<Text color={theme.text.primary}>{tier}</Text>
</StatRow>
)}
<StatRow title="Tool Calls:">
<Text color={theme.text.primary}>
{tools.totalCalls} ({' '}

View File

@@ -157,10 +157,16 @@ export type HistoryItemStats = HistoryItemBase & {
type: 'stats';
duration: string;
quotas?: RetrieveUserQuotaResponse;
selectedAuthType?: string;
userEmail?: string;
tier?: string;
};
export type HistoryItemModelStats = HistoryItemBase & {
type: 'model_stats';
selectedAuthType?: string;
userEmail?: string;
tier?: string;
};
export type HistoryItemToolStats = HistoryItemBase & {

View File

@@ -302,6 +302,13 @@
"default": false,
"type": "boolean"
},
"showUserIdentity": {
"title": "Show User Identity",
"description": "Show the logged-in user's identity (e.g. email) in the UI.",
"markdownDescription": "Show the logged-in user's identity (e.g. email) in the UI.\n\n- Category: `UI`\n- Requires restart: `no`\n- Default: `true`",
"default": true,
"type": "boolean"
},
"useAlternateBuffer": {
"title": "Use Alternate Screen Buffer",
"description": "Use an alternate screen buffer for the UI, preserving shell history.",