From 7311e242ec29795c191d3c094e55ebbee219bb63 Mon Sep 17 00:00:00 2001 From: Gal Zahavi <38544478+galz10@users.noreply.github.com> Date: Fri, 3 Apr 2026 18:32:35 -0700 Subject: [PATCH] feat(cli): enhance tool confirmation UI and selection layout (#24376) --- .../src/ui/__snapshots__/App.test.tsx.snap | 8 +- ...-the-frame-of-the-entire-terminal.snap.svg | 518 +++++----- .../ToolConfirmationFullFrame.test.tsx.snap | 54 +- .../components/ToolConfirmationQueue.test.tsx | 9 +- .../ui/components/ToolConfirmationQueue.tsx | 92 +- ...security-warning-height-correctly.snap.svg | 229 ++--- ...-and-content-for-large-edit-diffs.snap.svg | 923 ++++++++++-------- ...d-content-for-large-exec-commands.snap.svg | 358 ++++--- .../ToolConfirmationQueue.test.tsx.snap | 190 ++-- .../components/messages/DiffRenderer.test.tsx | 63 +- .../ui/components/messages/DiffRenderer.tsx | 19 +- .../messages/RedirectionConfirmation.test.tsx | 1 + .../messages/ToolConfirmationMessage.test.tsx | 29 +- .../messages/ToolConfirmationMessage.tsx | 442 +++++---- .../__snapshots__/DiffRenderer.test.tsx.snap | 14 +- .../RedirectionConfirmation.test.tsx.snap | 10 +- ...lable-height-for-large-edit-diffs.snap.svg | 881 +++++++++-------- ...le-height-for-large-exec-commands.snap.svg | 226 +++-- ...-newlines-and-syntax-highlighting.snap.svg | 62 +- .../ToolConfirmationMessage.test.tsx.snap | 185 ++-- .../ToolResultDisplay.test.tsx.snap | 7 +- .../src/ui/components/shared/MaxSizedBox.tsx | 26 +- packages/cli/src/ui/utils/CodeColorizer.tsx | 40 +- packages/core/src/tools/shell.ts | 2 +- 24 files changed, 2435 insertions(+), 1953 deletions(-) diff --git a/packages/cli/src/ui/__snapshots__/App.test.tsx.snap b/packages/cli/src/ui/__snapshots__/App.test.tsx.snap index f9799c2b07..94b1f9b1a4 100644 --- a/packages/cli/src/ui/__snapshots__/App.test.tsx.snap +++ b/packages/cli/src/ui/__snapshots__/App.test.tsx.snap @@ -124,13 +124,14 @@ HistoryItemDisplay │ │ │ ? ls list directory │ │ │ -│ ls │ -│ Allow execution of: 'ls'? │ +│ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │ +│ │ ls │ │ +│ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │ +│ Allow execution of [ls]? │ │ │ │ ● 1. Allow once │ │ 2. Allow for this session │ │ 3. No, suggest changes (esc) │ -│ │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ @@ -138,7 +139,6 @@ HistoryItemDisplay - Notifications Composer diff --git a/packages/cli/src/ui/__snapshots__/ToolConfirmationFullFrame-Full-Terminal-Tool-Confirmation-Snapshot-renders-tool-confirmation-box-in-the-frame-of-the-entire-terminal.snap.svg b/packages/cli/src/ui/__snapshots__/ToolConfirmationFullFrame-Full-Terminal-Tool-Confirmation-Snapshot-renders-tool-confirmation-box-in-the-frame-of-the-entire-terminal.snap.svg index b83d79928c..7565185d93 100644 --- a/packages/cli/src/ui/__snapshots__/ToolConfirmationFullFrame-Full-Terminal-Tool-Confirmation-Snapshot-renders-tool-confirmation-box-in-the-frame-of-the-entire-terminal.snap.svg +++ b/packages/cli/src/ui/__snapshots__/ToolConfirmationFullFrame-Full-Terminal-Tool-Confirmation-Snapshot-renders-tool-confirmation-box-in-the-frame-of-the-entire-terminal.snap.svg @@ -12,253 +12,283 @@ ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ - ╭─────────────────────────────────────────────────────────────────────────────────────────────────╮ - - Action Required - - - - - ? - Edit - packages/.../InputPrompt.tsx: return kittyProtocolSupporte... => return kittyProto - - - - - - ... first 44 lines hidden (Ctrl+O to show) ... - - - 45 - const - line45 - = - true - ; - - - 46 - const - line46 - = - true - ; - - - 47 - const - line47 - = - true - ; - - - - 48 - const - line48 - = - true - ; - - - - 49 - const - line49 - = - true - ; - - - - 50 - const - line50 - = - true - ; - - - - 51 - const - line51 - = - true - ; - - - - 52 - const - line52 - = - true - ; - - - - 53 - const - line53 - = - true - ; - - - - 54 - const - line54 - = - true - ; - - - - 55 - const - line55 - = - true - ; - - - - 56 - const - line56 - = - true - ; - - - - 57 - const - line57 - = - true - ; - - - - 58 - const - line58 - = - true - ; - - - - 59 - const - line59 - = - true - ; - - - - 60 - const - line60 - = - true - ; - - - - - 61 - - - - + ╭─────────────────────────────────────────────────────────────────────────────────────────────────╮ + + ? Edit + + + ╭─────────────────────────────────────────────────────────────────────────────────────────────╮ + + + + ... first 42 lines hidden (Ctrl+O to show) ... + + + + + 43 + const + line43 + = + true + ; + + + + + 44 + const + line44 + = + true + ; + + + + + 45 + const + line45 + = + true + ; + + + + + 46 + const + line46 + = + true + ; + + + + + 47 + const + line47 + = + true + ; + + │▄ + + + 48 + const + line48 + = + true + ; + + │█ + + + 49 + const + line49 + = + true + ; + + │█ + + + 50 + const + line50 + = + true + ; + + │█ + + + 51 + const + line51 + = + true + ; + + │█ + + + 52 + const + line52 + = + true + ; + + │█ + + + 53 + const + line53 + = + true + ; + + │█ + + + 54 + const + line54 + = + true + ; + + │█ + + + 55 + const + line55 + = + true + ; + + │█ + + + 56 + const + line56 + = + true + ; + + │█ + + + 57 + const + line57 + = + true + ; + + │█ + + + 58 + const + line58 + = + true + ; + + │█ + + + 59 + const + line59 + = + true + ; + + │█ + + + 60 + const + line60 + = + true + ; + + │█ + + + + 61 - - return - - kittyProtocolSupporte...; - - - - - 61 - - - + + - + + + + return + + kittyProtocolSupporte...; + + │█ + + + + 61 - - return - - kittyProtocolSupporte...; - - - - 62 - buffer: TextBuffer; - - - - 63 - onSubmit - : ( - value - : - string - ) => - void - ; - - - - Apply this change? - - - - - - - - - - - 1. - - - Allow once - - - - - 2. - Allow for this session - - - - 3. - Allow for this file in all future sessions - - - - 4. - Modify with external editor - - - - 5. - No, suggest changes (esc) - - - - - - ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯ - + + + + + + return + + kittyProtocolSupporte...; + + │█ + + + 62 + buffer: TextBuffer; + + │█ + + + 63 + onSubmit + : ( + value + : + string + ) => + void + ; + + │█ + + ╰─────────────────────────────────────────────────────────────────────────────────────────────╯ + │█ + + Apply this change? + │█ + + │█ + + + + + + 1. + + + Allow once + + │█ + + 2. + Allow for this session + │█ + + 3. + Allow for this file in all future sessions + ~/.gemini/policies/auto-saved.toml + │█ + + 4. + Modify with external editor + │█ + + 5. + No, suggest changes (esc) + │█ + ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯█ \ No newline at end of file diff --git a/packages/cli/src/ui/__snapshots__/ToolConfirmationFullFrame.test.tsx.snap b/packages/cli/src/ui/__snapshots__/ToolConfirmationFullFrame.test.tsx.snap index 6841182785..d9cc9f7ce3 100644 --- a/packages/cli/src/ui/__snapshots__/ToolConfirmationFullFrame.test.tsx.snap +++ b/packages/cli/src/ui/__snapshots__/ToolConfirmationFullFrame.test.tsx.snap @@ -5,39 +5,39 @@ exports[`Full Terminal Tool Confirmation Snapshot > renders tool confirmation bo ▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄▄ ╭─────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ Action Required │ -│ │ -│ ? Edit packages/.../InputPrompt.tsx: return kittyProtocolSupporte... => return kittyProto… │ -│ │ -│ ... first 44 lines hidden (Ctrl+O to show) ... │ -│ 45 const line45 = true; │ -│ 46 const line46 = true; │ -│ 47 const line47 = true; │▄ -│ 48 const line48 = true; │█ -│ 49 const line49 = true; │█ -│ 50 const line50 = true; │█ -│ 51 const line51 = true; │█ -│ 52 const line52 = true; │█ -│ 53 const line53 = true; │█ -│ 54 const line54 = true; │█ -│ 55 const line55 = true; │█ -│ 56 const line56 = true; │█ -│ 57 const line57 = true; │█ -│ 58 const line58 = true; │█ -│ 59 const line59 = true; │█ -│ 60 const line60 = true; │█ -│ 61 - return kittyProtocolSupporte...; │█ -│ 61 + return kittyProtocolSupporte...; │█ -│ 62 buffer: TextBuffer; │█ -│ 63 onSubmit: (value: string) => void; │█ +│ ? Edit │ +│ ╭─────────────────────────────────────────────────────────────────────────────────────────────╮ │ +│ │ ... first 42 lines hidden (Ctrl+O to show) ... │ │ +│ │ 43 const line43 = true; │ │ +│ │ 44 const line44 = true; │ │ +│ │ 45 const line45 = true; │ │ +│ │ 46 const line46 = true; │ │ +│ │ 47 const line47 = true; │ │▄ +│ │ 48 const line48 = true; │ │█ +│ │ 49 const line49 = true; │ │█ +│ │ 50 const line50 = true; │ │█ +│ │ 51 const line51 = true; │ │█ +│ │ 52 const line52 = true; │ │█ +│ │ 53 const line53 = true; │ │█ +│ │ 54 const line54 = true; │ │█ +│ │ 55 const line55 = true; │ │█ +│ │ 56 const line56 = true; │ │█ +│ │ 57 const line57 = true; │ │█ +│ │ 58 const line58 = true; │ │█ +│ │ 59 const line59 = true; │ │█ +│ │ 60 const line60 = true; │ │█ +│ │ 61 - return kittyProtocolSupporte...; │ │█ +│ │ 61 + return kittyProtocolSupporte...; │ │█ +│ │ 62 buffer: TextBuffer; │ │█ +│ │ 63 onSubmit: (value: string) => void; │ │█ +│ ╰─────────────────────────────────────────────────────────────────────────────────────────────╯ │█ │ Apply this change? │█ │ │█ │ ● 1. Allow once │█ │ 2. Allow for this session │█ -│ 3. Allow for this file in all future sessions │█ +│ 3. Allow for this file in all future sessions ~/.gemini/policies/auto-saved.toml │█ │ 4. Modify with external editor │█ │ 5. No, suggest changes (esc) │█ -│ │█ ╰─────────────────────────────────────────────────────────────────────────────────────────────────╯█ " `; diff --git a/packages/cli/src/ui/components/ToolConfirmationQueue.test.tsx b/packages/cli/src/ui/components/ToolConfirmationQueue.test.tsx index 451d0f4bb7..58a78d3c24 100644 --- a/packages/cli/src/ui/components/ToolConfirmationQueue.test.tsx +++ b/packages/cli/src/ui/components/ToolConfirmationQueue.test.tsx @@ -70,7 +70,7 @@ describe('ToolConfirmationQueue', () => { const confirmingTool = { tool: { callId: 'call-1', - name: 'ls', + name: 'run_shell_command', description: 'list files', status: CoreToolCallStatus.AwaitingApproval, confirmationDetails: { @@ -98,15 +98,12 @@ describe('ToolConfirmationQueue', () => { ); const output = lastFrame(); - expect(output).toContain('Action Required'); expect(output).toContain('1 of 3'); expect(output).toContain('ls'); // Tool name expect(output).toContain('list files'); // Tool description - expect(output).toContain("Allow execution of: 'ls'?"); + expect(output).toContain('Allow execution of [ls]?'); expect(output).toMatchSnapshot(); - const stickyHeaderProps = vi.mocked(StickyHeader).mock.calls[0][0]; - expect(stickyHeaderProps.borderColor).toBe(theme.status.warning); unmount(); }); @@ -183,7 +180,7 @@ describe('ToolConfirmationQueue', () => { // availableContentHeight = Math.max(9 - 6, 4) = 4 // MaxSizedBox in ToolConfirmationMessage will use 4 // It should show truncation message - await waitFor(() => expect(lastFrame()).toContain('49 hidden (Ctrl+O)')); + await waitFor(() => expect(lastFrame()).toContain('48 hidden (Ctrl+O)')); expect(lastFrame()).toMatchSnapshot(); unmount(); }); diff --git a/packages/cli/src/ui/components/ToolConfirmationQueue.tsx b/packages/cli/src/ui/components/ToolConfirmationQueue.tsx index e5294e9614..1a836662b7 100644 --- a/packages/cli/src/ui/components/ToolConfirmationQueue.tsx +++ b/packages/cli/src/ui/components/ToolConfirmationQueue.tsx @@ -9,7 +9,11 @@ import { Box, Text } from 'ink'; import { theme } from '../semantic-colors.js'; import { useConfig } from '../contexts/ConfigContext.js'; import { ToolConfirmationMessage } from './messages/ToolConfirmationMessage.js'; -import { ToolStatusIndicator, ToolInfo } from './messages/ToolShared.js'; +import { + isShellTool, + ToolStatusIndicator, + ToolInfo, +} from './messages/ToolShared.js'; import { useUIState } from '../contexts/UIStateContext.js'; import type { ConfirmingToolState } from '../hooks/useConfirmingTool.js'; import { StickyHeader } from './StickyHeader.js'; @@ -31,6 +35,16 @@ function getConfirmationHeader( return headers[details.type] ?? 'Action Required'; } +function getConfirmationLabel( + toolName: string, + details: SerializableConfirmationDetails | undefined, +): string { + if (details?.type === 'ask_user') return 'Questions'; + if (details?.type === 'exit_plan_mode') return 'Implementation'; + if (isShellTool(toolName)) return 'Shell'; + return toolName; +} + interface ToolConfirmationQueueProps { confirmingTool: ConfirmingToolState; } @@ -58,22 +72,78 @@ export const ToolConfirmationQueue: React.FC = ({ ? Math.max(uiAvailableHeight, 4) : Math.floor(terminalHeight * 0.5); + const isShell = isShellTool(tool.name); + const isEdit = tool.confirmationDetails?.type === 'edit'; + + if (isShell || isEdit) { + // Use the new simplified layout for Shell and Edit tools + const borderColor = theme.border.default; + const availableContentHeight = constrainHeight + ? Math.max(maxHeight - 3, 4) + : undefined; + + const toolLabel = getConfirmationLabel(tool.name, tool.confirmationDetails); + + return ( + + {/* Header Line */} + + + + ? {toolLabel} + {!isEdit && !!tool.description && ' '} + + {!isEdit && !!tool.description && ( + + + {tool.description} + + + )} + + {total > 1 && ( + + {index} of {total} + + )} + + + {/* Interactive Area */} + + + + + ); + } + + // Restore original logic for other tools const isRoutine = tool.confirmationDetails?.type === 'ask_user' || tool.confirmationDetails?.type === 'exit_plan_mode'; const borderColor = isRoutine ? theme.status.success : theme.status.warning; const hideToolIdentity = isRoutine; - // ToolConfirmationMessage needs to know the height available for its OWN content. - // We subtract the lines used by the Queue wrapper: - // - 2 lines for the rounded border - // - 2 lines for the Header (text + margin) - // - 2 lines for Tool Identity (text + margin) const availableContentHeight = constrainHeight ? Math.max(maxHeight - (hideToolIdentity ? 4 : 6), 4) : undefined; - const content = ( + return ( = ({ paddingX={1} flexDirection="column" > - {/* Interactive Area */} - {/* - Note: We force isFocused={true} because if this component is rendered, - it effectively acts as a modal over the shell/composer. - */} = ({ getPreferredEditor={getPreferredEditor} terminalWidth={mainAreaWidth - 4} // Adjust for parent border/padding availableTerminalHeight={availableContentHeight} + toolName={tool.name} isFocused={true} /> @@ -149,6 +215,4 @@ export const ToolConfirmationQueue: React.FC = ({ /> ); - - return content; }; diff --git a/packages/cli/src/ui/components/__snapshots__/ToolConfirmationQueue-ToolConfirmationQueue-height-allocation-and-layout-should-handle-security-warning-height-correctly.snap.svg b/packages/cli/src/ui/components/__snapshots__/ToolConfirmationQueue-ToolConfirmationQueue-height-allocation-and-layout-should-handle-security-warning-height-correctly.snap.svg index 678d4b42b3..8e57fe107e 100644 --- a/packages/cli/src/ui/components/__snapshots__/ToolConfirmationQueue-ToolConfirmationQueue-height-allocation-and-layout-should-handle-security-warning-height-correctly.snap.svg +++ b/packages/cli/src/ui/components/__snapshots__/ToolConfirmationQueue-ToolConfirmationQueue-height-allocation-and-layout-should-handle-security-warning-height-correctly.snap.svg @@ -1,130 +1,113 @@ - + - + - ╭──────────────────────────────────────────────────────────────────────────────╮ - - Action Required + ╭──────────────────────────────────────────────────────────────────────────────╮ + + ? Shell + Executes a bash command with a deceptive URL 3 of 3 - - - - - ? - run_shell_command - Executes a bash command with a deceptive URL - - - - - ... 6 hidden (Ctrl+O) ... - - - echo - "Line 37" - - - echo - "Line 38" - - - echo - "Line 39" - - - echo - "Line 40" - - - echo - "Line 41" - - - echo - "Line 42" - - - echo - "Line 43" - - - echo - "Line 44" - - - echo - "Line 45" - - - echo - "Line 46" - - - echo - "Line 47" - - - echo - "Line 48" - - - echo - "Line 49" - - - echo - "Line 50" - - - curl https://täst.com - - - - - - Warning: - Deceptive URL(s) detected: - - - - - Original: - https://täst.com/ - - - Actual Host (Punycode): - https://xn--tst-qla.com/ - - - - - Allow execution of: 'echo'? - - - - - - - - - 1. - - - Allow once - - - - 2. - Allow for this session - - - 3. - No, suggest changes (esc) - - - - ╰──────────────────────────────────────────────────────────────────────────────╯ + + + ... 6 hidden (Ctrl+O) ... + + + + echo + "Line 44" + + + + + echo + "Line 45" + + + + + echo + "Line 46" + + + + + echo + "Line 47" + + + + + echo + "Line 48" + + + + + echo + "Line 49" + + + + + echo + "Line 50" + + + + + curl https://täst.com + + + + ╰──────────────────────────────────────────────────────────────────────────╯ + + + + + + Warning: + Deceptive URL(s) detected: + + + + + Original: + https://täst.com/ + + + Actual Host (Punycode): + https://xn--tst-qla.com/ + + + + + Allow execution of + [echo] + ? + + + + + + + + + 1. + + + Allow once + + + + 2. + Allow for this session + + + 3. + No, suggest changes (esc) + + ╰──────────────────────────────────────────────────────────────────────────────╯ \ No newline at end of file diff --git a/packages/cli/src/ui/components/__snapshots__/ToolConfirmationQueue-ToolConfirmationQueue-height-allocation-and-layout-should-render-the-full-queue-wrapper-with-borders-and-content-for-large-edit-diffs.snap.svg b/packages/cli/src/ui/components/__snapshots__/ToolConfirmationQueue-ToolConfirmationQueue-height-allocation-and-layout-should-render-the-full-queue-wrapper-with-borders-and-content-for-large-edit-diffs.snap.svg index c39d7046bc..bbfedfab59 100644 --- a/packages/cli/src/ui/components/__snapshots__/ToolConfirmationQueue-ToolConfirmationQueue-height-allocation-and-layout-should-render-the-full-queue-wrapper-with-borders-and-content-for-large-edit-diffs.snap.svg +++ b/packages/cli/src/ui/components/__snapshots__/ToolConfirmationQueue-ToolConfirmationQueue-height-allocation-and-layout-should-render-the-full-queue-wrapper-with-borders-and-content-for-large-edit-diffs.snap.svg @@ -4,455 +4,540 @@ - ╭──────────────────────────────────────────────────────────────────────────────╮ - - Action Required - - - - - ? - replace - Replaces content in a file - - - - - ... 15 hidden (Ctrl+O) ... - - - - - 8 + ╭──────────────────────────────────────────────────────────────────────────────╮ + + ? replace + + + ╭──────────────────────────────────────────────────────────────────────────╮ + + + + ... 13 hidden (Ctrl+O) ... + + + + + + + 7 + + + + + + + const + + newLine7 = + + true + + ; + + + + + + + 8 + + + - + + + const + + oldLine8 = + + true + + ; + + + + - + + 8 - - const - - newLine8 = - - true - - ; - - - - - 9 + + + + + + const + + newLine8 = + + true + + ; + + + + - - + 9 - - const - - oldLine9 = - - true - - ; - - - - - 9 + + - + + + const + + oldLine9 = + + true + + ; + + + + - + + 9 - - const - - newLine9 = - - true - - ; - - - - 10 - - - - + + + + + + const + + newLine9 = + + true + + ; + + + + + + 10 - - const - - oldLine10 = - - true - - ; - - - - 10 - - - + + + - + + + const + + oldLine10 = + + true + + ; + + + + + + 10 - - const - - newLine10 = - - true - - ; - - - - 11 - - - - + + + + + + const + + newLine10 = + + true + + ; + + + + + + 11 - - const - - oldLine11 = - - true - - ; - - - - 11 - - - + + + - + + + const + + oldLine11 = + + true + + ; + + + + + + 11 - - const - - newLine11 = - - true - - ; - - - - 12 - - - - + + + + + + const + + newLine11 = + + true + + ; + + + + + + 12 - - const - - oldLine12 = - - true - - ; - - - - 12 - - - + + + - + + + const + + oldLine12 = + + true + + ; + + + + + + 12 - - const - - newLine12 = - - true - - ; - - - - 13 - - - - + + + + + + const + + newLine12 = + + true + + ; + + + + + + 13 - - const - - oldLine13 = - - true - - ; - - - - 13 - - - + + + - + + + const + + oldLine13 = + + true + + ; + + + + + + 13 - - const - - newLine13 = - - true - - ; - - - - 14 - - - - + + + + + + const + + newLine13 = + + true + + ; + + + + + + 14 - - const - - oldLine14 = - - true - - ; - - - - 14 - - - + + + - + + + const + + oldLine14 = + + true + + ; + + + + + + 14 - - const - - newLine14 = - - true - - ; - - - - 15 - - - - + + + + + + const + + newLine14 = + + true + + ; + + + + + + 15 - - const - - oldLine15 = - - true - - ; - - - - 15 - - - + + + - + + + const + + oldLine15 = + + true + + ; + + + + + + 15 - - const - - newLine15 = - - true - - ; - - - - 16 - - - - + + + + + + const + + newLine15 = + + true + + ; + + + + + + 16 - - const - - oldLine16 = - - true - - ; - - - - 16 - - - + + + - + + + const + + oldLine16 = + + true + + ; + + + + + + 16 - - const - - newLine16 = - - true - - ; - - - - 17 - - - - + + + + + + const + + newLine16 = + + true + + ; + + + + + + 17 - - const - - oldLine17 = - - true - - ; - - - - 17 - - - + + + - + + + const + + oldLine17 = + + true + + ; + + + + + + 17 - - const - - newLine17 = - - true - - ; - - - - 18 - - - - + + + + + + const + + newLine17 = + + true + + ; + + + + + + 18 - - const - - oldLine18 = - - true - - ; - - - - 18 - - - + + + - + + + const + + oldLine18 = + + true + + ; + + + + + + 18 - - const - - newLine18 = - - true - - ; - - - - 19 - - - - + + + + + + const + + newLine18 = + + true + + ; + + + + + + 19 - - const - - oldLine19 = - - true - - ; - - - - 19 - - - + + + - + + + const + + oldLine19 = + + true + + ; + + + + + + 19 - - const - - newLine19 = - - true - - ; - - - - 20 - - - - + + + + + + const + + newLine19 = + + true + + ; + + + + + + 20 - - const - - oldLine20 = - - true - - ; - - - - 20 - - - + + + - + + + const + + oldLine20 = + + true + + ; + + + + + + 20 - - const - - newLine20 = - - true - - ; - - - Apply this change? - - - - - - - - - 1. - - - Allow once - - - - 2. - Allow for this session - - - 3. - Modify with external editor - - - 4. - No, suggest changes (esc) - - - - ╰──────────────────────────────────────────────────────────────────────────────╯ + + + + + + const + + newLine20 = + + true + + ; + + + + ╰──────────────────────────────────────────────────────────────────────────╯ + + + Apply this change? + + + + + + + + + 1. + + + Allow once + + + + 2. + Allow for this session + + + 3. + Modify with external editor + + + 4. + No, suggest changes (esc) + + ╰──────────────────────────────────────────────────────────────────────────────╯ \ No newline at end of file diff --git a/packages/cli/src/ui/components/__snapshots__/ToolConfirmationQueue-ToolConfirmationQueue-height-allocation-and-layout-should-render-the-full-queue-wrapper-with-borders-and-content-for-large-exec-commands.snap.svg b/packages/cli/src/ui/components/__snapshots__/ToolConfirmationQueue-ToolConfirmationQueue-height-allocation-and-layout-should-render-the-full-queue-wrapper-with-borders-and-content-for-large-exec-commands.snap.svg index 508fc9d3c4..3f2d8451a8 100644 --- a/packages/cli/src/ui/components/__snapshots__/ToolConfirmationQueue-ToolConfirmationQueue-height-allocation-and-layout-should-render-the-full-queue-wrapper-with-borders-and-content-for-large-exec-commands.snap.svg +++ b/packages/cli/src/ui/components/__snapshots__/ToolConfirmationQueue-ToolConfirmationQueue-height-allocation-and-layout-should-render-the-full-queue-wrapper-with-borders-and-content-for-large-exec-commands.snap.svg @@ -4,153 +4,217 @@ - ╭──────────────────────────────────────────────────────────────────────────────╮ - - Action Required + ╭──────────────────────────────────────────────────────────────────────────────╮ + + ? Shell + Executes a bash command 2 of 3 - - - - - ? - run_shell_command - Executes a bash command - - - - - ... 24 hidden (Ctrl+O) ... - - - echo - "Line 25" - - - echo - "Line 26" - - - echo - "Line 27" - - - echo - "Line 28" - - - echo - "Line 29" - - - echo - "Line 30" - - - echo - "Line 31" - - - echo - "Line 32" - - - echo - "Line 33" - - - echo - "Line 34" - - - echo - "Line 35" - - - echo - "Line 36" - - - echo - "Line 37" - - - echo - "Line 38" - - - echo - "Line 39" - - - echo - "Line 40" - - - echo - "Line 41" - - - echo - "Line 42" - - - echo - "Line 43" - - - echo - "Line 44" - - - echo - "Line 45" - - - echo - "Line 46" - - - echo - "Line 47" - - - echo - "Line 48" - - - echo - "Line 49" - - - echo - "Line 50" - - - Allow execution of: 'echo'? - - - - - - - - - 1. - - - Allow once - - - - 2. - Allow for this session - - - 3. - No, suggest changes (esc) - - - - ╰──────────────────────────────────────────────────────────────────────────────╯ + + + ╭──────────────────────────────────────────────────────────────────────────╮ + + + + ... 22 hidden (Ctrl+O) ... + + + + + echo + "Line 23" + + + + + echo + "Line 24" + + + + + echo + "Line 25" + + + + + echo + "Line 26" + + + + + echo + "Line 27" + + + + + echo + "Line 28" + + + + + echo + "Line 29" + + + + + echo + "Line 30" + + + + + echo + "Line 31" + + + + + echo + "Line 32" + + + + + echo + "Line 33" + + + + + echo + "Line 34" + + + + + echo + "Line 35" + + + + + echo + "Line 36" + + + + + echo + "Line 37" + + + + + echo + "Line 38" + + + + + echo + "Line 39" + + + + + echo + "Line 40" + + + + + echo + "Line 41" + + + + + echo + "Line 42" + + + + + echo + "Line 43" + + + + + echo + "Line 44" + + + + + echo + "Line 45" + + + + + echo + "Line 46" + + + + + echo + "Line 47" + + + + + echo + "Line 48" + + + + + echo + "Line 49" + + + + + echo + "Line 50" + + + + ╰──────────────────────────────────────────────────────────────────────────╯ + + + Allow execution of + [echo] + ? + + + + + + + + + 1. + + + Allow once + + + + 2. + Allow for this session + + + 3. + No, suggest changes (esc) + + ╰──────────────────────────────────────────────────────────────────────────────╯ \ No newline at end of file diff --git a/packages/cli/src/ui/components/__snapshots__/ToolConfirmationQueue.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/ToolConfirmationQueue.test.tsx.snap index fdbb216cde..8d8667b51d 100644 --- a/packages/cli/src/ui/components/__snapshots__/ToolConfirmationQueue.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/ToolConfirmationQueue.test.tsx.snap @@ -2,32 +2,25 @@ exports[`ToolConfirmationQueue > calculates availableContentHeight based on availableTerminalHeight from UI state 1`] = ` "╭──────────────────────────────────────────────────────────────────────────────╮ -│ Action Required │ -│ │ -│ ? replace edit file │ -│ │ -│ ... 49 hidden (Ctrl+O) ... │ -│ 50 line │ +│ ? replace │ +│ ╭──────────────────────────────────────────────────────────────────────────╮ │ +│ ╰─... 48 hidden (Ctrl+O) ...───────────────────────────────────────────────╯ │ │ Apply this change? │ │ │ │ ● 1. Allow once │ │ 2. Allow for this session │ │ 3. Modify with external editor │ │ 4. No, suggest changes (esc) │ -│ │ ╰──────────────────────────────────────────────────────────────────────────────╯ " `; exports[`ToolConfirmationQueue > does not render expansion hint when constrainHeight is false 1`] = ` "╭──────────────────────────────────────────────────────────────────────────────╮ -│ Action Required │ -│ │ -│ ? replace edit file │ -│ │ +│ ? replace │ │ ╭──────────────────────────────────────────────────────────────────────────╮ │ │ │ │ │ -│ │ No changes detected. │ │ +│ │ No changes detected. │ │ │ │ │ │ │ ╰──────────────────────────────────────────────────────────────────────────╯ │ │ Apply this change? │ @@ -36,131 +29,120 @@ exports[`ToolConfirmationQueue > does not render expansion hint when constrainHe │ 2. Allow for this session │ │ 3. Modify with external editor │ │ 4. No, suggest changes (esc) │ -│ │ ╰──────────────────────────────────────────────────────────────────────────────╯ " `; exports[`ToolConfirmationQueue > height allocation and layout > should handle security warning height correctly 1`] = ` "╭──────────────────────────────────────────────────────────────────────────────╮ -│ Action Required 3 of 3 │ -│ │ -│ ? run_shell_command Executes a bash command with a deceptive URL │ -│ │ +│ ? Shell Executes a bash command with a deceptive URL 3 of 3 │ │ ... 6 hidden (Ctrl+O) ... │ -│ echo "Line 37" │ -│ echo "Line 38" │ -│ echo "Line 39" │ -│ echo "Line 40" │ -│ echo "Line 41" │ -│ echo "Line 42" │ -│ echo "Line 43" │ -│ echo "Line 44" │ -│ echo "Line 45" │ -│ echo "Line 46" │ -│ echo "Line 47" │ -│ echo "Line 48" │ -│ echo "Line 49" │ -│ echo "Line 50" │ -│ curl https://täst.com │ +│ │ echo "Line 44" │ │ +│ │ echo "Line 45" │ │ +│ │ echo "Line 46" │ │ +│ │ echo "Line 47" │ │ +│ │ echo "Line 48" │ │ +│ │ echo "Line 49" │ │ +│ │ echo "Line 50" │ │ +│ │ curl https://täst.com │ │ +│ ╰──────────────────────────────────────────────────────────────────────────╯ │ │ │ │ ⚠ Warning: Deceptive URL(s) detected: │ │ │ │ Original: https://täst.com/ │ │ Actual Host (Punycode): https://xn--tst-qla.com/ │ │ │ -│ Allow execution of: 'echo'? │ +│ Allow execution of [echo]? │ │ │ │ ● 1. Allow once │ │ 2. Allow for this session │ │ 3. No, suggest changes (esc) │ -│ │ ╰──────────────────────────────────────────────────────────────────────────────╯ " `; exports[`ToolConfirmationQueue > height allocation and layout > should render the full queue wrapper with borders and content for large edit diffs 1`] = ` "╭──────────────────────────────────────────────────────────────────────────────╮ -│ Action Required │ -│ │ -│ ? replace Replaces content in a file │ -│ │ -│ ... 15 hidden (Ctrl+O) ... │ -│ 8 + const newLine8 = true; │ -│ 9 - const oldLine9 = true; │ -│ 9 + const newLine9 = true; │ -│ 10 - const oldLine10 = true; │ -│ 10 + const newLine10 = true; │ -│ 11 - const oldLine11 = true; │ -│ 11 + const newLine11 = true; │ -│ 12 - const oldLine12 = true; │ -│ 12 + const newLine12 = true; │ -│ 13 - const oldLine13 = true; │ -│ 13 + const newLine13 = true; │ -│ 14 - const oldLine14 = true; │ -│ 14 + const newLine14 = true; │ -│ 15 - const oldLine15 = true; │ -│ 15 + const newLine15 = true; │ -│ 16 - const oldLine16 = true; │ -│ 16 + const newLine16 = true; │ -│ 17 - const oldLine17 = true; │ -│ 17 + const newLine17 = true; │ -│ 18 - const oldLine18 = true; │ -│ 18 + const newLine18 = true; │ -│ 19 - const oldLine19 = true; │ -│ 19 + const newLine19 = true; │ -│ 20 - const oldLine20 = true; │ -│ 20 + const newLine20 = true; │ +│ ? replace │ +│ ╭──────────────────────────────────────────────────────────────────────────╮ │ +│ │ ... 13 hidden (Ctrl+O) ... │ │ +│ │ 7 + const newLine7 = true; │ │ +│ │ 8 - const oldLine8 = true; │ │ +│ │ 8 + const newLine8 = true; │ │ +│ │ 9 - const oldLine9 = true; │ │ +│ │ 9 + const newLine9 = true; │ │ +│ │ 10 - const oldLine10 = true; │ │ +│ │ 10 + const newLine10 = true; │ │ +│ │ 11 - const oldLine11 = true; │ │ +│ │ 11 + const newLine11 = true; │ │ +│ │ 12 - const oldLine12 = true; │ │ +│ │ 12 + const newLine12 = true; │ │ +│ │ 13 - const oldLine13 = true; │ │ +│ │ 13 + const newLine13 = true; │ │ +│ │ 14 - const oldLine14 = true; │ │ +│ │ 14 + const newLine14 = true; │ │ +│ │ 15 - const oldLine15 = true; │ │ +│ │ 15 + const newLine15 = true; │ │ +│ │ 16 - const oldLine16 = true; │ │ +│ │ 16 + const newLine16 = true; │ │ +│ │ 17 - const oldLine17 = true; │ │ +│ │ 17 + const newLine17 = true; │ │ +│ │ 18 - const oldLine18 = true; │ │ +│ │ 18 + const newLine18 = true; │ │ +│ │ 19 - const oldLine19 = true; │ │ +│ │ 19 + const newLine19 = true; │ │ +│ │ 20 - const oldLine20 = true; │ │ +│ │ 20 + const newLine20 = true; │ │ +│ ╰──────────────────────────────────────────────────────────────────────────╯ │ │ Apply this change? │ │ │ │ ● 1. Allow once │ │ 2. Allow for this session │ │ 3. Modify with external editor │ │ 4. No, suggest changes (esc) │ -│ │ ╰──────────────────────────────────────────────────────────────────────────────╯ " `; exports[`ToolConfirmationQueue > height allocation and layout > should render the full queue wrapper with borders and content for large exec commands 1`] = ` "╭──────────────────────────────────────────────────────────────────────────────╮ -│ Action Required 2 of 3 │ -│ │ -│ ? run_shell_command Executes a bash command │ -│ │ -│ ... 24 hidden (Ctrl+O) ... │ -│ echo "Line 25" │ -│ echo "Line 26" │ -│ echo "Line 27" │ -│ echo "Line 28" │ -│ echo "Line 29" │ -│ echo "Line 30" │ -│ echo "Line 31" │ -│ echo "Line 32" │ -│ echo "Line 33" │ -│ echo "Line 34" │ -│ echo "Line 35" │ -│ echo "Line 36" │ -│ echo "Line 37" │ -│ echo "Line 38" │ -│ echo "Line 39" │ -│ echo "Line 40" │ -│ echo "Line 41" │ -│ echo "Line 42" │ -│ echo "Line 43" │ -│ echo "Line 44" │ -│ echo "Line 45" │ -│ echo "Line 46" │ -│ echo "Line 47" │ -│ echo "Line 48" │ -│ echo "Line 49" │ -│ echo "Line 50" │ -│ Allow execution of: 'echo'? │ +│ ? Shell Executes a bash command 2 of 3 │ +│ ╭──────────────────────────────────────────────────────────────────────────╮ │ +│ │ ... 22 hidden (Ctrl+O) ... │ │ +│ │ echo "Line 23" │ │ +│ │ echo "Line 24" │ │ +│ │ echo "Line 25" │ │ +│ │ echo "Line 26" │ │ +│ │ echo "Line 27" │ │ +│ │ echo "Line 28" │ │ +│ │ echo "Line 29" │ │ +│ │ echo "Line 30" │ │ +│ │ echo "Line 31" │ │ +│ │ echo "Line 32" │ │ +│ │ echo "Line 33" │ │ +│ │ echo "Line 34" │ │ +│ │ echo "Line 35" │ │ +│ │ echo "Line 36" │ │ +│ │ echo "Line 37" │ │ +│ │ echo "Line 38" │ │ +│ │ echo "Line 39" │ │ +│ │ echo "Line 40" │ │ +│ │ echo "Line 41" │ │ +│ │ echo "Line 42" │ │ +│ │ echo "Line 43" │ │ +│ │ echo "Line 44" │ │ +│ │ echo "Line 45" │ │ +│ │ echo "Line 46" │ │ +│ │ echo "Line 47" │ │ +│ │ echo "Line 48" │ │ +│ │ echo "Line 49" │ │ +│ │ echo "Line 50" │ │ +│ ╰──────────────────────────────────────────────────────────────────────────╯ │ +│ Allow execution of [echo]? │ │ │ │ ● 1. Allow once │ │ 2. Allow for this session │ │ 3. No, suggest changes (esc) │ -│ │ ╰──────────────────────────────────────────────────────────────────────────────╯ " `; @@ -216,17 +198,15 @@ exports[`ToolConfirmationQueue > renders ExitPlanMode tool confirmation with Suc exports[`ToolConfirmationQueue > renders the confirming tool with progress indicator 1`] = ` "╭──────────────────────────────────────────────────────────────────────────────╮ -│ Action Required 1 of 3 │ -│ │ -│ ? ls list files │ -│ │ -│ ls │ -│ Allow execution of: 'ls'? │ +│ ? Shell list files 1 of 3 │ +│ ╭──────────────────────────────────────────────────────────────────────────╮ │ +│ │ ls │ │ +│ ╰──────────────────────────────────────────────────────────────────────────╯ │ +│ Allow execution of [ls]? │ │ │ │ ● 1. Allow once │ │ 2. Allow for this session │ │ 3. No, suggest changes (esc) │ -│ │ ╰──────────────────────────────────────────────────────────────────────────────╯ " `; diff --git a/packages/cli/src/ui/components/messages/DiffRenderer.test.tsx b/packages/cli/src/ui/components/messages/DiffRenderer.test.tsx index 5f75d6e009..aa5a95fd8d 100644 --- a/packages/cli/src/ui/components/messages/DiffRenderer.test.tsx +++ b/packages/cli/src/ui/components/messages/DiffRenderer.test.tsx @@ -48,15 +48,18 @@ index 0000000..e69de29 }, ); await waitFor(() => - expect(mockColorizeCode).toHaveBeenCalledWith({ - code: 'print("hello world")', - language: 'python', - availableHeight: undefined, - maxWidth: 80, - theme: undefined, - settings: expect.anything(), - disableColor: false, - }), + expect(mockColorizeCode).toHaveBeenCalledWith( + expect.objectContaining({ + code: 'print("hello world")', + language: 'python', + availableHeight: undefined, + maxWidth: 80, + theme: undefined, + settings: expect.anything(), + disableColor: false, + paddingX: 0, + }), + ), ); }); @@ -83,15 +86,18 @@ index 0000000..e69de29 }, ); await waitFor(() => - expect(mockColorizeCode).toHaveBeenCalledWith({ - code: 'some content', - language: null, - availableHeight: undefined, - maxWidth: 80, - theme: undefined, - settings: expect.anything(), - disableColor: false, - }), + expect(mockColorizeCode).toHaveBeenCalledWith( + expect.objectContaining({ + code: 'some content', + language: null, + availableHeight: undefined, + maxWidth: 80, + theme: undefined, + settings: expect.anything(), + disableColor: false, + paddingX: 0, + }), + ), ); }); @@ -114,15 +120,18 @@ index 0000000..e69de29 }, ); await waitFor(() => - expect(mockColorizeCode).toHaveBeenCalledWith({ - code: 'some text content', - language: null, - availableHeight: undefined, - maxWidth: 80, - theme: undefined, - settings: expect.anything(), - disableColor: false, - }), + expect(mockColorizeCode).toHaveBeenCalledWith( + expect.objectContaining({ + code: 'some text content', + language: null, + availableHeight: undefined, + maxWidth: 80, + theme: undefined, + settings: expect.anything(), + disableColor: false, + paddingX: 0, + }), + ), ); }); diff --git a/packages/cli/src/ui/components/messages/DiffRenderer.tsx b/packages/cli/src/ui/components/messages/DiffRenderer.tsx index ddee2e55df..3eaadf8365 100644 --- a/packages/cli/src/ui/components/messages/DiffRenderer.tsx +++ b/packages/cli/src/ui/components/messages/DiffRenderer.tsx @@ -32,6 +32,7 @@ export function parseDiffWithLineNumbers(diffContent: string): DiffLine[] { for (const line of lines) { const hunkMatch = line.match(hunkHeaderRegex); if (hunkMatch) { + currentOldLine = parseInt(hunkMatch[1], 10); currentOldLine = parseInt(hunkMatch[1], 10); currentNewLine = parseInt(hunkMatch[2], 10); inHunk = true; @@ -89,6 +90,7 @@ interface DiffRendererProps { terminalWidth: number; theme?: Theme; disableColor?: boolean; + paddingX?: number; } const DEFAULT_TAB_WIDTH = 4; // Spaces per tab for normalization @@ -101,6 +103,7 @@ export const DiffRenderer: React.FC = ({ terminalWidth, theme, disableColor = false, + paddingX = 0, }) => { const settings = useSettings(); @@ -122,11 +125,7 @@ export const DiffRenderer: React.FC = ({ if (parsedLines.length === 0) { return ( - + No changes detected. ); @@ -162,12 +161,14 @@ export const DiffRenderer: React.FC = ({ theme, settings, disableColor, + paddingX, }); } else { const key = filename ? `diff-box-${filename}` : undefined; return ( = ({ settings, tabWidth, disableColor, + paddingX, ]); return renderedOutput; @@ -239,12 +241,7 @@ export const renderDiffLines = ({ if (displayableLines.length === 0) { return [ - + No changes detected. , ]; diff --git a/packages/cli/src/ui/components/messages/RedirectionConfirmation.test.tsx b/packages/cli/src/ui/components/messages/RedirectionConfirmation.test.tsx index 95f0cffb69..2b09401e55 100644 --- a/packages/cli/src/ui/components/messages/RedirectionConfirmation.test.tsx +++ b/packages/cli/src/ui/components/messages/RedirectionConfirmation.test.tsx @@ -42,6 +42,7 @@ describe('ToolConfirmationMessage Redirection', () => { getPreferredEditor={vi.fn()} availableTerminalHeight={30} terminalWidth={100} + toolName="shell" />, ); diff --git a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx index f04b47a63e..3a3a4df557 100644 --- a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx +++ b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx @@ -62,6 +62,7 @@ describe('ToolConfirmationMessage', () => { getPreferredEditor={vi.fn()} availableTerminalHeight={30} terminalWidth={80} + toolName="shell" />, ); @@ -88,6 +89,7 @@ describe('ToolConfirmationMessage', () => { getPreferredEditor={vi.fn()} availableTerminalHeight={30} terminalWidth={80} + toolName="shell" />, ); @@ -111,6 +113,7 @@ describe('ToolConfirmationMessage', () => { getPreferredEditor={vi.fn()} availableTerminalHeight={30} terminalWidth={80} + toolName="shell" />, ); @@ -140,6 +143,7 @@ describe('ToolConfirmationMessage', () => { getPreferredEditor={vi.fn()} availableTerminalHeight={30} terminalWidth={80} + toolName="shell" />, ); @@ -169,6 +173,7 @@ describe('ToolConfirmationMessage', () => { getPreferredEditor={vi.fn()} availableTerminalHeight={30} terminalWidth={80} + toolName="shell" />, ); @@ -197,6 +202,7 @@ describe('ToolConfirmationMessage', () => { getPreferredEditor={vi.fn()} availableTerminalHeight={30} terminalWidth={80} + toolName="shell" />, ); @@ -225,6 +231,7 @@ describe('ToolConfirmationMessage', () => { getPreferredEditor={vi.fn()} availableTerminalHeight={30} terminalWidth={80} + toolName="shell" />, ); @@ -253,6 +260,7 @@ describe('ToolConfirmationMessage', () => { getPreferredEditor={vi.fn()} availableTerminalHeight={30} terminalWidth={80} + toolName="shell" />, ); await result.waitUntilReady(); @@ -338,6 +346,7 @@ describe('ToolConfirmationMessage', () => { getPreferredEditor={vi.fn()} availableTerminalHeight={30} terminalWidth={80} + toolName="shell" />, ); @@ -361,6 +370,7 @@ describe('ToolConfirmationMessage', () => { getPreferredEditor={vi.fn()} availableTerminalHeight={30} terminalWidth={80} + toolName="shell" />, ); @@ -396,6 +406,7 @@ describe('ToolConfirmationMessage', () => { getPreferredEditor={vi.fn()} availableTerminalHeight={30} terminalWidth={80} + toolName="shell" />, { settings: createMockSettings({ @@ -423,6 +434,7 @@ describe('ToolConfirmationMessage', () => { getPreferredEditor={vi.fn()} availableTerminalHeight={30} terminalWidth={80} + toolName="shell" />, { settings: createMockSettings({ @@ -474,6 +486,7 @@ describe('ToolConfirmationMessage', () => { getPreferredEditor={vi.fn()} availableTerminalHeight={30} terminalWidth={80} + toolName="shell" />, ); @@ -505,6 +518,7 @@ describe('ToolConfirmationMessage', () => { getPreferredEditor={vi.fn()} availableTerminalHeight={30} terminalWidth={80} + toolName="shell" />, ); @@ -536,6 +550,7 @@ describe('ToolConfirmationMessage', () => { getPreferredEditor={vi.fn()} availableTerminalHeight={30} terminalWidth={80} + toolName="shell" />, ); @@ -562,6 +577,7 @@ describe('ToolConfirmationMessage', () => { getPreferredEditor={vi.fn()} availableTerminalHeight={30} terminalWidth={80} + toolName="shell" />, ); @@ -607,6 +623,7 @@ describe('ToolConfirmationMessage', () => { getPreferredEditor={vi.fn()} availableTerminalHeight={30} terminalWidth={80} + toolName="shell" />, ); @@ -638,6 +655,7 @@ describe('ToolConfirmationMessage', () => { getPreferredEditor={vi.fn()} availableTerminalHeight={30} terminalWidth={80} + toolName="shell" />, ); @@ -672,13 +690,14 @@ describe('ToolConfirmationMessage', () => { getPreferredEditor={vi.fn()} availableTerminalHeight={40} terminalWidth={80} + toolName="shell" />, ); await waitUntilReady(); const outputLines = lastFrame().split('\n'); - // Should use the entire terminal height minus 1 line for the "Press Ctrl+O to show more lines" hint - expect(outputLines.length).toBe(39); + // Should use the entire terminal height + expect(outputLines.length).toBe(40); await expect({ lastFrame, generateSvg }).toMatchSvgSnapshot(); unmount(); @@ -712,13 +731,14 @@ describe('ToolConfirmationMessage', () => { getPreferredEditor={vi.fn()} availableTerminalHeight={40} terminalWidth={80} + toolName="shell" />, ); await waitUntilReady(); const outputLines = lastFrame().split('\n'); - // Should use the entire terminal height minus 1 line for the "Press Ctrl+O to show more lines" hint - expect(outputLines.length).toBe(39); + // Should use the entire terminal height + expect(outputLines.length).toBe(40); await expect({ lastFrame, generateSvg }).toMatchSvgSnapshot(); unmount(); @@ -761,6 +781,7 @@ describe('ToolConfirmationMessage', () => { getPreferredEditor={vi.fn()} availableTerminalHeight={30} terminalWidth={80} + toolName="shell" />, ); diff --git a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx index fa565bc103..b23282959e 100644 --- a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx +++ b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx @@ -24,13 +24,14 @@ import { RadioButtonSelect, type RadioSelectItem, } from '../shared/RadioButtonSelect.js'; -import { MaxSizedBox, MINIMUM_MAX_HEIGHT } from '../shared/MaxSizedBox.js'; +import { MaxSizedBox } from '../shared/MaxSizedBox.js'; import { sanitizeForDisplay, stripUnsafeCharacters, } from '../../utils/textUtils.js'; import { useKeypress } from '../../hooks/useKeypress.js'; import { theme } from '../../semantic-colors.js'; +import { themeManager } from '../../themes/theme-manager.js'; import { useSettings } from '../../contexts/SettingsContext.js'; import { Command } from '../../key/keyMatchers.js'; import { formatCommand } from '../../key/keybindingUtils.js'; @@ -44,6 +45,7 @@ import { type DeceptiveUrlDetails, } from '../../utils/urlSecurityUtils.js'; import { useKeyMatchers } from '../../hooks/useKeyMatchers.js'; +import { isShellTool } from './ToolShared.js'; export interface ToolConfirmationMessageProps { callId: string; @@ -53,13 +55,9 @@ export interface ToolConfirmationMessageProps { isFocused?: boolean; availableTerminalHeight?: number; terminalWidth: number; + toolName: string; } -const REDIRECTION_WARNING_NOTE_LABEL = 'Note: '; -const REDIRECTION_WARNING_NOTE_TEXT = - 'Command contains redirection which can be undesirable.'; -const REDIRECTION_WARNING_TIP_LABEL = 'Tip: '; // Padded to align with "Note: " - export const ToolConfirmationMessage: React.FC< ToolConfirmationMessageProps > = ({ @@ -70,6 +68,7 @@ export const ToolConfirmationMessage: React.FC< isFocused = true, availableTerminalHeight, terminalWidth, + toolName, }) => { const keyMatchers = useKeyMatchers(); const { confirm, isDiffingEnabled } = useToolActions(); @@ -152,6 +151,7 @@ export const ToolConfirmationMessage: React.FC< }, []); const settings = useSettings(); + const activeTheme = themeManager.getActiveTheme(); const allowPermanentApproval = settings.merged.security.enablePermanentToolApproval && !config.getDisableAlwaysAllow(); @@ -254,8 +254,6 @@ export const ToolConfirmationMessage: React.FC< return true; } if (keyMatchers[Command.QUIT](key)) { - // Return false to let ctrl-C bubble up to AppContainer for exit flow. - // AppContainer will call cancelOngoingRequest which will cancel the tool. return false; } return false; @@ -398,7 +396,6 @@ export const ToolConfirmationMessage: React.FC< key: 'No, suggest changes (esc)', }); } else if (confirmationDetails.type === 'mcp') { - // mcp tool confirmation options.push({ label: 'Allow once', value: ToolConfirmationOutcome.ProceedOnce, @@ -449,40 +446,66 @@ export const ToolConfirmationMessage: React.FC< // Calculate the vertical space (in lines) consumed by UI elements // surrounding the main body content. - const PADDING_OUTER_Y = 1; // Main container has `paddingBottom={1}`. - const HEIGHT_QUESTION = 1; // The question text is one line. - const MARGIN_QUESTION_BOTTOM = 1; // Margin on the question container. - const SECURITY_WARNING_BOTTOM_MARGIN = 1; // Margin on the securityWarnings container. - const SHOW_MORE_LINES_HEIGHT = 1; // The "Press Ctrl+O to show more lines" hint. + const PADDING_OUTER_Y = 0; + const HEIGHT_QUESTION = 1; + const MARGIN_QUESTION_TOP = 0; + const MARGIN_QUESTION_BOTTOM = 1; + const SECURITY_WARNING_BOTTOM_MARGIN = 1; + const SHOW_MORE_LINES_HEIGHT = 1; const optionsCount = getOptions().length; - // The measured height includes the margin inside WarningMessage (1 line). - // We also add 1 line for the marginBottom on the securityWarnings container. const securityWarningsHeight = deceptiveUrlWarningText ? measuredSecurityWarningsHeight + SECURITY_WARNING_BOTTOM_MARGIN : 0; + let extraInfoLines = 0; + if (confirmationDetails.type === 'sandbox_expansion') { + const { additionalPermissions } = confirmationDetails; + if (additionalPermissions?.network) extraInfoLines++; + extraInfoLines += additionalPermissions?.fileSystem?.read?.length || 0; + extraInfoLines += additionalPermissions?.fileSystem?.write?.length || 0; + } else if (confirmationDetails.type === 'exec') { + const executionProps = confirmationDetails; + const commandsToDisplay = + executionProps.commands && executionProps.commands.length > 0 + ? executionProps.commands + : [executionProps.command]; + const containsRedirection = commandsToDisplay.some((cmd) => + hasRedirection(cmd), + ); + const isAutoEdit = + config.getApprovalMode() === ApprovalMode.YOLO || + config.getApprovalMode() === ApprovalMode.AUTO_EDIT; + if (containsRedirection && !isAutoEdit) { + extraInfoLines = 1; // Warning line + } + } + const surroundingElementsHeight = PADDING_OUTER_Y + HEIGHT_QUESTION + + MARGIN_QUESTION_TOP + MARGIN_QUESTION_BOTTOM + SHOW_MORE_LINES_HEIGHT + optionsCount + - securityWarningsHeight; + securityWarningsHeight + + extraInfoLines; - return Math.max(availableTerminalHeight - surroundingElementsHeight, 1); + return Math.max(availableTerminalHeight - surroundingElementsHeight, 2); }, [ availableTerminalHeight, handlesOwnUI, getOptions, measuredSecurityWarningsHeight, deceptiveUrlWarningText, + confirmationDetails, + config, ]); const { question, bodyContent, options, securityWarnings, initialIndex } = useMemo<{ - question: string; + question: React.ReactNode; bodyContent: React.ReactNode; options: Array>; securityWarnings: React.ReactNode; @@ -490,7 +513,7 @@ export const ToolConfirmationMessage: React.FC< }>(() => { let bodyContent: React.ReactNode | null = null; let securityWarnings: React.ReactNode | null = null; - let question = ''; + let question: React.ReactNode = ''; const options = getOptions(); let initialIndex = 0; @@ -519,6 +542,8 @@ export const ToolConfirmationMessage: React.FC< securityWarnings = ; } + const bodyHeight = availableBodyContentHeight(); + if (confirmationDetails.type === 'ask_user') { bodyContent = ( ); return { @@ -563,7 +588,7 @@ export const ToolConfirmationMessage: React.FC< handleConfirm(ToolConfirmationOutcome.Cancel); }} width={terminalWidth} - availableHeight={availableBodyContentHeight()} + availableHeight={bodyHeight} /> ); return { @@ -578,85 +603,109 @@ export const ToolConfirmationMessage: React.FC< if (confirmationDetails.type === 'edit') { if (!confirmationDetails.isModifying) { question = `Apply this change?`; - } - } else if (confirmationDetails.type === 'sandbox_expansion') { - question = `Allow sandbox expansion for: '${sanitizeForDisplay(confirmationDetails.rootCommand)}'?`; - } else if (confirmationDetails.type === 'exec') { - const executionProps = confirmationDetails; - - if (executionProps.commands && executionProps.commands.length > 1) { - question = `Allow execution of ${executionProps.commands.length} commands?`; - } else { - question = `Allow execution of: '${sanitizeForDisplay(executionProps.rootCommand)}'?`; - } - } else if (confirmationDetails.type === 'info') { - question = `Do you want to proceed?`; - } else if (confirmationDetails.type === 'mcp') { - // mcp tool confirmation - const mcpProps = confirmationDetails; - question = `Allow execution of MCP tool "${sanitizeForDisplay(mcpProps.toolName)}" from server "${sanitizeForDisplay(mcpProps.serverName)}"?`; - } - - if (confirmationDetails.type === 'edit') { - if (!confirmationDetails.isModifying) { bodyContent = ( - + <> + + + + ); } } else if (confirmationDetails.type === 'sandbox_expansion') { - const { additionalPermissions } = confirmationDetails; + const { additionalPermissions, command, rootCommand } = + confirmationDetails; const readPaths = additionalPermissions?.fileSystem?.read || []; const writePaths = additionalPermissions?.fileSystem?.write || []; const network = additionalPermissions?.network; + const isShell = isShellTool(toolName); + + const rootCmds = rootCommand + .split(',') + .map((c) => c.trim().split(/\s+/)[0]) + .filter((c) => c && !c.startsWith('redirection')); + const commandNames = Array.from(new Set(rootCmds)).join(', '); + question = ''; bodyContent = ( - - - The agent is requesting additional sandbox permissions to execute - this command: - - - - {sanitizeForDisplay(confirmationDetails.command)} - + <> + + {colorizeCode({ + code: command.trim(), + language: 'bash', + maxWidth: Math.max(terminalWidth, 1) - 6, + settings, + theme: activeTheme, + hideLineNumbers: true, + availableHeight: + bodyHeight !== undefined + ? Math.max(bodyHeight - 2, 2) + : undefined, + })} - {network && ( - - • Network Access - - )} - {readPaths.length > 0 && ( - - • Read Access: - {readPaths.map((p, i) => ( - - {' '} - {sanitizeForDisplay(p)} - - ))} - - )} - {writePaths.length > 0 && ( - - • Write Access: - {writePaths.map((p, i) => ( - - {' '} - {sanitizeForDisplay(p)} - - ))} - - )} - + + + To run{' '} + + [{sanitizeForDisplay(commandNames)}] + + , allow access to the following? + + {network && ( + + + • Network: + {' '} + All Urls + + )} + {writePaths.length > 0 && ( + + + • Write: + {' '} + {writePaths.map((p) => sanitizeForDisplay(p)).join(', ')} + + )} + {readPaths.length > 0 && ( + + + • Read: + {' '} + {readPaths.map((p) => sanitizeForDisplay(p)).join(', ')} + + )} + + ); } else if (confirmationDetails.type === 'exec') { const executionProps = confirmationDetails; - + const isShell = isShellTool(toolName); const commandsToDisplay = executionProps.commands && executionProps.commands.length > 1 ? executionProps.commands @@ -664,80 +713,96 @@ export const ToolConfirmationMessage: React.FC< const containsRedirection = commandsToDisplay.some((cmd) => hasRedirection(cmd), ); + const isAutoEdit = + config.getApprovalMode() === ApprovalMode.YOLO || + config.getApprovalMode() === ApprovalMode.AUTO_EDIT; - let bodyContentHeight = availableBodyContentHeight(); let warnings: React.ReactNode = null; - - const isAutoEdit = config.getApprovalMode() === ApprovalMode.AUTO_EDIT; if (containsRedirection && !isAutoEdit) { - // Calculate lines needed for Note and Tip - const safeWidth = Math.max(terminalWidth, 1); - const noteLength = - REDIRECTION_WARNING_NOTE_LABEL.length + - REDIRECTION_WARNING_NOTE_TEXT.length; - const tipText = `Toggle auto-edit (${formatCommand(Command.CYCLE_APPROVAL_MODE)}) to allow redirection in the future.`; - const tipLength = - REDIRECTION_WARNING_TIP_LABEL.length + tipText.length; - - const noteLines = Math.ceil(noteLength / safeWidth); - const tipLines = Math.ceil(tipLength / safeWidth); - const spacerLines = 1; - const warningHeight = noteLines + tipLines + spacerLines; - - if (bodyContentHeight !== undefined) { - bodyContentHeight = Math.max( - bodyContentHeight - warningHeight, - MINIMUM_MAX_HEIGHT, - ); - } - + const tipText = `To auto-accept, press ${formatCommand(Command.CYCLE_APPROVAL_MODE)}`; warnings = ( - <> - - - - {REDIRECTION_WARNING_NOTE_LABEL} - {REDIRECTION_WARNING_NOTE_TEXT} - - - - - {REDIRECTION_WARNING_TIP_LABEL} - {tipText} - - - + + + Redirection detected.{' '} + {tipText} + + ); } - bodyContent = ( - - cmd.trim().split(/\s+/)[0]) + .filter(Boolean), + ), + ).join(', '); + + const allowQuestion = ( + + Allow execution of{' '} + - - {commandsToDisplay.map((cmd, idx) => ( - - {colorizeCode({ - code: cmd, - language: 'bash', - maxWidth: Math.max(terminalWidth, 1), - settings, - hideLineNumbers: true, - })} - - ))} - - + [{sanitizeForDisplay(commandNames)}] + + {'?'} + + ); + + question = ( + + {allowQuestion} {warnings} ); + + bodyContent = ( + <> + + + + {commandsToDisplay.map((cmd, idx) => ( + + {colorizeCode({ + code: cmd.trim(), + language: 'bash', + maxWidth: Math.max(terminalWidth, 1) - 6, + settings, + theme: activeTheme, + hideLineNumbers: true, + availableHeight: + bodyHeight !== undefined + ? Math.max(bodyHeight - 2, 2) + : undefined, + })} + + ))} + + + + + ); } else if (confirmationDetails.type === 'info') { + question = `Do you want to proceed?`; const infoProps = confirmationDetails; const displayUrls = infoProps.urls && @@ -768,8 +833,8 @@ export const ToolConfirmationMessage: React.FC< ); } else if (confirmationDetails.type === 'mcp') { - // mcp tool confirmation const mcpProps = confirmationDetails; + question = `Allow execution of MCP tool "${sanitizeForDisplay(mcpProps.toolName)}" from server "${sanitizeForDisplay(mcpProps.serverName)}"?`; bodyContent = ( @@ -790,7 +855,26 @@ export const ToolConfirmationMessage: React.FC< (press {expandDetailsHintKey} to collapse MCP tool details) - {mcpToolDetailsText} + + {colorizeCode({ + code: mcpToolDetailsText || '', + language: 'json', + maxWidth: Math.max(terminalWidth, 1) - 4, + settings, + theme: activeTheme, + hideLineNumbers: true, + availableHeight: + bodyHeight !== undefined + ? Math.max(bodyHeight - 2, 2) + : undefined, + })} + ) : ( @@ -819,7 +903,9 @@ export const ToolConfirmationMessage: React.FC< isTrustedFolder, allowPermanentApproval, settings, + activeTheme, config, + toolName, ]); const bodyOverflowDirection: 'top' | 'bottom' = @@ -827,6 +913,30 @@ export const ToolConfirmationMessage: React.FC< ? 'bottom' : 'top'; + const renderRadioItem = useCallback( + ( + item: RadioSelectItem, + { titleColor }: { titleColor: string }, + ) => { + if (item.value === ToolConfirmationOutcome.ProceedAlwaysAndSave) { + return ( + + {item.label}{' '} + + ~/.gemini/policies/auto-saved.toml + + + ); + } + return ( + + {item.label} + + ); + }, + [], + ); + if (confirmationDetails.type === 'edit') { if (confirmationDetails.isModifying) { return ( @@ -849,13 +959,8 @@ export const ToolConfirmationMessage: React.FC< } return ( - - {/* System message from hook */} - {confirmationDetails.systemMessage && ( + + {!!confirmationDetails.systemMessage && ( {confirmationDetails.systemMessage} @@ -867,7 +972,11 @@ export const ToolConfirmationMessage: React.FC< bodyContent ) : ( <> - + )} - - {question} - + {!!question && ( + + {typeof question === 'string' ? ( + {question} + ) : ( + question + )} + + )} diff --git a/packages/cli/src/ui/components/messages/__snapshots__/DiffRenderer.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/DiffRenderer.test.tsx.snap index fed8b32bd0..7a36d3f840 100644 --- a/packages/cli/src/ui/components/messages/__snapshots__/DiffRenderer.test.tsx.snap +++ b/packages/cli/src/ui/components/messages/__snapshots__/DiffRenderer.test.tsx.snap @@ -50,11 +50,8 @@ exports[` > with useAlterna `; exports[` > with useAlternateBuffer = false > should handle diff with only header and no changes 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ │ -│ No changes detected. │ -│ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ +" + No changes detected. " `; @@ -143,11 +140,8 @@ exports[` > with useAlterna `; exports[` > with useAlternateBuffer = true > should handle diff with only header and no changes 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ │ -│ No changes detected. │ -│ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ +" + No changes detected. " `; diff --git a/packages/cli/src/ui/components/messages/__snapshots__/RedirectionConfirmation.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/RedirectionConfirmation.test.tsx.snap index f584e7f483..1694ca2350 100644 --- a/packages/cli/src/ui/components/messages/__snapshots__/RedirectionConfirmation.test.tsx.snap +++ b/packages/cli/src/ui/components/messages/__snapshots__/RedirectionConfirmation.test.tsx.snap @@ -1,11 +1,11 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html exports[`ToolConfirmationMessage Redirection > should display redirection warning and tip for redirected commands 1`] = ` -"echo "hello" > test.txt - -Note: Command contains redirection which can be undesirable. -Tip: Toggle auto-edit (Shift+Tab) to allow redirection in the future. -Allow execution of: 'echo, redirection (>)'? +"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ echo "hello" > test.txt │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ +Allow execution of [echo]? +Redirection detected. To auto-accept, press Shift+Tab ● 1. Allow once 2. Allow for this session diff --git a/packages/cli/src/ui/components/messages/__snapshots__/ToolConfirmationMessage-ToolConfirmationMessage-height-allocation-and-layout-should-expand-to-available-height-for-large-edit-diffs.snap.svg b/packages/cli/src/ui/components/messages/__snapshots__/ToolConfirmationMessage-ToolConfirmationMessage-height-allocation-and-layout-should-expand-to-available-height-for-large-edit-diffs.snap.svg index 4c570fb451..ffc73fdd5e 100644 --- a/packages/cli/src/ui/components/messages/__snapshots__/ToolConfirmationMessage-ToolConfirmationMessage-height-allocation-and-layout-should-expand-to-available-height-for-large-edit-diffs.snap.svg +++ b/packages/cli/src/ui/components/messages/__snapshots__/ToolConfirmationMessage-ToolConfirmationMessage-height-allocation-and-layout-should-expand-to-available-height-for-large-edit-diffs.snap.svg @@ -1,468 +1,517 @@ - + - + - ... first 9 lines hidden (Ctrl+O to show) ... - - - 5 - - - + - - - const - - newLine5 = - - true - - ; - - - 6 + ╭──────────────────────────────────────────────────────────────────────────────╮ + + ... 10 hidden (Ctrl+O) ... + + - - + 6 - - const - - oldLine6 = - - true - - ; - - - 6 + + - + + + const + + oldLine6 = + + true + + ; + + - + + 6 - - const - - newLine6 = - - true - - ; - - - 7 + + + + + + const + + newLine6 = + + true + + ; + + - - + 7 - - const - - oldLine7 = - - true - - ; - - - 7 + + - + + + const + + oldLine7 = + + true + + ; + + - + + 7 - - const - - newLine7 = - - true - - ; - - - 8 + + + + + + const + + newLine7 = + + true + + ; + + - - + 8 - - const - - oldLine8 = - - true - - ; - - - 8 + + - + + + const + + oldLine8 = + + true + + ; + + - + + 8 - - const - - newLine8 = - - true - - ; - - - 9 + + + + + + const + + newLine8 = + + true + + ; + + - - + 9 - - const - - oldLine9 = - - true - - ; - - - 9 + + - + + + const + + oldLine9 = + + true + + ; + + - + + 9 - - const - - newLine9 = - - true - - ; - - 10 - - - - + + + + + + const + + newLine9 = + + true + + ; + + + + 10 - - const - - oldLine10 = - - true - - ; - - 10 - - - + + + - + + + const + + oldLine10 = + + true + + ; + + + + 10 - - const - - newLine10 = - - true - - ; - - 11 - - - - + + + + + + const + + newLine10 = + + true + + ; + + + + 11 - - const - - oldLine11 = - - true - - ; - - 11 - - - + + + - + + + const + + oldLine11 = + + true + + ; + + + + 11 - - const - - newLine11 = - - true - - ; - - 12 - - - - + + + + + + const + + newLine11 = + + true + + ; + + + + 12 - - const - - oldLine12 = - - true - - ; - - 12 - - - + + + - + + + const + + oldLine12 = + + true + + ; + + + + 12 - - const - - newLine12 = - - true - - ; - - 13 - - - - + + + + + + const + + newLine12 = + + true + + ; + + + + 13 - - const - - oldLine13 = - - true - - ; - - 13 - - - + + + - + + + const + + oldLine13 = + + true + + ; + + + + 13 - - const - - newLine13 = - - true - - ; - - 14 - - - - + + + + + + const + + newLine13 = + + true + + ; + + + + 14 - - const - - oldLine14 = - - true - - ; - - 14 - - - + + + - + + + const + + oldLine14 = + + true + + ; + + + + 14 - - const - - newLine14 = - - true - - ; - - 15 - - - - + + + + + + const + + newLine14 = + + true + + ; + + + + 15 - - const - - oldLine15 = - - true - - ; - - 15 - - - + + + - + + + const + + oldLine15 = + + true + + ; + + + + 15 - - const - - newLine15 = - - true - - ; - - 16 - - - - + + + + + + const + + newLine15 = + + true + + ; + + + + 16 - - const - - oldLine16 = - - true - - ; - - 16 - - - + + + - + + + const + + oldLine16 = + + true + + ; + + + + 16 - - const - - newLine16 = - - true - - ; - - 17 - - - - + + + + + + const + + newLine16 = + + true + + ; + + + + 17 - - const - - oldLine17 = - - true - - ; - - 17 - - - + + + - + + + const + + oldLine17 = + + true + + ; + + + + 17 - - const - - newLine17 = - - true - - ; - - 18 - - - - + + + + + + const + + newLine17 = + + true + + ; + + + + 18 - - const - - oldLine18 = - - true - - ; - - 18 - - - + + + - + + + const + + oldLine18 = + + true + + ; + + + + 18 - - const - - newLine18 = - - true - - ; - - 19 - - - - + + + + + + const + + newLine18 = + + true + + ; + + + + 19 - - const - - oldLine19 = - - true - - ; - - 19 - - - + + + - + + + const + + oldLine19 = + + true + + ; + + + + 19 - - const - - newLine19 = - - true - - ; - - 20 - - - - + + + + + + const + + newLine19 = + + true + + ; + + + + 20 - - const - - oldLine20 = - - true - - ; - - 20 - - - + + + - + + + const + + oldLine20 = + + true + + ; + + + + 20 - - const - - newLine20 = - - true - - ; - Apply this change? - - - - - 1. - - - Allow once - - 2. - Allow for this session - 3. - Modify with external editor - 4. - No, suggest changes (esc) + + + + + + const + + newLine20 = + + true + + ; + + ╰──────────────────────────────────────────────────────────────────────────────╯ + Apply this change? + + + + + 1. + + + Allow once + + 2. + Allow for this session + 3. + Modify with external editor + 4. + No, suggest changes (esc) \ No newline at end of file diff --git a/packages/cli/src/ui/components/messages/__snapshots__/ToolConfirmationMessage-ToolConfirmationMessage-height-allocation-and-layout-should-expand-to-available-height-for-large-exec-commands.snap.svg b/packages/cli/src/ui/components/messages/__snapshots__/ToolConfirmationMessage-ToolConfirmationMessage-height-allocation-and-layout-should-expand-to-available-height-for-large-exec-commands.snap.svg index 4b34a3405f..68e2eb2247 100644 --- a/packages/cli/src/ui/components/messages/__snapshots__/ToolConfirmationMessage-ToolConfirmationMessage-height-allocation-and-layout-should-expand-to-available-height-for-large-exec-commands.snap.svg +++ b/packages/cli/src/ui/components/messages/__snapshots__/ToolConfirmationMessage-ToolConfirmationMessage-height-allocation-and-layout-should-expand-to-available-height-for-large-exec-commands.snap.svg @@ -1,87 +1,151 @@ - + - + - ... first 18 lines hidden (Ctrl+O to show) ... - echo - "Line 19" - echo - "Line 20" - echo - "Line 21" - echo - "Line 22" - echo - "Line 23" - echo - "Line 24" - echo - "Line 25" - echo - "Line 26" - echo - "Line 27" - echo - "Line 28" - echo - "Line 29" - echo - "Line 30" - echo - "Line 31" - echo - "Line 32" - echo - "Line 33" - echo - "Line 34" - echo - "Line 35" - echo - "Line 36" - echo - "Line 37" - echo - "Line 38" - echo - "Line 39" - echo - "Line 40" - echo - "Line 41" - echo - "Line 42" - echo - "Line 43" - echo - "Line 44" - echo - "Line 45" - echo - "Line 46" - echo - "Line 47" - echo - "Line 48" - echo - "Line 49" - echo - "Line 50" - Allow execution of: 'echo'? - - - - - 1. - - - Allow once - - 2. - Allow for this session - 3. - No, suggest changes (esc) + ╭──────────────────────────────────────────────────────────────────────────────╮ + + ... 19 hidden (Ctrl+O) ... + + + echo + "Line 20" + + + echo + "Line 21" + + + echo + "Line 22" + + + echo + "Line 23" + + + echo + "Line 24" + + + echo + "Line 25" + + + echo + "Line 26" + + + echo + "Line 27" + + + echo + "Line 28" + + + echo + "Line 29" + + + echo + "Line 30" + + + echo + "Line 31" + + + echo + "Line 32" + + + echo + "Line 33" + + + echo + "Line 34" + + + echo + "Line 35" + + + echo + "Line 36" + + + echo + "Line 37" + + + echo + "Line 38" + + + echo + "Line 39" + + + echo + "Line 40" + + + echo + "Line 41" + + + echo + "Line 42" + + + echo + "Line 43" + + + echo + "Line 44" + + + echo + "Line 45" + + + echo + "Line 46" + + + echo + "Line 47" + + + echo + "Line 48" + + + echo + "Line 49" + + + echo + "Line 50" + + ╰──────────────────────────────────────────────────────────────────────────────╯ + Allow execution of [echo]? + + + + + 1. + + + Allow once + + 2. + Allow for this session + 3. + No, suggest changes (esc) \ No newline at end of file diff --git a/packages/cli/src/ui/components/messages/__snapshots__/ToolConfirmationMessage-ToolConfirmationMessage-should-render-multiline-shell-scripts-with-correct-newlines-and-syntax-highlighting.snap.svg b/packages/cli/src/ui/components/messages/__snapshots__/ToolConfirmationMessage-ToolConfirmationMessage-should-render-multiline-shell-scripts-with-correct-newlines-and-syntax-highlighting.snap.svg index d1396e2335..a30b871f41 100644 --- a/packages/cli/src/ui/components/messages/__snapshots__/ToolConfirmationMessage-ToolConfirmationMessage-should-render-multiline-shell-scripts-with-correct-newlines-and-syntax-highlighting.snap.svg +++ b/packages/cli/src/ui/components/messages/__snapshots__/ToolConfirmationMessage-ToolConfirmationMessage-should-render-multiline-shell-scripts-with-correct-newlines-and-syntax-highlighting.snap.svg @@ -1,32 +1,42 @@ - + - + - echo - "hello" - for - i - in - 1 2 3; - do - echo - $i - done - Allow execution of: 'echo'? - - - - - 1. - - - Allow once - - 2. - Allow for this session - 3. - No, suggest changes (esc) + ╭──────────────────────────────────────────────────────────────────────────────╮ + + echo + "hello" + + + for + i + in + 1 2 3; + do + + + echo + $i + + + done + + ╰──────────────────────────────────────────────────────────────────────────────╯ + Allow execution of [echo]? + + + + + 1. + + + Allow once + + 2. + Allow for this session + 3. + No, suggest changes (esc) \ No newline at end of file diff --git a/packages/cli/src/ui/components/messages/__snapshots__/ToolConfirmationMessage.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/ToolConfirmationMessage.test.tsx.snap index eb9f856b0b..6d33b6fbfb 100644 --- a/packages/cli/src/ui/components/messages/__snapshots__/ToolConfirmationMessage.test.tsx.snap +++ b/packages/cli/src/ui/components/messages/__snapshots__/ToolConfirmationMessage.test.tsx.snap @@ -3,52 +3,53 @@ exports[`ToolConfirmationMessage > enablePermanentToolApproval setting > should show "Allow for all future sessions" when trusted 1`] = ` "╭──────────────────────────────────────────────────────────────────────────────╮ │ │ -│ No changes detected. │ +│ No changes detected. │ │ │ ╰──────────────────────────────────────────────────────────────────────────────╯ Apply this change? -● 1. Allow once +● 1. Allow once 2. Allow for this session - 3. Allow for this file in all future sessions + 3. Allow for this file in all future sessions ~/.gemini/policies/auto-saved.toml 4. Modify with external editor 5. No, suggest changes (esc) " `; exports[`ToolConfirmationMessage > height allocation and layout > should expand to available height for large edit diffs 1`] = ` -"... first 9 lines hidden (Ctrl+O to show) ... - 5 + const newLine5 = true; - 6 - const oldLine6 = true; - 6 + const newLine6 = true; - 7 - const oldLine7 = true; - 7 + const newLine7 = true; - 8 - const oldLine8 = true; - 8 + const newLine8 = true; - 9 - const oldLine9 = true; - 9 + const newLine9 = true; -10 - const oldLine10 = true; -10 + const newLine10 = true; -11 - const oldLine11 = true; -11 + const newLine11 = true; -12 - const oldLine12 = true; -12 + const newLine12 = true; -13 - const oldLine13 = true; -13 + const newLine13 = true; -14 - const oldLine14 = true; -14 + const newLine14 = true; -15 - const oldLine15 = true; -15 + const newLine15 = true; -16 - const oldLine16 = true; -16 + const newLine16 = true; -17 - const oldLine17 = true; -17 + const newLine17 = true; -18 - const oldLine18 = true; -18 + const newLine18 = true; -19 - const oldLine19 = true; -19 + const newLine19 = true; -20 - const oldLine20 = true; -20 + const newLine20 = true; +"╭──────────────────────────────────────────────────────────────────────────────╮ +│ ... 10 hidden (Ctrl+O) ... │ +│ 6 - const oldLine6 = true; │ +│ 6 + const newLine6 = true; │ +│ 7 - const oldLine7 = true; │ +│ 7 + const newLine7 = true; │ +│ 8 - const oldLine8 = true; │ +│ 8 + const newLine8 = true; │ +│ 9 - const oldLine9 = true; │ +│ 9 + const newLine9 = true; │ +│ 10 - const oldLine10 = true; │ +│ 10 + const newLine10 = true; │ +│ 11 - const oldLine11 = true; │ +│ 11 + const newLine11 = true; │ +│ 12 - const oldLine12 = true; │ +│ 12 + const newLine12 = true; │ +│ 13 - const oldLine13 = true; │ +│ 13 + const newLine13 = true; │ +│ 14 - const oldLine14 = true; │ +│ 14 + const newLine14 = true; │ +│ 15 - const oldLine15 = true; │ +│ 15 + const newLine15 = true; │ +│ 16 - const oldLine16 = true; │ +│ 16 + const newLine16 = true; │ +│ 17 - const oldLine17 = true; │ +│ 17 + const newLine17 = true; │ +│ 18 - const oldLine18 = true; │ +│ 18 + const newLine18 = true; │ +│ 19 - const oldLine19 = true; │ +│ 19 + const newLine19 = true; │ +│ 20 - const oldLine20 = true; │ +│ 20 + const newLine20 = true; │ +╰──────────────────────────────────────────────────────────────────────────────╯ Apply this change? ● 1. Allow once @@ -59,40 +60,41 @@ Apply this change? `; exports[`ToolConfirmationMessage > height allocation and layout > should expand to available height for large exec commands 1`] = ` -"... first 18 lines hidden (Ctrl+O to show) ... -echo "Line 19" -echo "Line 20" -echo "Line 21" -echo "Line 22" -echo "Line 23" -echo "Line 24" -echo "Line 25" -echo "Line 26" -echo "Line 27" -echo "Line 28" -echo "Line 29" -echo "Line 30" -echo "Line 31" -echo "Line 32" -echo "Line 33" -echo "Line 34" -echo "Line 35" -echo "Line 36" -echo "Line 37" -echo "Line 38" -echo "Line 39" -echo "Line 40" -echo "Line 41" -echo "Line 42" -echo "Line 43" -echo "Line 44" -echo "Line 45" -echo "Line 46" -echo "Line 47" -echo "Line 48" -echo "Line 49" -echo "Line 50" -Allow execution of: 'echo'? +"╭──────────────────────────────────────────────────────────────────────────────╮ +│ ... 19 hidden (Ctrl+O) ... │ +│ echo "Line 20" │ +│ echo "Line 21" │ +│ echo "Line 22" │ +│ echo "Line 23" │ +│ echo "Line 24" │ +│ echo "Line 25" │ +│ echo "Line 26" │ +│ echo "Line 27" │ +│ echo "Line 28" │ +│ echo "Line 29" │ +│ echo "Line 30" │ +│ echo "Line 31" │ +│ echo "Line 32" │ +│ echo "Line 33" │ +│ echo "Line 34" │ +│ echo "Line 35" │ +│ echo "Line 36" │ +│ echo "Line 37" │ +│ echo "Line 38" │ +│ echo "Line 39" │ +│ echo "Line 40" │ +│ echo "Line 41" │ +│ echo "Line 42" │ +│ echo "Line 43" │ +│ echo "Line 44" │ +│ echo "Line 45" │ +│ echo "Line 46" │ +│ echo "Line 47" │ +│ echo "Line 48" │ +│ echo "Line 49" │ +│ echo "Line 50" │ +╰──────────────────────────────────────────────────────────────────────────────╯ +Allow execution of [echo]? ● 1. Allow once 2. Allow for this session @@ -101,12 +103,14 @@ Allow execution of: 'echo'? `; exports[`ToolConfirmationMessage > should display multiple commands for exec type when provided 1`] = ` -"echo "hello" - -ls -la - -whoami -Allow execution of 3 commands? +"╭──────────────────────────────────────────────────────────────────────────────╮ +│ echo "hello" │ +│ │ +│ ls -la │ +│ │ +│ whoami │ +╰──────────────────────────────────────────────────────────────────────────────╯ +Allow execution of [echo, ls, whoami]? ● 1. Allow once 2. Allow for this session @@ -138,16 +142,17 @@ Do you want to proceed? `; exports[`ToolConfirmationMessage > should render multiline shell scripts with correct newlines and syntax highlighting 1`] = ` -"echo "hello" -for i in 1 2 3; do - echo $i -done -Allow execution of: 'echo'? +"╭──────────────────────────────────────────────────────────────────────────────╮ +│ echo "hello" │ +│ for i in 1 2 3; do │ +│ echo $i │ +│ done │ +╰──────────────────────────────────────────────────────────────────────────────╯ +Allow execution of [echo]? ● 1. Allow once 2. Allow for this session - 3. No, suggest changes (esc) -" + 3. No, suggest changes (esc)" `; exports[`ToolConfirmationMessage > should strip BiDi characters from MCP tool and server names 1`] = ` @@ -165,7 +170,7 @@ Allow execution of MCP tool "testtool" from server "testserver"? exports[`ToolConfirmationMessage > with folder trust > 'for edit confirmations' > should NOT show "allow always" when folder is untrusted 1`] = ` "╭──────────────────────────────────────────────────────────────────────────────╮ │ │ -│ No changes detected. │ +│ No changes detected. │ │ │ ╰──────────────────────────────────────────────────────────────────────────────╯ Apply this change? @@ -179,7 +184,7 @@ Apply this change? exports[`ToolConfirmationMessage > with folder trust > 'for edit confirmations' > should show "allow always" when folder is trusted 1`] = ` "╭──────────────────────────────────────────────────────────────────────────────╮ │ │ -│ No changes detected. │ +│ No changes detected. │ │ │ ╰──────────────────────────────────────────────────────────────────────────────╯ Apply this change? @@ -192,8 +197,10 @@ Apply this change? `; exports[`ToolConfirmationMessage > with folder trust > 'for exec confirmations' > should NOT show "allow always" when folder is untrusted 1`] = ` -"echo "hello" -Allow execution of: 'echo'? +"╭──────────────────────────────────────────────────────────────────────────────╮ +│ echo "hello" │ +╰──────────────────────────────────────────────────────────────────────────────╯ +Allow execution of [echo]? ● 1. Allow once 2. No, suggest changes (esc) @@ -201,8 +208,10 @@ Allow execution of: 'echo'? `; exports[`ToolConfirmationMessage > with folder trust > 'for exec confirmations' > should show "allow always" when folder is trusted 1`] = ` -"echo "hello" -Allow execution of: 'echo'? +"╭──────────────────────────────────────────────────────────────────────────────╮ +│ echo "hello" │ +╰──────────────────────────────────────────────────────────────────────────────╯ +Allow execution of [echo]? ● 1. Allow once 2. Allow for this session diff --git a/packages/cli/src/ui/components/messages/__snapshots__/ToolResultDisplay.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/ToolResultDisplay.test.tsx.snap index 77d99b2792..12eff841b8 100644 --- a/packages/cli/src/ui/components/messages/__snapshots__/ToolResultDisplay.test.tsx.snap +++ b/packages/cli/src/ui/components/messages/__snapshots__/ToolResultDisplay.test.tsx.snap @@ -16,11 +16,8 @@ exports[`ToolResultDisplay > renders ANSI output result 1`] = ` `; exports[`ToolResultDisplay > renders file diff result 1`] = ` -"╭─────────────────────────────────────────────────────────────────────────╮ -│ │ -│ No changes detected. │ -│ │ -╰─────────────────────────────────────────────────────────────────────────╯ +" + No changes detected. " `; diff --git a/packages/cli/src/ui/components/shared/MaxSizedBox.tsx b/packages/cli/src/ui/components/shared/MaxSizedBox.tsx index 7aa40cfc62..baadb3b9d8 100644 --- a/packages/cli/src/ui/components/shared/MaxSizedBox.tsx +++ b/packages/cli/src/ui/components/shared/MaxSizedBox.tsx @@ -26,6 +26,7 @@ export interface MaxSizedBoxProps { maxHeight?: number; overflowDirection?: 'top' | 'bottom'; additionalHiddenLinesCount?: number; + paddingX?: number; } /** @@ -38,6 +39,7 @@ export const MaxSizedBox: React.FC = ({ maxHeight, overflowDirection = 'top', additionalHiddenLinesCount = 0, + paddingX = 0, }) => { const id = useId(); const { addOverflowingId, removeOverflowingId } = useOverflowActions() || {}; @@ -132,11 +134,13 @@ export const MaxSizedBox: React.FC = ({ flexShrink={0} > {totalHiddenLines > 0 && overflowDirection === 'top' && ( - - {isNarrow - ? `... ${totalHiddenLines} hidden (${showMoreKey}) ...` - : `... first ${totalHiddenLines} line${totalHiddenLines === 1 ? '' : 's'} hidden (${showMoreKey} to show) ...`} - + + + {isNarrow + ? `... ${totalHiddenLines} hidden (${showMoreKey}) ...` + : `... first ${totalHiddenLines} line${totalHiddenLines === 1 ? '' : 's'} hidden (${showMoreKey} to show) ...`} + + )} = ({ {totalHiddenLines > 0 && overflowDirection === 'bottom' && ( - - {isNarrow - ? `... ${totalHiddenLines} hidden (${showMoreKey}) ...` - : `... last ${totalHiddenLines} line${totalHiddenLines === 1 ? '' : 's'} hidden (${showMoreKey} to show) ...`} - + + + {isNarrow + ? `... ${totalHiddenLines} hidden (${showMoreKey}) ...` + : `... last ${totalHiddenLines} line${totalHiddenLines === 1 ? '' : 's'} hidden (${showMoreKey} to show) ...`} + + )} ); diff --git a/packages/cli/src/ui/utils/CodeColorizer.tsx b/packages/cli/src/ui/utils/CodeColorizer.tsx index 828e041493..07d6429dbe 100644 --- a/packages/cli/src/ui/utils/CodeColorizer.tsx +++ b/packages/cli/src/ui/utils/CodeColorizer.tsx @@ -136,6 +136,7 @@ export interface ColorizeCodeOptions { hideLineNumbers?: boolean; disableColor?: boolean; returnLines?: boolean; + paddingX?: number; } /** @@ -160,6 +161,7 @@ export function colorizeCode({ hideLineNumbers = false, disableColor = false, returnLines = false, + paddingX = 0, }: ColorizeCodeOptions): React.ReactNode | React.ReactNode[] { const codeToHighlight = code.replace(/\n$/, ''); const activeTheme = theme || themeManager.getActiveTheme(); @@ -167,26 +169,29 @@ export function colorizeCode({ ? false : settings.merged.ui.showLineNumbers; - const useMaxSizedBox = !settings.merged.ui.useAlternateBuffer && !returnLines; + // We force MaxSizedBox if availableHeight is provided, even if alternate buffer is enabled, + // because this might be rendered in a constrained UI box (like tool confirmation). + const useMaxSizedBox = + (!settings.merged.ui.useAlternateBuffer || availableHeight !== undefined) && + !returnLines; + + let hiddenLinesCount = 0; + let finalLines = codeToHighlight.split(/\r?\n/); + try { - // Render the HAST tree using the adapted theme - // Apply the theme's default foreground color to the top-level Text element - let lines = codeToHighlight.split(/\r?\n/); - const padWidth = String(lines.length).length; // Calculate padding width based on number of lines - - let hiddenLinesCount = 0; - // Optimization to avoid highlighting lines that cannot possibly be displayed. if (availableHeight !== undefined && useMaxSizedBox) { availableHeight = Math.max(availableHeight, MINIMUM_MAX_HEIGHT); - if (lines.length > availableHeight) { - const sliceIndex = lines.length - availableHeight; + if (finalLines.length > availableHeight) { + const sliceIndex = finalLines.length - availableHeight; hiddenLinesCount = sliceIndex; - lines = lines.slice(sliceIndex); + finalLines = finalLines.slice(sliceIndex); } } - const renderedLines = lines.map((line, index) => { + const padWidth = String(finalLines.length + hiddenLinesCount).length; + + const renderedLines = finalLines.map((line, index) => { const contentToRender = disableColor ? line : highlightAndRenderLine(line, language, activeTheme); @@ -223,6 +228,7 @@ export function colorizeCode({ if (useMaxSizedBox) { return ( ( + const padWidth = String(finalLines.length + hiddenLinesCount).length; + const fallbackLines = finalLines.map((line, index) => ( {showLineNumbers && ( - {`${index + 1}`} + {`${index + 1 + hiddenLinesCount}`} )} @@ -275,8 +279,10 @@ export function colorizeCode({ if (useMaxSizedBox) { return ( {fallbackLines} diff --git a/packages/core/src/tools/shell.ts b/packages/core/src/tools/shell.ts index 81ac9d9a32..7ca475808a 100644 --- a/packages/core/src/tools/shell.ts +++ b/packages/core/src/tools/shell.ts @@ -136,7 +136,7 @@ export class ShellToolInvocation extends BaseToolInvocation< } getDescription(): string { - return `${this.params.command} ${this.getContextualDetails()}`; + return this.params.description || ''; } private simplifyPaths(paths: Set): string[] {