mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-17 17:41:24 -07:00
feat(cli): Moves tool confirmations to a queue UX (#17276)
Co-authored-by: Christian Gunderman <gundermanc@google.com>
This commit is contained in:
@@ -13,17 +13,21 @@ import { AlternateBufferQuittingDisplay } from './AlternateBufferQuittingDisplay
|
||||
import { ToolCallStatus } from '../types.js';
|
||||
import type { HistoryItem, HistoryItemWithoutId } from '../types.js';
|
||||
import { Text } from 'ink';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
|
||||
vi.mock('../utils/terminalSetup.js', () => ({
|
||||
getTerminalProgram: () => null,
|
||||
}));
|
||||
|
||||
vi.mock('../contexts/AppContext.js', () => ({
|
||||
useAppContext: () => ({
|
||||
version: '0.10.0',
|
||||
}),
|
||||
}));
|
||||
vi.mock('../contexts/AppContext.js', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('../contexts/AppContext.js')>();
|
||||
return {
|
||||
...actual,
|
||||
useAppContext: () => ({
|
||||
version: '0.10.0',
|
||||
}),
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
const actual =
|
||||
@@ -85,21 +89,6 @@ const mockPendingHistoryItems: HistoryItemWithoutId[] = [
|
||||
},
|
||||
];
|
||||
|
||||
const mockConfig = {
|
||||
getScreenReader: () => false,
|
||||
getEnableInteractiveShell: () => false,
|
||||
getModel: () => 'gemini-pro',
|
||||
getTargetDir: () => '/tmp',
|
||||
getDebugMode: () => false,
|
||||
getIdeMode: () => false,
|
||||
getGeminiMdFileCount: () => 0,
|
||||
getExperiments: () => ({
|
||||
flags: {},
|
||||
experimentIds: [],
|
||||
}),
|
||||
getPreviewFeatures: () => false,
|
||||
} as unknown as Config;
|
||||
|
||||
describe('AlternateBufferQuittingDisplay', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
@@ -127,7 +116,6 @@ describe('AlternateBufferQuittingDisplay', () => {
|
||||
history: mockHistory,
|
||||
pendingHistoryItems: mockPendingHistoryItems,
|
||||
},
|
||||
config: mockConfig,
|
||||
},
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot('with_history_and_pending');
|
||||
@@ -143,7 +131,6 @@ describe('AlternateBufferQuittingDisplay', () => {
|
||||
history: [],
|
||||
pendingHistoryItems: [],
|
||||
},
|
||||
config: mockConfig,
|
||||
},
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot('empty');
|
||||
@@ -159,7 +146,6 @@ describe('AlternateBufferQuittingDisplay', () => {
|
||||
history: mockHistory,
|
||||
pendingHistoryItems: [],
|
||||
},
|
||||
config: mockConfig,
|
||||
},
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot('with_history_no_pending');
|
||||
@@ -175,12 +161,50 @@ describe('AlternateBufferQuittingDisplay', () => {
|
||||
history: [],
|
||||
pendingHistoryItems: mockPendingHistoryItems,
|
||||
},
|
||||
config: mockConfig,
|
||||
},
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot('with_pending_no_history');
|
||||
});
|
||||
|
||||
it('renders with a tool awaiting confirmation', () => {
|
||||
persistentStateMock.setData({ tipsShown: 0 });
|
||||
const pendingHistoryItems: HistoryItemWithoutId[] = [
|
||||
{
|
||||
type: 'tool_group',
|
||||
tools: [
|
||||
{
|
||||
callId: 'call4',
|
||||
name: 'confirming_tool',
|
||||
description: 'Confirming tool description',
|
||||
status: ToolCallStatus.Confirming,
|
||||
resultDisplay: undefined,
|
||||
confirmationDetails: {
|
||||
type: 'info',
|
||||
title: 'Confirm Tool',
|
||||
prompt: 'Confirm this action?',
|
||||
onConfirm: async () => {},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
const { lastFrame } = renderWithProviders(
|
||||
<AlternateBufferQuittingDisplay />,
|
||||
{
|
||||
uiState: {
|
||||
...baseUIState,
|
||||
history: [],
|
||||
pendingHistoryItems,
|
||||
},
|
||||
},
|
||||
);
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('Action Required (was prompted):');
|
||||
expect(output).toContain('confirming_tool');
|
||||
expect(output).toContain('Confirming tool description');
|
||||
expect(output).toMatchSnapshot('with_confirming_tool');
|
||||
});
|
||||
|
||||
it('renders with user and gemini messages', () => {
|
||||
persistentStateMock.setData({ tipsShown: 0 });
|
||||
const history: HistoryItem[] = [
|
||||
@@ -195,7 +219,6 @@ describe('AlternateBufferQuittingDisplay', () => {
|
||||
history,
|
||||
pendingHistoryItems: [],
|
||||
},
|
||||
config: mockConfig,
|
||||
},
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot('with_user_gemini_messages');
|
||||
|
||||
@@ -4,17 +4,26 @@
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { Box } from 'ink';
|
||||
import { Box, Text } from 'ink';
|
||||
import { useUIState } from '../contexts/UIStateContext.js';
|
||||
import { AppHeader } from './AppHeader.js';
|
||||
import { HistoryItemDisplay } from './HistoryItemDisplay.js';
|
||||
import { QuittingDisplay } from './QuittingDisplay.js';
|
||||
import { useAppContext } from '../contexts/AppContext.js';
|
||||
import { MAX_GEMINI_MESSAGE_LINES } from '../constants.js';
|
||||
import { useConfirmingTool } from '../hooks/useConfirmingTool.js';
|
||||
import { useConfig } from '../contexts/ConfigContext.js';
|
||||
import { ToolStatusIndicator, ToolInfo } from './messages/ToolShared.js';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
|
||||
export const AlternateBufferQuittingDisplay = () => {
|
||||
const { version } = useAppContext();
|
||||
const uiState = useUIState();
|
||||
const config = useConfig();
|
||||
|
||||
const confirmingTool = useConfirmingTool();
|
||||
const showPromptedTool =
|
||||
config.isEventDrivenSchedulerEnabled() && confirmingTool !== null;
|
||||
|
||||
// We render the entire chat history and header here to ensure that the
|
||||
// conversation history is visible to the user after the app quits and the
|
||||
@@ -52,6 +61,25 @@ export const AlternateBufferQuittingDisplay = () => {
|
||||
embeddedShellFocused={uiState.embeddedShellFocused}
|
||||
/>
|
||||
))}
|
||||
{showPromptedTool && (
|
||||
<Box flexDirection="column" marginTop={1} marginBottom={1}>
|
||||
<Text color={theme.status.warning} bold>
|
||||
Action Required (was prompted):
|
||||
</Text>
|
||||
<Box marginTop={1}>
|
||||
<ToolStatusIndicator
|
||||
status={confirmingTool.tool.status}
|
||||
name={confirmingTool.tool.name}
|
||||
/>
|
||||
<ToolInfo
|
||||
name={confirmingTool.tool.name}
|
||||
status={confirmingTool.tool.status}
|
||||
description={confirmingTool.tool.description}
|
||||
emphasis="high"
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
<QuittingDisplay />
|
||||
</Box>
|
||||
);
|
||||
|
||||
@@ -29,7 +29,7 @@ import { StreamingState } from '../types.js';
|
||||
import { ConfigInitDisplay } from '../components/ConfigInitDisplay.js';
|
||||
import { TodoTray } from './messages/Todo.js';
|
||||
|
||||
export const Composer = () => {
|
||||
export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
const config = useConfig();
|
||||
const settings = useSettings();
|
||||
const isScreenReaderEnabled = useIsScreenReaderEnabled();
|
||||
@@ -133,7 +133,7 @@ export const Composer = () => {
|
||||
setShellModeActive={uiActions.setShellModeActive}
|
||||
approvalMode={showApprovalModeIndicator}
|
||||
onEscapePromptChange={uiActions.onEscapePromptChange}
|
||||
focus={true}
|
||||
focus={isFocused}
|
||||
vimHandleInput={uiActions.vimHandleInput}
|
||||
isEmbeddedShellFocused={uiState.embeddedShellFocused}
|
||||
popAllMessages={uiActions.popAllMessages}
|
||||
|
||||
@@ -8,9 +8,14 @@ import { render } from '../../test-utils/render.js';
|
||||
import { ContextUsageDisplay } from './ContextUsageDisplay.js';
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
|
||||
vi.mock('@google/gemini-cli-core', () => ({
|
||||
tokenLimit: () => 10000,
|
||||
}));
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('@google/gemini-cli-core')>();
|
||||
return {
|
||||
...actual,
|
||||
tokenLimit: () => 10000,
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../../config/settings.js', () => ({
|
||||
DEFAULT_MODEL_CONFIGS: {},
|
||||
|
||||
@@ -33,11 +33,17 @@ vi.mock('node:fs/promises', async () => {
|
||||
unlink: vi.fn().mockResolvedValue(undefined),
|
||||
};
|
||||
});
|
||||
vi.mock('node:os', () => ({
|
||||
default: {
|
||||
vi.mock('node:os', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('node:os')>();
|
||||
return {
|
||||
...actual,
|
||||
default: {
|
||||
...actual,
|
||||
homedir: () => '/mock/home',
|
||||
},
|
||||
homedir: () => '/mock/home',
|
||||
},
|
||||
}));
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('node:path', async () => {
|
||||
const actual = await vi.importActual<typeof import('node:path')>('node:path');
|
||||
@@ -47,13 +53,19 @@ vi.mock('node:path', async () => {
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('@google/gemini-cli-core', () => ({
|
||||
GEMINI_DIR: '.gemini',
|
||||
homedir: () => '/mock/home',
|
||||
Storage: {
|
||||
getGlobalTempDir: () => '/mock/temp',
|
||||
},
|
||||
}));
|
||||
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
|
||||
const actual =
|
||||
await importOriginal<typeof import('@google/gemini-cli-core')>();
|
||||
return {
|
||||
...actual,
|
||||
GEMINI_DIR: '.gemini',
|
||||
homedir: () => '/mock/home',
|
||||
Storage: {
|
||||
...actual.Storage,
|
||||
getGlobalTempDir: () => '/mock/temp',
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
vi.mock('../../config/settings.js', () => ({
|
||||
DEFAULT_MODEL_CONFIGS: {},
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi } from 'vitest';
|
||||
import { ToolConfirmationQueue } from './ToolConfirmationQueue.js';
|
||||
import { ToolCallStatus } from '../types.js';
|
||||
import { renderWithProviders } from '../../test-utils/render.js';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
import type { ConfirmingToolState } from '../hooks/useConfirmingTool.js';
|
||||
|
||||
describe('ToolConfirmationQueue', () => {
|
||||
const mockConfig = {
|
||||
isTrustedFolder: () => true,
|
||||
getIdeMode: () => false,
|
||||
getModel: () => 'gemini-pro',
|
||||
getDebugMode: () => false,
|
||||
} as unknown as Config;
|
||||
|
||||
it('renders the confirming tool with progress indicator', () => {
|
||||
const confirmingTool = {
|
||||
tool: {
|
||||
callId: 'call-1',
|
||||
name: 'ls',
|
||||
description: 'list files',
|
||||
status: ToolCallStatus.Confirming,
|
||||
confirmationDetails: {
|
||||
type: 'exec' as const,
|
||||
title: 'Confirm execution',
|
||||
command: 'ls',
|
||||
rootCommand: 'ls',
|
||||
rootCommands: ['ls'],
|
||||
onConfirm: vi.fn(),
|
||||
},
|
||||
},
|
||||
index: 1,
|
||||
total: 3,
|
||||
};
|
||||
|
||||
const { lastFrame } = renderWithProviders(
|
||||
<ToolConfirmationQueue
|
||||
confirmingTool={confirmingTool as unknown as ConfirmingToolState}
|
||||
/>,
|
||||
{
|
||||
config: mockConfig,
|
||||
uiState: {
|
||||
terminalWidth: 80,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
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).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('returns null if tool has no confirmation details', () => {
|
||||
const confirmingTool = {
|
||||
tool: {
|
||||
callId: 'call-1',
|
||||
name: 'ls',
|
||||
status: ToolCallStatus.Confirming,
|
||||
confirmationDetails: undefined,
|
||||
},
|
||||
index: 1,
|
||||
total: 1,
|
||||
};
|
||||
|
||||
const { lastFrame } = renderWithProviders(
|
||||
<ToolConfirmationQueue
|
||||
confirmingTool={confirmingTool as unknown as ConfirmingToolState}
|
||||
/>,
|
||||
{
|
||||
config: mockConfig,
|
||||
uiState: {
|
||||
terminalWidth: 80,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(lastFrame()).toBe('');
|
||||
});
|
||||
});
|
||||
89
packages/cli/src/ui/components/ToolConfirmationQueue.tsx
Normal file
89
packages/cli/src/ui/components/ToolConfirmationQueue.tsx
Normal file
@@ -0,0 +1,89 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2026 Google LLC
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
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 { useUIState } from '../contexts/UIStateContext.js';
|
||||
import type { ConfirmingToolState } from '../hooks/useConfirmingTool.js';
|
||||
|
||||
interface ToolConfirmationQueueProps {
|
||||
confirmingTool: ConfirmingToolState;
|
||||
}
|
||||
|
||||
export const ToolConfirmationQueue: React.FC<ToolConfirmationQueueProps> = ({
|
||||
confirmingTool,
|
||||
}) => {
|
||||
const config = useConfig();
|
||||
const { terminalWidth, terminalHeight } = useUIState();
|
||||
const { tool, index, total } = confirmingTool;
|
||||
|
||||
// Safety check: ToolConfirmationMessage requires confirmationDetails
|
||||
if (!tool.confirmationDetails) return null;
|
||||
|
||||
// V1: Constrain the queue to at most 50% of the terminal height to ensure
|
||||
// some history is always visible and to prevent flickering.
|
||||
// We pass this to ToolConfirmationMessage so it can calculate internal
|
||||
// truncation while keeping buttons visible.
|
||||
const maxHeight = Math.floor(terminalHeight * 0.5);
|
||||
|
||||
// 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 = Math.max(maxHeight - 6, 4);
|
||||
|
||||
return (
|
||||
<Box
|
||||
flexDirection="column"
|
||||
borderStyle="round"
|
||||
borderColor={theme.status.warning}
|
||||
paddingX={1}
|
||||
// Matches existing layout spacing
|
||||
width={terminalWidth}
|
||||
flexShrink={0}
|
||||
>
|
||||
{/* Header */}
|
||||
<Box marginBottom={1} justifyContent="space-between">
|
||||
<Text color={theme.status.warning} bold>
|
||||
Action Required
|
||||
</Text>
|
||||
<Text color={theme.text.secondary}>
|
||||
{index} of {total}
|
||||
</Text>
|
||||
</Box>
|
||||
|
||||
{/* Tool Identity (Context) */}
|
||||
<Box marginBottom={1}>
|
||||
<ToolStatusIndicator status={tool.status} name={tool.name} />
|
||||
<ToolInfo
|
||||
name={tool.name}
|
||||
status={tool.status}
|
||||
description={tool.description}
|
||||
emphasis="high"
|
||||
/>
|
||||
</Box>
|
||||
|
||||
{/* Interactive Area */}
|
||||
{/*
|
||||
Note: We force isFocused={true} because if this component is rendered,
|
||||
it effectively acts as a modal over the shell/composer.
|
||||
*/}
|
||||
<ToolConfirmationMessage
|
||||
callId={tool.callId}
|
||||
confirmationDetails={tool.confirmationDetails}
|
||||
config={config}
|
||||
terminalWidth={terminalWidth - 4} // Adjust for parent border/padding
|
||||
availableTerminalHeight={availableContentHeight}
|
||||
isFocused={true}
|
||||
/>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -1,5 +1,28 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`AlternateBufferQuittingDisplay > renders with a tool awaiting confirmation > with_confirming_tool 1`] = `
|
||||
"
|
||||
███ █████████
|
||||
░░░███ ███░░░░░███
|
||||
░░░███ ███ ░░░
|
||||
░░░███░███
|
||||
███░ ░███ █████
|
||||
███░ ░░███ ░░███
|
||||
███░ ░░█████████
|
||||
░░░ ░░░░░░░░░
|
||||
|
||||
Tips for getting started:
|
||||
1. Ask questions, edit files, or run commands.
|
||||
2. Be specific for the best results.
|
||||
3. Create GEMINI.md files to customize your interactions with Gemini.
|
||||
4. /help for more information.
|
||||
|
||||
Action Required (was prompted):
|
||||
|
||||
? confirming_tool Confirming tool description
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`AlternateBufferQuittingDisplay > renders with active and pending tool messages > with_history_and_pending 1`] = `
|
||||
"
|
||||
███ █████████
|
||||
@@ -23,10 +46,6 @@ Tips for getting started:
|
||||
╭─────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ tool2 Description for tool 2 │
|
||||
│ │
|
||||
╰─────────────────────────────────────────────────────────────────────────────╯
|
||||
╭─────────────────────────────────────────────────────────────────────────────╮
|
||||
│ o tool3 Description for tool 3 │
|
||||
│ │
|
||||
╰─────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
@@ -89,11 +108,7 @@ Tips for getting started:
|
||||
1. Ask questions, edit files, or run commands.
|
||||
2. Be specific for the best results.
|
||||
3. Create GEMINI.md files to customize your interactions with Gemini.
|
||||
4. /help for more information.
|
||||
╭─────────────────────────────────────────────────────────────────────────────╮
|
||||
│ o tool3 Description for tool 3 │
|
||||
│ │
|
||||
╰─────────────────────────────────────────────────────────────────────────────╯"
|
||||
4. /help for more information."
|
||||
`;
|
||||
|
||||
exports[`AlternateBufferQuittingDisplay > renders with user and gemini messages > with_user_gemini_messages 1`] = `
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`ToolConfirmationQueue > renders the confirming tool with progress indicator 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ Action Required 1 of 3 │
|
||||
│ │
|
||||
│ ? ls list files │
|
||||
│ │
|
||||
│ ls │
|
||||
│ │
|
||||
│ Allow execution of: 'ls'? │
|
||||
│ │
|
||||
│ ● 1. Allow once │
|
||||
│ 2. Allow for this session │
|
||||
│ 3. No, suggest changes (esc) │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { useMemo, useCallback } from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { DiffRenderer } from './DiffRenderer.js';
|
||||
import { RenderInline } from '../../utils/InlineMarkdownRenderer.js';
|
||||
@@ -58,14 +58,17 @@ export const ToolConfirmationMessage: React.FC<
|
||||
const allowPermanentApproval =
|
||||
settings.merged.security.enablePermanentToolApproval;
|
||||
|
||||
const handleConfirm = (outcome: ToolConfirmationOutcome) => {
|
||||
void confirm(callId, outcome).catch((error) => {
|
||||
debugLogger.error(
|
||||
`Failed to handle tool confirmation for ${callId}:`,
|
||||
error,
|
||||
);
|
||||
});
|
||||
};
|
||||
const handleConfirm = useCallback(
|
||||
(outcome: ToolConfirmationOutcome) => {
|
||||
void confirm(callId, outcome).catch((error) => {
|
||||
debugLogger.error(
|
||||
`Failed to handle tool confirmation for ${callId}:`,
|
||||
error,
|
||||
);
|
||||
});
|
||||
},
|
||||
[confirm, callId],
|
||||
);
|
||||
|
||||
const isTrustedFolder = config.isTrustedFolder();
|
||||
|
||||
@@ -79,16 +82,16 @@ export const ToolConfirmationMessage: React.FC<
|
||||
{ isActive: isFocused },
|
||||
);
|
||||
|
||||
const handleSelect = (item: ToolConfirmationOutcome) => handleConfirm(item);
|
||||
const handleSelect = useCallback(
|
||||
(item: ToolConfirmationOutcome) => handleConfirm(item),
|
||||
[handleConfirm],
|
||||
);
|
||||
|
||||
const { question, bodyContent, options } = useMemo(() => {
|
||||
let bodyContent: React.ReactNode | null = null;
|
||||
let question = '';
|
||||
const getOptions = useCallback(() => {
|
||||
const options: Array<RadioSelectItem<ToolConfirmationOutcome>> = [];
|
||||
|
||||
if (confirmationDetails.type === 'edit') {
|
||||
if (!confirmationDetails.isModifying) {
|
||||
question = `Apply this change?`;
|
||||
options.push({
|
||||
label: 'Allow once',
|
||||
value: ToolConfirmationOutcome.ProceedOnce,
|
||||
@@ -125,13 +128,6 @@ export const ToolConfirmationMessage: React.FC<
|
||||
});
|
||||
}
|
||||
} 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: '${executionProps.rootCommand}'?`;
|
||||
}
|
||||
options.push({
|
||||
label: 'Allow once',
|
||||
value: ToolConfirmationOutcome.ProceedOnce,
|
||||
@@ -157,7 +153,6 @@ export const ToolConfirmationMessage: React.FC<
|
||||
key: 'No, suggest changes (esc)',
|
||||
});
|
||||
} else if (confirmationDetails.type === 'info') {
|
||||
question = `Do you want to proceed?`;
|
||||
options.push({
|
||||
label: 'Allow once',
|
||||
value: ToolConfirmationOutcome.ProceedOnce,
|
||||
@@ -184,8 +179,6 @@ export const ToolConfirmationMessage: React.FC<
|
||||
});
|
||||
} else {
|
||||
// mcp tool confirmation
|
||||
const mcpProps = confirmationDetails;
|
||||
question = `Allow execution of MCP tool "${mcpProps.toolName}" from server "${mcpProps.serverName}"?`;
|
||||
options.push({
|
||||
label: 'Allow once',
|
||||
value: ToolConfirmationOutcome.ProceedOnce,
|
||||
@@ -216,33 +209,56 @@ export const ToolConfirmationMessage: React.FC<
|
||||
key: 'No, suggest changes (esc)',
|
||||
});
|
||||
}
|
||||
return options;
|
||||
}, [confirmationDetails, isTrustedFolder, allowPermanentApproval, config]);
|
||||
|
||||
function availableBodyContentHeight() {
|
||||
if (options.length === 0) {
|
||||
// Should not happen if we populated options correctly above for all types
|
||||
// except when isModifying is true, but in that case we don't call this because we don't enter the if block for it.
|
||||
return undefined;
|
||||
const availableBodyContentHeight = useCallback(() => {
|
||||
if (availableTerminalHeight === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Calculate the vertical space (in lines) consumed by UI elements
|
||||
// surrounding the main body content.
|
||||
const PADDING_OUTER_Y = 2; // Main container has `padding={1}` (top & bottom).
|
||||
const MARGIN_BODY_BOTTOM = 1; // margin on the body container.
|
||||
const HEIGHT_QUESTION = 1; // The question text is one line.
|
||||
const MARGIN_QUESTION_BOTTOM = 1; // Margin on the question container.
|
||||
|
||||
const optionsCount = getOptions().length;
|
||||
|
||||
const surroundingElementsHeight =
|
||||
PADDING_OUTER_Y +
|
||||
MARGIN_BODY_BOTTOM +
|
||||
HEIGHT_QUESTION +
|
||||
MARGIN_QUESTION_BOTTOM +
|
||||
optionsCount;
|
||||
|
||||
return Math.max(availableTerminalHeight - surroundingElementsHeight, 1);
|
||||
}, [availableTerminalHeight, getOptions]);
|
||||
|
||||
const { question, bodyContent, options } = useMemo(() => {
|
||||
let bodyContent: React.ReactNode | null = null;
|
||||
let question = '';
|
||||
const options = getOptions();
|
||||
|
||||
if (confirmationDetails.type === 'edit') {
|
||||
if (!confirmationDetails.isModifying) {
|
||||
question = `Apply this change?`;
|
||||
}
|
||||
} else if (confirmationDetails.type === 'exec') {
|
||||
const executionProps = confirmationDetails;
|
||||
|
||||
if (availableTerminalHeight === undefined) {
|
||||
return undefined;
|
||||
if (executionProps.commands && executionProps.commands.length > 1) {
|
||||
question = `Allow execution of ${executionProps.commands.length} commands?`;
|
||||
} else {
|
||||
question = `Allow execution of: '${executionProps.rootCommand}'?`;
|
||||
}
|
||||
|
||||
// Calculate the vertical space (in lines) consumed by UI elements
|
||||
// surrounding the main body content.
|
||||
const PADDING_OUTER_Y = 2; // Main container has `padding={1}` (top & bottom).
|
||||
const MARGIN_BODY_BOTTOM = 1; // margin on the body container.
|
||||
const HEIGHT_QUESTION = 1; // The question text is one line.
|
||||
const MARGIN_QUESTION_BOTTOM = 1; // Margin on the question container.
|
||||
const HEIGHT_OPTIONS = options.length; // Each option in the radio select takes one line.
|
||||
|
||||
const surroundingElementsHeight =
|
||||
PADDING_OUTER_Y +
|
||||
MARGIN_BODY_BOTTOM +
|
||||
HEIGHT_QUESTION +
|
||||
MARGIN_QUESTION_BOTTOM +
|
||||
HEIGHT_OPTIONS;
|
||||
return Math.max(availableTerminalHeight - surroundingElementsHeight, 1);
|
||||
} else if (confirmationDetails.type === 'info') {
|
||||
question = `Do you want to proceed?`;
|
||||
} else {
|
||||
// mcp tool confirmation
|
||||
const mcpProps = confirmationDetails;
|
||||
question = `Allow execution of MCP tool "${mcpProps.toolName}" from server "${mcpProps.serverName}"?`;
|
||||
}
|
||||
|
||||
if (confirmationDetails.type === 'edit') {
|
||||
@@ -376,11 +392,9 @@ export const ToolConfirmationMessage: React.FC<
|
||||
return { question, bodyContent, options };
|
||||
}, [
|
||||
confirmationDetails,
|
||||
isTrustedFolder,
|
||||
config,
|
||||
availableTerminalHeight,
|
||||
getOptions,
|
||||
availableBodyContentHeight,
|
||||
terminalWidth,
|
||||
allowPermanentApproval,
|
||||
]);
|
||||
|
||||
if (confirmationDetails.type === 'edit') {
|
||||
@@ -409,7 +423,13 @@ export const ToolConfirmationMessage: React.FC<
|
||||
{/* Body Content (Diff Renderer or Command Info) */}
|
||||
{/* No separate context display here anymore for edits */}
|
||||
<Box flexGrow={1} flexShrink={1} overflow="hidden" marginBottom={1}>
|
||||
{bodyContent}
|
||||
<MaxSizedBox
|
||||
maxHeight={availableBodyContentHeight()}
|
||||
maxWidth={terminalWidth}
|
||||
overflowDirection="top"
|
||||
>
|
||||
{bodyContent}
|
||||
</MaxSizedBox>
|
||||
</Box>
|
||||
|
||||
{/* Confirmation Question */}
|
||||
|
||||
@@ -13,6 +13,7 @@ import { ToolGroupMessage } from './ToolGroupMessage.js';
|
||||
import type { IndividualToolCallDisplay } from '../../types.js';
|
||||
import { ToolCallStatus } from '../../types.js';
|
||||
import { Scrollable } from '../shared/Scrollable.js';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
|
||||
describe('<ToolGroupMessage />', () => {
|
||||
const createToolCall = (
|
||||
@@ -34,12 +35,24 @@ describe('<ToolGroupMessage />', () => {
|
||||
isFocused: true,
|
||||
};
|
||||
|
||||
const baseMockConfig = {
|
||||
getModel: () => 'gemini-pro',
|
||||
getTargetDir: () => '/test',
|
||||
getDebugMode: () => false,
|
||||
isTrustedFolder: () => true,
|
||||
getIdeMode: () => false,
|
||||
getEnableInteractiveShell: () => true,
|
||||
getPreviewFeatures: () => false,
|
||||
isEventDrivenSchedulerEnabled: () => true,
|
||||
} as unknown as Config;
|
||||
|
||||
describe('Golden Snapshots', () => {
|
||||
it('renders single successful tool call', () => {
|
||||
const toolCalls = [createToolCall()];
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
{
|
||||
config: baseMockConfig,
|
||||
uiState: {
|
||||
pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }],
|
||||
},
|
||||
@@ -70,9 +83,15 @@ describe('<ToolGroupMessage />', () => {
|
||||
status: ToolCallStatus.Error,
|
||||
}),
|
||||
];
|
||||
const mockConfig = {
|
||||
...baseMockConfig,
|
||||
isEventDrivenSchedulerEnabled: () => false,
|
||||
} as unknown as Config;
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
{
|
||||
config: mockConfig,
|
||||
uiState: {
|
||||
pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }],
|
||||
},
|
||||
@@ -97,9 +116,15 @@ describe('<ToolGroupMessage />', () => {
|
||||
},
|
||||
}),
|
||||
];
|
||||
const mockConfig = {
|
||||
...baseMockConfig,
|
||||
isEventDrivenSchedulerEnabled: () => false,
|
||||
} as unknown as Config;
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
{
|
||||
config: mockConfig,
|
||||
uiState: {
|
||||
pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }],
|
||||
},
|
||||
@@ -121,6 +146,7 @@ describe('<ToolGroupMessage />', () => {
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
{
|
||||
config: baseMockConfig,
|
||||
uiState: {
|
||||
pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }],
|
||||
},
|
||||
@@ -151,9 +177,15 @@ describe('<ToolGroupMessage />', () => {
|
||||
status: ToolCallStatus.Pending,
|
||||
}),
|
||||
];
|
||||
const mockConfig = {
|
||||
...baseMockConfig,
|
||||
isEventDrivenSchedulerEnabled: () => false,
|
||||
} as unknown as Config;
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
{
|
||||
config: mockConfig,
|
||||
uiState: {
|
||||
pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }],
|
||||
},
|
||||
@@ -186,6 +218,7 @@ describe('<ToolGroupMessage />', () => {
|
||||
availableTerminalHeight={10}
|
||||
/>,
|
||||
{
|
||||
config: baseMockConfig,
|
||||
uiState: {
|
||||
pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }],
|
||||
},
|
||||
@@ -204,6 +237,7 @@ describe('<ToolGroupMessage />', () => {
|
||||
isFocused={false}
|
||||
/>,
|
||||
{
|
||||
config: baseMockConfig,
|
||||
uiState: {
|
||||
pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }],
|
||||
},
|
||||
@@ -228,6 +262,7 @@ describe('<ToolGroupMessage />', () => {
|
||||
terminalWidth={40}
|
||||
/>,
|
||||
{
|
||||
config: baseMockConfig,
|
||||
uiState: {
|
||||
pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }],
|
||||
},
|
||||
@@ -241,6 +276,7 @@ describe('<ToolGroupMessage />', () => {
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={[]} />,
|
||||
{
|
||||
config: baseMockConfig,
|
||||
uiState: {
|
||||
pendingHistoryItems: [{ type: 'tool_group', tools: [] }],
|
||||
},
|
||||
@@ -271,6 +307,7 @@ describe('<ToolGroupMessage />', () => {
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />
|
||||
</Scrollable>,
|
||||
{
|
||||
config: baseMockConfig,
|
||||
uiState: {
|
||||
pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }],
|
||||
},
|
||||
@@ -293,6 +330,7 @@ describe('<ToolGroupMessage />', () => {
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
{
|
||||
config: baseMockConfig,
|
||||
uiState: {
|
||||
pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }],
|
||||
},
|
||||
@@ -326,6 +364,7 @@ describe('<ToolGroupMessage />', () => {
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls2} />
|
||||
</Scrollable>,
|
||||
{
|
||||
config: baseMockConfig,
|
||||
uiState: {
|
||||
pendingHistoryItems: [
|
||||
{ type: 'tool_group', tools: toolCalls1 },
|
||||
@@ -342,9 +381,15 @@ describe('<ToolGroupMessage />', () => {
|
||||
describe('Border Color Logic', () => {
|
||||
it('uses yellow border when tools are pending', () => {
|
||||
const toolCalls = [createToolCall({ status: ToolCallStatus.Pending })];
|
||||
const mockConfig = {
|
||||
...baseMockConfig,
|
||||
isEventDrivenSchedulerEnabled: () => false,
|
||||
} as unknown as Config;
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
{
|
||||
config: mockConfig,
|
||||
uiState: {
|
||||
pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }],
|
||||
},
|
||||
@@ -365,6 +410,7 @@ describe('<ToolGroupMessage />', () => {
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
{
|
||||
config: baseMockConfig,
|
||||
uiState: {
|
||||
pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }],
|
||||
},
|
||||
@@ -386,6 +432,7 @@ describe('<ToolGroupMessage />', () => {
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
{
|
||||
config: baseMockConfig,
|
||||
uiState: {
|
||||
pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }],
|
||||
},
|
||||
@@ -419,6 +466,7 @@ describe('<ToolGroupMessage />', () => {
|
||||
availableTerminalHeight={20}
|
||||
/>,
|
||||
{
|
||||
config: baseMockConfig,
|
||||
uiState: {
|
||||
pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }],
|
||||
},
|
||||
@@ -455,9 +503,15 @@ describe('<ToolGroupMessage />', () => {
|
||||
},
|
||||
}),
|
||||
];
|
||||
const mockConfig = {
|
||||
...baseMockConfig,
|
||||
isEventDrivenSchedulerEnabled: () => false,
|
||||
} as unknown as Config;
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
{
|
||||
config: mockConfig,
|
||||
uiState: {
|
||||
pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }],
|
||||
},
|
||||
@@ -485,10 +539,16 @@ describe('<ToolGroupMessage />', () => {
|
||||
const settings = createMockSettings({
|
||||
security: { enablePermanentToolApproval: true },
|
||||
});
|
||||
const mockConfig = {
|
||||
...baseMockConfig,
|
||||
isEventDrivenSchedulerEnabled: () => false,
|
||||
} as unknown as Config;
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
{
|
||||
settings,
|
||||
config: mockConfig,
|
||||
uiState: {
|
||||
pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }],
|
||||
},
|
||||
@@ -502,32 +562,100 @@ describe('<ToolGroupMessage />', () => {
|
||||
it('renders confirmation with permanent approval disabled', () => {
|
||||
const toolCalls = [
|
||||
createToolCall({
|
||||
callId: 'tool-1',
|
||||
callId: 'confirm-tool',
|
||||
name: 'confirm-tool',
|
||||
status: ToolCallStatus.Confirming,
|
||||
confirmationDetails: {
|
||||
type: 'info',
|
||||
title: 'Confirm Tool',
|
||||
title: 'Confirm tool',
|
||||
prompt: 'Do you want to proceed?',
|
||||
onConfirm: vi.fn(),
|
||||
},
|
||||
}),
|
||||
];
|
||||
const settings = createMockSettings({
|
||||
security: { enablePermanentToolApproval: false },
|
||||
});
|
||||
|
||||
const mockConfig = {
|
||||
...baseMockConfig,
|
||||
isEventDrivenSchedulerEnabled: () => false,
|
||||
} as unknown as Config;
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
{
|
||||
settings,
|
||||
uiState: {
|
||||
pendingHistoryItems: [{ type: 'tool_group', tools: toolCalls }],
|
||||
},
|
||||
},
|
||||
{ config: mockConfig },
|
||||
);
|
||||
expect(lastFrame()).not.toContain('Allow for all future sessions');
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Event-Driven Scheduler', () => {
|
||||
it('hides confirming tools when event-driven scheduler is enabled', () => {
|
||||
const toolCalls = [
|
||||
createToolCall({
|
||||
callId: 'confirm-tool',
|
||||
status: ToolCallStatus.Confirming,
|
||||
confirmationDetails: {
|
||||
type: 'info',
|
||||
title: 'Confirm tool',
|
||||
prompt: 'Do you want to proceed?',
|
||||
onConfirm: vi.fn(),
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
const mockConfig = {
|
||||
...baseMockConfig,
|
||||
isEventDrivenSchedulerEnabled: () => true,
|
||||
} as unknown as Config;
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
{ config: mockConfig },
|
||||
);
|
||||
|
||||
// Should render nothing because all tools in the group are confirming
|
||||
expect(lastFrame()).toBe('');
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('shows only successful tools when mixed with confirming tools', () => {
|
||||
const toolCalls = [
|
||||
createToolCall({
|
||||
callId: 'success-tool',
|
||||
name: 'success-tool',
|
||||
status: ToolCallStatus.Success,
|
||||
}),
|
||||
createToolCall({
|
||||
callId: 'confirm-tool',
|
||||
name: 'confirm-tool',
|
||||
status: ToolCallStatus.Confirming,
|
||||
confirmationDetails: {
|
||||
type: 'info',
|
||||
title: 'Confirm tool',
|
||||
prompt: 'Do you want to proceed?',
|
||||
onConfirm: vi.fn(),
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
const mockConfig = {
|
||||
...baseMockConfig,
|
||||
isEventDrivenSchedulerEnabled: () => true,
|
||||
} as unknown as Config;
|
||||
|
||||
const { lastFrame, unmount } = renderWithProviders(
|
||||
<ToolGroupMessage {...baseProps} toolCalls={toolCalls} />,
|
||||
{ config: mockConfig },
|
||||
);
|
||||
|
||||
const output = lastFrame();
|
||||
expect(output).toContain('success-tool');
|
||||
expect(output).not.toContain('confirm-tool');
|
||||
expect(output).not.toContain('Do you want to proceed?');
|
||||
expect(output).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -36,7 +36,28 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
||||
activeShellPtyId,
|
||||
embeddedShellFocused,
|
||||
}) => {
|
||||
const isEmbeddedShellFocused = toolCalls.some((t) =>
|
||||
const config = useConfig();
|
||||
|
||||
const isEventDriven = config.isEventDrivenSchedulerEnabled();
|
||||
|
||||
// If Event-Driven Scheduler is enabled, we HIDE tools that are still in
|
||||
// pre-execution states (Confirming, Pending) from the History log.
|
||||
// They live in the Global Queue or wait for their turn.
|
||||
const visibleToolCalls = useMemo(() => {
|
||||
if (!isEventDriven) {
|
||||
return toolCalls;
|
||||
}
|
||||
// Only show tools that are actually running or finished.
|
||||
// We explicitly exclude Pending and Confirming to ensure they only
|
||||
// appear in the Global Queue until they are approved and start executing.
|
||||
return toolCalls.filter(
|
||||
(t) =>
|
||||
t.status !== ToolCallStatus.Pending &&
|
||||
t.status !== ToolCallStatus.Confirming,
|
||||
);
|
||||
}, [toolCalls, isEventDriven]);
|
||||
|
||||
const isEmbeddedShellFocused = visibleToolCalls.some((t) =>
|
||||
isThisShellFocused(
|
||||
t.name,
|
||||
t.status,
|
||||
@@ -46,11 +67,10 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
||||
),
|
||||
);
|
||||
|
||||
const hasPending = !toolCalls.every(
|
||||
const hasPending = !visibleToolCalls.every(
|
||||
(t) => t.status === ToolCallStatus.Success,
|
||||
);
|
||||
|
||||
const config = useConfig();
|
||||
const isShellCommand = toolCalls.some((t) => isShellTool(t.name));
|
||||
const borderColor =
|
||||
(isShellCommand && hasPending) || isEmbeddedShellFocused
|
||||
@@ -64,20 +84,29 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
||||
|
||||
const staticHeight = /* border */ 2 + /* marginBottom */ 1;
|
||||
|
||||
// only prompt for tool approval on the first 'confirming' tool in the list
|
||||
// note, after the CTA, this automatically moves over to the next 'confirming' tool
|
||||
// Inline confirmations are ONLY used when the Global Queue is disabled.
|
||||
const toolAwaitingApproval = useMemo(
|
||||
() => toolCalls.find((tc) => tc.status === ToolCallStatus.Confirming),
|
||||
[toolCalls],
|
||||
() =>
|
||||
isEventDriven
|
||||
? undefined
|
||||
: toolCalls.find((tc) => tc.status === ToolCallStatus.Confirming),
|
||||
[toolCalls, isEventDriven],
|
||||
);
|
||||
|
||||
// If all tools are hidden (e.g. group only contains confirming or pending tools),
|
||||
// render nothing in the history log.
|
||||
if (visibleToolCalls.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let countToolCallsWithResults = 0;
|
||||
for (const tool of toolCalls) {
|
||||
for (const tool of visibleToolCalls) {
|
||||
if (tool.resultDisplay !== undefined && tool.resultDisplay !== '') {
|
||||
countToolCallsWithResults++;
|
||||
}
|
||||
}
|
||||
const countOneLineToolCalls = toolCalls.length - countToolCallsWithResults;
|
||||
const countOneLineToolCalls =
|
||||
visibleToolCalls.length - countToolCallsWithResults;
|
||||
const availableTerminalHeightPerToolMessage = availableTerminalHeight
|
||||
? Math.max(
|
||||
Math.floor(
|
||||
@@ -102,7 +131,7 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
||||
*/
|
||||
width={terminalWidth}
|
||||
>
|
||||
{toolCalls.map((tool, index) => {
|
||||
{visibleToolCalls.map((tool, index) => {
|
||||
const isConfirming = toolAwaitingApproval?.callId === tool.callId;
|
||||
const isFirst = index === 0;
|
||||
const isShellToolCall = isShellTool(tool.name);
|
||||
@@ -180,7 +209,7 @@ export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
|
||||
We have to keep the bottom border separate so it doesn't get
|
||||
drawn over by the sticky header directly inside it.
|
||||
*/
|
||||
toolCalls.length > 0 && (
|
||||
visibleToolCalls.length > 0 && (
|
||||
<Box
|
||||
height={0}
|
||||
width={terminalWidth}
|
||||
|
||||
@@ -39,11 +39,11 @@ Do you want to proceed?
|
||||
`;
|
||||
|
||||
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?
|
||||
|
||||
@@ -54,11 +54,11 @@ 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?
|
||||
|
||||
|
||||
@@ -81,6 +81,16 @@ exports[`<ToolGroupMessage /> > Confirmation Handling > shows confirmation dialo
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`<ToolGroupMessage /> > Event-Driven Scheduler > hides confirming tools when event-driven scheduler is enabled 1`] = `""`;
|
||||
|
||||
exports[`<ToolGroupMessage /> > Event-Driven Scheduler > shows only successful tools when mixed with confirming tools 1`] = `
|
||||
"╭──────────────────────────────────────────────────────────────────────────────╮
|
||||
│ ✓ success-tool A tool for testing │
|
||||
│ │
|
||||
│ Test result │
|
||||
╰──────────────────────────────────────────────────────────────────────────────╯"
|
||||
`;
|
||||
|
||||
exports[`<ToolGroupMessage /> > Golden Snapshots > renders empty tool calls array 1`] = `""`;
|
||||
|
||||
exports[`<ToolGroupMessage /> > Golden Snapshots > renders header when scrolled 1`] = `
|
||||
|
||||
Reference in New Issue
Block a user