feat(cli): Moves tool confirmations to a queue UX (#17276)

Co-authored-by: Christian Gunderman <gundermanc@google.com>
This commit is contained in:
Abhi
2026-01-23 20:32:35 -05:00
committed by GitHub
parent 77aef861fe
commit 1832f7b90a
27 changed files with 1009 additions and 285 deletions
@@ -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();
});
});
});