diff --git a/packages/cli/src/ui/AppContainer.test.tsx b/packages/cli/src/ui/AppContainer.test.tsx index 13550d3f42..38117df6bf 100644 --- a/packages/cli/src/ui/AppContainer.test.tsx +++ b/packages/cli/src/ui/AppContainer.test.tsx @@ -34,12 +34,19 @@ import { } from '@google/gemini-cli-core'; // Mock coreEvents -const mockCoreEvents = vi.hoisted(() => ({ - on: vi.fn(), - off: vi.fn(), - drainBacklogs: vi.fn(), - emit: vi.fn(), -})); +const mockCoreEvents = vi.hoisted(() => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const mock: any = { + on: vi.fn(), + off: vi.fn(), + drainBacklogs: vi.fn(), + emit: vi.fn(), + }; + mock.emitFeedback = vi.fn((severity, message, error) => { + mock.emit('feedback', { type: severity, message, error }); + }); + return mock; +}); // Mock IdeClient const mockIdeClient = vi.hoisted(() => ({ @@ -98,7 +105,7 @@ import ansiEscapes from 'ansi-escapes'; import { mergeSettings, type LoadedSettings } from '../config/settings.js'; import type { InitializationResult } from '../core/initializer.js'; import { useQuotaAndFallback } from './hooks/useQuotaAndFallback.js'; -import { StreamingState } from './types.js'; +import { StreamingState, AuthState } from './types.js'; import { UIStateContext, type UIState } from './contexts/UIStateContext.js'; import { UIActionsContext, @@ -3394,7 +3401,10 @@ describe('AppContainer State Management', () => { }).unmount; }); - await waitFor(() => expect(capturedUIActions).toBeTruthy()); + await waitFor(() => { + expect(capturedUIActions).toBeTruthy(); + expect(capturedUIState.isConfigInitialized).toBe(true); + }); // Expand first act(() => capturedUIActions.setConstrainHeight(false)); @@ -3436,7 +3446,10 @@ describe('AppContainer State Management', () => { }).unmount; }); - await waitFor(() => expect(capturedUIActions).toBeTruthy()); + await waitFor(() => { + expect(capturedUIActions).toBeTruthy(); + expect(capturedUIState.isConfigInitialized).toBe(true); + }); // Expand first act(() => capturedUIActions.setConstrainHeight(false)); @@ -3747,7 +3760,10 @@ describe('AppContainer State Management', () => { let unmount: () => void; await act(async () => (unmount = renderAppContainer().unmount)); - await waitFor(() => expect(capturedUIActions).toBeTruthy()); + await waitFor(() => { + expect(capturedUIActions).toBeTruthy(); + expect(capturedUIState.isConfigInitialized).toBe(true); + }); await act(async () => capturedUIActions.handleFinalSubmit('read @file.txt'), @@ -3776,7 +3792,10 @@ describe('AppContainer State Management', () => { let unmount: () => void; await act(async () => (unmount = renderAppContainer().unmount)); - await waitFor(() => expect(capturedUIActions).toBeTruthy()); + await waitFor(() => { + expect(capturedUIActions).toBeTruthy(); + expect(capturedUIState.isConfigInitialized).toBe(true); + }); await act(async () => capturedUIActions.handleFinalSubmit('read @file.txt'), @@ -3893,4 +3912,160 @@ describe('AppContainer State Management', () => { unmount!(); }); }); + + describe('Authentication Optimization', () => { + it('does NOT show AuthInProgress and unblocks UI during initial authentication', async () => { + const mockedUseAuthCommand = useAuthCommand as Mock; + mockedUseAuthCommand.mockReturnValue({ + authState: AuthState.Unauthenticated, // isAuthenticating will be true + setAuthState: vi.fn(), + authError: null, + onAuthError: vi.fn(), + }); + + let unmount: () => void; + await act(async () => { + unmount = renderAppContainer().unmount; + }); + + await waitFor(() => expect(capturedUIState).toBeTruthy()); + + // dialogsVisible should be false even if isAuthenticating is true + expect(capturedUIState.isAuthenticating).toBe(true); + expect(capturedUIState.dialogsVisible).toBe(false); + unmount!(); + }); + + it('allows typing and queues prompts if submitted while initializing', async () => { + mockedUseAuthCommand.mockReturnValue({ + authState: AuthState.Authenticated, + setAuthState: vi.fn(), + authError: null, + onAuthError: vi.fn(), + }); + + let unmount: () => void; + await act(async () => { + vi.spyOn(mockConfig, 'isInitialized').mockReturnValue(false); + const initPromise = new Promise(() => {}); // Never resolves + // eslint-disable-next-line @typescript-eslint/no-explicit-any + vi.spyOn(mockConfig, 'initialize').mockReturnValue(initPromise as any); + + unmount = renderAppContainer().unmount; + }); + + await waitFor(() => expect(capturedUIState).toBeTruthy()); + expect(capturedUIState.isInputActive).toBe(true); + expect(capturedUIState.isConfigInitialized).toBe(false); + + // Reset mockCoreEvents.emit feedback call count + mockCoreEvents.emit.mockClear(); + + // Submit the prompt while still initializing + await act(async () => { + await capturedUIActions.handleFinalSubmit('hello'); + }); + + // Feedback should be emitted with "Initializing..." message + expect(mockCoreEvents.emit).toHaveBeenCalledWith( + 'feedback', + expect.objectContaining({ + type: 'info', + message: + 'Initializing... Slash commands are still available and prompts will be queued.', + }), + ); + + unmount!(); + }); + + it('queues prompts and shows feedback if submitted while authenticating', async () => { + const mockedUseAuthCommand = useAuthCommand as Mock; + mockedUseAuthCommand.mockReturnValue({ + authState: AuthState.Unauthenticated, + setAuthState: vi.fn(), + authError: null, + onAuthError: vi.fn(), + }); + + let unmount: () => void; + await act(async () => { + unmount = renderAppContainer().unmount; + }); + + await waitFor(() => { + expect(capturedUIActions).toBeTruthy(); + expect(capturedUIState.isConfigInitialized).toBe(true); + }); + + // Reset mockCoreEvents.emit feedback call count + mockCoreEvents.emit.mockClear(); + + // Submit a prompt while authenticating + await act(async () => { + await capturedUIActions.handleFinalSubmit('hello'); + }); + + // Feedback should be emitted + expect(mockCoreEvents.emit).toHaveBeenCalledWith( + 'feedback', + expect.objectContaining({ + type: 'info', + message: + 'Authentication is still in progress... Slash commands are still available and prompts will be queued.', + }), + ); + + unmount!(); + }); + + it('queues prompts and shows feedback if submitted while initializing', async () => { + mockedUseAuthCommand.mockReturnValue({ + authState: AuthState.Authenticated, + setAuthState: vi.fn(), + authError: null, + onAuthError: vi.fn(), + }); + + // Override the default mock to simulate non-initialized config + let unmount: () => void; + await act(async () => { + // We need to render with isConfigInitialized = false + // AppContainer state starts with isConfigInitialized = false + // and only sets it to true after config.initialize() completes. + // In tests, we can control how long config.initialize() takes. + vi.spyOn(mockConfig, 'isInitialized').mockReturnValue(false); + const initPromise = new Promise(() => {}); // Never resolves + // eslint-disable-next-line @typescript-eslint/no-explicit-any + vi.spyOn(mockConfig, 'initialize').mockReturnValue(initPromise as any); + + unmount = renderAppContainer().unmount; + }); + + await waitFor(() => expect(capturedUIActions).toBeTruthy()); + + // isConfigInitialized should be false because we mocked initialize() to never resolve + expect(capturedUIState.isConfigInitialized).toBe(false); + + // Reset mockCoreEvents.emit feedback call count + mockCoreEvents.emit.mockClear(); + + // Submit a prompt while initializing + await act(async () => { + await capturedUIActions.handleFinalSubmit('hello'); + }); + + // Feedback should be emitted with "Initializing..." message + expect(mockCoreEvents.emit).toHaveBeenCalledWith( + 'feedback', + expect.objectContaining({ + type: 'info', + message: + 'Initializing... Slash commands are still available and prompts will be queued.', + }), + ); + + unmount!(); + }); + }); }); diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index 03e001546b..3e79b5cee0 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -1295,7 +1295,10 @@ Logging in with Google... Restarting Gemini CLI to continue. return; } - if (isSlash || (isIdle && isMcpReady)) { + if ( + isSlash || + (isIdle && isMcpReady && !isAuthenticating && isConfigInitialized) + ) { if (!isSlash) { const permissions = await checkPermissions(submittedValue, config); if (permissions.length > 0) { @@ -1318,11 +1321,17 @@ Logging in with Google... Restarting Gemini CLI to continue. void submitQuery(submittedValue); } else { // Check messageQueue.length === 0 to only notify on the first queued item - if (isIdle && !isMcpReady && messageQueue.length === 0) { - coreEvents.emitFeedback( - 'info', - 'Waiting for MCP servers to initialize... Slash commands are still available and prompts will be queued.', - ); + if ( + isIdle && + (!isMcpReady || isAuthenticating || !isConfigInitialized) && + messageQueue.length === 0 + ) { + const message = isAuthenticating + ? 'Authentication is still in progress... Slash commands are still available and prompts will be queued.' + : !isConfigInitialized + ? 'Initializing... Slash commands are still available and prompts will be queued.' + : 'Waiting for MCP servers to initialize... Slash commands are still available and prompts will be queued.'; + coreEvents.emitFeedback('info', message); } addMessage(submittedValue); } @@ -1345,6 +1354,8 @@ Logging in with Google... Restarting Gemini CLI to continue. reset, handleHintSubmit, triggerExpandHint, + isAuthenticating, + isConfigInitialized, ], ); @@ -1373,15 +1384,7 @@ Logging in with Google... Restarting Gemini CLI to continue. * - Tool confirmations (WaitingForConfirmation state) * - Any future streaming states not explicitly allowed */ - const isInputActive = - isConfigInitialized && - !initError && - !isProcessing && - !isResuming && - !!slashCommands && - (streamingState === StreamingState.Idle || - streamingState === StreamingState.Responding) && - !proQuotaRequest; + const isInputActive = !initError && !isProcessing && !proQuotaRequest; const [controlsHeight, setControlsHeight] = useState(0); @@ -2004,7 +2007,6 @@ Logging in with Google... Restarting Gemini CLI to continue. isModelDialogOpen || isAgentConfigDialogOpen || isPermissionsDialogOpen || - isAuthenticating || isAuthDialogOpen || isEditorDialogOpen || showPrivacyNotice || @@ -2170,6 +2172,7 @@ Logging in with Google... Restarting Gemini CLI to continue. themeError, isAuthenticating, isConfigInitialized, + isMcpReady, authError, accountSuspensionInfo, isAuthDialogOpen, @@ -2300,6 +2303,7 @@ Logging in with Google... Restarting Gemini CLI to continue. themeError, isAuthenticating, isConfigInitialized, + isMcpReady, authError, accountSuspensionInfo, isAuthDialogOpen, diff --git a/packages/cli/src/ui/auth/AuthInProgress.test.tsx b/packages/cli/src/ui/auth/AuthInProgress.test.tsx deleted file mode 100644 index bd6a3cb126..0000000000 --- a/packages/cli/src/ui/auth/AuthInProgress.test.tsx +++ /dev/null @@ -1,133 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; -import { render } from '../../test-utils/render.js'; -import { act } from 'react'; -import { AuthInProgress } from './AuthInProgress.js'; -import { useKeypress, type Key } from '../hooks/useKeypress.js'; -import { debugLogger } from '@google/gemini-cli-core'; - -// Mock dependencies -vi.mock('@google/gemini-cli-core', async (importOriginal) => { - const actual = - await importOriginal(); - return { - ...actual, - debugLogger: { - log: vi.fn(), - warn: vi.fn(), - error: vi.fn(), - debug: vi.fn(), - }, - }; -}); - -vi.mock('../hooks/useKeypress.js', () => ({ - useKeypress: vi.fn(), -})); - -vi.mock('../components/CliSpinner.js', () => ({ - CliSpinner: () => '[Spinner]', -})); - -describe('AuthInProgress', () => { - const onTimeout = vi.fn(); - - beforeEach(() => { - vi.clearAllMocks(); - vi.useFakeTimers(); - vi.mocked(debugLogger.error).mockImplementation((...args) => { - if ( - typeof args[0] === 'string' && - args[0].includes('was not wrapped in act') - ) { - return; - } - }); - }); - - afterEach(() => { - vi.useRealTimers(); - }); - - it('renders initial state with spinner', async () => { - const { lastFrame, waitUntilReady, unmount } = render( - , - ); - await waitUntilReady(); - expect(lastFrame()).toContain('[Spinner] Waiting for authentication...'); - expect(lastFrame()).toContain('Press Esc or Ctrl+C to cancel'); - unmount(); - }); - - it('calls onTimeout when ESC is pressed', async () => { - const { waitUntilReady, unmount } = render( - , - ); - await waitUntilReady(); - const keypressHandler = vi.mocked(useKeypress).mock.calls[0][0]; - - await act(async () => { - keypressHandler({ name: 'escape' } as unknown as Key); - }); - // Escape key has a 50ms timeout in KeypressContext, so we need to wrap waitUntilReady in act - await act(async () => { - await waitUntilReady(); - }); - - expect(onTimeout).toHaveBeenCalled(); - unmount(); - }); - - it('calls onTimeout when Ctrl+C is pressed', async () => { - const { waitUntilReady, unmount } = render( - , - ); - await waitUntilReady(); - const keypressHandler = vi.mocked(useKeypress).mock.calls[0][0]; - - await act(async () => { - keypressHandler({ name: 'c', ctrl: true } as unknown as Key); - }); - await waitUntilReady(); - - expect(onTimeout).toHaveBeenCalled(); - unmount(); - }); - - it('calls onTimeout and shows timeout message after 3 minutes', async () => { - const { lastFrame, waitUntilReady, unmount } = render( - , - ); - await waitUntilReady(); - - await act(async () => { - vi.advanceTimersByTime(180000); - }); - await waitUntilReady(); - - expect(onTimeout).toHaveBeenCalled(); - expect(lastFrame()).toContain('Authentication timed out'); - unmount(); - }); - - it('clears timer on unmount', async () => { - const { waitUntilReady, unmount } = render( - , - ); - await waitUntilReady(); - - await act(async () => { - unmount(); - }); - - await act(async () => { - vi.advanceTimersByTime(180000); - }); - expect(onTimeout).not.toHaveBeenCalled(); - }); -}); diff --git a/packages/cli/src/ui/auth/AuthInProgress.tsx b/packages/cli/src/ui/auth/AuthInProgress.tsx deleted file mode 100644 index 03d609c444..0000000000 --- a/packages/cli/src/ui/auth/AuthInProgress.tsx +++ /dev/null @@ -1,63 +0,0 @@ -/** - * @license - * Copyright 2025 Google LLC - * SPDX-License-Identifier: Apache-2.0 - */ - -import type React from 'react'; -import { useState, useEffect } from 'react'; -import { Box, Text } from 'ink'; -import { CliSpinner } from '../components/CliSpinner.js'; -import { theme } from '../semantic-colors.js'; -import { useKeypress } from '../hooks/useKeypress.js'; - -interface AuthInProgressProps { - onTimeout: () => void; -} - -export function AuthInProgress({ - onTimeout, -}: AuthInProgressProps): React.JSX.Element { - const [timedOut, setTimedOut] = useState(false); - - useKeypress( - (key) => { - if (key.name === 'escape' || (key.ctrl && key.name === 'c')) { - onTimeout(); - } - }, - { isActive: true }, - ); - - useEffect(() => { - const timer = setTimeout(() => { - setTimedOut(true); - onTimeout(); - }, 180000); - - return () => clearTimeout(timer); - }, [onTimeout]); - - return ( - - {timedOut ? ( - - Authentication timed out. Please try again. - - ) : ( - - - Waiting for authentication... (Press Esc - or Ctrl+C to cancel) - - - )} - - ); -} diff --git a/packages/cli/src/ui/components/Composer.tsx b/packages/cli/src/ui/components/Composer.tsx index 0864b8f02b..d2bd1bc775 100644 --- a/packages/cli/src/ui/components/Composer.tsx +++ b/packages/cli/src/ui/components/Composer.tsx @@ -25,6 +25,7 @@ import { Footer } from './Footer.js'; import { ShowMoreLines } from './ShowMoreLines.js'; import { QueuedMessageDisplay } from './QueuedMessageDisplay.js'; import { ContextUsageDisplay } from './ContextUsageDisplay.js'; +import { CliSpinner } from './CliSpinner.js'; import { HorizontalLine } from './shared/HorizontalLine.js'; import { OverflowProvider } from '../contexts/OverflowContext.js'; import { isNarrowWidth } from '../utils/isNarrowWidth.js'; @@ -35,7 +36,6 @@ import { useConfig } from '../contexts/ConfigContext.js'; import { useSettings } from '../contexts/SettingsContext.js'; import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js'; import { StreamingState, type HistoryItemToolGroup } from '../types.js'; -import { ConfigInitDisplay } from '../components/ConfigInitDisplay.js'; import { TodoTray } from './messages/Todo.js'; import { getInlineThinkingMode } from '../utils/inlineThinkingMode.js'; import { isContextUsageHigh } from '../utils/contextUsage.js'; @@ -196,16 +196,20 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => { flexGrow={0} flexShrink={0} > - {(!uiState.slashCommands || - !uiState.isConfigInitialized || - uiState.isResuming) && ( - - )} - {showUiDetails && ( - + + + {(!uiState.isConfigInitialized || + uiState.isAuthenticating || + uiState.isResuming) && ( + + + {' '} + {uiState.isResuming ? 'Resuming session...' : 'Initializing...'} + + + )} + )} {showUiDetails && } diff --git a/packages/cli/src/ui/components/DialogManager.test.tsx b/packages/cli/src/ui/components/DialogManager.test.tsx index 6329ca89a1..f65308b15d 100644 --- a/packages/cli/src/ui/components/DialogManager.test.tsx +++ b/packages/cli/src/ui/components/DialogManager.test.tsx @@ -31,9 +31,6 @@ vi.mock('./ThemeDialog.js', () => ({ vi.mock('./SettingsDialog.js', () => ({ SettingsDialog: () => SettingsDialog, })); -vi.mock('../auth/AuthInProgress.js', () => ({ - AuthInProgress: () => AuthInProgress, -})); vi.mock('../auth/AuthDialog.js', () => ({ AuthDialog: () => AuthDialog, })); @@ -169,7 +166,6 @@ describe('DialogManager', () => { [{ isThemeDialogOpen: true }, 'ThemeDialog'], [{ isSettingsDialogOpen: true }, 'SettingsDialog'], [{ isModelDialogOpen: true }, 'ModelDialog'], - [{ isAuthenticating: true }, 'AuthInProgress'], [{ isAwaitingApiKeyInput: true }, 'ApiAuthDialog'], [{ isAuthDialogOpen: true }, 'AuthDialog'], [{ isEditorDialogOpen: true }, 'EditorSettingsDialog'], diff --git a/packages/cli/src/ui/components/DialogManager.tsx b/packages/cli/src/ui/components/DialogManager.tsx index de62401e1e..be0d16718b 100644 --- a/packages/cli/src/ui/components/DialogManager.tsx +++ b/packages/cli/src/ui/components/DialogManager.tsx @@ -11,9 +11,8 @@ import { FolderTrustDialog } from './FolderTrustDialog.js'; import { ConsentPrompt } from './ConsentPrompt.js'; import { ThemeDialog } from './ThemeDialog.js'; import { SettingsDialog } from './SettingsDialog.js'; -import { AuthInProgress } from '../auth/AuthInProgress.js'; -import { AuthDialog } from '../auth/AuthDialog.js'; import { BannedAccountDialog } from '../auth/BannedAccountDialog.js'; +import { AuthDialog } from '../auth/AuthDialog.js'; import { ApiAuthDialog } from '../auth/ApiAuthDialog.js'; import { EditorSettingsDialog } from './EditorSettingsDialog.js'; import { PrivacyNotice } from '../privacy/PrivacyNotice.js'; @@ -280,15 +279,6 @@ export const DialogManager = ({ ); } - if (uiState.isAuthenticating) { - return ( - { - uiActions.onAuthError('Authentication cancelled.'); - }} - /> - ); - } if (uiState.isAwaitingApiKeyInput) { return ( diff --git a/packages/cli/src/ui/components/InputPrompt.tsx b/packages/cli/src/ui/components/InputPrompt.tsx index 94b1d2dc00..7778a36850 100644 --- a/packages/cli/src/ui/components/InputPrompt.tsx +++ b/packages/cli/src/ui/components/InputPrompt.tsx @@ -62,7 +62,6 @@ import * as path from 'node:path'; import { SCREEN_READER_USER_PREFIX } from '../textConstants.js'; import { getSafeLowColorBackground } from '../themes/color-utils.js'; import { isLowColorDepth } from '../utils/terminalUtils.js'; -import { useShellFocusState } from '../contexts/ShellFocusContext.js'; import { useUIState } from '../contexts/UIStateContext.js'; import { appEvents, @@ -216,7 +215,6 @@ export const InputPrompt: React.FC = ({ const { stdout } = useStdout(); const { merged: settings } = useSettings(); const kittyProtocol = useKittyKeyboardProtocol(); - const isShellFocused = useShellFocusState(); const { setEmbeddedShellFocused, setShortcutsHelpVisible, @@ -329,7 +327,7 @@ export const InputPrompt: React.FC = ({ isShellSuggestionsVisible, } = completion; - const showCursor = focus && isShellFocused && !isEmbeddedShellFocused; + const showCursor = focus && !isEmbeddedShellFocused; // Notify parent component about escape prompt state changes useEffect(() => { @@ -1277,7 +1275,7 @@ export const InputPrompt: React.FC = ({ ); useKeypress(handleInput, { - isActive: !isEmbeddedShellFocused, + isActive: focus && !isEmbeddedShellFocused, priority: true, }); @@ -1470,7 +1468,7 @@ export const InputPrompt: React.FC = ({ ) : null; const borderColor = - isShellFocused && !isEmbeddedShellFocused + focus && !isEmbeddedShellFocused ? (statusColor ?? theme.ui.focus) : theme.border.default; diff --git a/packages/cli/src/ui/contexts/UIStateContext.tsx b/packages/cli/src/ui/contexts/UIStateContext.tsx index ea9025aa6b..c18211a0de 100644 --- a/packages/cli/src/ui/contexts/UIStateContext.tsx +++ b/packages/cli/src/ui/contexts/UIStateContext.tsx @@ -110,6 +110,7 @@ export interface UIState { themeError: string | null; isAuthenticating: boolean; isConfigInitialized: boolean; + isMcpReady: boolean; authError: string | null; accountSuspensionInfo: AccountSuspensionInfo | null; isAuthDialogOpen: boolean; diff --git a/packages/cli/src/ui/hooks/useMessageQueue.test.tsx b/packages/cli/src/ui/hooks/useMessageQueue.test.tsx index 5b05d2a9f1..db2334c5f0 100644 --- a/packages/cli/src/ui/hooks/useMessageQueue.test.tsx +++ b/packages/cli/src/ui/hooks/useMessageQueue.test.tsx @@ -29,6 +29,7 @@ describe('useMessageQueue', () => { streamingState: StreamingState; submitQuery: (query: string) => void; isMcpReady: boolean; + isAuthenticating?: boolean; }) => { let hookResult: ReturnType; function TestComponent(props: typeof initialProps) { @@ -264,6 +265,56 @@ describe('useMessageQueue', () => { }); }); + it('should not submit queued messages if isAuthenticating is true', () => { + const { result, rerender } = renderMessageQueueHook({ + isConfigInitialized: true, + streamingState: StreamingState.Responding, + submitQuery: mockSubmitQuery, + isMcpReady: true, + isAuthenticating: true, + }); + + act(() => { + result.current.addMessage('Message 1'); + }); + + rerender({ + isConfigInitialized: true, + streamingState: StreamingState.Idle, + submitQuery: mockSubmitQuery, + isMcpReady: true, + isAuthenticating: true, + }); + + expect(mockSubmitQuery).not.toHaveBeenCalled(); + expect(result.current.messageQueue).toEqual(['Message 1']); + }); + + it('should submit queued messages when isAuthenticating becomes false', () => { + const { result, rerender } = renderMessageQueueHook({ + isConfigInitialized: true, + streamingState: StreamingState.Idle, + submitQuery: mockSubmitQuery, + isMcpReady: true, + isAuthenticating: true, + }); + + act(() => { + result.current.addMessage('Message 1'); + }); + + rerender({ + isConfigInitialized: true, + streamingState: StreamingState.Idle, + submitQuery: mockSubmitQuery, + isMcpReady: true, + isAuthenticating: false, + }); + + expect(mockSubmitQuery).toHaveBeenCalledWith('Message 1'); + expect(result.current.messageQueue).toEqual([]); + }); + describe('popAllMessages', () => { it('should pop all messages and return them joined with double newlines', () => { const { result } = renderMessageQueueHook({ diff --git a/packages/cli/src/ui/hooks/useMessageQueue.ts b/packages/cli/src/ui/hooks/useMessageQueue.ts index 93bb0ab7a9..deeffeff40 100644 --- a/packages/cli/src/ui/hooks/useMessageQueue.ts +++ b/packages/cli/src/ui/hooks/useMessageQueue.ts @@ -12,6 +12,7 @@ export interface UseMessageQueueOptions { streamingState: StreamingState; submitQuery: (query: string) => void; isMcpReady: boolean; + isAuthenticating?: boolean; } export interface UseMessageQueueReturn { @@ -32,6 +33,7 @@ export function useMessageQueue({ streamingState, submitQuery, isMcpReady, + isAuthenticating = false, }: UseMessageQueueOptions): UseMessageQueueReturn { const [messageQueue, setMessageQueue] = useState([]); @@ -70,6 +72,7 @@ export function useMessageQueue({ isConfigInitialized && streamingState === StreamingState.Idle && isMcpReady && + !isAuthenticating && messageQueue.length > 0 ) { // Combine all messages with double newlines for clarity @@ -82,6 +85,7 @@ export function useMessageQueue({ isConfigInitialized, streamingState, isMcpReady, + isAuthenticating, messageQueue, submitQuery, ]);