diff --git a/packages/cli/src/ui/components/ContextSummaryDisplay.test.tsx b/packages/cli/src/ui/components/ContextSummaryDisplay.test.tsx
index fd066d5534..0766e6f6da 100644
--- a/packages/cli/src/ui/components/ContextSummaryDisplay.test.tsx
+++ b/packages/cli/src/ui/components/ContextSummaryDisplay.test.tsx
@@ -31,14 +31,14 @@ describe('', () => {
mcpServers: { 'test-server': { command: 'test' } },
ideContext: {
workspaceState: {
- openFiles: [{ path: '/a/b/c' }],
+ openFiles: [{ path: '/a/b/c', timestamp: Date.now() }],
},
},
};
it('should render on a single line on a wide screen', () => {
const { lastFrame } = renderWithWidth(120, baseProps);
- const output = lastFrame();
+ const output = lastFrame()!;
expect(output).toContain(
'Using: 1 open file (ctrl+g to view) | 1 GEMINI.md file | 1 MCP server',
);
@@ -48,7 +48,7 @@ describe('', () => {
it('should render on multiple lines on a narrow screen', () => {
const { lastFrame } = renderWithWidth(60, baseProps);
- const output = lastFrame();
+ const output = lastFrame()!;
const expectedLines = [
' Using:',
' - 1 open file (ctrl+g to view)',
@@ -62,12 +62,12 @@ describe('', () => {
it('should switch layout at the 80-column breakpoint', () => {
// At 80 columns, should be on one line
const { lastFrame: wideFrame } = renderWithWidth(80, baseProps);
- expect(wideFrame().includes('\n')).toBe(false);
+ expect(wideFrame()!.includes('\n')).toBe(false);
// At 79 columns, should be on multiple lines
const { lastFrame: narrowFrame } = renderWithWidth(79, baseProps);
- expect(narrowFrame().includes('\n')).toBe(true);
- expect(narrowFrame().split('\n').length).toBe(4);
+ expect(narrowFrame()!.includes('\n')).toBe(true);
+ expect(narrowFrame()!.split('\n').length).toBe(4);
});
it('should not render empty parts', () => {
@@ -79,7 +79,7 @@ describe('', () => {
};
const { lastFrame } = renderWithWidth(60, props);
const expectedLines = [' Using:', ' - 1 open file (ctrl+g to view)'];
- const actualLines = lastFrame().split('\n');
+ const actualLines = lastFrame()!.split('\n');
expect(actualLines).toEqual(expectedLines);
});
});
diff --git a/packages/cli/src/ui/components/Footer.test.tsx b/packages/cli/src/ui/components/Footer.test.tsx
index 5d7df49610..a27f6b26d1 100644
--- a/packages/cli/src/ui/components/Footer.test.tsx
+++ b/packages/cli/src/ui/components/Footer.test.tsx
@@ -9,7 +9,8 @@ import {
createMockSettings,
} from '../../test-utils/render.js';
import { Footer } from './Footer.js';
-import { tildeifyPath } from '@google/gemini-cli-core';
+import { tildeifyPath, ToolCallDecision } from '@google/gemini-cli-core';
+import type { SessionStatsState } from '../contexts/SessionContext.js';
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
const original =
@@ -32,15 +33,41 @@ const defaultProps = {
branchName: 'main',
};
-const sessionStats = {
- sessionStats: { lastPromptTokenCount: 0, lastResponseTokenCount: 0 },
+const mockSessionStats: SessionStatsState = {
+ sessionId: 'test-session',
+ sessionStartTime: new Date(),
+ lastPromptTokenCount: 0,
+ promptCount: 0,
+ metrics: {
+ models: {},
+ 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,
+ },
+ },
};
describe('', () => {
it('renders the component', () => {
const { lastFrame } = renderWithProviders(, {
width: 120,
- uiState: { branchName: defaultProps.branchName, ...sessionStats },
+ uiState: {
+ branchName: defaultProps.branchName,
+ sessionStats: mockSessionStats,
+ },
});
expect(lastFrame()).toBeDefined();
});
@@ -49,7 +76,7 @@ describe('', () => {
it('should display a shortened path on a narrow terminal', () => {
const { lastFrame } = renderWithProviders(, {
width: 79,
- uiState: { ...sessionStats },
+ uiState: { sessionStats: mockSessionStats },
});
const tildePath = tildeifyPath(defaultProps.targetDir);
const pathLength = Math.max(20, Math.floor(79 * 0.25));
@@ -61,7 +88,7 @@ describe('', () => {
it('should use wide layout at 80 columns', () => {
const { lastFrame } = renderWithProviders(, {
width: 80,
- uiState: { ...sessionStats },
+ uiState: { sessionStats: mockSessionStats },
});
const tildePath = tildeifyPath(defaultProps.targetDir);
const expectedPath =
@@ -73,7 +100,10 @@ describe('', () => {
it('displays the branch name when provided', () => {
const { lastFrame } = renderWithProviders(, {
width: 120,
- uiState: { branchName: defaultProps.branchName, ...sessionStats },
+ uiState: {
+ branchName: defaultProps.branchName,
+ sessionStats: mockSessionStats,
+ },
});
expect(lastFrame()).toContain(`(${defaultProps.branchName}*)`);
});
@@ -81,7 +111,7 @@ describe('', () => {
it('does not display the branch name when not provided', () => {
const { lastFrame } = renderWithProviders(, {
width: 120,
- uiState: { branchName: undefined, ...sessionStats },
+ uiState: { branchName: undefined, sessionStats: mockSessionStats },
});
expect(lastFrame()).not.toContain(`(${defaultProps.branchName}*)`);
});
@@ -89,7 +119,7 @@ describe('', () => {
it('displays the model name and context percentage', () => {
const { lastFrame } = renderWithProviders(, {
width: 120,
- uiState: { ...sessionStats },
+ uiState: { sessionStats: mockSessionStats },
});
expect(lastFrame()).toContain(defaultProps.model);
expect(lastFrame()).toMatch(/\(\d+% context left\)/);
@@ -98,7 +128,7 @@ describe('', () => {
it('displays the model name and abbreviated context percentage', () => {
const { lastFrame } = renderWithProviders(, {
width: 99,
- uiState: { ...sessionStats },
+ uiState: { sessionStats: mockSessionStats },
});
expect(lastFrame()).toContain(defaultProps.model);
expect(lastFrame()).toMatch(/\(\d+%\)/);
@@ -108,7 +138,7 @@ describe('', () => {
it('should display untrusted when isTrustedFolder is false', () => {
const { lastFrame } = renderWithProviders(, {
width: 120,
- uiState: { isTrustedFolder: false, ...sessionStats },
+ uiState: { isTrustedFolder: false, sessionStats: mockSessionStats },
});
expect(lastFrame()).toContain('untrusted');
});
@@ -117,7 +147,7 @@ describe('', () => {
vi.stubEnv('SANDBOX', 'gemini-cli-test-sandbox');
const { lastFrame } = renderWithProviders(, {
width: 120,
- uiState: { isTrustedFolder: undefined, ...sessionStats },
+ uiState: { isTrustedFolder: undefined, sessionStats: mockSessionStats },
});
expect(lastFrame()).toContain('test');
vi.unstubAllEnvs();
@@ -128,7 +158,7 @@ describe('', () => {
vi.stubEnv('SEATBELT_PROFILE', 'test-profile');
const { lastFrame } = renderWithProviders(, {
width: 120,
- uiState: { isTrustedFolder: true, ...sessionStats },
+ uiState: { isTrustedFolder: true, sessionStats: mockSessionStats },
});
expect(lastFrame()).toMatch(/macOS Seatbelt.*\(test-profile\)/s);
vi.unstubAllEnvs();
@@ -139,7 +169,7 @@ describe('', () => {
vi.stubEnv('SANDBOX', '');
const { lastFrame } = renderWithProviders(, {
width: 120,
- uiState: { isTrustedFolder: true, ...sessionStats },
+ uiState: { isTrustedFolder: true, sessionStats: mockSessionStats },
});
expect(lastFrame()).toContain('no sandbox');
vi.unstubAllEnvs();
@@ -149,7 +179,7 @@ describe('', () => {
vi.stubEnv('SANDBOX', 'gemini-cli-test-sandbox');
const { lastFrame } = renderWithProviders(, {
width: 120,
- uiState: { isTrustedFolder: false, ...sessionStats },
+ uiState: { isTrustedFolder: false, sessionStats: mockSessionStats },
});
expect(lastFrame()).toContain('untrusted');
expect(lastFrame()).not.toMatch(/test-sandbox/s);
@@ -161,7 +191,7 @@ describe('', () => {
it('renders complete footer with all sections visible (baseline)', () => {
const { lastFrame } = renderWithProviders(, {
width: 120,
- uiState: { ...sessionStats },
+ uiState: { sessionStats: mockSessionStats },
});
expect(lastFrame()).toMatchSnapshot('complete-footer-wide');
});
@@ -169,7 +199,7 @@ describe('', () => {
it('renders footer with all optional sections hidden (minimal footer)', () => {
const { lastFrame } = renderWithProviders(, {
width: 120,
- uiState: { ...sessionStats },
+ uiState: { sessionStats: mockSessionStats },
settings: createMockSettings({
ui: {
footer: {
@@ -186,7 +216,7 @@ describe('', () => {
it('renders footer with only model info hidden (partial filtering)', () => {
const { lastFrame } = renderWithProviders(, {
width: 120,
- uiState: { ...sessionStats },
+ uiState: { sessionStats: mockSessionStats },
settings: createMockSettings({
ui: {
footer: {
@@ -203,7 +233,7 @@ describe('', () => {
it('renders footer with CWD and model info hidden to test alignment (only sandbox visible)', () => {
const { lastFrame } = renderWithProviders(, {
width: 120,
- uiState: { ...sessionStats },
+ uiState: { sessionStats: mockSessionStats },
settings: createMockSettings({
ui: {
footer: {
@@ -220,7 +250,7 @@ describe('', () => {
it('renders complete footer in narrow terminal (baseline narrow)', () => {
const { lastFrame } = renderWithProviders(, {
width: 79,
- uiState: { ...sessionStats },
+ uiState: { sessionStats: mockSessionStats },
});
expect(lastFrame()).toMatchSnapshot('complete-footer-narrow');
});
diff --git a/packages/cli/src/ui/components/InputPrompt.test.tsx b/packages/cli/src/ui/components/InputPrompt.test.tsx
index 00cb264bb5..eadbc75a0c 100644
--- a/packages/cli/src/ui/components/InputPrompt.test.tsx
+++ b/packages/cli/src/ui/components/InputPrompt.test.tsx
@@ -172,6 +172,9 @@ describe('InputPrompt', () => {
text: '',
accept: vi.fn(),
clear: vi.fn(),
+ isLoading: false,
+ isActive: false,
+ markSelected: vi.fn(),
},
};
mockedUseCommandCompletion.mockReturnValue(mockCommandCompletion);
@@ -200,6 +203,8 @@ describe('InputPrompt', () => {
mockedUseKittyKeyboardProtocol.mockReturnValue({
supported: false,
+ enabled: false,
+ checking: false,
});
props = {
@@ -223,6 +228,8 @@ describe('InputPrompt', () => {
inputWidth: 80,
suggestionsWidth: 80,
focus: true,
+ setQueueErrorMessage: vi.fn(),
+ streamingState: StreamingState.Idle,
};
});
@@ -1553,7 +1560,11 @@ describe('InputPrompt', () => {
describe('paste auto-submission protection', () => {
beforeEach(() => {
vi.useFakeTimers();
- mockedUseKittyKeyboardProtocol.mockReturnValue({ supported: false });
+ mockedUseKittyKeyboardProtocol.mockReturnValue({
+ supported: false,
+ enabled: false,
+ checking: false,
+ });
});
afterEach(() => {
@@ -1618,7 +1629,11 @@ describe('InputPrompt', () => {
{
name: 'kitty',
setup: () =>
- mockedUseKittyKeyboardProtocol.mockReturnValue({ supported: true }),
+ mockedUseKittyKeyboardProtocol.mockReturnValue({
+ supported: true,
+ enabled: true,
+ checking: false,
+ }),
},
])(
'should allow immediate submission for a trusted paste ($name)',
diff --git a/packages/cli/src/ui/components/ModelStatsDisplay.test.tsx b/packages/cli/src/ui/components/ModelStatsDisplay.test.tsx
index 591c2a2804..e0072b8fa4 100644
--- a/packages/cli/src/ui/components/ModelStatsDisplay.test.tsx
+++ b/packages/cli/src/ui/components/ModelStatsDisplay.test.tsx
@@ -9,6 +9,7 @@ import { describe, it, expect, vi, beforeAll, afterAll } from 'vitest';
import { ModelStatsDisplay } from './ModelStatsDisplay.js';
import * as SessionContext from '../contexts/SessionContext.js';
import type { SessionMetrics } from '../contexts/SessionContext.js';
+import { ToolCallDecision } from '@google/gemini-cli-core';
// Mock the context to provide controlled data for testing
vi.mock('../contexts/SessionContext.js', async (importOriginal) => {
@@ -24,6 +25,7 @@ const useSessionStatsMock = vi.mocked(SessionContext.useSessionStats);
const renderWithMockedStats = (metrics: SessionMetrics) => {
useSessionStatsMock.mockReturnValue({
stats: {
+ sessionId: 'test-session',
sessionStartTime: new Date(),
metrics,
lastPromptTokenCount: 0,
@@ -59,9 +61,18 @@ describe('', () => {
totalSuccess: 0,
totalFail: 0,
totalDurationMs: 0,
- totalDecisions: { accept: 0, reject: 0, modify: 0 },
+ totalDecisions: {
+ accept: 0,
+ reject: 0,
+ modify: 0,
+ [ToolCallDecision.AUTO_ACCEPT]: 0,
+ },
byName: {},
},
+ files: {
+ totalLinesAdded: 0,
+ totalLinesRemoved: 0,
+ },
});
expect(lastFrame()).toContain(
@@ -90,9 +101,18 @@ describe('', () => {
totalSuccess: 0,
totalFail: 0,
totalDurationMs: 0,
- totalDecisions: { accept: 0, reject: 0, modify: 0 },
+ totalDecisions: {
+ accept: 0,
+ reject: 0,
+ modify: 0,
+ [ToolCallDecision.AUTO_ACCEPT]: 0,
+ },
byName: {},
},
+ files: {
+ totalLinesAdded: 0,
+ totalLinesRemoved: 0,
+ },
});
const output = lastFrame();
@@ -133,9 +153,18 @@ describe('', () => {
totalSuccess: 0,
totalFail: 0,
totalDurationMs: 0,
- totalDecisions: { accept: 0, reject: 0, modify: 0 },
+ totalDecisions: {
+ accept: 0,
+ reject: 0,
+ modify: 0,
+ [ToolCallDecision.AUTO_ACCEPT]: 0,
+ },
byName: {},
},
+ files: {
+ totalLinesAdded: 0,
+ totalLinesRemoved: 0,
+ },
});
const output = lastFrame();
@@ -176,9 +205,18 @@ describe('', () => {
totalSuccess: 0,
totalFail: 0,
totalDurationMs: 0,
- totalDecisions: { accept: 0, reject: 0, modify: 0 },
+ totalDecisions: {
+ accept: 0,
+ reject: 0,
+ modify: 0,
+ [ToolCallDecision.AUTO_ACCEPT]: 0,
+ },
byName: {},
},
+ files: {
+ totalLinesAdded: 0,
+ totalLinesRemoved: 0,
+ },
});
const output = lastFrame();
@@ -211,9 +249,18 @@ describe('', () => {
totalSuccess: 0,
totalFail: 0,
totalDurationMs: 0,
- totalDecisions: { accept: 0, reject: 0, modify: 0 },
+ totalDecisions: {
+ accept: 0,
+ reject: 0,
+ modify: 0,
+ [ToolCallDecision.AUTO_ACCEPT]: 0,
+ },
byName: {},
},
+ files: {
+ totalLinesAdded: 0,
+ totalLinesRemoved: 0,
+ },
});
expect(lastFrame()).toMatchSnapshot();
@@ -239,9 +286,18 @@ describe('', () => {
totalSuccess: 0,
totalFail: 0,
totalDurationMs: 0,
- totalDecisions: { accept: 0, reject: 0, modify: 0 },
+ totalDecisions: {
+ accept: 0,
+ reject: 0,
+ modify: 0,
+ [ToolCallDecision.AUTO_ACCEPT]: 0,
+ },
byName: {},
},
+ files: {
+ totalLinesAdded: 0,
+ totalLinesRemoved: 0,
+ },
});
const output = lastFrame();
diff --git a/packages/cli/src/ui/components/SessionSummaryDisplay.test.tsx b/packages/cli/src/ui/components/SessionSummaryDisplay.test.tsx
index 766e851a6e..c5ea398dee 100644
--- a/packages/cli/src/ui/components/SessionSummaryDisplay.test.tsx
+++ b/packages/cli/src/ui/components/SessionSummaryDisplay.test.tsx
@@ -9,6 +9,7 @@ import { describe, it, expect, vi } from 'vitest';
import { SessionSummaryDisplay } from './SessionSummaryDisplay.js';
import * as SessionContext from '../contexts/SessionContext.js';
import type { SessionMetrics } from '../contexts/SessionContext.js';
+import { ToolCallDecision } from '@google/gemini-cli-core';
vi.mock('../contexts/SessionContext.js', async (importOriginal) => {
const actual = await importOriginal();
@@ -23,6 +24,7 @@ const useSessionStatsMock = vi.mocked(SessionContext.useSessionStats);
const renderWithMockedStats = (metrics: SessionMetrics) => {
useSessionStatsMock.mockReturnValue({
stats: {
+ sessionId: 'test-session',
sessionStartTime: new Date(),
metrics,
lastPromptTokenCount: 0,
@@ -57,7 +59,12 @@ describe('', () => {
totalSuccess: 0,
totalFail: 0,
totalDurationMs: 0,
- totalDecisions: { accept: 0, reject: 0, modify: 0 },
+ totalDecisions: {
+ accept: 0,
+ reject: 0,
+ modify: 0,
+ [ToolCallDecision.AUTO_ACCEPT]: 0,
+ },
byName: {},
},
files: {
diff --git a/packages/cli/src/ui/components/StatsDisplay.test.tsx b/packages/cli/src/ui/components/StatsDisplay.test.tsx
index 6820171cb9..9609bbf378 100644
--- a/packages/cli/src/ui/components/StatsDisplay.test.tsx
+++ b/packages/cli/src/ui/components/StatsDisplay.test.tsx
@@ -9,6 +9,7 @@ import { describe, it, expect, vi } from 'vitest';
import { StatsDisplay } from './StatsDisplay.js';
import * as SessionContext from '../contexts/SessionContext.js';
import type { SessionMetrics } from '../contexts/SessionContext.js';
+import { ToolCallDecision } from '@google/gemini-cli-core';
// Mock the context to provide controlled data for testing
vi.mock('../contexts/SessionContext.js', async (importOriginal) => {
@@ -38,23 +39,34 @@ const renderWithMockedStats = (metrics: SessionMetrics) => {
return render();
};
+// Helper to create metrics with default zero values
+const createTestMetrics = (
+ overrides: Partial = {},
+): SessionMetrics => ({
+ models: {},
+ 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,
+ },
+ ...overrides,
+});
+
describe('', () => {
it('renders only the Performance section in its zero state', () => {
- const zeroMetrics: SessionMetrics = {
- models: {},
- tools: {
- totalCalls: 0,
- totalSuccess: 0,
- totalFail: 0,
- totalDurationMs: 0,
- totalDecisions: { accept: 0, reject: 0, modify: 0 },
- byName: {},
- },
- files: {
- totalLinesAdded: 0,
- totalLinesRemoved: 0,
- },
- };
+ const zeroMetrics = createTestMetrics();
const { lastFrame } = renderWithMockedStats(zeroMetrics);
const output = lastFrame();
@@ -67,7 +79,7 @@ describe('', () => {
});
it('renders a table with two models correctly', () => {
- const metrics: SessionMetrics = {
+ const metrics = createTestMetrics({
models: {
'gemini-2.5-pro': {
api: { totalRequests: 3, totalErrors: 0, totalLatencyMs: 15000 },
@@ -92,19 +104,7 @@ describe('', () => {
},
},
},
- tools: {
- totalCalls: 0,
- totalSuccess: 0,
- totalFail: 0,
- totalDurationMs: 0,
- totalDecisions: { accept: 0, reject: 0, modify: 0 },
- byName: {},
- },
- files: {
- totalLinesAdded: 0,
- totalLinesRemoved: 0,
- },
- };
+ });
const { lastFrame } = renderWithMockedStats(metrics);
const output = lastFrame();
@@ -117,7 +117,7 @@ describe('', () => {
});
it('renders all sections when all data is present', () => {
- const metrics: SessionMetrics = {
+ const metrics = createTestMetrics({
models: {
'gemini-2.5-pro': {
api: { totalRequests: 1, totalErrors: 0, totalLatencyMs: 100 },
@@ -136,22 +136,28 @@ describe('', () => {
totalSuccess: 1,
totalFail: 1,
totalDurationMs: 123,
- totalDecisions: { accept: 1, reject: 0, modify: 0 },
+ totalDecisions: {
+ accept: 1,
+ reject: 0,
+ modify: 0,
+ [ToolCallDecision.AUTO_ACCEPT]: 0,
+ },
byName: {
'test-tool': {
count: 2,
success: 1,
fail: 1,
durationMs: 123,
- decisions: { accept: 1, reject: 0, modify: 0 },
+ decisions: {
+ accept: 1,
+ reject: 0,
+ modify: 0,
+ [ToolCallDecision.AUTO_ACCEPT]: 0,
+ },
},
},
},
- files: {
- totalLinesAdded: 0,
- totalLinesRemoved: 0,
- },
- };
+ });
const { lastFrame } = renderWithMockedStats(metrics);
const output = lastFrame();
@@ -166,29 +172,34 @@ describe('', () => {
describe('Conditional Rendering Tests', () => {
it('hides User Agreement when no decisions are made', () => {
- const metrics: SessionMetrics = {
- models: {},
+ const metrics = createTestMetrics({
tools: {
totalCalls: 2,
totalSuccess: 1,
totalFail: 1,
totalDurationMs: 123,
- totalDecisions: { accept: 0, reject: 0, modify: 0 }, // No decisions
+ totalDecisions: {
+ accept: 0,
+ reject: 0,
+ modify: 0,
+ [ToolCallDecision.AUTO_ACCEPT]: 0,
+ }, // No decisions
byName: {
'test-tool': {
count: 2,
success: 1,
fail: 1,
durationMs: 123,
- decisions: { accept: 0, reject: 0, modify: 0 },
+ decisions: {
+ accept: 0,
+ reject: 0,
+ modify: 0,
+ [ToolCallDecision.AUTO_ACCEPT]: 0,
+ },
},
},
},
- files: {
- totalLinesAdded: 0,
- totalLinesRemoved: 0,
- },
- };
+ });
const { lastFrame } = renderWithMockedStats(metrics);
const output = lastFrame();
@@ -200,7 +211,7 @@ describe('', () => {
});
it('hides Efficiency section when cache is not used', () => {
- const metrics: SessionMetrics = {
+ const metrics = createTestMetrics({
models: {
'gemini-2.5-pro': {
api: { totalRequests: 1, totalErrors: 0, totalLatencyMs: 100 },
@@ -214,19 +225,7 @@ describe('', () => {
},
},
},
- tools: {
- totalCalls: 0,
- totalSuccess: 0,
- totalFail: 0,
- totalDurationMs: 0,
- totalDecisions: { accept: 0, reject: 0, modify: 0 },
- byName: {},
- },
- files: {
- totalLinesAdded: 0,
- totalLinesRemoved: 0,
- },
- };
+ });
const { lastFrame } = renderWithMockedStats(metrics);
const output = lastFrame();
@@ -238,61 +237,61 @@ describe('', () => {
describe('Conditional Color Tests', () => {
it('renders success rate in green for high values', () => {
- const metrics: SessionMetrics = {
- models: {},
+ const metrics = createTestMetrics({
tools: {
totalCalls: 10,
totalSuccess: 10,
totalFail: 0,
totalDurationMs: 0,
- totalDecisions: { accept: 0, reject: 0, modify: 0 },
+ totalDecisions: {
+ accept: 0,
+ reject: 0,
+ modify: 0,
+ [ToolCallDecision.AUTO_ACCEPT]: 0,
+ },
byName: {},
},
- files: {
- totalLinesAdded: 0,
- totalLinesRemoved: 0,
- },
- };
+ });
const { lastFrame } = renderWithMockedStats(metrics);
expect(lastFrame()).toMatchSnapshot();
});
it('renders success rate in yellow for medium values', () => {
- const metrics: SessionMetrics = {
- models: {},
+ const metrics = createTestMetrics({
tools: {
totalCalls: 10,
totalSuccess: 9,
totalFail: 1,
totalDurationMs: 0,
- totalDecisions: { accept: 0, reject: 0, modify: 0 },
+ totalDecisions: {
+ accept: 0,
+ reject: 0,
+ modify: 0,
+ [ToolCallDecision.AUTO_ACCEPT]: 0,
+ },
byName: {},
},
- files: {
- totalLinesAdded: 0,
- totalLinesRemoved: 0,
- },
- };
+ });
const { lastFrame } = renderWithMockedStats(metrics);
expect(lastFrame()).toMatchSnapshot();
});
it('renders success rate in red for low values', () => {
- const metrics: SessionMetrics = {
- models: {},
+ const metrics = createTestMetrics({
tools: {
totalCalls: 10,
totalSuccess: 5,
totalFail: 5,
totalDurationMs: 0,
- totalDecisions: { accept: 0, reject: 0, modify: 0 },
+ totalDecisions: {
+ accept: 0,
+ reject: 0,
+ modify: 0,
+ [ToolCallDecision.AUTO_ACCEPT]: 0,
+ },
byName: {},
},
- files: {
- totalLinesAdded: 0,
- totalLinesRemoved: 0,
- },
- };
+ });
const { lastFrame } = renderWithMockedStats(metrics);
expect(lastFrame()).toMatchSnapshot();
});
@@ -300,21 +299,25 @@ describe('', () => {
describe('Code Changes Display', () => {
it('displays Code Changes when line counts are present', () => {
- const metrics: SessionMetrics = {
- models: {},
+ const metrics = createTestMetrics({
tools: {
totalCalls: 1,
totalSuccess: 1,
totalFail: 0,
totalDurationMs: 100,
- totalDecisions: { accept: 0, reject: 0, modify: 0 },
+ totalDecisions: {
+ accept: 0,
+ reject: 0,
+ modify: 0,
+ [ToolCallDecision.AUTO_ACCEPT]: 0,
+ },
byName: {},
},
files: {
totalLinesAdded: 42,
totalLinesRemoved: 18,
},
- };
+ });
const { lastFrame } = renderWithMockedStats(metrics);
const output = lastFrame();
@@ -326,21 +329,21 @@ describe('', () => {
});
it('hides Code Changes when no lines are added or removed', () => {
- const metrics: SessionMetrics = {
- models: {},
+ const metrics = createTestMetrics({
tools: {
totalCalls: 1,
totalSuccess: 1,
totalFail: 0,
totalDurationMs: 100,
- totalDecisions: { accept: 0, reject: 0, modify: 0 },
+ totalDecisions: {
+ accept: 0,
+ reject: 0,
+ modify: 0,
+ [ToolCallDecision.AUTO_ACCEPT]: 0,
+ },
byName: {},
},
- files: {
- totalLinesAdded: 0,
- totalLinesRemoved: 0,
- },
- };
+ });
const { lastFrame } = renderWithMockedStats(metrics);
const output = lastFrame();
@@ -351,21 +354,7 @@ describe('', () => {
});
describe('Title Rendering', () => {
- const zeroMetrics: SessionMetrics = {
- models: {},
- tools: {
- totalCalls: 0,
- totalSuccess: 0,
- totalFail: 0,
- totalDurationMs: 0,
- totalDecisions: { accept: 0, reject: 0, modify: 0 },
- byName: {},
- },
- files: {
- totalLinesAdded: 0,
- totalLinesRemoved: 0,
- },
- };
+ const zeroMetrics = createTestMetrics();
it('renders the default title when no title prop is provided', () => {
const { lastFrame } = renderWithMockedStats(zeroMetrics);
diff --git a/packages/cli/src/ui/components/ToolStatsDisplay.test.tsx b/packages/cli/src/ui/components/ToolStatsDisplay.test.tsx
index cf2d7f13d8..e8d588061e 100644
--- a/packages/cli/src/ui/components/ToolStatsDisplay.test.tsx
+++ b/packages/cli/src/ui/components/ToolStatsDisplay.test.tsx
@@ -9,6 +9,7 @@ import { describe, it, expect, vi } from 'vitest';
import { ToolStatsDisplay } from './ToolStatsDisplay.js';
import * as SessionContext from '../contexts/SessionContext.js';
import type { SessionMetrics } from '../contexts/SessionContext.js';
+import { ToolCallDecision } from '@google/gemini-cli-core';
// Mock the context to provide controlled data for testing
vi.mock('../contexts/SessionContext.js', async (importOriginal) => {
@@ -24,6 +25,7 @@ const useSessionStatsMock = vi.mocked(SessionContext.useSessionStats);
const renderWithMockedStats = (metrics: SessionMetrics) => {
useSessionStatsMock.mockReturnValue({
stats: {
+ sessionId: 'test-session-id',
sessionStartTime: new Date(),
metrics,
lastPromptTokenCount: 0,
@@ -46,9 +48,18 @@ describe('', () => {
totalSuccess: 0,
totalFail: 0,
totalDurationMs: 0,
- totalDecisions: { accept: 0, reject: 0, modify: 0 },
+ totalDecisions: {
+ accept: 0,
+ reject: 0,
+ modify: 0,
+ [ToolCallDecision.AUTO_ACCEPT]: 0,
+ },
byName: {},
},
+ files: {
+ totalLinesAdded: 0,
+ totalLinesRemoved: 0,
+ },
});
expect(lastFrame()).toContain(
@@ -65,17 +76,31 @@ describe('', () => {
totalSuccess: 1,
totalFail: 0,
totalDurationMs: 100,
- totalDecisions: { accept: 1, reject: 0, modify: 0 },
+ totalDecisions: {
+ accept: 1,
+ reject: 0,
+ modify: 0,
+ [ToolCallDecision.AUTO_ACCEPT]: 0,
+ },
byName: {
'test-tool': {
count: 1,
success: 1,
fail: 0,
durationMs: 100,
- decisions: { accept: 1, reject: 0, modify: 0 },
+ decisions: {
+ accept: 1,
+ reject: 0,
+ modify: 0,
+ [ToolCallDecision.AUTO_ACCEPT]: 0,
+ },
},
},
},
+ files: {
+ totalLinesAdded: 0,
+ totalLinesRemoved: 0,
+ },
});
const output = lastFrame();
@@ -91,24 +116,43 @@ describe('', () => {
totalSuccess: 2,
totalFail: 1,
totalDurationMs: 300,
- totalDecisions: { accept: 1, reject: 1, modify: 1 },
+ totalDecisions: {
+ accept: 1,
+ reject: 1,
+ modify: 1,
+ [ToolCallDecision.AUTO_ACCEPT]: 0,
+ },
byName: {
'tool-a': {
count: 2,
success: 1,
fail: 1,
durationMs: 200,
- decisions: { accept: 1, reject: 1, modify: 0 },
+ decisions: {
+ accept: 1,
+ reject: 1,
+ modify: 0,
+ [ToolCallDecision.AUTO_ACCEPT]: 0,
+ },
},
'tool-b': {
count: 1,
success: 1,
fail: 0,
durationMs: 100,
- decisions: { accept: 0, reject: 0, modify: 1 },
+ decisions: {
+ accept: 0,
+ reject: 0,
+ modify: 1,
+ [ToolCallDecision.AUTO_ACCEPT]: 0,
+ },
},
},
},
+ files: {
+ totalLinesAdded: 0,
+ totalLinesRemoved: 0,
+ },
});
const output = lastFrame();
@@ -129,6 +173,7 @@ describe('', () => {
accept: 123456789,
reject: 98765432,
modify: 12345,
+ [ToolCallDecision.AUTO_ACCEPT]: 0,
},
byName: {
'long-named-tool-for-testing-wrapping-and-such': {
@@ -140,10 +185,15 @@ describe('', () => {
accept: 123456789,
reject: 98765432,
modify: 12345,
+ [ToolCallDecision.AUTO_ACCEPT]: 0,
},
},
},
},
+ files: {
+ totalLinesAdded: 0,
+ totalLinesRemoved: 0,
+ },
});
expect(lastFrame()).toMatchSnapshot();
@@ -157,17 +207,31 @@ describe('', () => {
totalSuccess: 1,
totalFail: 0,
totalDurationMs: 100,
- totalDecisions: { accept: 0, reject: 0, modify: 0 },
+ totalDecisions: {
+ accept: 0,
+ reject: 0,
+ modify: 0,
+ [ToolCallDecision.AUTO_ACCEPT]: 0,
+ },
byName: {
'test-tool': {
count: 1,
success: 1,
fail: 0,
durationMs: 100,
- decisions: { accept: 0, reject: 0, modify: 0 },
+ decisions: {
+ accept: 0,
+ reject: 0,
+ modify: 0,
+ [ToolCallDecision.AUTO_ACCEPT]: 0,
+ },
},
},
},
+ files: {
+ totalLinesAdded: 0,
+ totalLinesRemoved: 0,
+ },
});
const output = lastFrame();
diff --git a/packages/cli/src/ui/components/__snapshots__/SessionSummaryDisplay.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/SessionSummaryDisplay.test.tsx.snap
index 7c925f721c..2912ac155d 100644
--- a/packages/cli/src/ui/components/__snapshots__/SessionSummaryDisplay.test.tsx.snap
+++ b/packages/cli/src/ui/components/__snapshots__/SessionSummaryDisplay.test.tsx.snap
@@ -6,7 +6,7 @@ exports[` > renders the summary display with a title 1`
│ Agent powering down. Goodbye! │
│ │
│ Interaction Summary │
-│ Session ID: │
+│ Session ID: test-session │
│ Tool Calls: 0 ( ✓ 0 x 0 ) │
│ Success Rate: 0.0% │
│ Code Changes: +42 -15 │
diff --git a/packages/cli/src/ui/components/shared/text-buffer.test.ts b/packages/cli/src/ui/components/shared/text-buffer.test.ts
index 118a3a4979..85555a14cf 100644
--- a/packages/cli/src/ui/components/shared/text-buffer.test.ts
+++ b/packages/cli/src/ui/components/shared/text-buffer.test.ts
@@ -12,6 +12,7 @@ import type {
TextBuffer,
TextBufferState,
TextBufferAction,
+ VisualLayout,
} from './text-buffer.js';
import {
useTextBuffer,
@@ -24,6 +25,12 @@ import {
} from './text-buffer.js';
import { cpLen } from '../../utils/textUtils.js';
+const defaultVisualLayout: VisualLayout = {
+ visualLines: [''],
+ logicalToVisualMap: [[[0, 0]]],
+ visualToLogicalMap: [[0, 0]],
+};
+
const initialState: TextBufferState = {
lines: [''],
cursorRow: 0,
@@ -33,6 +40,9 @@ const initialState: TextBufferState = {
redoStack: [],
clipboard: null,
selectionAnchor: null,
+ viewportWidth: 80,
+ viewportHeight: 24,
+ visualLayout: defaultVisualLayout,
};
describe('textBufferReducer', () => {
@@ -1700,28 +1710,40 @@ describe('logicalPosToOffset', () => {
});
});
+// Helper to create state for reducer tests
+const createTestState = (
+ lines: string[],
+ cursorRow: number,
+ cursorCol: number,
+ viewportWidth = 80,
+): TextBufferState => {
+ const text = lines.join('\n');
+ let state = textBufferReducer(initialState, {
+ type: 'set_text',
+ payload: text,
+ });
+ state = textBufferReducer(state, {
+ type: 'set_cursor',
+ payload: { cursorRow, cursorCol, preferredCol: null },
+ });
+ state = textBufferReducer(state, {
+ type: 'set_viewport',
+ payload: { width: viewportWidth, height: 24 },
+ });
+ return state;
+};
+
describe('textBufferReducer vim operations', () => {
describe('vim_delete_line', () => {
it('should delete a single line including newline in multi-line text', () => {
- const initialState: TextBufferState = {
- lines: ['line1', 'line2', 'line3'],
- cursorRow: 1,
- cursorCol: 2,
- preferredCol: null,
- visualLines: [['line1'], ['line2'], ['line3']],
- visualScrollRow: 0,
- visualCursor: { row: 1, col: 2 },
- viewport: { width: 10, height: 5 },
- undoStack: [],
- redoStack: [],
- };
+ const state = createTestState(['line1', 'line2', 'line3'], 1, 2);
const action: TextBufferAction = {
type: 'vim_delete_line',
payload: { count: 1 },
};
- const result = textBufferReducer(initialState, action);
+ const result = textBufferReducer(state, action);
expect(result).toHaveOnlyValidCharacters();
// After deleting line2, we should have line1 and line3, with cursor on line3 (now at index 1)
@@ -1731,25 +1753,14 @@ describe('textBufferReducer vim operations', () => {
});
it('should delete multiple lines when count > 1', () => {
- const initialState: TextBufferState = {
- lines: ['line1', 'line2', 'line3', 'line4'],
- cursorRow: 1,
- cursorCol: 0,
- preferredCol: null,
- visualLines: [['line1'], ['line2'], ['line3'], ['line4']],
- visualScrollRow: 0,
- visualCursor: { row: 1, col: 0 },
- viewport: { width: 10, height: 5 },
- undoStack: [],
- redoStack: [],
- };
+ const state = createTestState(['line1', 'line2', 'line3', 'line4'], 1, 0);
const action: TextBufferAction = {
type: 'vim_delete_line',
payload: { count: 2 },
};
- const result = textBufferReducer(initialState, action);
+ const result = textBufferReducer(state, action);
expect(result).toHaveOnlyValidCharacters();
// Should delete line2 and line3, leaving line1 and line4
@@ -1759,25 +1770,14 @@ describe('textBufferReducer vim operations', () => {
});
it('should clear single line content when only one line exists', () => {
- const initialState: TextBufferState = {
- lines: ['only line'],
- cursorRow: 0,
- cursorCol: 5,
- preferredCol: null,
- visualLines: [['only line']],
- visualScrollRow: 0,
- visualCursor: { row: 0, col: 5 },
- viewport: { width: 10, height: 5 },
- undoStack: [],
- redoStack: [],
- };
+ const state = createTestState(['only line'], 0, 5);
const action: TextBufferAction = {
type: 'vim_delete_line',
payload: { count: 1 },
};
- const result = textBufferReducer(initialState, action);
+ const result = textBufferReducer(state, action);
expect(result).toHaveOnlyValidCharacters();
// Should clear the line content but keep the line
@@ -1787,25 +1787,14 @@ describe('textBufferReducer vim operations', () => {
});
it('should handle deleting the last line properly', () => {
- const initialState: TextBufferState = {
- lines: ['line1', 'line2'],
- cursorRow: 1,
- cursorCol: 0,
- preferredCol: null,
- visualLines: [['line1'], ['line2']],
- visualScrollRow: 0,
- visualCursor: { row: 1, col: 0 },
- viewport: { width: 10, height: 5 },
- undoStack: [],
- redoStack: [],
- };
+ const state = createTestState(['line1', 'line2'], 1, 0);
const action: TextBufferAction = {
type: 'vim_delete_line',
payload: { count: 1 },
};
- const result = textBufferReducer(initialState, action);
+ const result = textBufferReducer(state, action);
expect(result).toHaveOnlyValidCharacters();
// Should delete the last line completely, not leave empty line
@@ -1815,18 +1804,7 @@ describe('textBufferReducer vim operations', () => {
});
it('should handle deleting all lines and maintain valid state for subsequent paste', () => {
- const initialState: TextBufferState = {
- lines: ['line1', 'line2', 'line3', 'line4'],
- cursorRow: 0,
- cursorCol: 0,
- preferredCol: null,
- visualLines: [['line1'], ['line2'], ['line3'], ['line4']],
- visualScrollRow: 0,
- visualCursor: { row: 0, col: 0 },
- viewport: { width: 10, height: 5 },
- undoStack: [],
- redoStack: [],
- };
+ const state = createTestState(['line1', 'line2', 'line3', 'line4'], 0, 0);
// Delete all 4 lines with 4dd
const deleteAction: TextBufferAction = {
@@ -1834,7 +1812,7 @@ describe('textBufferReducer vim operations', () => {
payload: { count: 4 },
};
- const afterDelete = textBufferReducer(initialState, deleteAction);
+ const afterDelete = textBufferReducer(state, deleteAction);
expect(afterDelete).toHaveOnlyValidCharacters();
// After deleting all lines, should have one empty line
diff --git a/packages/cli/src/ui/components/shared/vim-buffer-actions.test.ts b/packages/cli/src/ui/components/shared/vim-buffer-actions.test.ts
index a07d8df195..9d163b079b 100644
--- a/packages/cli/src/ui/components/shared/vim-buffer-actions.test.ts
+++ b/packages/cli/src/ui/components/shared/vim-buffer-actions.test.ts
@@ -6,7 +6,13 @@
import { describe, it, expect } from 'vitest';
import { handleVimAction } from './vim-buffer-actions.js';
-import type { TextBufferState } from './text-buffer.js';
+import type { TextBufferState, VisualLayout } from './text-buffer.js';
+
+const defaultVisualLayout: VisualLayout = {
+ visualLines: [''],
+ logicalToVisualMap: [[[0, 0]]],
+ visualToLogicalMap: [[0, 0]],
+};
// Helper to create test state
const createTestState = (
@@ -23,6 +29,8 @@ const createTestState = (
clipboard: null,
selectionAnchor: null,
viewportWidth: 80,
+ viewportHeight: 24,
+ visualLayout: defaultVisualLayout,
});
describe('vim-buffer-actions', () => {
@@ -266,7 +274,7 @@ describe('vim-buffer-actions', () => {
const result = handleVimAction(state, action);
expect(result).toHaveOnlyValidCharacters();
- expect(result.cursorRow).toBe(1); // Should move to empty line
+ expect(result.cursorRow).toBe(1);
expect(result.cursorCol).toBe(0); // Beginning of empty line
});
});
@@ -331,7 +339,7 @@ describe('vim-buffer-actions', () => {
const result = handleVimAction(state, action);
expect(result).toHaveOnlyValidCharacters();
- expect(result.cursorRow).toBe(2); // Should move to line with 'test'
+ expect(result.cursorRow).toBe(2);
expect(result.cursorCol).toBe(3); // Should be at 't' (end of 'test')
});
@@ -354,7 +362,7 @@ describe('vim-buffer-actions', () => {
payload: { count: 1 },
});
expect(result).toHaveOnlyValidCharacters();
- expect(result.cursorRow).toBe(1); // Should move to empty line
+ expect(result.cursorRow).toBe(1);
expect(result.cursorCol).toBe(0); // Empty line has col 0
});
@@ -382,7 +390,7 @@ describe('vim-buffer-actions', () => {
it('should handle precomposed characters with diacritics', () => {
// Test case with precomposed é for comparison
- const state = createTestState(['café test'], 0, 0); // Start at 'c'
+ const state = createTestState(['café test'], 0, 0);
// First 'e' command should move to the 'é' (position 3)
let result = handleVimAction(state, {
@@ -814,7 +822,7 @@ describe('vim-buffer-actions', () => {
const state = createTestState(['hello world'], 0, 5);
const action = {
type: 'vim_change_movement' as const,
- payload: { movement: 'h', count: 2 },
+ payload: { movement: 'h' as const, count: 2 },
};
const result = handleVimAction(state, action);
@@ -827,7 +835,7 @@ describe('vim-buffer-actions', () => {
const state = createTestState(['hello world'], 0, 5);
const action = {
type: 'vim_change_movement' as const,
- payload: { movement: 'l', count: 3 },
+ payload: { movement: 'l' as const, count: 3 },
};
const result = handleVimAction(state, action);
@@ -840,7 +848,7 @@ describe('vim-buffer-actions', () => {
const state = createTestState(['line1', 'line2', 'line3'], 0, 2);
const action = {
type: 'vim_change_movement' as const,
- payload: { movement: 'j', count: 2 },
+ payload: { movement: 'j' as const, count: 2 },
};
const result = handleVimAction(state, action);
diff --git a/packages/cli/tsconfig.json b/packages/cli/tsconfig.json
index ebff48d3fa..1cb77026c6 100644
--- a/packages/cli/tsconfig.json
+++ b/packages/cli/tsconfig.json
@@ -28,16 +28,6 @@
"src/utils/handleAutoUpdate.test.ts",
"src/utils/startupWarnings.test.ts",
"src/ui/App.test.tsx",
- "src/ui/components/ContextSummaryDisplay.test.tsx",
- "src/ui/components/Footer.test.tsx",
- "src/ui/components/InputPrompt.test.tsx",
- "src/ui/components/ModelStatsDisplay.test.tsx",
- "src/ui/components/SessionSummaryDisplay.test.tsx",
- "src/ui/components/shared/text-buffer.test.ts",
- "src/ui/components/shared/vim-buffer-actions.test.ts",
- "src/ui/components/StatsDisplay.test.tsx",
- "src/ui/components/ToolStatsDisplay.test.tsx",
- "src/ui/components/WarningMessage.test.tsx",
"src/ui/contexts/SessionContext.test.tsx",
"src/ui/hooks/slashCommandProcessor.test.ts",
"src/ui/hooks/useAtCompletion.test.ts",
diff --git a/packages/core/src/telemetry/index.ts b/packages/core/src/telemetry/index.ts
index 6834c7d687..2b5ff5c1f3 100644
--- a/packages/core/src/telemetry/index.ts
+++ b/packages/core/src/telemetry/index.ts
@@ -62,6 +62,7 @@ export {
KittySequenceOverflowEvent,
ToolOutputTruncatedEvent,
WebFetchFallbackAttemptEvent,
+ ToolCallDecision,
} from './types.js';
export { makeSlashCommandEvent, makeChatCompressionEvent } from './types.js';
export type { TelemetryEvent } from './types.js';