mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-21 18:44:30 -07:00
fix(ui): make tool confirmations take up entire terminal height (#22366)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { cleanup, renderWithProviders } from '../test-utils/render.js';
|
||||
import { createMockSettings } from '../test-utils/settings.js';
|
||||
import { App } from './App.js';
|
||||
import {
|
||||
CoreToolCallStatus,
|
||||
ApprovalMode,
|
||||
makeFakeConfig,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { type UIState } from './contexts/UIStateContext.js';
|
||||
import type { SerializableConfirmationDetails } from '@google/gemini-cli-core';
|
||||
import { act } from 'react';
|
||||
import { StreamingState } from './types.js';
|
||||
|
||||
vi.mock('ink', async (importOriginal) => {
|
||||
const original = await importOriginal<typeof import('ink')>();
|
||||
return {
|
||||
...original,
|
||||
useIsScreenReaderEnabled: vi.fn(() => false),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('./components/GeminiSpinner.js', () => ({
|
||||
GeminiSpinner: () => null,
|
||||
}));
|
||||
|
||||
vi.mock('./components/CliSpinner.js', () => ({
|
||||
CliSpinner: () => null,
|
||||
}));
|
||||
|
||||
// Mock hooks to align with codebase style, even if App uses UIState directly
|
||||
vi.mock('./hooks/useGeminiStream.js');
|
||||
vi.mock('./hooks/useHistoryManager.js');
|
||||
vi.mock('./hooks/useQuotaAndFallback.js');
|
||||
vi.mock('./hooks/useThemeCommand.js');
|
||||
vi.mock('./auth/useAuth.js');
|
||||
vi.mock('./hooks/useEditorSettings.js');
|
||||
vi.mock('./hooks/useSettingsCommand.js');
|
||||
vi.mock('./hooks/useModelCommand.js');
|
||||
vi.mock('./hooks/slashCommandProcessor.js');
|
||||
vi.mock('./hooks/useConsoleMessages.js');
|
||||
vi.mock('./hooks/useTerminalSize.js', () => ({
|
||||
useTerminalSize: vi.fn(() => ({ columns: 100, rows: 30 })),
|
||||
}));
|
||||
|
||||
describe('Full Terminal Tool Confirmation Snapshot', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('renders tool confirmation box in the frame of the entire terminal', async () => {
|
||||
// Generate a large diff to warrant truncation
|
||||
let largeDiff =
|
||||
'--- a/packages/cli/src/ui/components/InputPrompt.tsx\n+++ b/packages/cli/src/ui/components/InputPrompt.tsx\n@@ -1,100 +1,105 @@\n';
|
||||
for (let i = 1; i <= 60; i++) {
|
||||
largeDiff += ` const line${i} = true;\n`;
|
||||
}
|
||||
largeDiff += '- return kittyProtocolSupporte...;\n';
|
||||
largeDiff += '+ return kittyProtocolSupporte...;\n';
|
||||
largeDiff += ' buffer: TextBuffer;\n';
|
||||
largeDiff += ' onSubmit: (value: string) => void;';
|
||||
|
||||
const confirmationDetails: SerializableConfirmationDetails = {
|
||||
type: 'edit',
|
||||
title: 'Edit packages/.../InputPrompt.tsx',
|
||||
fileName: 'InputPrompt.tsx',
|
||||
filePath: 'packages/.../InputPrompt.tsx',
|
||||
fileDiff: largeDiff,
|
||||
originalContent: 'old',
|
||||
newContent: 'new',
|
||||
isModifying: false,
|
||||
};
|
||||
|
||||
const toolCalls = [
|
||||
{
|
||||
callId: 'call-1-modify-selected',
|
||||
name: 'Edit',
|
||||
description:
|
||||
'packages/.../InputPrompt.tsx: return kittyProtocolSupporte... => return kittyProtocolSupporte...',
|
||||
status: CoreToolCallStatus.AwaitingApproval,
|
||||
resultDisplay: '',
|
||||
confirmationDetails,
|
||||
},
|
||||
];
|
||||
|
||||
const mockUIState = {
|
||||
history: [
|
||||
{
|
||||
id: 1,
|
||||
type: 'user',
|
||||
text: 'Can you edit InputPrompt.tsx for me?',
|
||||
},
|
||||
],
|
||||
mainAreaWidth: 99,
|
||||
availableTerminalHeight: 36,
|
||||
streamingState: StreamingState.WaitingForConfirmation,
|
||||
constrainHeight: true,
|
||||
isConfigInitialized: true,
|
||||
cleanUiDetailsVisible: true,
|
||||
quota: {
|
||||
userTier: 'PRO',
|
||||
stats: {
|
||||
limits: {},
|
||||
usage: {},
|
||||
},
|
||||
proQuotaRequest: null,
|
||||
validationRequest: null,
|
||||
},
|
||||
pendingHistoryItems: [
|
||||
{
|
||||
id: 2,
|
||||
type: 'tool_group',
|
||||
tools: toolCalls,
|
||||
},
|
||||
],
|
||||
showApprovalModeIndicator: ApprovalMode.DEFAULT,
|
||||
sessionStats: {
|
||||
lastPromptTokenCount: 175400,
|
||||
contextPercentage: 3,
|
||||
},
|
||||
buffer: { text: '' },
|
||||
messageQueue: [],
|
||||
activeHooks: [],
|
||||
contextFileNames: [],
|
||||
rootUiRef: { current: null },
|
||||
} as unknown as UIState;
|
||||
|
||||
const mockConfig = makeFakeConfig();
|
||||
mockConfig.getUseAlternateBuffer = () => true;
|
||||
mockConfig.isTrustedFolder = () => true;
|
||||
mockConfig.getDisableAlwaysAllow = () => false;
|
||||
mockConfig.getIdeMode = () => false;
|
||||
mockConfig.getTargetDir = () => '/directory';
|
||||
|
||||
const { waitUntilReady, lastFrame, generateSvg, unmount } =
|
||||
await renderWithProviders(<App />, {
|
||||
uiState: mockUIState,
|
||||
config: mockConfig,
|
||||
settings: createMockSettings({
|
||||
merged: {
|
||||
ui: {
|
||||
useAlternateBuffer: true,
|
||||
theme: 'default',
|
||||
showUserIdentity: false,
|
||||
showShortcutsHint: false,
|
||||
footer: {
|
||||
hideContextPercentage: false,
|
||||
hideTokens: false,
|
||||
hideModel: false,
|
||||
},
|
||||
},
|
||||
security: {
|
||||
enablePermanentToolApproval: true,
|
||||
},
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
await waitUntilReady();
|
||||
|
||||
// Give it a moment to render
|
||||
await act(async () => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||
});
|
||||
|
||||
await expect({ lastFrame, generateSvg }).toMatchSvgSnapshot();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user