From bee1267e2ab68d5dfcfc3fc0bd40bbfd6b2ad0b9 Mon Sep 17 00:00:00 2001 From: Sehoon Shon Date: Wed, 28 Jan 2026 16:26:33 -0500 Subject: [PATCH] feat(cli): add user identity info to stats command (#17612) --- docs/cli/settings.md | 1 + docs/get-started/configuration.md | 4 + packages/cli/src/config/settingsSchema.ts | 10 + .../cli/src/ui/commands/statsCommand.test.ts | 20 ++ packages/cli/src/ui/commands/statsCommand.ts | 33 +++- .../cli/src/ui/components/AboutBox.test.tsx | 10 +- packages/cli/src/ui/components/AboutBox.tsx | 180 +++++++++--------- .../src/ui/components/HistoryItemDisplay.tsx | 11 +- .../ui/components/ModelStatsDisplay.test.tsx | 89 +++++++++ .../src/ui/components/ModelStatsDisplay.tsx | 66 ++++++- .../components/SessionSummaryDisplay.test.tsx | 6 +- .../src/ui/components/StatsDisplay.test.tsx | 73 ++++++- .../cli/src/ui/components/StatsDisplay.tsx | 25 +++ packages/cli/src/ui/types.ts | 6 + schemas/settings.schema.json | 7 + 15 files changed, 429 insertions(+), 112 deletions(-) diff --git a/docs/cli/settings.md b/docs/cli/settings.md index b5421b581c..6cee55b709 100644 --- a/docs/cli/settings.md +++ b/docs/cli/settings.md @@ -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` | diff --git a/docs/get-started/configuration.md b/docs/get-started/configuration.md index 22f130b1c1..6ffdd87282 100644 --- a/docs/get-started/configuration.md +++ b/docs/get-started/configuration.md @@ -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. diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index 9cfde9fab6..ffa4c6111a 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -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', diff --git a/packages/cli/src/ui/commands/statsCommand.test.ts b/packages/cli/src/ui/commands/statsCommand.test.ts index cf948790d6..f89c76caac 100644 --- a/packages/cli/src/ui/commands/statsCommand.test.ts +++ b/packages/cli/src/ui/commands/statsCommand.test.ts @@ -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(); + 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', }); }); diff --git a/packages/cli/src/ui/commands/statsCommand.ts b/packages/cli/src/ui/commands/statsCommand.ts index 917c52c143..8d4466ba86 100644 --- a/packages/cli/src/ui/commands/statsCommand.ts +++ b/packages/cli/src/ui/commands/statsCommand.ts @@ -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); }, }, ], diff --git a/packages/cli/src/ui/components/AboutBox.test.tsx b/packages/cli/src/ui/components/AboutBox.test.tsx index 1e4810fec5..eab18ad089 100644 --- a/packages/cli/src/ui/components/AboutBox.test.tsx +++ b/packages/cli/src/ui/components/AboutBox.test.tsx @@ -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(); + const { lastFrame } = renderWithProviders(); 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(); + const { lastFrame } = renderWithProviders(); 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(); + const { lastFrame } = renderWithProviders(); 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(); + const { lastFrame } = renderWithProviders(); const output = lastFrame(); expect(output).toContain('api-key'); }); diff --git a/packages/cli/src/ui/components/AboutBox.tsx b/packages/cli/src/ui/components/AboutBox.tsx index 4b45a55b37..ea5512b48d 100644 --- a/packages/cli/src/ui/components/AboutBox.tsx +++ b/packages/cli/src/ui/components/AboutBox.tsx @@ -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 = ({ ideClient, userEmail, tier, -}) => ( - - - - About Gemini CLI - - - - - - CLI Version +}) => { + const settings = useSettings(); + const showUserIdentity = settings.merged.ui.showUserIdentity; + + return ( + + + + About Gemini CLI - - {cliVersion} - - - {GIT_COMMIT_INFO && !['N/A'].includes(GIT_COMMIT_INFO) && ( - Git Commit + CLI Version - {GIT_COMMIT_INFO} + {cliVersion} - )} - - - - Model - - - - {modelVersion} - - - - - - Sandbox - - - - {sandboxEnv} - - - - - - OS - - - - {osVersion} - - - - - - Auth Method - - - - - {selectedAuthType.startsWith('oauth') - ? userEmail - ? `Logged in with Google (${userEmail})` - : 'Logged in with Google' - : selectedAuthType} - - - - {tier && ( + {GIT_COMMIT_INFO && !['N/A'].includes(GIT_COMMIT_INFO) && ( + + + + Git Commit + + + + {GIT_COMMIT_INFO} + + + )} - Tier + Model - {tier} + {modelVersion} - )} - {gcpProject && ( - GCP Project + Sandbox - {gcpProject} + {sandboxEnv} - )} - {ideClient && ( - IDE Client + OS - {ideClient} + {osVersion} - )} - -); + {showUserIdentity && ( + + + + Auth Method + + + + + {selectedAuthType.startsWith('oauth') + ? userEmail + ? `Logged in with Google (${userEmail})` + : 'Logged in with Google' + : selectedAuthType} + + + + )} + {showUserIdentity && tier && ( + + + + Tier + + + + {tier} + + + )} + {gcpProject && ( + + + + GCP Project + + + + {gcpProject} + + + )} + {ideClient && ( + + + + IDE Client + + + + {ideClient} + + + )} + + ); +}; diff --git a/packages/cli/src/ui/components/HistoryItemDisplay.tsx b/packages/cli/src/ui/components/HistoryItemDisplay.tsx index f04ba39ec6..ed399dd38f 100644 --- a/packages/cli/src/ui/components/HistoryItemDisplay.tsx +++ b/packages/cli/src/ui/components/HistoryItemDisplay.tsx @@ -122,9 +122,18 @@ export const HistoryItemDisplay: React.FC = ({ + )} + {itemForDisplay.type === 'model_stats' && ( + )} - {itemForDisplay.type === 'model_stats' && } {itemForDisplay.type === 'tool_stats' && } {itemForDisplay.type === 'model' && ( diff --git a/packages/cli/src/ui/components/ModelStatsDisplay.test.tsx b/packages/cli/src/ui/components/ModelStatsDisplay.test.tsx index d233d3b385..7b1b233880 100644 --- a/packages/cli/src/ui/components/ModelStatsDisplay.test.tsx +++ b/packages/cli/src/ui/components/ModelStatsDisplay.test.tsx @@ -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(); + 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(, width); }; @@ -368,4 +387,74 @@ describe('', () => { 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( + , + ); + + 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'); + }); }); diff --git a/packages/cli/src/ui/components/ModelStatsDisplay.tsx b/packages/cli/src/ui/components/ModelStatsDisplay.tsx index f765bcede3..199311406c 100644 --- a/packages/cli/src/ui/components/ModelStatsDisplay.tsx +++ b/packages/cli/src/ui/components/ModelStatsDisplay.tsx @@ -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 = ({ + 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 = () => { ); }), + ); + 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) => ( {m.tokens.total.toLocaleString()} )), + ); + 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 + + {showUserIdentity && selectedAuthType && ( + + + Auth Method: + + + {selectedAuthType.startsWith('oauth') + ? userEmail + ? `Logged in with Google (${userEmail})` + : 'Logged in with Google' + : selectedAuthType} + + + )} + {showUserIdentity && tier && ( + + + Tier: + + {tier} + + )} + {showUserIdentity && (selectedAuthType || tier) && } + ); diff --git a/packages/cli/src/ui/components/SessionSummaryDisplay.test.tsx b/packages/cli/src/ui/components/SessionSummaryDisplay.test.tsx index 7476fa08d4..f878cc35c3 100644 --- a/packages/cli/src/ui/components/SessionSummaryDisplay.test.tsx +++ b/packages/cli/src/ui/components/SessionSummaryDisplay.test.tsx @@ -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(); + return renderWithProviders(, { + width: 100, + }); }; describe('', () => { diff --git a/packages/cli/src/ui/components/StatsDisplay.test.tsx b/packages/cli/src/ui/components/StatsDisplay.test.tsx index eb34fa6bd2..c2813e7a3a 100644 --- a/packages/cli/src/ui/components/StatsDisplay.test.tsx +++ b/packages/cli/src/ui/components/StatsDisplay.test.tsx @@ -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(); + return renderWithProviders(, { width: 100 }); }; // Helper to create metrics with default zero values @@ -381,8 +381,9 @@ describe('', () => { startNewPrompt: vi.fn(), }); - const { lastFrame } = render( + const { lastFrame } = renderWithProviders( , + { width: 100 }, ); const output = lastFrame(); expect(output).toContain('Agent powering down. Goodbye!'); @@ -439,8 +440,9 @@ describe('', () => { startNewPrompt: vi.fn(), }); - const { lastFrame } = render( + const { lastFrame } = renderWithProviders( , + { width: 100 }, ); const output = lastFrame(); @@ -484,8 +486,9 @@ describe('', () => { startNewPrompt: vi.fn(), }); - const { lastFrame } = render( + const { lastFrame } = renderWithProviders( , + { width: 100 }, ); const output = lastFrame(); @@ -498,4 +501,64 @@ describe('', () => { 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( + , + { 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( + , + { width: 100 }, + ); + const output = lastFrame(); + + expect(output).toContain('Auth Method:'); + expect(output).toContain('Google API Key'); + expect(output).not.toContain('Tier:'); + }); + }); }); diff --git a/packages/cli/src/ui/components/StatsDisplay.tsx b/packages/cli/src/ui/components/StatsDisplay.tsx index 8e89f54a6d..ce7b00f64e 100644 --- a/packages/cli/src/ui/components/StatsDisplay.tsx +++ b/packages/cli/src/ui/components/StatsDisplay.tsx @@ -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 = ({ 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 = ({ {stats.sessionId} + {showUserIdentity && selectedAuthType && ( + + + {selectedAuthType.startsWith('oauth') + ? userEmail + ? `Logged in with Google (${userEmail})` + : 'Logged in with Google' + : selectedAuthType} + + + )} + {showUserIdentity && tier && ( + + {tier} + + )} {tools.totalCalls} ({' '} diff --git a/packages/cli/src/ui/types.ts b/packages/cli/src/ui/types.ts index 5cf8b42619..aa00b800a5 100644 --- a/packages/cli/src/ui/types.ts +++ b/packages/cli/src/ui/types.ts @@ -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 & { diff --git a/schemas/settings.schema.json b/schemas/settings.schema.json index 8006662d7e..0a1ad9e879 100644 --- a/schemas/settings.schema.json +++ b/schemas/settings.schema.json @@ -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.",