Skip to content

Commit 7311e24

Browse files
authored
feat(cli): enhance tool confirmation UI and selection layout (#24376)
1 parent 21a3925 commit 7311e24

24 files changed

Lines changed: 2433 additions & 1951 deletions

File tree

packages/cli/src/ui/__snapshots__/App.test.tsx.snap

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -124,21 +124,21 @@ HistoryItemDisplay
124124
│ │
125125
│ ? ls list directory │
126126
│ │
127-
│ ls │
128-
│ Allow execution of: 'ls'? │
127+
│ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │
128+
│ │ ls │ │
129+
│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │
130+
│ Allow execution of [ls]? │
129131
│ │
130132
│ ● 1. Allow once │
131133
│ 2. Allow for this session │
132134
│ 3. No, suggest changes (esc) │
133-
│ │
134135
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
135136
136137
137138
138139
139140
140141
141-
142142
Notifications
143143
144144
Composer

packages/cli/src/ui/__snapshots__/ToolConfirmationFullFrame-Full-Terminal-Tool-Confirmation-Snapshot-renders-tool-confirmation-box-in-the-frame-of-the-entire-terminal.snap.svg

Lines changed: 274 additions & 244 deletions
Loading

packages/cli/src/ui/__snapshots__/ToolConfirmationFullFrame.test.tsx.snap

Lines changed: 27 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,39 +5,39 @@ exports[`Full Terminal Tool Confirmation Snapshot > renders tool confirmation bo
55
▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄
66
77
╭─────────────────────────────────────────────────────────────────────────────────────────────────╮
8-
│ Action Required │
9-
│ │
10-
│ ? Edit packages/.../InputPrompt.tsx: return kittyProtocolSupporte... => return kittyProto… │
11-
│ │
12-
│ ... first 44 lines hidden (Ctrl+O to show) ... │
13-
│ 45 const line45 = true; │
14-
│ 46 const line46 = true; │
15-
│ 47 const line47 = true; │▄
16-
│ 48 const line48 = true; │█
17-
│ 49 const line49 = true; │█
18-
│ 50 const line50 = true; │█
19-
│ 51 const line51 = true; │█
20-
│ 52 const line52 = true; │█
21-
│ 53 const line53 = true; │█
22-
│ 54 const line54 = true; │█
23-
│ 55 const line55 = true; │█
24-
│ 56 const line56 = true; │█
25-
│ 57 const line57 = true; │█
26-
│ 58 const line58 = true; │█
27-
│ 59 const line59 = true; │█
28-
│ 60 const line60 = true; │█
29-
│ 61 - return kittyProtocolSupporte...; │█
30-
│ 61 + return kittyProtocolSupporte...; │█
31-
│ 62 buffer: TextBuffer; │█
32-
│ 63 onSubmit: (value: string) => void; │█
8+
│ ? Edit │
9+
│ ╭─────────────────────────────────────────────────────────────────────────────────────────────╮ │
10+
│ │ ... first 42 lines hidden (Ctrl+O to show) ... │ │
11+
│ │ 43 const line43 = true; │ │
12+
│ │ 44 const line44 = true; │ │
13+
│ │ 45 const line45 = true; │ │
14+
│ │ 46 const line46 = true; │ │
15+
│ │ 47 const line47 = true; │ │▄
16+
│ │ 48 const line48 = true; │ │█
17+
│ │ 49 const line49 = true; │ │█
18+
│ │ 50 const line50 = true; │ │█
19+
│ │ 51 const line51 = true; │ │█
20+
│ │ 52 const line52 = true; │ │█
21+
│ │ 53 const line53 = true; │ │█
22+
│ │ 54 const line54 = true; │ │█
23+
│ │ 55 const line55 = true; │ │█
24+
│ │ 56 const line56 = true; │ │█
25+
│ │ 57 const line57 = true; │ │█
26+
│ │ 58 const line58 = true; │ │█
27+
│ │ 59 const line59 = true; │ │█
28+
│ │ 60 const line60 = true; │ │█
29+
│ │ 61 - return kittyProtocolSupporte...; │ │█
30+
│ │ 61 + return kittyProtocolSupporte...; │ │█
31+
│ │ 62 buffer: TextBuffer; │ │█
32+
│ │ 63 onSubmit: (value: string) => void; │ │█
33+
│ ╰─────────────────────────────────────────────────────────────────────────────────────────────╯ │█
3334
│ Apply this change? │█
3435
│ │█
3536
│ ● 1. Allow once │█
3637
│ 2. Allow for this session │█
37-
│ 3. Allow for this file in all future sessions │█
38+
│ 3. Allow for this file in all future sessions ~/.gemini/policies/auto-saved.toml │█
3839
│ 4. Modify with external editor │█
3940
│ 5. No, suggest changes (esc) │█
40-
│ │█
4141
╰─────────────────────────────────────────────────────────────────────────────────────────────────╯█
4242
"
4343
`;

packages/cli/src/ui/components/ToolConfirmationQueue.test.tsx

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ describe('ToolConfirmationQueue', () => {
7070
const confirmingTool = {
7171
tool: {
7272
callId: 'call-1',
73-
name: 'ls',
73+
name: 'run_shell_command',
7474
description: 'list files',
7575
status: CoreToolCallStatus.AwaitingApproval,
7676
confirmationDetails: {
@@ -98,15 +98,12 @@ describe('ToolConfirmationQueue', () => {
9898
);
9999

100100
const output = lastFrame();
101-
expect(output).toContain('Action Required');
102101
expect(output).toContain('1 of 3');
103102
expect(output).toContain('ls'); // Tool name
104103
expect(output).toContain('list files'); // Tool description
105-
expect(output).toContain("Allow execution of: 'ls'?");
104+
expect(output).toContain('Allow execution of [ls]?');
106105
expect(output).toMatchSnapshot();
107106

108-
const stickyHeaderProps = vi.mocked(StickyHeader).mock.calls[0][0];
109-
expect(stickyHeaderProps.borderColor).toBe(theme.status.warning);
110107
unmount();
111108
});
112109

@@ -183,7 +180,7 @@ describe('ToolConfirmationQueue', () => {
183180
// availableContentHeight = Math.max(9 - 6, 4) = 4
184181
// MaxSizedBox in ToolConfirmationMessage will use 4
185182
// It should show truncation message
186-
await waitFor(() => expect(lastFrame()).toContain('49 hidden (Ctrl+O)'));
183+
await waitFor(() => expect(lastFrame()).toContain('48 hidden (Ctrl+O)'));
187184
expect(lastFrame()).toMatchSnapshot();
188185
unmount();
189186
});

packages/cli/src/ui/components/ToolConfirmationQueue.tsx

Lines changed: 78 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ import { Box, Text } from 'ink';
99
import { theme } from '../semantic-colors.js';
1010
import { useConfig } from '../contexts/ConfigContext.js';
1111
import { ToolConfirmationMessage } from './messages/ToolConfirmationMessage.js';
12-
import { ToolStatusIndicator, ToolInfo } from './messages/ToolShared.js';
12+
import {
13+
isShellTool,
14+
ToolStatusIndicator,
15+
ToolInfo,
16+
} from './messages/ToolShared.js';
1317
import { useUIState } from '../contexts/UIStateContext.js';
1418
import type { ConfirmingToolState } from '../hooks/useConfirmingTool.js';
1519
import { StickyHeader } from './StickyHeader.js';
@@ -31,6 +35,16 @@ function getConfirmationHeader(
3135
return headers[details.type] ?? 'Action Required';
3236
}
3337

38+
function getConfirmationLabel(
39+
toolName: string,
40+
details: SerializableConfirmationDetails | undefined,
41+
): string {
42+
if (details?.type === 'ask_user') return 'Questions';
43+
if (details?.type === 'exit_plan_mode') return 'Implementation';
44+
if (isShellTool(toolName)) return 'Shell';
45+
return toolName;
46+
}
47+
3448
interface ToolConfirmationQueueProps {
3549
confirmingTool: ConfirmingToolState;
3650
}
@@ -58,22 +72,78 @@ export const ToolConfirmationQueue: React.FC<ToolConfirmationQueueProps> = ({
5872
? Math.max(uiAvailableHeight, 4)
5973
: Math.floor(terminalHeight * 0.5);
6074

75+
const isShell = isShellTool(tool.name);
76+
const isEdit = tool.confirmationDetails?.type === 'edit';
77+
78+
if (isShell || isEdit) {
79+
// Use the new simplified layout for Shell and Edit tools
80+
const borderColor = theme.border.default;
81+
const availableContentHeight = constrainHeight
82+
? Math.max(maxHeight - 3, 4)
83+
: undefined;
84+
85+
const toolLabel = getConfirmationLabel(tool.name, tool.confirmationDetails);
86+
87+
return (
88+
<Box
89+
flexDirection="column"
90+
width={mainAreaWidth}
91+
flexShrink={0}
92+
borderStyle="round"
93+
borderColor={borderColor}
94+
paddingX={1}
95+
>
96+
{/* Header Line */}
97+
<Box justifyContent="space-between" marginBottom={0}>
98+
<Box flexDirection="row" flexShrink={1} overflow="hidden">
99+
<Text color={theme.status.warning} bold>
100+
? {toolLabel}
101+
{!isEdit && !!tool.description && ' '}
102+
</Text>
103+
{!isEdit && !!tool.description && (
104+
<Box flexShrink={1} overflow="hidden">
105+
<Text color={theme.text.primary} wrap="truncate-end">
106+
{tool.description}
107+
</Text>
108+
</Box>
109+
)}
110+
</Box>
111+
{total > 1 && (
112+
<Text color={theme.text.secondary}>
113+
{index} of {total}
114+
</Text>
115+
)}
116+
</Box>
117+
118+
{/* Interactive Area */}
119+
<Box flexDirection="column">
120+
<ToolConfirmationMessage
121+
callId={tool.callId}
122+
confirmationDetails={tool.confirmationDetails}
123+
config={config}
124+
getPreferredEditor={getPreferredEditor}
125+
terminalWidth={mainAreaWidth - 4} // Adjust for parent border/padding
126+
availableTerminalHeight={availableContentHeight}
127+
toolName={tool.name}
128+
isFocused={true}
129+
/>
130+
</Box>
131+
</Box>
132+
);
133+
}
134+
135+
// Restore original logic for other tools
61136
const isRoutine =
62137
tool.confirmationDetails?.type === 'ask_user' ||
63138
tool.confirmationDetails?.type === 'exit_plan_mode';
64139
const borderColor = isRoutine ? theme.status.success : theme.status.warning;
65140
const hideToolIdentity = isRoutine;
66141

67-
// ToolConfirmationMessage needs to know the height available for its OWN content.
68-
// We subtract the lines used by the Queue wrapper:
69-
// - 2 lines for the rounded border
70-
// - 2 lines for the Header (text + margin)
71-
// - 2 lines for Tool Identity (text + margin)
72142
const availableContentHeight = constrainHeight
73143
? Math.max(maxHeight - (hideToolIdentity ? 4 : 6), 4)
74144
: undefined;
75145

76-
const content = (
146+
return (
77147
<Box flexDirection="column" width={mainAreaWidth} flexShrink={0}>
78148
<StickyHeader
79149
width={mainAreaWidth}
@@ -122,18 +192,14 @@ export const ToolConfirmationQueue: React.FC<ToolConfirmationQueueProps> = ({
122192
paddingX={1}
123193
flexDirection="column"
124194
>
125-
{/* Interactive Area */}
126-
{/*
127-
Note: We force isFocused={true} because if this component is rendered,
128-
it effectively acts as a modal over the shell/composer.
129-
*/}
130195
<ToolConfirmationMessage
131196
callId={tool.callId}
132197
confirmationDetails={tool.confirmationDetails}
133198
config={config}
134199
getPreferredEditor={getPreferredEditor}
135200
terminalWidth={mainAreaWidth - 4} // Adjust for parent border/padding
136201
availableTerminalHeight={availableContentHeight}
202+
toolName={tool.name}
137203
isFocused={true}
138204
/>
139205
</Box>
@@ -149,6 +215,4 @@ export const ToolConfirmationQueue: React.FC<ToolConfirmationQueueProps> = ({
149215
/>
150216
</Box>
151217
);
152-
153-
return content;
154218
};

0 commit comments

Comments
 (0)