mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-02-01 22:48:03 +00:00
feat(cli): add user identity info to stats command (#17612)
This commit is contained in:
@@ -57,6 +57,7 @@ they appear in the UI.
|
|||||||
| Show Line Numbers | `ui.showLineNumbers` | Show line numbers in the chat. | `true` |
|
| 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 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 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 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` |
|
| 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` |
|
| 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` |
|
||||||
|
|||||||
@@ -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.
|
- **Description:** Show the model name in the chat for each model turn.
|
||||||
- **Default:** `false`
|
- **Default:** `false`
|
||||||
|
|
||||||
|
- **`ui.showUserIdentity`** (boolean):
|
||||||
|
- **Description:** Show the logged-in user's identity (e.g. email) in the UI.
|
||||||
|
- **Default:** `true`
|
||||||
|
|
||||||
- **`ui.useAlternateBuffer`** (boolean):
|
- **`ui.useAlternateBuffer`** (boolean):
|
||||||
- **Description:** Use an alternate screen buffer for the UI, preserving shell
|
- **Description:** Use an alternate screen buffer for the UI, preserving shell
|
||||||
history.
|
history.
|
||||||
|
|||||||
@@ -524,6 +524,16 @@ const SETTINGS_SCHEMA = {
|
|||||||
description: 'Show the model name in the chat for each model turn.',
|
description: 'Show the model name in the chat for each model turn.',
|
||||||
showInDialog: true,
|
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: {
|
useAlternateBuffer: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
label: 'Use Alternate Screen Buffer',
|
label: 'Use Alternate Screen Buffer',
|
||||||
|
|||||||
@@ -12,6 +12,17 @@ import { MessageType } from '../types.js';
|
|||||||
import { formatDuration } from '../utils/formatters.js';
|
import { formatDuration } from '../utils/formatters.js';
|
||||||
import type { Config } from '@google/gemini-cli-core';
|
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', () => {
|
describe('statsCommand', () => {
|
||||||
let mockContext: CommandContext;
|
let mockContext: CommandContext;
|
||||||
const startTime = new Date('2025-07-14T10:00:00.000Z');
|
const startTime = new Date('2025-07-14T10:00:00.000Z');
|
||||||
@@ -40,6 +51,9 @@ describe('statsCommand', () => {
|
|||||||
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
|
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
|
||||||
type: MessageType.STATS,
|
type: MessageType.STATS,
|
||||||
duration: expectedDuration,
|
duration: expectedDuration,
|
||||||
|
selectedAuthType: '',
|
||||||
|
tier: undefined,
|
||||||
|
userEmail: 'mock@example.com',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -48,8 +62,10 @@ describe('statsCommand', () => {
|
|||||||
|
|
||||||
const mockQuota = { buckets: [] };
|
const mockQuota = { buckets: [] };
|
||||||
const mockRefreshUserQuota = vi.fn().mockResolvedValue(mockQuota);
|
const mockRefreshUserQuota = vi.fn().mockResolvedValue(mockQuota);
|
||||||
|
const mockGetUserTierName = vi.fn().mockReturnValue('Basic');
|
||||||
mockContext.services.config = {
|
mockContext.services.config = {
|
||||||
refreshUserQuota: mockRefreshUserQuota,
|
refreshUserQuota: mockRefreshUserQuota,
|
||||||
|
getUserTierName: mockGetUserTierName,
|
||||||
} as unknown as Config;
|
} as unknown as Config;
|
||||||
|
|
||||||
await statsCommand.action(mockContext, '');
|
await statsCommand.action(mockContext, '');
|
||||||
@@ -58,6 +74,7 @@ describe('statsCommand', () => {
|
|||||||
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
expect(mockContext.ui.addItem).toHaveBeenCalledWith(
|
||||||
expect.objectContaining({
|
expect.objectContaining({
|
||||||
quotas: mockQuota,
|
quotas: mockQuota,
|
||||||
|
tier: 'Basic',
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -73,6 +90,9 @@ describe('statsCommand', () => {
|
|||||||
|
|
||||||
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
|
expect(mockContext.ui.addItem).toHaveBeenCalledWith({
|
||||||
type: MessageType.MODEL_STATS,
|
type: MessageType.MODEL_STATS,
|
||||||
|
selectedAuthType: '',
|
||||||
|
tier: undefined,
|
||||||
|
userEmail: 'mock@example.com',
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -4,15 +4,33 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* 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 { MessageType } from '../types.js';
|
||||||
import { formatDuration } from '../utils/formatters.js';
|
import { formatDuration } from '../utils/formatters.js';
|
||||||
|
import { UserAccountManager } from '@google/gemini-cli-core';
|
||||||
import {
|
import {
|
||||||
type CommandContext,
|
type CommandContext,
|
||||||
type SlashCommand,
|
type SlashCommand,
|
||||||
CommandKind,
|
CommandKind,
|
||||||
} from './types.js';
|
} 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) {
|
async function defaultSessionView(context: CommandContext) {
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
const { sessionStartTime } = context.session.stats;
|
const { sessionStartTime } = context.session.stats;
|
||||||
@@ -25,9 +43,14 @@ async function defaultSessionView(context: CommandContext) {
|
|||||||
}
|
}
|
||||||
const wallDuration = now.getTime() - sessionStartTime.getTime();
|
const wallDuration = now.getTime() - sessionStartTime.getTime();
|
||||||
|
|
||||||
|
const { selectedAuthType, userEmail, tier } = getUserIdentity(context);
|
||||||
|
|
||||||
const statsItem: HistoryItemStats = {
|
const statsItem: HistoryItemStats = {
|
||||||
type: MessageType.STATS,
|
type: MessageType.STATS,
|
||||||
duration: formatDuration(wallDuration),
|
duration: formatDuration(wallDuration),
|
||||||
|
selectedAuthType,
|
||||||
|
userEmail,
|
||||||
|
tier,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (context.services.config) {
|
if (context.services.config) {
|
||||||
@@ -65,9 +88,13 @@ export const statsCommand: SlashCommand = {
|
|||||||
kind: CommandKind.BUILT_IN,
|
kind: CommandKind.BUILT_IN,
|
||||||
autoExecute: true,
|
autoExecute: true,
|
||||||
action: (context: CommandContext) => {
|
action: (context: CommandContext) => {
|
||||||
|
const { selectedAuthType, userEmail, tier } = getUserIdentity(context);
|
||||||
context.ui.addItem({
|
context.ui.addItem({
|
||||||
type: MessageType.MODEL_STATS,
|
type: MessageType.MODEL_STATS,
|
||||||
});
|
selectedAuthType,
|
||||||
|
userEmail,
|
||||||
|
tier,
|
||||||
|
} as HistoryItemModelStats);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -78,7 +105,7 @@ export const statsCommand: SlashCommand = {
|
|||||||
action: (context: CommandContext) => {
|
action: (context: CommandContext) => {
|
||||||
context.ui.addItem({
|
context.ui.addItem({
|
||||||
type: MessageType.TOOL_STATS,
|
type: MessageType.TOOL_STATS,
|
||||||
});
|
} as HistoryItemToolStats);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* 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 { AboutBox } from './AboutBox.js';
|
||||||
import { describe, it, expect, vi } from 'vitest';
|
import { describe, it, expect, vi } from 'vitest';
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ describe('AboutBox', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
it('renders with required props', () => {
|
it('renders with required props', () => {
|
||||||
const { lastFrame } = render(<AboutBox {...defaultProps} />);
|
const { lastFrame } = renderWithProviders(<AboutBox {...defaultProps} />);
|
||||||
const output = lastFrame();
|
const output = lastFrame();
|
||||||
expect(output).toContain('About Gemini CLI');
|
expect(output).toContain('About Gemini CLI');
|
||||||
expect(output).toContain('1.0.0');
|
expect(output).toContain('1.0.0');
|
||||||
@@ -42,7 +42,7 @@ describe('AboutBox', () => {
|
|||||||
['tier', 'Enterprise', 'Tier'],
|
['tier', 'Enterprise', 'Tier'],
|
||||||
])('renders optional prop %s', (prop, value, label) => {
|
])('renders optional prop %s', (prop, value, label) => {
|
||||||
const props = { ...defaultProps, [prop]: value };
|
const props = { ...defaultProps, [prop]: value };
|
||||||
const { lastFrame } = render(<AboutBox {...props} />);
|
const { lastFrame } = renderWithProviders(<AboutBox {...props} />);
|
||||||
const output = lastFrame();
|
const output = lastFrame();
|
||||||
expect(output).toContain(label);
|
expect(output).toContain(label);
|
||||||
expect(output).toContain(value);
|
expect(output).toContain(value);
|
||||||
@@ -50,14 +50,14 @@ describe('AboutBox', () => {
|
|||||||
|
|
||||||
it('renders Auth Method with email when userEmail is provided', () => {
|
it('renders Auth Method with email when userEmail is provided', () => {
|
||||||
const props = { ...defaultProps, userEmail: 'test@example.com' };
|
const props = { ...defaultProps, userEmail: 'test@example.com' };
|
||||||
const { lastFrame } = render(<AboutBox {...props} />);
|
const { lastFrame } = renderWithProviders(<AboutBox {...props} />);
|
||||||
const output = lastFrame();
|
const output = lastFrame();
|
||||||
expect(output).toContain('Logged in with Google (test@example.com)');
|
expect(output).toContain('Logged in with Google (test@example.com)');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders Auth Method correctly when not oauth', () => {
|
it('renders Auth Method correctly when not oauth', () => {
|
||||||
const props = { ...defaultProps, selectedAuthType: 'api-key' };
|
const props = { ...defaultProps, selectedAuthType: 'api-key' };
|
||||||
const { lastFrame } = render(<AboutBox {...props} />);
|
const { lastFrame } = renderWithProviders(<AboutBox {...props} />);
|
||||||
const output = lastFrame();
|
const output = lastFrame();
|
||||||
expect(output).toContain('api-key');
|
expect(output).toContain('api-key');
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import type React from 'react';
|
|||||||
import { Box, Text } from 'ink';
|
import { Box, Text } from 'ink';
|
||||||
import { theme } from '../semantic-colors.js';
|
import { theme } from '../semantic-colors.js';
|
||||||
import { GIT_COMMIT_INFO } from '../../generated/git-commit.js';
|
import { GIT_COMMIT_INFO } from '../../generated/git-commit.js';
|
||||||
|
import { useSettings } from '../contexts/SettingsContext.js';
|
||||||
|
|
||||||
interface AboutBoxProps {
|
interface AboutBoxProps {
|
||||||
cliVersion: string;
|
cliVersion: string;
|
||||||
@@ -31,123 +32,130 @@ export const AboutBox: React.FC<AboutBoxProps> = ({
|
|||||||
ideClient,
|
ideClient,
|
||||||
userEmail,
|
userEmail,
|
||||||
tier,
|
tier,
|
||||||
}) => (
|
}) => {
|
||||||
<Box
|
const settings = useSettings();
|
||||||
borderStyle="round"
|
const showUserIdentity = settings.merged.ui.showUserIdentity;
|
||||||
borderColor={theme.border.default}
|
|
||||||
flexDirection="column"
|
return (
|
||||||
padding={1}
|
<Box
|
||||||
marginY={1}
|
borderStyle="round"
|
||||||
width="100%"
|
borderColor={theme.border.default}
|
||||||
>
|
flexDirection="column"
|
||||||
<Box marginBottom={1}>
|
padding={1}
|
||||||
<Text bold color={theme.text.accent}>
|
marginY={1}
|
||||||
About Gemini CLI
|
width="100%"
|
||||||
</Text>
|
>
|
||||||
</Box>
|
<Box marginBottom={1}>
|
||||||
<Box flexDirection="row">
|
<Text bold color={theme.text.accent}>
|
||||||
<Box width="35%">
|
About Gemini CLI
|
||||||
<Text bold color={theme.text.link}>
|
|
||||||
CLI Version
|
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
|
||||||
<Text color={theme.text.primary}>{cliVersion}</Text>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
{GIT_COMMIT_INFO && !['N/A'].includes(GIT_COMMIT_INFO) && (
|
|
||||||
<Box flexDirection="row">
|
<Box flexDirection="row">
|
||||||
<Box width="35%">
|
<Box width="35%">
|
||||||
<Text bold color={theme.text.link}>
|
<Text bold color={theme.text.link}>
|
||||||
Git Commit
|
CLI Version
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<Text color={theme.text.primary}>{GIT_COMMIT_INFO}</Text>
|
<Text color={theme.text.primary}>{cliVersion}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
{GIT_COMMIT_INFO && !['N/A'].includes(GIT_COMMIT_INFO) && (
|
||||||
<Box flexDirection="row">
|
<Box flexDirection="row">
|
||||||
<Box width="35%">
|
<Box width="35%">
|
||||||
<Text bold color={theme.text.link}>
|
<Text bold color={theme.text.link}>
|
||||||
Model
|
Git Commit
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<Text color={theme.text.primary}>{modelVersion}</Text>
|
<Text color={theme.text.primary}>{GIT_COMMIT_INFO}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</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 && (
|
|
||||||
<Box flexDirection="row">
|
<Box flexDirection="row">
|
||||||
<Box width="35%">
|
<Box width="35%">
|
||||||
<Text bold color={theme.text.link}>
|
<Text bold color={theme.text.link}>
|
||||||
Tier
|
Model
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<Text color={theme.text.primary}>{tier}</Text>
|
<Text color={theme.text.primary}>{modelVersion}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
|
||||||
{gcpProject && (
|
|
||||||
<Box flexDirection="row">
|
<Box flexDirection="row">
|
||||||
<Box width="35%">
|
<Box width="35%">
|
||||||
<Text bold color={theme.text.link}>
|
<Text bold color={theme.text.link}>
|
||||||
GCP Project
|
Sandbox
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<Text color={theme.text.primary}>{gcpProject}</Text>
|
<Text color={theme.text.primary}>{sandboxEnv}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
|
||||||
{ideClient && (
|
|
||||||
<Box flexDirection="row">
|
<Box flexDirection="row">
|
||||||
<Box width="35%">
|
<Box width="35%">
|
||||||
<Text bold color={theme.text.link}>
|
<Text bold color={theme.text.link}>
|
||||||
IDE Client
|
OS
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
<Box>
|
<Box>
|
||||||
<Text color={theme.text.primary}>{ideClient}</Text>
|
<Text color={theme.text.primary}>{osVersion}</Text>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
{showUserIdentity && (
|
||||||
</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>
|
||||||
|
)}
|
||||||
|
{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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -122,9 +122,18 @@ export const HistoryItemDisplay: React.FC<HistoryItemDisplayProps> = ({
|
|||||||
<StatsDisplay
|
<StatsDisplay
|
||||||
duration={itemForDisplay.duration}
|
duration={itemForDisplay.duration}
|
||||||
quotas={itemForDisplay.quotas}
|
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 === 'tool_stats' && <ToolStatsDisplay />}
|
||||||
{itemForDisplay.type === 'model' && (
|
{itemForDisplay.type === 'model' && (
|
||||||
<ModelMessage model={itemForDisplay.model} />
|
<ModelMessage model={itemForDisplay.model} />
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ import { render } from '../../test-utils/render.js';
|
|||||||
import { describe, it, expect, vi, beforeAll, afterAll } from 'vitest';
|
import { describe, it, expect, vi, beforeAll, afterAll } from 'vitest';
|
||||||
import { ModelStatsDisplay } from './ModelStatsDisplay.js';
|
import { ModelStatsDisplay } from './ModelStatsDisplay.js';
|
||||||
import * as SessionContext from '../contexts/SessionContext.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 type { SessionMetrics } from '../contexts/SessionContext.js';
|
||||||
import { ToolCallDecision } from '@google/gemini-cli-core';
|
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 useSessionStatsMock = vi.mocked(SessionContext.useSessionStats);
|
||||||
|
const useSettingsMock = vi.mocked(SettingsContext.useSettings);
|
||||||
|
|
||||||
const renderWithMockedStats = (metrics: SessionMetrics, width?: number) => {
|
const renderWithMockedStats = (metrics: SessionMetrics, width?: number) => {
|
||||||
useSessionStatsMock.mockReturnValue({
|
useSessionStatsMock.mockReturnValue({
|
||||||
@@ -36,6 +47,14 @@ const renderWithMockedStats = (metrics: SessionMetrics, width?: number) => {
|
|||||||
startNewPrompt: vi.fn(),
|
startNewPrompt: vi.fn(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
useSettingsMock.mockReturnValue({
|
||||||
|
merged: {
|
||||||
|
ui: {
|
||||||
|
showUserIdentity: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as unknown as LoadedSettings);
|
||||||
|
|
||||||
return render(<ModelStatsDisplay />, width);
|
return render(<ModelStatsDisplay />, width);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -368,4 +387,74 @@ describe('<ModelStatsDisplay />', () => {
|
|||||||
expect(output).toContain('gemini-3-flash-');
|
expect(output).toContain('gemini-3-flash-');
|
||||||
expect(output).toMatchSnapshot();
|
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');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import {
|
|||||||
} from '../utils/computeStats.js';
|
} from '../utils/computeStats.js';
|
||||||
import { useSessionStats } from '../contexts/SessionContext.js';
|
import { useSessionStats } from '../contexts/SessionContext.js';
|
||||||
import { Table, type Column } from './Table.js';
|
import { Table, type Column } from './Table.js';
|
||||||
|
import { useSettings } from '../contexts/SettingsContext.js';
|
||||||
|
|
||||||
interface StatRowData {
|
interface StatRowData {
|
||||||
metric: string;
|
metric: string;
|
||||||
@@ -24,9 +25,21 @@ interface StatRowData {
|
|||||||
[key: string]: string | React.ReactNode | boolean | undefined;
|
[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 { stats } = useSessionStats();
|
||||||
const { models } = stats.metrics;
|
const { models } = stats.metrics;
|
||||||
|
const settings = useSettings();
|
||||||
|
const showUserIdentity = settings.merged.ui.showUserIdentity;
|
||||||
const activeModels = Object.entries(models).filter(
|
const activeModels = Object.entries(models).filter(
|
||||||
([, metrics]) => metrics.api.totalRequests > 0,
|
([, metrics]) => metrics.api.totalRequests > 0,
|
||||||
);
|
);
|
||||||
@@ -75,10 +88,12 @@ export const ModelStatsDisplay: React.FC = () => {
|
|||||||
return row;
|
return row;
|
||||||
};
|
};
|
||||||
|
|
||||||
const rows: StatRowData[] = [
|
const rows: StatRowData[] = [];
|
||||||
// API Section
|
|
||||||
{ metric: 'API', isSection: true },
|
// API Section
|
||||||
createRow('Requests', (m) => m.api.totalRequests.toLocaleString()),
|
rows.push({ metric: 'API', isSection: true });
|
||||||
|
rows.push(createRow('Requests', (m) => m.api.totalRequests.toLocaleString()));
|
||||||
|
rows.push(
|
||||||
createRow('Errors', (m) => {
|
createRow('Errors', (m) => {
|
||||||
const errorRate = calculateErrorRate(m);
|
const errorRate = calculateErrorRate(m);
|
||||||
return (
|
return (
|
||||||
@@ -91,18 +106,24 @@ export const ModelStatsDisplay: React.FC = () => {
|
|||||||
</Text>
|
</Text>
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
|
);
|
||||||
|
rows.push(
|
||||||
createRow('Avg Latency', (m) => formatDuration(calculateAverageLatency(m))),
|
createRow('Avg Latency', (m) => formatDuration(calculateAverageLatency(m))),
|
||||||
|
);
|
||||||
|
|
||||||
// Spacer
|
// Spacer
|
||||||
{ metric: '' },
|
rows.push({ metric: '' });
|
||||||
|
|
||||||
// Tokens Section
|
// Tokens Section
|
||||||
{ metric: 'Tokens', isSection: true },
|
rows.push({ metric: 'Tokens', isSection: true });
|
||||||
|
rows.push(
|
||||||
createRow('Total', (m) => (
|
createRow('Total', (m) => (
|
||||||
<Text color={theme.text.secondary}>
|
<Text color={theme.text.secondary}>
|
||||||
{m.tokens.total.toLocaleString()}
|
{m.tokens.total.toLocaleString()}
|
||||||
</Text>
|
</Text>
|
||||||
)),
|
)),
|
||||||
|
);
|
||||||
|
rows.push(
|
||||||
createRow(
|
createRow(
|
||||||
'Input',
|
'Input',
|
||||||
(m) => (
|
(m) => (
|
||||||
@@ -112,7 +133,7 @@ export const ModelStatsDisplay: React.FC = () => {
|
|||||||
),
|
),
|
||||||
{ isSubtle: true },
|
{ isSubtle: true },
|
||||||
),
|
),
|
||||||
];
|
);
|
||||||
|
|
||||||
if (hasCached) {
|
if (hasCached) {
|
||||||
rows.push(
|
rows.push(
|
||||||
@@ -214,6 +235,31 @@ export const ModelStatsDisplay: React.FC = () => {
|
|||||||
Model Stats For Nerds
|
Model Stats For Nerds
|
||||||
</Text>
|
</Text>
|
||||||
<Box height={1} />
|
<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} />
|
<Table data={rows} columns={columns} />
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* 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 { describe, it, expect, vi } from 'vitest';
|
||||||
import { SessionSummaryDisplay } from './SessionSummaryDisplay.js';
|
import { SessionSummaryDisplay } from './SessionSummaryDisplay.js';
|
||||||
import * as SessionContext from '../contexts/SessionContext.js';
|
import * as SessionContext from '../contexts/SessionContext.js';
|
||||||
@@ -35,7 +35,9 @@ const renderWithMockedStats = (metrics: SessionMetrics) => {
|
|||||||
startNewPrompt: vi.fn(),
|
startNewPrompt: vi.fn(),
|
||||||
});
|
});
|
||||||
|
|
||||||
return render(<SessionSummaryDisplay duration="1h 23m 45s" />);
|
return renderWithProviders(<SessionSummaryDisplay duration="1h 23m 45s" />, {
|
||||||
|
width: 100,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('<SessionSummaryDisplay />', () => {
|
describe('<SessionSummaryDisplay />', () => {
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
* SPDX-License-Identifier: Apache-2.0
|
* 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 { describe, it, expect, vi } from 'vitest';
|
||||||
import { StatsDisplay } from './StatsDisplay.js';
|
import { StatsDisplay } from './StatsDisplay.js';
|
||||||
import * as SessionContext from '../contexts/SessionContext.js';
|
import * as SessionContext from '../contexts/SessionContext.js';
|
||||||
@@ -39,7 +39,7 @@ const renderWithMockedStats = (metrics: SessionMetrics) => {
|
|||||||
startNewPrompt: vi.fn(),
|
startNewPrompt: vi.fn(),
|
||||||
});
|
});
|
||||||
|
|
||||||
return render(<StatsDisplay duration="1s" />);
|
return renderWithProviders(<StatsDisplay duration="1s" />, { width: 100 });
|
||||||
};
|
};
|
||||||
|
|
||||||
// Helper to create metrics with default zero values
|
// Helper to create metrics with default zero values
|
||||||
@@ -381,8 +381,9 @@ describe('<StatsDisplay />', () => {
|
|||||||
startNewPrompt: vi.fn(),
|
startNewPrompt: vi.fn(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { lastFrame } = render(
|
const { lastFrame } = renderWithProviders(
|
||||||
<StatsDisplay duration="1s" title="Agent powering down. Goodbye!" />,
|
<StatsDisplay duration="1s" title="Agent powering down. Goodbye!" />,
|
||||||
|
{ width: 100 },
|
||||||
);
|
);
|
||||||
const output = lastFrame();
|
const output = lastFrame();
|
||||||
expect(output).toContain('Agent powering down. Goodbye!');
|
expect(output).toContain('Agent powering down. Goodbye!');
|
||||||
@@ -439,8 +440,9 @@ describe('<StatsDisplay />', () => {
|
|||||||
startNewPrompt: vi.fn(),
|
startNewPrompt: vi.fn(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { lastFrame } = render(
|
const { lastFrame } = renderWithProviders(
|
||||||
<StatsDisplay duration="1s" quotas={quotas} />,
|
<StatsDisplay duration="1s" quotas={quotas} />,
|
||||||
|
{ width: 100 },
|
||||||
);
|
);
|
||||||
const output = lastFrame();
|
const output = lastFrame();
|
||||||
|
|
||||||
@@ -484,8 +486,9 @@ describe('<StatsDisplay />', () => {
|
|||||||
startNewPrompt: vi.fn(),
|
startNewPrompt: vi.fn(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const { lastFrame } = render(
|
const { lastFrame } = renderWithProviders(
|
||||||
<StatsDisplay duration="1s" quotas={quotas} />,
|
<StatsDisplay duration="1s" quotas={quotas} />,
|
||||||
|
{ width: 100 },
|
||||||
);
|
);
|
||||||
const output = lastFrame();
|
const output = lastFrame();
|
||||||
|
|
||||||
@@ -498,4 +501,64 @@ describe('<StatsDisplay />', () => {
|
|||||||
vi.useRealTimers();
|
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:');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import {
|
|||||||
type RetrieveUserQuotaResponse,
|
type RetrieveUserQuotaResponse,
|
||||||
VALID_GEMINI_MODELS,
|
VALID_GEMINI_MODELS,
|
||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
|
import { useSettings } from '../contexts/SettingsContext.js';
|
||||||
|
|
||||||
// A more flexible and powerful StatRow component
|
// A more flexible and powerful StatRow component
|
||||||
interface StatRowProps {
|
interface StatRowProps {
|
||||||
@@ -364,17 +365,25 @@ interface StatsDisplayProps {
|
|||||||
duration: string;
|
duration: string;
|
||||||
title?: string;
|
title?: string;
|
||||||
quotas?: RetrieveUserQuotaResponse;
|
quotas?: RetrieveUserQuotaResponse;
|
||||||
|
selectedAuthType?: string;
|
||||||
|
userEmail?: string;
|
||||||
|
tier?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StatsDisplay: React.FC<StatsDisplayProps> = ({
|
export const StatsDisplay: React.FC<StatsDisplayProps> = ({
|
||||||
duration,
|
duration,
|
||||||
title,
|
title,
|
||||||
quotas,
|
quotas,
|
||||||
|
selectedAuthType,
|
||||||
|
userEmail,
|
||||||
|
tier,
|
||||||
}) => {
|
}) => {
|
||||||
const { stats } = useSessionStats();
|
const { stats } = useSessionStats();
|
||||||
const { metrics } = stats;
|
const { metrics } = stats;
|
||||||
const { models, tools, files } = metrics;
|
const { models, tools, files } = metrics;
|
||||||
const computed = computeSessionStats(metrics);
|
const computed = computeSessionStats(metrics);
|
||||||
|
const settings = useSettings();
|
||||||
|
const showUserIdentity = settings.merged.ui.showUserIdentity;
|
||||||
|
|
||||||
const successThresholds = {
|
const successThresholds = {
|
||||||
green: TOOL_SUCCESS_RATE_HIGH,
|
green: TOOL_SUCCESS_RATE_HIGH,
|
||||||
@@ -417,6 +426,22 @@ export const StatsDisplay: React.FC<StatsDisplayProps> = ({
|
|||||||
<StatRow title="Session ID:">
|
<StatRow title="Session ID:">
|
||||||
<Text color={theme.text.primary}>{stats.sessionId}</Text>
|
<Text color={theme.text.primary}>{stats.sessionId}</Text>
|
||||||
</StatRow>
|
</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:">
|
<StatRow title="Tool Calls:">
|
||||||
<Text color={theme.text.primary}>
|
<Text color={theme.text.primary}>
|
||||||
{tools.totalCalls} ({' '}
|
{tools.totalCalls} ({' '}
|
||||||
|
|||||||
@@ -157,10 +157,16 @@ export type HistoryItemStats = HistoryItemBase & {
|
|||||||
type: 'stats';
|
type: 'stats';
|
||||||
duration: string;
|
duration: string;
|
||||||
quotas?: RetrieveUserQuotaResponse;
|
quotas?: RetrieveUserQuotaResponse;
|
||||||
|
selectedAuthType?: string;
|
||||||
|
userEmail?: string;
|
||||||
|
tier?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type HistoryItemModelStats = HistoryItemBase & {
|
export type HistoryItemModelStats = HistoryItemBase & {
|
||||||
type: 'model_stats';
|
type: 'model_stats';
|
||||||
|
selectedAuthType?: string;
|
||||||
|
userEmail?: string;
|
||||||
|
tier?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type HistoryItemToolStats = HistoryItemBase & {
|
export type HistoryItemToolStats = HistoryItemBase & {
|
||||||
|
|||||||
@@ -302,6 +302,13 @@
|
|||||||
"default": false,
|
"default": false,
|
||||||
"type": "boolean"
|
"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": {
|
"useAlternateBuffer": {
|
||||||
"title": "Use Alternate Screen Buffer",
|
"title": "Use Alternate Screen Buffer",
|
||||||
"description": "Use an alternate screen buffer for the UI, preserving shell history.",
|
"description": "Use an alternate screen buffer for the UI, preserving shell history.",
|
||||||
|
|||||||
Reference in New Issue
Block a user