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 {