feat: wire up AskUserTool with dialog (#17411)

This commit is contained in:
Jack Wotherspoon
2026-01-27 13:30:44 -05:00
committed by GitHub
parent 246a6d10c3
commit 36d618f72a
11 changed files with 441 additions and 44 deletions

View File

@@ -26,6 +26,7 @@ import {
} from '../ui/contexts/UIActionsContext.js';
import { type HistoryItemToolGroup, StreamingState } from '../ui/types.js';
import { ToolActionsProvider } from '../ui/contexts/ToolActionsContext.js';
import { AskUserActionsProvider } from '../ui/contexts/AskUserActionsContext.js';
import { makeFakeConfig, type Config } from '@google/gemini-cli-core';
import { FakePersistentState } from './persistentStateFake.js';
@@ -300,20 +301,28 @@ export const renderWithProviders = (
config={config}
toolCalls={allToolCalls}
>
<KeypressProvider>
<MouseProvider mouseEventsEnabled={mouseEventsEnabled}>
<ScrollProvider>
<Box
width={terminalWidth}
flexShrink={0}
flexGrow={0}
flexDirection="column"
>
{component}
</Box>
</ScrollProvider>
</MouseProvider>
</KeypressProvider>
<AskUserActionsProvider
request={null}
onSubmit={vi.fn()}
onCancel={vi.fn()}
>
<KeypressProvider>
<MouseProvider
mouseEventsEnabled={mouseEventsEnabled}
>
<ScrollProvider>
<Box
width={terminalWidth}
flexShrink={0}
flexGrow={0}
flexDirection="column"
>
{component}
</Box>
</ScrollProvider>
</MouseProvider>
</KeypressProvider>
</AskUserActionsProvider>
</ToolActionsProvider>
</UIActionsContext.Provider>
</StreamingContext.Provider>

View File

@@ -30,6 +30,10 @@ import {
} from './types.js';
import { MessageType, StreamingState } from './types.js';
import { ToolActionsProvider } from './contexts/ToolActionsContext.js';
import {
AskUserActionsProvider,
type AskUserState,
} from './contexts/AskUserActionsContext.js';
import {
type EditorType,
type Config,
@@ -63,6 +67,8 @@ import {
SessionStartSource,
SessionEndReason,
generateSummary,
MessageBusType,
type AskUserRequest,
type AgentsDiscoveredPayload,
ChangeAuthRequestedError,
} from '@google/gemini-cli-core';
@@ -282,6 +288,11 @@ export const AppContainer = (props: AppContainerProps) => {
AgentDefinition | undefined
>();
// AskUser dialog state
const [askUserRequest, setAskUserRequest] = useState<AskUserState | null>(
null,
);
const openAgentConfigDialog = useCallback(
(name: string, displayName: string, definition: AgentDefinition) => {
setSelectedAgentName(name);
@@ -299,6 +310,56 @@ export const AppContainer = (props: AppContainerProps) => {
setSelectedAgentDefinition(undefined);
}, []);
// Subscribe to ASK_USER_REQUEST messages from the message bus
useEffect(() => {
const messageBus = config.getMessageBus();
const handler = (msg: AskUserRequest) => {
setAskUserRequest({
questions: msg.questions,
correlationId: msg.correlationId,
});
};
messageBus.subscribe(MessageBusType.ASK_USER_REQUEST, handler);
return () => {
messageBus.unsubscribe(MessageBusType.ASK_USER_REQUEST, handler);
};
}, [config]);
// Handler to submit ask_user answers
const handleAskUserSubmit = useCallback(
async (answers: { [questionIndex: string]: string }) => {
if (!askUserRequest) return;
const messageBus = config.getMessageBus();
await messageBus.publish({
type: MessageBusType.ASK_USER_RESPONSE,
correlationId: askUserRequest.correlationId,
answers,
});
setAskUserRequest(null);
},
[config, askUserRequest],
);
// Handler to cancel ask_user dialog
const handleAskUserCancel = useCallback(async () => {
if (!askUserRequest) return;
const messageBus = config.getMessageBus();
await messageBus.publish({
type: MessageBusType.ASK_USER_RESPONSE,
correlationId: askUserRequest.correlationId,
answers: {},
cancelled: true,
});
setAskUserRequest(null);
}, [config, askUserRequest]);
const toggleDebugProfiler = useCallback(
() => setShowDebugProfiler((prev) => !prev),
[],
@@ -1355,6 +1416,10 @@ Logging in with Google... Restarting Gemini CLI to continue.
}
if (keyMatchers[Command.QUIT](key)) {
// Skip when ask_user dialog is open (use Esc to cancel instead)
if (askUserRequest) {
return;
}
// If the user presses Ctrl+C, we want to cancel any ongoing requests.
// This should happen regardless of the count.
cancelOngoingRequest?.();
@@ -1442,6 +1507,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
setCtrlDPressCount,
handleSlashCommand,
cancelOngoingRequest,
askUserRequest,
activePtyId,
embeddedShellFocused,
settings.merged.general.debugKeystrokeLogging,
@@ -1554,6 +1620,7 @@ Logging in with Google... Restarting Gemini CLI to continue.
const nightly = props.version.includes('nightly');
const dialogsVisible =
!!askUserRequest ||
shouldShowIdePrompt ||
isFolderTrustDialogOpen ||
adminSettingsChanged ||
@@ -1988,9 +2055,15 @@ Logging in with Google... Restarting Gemini CLI to continue.
}}
>
<ToolActionsProvider config={config} toolCalls={allToolCalls}>
<ShellFocusContext.Provider value={isFocused}>
<App />
</ShellFocusContext.Provider>
<AskUserActionsProvider
request={askUserRequest}
onSubmit={handleAskUserSubmit}
onCancel={handleAskUserCancel}
>
<ShellFocusContext.Provider value={isFocused}>
<App />
</ShellFocusContext.Provider>
</AskUserActionsProvider>
</ToolActionsProvider>
</AppContext.Provider>
</ConfigContext.Provider>

View File

@@ -32,6 +32,8 @@ import process from 'node:process';
import { type UseHistoryManagerReturn } from '../hooks/useHistoryManager.js';
import { AdminSettingsChangedDialog } from './AdminSettingsChangedDialog.js';
import { IdeTrustChangeDialog } from './IdeTrustChangeDialog.js';
import { AskUserDialog } from './AskUserDialog.js';
import { useAskUserActions } from '../contexts/AskUserActionsContext.js';
import { NewAgentsNotification } from './NewAgentsNotification.js';
import { AgentConfigDialog } from './AgentConfigDialog.js';
@@ -57,6 +59,22 @@ export const DialogManager = ({
terminalWidth: uiTerminalWidth,
} = uiState;
const {
request: askUserRequest,
submit: askUserSubmit,
cancel: askUserCancel,
} = useAskUserActions();
if (askUserRequest) {
return (
<AskUserDialog
questions={askUserRequest.questions}
onSubmit={askUserSubmit}
onCancel={askUserCancel}
/>
);
}
if (uiState.adminSettingsChanged) {
return <AdminSettingsChangedDialog />;
}

View File

@@ -15,6 +15,7 @@ import { ToolConfirmationMessage } from './ToolConfirmationMessage.js';
import { theme } from '../../semantic-colors.js';
import { useConfig } from '../../contexts/ConfigContext.js';
import { isShellTool, isThisShellFocused } from './ToolShared.js';
import { ASK_USER_DISPLAY_NAME } from '@google/gemini-cli-core';
interface ToolGroupMessageProps {
groupId: number;
@@ -27,15 +28,30 @@ interface ToolGroupMessageProps {
onShellInputSubmit?: (input: string) => void;
}
// Helper to identify Ask User tools that are in progress (have their own dialog UI)
const isAskUserInProgress = (t: IndividualToolCallDisplay): boolean =>
t.name === ASK_USER_DISPLAY_NAME &&
[
ToolCallStatus.Pending,
ToolCallStatus.Executing,
ToolCallStatus.Confirming,
].includes(t.status);
// Main component renders the border and maps the tools using ToolMessage
export const ToolGroupMessage: React.FC<ToolGroupMessageProps> = ({
toolCalls,
toolCalls: allToolCalls,
availableTerminalHeight,
terminalWidth,
isFocused = true,
activeShellPtyId,
embeddedShellFocused,
}) => {
// Filter out in-progress Ask User tools (they have their own AskUserDialog UI)
const toolCalls = useMemo(
() => allToolCalls.filter((t) => !isAskUserInProgress(t)),
[allToolCalls],
);
const config = useConfig();
const isEventDriven = config.isEventDrivenSchedulerEnabled();

View File

@@ -18,6 +18,7 @@ import { theme } from '../../semantic-colors.js';
import {
type Config,
SHELL_TOOL_NAME,
ASK_USER_DISPLAY_NAME,
type ToolResultDisplay,
} from '@google/gemini-cli-core';
import { useInactivityTimer } from '../../hooks/useInactivityTimer.js';
@@ -198,13 +199,28 @@ export const ToolInfo: React.FC<ToolInfoProps> = ({
}
}
}, [emphasis]);
// Hide description for completed Ask User tools (the result display speaks for itself)
const isCompletedAskUser =
name === ASK_USER_DISPLAY_NAME &&
[
ToolCallStatus.Success,
ToolCallStatus.Error,
ToolCallStatus.Canceled,
].includes(status);
return (
<Box overflow="hidden" height={1} flexGrow={1} flexShrink={1}>
<Text strikethrough={status === ToolCallStatus.Canceled} wrap="truncate">
<Text color={nameColor} bold>
{name}
</Text>{' '}
<Text color={theme.text.secondary}>{description}</Text>
</Text>
{!isCompletedAskUser && (
<>
{' '}
<Text color={theme.text.secondary}>{description}</Text>
</>
)}
</Text>
</Box>
);

View File

@@ -0,0 +1,77 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import { createContext, useContext, useMemo } from 'react';
import type { Question } from '@google/gemini-cli-core';
export interface AskUserState {
questions: Question[];
correlationId: string;
}
interface AskUserActionsContextValue {
/** Current ask_user request, or null if no dialog should be shown */
request: AskUserState | null;
/** Submit answers - publishes ASK_USER_RESPONSE to message bus */
submit: (answers: { [questionIndex: string]: string }) => Promise<void>;
/** Cancel the dialog - clears request state */
cancel: () => void;
}
const AskUserActionsContext = createContext<AskUserActionsContextValue | null>(
null,
);
export const useAskUserActions = () => {
const context = useContext(AskUserActionsContext);
if (!context) {
throw new Error(
'useAskUserActions must be used within an AskUserActionsProvider',
);
}
return context;
};
interface AskUserActionsProviderProps {
children: React.ReactNode;
/** Current ask_user request state (managed by AppContainer) */
request: AskUserState | null;
/** Handler to submit answers */
onSubmit: (answers: { [questionIndex: string]: string }) => Promise<void>;
/** Handler to cancel the dialog */
onCancel: () => void;
}
/**
* Provides ask_user dialog state and actions to child components.
*
* State is managed by AppContainer (which subscribes to the message bus)
* and passed here as props. This follows the same pattern as ToolActionsProvider.
*/
export const AskUserActionsProvider: React.FC<AskUserActionsProviderProps> = ({
children,
request,
onSubmit,
onCancel,
}) => {
const value = useMemo(
() => ({
request,
submit: onSubmit,
cancel: onCancel,
}),
[request, onSubmit, onCancel],
);
return (
<AskUserActionsContext.Provider value={value}>
{children}
</AskUserActionsContext.Provider>
);
};