refactor(cli): unify shell confirmation dialogs (#16828)

This commit is contained in:
N. Taylor Mullen
2026-01-16 15:06:52 -08:00
committed by GitHub
parent 608da23393
commit 1681ae1842
14 changed files with 102 additions and 446 deletions

View File

@@ -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,

View File

@@ -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: () => <Text>FolderTrustDialog</Text>,
}));
vi.mock('./ShellConfirmationDialog.js', () => ({
ShellConfirmationDialog: () => <Text>ShellConfirmationDialog</Text>,
}));
vi.mock('./ConsentPrompt.js', () => ({
ConsentPrompt: () => <Text>ConsentPrompt</Text>,
}));
@@ -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',

View File

@@ -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 (
<ShellConfirmationDialog request={uiState.shellConfirmationRequest} />
);
}
if (uiState.loopDetectionConfirmationRequest) {
return (
<LoopDetectionConfirmation

View File

@@ -1,55 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { renderWithProviders } from '../../test-utils/render.js';
import { describe, it, expect, vi } from 'vitest';
import { ShellConfirmationDialog } from './ShellConfirmationDialog.js';
describe('ShellConfirmationDialog', () => {
const onConfirm = vi.fn();
const request = {
commands: ['ls -la', 'echo "hello"'],
onConfirm,
};
it('renders correctly', () => {
const { lastFrame } = renderWithProviders(
<ShellConfirmationDialog request={request} />,
{ width: 101 },
);
expect(lastFrame()).toMatchSnapshot();
});
it('calls onConfirm with ProceedOnce when "Allow once" is selected', () => {
const { lastFrame } = renderWithProviders(
<ShellConfirmationDialog request={request} />,
);
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(
<ShellConfirmationDialog request={request} />,
);
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(
<ShellConfirmationDialog request={request} />,
{ width: 100 },
);
const select = lastFrame()!.toString();
// Simulate selecting the third option
expect(select).toContain('No (esc)');
});
});

View File

@@ -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<RadioSelectItem<ToolConfirmationOutcome>> = [
{
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 (
<Box flexDirection="row" width="100%">
<Box
flexDirection="column"
borderStyle="round"
borderColor={theme.status.warning}
padding={1}
flexGrow={1}
marginLeft={1}
>
<Box flexDirection="column" marginBottom={1}>
<Text bold color={theme.text.primary}>
Shell Command Execution
</Text>
<Text color={theme.text.primary}>
A custom command wants to run the following shell commands:
</Text>
<Box
flexDirection="column"
borderStyle="round"
borderColor={theme.border.default}
paddingX={1}
marginTop={1}
>
{commands.map((cmd) => (
<Text key={cmd} color={theme.text.link}>
<RenderInline text={cmd} defaultColor={theme.text.link} />
</Text>
))}
</Box>
</Box>
<Box marginBottom={1}>
<Text color={theme.text.primary}>Do you want to proceed?</Text>
</Box>
<RadioButtonSelect items={options} onSelect={handleSelect} isFocused />
</Box>
</Box>
);
};

View File

@@ -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) │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
`;

View File

@@ -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(
<ToolConfirmationMessage
confirmationDetails={confirmationDetails}
config={mockConfig}
availableTerminalHeight={30}
terminalWidth={80}
/>,
);
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',

View File

@@ -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)}
>
<Box>
<Text color={theme.text.link}>{executionProps.command}</Text>
<Box flexDirection="column">
{executionProps.commands && executionProps.commands.length > 1 ? (
executionProps.commands.map((cmd, idx) => (
<Text key={idx} color={theme.text.link}>
{cmd}
</Text>
))
) : (
<Box>
<Text color={theme.text.link}>{executionProps.command}</Text>
</Box>
)}
</Box>
</MaxSizedBox>
);

View File

@@ -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

View File

@@ -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;

View File

@@ -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<false | SlashCommandProcessorResult>
| 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<false | SlashCommandProcessorResult>
| 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' });

View File

@@ -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<null | {
commands: string[];
onConfirm: (
outcome: ToolConfirmationOutcome,
approvedCommands?: string[],
) => void;
}>(null);
const [confirmationRequest, setConfirmationRequest] = useState<null | {
prompt: React.ReactNode;
onConfirm: (confirmed: boolean) => 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,
};
};

View File

@@ -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;

View File

@@ -694,6 +694,7 @@ export interface ToolExecuteConfirmationDetails {
command: string;
rootCommand: string;
rootCommands: string[];
commands?: string[];
}
export interface ToolMcpConfirmationDetails {