diff --git a/packages/cli/src/ui/AppContainer.test.tsx b/packages/cli/src/ui/AppContainer.test.tsx
index 431c78ab48..b580f2fed6 100644
--- a/packages/cli/src/ui/AppContainer.test.tsx
+++ b/packages/cli/src/ui/AppContainer.test.tsx
@@ -29,7 +29,13 @@ import {
type ResumedSessionData,
AuthType,
type AgentDefinition,
+ MessageBusType,
+ QuestionType,
} from '@google/gemini-cli-core';
+import {
+ AskUserActionsContext,
+ type AskUserState,
+} from './contexts/AskUserActionsContext.js';
// Mock coreEvents
const mockCoreEvents = vi.hoisted(() => ({
@@ -107,9 +113,11 @@ vi.mock('ink', async (importOriginal) => {
// so we can assert against them in our tests.
let capturedUIState: UIState;
let capturedUIActions: UIActions;
+let capturedAskUserRequest: AskUserState | null;
function TestContextConsumer() {
capturedUIState = useContext(UIStateContext)!;
capturedUIActions = useContext(UIActionsContext)!;
+ capturedAskUserRequest = useContext(AskUserActionsContext)?.request ?? null;
return null;
}
@@ -259,6 +267,7 @@ describe('AppContainer State Management', () => {
mocks.mockStdout.write.mockClear();
capturedUIState = null!;
+ capturedAskUserRequest = null;
// **Provide a default return value for EVERY mocked hook.**
mockedUseQuotaAndFallback.mockReturnValue({
@@ -2498,6 +2507,41 @@ describe('AppContainer State Management', () => {
unmount!();
});
+
+ it('should show ask user dialog when request is received', async () => {
+ let unmount: () => void;
+ await act(async () => {
+ const result = renderAppContainer();
+ unmount = result.unmount;
+ });
+
+ const questions = [
+ {
+ question: 'What is your favorite color?',
+ header: 'Color Preference',
+ type: QuestionType.TEXT,
+ },
+ ];
+
+ await act(async () => {
+ await mockConfig.getMessageBus().publish({
+ type: MessageBusType.ASK_USER_REQUEST,
+ questions,
+ correlationId: 'test-id',
+ });
+ });
+
+ await waitFor(
+ () => {
+ expect(capturedAskUserRequest).not.toBeNull();
+ expect(capturedAskUserRequest?.questions).toEqual(questions);
+ expect(capturedAskUserRequest?.correlationId).toBe('test-id');
+ },
+ { timeout: 2000 },
+ );
+
+ unmount!();
+ });
});
describe('Regression Tests', () => {
diff --git a/packages/cli/src/ui/components/messages/ToolGroupMessage.test.tsx b/packages/cli/src/ui/components/messages/ToolGroupMessage.test.tsx
index 61ee78f498..2a9be3f7b5 100644
--- a/packages/cli/src/ui/components/messages/ToolGroupMessage.test.tsx
+++ b/packages/cli/src/ui/components/messages/ToolGroupMessage.test.tsx
@@ -8,14 +8,19 @@ import {
renderWithProviders,
createMockSettings,
} from '../../../test-utils/render.js';
-import { describe, it, expect, vi } from 'vitest';
+import { describe, it, expect, vi, afterEach } from 'vitest';
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';
+import { ASK_USER_DISPLAY_NAME, makeFakeConfig } from '@google/gemini-cli-core';
+import os from 'node:os';
describe('', () => {
+ afterEach(() => {
+ vi.restoreAllMocks();
+ });
+
const createToolCall = (
overrides: Partial = {},
): IndividualToolCallDisplay => ({
@@ -35,16 +40,16 @@ describe('', () => {
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;
+ const baseMockConfig = makeFakeConfig({
+ model: 'gemini-pro',
+ targetDir: os.tmpdir(),
+ debugMode: false,
+ folderTrust: false,
+ ideMode: false,
+ enableInteractiveShell: true,
+ previewFeatures: false,
+ enableEventDrivenScheduler: true,
+ });
describe('Golden Snapshots', () => {
it('renders single successful tool call', () => {
@@ -83,10 +88,11 @@ describe('', () => {
status: ToolCallStatus.Error,
}),
];
- const mockConfig = {
- ...baseMockConfig,
- isEventDrivenSchedulerEnabled: () => false,
- } as unknown as Config;
+ const mockConfig = makeFakeConfig({
+ model: 'gemini-pro',
+ targetDir: os.tmpdir(),
+ enableEventDrivenScheduler: false,
+ });
const { lastFrame, unmount } = renderWithProviders(
,
@@ -116,10 +122,11 @@ describe('', () => {
},
}),
];
- const mockConfig = {
- ...baseMockConfig,
- isEventDrivenSchedulerEnabled: () => false,
- } as unknown as Config;
+ const mockConfig = makeFakeConfig({
+ model: 'gemini-pro',
+ targetDir: os.tmpdir(),
+ enableEventDrivenScheduler: false,
+ });
const { lastFrame, unmount } = renderWithProviders(
,
@@ -177,10 +184,11 @@ describe('', () => {
status: ToolCallStatus.Pending,
}),
];
- const mockConfig = {
- ...baseMockConfig,
- isEventDrivenSchedulerEnabled: () => false,
- } as unknown as Config;
+ const mockConfig = makeFakeConfig({
+ model: 'gemini-pro',
+ targetDir: os.tmpdir(),
+ enableEventDrivenScheduler: false,
+ });
const { lastFrame, unmount } = renderWithProviders(
,
@@ -381,10 +389,11 @@ describe('', () => {
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 mockConfig = makeFakeConfig({
+ model: 'gemini-pro',
+ targetDir: os.tmpdir(),
+ enableEventDrivenScheduler: false,
+ });
const { lastFrame, unmount } = renderWithProviders(
,
@@ -503,10 +512,11 @@ describe('', () => {
},
}),
];
- const mockConfig = {
- ...baseMockConfig,
- isEventDrivenSchedulerEnabled: () => false,
- } as unknown as Config;
+ const mockConfig = makeFakeConfig({
+ model: 'gemini-pro',
+ targetDir: os.tmpdir(),
+ enableEventDrivenScheduler: false,
+ });
const { lastFrame, unmount } = renderWithProviders(
,
@@ -539,10 +549,11 @@ describe('', () => {
const settings = createMockSettings({
security: { enablePermanentToolApproval: true },
});
- const mockConfig = {
- ...baseMockConfig,
- isEventDrivenSchedulerEnabled: () => false,
- } as unknown as Config;
+ const mockConfig = makeFakeConfig({
+ model: 'gemini-pro',
+ targetDir: os.tmpdir(),
+ enableEventDrivenScheduler: false,
+ });
const { lastFrame, unmount } = renderWithProviders(
,
@@ -574,10 +585,11 @@ describe('', () => {
}),
];
- const mockConfig = {
- ...baseMockConfig,
- isEventDrivenSchedulerEnabled: () => false,
- } as unknown as Config;
+ const mockConfig = makeFakeConfig({
+ model: 'gemini-pro',
+ targetDir: os.tmpdir(),
+ enableEventDrivenScheduler: false,
+ });
const { lastFrame, unmount } = renderWithProviders(
,
@@ -604,10 +616,7 @@ describe('', () => {
}),
];
- const mockConfig = {
- ...baseMockConfig,
- isEventDrivenSchedulerEnabled: () => true,
- } as unknown as Config;
+ const mockConfig = baseMockConfig;
const { lastFrame, unmount } = renderWithProviders(
,
@@ -640,10 +649,7 @@ describe('', () => {
}),
];
- const mockConfig = {
- ...baseMockConfig,
- isEventDrivenSchedulerEnabled: () => true,
- } as unknown as Config;
+ const mockConfig = baseMockConfig;
const { lastFrame, unmount } = renderWithProviders(
,
@@ -658,4 +664,72 @@ describe('', () => {
unmount();
});
});
+
+ describe('Ask User Filtering', () => {
+ it.each([
+ ToolCallStatus.Pending,
+ ToolCallStatus.Executing,
+ ToolCallStatus.Confirming,
+ ])('filters out ask_user when status is %s', (status) => {
+ const toolCalls = [
+ createToolCall({
+ callId: `ask-user-${status}`,
+ name: ASK_USER_DISPLAY_NAME,
+ status,
+ }),
+ ];
+
+ const { lastFrame, unmount } = renderWithProviders(
+ ,
+ { config: baseMockConfig },
+ );
+
+ expect(lastFrame()).toMatchSnapshot();
+ unmount();
+ });
+
+ it.each([ToolCallStatus.Success, ToolCallStatus.Error])(
+ 'does NOT filter out ask_user when status is %s',
+ (status) => {
+ const toolCalls = [
+ createToolCall({
+ callId: `ask-user-${status}`,
+ name: ASK_USER_DISPLAY_NAME,
+ status,
+ }),
+ ];
+
+ const { lastFrame, unmount } = renderWithProviders(
+ ,
+ { config: baseMockConfig },
+ );
+
+ expect(lastFrame()).toMatchSnapshot();
+ unmount();
+ },
+ );
+
+ it('shows other tools when ask_user is filtered out', () => {
+ const toolCalls = [
+ createToolCall({
+ callId: 'other-tool',
+ name: 'other-tool',
+ status: ToolCallStatus.Success,
+ }),
+ createToolCall({
+ callId: 'ask-user-pending',
+ name: ASK_USER_DISPLAY_NAME,
+ status: ToolCallStatus.Pending,
+ }),
+ ];
+
+ const { lastFrame, unmount } = renderWithProviders(
+ ,
+ { config: baseMockConfig },
+ );
+
+ expect(lastFrame()).toMatchSnapshot();
+ unmount();
+ });
+ });
});
diff --git a/packages/cli/src/ui/components/messages/__snapshots__/ToolGroupMessage.test.tsx.snap b/packages/cli/src/ui/components/messages/__snapshots__/ToolGroupMessage.test.tsx.snap
index e664288a06..422c3de760 100644
--- a/packages/cli/src/ui/components/messages/__snapshots__/ToolGroupMessage.test.tsx.snap
+++ b/packages/cli/src/ui/components/messages/__snapshots__/ToolGroupMessage.test.tsx.snap
@@ -1,5 +1,35 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+exports[` > Ask User Filtering > does NOT filter out ask_user when status is Error 1`] = `
+"╭──────────────────────────────────────────────────────────────────────────────╮
+│ x Ask User │
+│ │
+│ Test result │
+╰──────────────────────────────────────────────────────────────────────────────╯"
+`;
+
+exports[` > Ask User Filtering > does NOT filter out ask_user when status is Success 1`] = `
+"╭──────────────────────────────────────────────────────────────────────────────╮
+│ ✓ Ask User │
+│ │
+│ Test result │
+╰──────────────────────────────────────────────────────────────────────────────╯"
+`;
+
+exports[` > Ask User Filtering > filters out ask_user when status is Confirming 1`] = `""`;
+
+exports[` > Ask User Filtering > filters out ask_user when status is Executing 1`] = `""`;
+
+exports[` > Ask User Filtering > filters out ask_user when status is Pending 1`] = `""`;
+
+exports[` > Ask User Filtering > shows other tools when ask_user is filtered out 1`] = `
+"╭──────────────────────────────────────────────────────────────────────────────╮
+│ ✓ other-tool A tool for testing │
+│ │
+│ Test result │
+╰──────────────────────────────────────────────────────────────────────────────╯"
+`;
+
exports[` > Border Color Logic > uses gray border when all tools are successful and no shell commands 1`] = `
"╭──────────────────────────────────────────────────────────────────────────────╮
│ ✓ test-tool A tool for testing │
diff --git a/packages/cli/src/ui/contexts/AskUserActionsContext.tsx b/packages/cli/src/ui/contexts/AskUserActionsContext.tsx
index 5e77fce3e5..b76423505f 100644
--- a/packages/cli/src/ui/contexts/AskUserActionsContext.tsx
+++ b/packages/cli/src/ui/contexts/AskUserActionsContext.tsx
@@ -24,9 +24,8 @@ interface AskUserActionsContextValue {
cancel: () => void;
}
-const AskUserActionsContext = createContext(
- null,
-);
+export const AskUserActionsContext =
+ createContext(null);
export const useAskUserActions = () => {
const context = useContext(AskUserActionsContext);