diff --git a/packages/cli/src/test-utils/render.tsx b/packages/cli/src/test-utils/render.tsx
index f1e87c0f15..97891067fe 100644
--- a/packages/cli/src/test-utils/render.tsx
+++ b/packages/cli/src/test-utils/render.tsx
@@ -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}
>
-
-
-
-
- {component}
-
-
-
-
+
+
+
+
+
+ {component}
+
+
+
+
+
diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx
index f08d947eca..333c5b4cc3 100644
--- a/packages/cli/src/ui/AppContainer.tsx
+++ b/packages/cli/src/ui/AppContainer.tsx
@@ -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(
+ 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.
}}
>
-
-
-
+
+
+
+
+
diff --git a/packages/cli/src/ui/components/DialogManager.tsx b/packages/cli/src/ui/components/DialogManager.tsx
index b8bf51a81e..b1a159c93e 100644
--- a/packages/cli/src/ui/components/DialogManager.tsx
+++ b/packages/cli/src/ui/components/DialogManager.tsx
@@ -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 (
+
+ );
+ }
+
if (uiState.adminSettingsChanged) {
return ;
}
diff --git a/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx b/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx
index 6c865640c3..a43c67dadd 100644
--- a/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx
+++ b/packages/cli/src/ui/components/messages/ToolGroupMessage.tsx
@@ -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 = ({
- 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();
diff --git a/packages/cli/src/ui/components/messages/ToolShared.tsx b/packages/cli/src/ui/components/messages/ToolShared.tsx
index ccd38f6f77..46065fe59e 100644
--- a/packages/cli/src/ui/components/messages/ToolShared.tsx
+++ b/packages/cli/src/ui/components/messages/ToolShared.tsx
@@ -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 = ({
}
}
}, [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 (
{name}
- {' '}
- {description}
+
+ {!isCompletedAskUser && (
+ <>
+ {' '}
+ {description}
+ >
+ )}
);
diff --git a/packages/cli/src/ui/contexts/AskUserActionsContext.tsx b/packages/cli/src/ui/contexts/AskUserActionsContext.tsx
new file mode 100644
index 0000000000..5e77fce3e5
--- /dev/null
+++ b/packages/cli/src/ui/contexts/AskUserActionsContext.tsx
@@ -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;
+
+ /** Cancel the dialog - clears request state */
+ cancel: () => void;
+}
+
+const AskUserActionsContext = createContext(
+ 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;
+ /** 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 = ({
+ children,
+ request,
+ onSubmit,
+ onCancel,
+}) => {
+ const value = useMemo(
+ () => ({
+ request,
+ submit: onSubmit,
+ cancel: onCancel,
+ }),
+ [request, onSubmit, onCancel],
+ );
+
+ return (
+
+ {children}
+
+ );
+};
diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts
index b4be2cbdfc..ab83dc595d 100644
--- a/packages/core/src/config/config.ts
+++ b/packages/core/src/config/config.ts
@@ -32,6 +32,7 @@ import { WriteFileTool } from '../tools/write-file.js';
import { WebFetchTool } from '../tools/web-fetch.js';
import { MemoryTool, setGeminiMdFilename } from '../tools/memoryTool.js';
import { WebSearchTool } from '../tools/web-search.js';
+import { AskUserTool } from '../tools/ask-user.js';
import { GeminiClient } from '../core/client.js';
import { BaseLlmClient } from '../core/baseLlmClient.js';
import type { HookDefinition, HookEventName } from '../hooks/types.js';
@@ -2005,6 +2006,7 @@ export class Config {
registerCoreTool(ShellTool, this);
registerCoreTool(MemoryTool);
registerCoreTool(WebSearchTool, this);
+ registerCoreTool(AskUserTool);
if (this.getUseWriteTodos()) {
registerCoreTool(WriteTodosTool);
}
diff --git a/packages/core/src/confirmation-bus/types.ts b/packages/core/src/confirmation-bus/types.ts
index 9279485986..5a27b08d40 100644
--- a/packages/core/src/confirmation-bus/types.ts
+++ b/packages/core/src/confirmation-bus/types.ts
@@ -134,11 +134,11 @@ export interface Question {
header: string;
/** Question type: 'choice' renders selectable options, 'text' renders free-form input, 'yesno' renders a binary Yes/No choice. Defaults to 'choice'. */
type?: QuestionType;
- /** Available choices. Required when type is 'choice' (or omitted), ignored for 'text'. */
+ /** Selectable choices. REQUIRED when type='choice' or omitted. IGNORED for 'text' and 'yesno'. */
options?: QuestionOption[];
- /** Allow multiple selections. Only applies to 'choice' type. */
+ /** Allow multiple selections. Only applies when type='choice'. */
multiSelect?: boolean;
- /** Placeholder hint text for 'text' type input field. */
+ /** Placeholder hint text. Only applies when type='text'. */
placeholder?: string;
}
@@ -152,6 +152,8 @@ export interface AskUserResponse {
type: MessageBusType.ASK_USER_RESPONSE;
correlationId: string;
answers: { [questionIndex: string]: string };
+ /** When true, indicates the user cancelled the dialog without submitting answers */
+ cancelled?: boolean;
}
export type Message =
diff --git a/packages/core/src/tools/ask-user.test.ts b/packages/core/src/tools/ask-user.test.ts
index 05b64313b9..01dfefb2ee 100644
--- a/packages/core/src/tools/ask-user.test.ts
+++ b/packages/core/src/tools/ask-user.test.ts
@@ -87,7 +87,9 @@ describe('AskUserTool', () => {
},
],
});
- expect(result).toContain('must NOT have fewer than 2 items');
+ expect(result).toContain(
+ "type='choice' requires 'options' array with 2-4 items",
+ );
});
it('should return error if options has more than 4 items', () => {
@@ -106,7 +108,7 @@ describe('AskUserTool', () => {
},
],
});
- expect(result).toContain('must NOT have more than 4 items');
+ expect(result).toContain("'options' array must have at most 4 items");
});
it('should return null for valid params', () => {
@@ -124,6 +126,91 @@ describe('AskUserTool', () => {
});
expect(result).toBeNull();
});
+
+ it('should return error if choice type has no options', () => {
+ const result = tool.validateToolParams({
+ questions: [
+ {
+ question: 'Pick one?',
+ header: 'Choice',
+ type: QuestionType.CHOICE,
+ },
+ ],
+ });
+ expect(result).toContain("type='choice' requires 'options'");
+ });
+
+ it('should return error if type is omitted and options missing (defaults to choice)', () => {
+ const result = tool.validateToolParams({
+ questions: [
+ {
+ question: 'Pick one?',
+ header: 'Choice',
+ // type omitted, defaults to 'choice'
+ // options missing
+ },
+ ],
+ });
+ expect(result).toContain("type='choice' requires 'options'");
+ });
+
+ it('should accept text type without options', () => {
+ const result = tool.validateToolParams({
+ questions: [
+ {
+ question: 'Enter your name?',
+ header: 'Name',
+ type: QuestionType.TEXT,
+ },
+ ],
+ });
+ expect(result).toBeNull();
+ });
+
+ it('should accept yesno type without options', () => {
+ const result = tool.validateToolParams({
+ questions: [
+ {
+ question: 'Do you want to proceed?',
+ header: 'Confirm',
+ type: QuestionType.YESNO,
+ },
+ ],
+ });
+ expect(result).toBeNull();
+ });
+
+ it('should return error if option has empty label', () => {
+ const result = tool.validateToolParams({
+ questions: [
+ {
+ question: 'Pick one?',
+ header: 'Choice',
+ options: [
+ { label: '', description: 'Empty label' },
+ { label: 'B', description: 'Option B' },
+ ],
+ },
+ ],
+ });
+ expect(result).toContain("'label' is required");
+ });
+
+ it('should return error if option is missing description', () => {
+ const result = tool.validateToolParams({
+ questions: [
+ {
+ question: 'Pick one?',
+ header: 'Choice',
+ options: [
+ { label: 'A' } as { label: string; description: string },
+ { label: 'B', description: 'Option B' },
+ ],
+ },
+ ],
+ });
+ expect(result).toContain("must have required property 'description'");
+ });
});
it('should publish ASK_USER_REQUEST and wait for response', async () => {
@@ -195,6 +282,46 @@ describe('AskUserTool', () => {
expect(JSON.parse(result.llmContent as string)).toEqual({ answers });
});
+ it('should display message when user submits without answering', async () => {
+ const questions = [
+ {
+ question: 'Which approach?',
+ header: 'Approach',
+ options: [
+ { label: 'Option A', description: 'First option' },
+ { label: 'Option B', description: 'Second option' },
+ ],
+ },
+ ];
+
+ const invocation = tool.build({ questions });
+ const executePromise = invocation.execute(new AbortController().signal);
+
+ // Get the correlation ID from the published message
+ const publishCall = vi.mocked(mockMessageBus.publish).mock.calls[0][0] as {
+ correlationId: string;
+ };
+ const correlationId = publishCall.correlationId;
+
+ // Simulate response with empty answers
+ const subscribeCall = vi
+ .mocked(mockMessageBus.subscribe)
+ .mock.calls.find((call) => call[0] === MessageBusType.ASK_USER_RESPONSE);
+ const handler = subscribeCall![1];
+
+ handler({
+ type: MessageBusType.ASK_USER_RESPONSE,
+ correlationId,
+ answers: {},
+ });
+
+ const result = await executePromise;
+ expect(result.returnDisplay).toBe(
+ 'User submitted without answering questions.',
+ );
+ expect(JSON.parse(result.llmContent as string)).toEqual({ answers: {} });
+ });
+
it('should handle cancellation', async () => {
const invocation = tool.build({
questions: [
diff --git a/packages/core/src/tools/ask-user.ts b/packages/core/src/tools/ask-user.ts
index 7d0fb8ef3a..81d62d021c 100644
--- a/packages/core/src/tools/ask-user.ts
+++ b/packages/core/src/tools/ask-user.ts
@@ -20,7 +20,7 @@ import {
type AskUserResponse,
} from '../confirmation-bus/types.js';
import { randomUUID } from 'node:crypto';
-import { ASK_USER_TOOL_NAME } from './tool-names.js';
+import { ASK_USER_TOOL_NAME, ASK_USER_DISPLAY_NAME } from './tool-names.js';
export interface AskUserParams {
questions: Question[];
@@ -33,7 +33,7 @@ export class AskUserTool extends BaseDeclarativeTool<
constructor(messageBus: MessageBus) {
super(
ASK_USER_TOOL_NAME,
- 'Ask User',
+ ASK_USER_DISPLAY_NAME,
'Ask the user one or more questions to gather preferences, clarify requirements, or make decisions.',
Kind.Communicate,
{
@@ -62,15 +62,14 @@ export class AskUserTool extends BaseDeclarativeTool<
type: {
type: 'string',
enum: ['choice', 'text', 'yesno'],
+ default: 'choice',
description:
- "Question type. 'choice' (default) shows selectable options, 'text' shows a free-form text input, 'yesno' shows a binary Yes/No choice.",
+ "Question type: 'choice' (default) for multiple-choice with options, 'text' for free-form input, 'yesno' for Yes/No confirmation.",
},
options: {
type: 'array',
description:
- "Required for 'choice' type, ignored for 'text' and 'yesno'. The available choices (2-4 options). Do NOT include an 'Other' option - one is automatically added for 'choice' type.",
- minItems: 2,
- maxItems: 4,
+ "The selectable choices for 'choice' type questions. Provide 2-4 options. An 'Other' option is automatically added. Not needed for 'text' or 'yesno' types.",
items: {
type: 'object',
required: ['label', 'description'],
@@ -78,12 +77,12 @@ export class AskUserTool extends BaseDeclarativeTool<
label: {
type: 'string',
description:
- 'The display text for this option that the user will see and select. Should be concise (1-5 words) and clearly describe the choice.',
+ 'The display text for this option (1-5 words). Example: "OAuth 2.0"',
},
description: {
type: 'string',
description:
- 'Explanation of what this option means or what will happen if chosen. Useful for providing context about trade-offs or implications.',
+ 'Brief explanation of this option. Example: "Industry standard, supports SSO"',
},
},
},
@@ -91,12 +90,12 @@ export class AskUserTool extends BaseDeclarativeTool<
multiSelect: {
type: 'boolean',
description:
- "Only applies to 'choice' type. Set to true to allow multiple selections.",
+ "Only applies when type='choice'. Set to true to allow selecting multiple options.",
},
placeholder: {
type: 'string',
description:
- "Optional hint text for 'text' type input field.",
+ "Only applies when type='text'. Hint text shown in the input field.",
},
},
},
@@ -107,6 +106,51 @@ export class AskUserTool extends BaseDeclarativeTool<
);
}
+ protected override validateToolParamValues(
+ params: AskUserParams,
+ ): string | null {
+ if (!params.questions || params.questions.length === 0) {
+ return 'At least one question is required.';
+ }
+
+ for (let i = 0; i < params.questions.length; i++) {
+ const q = params.questions[i];
+ const questionType = q.type ?? QuestionType.CHOICE;
+
+ // Validate that 'choice' type has options
+ if (questionType === QuestionType.CHOICE) {
+ if (!q.options || q.options.length < 2) {
+ return `Question ${i + 1}: type='choice' requires 'options' array with 2-4 items.`;
+ }
+ if (q.options.length > 4) {
+ return `Question ${i + 1}: 'options' array must have at most 4 items.`;
+ }
+ }
+
+ // Validate option structure if provided
+ if (q.options) {
+ for (let j = 0; j < q.options.length; j++) {
+ const opt = q.options[j];
+ if (
+ !opt.label ||
+ typeof opt.label !== 'string' ||
+ !opt.label.trim()
+ ) {
+ return `Question ${i + 1}, option ${j + 1}: 'label' is required and must be a non-empty string.`;
+ }
+ if (
+ opt.description === undefined ||
+ typeof opt.description !== 'string'
+ ) {
+ return `Question ${i + 1}, option ${j + 1}: 'description' is required and must be a string.`;
+ }
+ }
+ }
+ }
+
+ return null;
+ }
+
protected createInvocation(
params: AskUserParams,
messageBus: MessageBus,
@@ -148,16 +192,28 @@ export class AskUserInvocation extends BaseToolInvocation<
if (response.correlationId === correlationId) {
cleanup();
- // Build formatted key-value display
- const formattedAnswers = Object.entries(response.answers)
- .map(([index, answer]) => {
- const question = this.params.questions[parseInt(index, 10)];
- const category = question?.header ?? `Q${index}`;
- return ` ${category} → ${answer}`;
- })
- .join('\n');
+ // Handle user cancellation
+ if (response.cancelled) {
+ resolve({
+ llmContent: 'User dismissed ask user dialog without answering.',
+ returnDisplay: 'User dismissed dialog',
+ });
+ return;
+ }
- const returnDisplay = `User answered:\n${formattedAnswers}`;
+ // Build formatted key-value display
+ const answerEntries = Object.entries(response.answers);
+ const hasAnswers = answerEntries.length > 0;
+
+ const returnDisplay = hasAnswers
+ ? `**User answered:**\n${answerEntries
+ .map(([index, answer]) => {
+ const question = this.params.questions[parseInt(index, 10)];
+ const category = question?.header ?? `Q${index}`;
+ return ` ${category} → ${answer}`;
+ })
+ .join('\n')}`
+ : 'User submitted without answering questions.';
resolve({
llmContent: JSON.stringify({ answers: response.answers }),
diff --git a/packages/core/src/tools/tool-names.ts b/packages/core/src/tools/tool-names.ts
index 897c846c57..e00b626579 100644
--- a/packages/core/src/tools/tool-names.ts
+++ b/packages/core/src/tools/tool-names.ts
@@ -24,6 +24,7 @@ export const GET_INTERNAL_DOCS_TOOL_NAME = 'get_internal_docs';
export const ACTIVATE_SKILL_TOOL_NAME = 'activate_skill';
export const EDIT_TOOL_NAMES = new Set([EDIT_TOOL_NAME, WRITE_FILE_TOOL_NAME]);
export const ASK_USER_TOOL_NAME = 'ask_user';
+export const ASK_USER_DISPLAY_NAME = 'Ask User';
/** Prefix used for tools discovered via the toolDiscoveryCommand. */
export const DISCOVERED_TOOL_PREFIX = 'discovered_tool_';