mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-02-01 22:48:03 +00:00
Merge branch 'main' into ntm/sys.prompt.overhaul
This commit is contained in:
@@ -33,8 +33,8 @@
|
||||
a natural tone.
|
||||
- **Simple vocabulary:** Use common words. Define technical terms when
|
||||
necessary.
|
||||
- **Conciseness:** Keep sentences short and focused, but don't omit
|
||||
helpful information.
|
||||
- **Conciseness:** Keep sentences short and focused, but don't omit helpful
|
||||
information.
|
||||
- **"Please":** Avoid using the word "please."
|
||||
|
||||
## IV. Procedures and steps
|
||||
|
||||
@@ -288,12 +288,12 @@ please see the dedicated [Custom Commands documentation](./custom-commands.md).
|
||||
These shortcuts apply directly to the input prompt for text manipulation.
|
||||
|
||||
- **Undo:**
|
||||
- **Keyboard shortcut:** Press **Ctrl+z** to undo the last action in the input
|
||||
prompt.
|
||||
- **Keyboard shortcut:** Press **Cmd+z** or **Alt+z** to undo the last action
|
||||
in the input prompt.
|
||||
|
||||
- **Redo:**
|
||||
- **Keyboard shortcut:** Press **Ctrl+Shift+Z** to redo the last undone action
|
||||
in the input prompt.
|
||||
- **Keyboard shortcut:** Press **Shift+Cmd+Z** or **Shift+Alt+Z** to redo the
|
||||
last undone action in the input prompt.
|
||||
|
||||
## At commands (`@`)
|
||||
|
||||
|
||||
@@ -30,17 +30,17 @@ available combinations.
|
||||
|
||||
#### Editing
|
||||
|
||||
| Action | Keys |
|
||||
| ------------------------------------------------ | --------------------------------------------------------- |
|
||||
| Delete from the cursor to the end of the line. | `Ctrl + K` |
|
||||
| Delete from the cursor to the start of the line. | `Ctrl + U` |
|
||||
| Clear all text in the input field. | `Ctrl + C` |
|
||||
| Delete the previous word. | `Ctrl + Backspace`<br />`Alt + Backspace`<br />`Ctrl + W` |
|
||||
| Delete the next word. | `Ctrl + Delete`<br />`Alt + Delete` |
|
||||
| Delete the character to the left. | `Backspace`<br />`Ctrl + H` |
|
||||
| Delete the character to the right. | `Delete`<br />`Ctrl + D` |
|
||||
| Undo the most recent text edit. | `Ctrl + Z (no Shift)` |
|
||||
| Redo the most recent undone text edit. | `Shift + Ctrl + Z` |
|
||||
| Action | Keys |
|
||||
| ------------------------------------------------ | ---------------------------------------------------------------- |
|
||||
| Delete from the cursor to the end of the line. | `Ctrl + K` |
|
||||
| Delete from the cursor to the start of the line. | `Ctrl + U` |
|
||||
| Clear all text in the input field. | `Ctrl + C` |
|
||||
| Delete the previous word. | `Ctrl + Backspace`<br />`Alt + Backspace`<br />`Ctrl + W` |
|
||||
| Delete the next word. | `Ctrl + Delete`<br />`Alt + Delete` |
|
||||
| Delete the character to the left. | `Backspace`<br />`Ctrl + H` |
|
||||
| Delete the character to the right. | `Delete`<br />`Ctrl + D` |
|
||||
| Undo the most recent text edit. | `Cmd + Z (no Shift)`<br />`Alt + Z (no Shift)` |
|
||||
| Redo the most recent undone text edit. | `Shift + Ctrl + Z`<br />`Shift + Cmd + Z`<br />`Shift + Alt + Z` |
|
||||
|
||||
#### Scrolling
|
||||
|
||||
@@ -110,6 +110,7 @@ available combinations.
|
||||
| Focus the Gemini input from the shell input. | `Tab` |
|
||||
| Clear the terminal screen and redraw the UI. | `Ctrl + L` |
|
||||
| Restart the application. | `R` |
|
||||
| Suspend the application (not yet implemented). | `Ctrl + Z` |
|
||||
|
||||
<!-- KEYBINDINGS-AUTOGEN:END -->
|
||||
|
||||
|
||||
@@ -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` |
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -104,7 +104,7 @@ export function evalTest(policy: EvalPolicy, evalCase: EvalCase) {
|
||||
}
|
||||
|
||||
async function prepareLogDir(name: string) {
|
||||
const logDir = 'evals/logs';
|
||||
const logDir = path.resolve(process.cwd(), 'evals/logs');
|
||||
await fs.promises.mkdir(logDir, { recursive: true });
|
||||
const sanitizedName = name.replace(/[^a-z0-9]/gi, '_').toLowerCase();
|
||||
return { logDir, sanitizedName };
|
||||
|
||||
@@ -85,6 +85,7 @@ export enum Command {
|
||||
UNFOCUS_SHELL_INPUT = 'app.unfocusShellInput',
|
||||
CLEAR_SCREEN = 'app.clearScreen',
|
||||
RESTART_APP = 'app.restart',
|
||||
SUSPEND_APP = 'app.suspend',
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -170,8 +171,15 @@ export const defaultKeyBindings: KeyBindingConfig = {
|
||||
],
|
||||
[Command.DELETE_CHAR_LEFT]: [{ key: 'backspace' }, { key: 'h', ctrl: true }],
|
||||
[Command.DELETE_CHAR_RIGHT]: [{ key: 'delete' }, { key: 'd', ctrl: true }],
|
||||
[Command.UNDO]: [{ key: 'z', shift: false, ctrl: true }],
|
||||
[Command.REDO]: [{ key: 'z', shift: true, ctrl: true }],
|
||||
[Command.UNDO]: [
|
||||
{ key: 'z', cmd: true, shift: false },
|
||||
{ key: 'z', alt: true, shift: false },
|
||||
],
|
||||
[Command.REDO]: [
|
||||
{ key: 'z', ctrl: true, shift: true },
|
||||
{ key: 'z', cmd: true, shift: true },
|
||||
{ key: 'z', alt: true, shift: true },
|
||||
],
|
||||
|
||||
// Scrolling
|
||||
[Command.SCROLL_UP]: [{ key: 'up', shift: true }],
|
||||
@@ -265,6 +273,7 @@ export const defaultKeyBindings: KeyBindingConfig = {
|
||||
[Command.UNFOCUS_SHELL_INPUT]: [{ key: 'tab' }],
|
||||
[Command.CLEAR_SCREEN]: [{ key: 'l', ctrl: true }],
|
||||
[Command.RESTART_APP]: [{ key: 'r' }],
|
||||
[Command.SUSPEND_APP]: [{ key: 'z', ctrl: true }],
|
||||
};
|
||||
|
||||
interface CommandCategory {
|
||||
@@ -374,6 +383,7 @@ export const commandCategories: readonly CommandCategory[] = [
|
||||
Command.UNFOCUS_SHELL_INPUT,
|
||||
Command.CLEAR_SCREEN,
|
||||
Command.RESTART_APP,
|
||||
Command.SUSPEND_APP,
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -464,4 +474,5 @@ export const commandDescriptions: Readonly<Record<Command, string>> = {
|
||||
[Command.UNFOCUS_SHELL_INPUT]: 'Focus the Gemini input from the shell input.',
|
||||
[Command.CLEAR_SCREEN]: 'Clear the terminal screen and redraw the UI.',
|
||||
[Command.RESTART_APP]: 'Restart the application.',
|
||||
[Command.SUSPEND_APP]: 'Suspend the application (not yet implemented).',
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -193,6 +193,7 @@ import {
|
||||
disableMouseEvents,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { type ExtensionManager } from '../config/extension-manager.js';
|
||||
import { WARNING_PROMPT_DURATION_MS } from './constants.js';
|
||||
|
||||
describe('AppContainer State Management', () => {
|
||||
let mockConfig: Config;
|
||||
@@ -1900,7 +1901,7 @@ describe('AppContainer State Management', () => {
|
||||
|
||||
// Advance timer past the reset threshold
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(1001);
|
||||
vi.advanceTimersByTime(WARNING_PROMPT_DURATION_MS + 1);
|
||||
});
|
||||
|
||||
pressKey({ name: 'c', ctrl: true });
|
||||
@@ -1945,7 +1946,7 @@ describe('AppContainer State Management', () => {
|
||||
|
||||
// Advance timer past the reset threshold
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(1001);
|
||||
vi.advanceTimersByTime(WARNING_PROMPT_DURATION_MS + 1);
|
||||
});
|
||||
|
||||
pressKey({ name: 'd', ctrl: true });
|
||||
|
||||
@@ -1443,6 +1443,9 @@ Logging in with Google... Restarting Gemini CLI to continue.
|
||||
if (keyMatchers[Command.SHOW_ERROR_DETAILS](key)) {
|
||||
setShowErrorDetails((prev) => !prev);
|
||||
return true;
|
||||
} else if (keyMatchers[Command.SUSPEND_APP](key)) {
|
||||
handleWarning('Undo has been moved to Cmd + Z or Alt/Opt + Z');
|
||||
return true;
|
||||
} else if (keyMatchers[Command.SHOW_FULL_TODOS](key)) {
|
||||
setShowFullTodos((prev) => !prev);
|
||||
return true;
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 />', () => {
|
||||
|
||||
@@ -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:');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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} ({' '}
|
||||
|
||||
@@ -137,6 +137,56 @@ describe('ToolConfirmationQueue', () => {
|
||||
expect(lastFrame()).toContain('Press ctrl-o to show more lines');
|
||||
});
|
||||
|
||||
it('calculates availableContentHeight based on availableTerminalHeight from UI state', async () => {
|
||||
const longDiff = '@@ -1,1 +1,50 @@\n' + '+line\n'.repeat(50);
|
||||
const confirmingTool = {
|
||||
tool: {
|
||||
callId: 'call-1',
|
||||
name: 'replace',
|
||||
description: 'edit file',
|
||||
status: ToolCallStatus.Confirming,
|
||||
confirmationDetails: {
|
||||
type: 'edit' as const,
|
||||
title: 'Confirm edit',
|
||||
fileName: 'test.ts',
|
||||
filePath: '/test.ts',
|
||||
fileDiff: longDiff,
|
||||
originalContent: 'old',
|
||||
newContent: 'new',
|
||||
onConfirm: vi.fn(),
|
||||
},
|
||||
},
|
||||
index: 1,
|
||||
total: 1,
|
||||
};
|
||||
|
||||
// Use a small availableTerminalHeight to force truncation
|
||||
const { lastFrame } = renderWithProviders(
|
||||
<ToolConfirmationQueue
|
||||
confirmingTool={confirmingTool as unknown as ConfirmingToolState}
|
||||
/>,
|
||||
{
|
||||
config: mockConfig,
|
||||
useAlternateBuffer: false,
|
||||
uiState: {
|
||||
terminalWidth: 80,
|
||||
terminalHeight: 40,
|
||||
availableTerminalHeight: 10,
|
||||
constrainHeight: true,
|
||||
streamingState: StreamingState.WaitingForConfirmation,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// With availableTerminalHeight = 10:
|
||||
// maxHeight = Math.max(10 - 1, 4) = 9
|
||||
// availableContentHeight = Math.max(9 - 6, 4) = 4
|
||||
// MaxSizedBox in ToolConfirmationMessage will use 4
|
||||
// It should show truncation message
|
||||
await waitFor(() => expect(lastFrame()).toContain('first 49 lines hidden'));
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('does not render expansion hint when constrainHeight is false', () => {
|
||||
const longDiff = 'line\n'.repeat(50);
|
||||
const confirmingTool = {
|
||||
|
||||
@@ -26,17 +26,23 @@ export const ToolConfirmationQueue: React.FC<ToolConfirmationQueueProps> = ({
|
||||
}) => {
|
||||
const config = useConfig();
|
||||
const isAlternateBuffer = useAlternateBuffer();
|
||||
const { mainAreaWidth, terminalHeight, constrainHeight } = useUIState();
|
||||
const {
|
||||
mainAreaWidth,
|
||||
terminalHeight,
|
||||
constrainHeight,
|
||||
availableTerminalHeight: uiAvailableHeight,
|
||||
} = useUIState();
|
||||
const { tool, index, total } = confirmingTool;
|
||||
|
||||
// Safety check: ToolConfirmationMessage requires confirmationDetails
|
||||
if (!tool.confirmationDetails) return null;
|
||||
|
||||
// V1: Constrain the queue to at most 50% of the terminal height to ensure
|
||||
// some history is always visible and to prevent flickering.
|
||||
// We pass this to ToolConfirmationMessage so it can calculate internal
|
||||
// truncation while keeping buttons visible.
|
||||
const maxHeight = Math.floor(terminalHeight * 0.5);
|
||||
// Render up to 100% of the available terminal height (minus 1 line for safety)
|
||||
// to maximize space for diffs and other content.
|
||||
const maxHeight =
|
||||
uiAvailableHeight !== undefined
|
||||
? Math.max(uiAvailableHeight - 1, 4)
|
||||
: Math.floor(terminalHeight * 0.5);
|
||||
|
||||
// ToolConfirmationMessage needs to know the height available for its OWN content.
|
||||
// We subtract the lines used by the Queue wrapper:
|
||||
|
||||
@@ -413,371 +413,3 @@ exports[`SettingsDialog > Snapshot Tests > should render 'various boolean settin
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`SettingsDialog > Snapshot Tests > should render accessibility settings enabled correctly 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
│ > Settings │
|
||||
│ │
|
||||
│ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │
|
||||
│ │ Search to filter │ │
|
||||
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
|
||||
│ │
|
||||
│ ▲ │
|
||||
│ ● Preview Features (e.g., models) false │
|
||||
│ Enable preview features (e.g., preview models). │
|
||||
│ │
|
||||
│ Vim Mode false │
|
||||
│ Enable Vim keybindings │
|
||||
│ │
|
||||
│ Enable Auto Update true │
|
||||
│ Enable automatic updates. │
|
||||
│ │
|
||||
│ Enable Prompt Completion false │
|
||||
│ Enable AI-powered prompt completion suggestions while typing. │
|
||||
│ │
|
||||
│ Debug Keystroke Logging false │
|
||||
│ Enable debug logging of keystrokes to the console. │
|
||||
│ │
|
||||
│ Enable Session Cleanup false │
|
||||
│ Enable automatic session cleanup │
|
||||
│ │
|
||||
│ Output Format Text │
|
||||
│ The format of the CLI output. Can be \`text\` or \`json\`. │
|
||||
│ │
|
||||
│ Hide Window Title false │
|
||||
│ Hide the window title bar │
|
||||
│ │
|
||||
│ ▼ │
|
||||
│ │
|
||||
│ Apply To │
|
||||
│ ● User Settings │
|
||||
│ Workspace Settings │
|
||||
│ System Settings │
|
||||
│ │
|
||||
│ (Use Enter to select, Tab to change focus, Esc to close) │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`SettingsDialog > Snapshot Tests > should render all boolean settings disabled correctly 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
│ > Settings │
|
||||
│ │
|
||||
│ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │
|
||||
│ │ Search to filter │ │
|
||||
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
|
||||
│ │
|
||||
│ ▲ │
|
||||
│ ● Preview Features (e.g., models) false* │
|
||||
│ Enable preview features (e.g., preview models). │
|
||||
│ │
|
||||
│ Vim Mode false* │
|
||||
│ Enable Vim keybindings │
|
||||
│ │
|
||||
│ Enable Auto Update false* │
|
||||
│ Enable automatic updates. │
|
||||
│ │
|
||||
│ Enable Prompt Completion false │
|
||||
│ Enable AI-powered prompt completion suggestions while typing. │
|
||||
│ │
|
||||
│ Debug Keystroke Logging false │
|
||||
│ Enable debug logging of keystrokes to the console. │
|
||||
│ │
|
||||
│ Enable Session Cleanup false │
|
||||
│ Enable automatic session cleanup │
|
||||
│ │
|
||||
│ Output Format Text │
|
||||
│ The format of the CLI output. Can be \`text\` or \`json\`. │
|
||||
│ │
|
||||
│ Hide Window Title false │
|
||||
│ Hide the window title bar │
|
||||
│ │
|
||||
│ ▼ │
|
||||
│ │
|
||||
│ Apply To │
|
||||
│ ● User Settings │
|
||||
│ Workspace Settings │
|
||||
│ System Settings │
|
||||
│ │
|
||||
│ (Use Enter to select, Tab to change focus, Esc to close) │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`SettingsDialog > Snapshot Tests > should render default state correctly 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
│ > Settings │
|
||||
│ │
|
||||
│ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │
|
||||
│ │ Search to filter │ │
|
||||
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
|
||||
│ │
|
||||
│ ▲ │
|
||||
│ ● Preview Features (e.g., models) false │
|
||||
│ Enable preview features (e.g., preview models). │
|
||||
│ │
|
||||
│ Vim Mode false │
|
||||
│ Enable Vim keybindings │
|
||||
│ │
|
||||
│ Enable Auto Update true │
|
||||
│ Enable automatic updates. │
|
||||
│ │
|
||||
│ Enable Prompt Completion false │
|
||||
│ Enable AI-powered prompt completion suggestions while typing. │
|
||||
│ │
|
||||
│ Debug Keystroke Logging false │
|
||||
│ Enable debug logging of keystrokes to the console. │
|
||||
│ │
|
||||
│ Enable Session Cleanup false │
|
||||
│ Enable automatic session cleanup │
|
||||
│ │
|
||||
│ Output Format Text │
|
||||
│ The format of the CLI output. Can be \`text\` or \`json\`. │
|
||||
│ │
|
||||
│ Hide Window Title false │
|
||||
│ Hide the window title bar │
|
||||
│ │
|
||||
│ ▼ │
|
||||
│ │
|
||||
│ Apply To │
|
||||
│ ● User Settings │
|
||||
│ Workspace Settings │
|
||||
│ System Settings │
|
||||
│ │
|
||||
│ (Use Enter to select, Tab to change focus, Esc to close) │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`SettingsDialog > Snapshot Tests > should render file filtering settings configured correctly 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
│ > Settings │
|
||||
│ │
|
||||
│ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │
|
||||
│ │ Search to filter │ │
|
||||
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
|
||||
│ │
|
||||
│ ▲ │
|
||||
│ ● Preview Features (e.g., models) false │
|
||||
│ Enable preview features (e.g., preview models). │
|
||||
│ │
|
||||
│ Vim Mode false │
|
||||
│ Enable Vim keybindings │
|
||||
│ │
|
||||
│ Enable Auto Update true │
|
||||
│ Enable automatic updates. │
|
||||
│ │
|
||||
│ Enable Prompt Completion false │
|
||||
│ Enable AI-powered prompt completion suggestions while typing. │
|
||||
│ │
|
||||
│ Debug Keystroke Logging false │
|
||||
│ Enable debug logging of keystrokes to the console. │
|
||||
│ │
|
||||
│ Enable Session Cleanup false │
|
||||
│ Enable automatic session cleanup │
|
||||
│ │
|
||||
│ Output Format Text │
|
||||
│ The format of the CLI output. Can be \`text\` or \`json\`. │
|
||||
│ │
|
||||
│ Hide Window Title false │
|
||||
│ Hide the window title bar │
|
||||
│ │
|
||||
│ ▼ │
|
||||
│ │
|
||||
│ Apply To │
|
||||
│ ● User Settings │
|
||||
│ Workspace Settings │
|
||||
│ System Settings │
|
||||
│ │
|
||||
│ (Use Enter to select, Tab to change focus, Esc to close) │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`SettingsDialog > Snapshot Tests > should render focused on scope selector correctly 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
│ Settings │
|
||||
│ │
|
||||
│ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │
|
||||
│ │ Search to filter │ │
|
||||
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
|
||||
│ │
|
||||
│ ▲ │
|
||||
│ Preview Features (e.g., models) false │
|
||||
│ Enable preview features (e.g., preview models). │
|
||||
│ │
|
||||
│ Vim Mode false │
|
||||
│ Enable Vim keybindings │
|
||||
│ │
|
||||
│ Enable Auto Update true │
|
||||
│ Enable automatic updates. │
|
||||
│ │
|
||||
│ Enable Prompt Completion false │
|
||||
│ Enable AI-powered prompt completion suggestions while typing. │
|
||||
│ │
|
||||
│ Debug Keystroke Logging false │
|
||||
│ Enable debug logging of keystrokes to the console. │
|
||||
│ │
|
||||
│ Enable Session Cleanup false │
|
||||
│ Enable automatic session cleanup │
|
||||
│ │
|
||||
│ Output Format Text │
|
||||
│ The format of the CLI output. Can be \`text\` or \`json\`. │
|
||||
│ │
|
||||
│ Hide Window Title false │
|
||||
│ Hide the window title bar │
|
||||
│ │
|
||||
│ ▼ │
|
||||
│ │
|
||||
│ > Apply To │
|
||||
│ ● 1. User Settings │
|
||||
│ 2. Workspace Settings │
|
||||
│ 3. System Settings │
|
||||
│ │
|
||||
│ (Use Enter to select, Tab to change focus, Esc to close) │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`SettingsDialog > Snapshot Tests > should render mixed boolean and number settings correctly 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
│ > Settings │
|
||||
│ │
|
||||
│ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │
|
||||
│ │ Search to filter │ │
|
||||
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
|
||||
│ │
|
||||
│ ▲ │
|
||||
│ ● Preview Features (e.g., models) false │
|
||||
│ Enable preview features (e.g., preview models). │
|
||||
│ │
|
||||
│ Vim Mode true* │
|
||||
│ Enable Vim keybindings │
|
||||
│ │
|
||||
│ Enable Auto Update true │
|
||||
│ Enable automatic updates. │
|
||||
│ │
|
||||
│ Enable Prompt Completion false │
|
||||
│ Enable AI-powered prompt completion suggestions while typing. │
|
||||
│ │
|
||||
│ Debug Keystroke Logging false │
|
||||
│ Enable debug logging of keystrokes to the console. │
|
||||
│ │
|
||||
│ Enable Session Cleanup false │
|
||||
│ Enable automatic session cleanup │
|
||||
│ │
|
||||
│ Output Format Text │
|
||||
│ The format of the CLI output. Can be \`text\` or \`json\`. │
|
||||
│ │
|
||||
│ Hide Window Title false │
|
||||
│ Hide the window title bar │
|
||||
│ │
|
||||
│ ▼ │
|
||||
│ │
|
||||
│ Apply To │
|
||||
│ ● User Settings │
|
||||
│ Workspace Settings │
|
||||
│ System Settings │
|
||||
│ │
|
||||
│ (Use Enter to select, Tab to change focus, Esc to close) │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`SettingsDialog > Snapshot Tests > should render tools and security settings correctly 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
│ > Settings │
|
||||
│ │
|
||||
│ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │
|
||||
│ │ Search to filter │ │
|
||||
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
|
||||
│ │
|
||||
│ ▲ │
|
||||
│ ● Preview Features (e.g., models) false │
|
||||
│ Enable preview features (e.g., preview models). │
|
||||
│ │
|
||||
│ Vim Mode false │
|
||||
│ Enable Vim keybindings │
|
||||
│ │
|
||||
│ Enable Auto Update true │
|
||||
│ Enable automatic updates. │
|
||||
│ │
|
||||
│ Enable Prompt Completion false │
|
||||
│ Enable AI-powered prompt completion suggestions while typing. │
|
||||
│ │
|
||||
│ Debug Keystroke Logging false │
|
||||
│ Enable debug logging of keystrokes to the console. │
|
||||
│ │
|
||||
│ Enable Session Cleanup false │
|
||||
│ Enable automatic session cleanup │
|
||||
│ │
|
||||
│ Output Format Text │
|
||||
│ The format of the CLI output. Can be \`text\` or \`json\`. │
|
||||
│ │
|
||||
│ Hide Window Title false │
|
||||
│ Hide the window title bar │
|
||||
│ │
|
||||
│ ▼ │
|
||||
│ │
|
||||
│ Apply To │
|
||||
│ ● User Settings │
|
||||
│ Workspace Settings │
|
||||
│ System Settings │
|
||||
│ │
|
||||
│ (Use Enter to select, Tab to change focus, Esc to close) │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`SettingsDialog > Snapshot Tests > should render various boolean settings enabled correctly 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||
│ │
|
||||
│ > Settings │
|
||||
│ │
|
||||
│ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │
|
||||
│ │ Search to filter │ │
|
||||
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
|
||||
│ │
|
||||
│ ▲ │
|
||||
│ ● Preview Features (e.g., models) true* │
|
||||
│ Enable preview features (e.g., preview models). │
|
||||
│ │
|
||||
│ Vim Mode true* │
|
||||
│ Enable Vim keybindings │
|
||||
│ │
|
||||
│ Enable Auto Update true │
|
||||
│ Enable automatic updates. │
|
||||
│ │
|
||||
│ Enable Prompt Completion false │
|
||||
│ Enable AI-powered prompt completion suggestions while typing. │
|
||||
│ │
|
||||
│ Debug Keystroke Logging false │
|
||||
│ Enable debug logging of keystrokes to the console. │
|
||||
│ │
|
||||
│ Enable Session Cleanup false │
|
||||
│ Enable automatic session cleanup │
|
||||
│ │
|
||||
│ Output Format Text │
|
||||
│ The format of the CLI output. Can be \`text\` or \`json\`. │
|
||||
│ │
|
||||
│ Hide Window Title false │
|
||||
│ Hide the window title bar │
|
||||
│ │
|
||||
│ ▼ │
|
||||
│ │
|
||||
│ Apply To │
|
||||
│ ● User Settings │
|
||||
│ Workspace Settings │
|
||||
│ System Settings │
|
||||
│ │
|
||||
│ (Use Enter to select, Tab to change focus, Esc to close) │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
@@ -1,5 +1,25 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`ToolConfirmationQueue > calculates availableContentHeight based on availableTerminalHeight from UI state 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ Action Required 1 of 1 │
|
||||
│ │
|
||||
│ ? replace edit file │
|
||||
│ │
|
||||
│ ... first 49 lines hidden ... │
|
||||
│ 50 line │
|
||||
│ Apply this change? │
|
||||
│ │
|
||||
│ ● 1. Allow once │
|
||||
│ 2. Allow for this session │
|
||||
│ 3. Modify with external editor │
|
||||
│ 4. No, suggest changes (esc) │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯
|
||||
Press ctrl-o to show more lines
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`ToolConfirmationQueue > does not render expansion hint when constrainHeight is false 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ Action Required 1 of 1 │
|
||||
|
||||
@@ -29,7 +29,7 @@ export const TOOL_STATUS = {
|
||||
// Maximum number of MCP resources to display per server before truncating
|
||||
export const MAX_MCP_RESOURCES_TO_SHOW = 10;
|
||||
|
||||
export const WARNING_PROMPT_DURATION_MS = 1000;
|
||||
export const WARNING_PROMPT_DURATION_MS = 3000;
|
||||
export const QUEUE_ERROR_DISPLAY_DURATION_MS = 3000;
|
||||
export const SHELL_ACTION_REQUIRED_TITLE_DELAY_MS = 30000;
|
||||
export const SHELL_SILENT_WORKING_TITLE_DELAY_MS = 120000;
|
||||
|
||||
@@ -110,8 +110,8 @@ export const INFORMATIVE_TIPS = [
|
||||
'Delete from the cursor to the end of the line with Ctrl+K…',
|
||||
'Clear the entire input prompt with a double-press of Esc…',
|
||||
'Paste from your clipboard with Ctrl+V…',
|
||||
'Undo text edits in the input with Ctrl+Z…',
|
||||
'Redo undone text edits with Ctrl+Shift+Z…',
|
||||
'Undo text edits in the input with Cmd+Z or Alt+Z…',
|
||||
'Redo undone text edits with Shift+Cmd+Z or Shift+Alt+Z…',
|
||||
'Open the current prompt in an external editor with Ctrl+X…',
|
||||
'In menus, move up/down with k/j or the arrow keys…',
|
||||
'In menus, select an item by typing its number…',
|
||||
|
||||
@@ -124,6 +124,8 @@ function charLengthAt(str: string, i: number): number {
|
||||
return code !== undefined && code >= kUTF16SurrogateThreshold ? 2 : 1;
|
||||
}
|
||||
|
||||
// Note: we do not convert alt+z, alt+shift+z, or alt+v here
|
||||
// because mac users have alternative hotkeys.
|
||||
const MAC_ALT_KEY_CHARACTER_MAP: Record<string, string> = {
|
||||
'\u222B': 'b', // "∫" back one word
|
||||
'\u0192': 'f', // "ƒ" forward one word
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
type HookEndPayload,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { act } from 'react';
|
||||
import { WARNING_PROMPT_DURATION_MS } from '../constants.js';
|
||||
|
||||
describe('useHookDisplayState', () => {
|
||||
beforeEach(() => {
|
||||
@@ -53,7 +54,7 @@ describe('useHookDisplayState', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove a hook immediately if duration > 1s', () => {
|
||||
it('should remove a hook immediately if duration > minimum duration', () => {
|
||||
const { result } = renderHook(() => useHookDisplayState());
|
||||
|
||||
const startPayload: HookStartPayload = {
|
||||
@@ -65,9 +66,9 @@ describe('useHookDisplayState', () => {
|
||||
coreEvents.emitHookStart(startPayload);
|
||||
});
|
||||
|
||||
// Advance time by 1.1 seconds
|
||||
// Advance time by slightly more than the minimum duration
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(1100);
|
||||
vi.advanceTimersByTime(WARNING_PROMPT_DURATION_MS + 100);
|
||||
});
|
||||
|
||||
const endPayload: HookEndPayload = {
|
||||
@@ -83,7 +84,7 @@ describe('useHookDisplayState', () => {
|
||||
expect(result.current).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should delay removal if duration < 1s', () => {
|
||||
it('should delay removal if duration < minimum duration', () => {
|
||||
const { result } = renderHook(() => useHookDisplayState());
|
||||
|
||||
const startPayload: HookStartPayload = {
|
||||
@@ -113,9 +114,9 @@ describe('useHookDisplayState', () => {
|
||||
// Should still be present
|
||||
expect(result.current).toHaveLength(1);
|
||||
|
||||
// Advance remaining time (900ms needed, let's go 950ms)
|
||||
// Advance remaining time + buffer
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(950);
|
||||
vi.advanceTimersByTime(WARNING_PROMPT_DURATION_MS - 100 + 50);
|
||||
});
|
||||
|
||||
expect(result.current).toHaveLength(0);
|
||||
@@ -138,7 +139,7 @@ describe('useHookDisplayState', () => {
|
||||
|
||||
expect(result.current).toHaveLength(2);
|
||||
|
||||
// End h1 (total time 500ms -> needs 500ms delay)
|
||||
// End h1 (total time 500ms -> needs remaining delay)
|
||||
act(() => {
|
||||
coreEvents.emitHookEnd({
|
||||
hookName: 'h1',
|
||||
@@ -150,15 +151,24 @@ describe('useHookDisplayState', () => {
|
||||
// h1 still there
|
||||
expect(result.current).toHaveLength(2);
|
||||
|
||||
// Advance 600ms. h1 should disappear. h2 has been running for 600ms.
|
||||
// Advance enough for h1 to expire.
|
||||
// h1 ran for 500ms. Needs WARNING_PROMPT_DURATION_MS total.
|
||||
// So advance WARNING_PROMPT_DURATION_MS - 500 + 100.
|
||||
const advanceForH1 = WARNING_PROMPT_DURATION_MS - 500 + 100;
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(600);
|
||||
vi.advanceTimersByTime(advanceForH1);
|
||||
});
|
||||
|
||||
// h1 should disappear. h2 has been running for 500 (initial) + advanceForH1.
|
||||
expect(result.current).toHaveLength(1);
|
||||
expect(result.current[0].name).toBe('h2');
|
||||
|
||||
// End h2 (total time 600ms -> needs 400ms delay)
|
||||
// End h2.
|
||||
// h2 duration so far: 0 (start) -> 500 (start h2) -> (end h1) -> advanceForH1.
|
||||
// Actually h2 started at t=500. Current time is t=500 + advanceForH1.
|
||||
// Duration = advanceForH1.
|
||||
// advanceForH1 = 3000 - 500 + 100 = 2600.
|
||||
// So h2 has run for 2600ms. Needs 400ms more.
|
||||
act(() => {
|
||||
coreEvents.emitHookEnd({
|
||||
hookName: 'h2',
|
||||
@@ -169,6 +179,8 @@ describe('useHookDisplayState', () => {
|
||||
|
||||
expect(result.current).toHaveLength(1);
|
||||
|
||||
// Advance remaining needed for h2 + buffer
|
||||
// 3000 - 2600 = 400.
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(500);
|
||||
});
|
||||
@@ -199,34 +211,42 @@ describe('useHookDisplayState', () => {
|
||||
expect(result.current[0].name).toBe('same-hook');
|
||||
expect(result.current[1].name).toBe('same-hook');
|
||||
|
||||
// End Hook 1 at t=600 (Duration 600ms -> delay 400ms)
|
||||
// End Hook 1 at t=600 (Duration 600ms -> delay needed)
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(100);
|
||||
coreEvents.emitHookEnd({ ...hook, success: true });
|
||||
});
|
||||
|
||||
// Both still visible (Hook 1 pending removal in 400ms)
|
||||
// Both still visible
|
||||
expect(result.current).toHaveLength(2);
|
||||
|
||||
// Advance 400ms (t=1000). Hook 1 should be removed.
|
||||
// Advance to make Hook 1 expire.
|
||||
// Hook 1 duration 600ms. Needs WARNING_PROMPT_DURATION_MS total.
|
||||
// Needs WARNING_PROMPT_DURATION_MS - 600 more.
|
||||
const advanceForHook1 = WARNING_PROMPT_DURATION_MS - 600;
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(400);
|
||||
vi.advanceTimersByTime(advanceForHook1);
|
||||
});
|
||||
|
||||
expect(result.current).toHaveLength(1);
|
||||
|
||||
// End Hook 2 at t=1100 (Duration: 1100 - 500 = 600ms -> delay 400ms)
|
||||
// End Hook 2.
|
||||
// Hook 2 started at t=500.
|
||||
// Current time: t = 600 (hook 1 end) + advanceForHook1 = 600 + 3000 - 600 = 3000.
|
||||
// Hook 2 duration = 3000 - 500 = 2500ms.
|
||||
// Needs 3000 - 2500 = 500ms more.
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(100);
|
||||
vi.advanceTimersByTime(100); // just a small step before ending
|
||||
coreEvents.emitHookEnd({ ...hook, success: true });
|
||||
});
|
||||
|
||||
// Hook 2 still visible (pending removal in 400ms)
|
||||
// Hook 2 still visible (pending removal)
|
||||
// Total run time: 2500 + 100 = 2600ms. Needs 400ms.
|
||||
expect(result.current).toHaveLength(1);
|
||||
|
||||
// Advance 400ms (t=1500). Hook 2 should be removed.
|
||||
// Advance remaining
|
||||
act(() => {
|
||||
vi.advanceTimersByTime(400);
|
||||
vi.advanceTimersByTime(500);
|
||||
});
|
||||
|
||||
expect(result.current).toHaveLength(0);
|
||||
|
||||
@@ -131,13 +131,24 @@ describe('keyMatchers', () => {
|
||||
},
|
||||
{
|
||||
command: Command.UNDO,
|
||||
positive: [createKey('z', { shift: false, ctrl: true })],
|
||||
negative: [createKey('z'), createKey('z', { shift: true, ctrl: true })],
|
||||
positive: [
|
||||
createKey('z', { shift: false, cmd: true }),
|
||||
createKey('z', { shift: false, alt: true }),
|
||||
],
|
||||
negative: [
|
||||
createKey('z'),
|
||||
createKey('z', { shift: true, cmd: true }),
|
||||
createKey('z', { shift: false, ctrl: true }),
|
||||
],
|
||||
},
|
||||
{
|
||||
command: Command.REDO,
|
||||
positive: [createKey('z', { shift: true, ctrl: true })],
|
||||
negative: [createKey('z'), createKey('z', { shift: false, ctrl: true })],
|
||||
positive: [
|
||||
createKey('z', { shift: true, cmd: true }),
|
||||
createKey('z', { shift: true, alt: true }),
|
||||
createKey('z', { shift: true, ctrl: true }),
|
||||
],
|
||||
negative: [createKey('z'), createKey('z', { shift: false, cmd: true })],
|
||||
},
|
||||
|
||||
// Screen control
|
||||
|
||||
@@ -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 & {
|
||||
|
||||
@@ -31,4 +31,34 @@ describe('TableRenderer', () => {
|
||||
expect(output).toContain('Row 3, Col 3');
|
||||
expect(output).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('renders a table with long headers and 4 columns correctly', () => {
|
||||
const headers = [
|
||||
'Very Long Column Header One',
|
||||
'Very Long Column Header Two',
|
||||
'Very Long Column Header Three',
|
||||
'Very Long Column Header Four',
|
||||
];
|
||||
const rows = [
|
||||
['Data 1.1', 'Data 1.2', 'Data 1.3', 'Data 1.4'],
|
||||
['Data 2.1', 'Data 2.2', 'Data 2.3', 'Data 2.4'],
|
||||
['Data 3.1', 'Data 3.2', 'Data 3.3', 'Data 3.4'],
|
||||
];
|
||||
const terminalWidth = 80;
|
||||
|
||||
const { lastFrame } = renderWithProviders(
|
||||
<TableRenderer
|
||||
headers={headers}
|
||||
rows={rows}
|
||||
terminalWidth={terminalWidth}
|
||||
/>,
|
||||
);
|
||||
|
||||
const output = lastFrame();
|
||||
// Since terminalWidth is 80 and headers are long, they might be truncated.
|
||||
// We just check for some of the content.
|
||||
expect(output).toContain('Data 1.1');
|
||||
expect(output).toContain('Data 3.4');
|
||||
expect(output).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -34,11 +34,24 @@ export const TableRenderer: React.FC<TableRendererProps> = ({
|
||||
});
|
||||
|
||||
// Ensure table fits within terminal width
|
||||
const totalWidth = columnWidths.reduce((sum, width) => sum + width + 1, 1);
|
||||
// We calculate scale based on content width vs available width (terminal - borders)
|
||||
// First, extract content widths by removing the 2-char padding.
|
||||
const contentWidths = columnWidths.map((width) => Math.max(0, width - 2));
|
||||
const totalContentWidth = contentWidths.reduce(
|
||||
(sum, width) => sum + width,
|
||||
0,
|
||||
);
|
||||
|
||||
// Fixed overhead includes padding (2 per column) and separators (1 per column + 1 final).
|
||||
const fixedOverhead = headers.length * 2 + (headers.length + 1);
|
||||
|
||||
// Subtract 1 from available width to avoid edge-case wrapping on some terminals
|
||||
const availableWidth = Math.max(0, terminalWidth - fixedOverhead - 1);
|
||||
|
||||
const scaleFactor =
|
||||
totalWidth > terminalWidth ? terminalWidth / totalWidth : 1;
|
||||
const adjustedWidths = columnWidths.map((width) =>
|
||||
Math.floor(width * scaleFactor),
|
||||
totalContentWidth > availableWidth ? availableWidth / totalContentWidth : 1;
|
||||
const adjustedWidths = contentWidths.map(
|
||||
(width) => Math.floor(width * scaleFactor) + 2,
|
||||
);
|
||||
|
||||
// Helper function to render a cell with proper width
|
||||
|
||||
@@ -54,6 +54,18 @@ exports[`TableRenderer > renders a simple table correctly 1`] = `
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`TableRenderer > renders a table with long headers and 4 columns correctly 1`] = `
|
||||
"
|
||||
┌──────────────────┬──────────────────┬───────────────────┬──────────────────┐
|
||||
│ Very Long Col... │ Very Long Col... │ Very Long Colu... │ Very Long Col... │
|
||||
├──────────────────┼──────────────────┼───────────────────┼──────────────────┤
|
||||
│ Data 1.1 │ Data 1.2 │ Data 1.3 │ Data 1.4 │
|
||||
│ Data 2.1 │ Data 2.2 │ Data 2.3 │ Data 2.4 │
|
||||
│ Data 3.1 │ Data 3.2 │ Data 3.3 │ Data 3.4 │
|
||||
└──────────────────┴──────────────────┴───────────────────┴──────────────────┘
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`TableRenderer > truncates content when terminal width is small 1`] = `
|
||||
"
|
||||
┌────────┬─────────┬─────────┐
|
||||
|
||||
@@ -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.",
|
||||
|
||||
Reference in New Issue
Block a user