diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index 5d23e86aed..72f74a76f0 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -708,7 +708,6 @@ Logging in with Google... Restarting Gemini CLI to continue. slashCommands, pendingHistoryItems: pendingSlashCommandHistoryItems, commandContext, - shellConfirmationRequest, confirmationRequest, } = useSlashCommandProcessor( config, @@ -1358,7 +1357,6 @@ Logging in with Google... Restarting Gemini CLI to continue. useKeypress(handleGlobalKeypress, { isActive: true }); - // Update terminal title with Gemini CLI status and thoughts useEffect(() => { // Respect hideWindowTitle settings if (settings.merged.ui.hideWindowTitle) return; @@ -1366,10 +1364,7 @@ Logging in with Google... Restarting Gemini CLI to continue. const paddedTitle = computeTerminalTitle({ streamingState, thoughtSubject: thought?.subject, - isConfirming: - !!shellConfirmationRequest || - !!confirmationRequest || - showShellActionRequired, + isConfirming: !!confirmationRequest || showShellActionRequired, folderName: basename(config.getTargetDir()), showThoughts: !!settings.merged.ui.showStatusInTitle, useDynamicTitle: settings.merged.ui.dynamicWindowTitle, @@ -1384,7 +1379,6 @@ Logging in with Google... Restarting Gemini CLI to continue. }, [ streamingState, thought, - shellConfirmationRequest, confirmationRequest, showShellActionRequired, settings.merged.ui.showStatusInTitle, @@ -1463,7 +1457,6 @@ Logging in with Google... Restarting Gemini CLI to continue. shouldShowIdePrompt || isFolderTrustDialogOpen || adminSettingsChanged || - !!shellConfirmationRequest || !!confirmationRequest || !!customDialog || confirmUpdateExtensionRequests.length > 0 || @@ -1558,7 +1551,6 @@ Logging in with Google... Restarting Gemini CLI to continue. slashCommands, pendingSlashCommandHistoryItems, commandContext, - shellConfirmationRequest, confirmationRequest, confirmUpdateExtensionRequests, loopDetectionConfirmationRequest, @@ -1650,7 +1642,6 @@ Logging in with Google... Restarting Gemini CLI to continue. slashCommands, pendingSlashCommandHistoryItems, commandContext, - shellConfirmationRequest, confirmationRequest, confirmUpdateExtensionRequests, loopDetectionConfirmationRequest, diff --git a/packages/cli/src/ui/components/DialogManager.test.tsx b/packages/cli/src/ui/components/DialogManager.test.tsx index 7477cc3cf4..196d0294b8 100644 --- a/packages/cli/src/ui/components/DialogManager.test.tsx +++ b/packages/cli/src/ui/components/DialogManager.test.tsx @@ -11,7 +11,6 @@ import { Text } from 'ink'; import { type UIState } from '../contexts/UIStateContext.js'; import { type RestartReason } from '../hooks/useIdeTrustListener.js'; import { type IdeInfo } from '@google/gemini-cli-core'; -import { type ShellConfirmationRequest } from '../types.js'; // Mock child components vi.mock('../IdeIntegrationNudge.js', () => ({ @@ -23,9 +22,6 @@ vi.mock('./LoopDetectionConfirmation.js', () => ({ vi.mock('./FolderTrustDialog.js', () => ({ FolderTrustDialog: () => FolderTrustDialog, })); -vi.mock('./ShellConfirmationDialog.js', () => ({ - ShellConfirmationDialog: () => ShellConfirmationDialog, -})); vi.mock('./ConsentPrompt.js', () => ({ ConsentPrompt: () => ConsentPrompt, })); @@ -79,7 +75,6 @@ describe('DialogManager', () => { proQuotaRequest: null, shouldShowIdePrompt: false, isFolderTrustDialogOpen: false, - shellConfirmationRequest: null, loopDetectionConfirmationRequest: null, confirmationRequest: null, isThemeDialogOpen: false, @@ -130,15 +125,6 @@ describe('DialogManager', () => { 'IdeIntegrationNudge', ], [{ isFolderTrustDialogOpen: true }, 'FolderTrustDialog'], - [ - { - shellConfirmationRequest: { - commands: [], - onConfirm: vi.fn(), - } as unknown as ShellConfirmationRequest, - }, - 'ShellConfirmationDialog', - ], [ { loopDetectionConfirmationRequest: { onComplete: vi.fn() } }, 'LoopDetectionConfirmation', diff --git a/packages/cli/src/ui/components/DialogManager.tsx b/packages/cli/src/ui/components/DialogManager.tsx index ce666346fd..f915bc7852 100644 --- a/packages/cli/src/ui/components/DialogManager.tsx +++ b/packages/cli/src/ui/components/DialogManager.tsx @@ -8,7 +8,6 @@ import { Box, Text } from 'ink'; import { IdeIntegrationNudge } from '../IdeIntegrationNudge.js'; import { LoopDetectionConfirmation } from './LoopDetectionConfirmation.js'; import { FolderTrustDialog } from './FolderTrustDialog.js'; -import { ShellConfirmationDialog } from './ShellConfirmationDialog.js'; import { ConsentPrompt } from './ConsentPrompt.js'; import { ThemeDialog } from './ThemeDialog.js'; import { SettingsDialog } from './SettingsDialog.js'; @@ -85,11 +84,6 @@ export const DialogManager = ({ /> ); } - if (uiState.shellConfirmationRequest) { - return ( - - ); - } if (uiState.loopDetectionConfirmationRequest) { return ( { - const onConfirm = vi.fn(); - - const request = { - commands: ['ls -la', 'echo "hello"'], - onConfirm, - }; - - it('renders correctly', () => { - const { lastFrame } = renderWithProviders( - , - { width: 101 }, - ); - expect(lastFrame()).toMatchSnapshot(); - }); - - it('calls onConfirm with ProceedOnce when "Allow once" is selected', () => { - const { lastFrame } = renderWithProviders( - , - ); - const select = lastFrame()!.toString(); - // Simulate selecting the first option - // This is a simplified way to test the selection - expect(select).toContain('Allow once'); - }); - - it('calls onConfirm with ProceedAlways when "Allow for this session" is selected', () => { - const { lastFrame } = renderWithProviders( - , - ); - const select = lastFrame()!.toString(); - // Simulate selecting the second option - expect(select).toContain('Allow for this session'); - }); - - it('calls onConfirm with Cancel when "No (esc)" is selected', () => { - const { lastFrame } = renderWithProviders( - , - { width: 100 }, - ); - const select = lastFrame()!.toString(); - // Simulate selecting the third option - expect(select).toContain('No (esc)'); - }); -}); diff --git a/packages/cli/src/ui/components/ShellConfirmationDialog.tsx b/packages/cli/src/ui/components/ShellConfirmationDialog.tsx deleted file mode 100644 index b31b267677..0000000000 --- a/packages/cli/src/ui/components/ShellConfirmationDialog.tsx +++ /dev/null @@ -1,110 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import { ToolConfirmationOutcome } from '@google/gemini-cli-core'; -import { Box, Text } from 'ink'; -import type React from 'react'; -import { theme } from '../semantic-colors.js'; -import { RenderInline } from '../utils/InlineMarkdownRenderer.js'; -import type { RadioSelectItem } from './shared/RadioButtonSelect.js'; -import { RadioButtonSelect } from './shared/RadioButtonSelect.js'; -import { useKeypress } from '../hooks/useKeypress.js'; - -export interface ShellConfirmationRequest { - commands: string[]; - onConfirm: ( - outcome: ToolConfirmationOutcome, - approvedCommands?: string[], - ) => void; -} - -export interface ShellConfirmationDialogProps { - request: ShellConfirmationRequest; -} - -export const ShellConfirmationDialog: React.FC< - ShellConfirmationDialogProps -> = ({ request }) => { - const { commands, onConfirm } = request; - - useKeypress( - (key) => { - if (key.name === 'escape') { - onConfirm(ToolConfirmationOutcome.Cancel); - } - }, - { isActive: true }, - ); - - const handleSelect = (item: ToolConfirmationOutcome) => { - if (item === ToolConfirmationOutcome.Cancel) { - onConfirm(item); - } else { - // For both ProceedOnce and ProceedAlways, we approve all the - // commands that were requested. - onConfirm(item, commands); - } - }; - - const options: Array> = [ - { - label: 'Allow once', - value: ToolConfirmationOutcome.ProceedOnce, - key: 'Allow once', - }, - { - label: 'Allow for this session', - value: ToolConfirmationOutcome.ProceedAlways, - key: 'Allow for this session', - }, - { - label: 'No (esc)', - value: ToolConfirmationOutcome.Cancel, - key: 'No (esc)', - }, - ]; - - return ( - - - - - Shell Command Execution - - - A custom command wants to run the following shell commands: - - - {commands.map((cmd) => ( - - - - ))} - - - - - Do you want to proceed? - - - - - - ); -}; diff --git a/packages/cli/src/ui/components/__snapshots__/ShellConfirmationDialog.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/ShellConfirmationDialog.test.tsx.snap deleted file mode 100644 index 500ac184fb..0000000000 --- a/packages/cli/src/ui/components/__snapshots__/ShellConfirmationDialog.test.tsx.snap +++ /dev/null @@ -1,21 +0,0 @@ -// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html - -exports[`ShellConfirmationDialog > renders correctly 1`] = ` -" ╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ - │ │ - │ Shell Command Execution │ - │ A custom command wants to run the following shell commands: │ - │ │ - │ ╭──────────────────────────────────────────────────────────────────────────────────────────────╮ │ - │ │ ls -la │ │ - │ │ echo "hello" │ │ - │ ╰──────────────────────────────────────────────────────────────────────────────────────────────╯ │ - │ │ - │ Do you want to proceed? │ - │ │ - │ ● 1. Allow once │ - │ 2. Allow for this session │ - │ 3. No (esc) │ - │ │ - ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" -`; diff --git a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx index 7444f4f2ec..4682690b61 100644 --- a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx +++ b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx @@ -66,6 +66,33 @@ describe('ToolConfirmationMessage', () => { expect(lastFrame()).toMatchSnapshot(); }); + it('should display multiple commands for exec type when provided', () => { + const confirmationDetails: ToolCallConfirmationDetails = { + type: 'exec', + title: 'Confirm Multiple Commands', + command: 'echo "hello"', // Primary command + rootCommand: 'echo', + rootCommands: ['echo'], + commands: ['echo "hello"', 'ls -la', 'whoami'], // Multi-command list + onConfirm: vi.fn(), + }; + + const { lastFrame } = renderWithProviders( + , + ); + + const output = lastFrame(); + expect(output).toContain('echo "hello"'); + expect(output).toContain('ls -la'); + expect(output).toContain('whoami'); + expect(output).toMatchSnapshot(); + }); + describe('with folder trust', () => { const editConfirmationDetails: ToolCallConfirmationDetails = { type: 'edit', diff --git a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx index 051ca0eae2..24dac94e67 100644 --- a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx +++ b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx @@ -139,7 +139,11 @@ export const ToolConfirmationMessage: React.FC< } else if (confirmationDetails.type === 'exec') { const executionProps = confirmationDetails; - question = `Allow execution of: '${executionProps.rootCommand}'?`; + if (executionProps.commands && executionProps.commands.length > 1) { + question = `Allow execution of ${executionProps.commands.length} commands?`; + } else { + question = `Allow execution of: '${executionProps.rootCommand}'?`; + } options.push({ label: 'Allow once', value: ToolConfirmationOutcome.ProceedOnce, @@ -276,8 +280,18 @@ export const ToolConfirmationMessage: React.FC< maxHeight={bodyContentHeight} maxWidth={Math.max(terminalWidth, 1)} > - - {executionProps.command} + + {executionProps.commands && executionProps.commands.length > 1 ? ( + executionProps.commands.map((cmd, idx) => ( + + {cmd} + + )) + ) : ( + + {executionProps.command} + + )} ); 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 d89a4686eb..284aef6c59 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 @@ -1,5 +1,18 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`ToolConfirmationMessage > should display multiple commands for exec type when provided 1`] = ` +"echo "hello" +ls -la +whoami + +Allow execution of 3 commands? + +● 1. Allow once + 2. Allow for this session + 3. No, suggest changes (esc) +" +`; + exports[`ToolConfirmationMessage > should display urls if prompt and url are different 1`] = ` "fetch https://github.com/google/gemini-react/blob/main/README.md diff --git a/packages/cli/src/ui/contexts/UIStateContext.tsx b/packages/cli/src/ui/contexts/UIStateContext.tsx index 236e48563a..80db5782ff 100644 --- a/packages/cli/src/ui/contexts/UIStateContext.tsx +++ b/packages/cli/src/ui/contexts/UIStateContext.tsx @@ -9,7 +9,6 @@ import type { HistoryItem, ThoughtSummary, ConsoleMessageItem, - ShellConfirmationRequest, ConfirmationRequest, LoopDetectionConfirmationRequest, HistoryItemWithoutId, @@ -68,7 +67,6 @@ export interface UIState { slashCommands: readonly SlashCommand[] | undefined; pendingSlashCommandHistoryItems: HistoryItemWithoutId[]; commandContext: CommandContext; - shellConfirmationRequest: ShellConfirmationRequest | null; confirmationRequest: ConfirmationRequest | null; confirmUpdateExtensionRequests: ConfirmationRequest[]; loopDetectionConfirmationRequest: LoopDetectionConfirmationRequest | null; diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.test.tsx b/packages/cli/src/ui/hooks/slashCommandProcessor.test.tsx index d9831952b4..c66cd25f71 100644 --- a/packages/cli/src/ui/hooks/slashCommandProcessor.test.tsx +++ b/packages/cli/src/ui/hooks/slashCommandProcessor.test.tsx @@ -9,21 +9,16 @@ import { act } from 'react'; import { renderHook } from '../../test-utils/render.js'; import { waitFor } from '../../test-utils/async.js'; import { useSlashCommandProcessor } from './slashCommandProcessor.js'; -import type { - CommandContext, - ConfirmShellCommandsActionReturn, - SlashCommand, -} from '../commands/types.js'; +import type { SlashCommand } from '../commands/types.js'; import { CommandKind } from '../commands/types.js'; import type { LoadedSettings } from '../../config/settings.js'; -import { MessageType, type SlashCommandProcessorResult } from '../types.js'; +import { MessageType } from '../types.js'; import { BuiltinCommandLoader } from '../../services/BuiltinCommandLoader.js'; import { FileCommandLoader } from '../../services/FileCommandLoader.js'; import { McpPromptLoader } from '../../services/McpPromptLoader.js'; import { type GeminiClient, SlashCommandStatus, - ToolConfirmationOutcome, makeFakeConfig, } from '@google/gemini-cli-core'; import { appEvents } from '../../utils/events.js'; @@ -638,197 +633,6 @@ describe('useSlashCommandProcessor', () => { }); }); - describe('Shell Command Confirmation Flow', () => { - // Use a generic vi.fn() for the action. We will change its behavior in each test. - const mockCommandAction = vi.fn(); - - const shellCommand = createTestCommand({ - name: 'shellcmd', - action: mockCommandAction, - }); - - beforeEach(() => { - // Reset the mock before each test - mockCommandAction.mockClear(); - - // Default behavior: request confirmation - mockCommandAction.mockResolvedValue({ - type: 'confirm_shell_commands', - commandsToConfirm: ['rm -rf /'], - originalInvocation: { raw: '/shellcmd' }, - } as ConfirmShellCommandsActionReturn); - }); - - it('should set confirmation request when action returns confirm_shell_commands', async () => { - const result = await setupProcessorHook([shellCommand]); - await waitFor(() => expect(result.current.slashCommands).toHaveLength(1)); - - // Trigger command, don't await it yet as it suspends for confirmation - await act(async () => { - void result.current.handleSlashCommand('/shellcmd'); - }); - - // We now wait for the state to be updated with the request. - await act(async () => { - await waitFor(() => { - expect(result.current.shellConfirmationRequest).not.toBeNull(); - }); - }); - - expect(result.current.shellConfirmationRequest?.commands).toEqual([ - 'rm -rf /', - ]); - }); - - it('should do nothing if user cancels confirmation', async () => { - const result = await setupProcessorHook([shellCommand]); - await waitFor(() => expect(result.current.slashCommands).toHaveLength(1)); - - await act(async () => { - void result.current.handleSlashCommand('/shellcmd'); - }); - - // Wait for the confirmation dialog to be set - await act(async () => { - await waitFor(() => { - expect(result.current.shellConfirmationRequest).not.toBeNull(); - }); - }); - - const onConfirm = result.current.shellConfirmationRequest?.onConfirm; - expect(onConfirm).toBeDefined(); - - // Change the mock action's behavior for a potential second run. - // If the test is flawed, this will be called, and we can detect it. - mockCommandAction.mockResolvedValue({ - type: 'message', - messageType: 'info', - content: 'This should not be called', - }); - - await act(async () => { - onConfirm!(ToolConfirmationOutcome.Cancel, []); // Pass empty array for safety - }); - - expect(result.current.shellConfirmationRequest).toBeNull(); - // Verify the action was only called the initial time. - expect(mockCommandAction).toHaveBeenCalledTimes(1); - }); - - it('should re-run command with one-time allowlist on "Proceed Once"', async () => { - const result = await setupProcessorHook([shellCommand]); - await waitFor(() => expect(result.current.slashCommands).toHaveLength(1)); - - let commandPromise: - | Promise - | undefined; - await act(async () => { - commandPromise = result.current.handleSlashCommand('/shellcmd'); - }); - await act(async () => { - await waitFor(() => { - expect(result.current.shellConfirmationRequest).not.toBeNull(); - }); - }); - - const onConfirm = result.current.shellConfirmationRequest?.onConfirm; - - // **Change the mock's behavior for the SECOND run.** - // This is the key to testing the outcome. - mockCommandAction.mockResolvedValue({ - type: 'message', - messageType: 'info', - content: 'Success!', - }); - - await act(async () => { - onConfirm!(ToolConfirmationOutcome.ProceedOnce, ['rm -rf /']); - }); - - await act(async () => { - await commandPromise; - }); - - expect(result.current.shellConfirmationRequest).toBeNull(); - - // The action should have been called twice (initial + re-run). - await waitFor(() => { - expect(mockCommandAction).toHaveBeenCalledTimes(2); - }); - - // We can inspect the context of the second call to ensure the one-time list was used. - const secondCallContext = mockCommandAction.mock - .calls[1][0] as CommandContext; - expect( - secondCallContext.session.sessionShellAllowlist.has('rm -rf /'), - ).toBe(true); - - // Verify the final success message was added. - expect(mockAddItem).toHaveBeenCalledWith( - { type: MessageType.INFO, text: 'Success!' }, - expect.any(Number), - ); - - // Verify the session-wide allowlist was NOT permanently updated. - // Re-render the hook by calling a no-op command to get the latest context. - await act(async () => { - await result.current.handleSlashCommand('/no-op'); - }); - const finalContext = result.current.commandContext; - expect(finalContext.session.sessionShellAllowlist.size).toBe(0); - }); - - it('should re-run command and update session allowlist on "Proceed Always"', async () => { - const result = await setupProcessorHook([shellCommand]); - await waitFor(() => expect(result.current.slashCommands).toHaveLength(1)); - - let commandPromise: - | Promise - | undefined; - await act(async () => { - commandPromise = result.current.handleSlashCommand('/shellcmd'); - }); - await act(async () => { - await waitFor(() => { - expect(result.current.shellConfirmationRequest).not.toBeNull(); - }); - }); - - const onConfirm = result.current.shellConfirmationRequest?.onConfirm; - mockCommandAction.mockResolvedValue({ - type: 'message', - messageType: 'info', - content: 'Success!', - }); - - await act(async () => { - onConfirm!(ToolConfirmationOutcome.ProceedAlways, ['rm -rf /']); - }); - - await act(async () => { - await commandPromise; - }); - - expect(result.current.shellConfirmationRequest).toBeNull(); - await waitFor(() => { - expect(mockCommandAction).toHaveBeenCalledTimes(2); - }); - - expect(mockAddItem).toHaveBeenCalledWith( - { type: MessageType.INFO, text: 'Success!' }, - expect.any(Number), - ); - - // Check that the session-wide allowlist WAS updated. - await waitFor(() => { - const finalContext = result.current.commandContext; - expect(finalContext.session.sessionShellAllowlist.has('rm -rf /')).toBe( - true, - ); - }); - }); - }); - describe('Command Parsing and Matching', () => { it('should be case-sensitive', async () => { const command = createTestCommand({ name: 'test' }); diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.ts b/packages/cli/src/ui/hooks/slashCommandProcessor.ts index 238f82f9b7..4eb8949db1 100644 --- a/packages/cli/src/ui/hooks/slashCommandProcessor.ts +++ b/packages/cli/src/ui/hooks/slashCommandProcessor.ts @@ -18,6 +18,7 @@ import type { Config, ExtensionsStartingEvent, ExtensionsStoppingEvent, + ToolCallConfirmationDetails, } from '@google/gemini-cli-core'; import { GitService, @@ -39,8 +40,9 @@ import type { SlashCommandProcessorResult, HistoryItem, ConfirmationRequest, + IndividualToolCallDisplay, } from '../types.js'; -import { MessageType } from '../types.js'; +import { MessageType, ToolCallStatus } from '../types.js'; import type { LoadedSettings } from '../../config/settings.js'; import { type CommandContext, type SlashCommand } from '../commands/types.js'; import { CommandService } from '../../services/CommandService.js'; @@ -103,14 +105,6 @@ export const useSlashCommandProcessor = ( const reloadCommands = useCallback(() => { setReloadTrigger((v) => v + 1); }, []); - const [shellConfirmationRequest, setShellConfirmationRequest] = - useState void; - }>(null); const [confirmationRequest, setConfirmationRequest] = useState void; @@ -484,30 +478,59 @@ export const useSlashCommandProcessor = ( content: result.content, }; case 'confirm_shell_commands': { + const callId = `expansion-${Date.now()}`; const { outcome, approvedCommands } = await new Promise<{ outcome: ToolConfirmationOutcome; approvedCommands?: string[]; }>((resolve) => { - setShellConfirmationRequest({ + const confirmationDetails: ToolCallConfirmationDetails = { + type: 'exec', + title: `Confirm Shell Expansion`, + command: result.commandsToConfirm[0] || '', + rootCommand: result.commandsToConfirm[0] || '', + rootCommands: result.commandsToConfirm, commands: result.commandsToConfirm, - onConfirm: ( - resolvedOutcome, - resolvedApprovedCommands, - ) => { - setShellConfirmationRequest(null); // Close the dialog + onConfirm: async (resolvedOutcome) => { + // Close the pending tool display by resolving resolve({ outcome: resolvedOutcome, - approvedCommands: resolvedApprovedCommands, + approvedCommands: + resolvedOutcome === ToolConfirmationOutcome.Cancel + ? [] + : result.commandsToConfirm, }); }, + }; + + const toolDisplay: IndividualToolCallDisplay = { + callId, + name: 'Expansion', + description: 'Command expansion needs shell access', + status: ToolCallStatus.Confirming, + resultDisplay: undefined, + confirmationDetails, + }; + + setPendingItem({ + type: 'tool_group', + tools: [toolDisplay], }); }); + setPendingItem(null); + if ( outcome === ToolConfirmationOutcome.Cancel || !approvedCommands || approvedCommands.length === 0 ) { + addItem( + { + type: MessageType.INFO, + text: 'Slash command shell execution declined.', + }, + Date.now(), + ); return { type: 'handled' }; } @@ -521,6 +544,8 @@ export const useSlashCommandProcessor = ( result.originalInvocation.raw, // Pass the approved commands as a one-time grant for this execution. new Set(approvedCommands), + undefined, + false, // Do not add to history again ); } case 'confirm_action': { @@ -633,7 +658,6 @@ export const useSlashCommandProcessor = ( commands, commandContext, addMessage, - setShellConfirmationRequest, setSessionShellAllowlist, setIsProcessing, setConfirmationRequest, @@ -646,7 +670,6 @@ export const useSlashCommandProcessor = ( slashCommands: commands, pendingHistoryItems, commandContext, - shellConfirmationRequest, confirmationRequest, }; }; diff --git a/packages/cli/src/ui/types.ts b/packages/cli/src/ui/types.ts index 4f9a970278..f013d27fbf 100644 --- a/packages/cli/src/ui/types.ts +++ b/packages/cli/src/ui/types.ts @@ -10,7 +10,6 @@ import type { MCPServerConfig, ThoughtSummary, ToolCallConfirmationDetails, - ToolConfirmationOutcome, ToolResultDisplay, RetrieveUserQuotaResponse, SkillDefinition, @@ -417,14 +416,6 @@ export type SlashCommandProcessorResult = } | SubmitPromptResult; -export interface ShellConfirmationRequest { - commands: string[]; - onConfirm: ( - outcome: ToolConfirmationOutcome, - approvedCommands?: string[], - ) => void; -} - export interface ConfirmationRequest { prompt: ReactNode; onConfirm: (confirm: boolean) => void; diff --git a/packages/core/src/tools/tools.ts b/packages/core/src/tools/tools.ts index 1b365bde40..cca65ce404 100644 --- a/packages/core/src/tools/tools.ts +++ b/packages/core/src/tools/tools.ts @@ -694,6 +694,7 @@ export interface ToolExecuteConfirmationDetails { command: string; rootCommand: string; rootCommands: string[]; + commands?: string[]; } export interface ToolMcpConfirmationDetails {