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
@@ -7,7 +7,6 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { mapCoreStatusToDisplayStatus, mapToDisplay } from './toolMapping.js';
import {
debugLogger,
type AnyDeclarativeTool,
type AnyToolInvocation,
type ToolCallRequestInfo,
@@ -40,7 +39,7 @@ describe('toolMapping', () => {
describe('mapCoreStatusToDisplayStatus', () => {
it.each([
['validating', ToolCallStatus.Executing],
['validating', ToolCallStatus.Pending],
['awaiting_approval', ToolCallStatus.Confirming],
['executing', ToolCallStatus.Executing],
['success', ToolCallStatus.Success],
@@ -53,12 +52,10 @@ describe('toolMapping', () => {
);
});
it('logs warning and defaults to Error for unknown status', () => {
const result = mapCoreStatusToDisplayStatus('unknown_status' as Status);
expect(result).toBe(ToolCallStatus.Error);
expect(debugLogger.warn).toHaveBeenCalledWith(
'Unknown core status encountered: unknown_status',
);
it('throws error for unknown status due to checkExhaustive', () => {
expect(() =>
mapCoreStatusToDisplayStatus('unknown_status' as Status),
).toThrow('unexpected value unknown_status!');
});
});
+4 -3
View File
@@ -18,12 +18,14 @@ import {
type IndividualToolCallDisplay,
} from '../types.js';
import { checkExhaustive } from '../../utils/checks.js';
export function mapCoreStatusToDisplayStatus(
coreStatus: CoreStatus,
): ToolCallStatus {
switch (coreStatus) {
case 'validating':
return ToolCallStatus.Executing;
return ToolCallStatus.Pending;
case 'awaiting_approval':
return ToolCallStatus.Confirming;
case 'executing':
@@ -37,8 +39,7 @@ export function mapCoreStatusToDisplayStatus(
case 'scheduled':
return ToolCallStatus.Pending;
default:
debugLogger.warn(`Unknown core status encountered: ${coreStatus}`);
return ToolCallStatus.Error;
return checkExhaustive(coreStatus);
}
}
@@ -0,0 +1,62 @@
/**
* @license
* Copyright 2026 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import { useMemo } from 'react';
import { useUIState } from '../contexts/UIStateContext.js';
import {
ToolCallStatus,
type IndividualToolCallDisplay,
type HistoryItemToolGroup,
} from '../types.js';
export interface ConfirmingToolState {
tool: IndividualToolCallDisplay;
index: number;
total: number;
}
/**
* Selects the "Head" of the confirmation queue.
* Returns the first tool in the pending state that requires confirmation.
*/
export function useConfirmingTool(): ConfirmingToolState | null {
// We use pendingHistoryItems to ensure we capture tools from both
// Gemini responses and Slash commands.
const { pendingHistoryItems } = useUIState();
return useMemo(() => {
// 1. Flatten all pending tools from all pending history groups
const allPendingTools = pendingHistoryItems
.filter(
(item): item is HistoryItemToolGroup => item.type === 'tool_group',
)
.flatMap((group) => group.tools);
// 2. Filter for those requiring confirmation
const confirmingTools = allPendingTools.filter(
(t) => t.status === ToolCallStatus.Confirming,
);
if (confirmingTools.length === 0) {
return null;
}
// 3. Select Head (FIFO)
const head = confirmingTools[0];
// 4. Calculate progress based on the full tool list
// This gives the user context of where they are in the current batch.
const headIndexInFullList = allPendingTools.findIndex(
(t) => t.callId === head.callId,
);
return {
tool: head,
index: headIndexInFullList + 1,
total: allPendingTools.length,
};
}, [pendingHistoryItems]);
}
@@ -938,7 +938,7 @@ describe('mapToDisplay', () => {
name: 'validating',
status: 'validating',
extraProps: { tool: baseTool, invocation: baseInvocation },
expectedStatus: ToolCallStatus.Executing,
expectedStatus: ToolCallStatus.Pending,
expectedName: baseTool.displayName,
expectedDescription: baseInvocation.getDescription(),
},