diff --git a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx
index ec623f69a4..1e11742e30 100644
--- a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx
+++ b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx
@@ -4,16 +4,18 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import { describe, it, expect, vi } from 'vitest';
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { ToolConfirmationMessage } from './ToolConfirmationMessage.js';
-import type {
- SerializableConfirmationDetails,
- ToolCallConfirmationDetails,
- Config,
+import {
+ type SerializableConfirmationDetails,
+ type ToolCallConfirmationDetails,
+ type Config,
+ ToolConfirmationOutcome,
} from '@google/gemini-cli-core';
import { renderWithProviders } from '../../../test-utils/render.js';
import { createMockSettings } from '../../../test-utils/settings.js';
import { useToolActions } from '../../contexts/ToolActionsContext.js';
+import { act } from 'react';
vi.mock('../../contexts/ToolActionsContext.js', async (importOriginal) => {
const actual =
@@ -644,4 +646,125 @@ describe('ToolConfirmationMessage', () => {
expect(output).not.toContain('Invocation Arguments:');
unmount();
});
+
+ describe('ESCAPE key behavior', () => {
+ beforeEach(() => {
+ vi.useFakeTimers();
+ });
+
+ afterEach(() => {
+ vi.useRealTimers();
+ vi.restoreAllMocks();
+ });
+
+ it('should call confirm(Cancel) and reset constrainHeight when ESC is pressed', async () => {
+ const mockConfirm = vi.fn().mockResolvedValue(undefined);
+ const mockSetConstrainHeight = vi.fn();
+ const mockRefreshStatic = vi.fn();
+
+ vi.mocked(useToolActions).mockReturnValue({
+ confirm: mockConfirm,
+ cancel: vi.fn(),
+ isDiffingEnabled: false,
+ });
+
+ const confirmationDetails: SerializableConfirmationDetails = {
+ type: 'info',
+ title: 'Confirm Web Fetch',
+ prompt: 'https://example.com',
+ urls: ['https://example.com'],
+ };
+
+ const { stdin, waitUntilReady, unmount } = renderWithProviders(
+ ,
+ {
+ uiState: { constrainHeight: false },
+ uiActions: {
+ setConstrainHeight: mockSetConstrainHeight,
+ refreshStatic: mockRefreshStatic,
+ },
+ },
+ );
+ await waitUntilReady();
+
+ // Simulate ESC key
+ stdin.write('\x1b');
+
+ await act(async () => {
+ await vi.runAllTimersAsync();
+ });
+
+ expect(mockConfirm).toHaveBeenCalledWith(
+ 'test-call-id',
+ ToolConfirmationOutcome.Cancel,
+ undefined,
+ );
+ expect(mockSetConstrainHeight).toHaveBeenCalledWith(true);
+ expect(mockRefreshStatic).toHaveBeenCalled();
+
+ unmount();
+ });
+
+ it('should NOT reset constrainHeight if it is already true when ESC is pressed', async () => {
+ const mockConfirm = vi.fn().mockResolvedValue(undefined);
+ const mockSetConstrainHeight = vi.fn();
+ const mockRefreshStatic = vi.fn();
+
+ vi.mocked(useToolActions).mockReturnValue({
+ confirm: mockConfirm,
+ cancel: vi.fn(),
+ isDiffingEnabled: false,
+ });
+
+ const confirmationDetails: SerializableConfirmationDetails = {
+ type: 'info',
+ title: 'Confirm Web Fetch',
+ prompt: 'https://example.com',
+ urls: ['https://example.com'],
+ };
+
+ const { stdin, waitUntilReady, unmount } = renderWithProviders(
+ ,
+ {
+ uiState: { constrainHeight: true },
+ uiActions: {
+ setConstrainHeight: mockSetConstrainHeight,
+ refreshStatic: mockRefreshStatic,
+ },
+ },
+ );
+ await waitUntilReady();
+
+ // Simulate ESC key
+ stdin.write('\x1b');
+
+ await act(async () => {
+ await vi.runAllTimersAsync();
+ });
+
+ expect(mockConfirm).toHaveBeenCalledWith(
+ 'test-call-id',
+ ToolConfirmationOutcome.Cancel,
+ undefined,
+ );
+ expect(mockSetConstrainHeight).not.toHaveBeenCalled();
+ expect(mockRefreshStatic).not.toHaveBeenCalled();
+
+ unmount();
+ });
+ });
});
diff --git a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx
index 113852cb8d..534b459f66 100644
--- a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx
+++ b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx
@@ -5,7 +5,7 @@
*/
import type React from 'react';
-import { useMemo, useCallback, useState } from 'react';
+import { useEffect, useMemo, useCallback, useState } from 'react';
import { Box, Text } from 'ink';
import { DiffRenderer } from './DiffRenderer.js';
import { RenderInline } from '../../utils/InlineMarkdownRenderer.js';
@@ -21,6 +21,8 @@ import {
import type { RadioSelectItem } from '../shared/RadioButtonSelect.js';
import { useToolActions } from '../../contexts/ToolActionsContext.js';
import { RadioButtonSelect } from '../shared/RadioButtonSelect.js';
+import { useUIActions } from '../../contexts/UIActionsContext.js';
+import { useUIState } from '../../contexts/UIStateContext.js';
import { MaxSizedBox, MINIMUM_MAX_HEIGHT } from '../shared/MaxSizedBox.js';
import {
sanitizeForDisplay,
@@ -70,6 +72,8 @@ export const ToolConfirmationMessage: React.FC<
}) => {
const keyMatchers = useKeyMatchers();
const { confirm, isDiffingEnabled } = useToolActions();
+ const uiActions = useUIActions();
+ const { constrainHeight } = useUIState();
const [mcpDetailsExpansionState, setMcpDetailsExpansionState] = useState<{
callId: string;
expanded: boolean;
@@ -77,6 +81,7 @@ export const ToolConfirmationMessage: React.FC<
callId,
expanded: false,
});
+ const [isCancelling, setIsCancelling] = useState(false);
const isMcpToolDetailsExpanded =
mcpDetailsExpansionState.callId === callId
? mcpDetailsExpansionState.expanded
@@ -179,7 +184,7 @@ export const ToolConfirmationMessage: React.FC<
return true;
}
if (keyMatchers[Command.ESCAPE](key)) {
- handleConfirm(ToolConfirmationOutcome.Cancel);
+ setIsCancelling(true);
return true;
}
if (keyMatchers[Command.QUIT](key)) {
@@ -192,6 +197,16 @@ export const ToolConfirmationMessage: React.FC<
{ isActive: isFocused, priority: true },
);
+ useEffect(() => {
+ if (isCancelling) {
+ if (!constrainHeight) {
+ uiActions.setConstrainHeight(true);
+ uiActions.refreshStatic();
+ }
+ handleConfirm(ToolConfirmationOutcome.Cancel);
+ }
+ }, [isCancelling, constrainHeight, uiActions, handleConfirm]);
+
const handleSelect = useCallback(
(item: ToolConfirmationOutcome) => handleConfirm(item),
[handleConfirm],