diff --git a/packages/cli/src/ui/AppContainer.test.tsx b/packages/cli/src/ui/AppContainer.test.tsx index 3e420f141d..650804025b 100644 --- a/packages/cli/src/ui/AppContainer.test.tsx +++ b/packages/cli/src/ui/AppContainer.test.tsx @@ -212,7 +212,7 @@ import { useEditorSettings } from './hooks/useEditorSettings.js'; import { useSettingsCommand } from './hooks/useSettingsCommand.js'; import { useModelCommand } from './hooks/useModelCommand.js'; import { useSlashCommandProcessor } from './hooks/slashCommandProcessor.js'; -import { useConsoleMessages } from './hooks/useConsoleMessages.js'; +import { useErrorCount } from './hooks/useConsoleMessages.js'; import { useGeminiStream } from './hooks/useGeminiStream.js'; import { useVim } from './hooks/vim.js'; import { useFolderTrust } from './hooks/useFolderTrust.js'; @@ -294,7 +294,7 @@ describe('AppContainer State Management', () => { const mockedUseSettingsCommand = useSettingsCommand as Mock; const mockedUseModelCommand = useModelCommand as Mock; const mockedUseSlashCommandProcessor = useSlashCommandProcessor as Mock; - const mockedUseConsoleMessages = useConsoleMessages as Mock; + const mockedUseConsoleMessages = useErrorCount as Mock; const mockedUseGeminiStream = useGeminiStream as Mock; const mockedUseVim = useVim as Mock; const mockedUseFolderTrust = useFolderTrust as Mock; @@ -396,9 +396,9 @@ describe('AppContainer State Management', () => { confirmationRequest: null, }); mockedUseConsoleMessages.mockReturnValue({ - consoleMessages: [], + errorCount: 0, handleNewMessage: vi.fn(), - clearConsoleMessages: vi.fn(), + clearErrorCount: vi.fn(), }); mockedUseGeminiStream.mockReturnValue(DEFAULT_GEMINI_STREAM_MOCK); mockedUseVim.mockReturnValue({ handleInput: vi.fn() }); diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index b2402f9fe9..07edb72642 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -103,7 +103,7 @@ import { useOverflowActions, useOverflowState, } from './contexts/OverflowContext.js'; -import { useConsoleMessages } from './hooks/useConsoleMessages.js'; +import { useErrorCount } from './hooks/useConsoleMessages.js'; import { useTerminalSize } from './hooks/useTerminalSize.js'; import { calculatePromptWidths } from './components/InputPrompt.js'; import { calculateMainAreaWidth } from './utils/ui-sizing.js'; @@ -552,8 +552,7 @@ export const AppContainer = (props: AppContainerProps) => { }; }, [settings]); - const { consoleMessages, clearConsoleMessages: clearConsoleMessagesState } = - useConsoleMessages(); + const { errorCount, clearErrorCount } = useErrorCount(); const mainAreaWidth = calculateMainAreaWidth(terminalWidth, config); // Derive widths for InputPrompt using shared helper @@ -1372,11 +1371,11 @@ Logging in with Google... Restarting Gemini CLI to continue. // Explicitly hide the expansion hint and clear its x-second timer when clearing the screen. triggerExpandHint(null); historyManager.clearItems(); - clearConsoleMessagesState(); + clearErrorCount(); refreshStatic(); }, [ historyManager, - clearConsoleMessagesState, + clearErrorCount, refreshStatic, reset, triggerExpandHint, @@ -1983,22 +1982,6 @@ Logging in with Google... Restarting Gemini CLI to continue. }; }, [historyManager]); - const filteredConsoleMessages = useMemo(() => { - if (config.getDebugMode()) { - return consoleMessages; - } - return consoleMessages.filter((msg) => msg.type !== 'debug'); - }, [consoleMessages, config]); - - // Computed values - const errorCount = useMemo( - () => - filteredConsoleMessages - .filter((msg) => msg.type === 'error') - .reduce((total, msg) => total + msg.count, 0), - [filteredConsoleMessages], - ); - const nightly = props.version.includes('nightly'); const dialogsVisible = @@ -2233,7 +2216,6 @@ Logging in with Google... Restarting Gemini CLI to continue. constrainHeight, showErrorDetails, showFullTodos, - filteredConsoleMessages, ideContextState, renderMarkdown, ctrlCPressedOnce: ctrlCPressCount >= 1, @@ -2361,7 +2343,6 @@ Logging in with Google... Restarting Gemini CLI to continue. constrainHeight, showErrorDetails, showFullTodos, - filteredConsoleMessages, ideContextState, renderMarkdown, ctrlCPressCount, diff --git a/packages/cli/src/ui/components/Composer.test.tsx b/packages/cli/src/ui/components/Composer.test.tsx index e0919947fb..641fc24810 100644 --- a/packages/cli/src/ui/components/Composer.test.tsx +++ b/packages/cli/src/ui/components/Composer.test.tsx @@ -183,7 +183,6 @@ const createMockUIState = (overrides: Partial = {}): UIState => ideContextState: null, geminiMdFileCount: 0, renderMarkdown: true, - filteredConsoleMessages: [], history: [], sessionStats: { sessionId: 'test-session', @@ -757,13 +756,6 @@ describe('Composer', () => { it('shows DetailedMessagesDisplay when showErrorDetails is true', async () => { const uiState = createMockUIState({ showErrorDetails: true, - filteredConsoleMessages: [ - { - type: 'error', - content: 'Test error', - count: 1, - }, - ], }); const { lastFrame } = await renderComposer(uiState); diff --git a/packages/cli/src/ui/components/Composer.tsx b/packages/cli/src/ui/components/Composer.tsx index 0864b8f02b..89c9c9d3d6 100644 --- a/packages/cli/src/ui/components/Composer.tsx +++ b/packages/cli/src/ui/components/Composer.tsx @@ -422,7 +422,6 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => { ({ + useConsoleMessages: vi.fn(), +})); vi.mock('./shared/ScrollableList.js', () => ({ ScrollableList: ({ @@ -29,14 +34,15 @@ vi.mock('./shared/ScrollableList.js', () => ({ })); describe('DetailedMessagesDisplay', () => { + beforeEach(() => { + vi.mocked(useConsoleMessages).mockReturnValue({ + consoleMessages: [], + clearConsoleMessages: vi.fn(), + }); + }); it('renders nothing when messages are empty', async () => { const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( - , + , { settings: createMockSettings({ ui: { errorVerbosity: 'full' } }), }, @@ -53,14 +59,13 @@ describe('DetailedMessagesDisplay', () => { { type: 'error', content: 'Error message', count: 1 }, { type: 'debug', content: 'Debug message', count: 1 }, ]; + vi.mocked(useConsoleMessages).mockReturnValue({ + consoleMessages: messages, + clearConsoleMessages: vi.fn(), + }); const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( - , + , { settings: createMockSettings({ ui: { errorVerbosity: 'full' } }), }, @@ -76,14 +81,13 @@ describe('DetailedMessagesDisplay', () => { const messages: ConsoleMessageItem[] = [ { type: 'error', content: 'Error message', count: 1 }, ]; + vi.mocked(useConsoleMessages).mockReturnValue({ + consoleMessages: messages, + clearConsoleMessages: vi.fn(), + }); const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( - , + , { settings: createMockSettings({ ui: { errorVerbosity: 'low' } }), }, @@ -97,14 +101,13 @@ describe('DetailedMessagesDisplay', () => { const messages: ConsoleMessageItem[] = [ { type: 'error', content: 'Error message', count: 1 }, ]; + vi.mocked(useConsoleMessages).mockReturnValue({ + consoleMessages: messages, + clearConsoleMessages: vi.fn(), + }); const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( - , + , { settings: createMockSettings({ ui: { errorVerbosity: 'full' } }), }, @@ -118,14 +121,13 @@ describe('DetailedMessagesDisplay', () => { const messages: ConsoleMessageItem[] = [ { type: 'log', content: 'Repeated message', count: 5 }, ]; + vi.mocked(useConsoleMessages).mockReturnValue({ + consoleMessages: messages, + clearConsoleMessages: vi.fn(), + }); const { lastFrame, waitUntilReady, unmount } = await renderWithProviders( - , + , { settings: createMockSettings({ ui: { errorVerbosity: 'full' } }), }, diff --git a/packages/cli/src/ui/components/DetailedMessagesDisplay.tsx b/packages/cli/src/ui/components/DetailedMessagesDisplay.tsx index 13f3872e5d..2daa1c39e3 100644 --- a/packages/cli/src/ui/components/DetailedMessagesDisplay.tsx +++ b/packages/cli/src/ui/components/DetailedMessagesDisplay.tsx @@ -5,7 +5,7 @@ */ import type React from 'react'; -import { useRef, useCallback } from 'react'; +import { useRef, useCallback, useMemo } from 'react'; import { Box, Text } from 'ink'; import { theme } from '../semantic-colors.js'; import type { ConsoleMessageItem } from '../types.js'; @@ -13,9 +13,10 @@ import { ScrollableList, type ScrollableListRef, } from './shared/ScrollableList.js'; +import { useConsoleMessages } from '../hooks/useConsoleMessages.js'; +import { useConfig } from '../contexts/ConfigContext.js'; interface DetailedMessagesDisplayProps { - messages: ConsoleMessageItem[]; maxHeight: number | undefined; width: number; hasFocus: boolean; @@ -25,9 +26,19 @@ const iconBoxWidth = 3; export const DetailedMessagesDisplay: React.FC< DetailedMessagesDisplayProps -> = ({ messages, maxHeight, width, hasFocus }) => { +> = ({ maxHeight, width, hasFocus }) => { const scrollableListRef = useRef>(null); + const { consoleMessages } = useConsoleMessages(); + const config = useConfig(); + + const messages = useMemo(() => { + if (config.getDebugMode()) { + return consoleMessages; + } + return consoleMessages.filter((msg) => msg.type !== 'debug'); + }, [consoleMessages, config]); + const borderAndPadding = 3; const estimatedItemHeight = useCallback( diff --git a/packages/cli/src/ui/components/__snapshots__/DetailedMessagesDisplay.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/DetailedMessagesDisplay.test.tsx.snap index c720c994d4..a8454fd571 100644 --- a/packages/cli/src/ui/components/__snapshots__/DetailedMessagesDisplay.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/DetailedMessagesDisplay.test.tsx.snap @@ -23,7 +23,7 @@ exports[`DetailedMessagesDisplay > renders messages correctly 1`] = ` │ ℹ Log message │ │ ⚠ Warning message │ │ ✖ Error message │ -│ 🔍 Debug message │ +│ │ │ │ │ │ │ │ diff --git a/packages/cli/src/ui/contexts/UIStateContext.tsx b/packages/cli/src/ui/contexts/UIStateContext.tsx index ea9025aa6b..d393be8fe2 100644 --- a/packages/cli/src/ui/contexts/UIStateContext.tsx +++ b/packages/cli/src/ui/contexts/UIStateContext.tsx @@ -8,7 +8,6 @@ import { createContext, useContext } from 'react'; import type { HistoryItem, ThoughtSummary, - ConsoleMessageItem, ConfirmationRequest, QuotaStats, LoopDetectionConfirmationRequest, @@ -158,7 +157,6 @@ export interface UIState { isTrustedFolder: boolean | undefined; constrainHeight: boolean; showErrorDetails: boolean; - filteredConsoleMessages: ConsoleMessageItem[]; ideContextState: IdeContext | undefined; renderMarkdown: boolean; ctrlCPressedOnce: boolean; diff --git a/packages/cli/src/ui/hooks/useConsoleMessages.ts b/packages/cli/src/ui/hooks/useConsoleMessages.ts index 8dfa4814cd..da000a9da1 100644 --- a/packages/cli/src/ui/hooks/useConsoleMessages.ts +++ b/packages/cli/src/ui/hooks/useConsoleMessages.ts @@ -179,3 +179,47 @@ export function useConsoleMessages(): UseConsoleMessagesReturn { return { consoleMessages, clearConsoleMessages }; } + +export interface UseErrorCountReturn { + errorCount: number; + clearErrorCount: () => void; +} + +export function useErrorCount(): UseErrorCountReturn { + const [errorCount, dispatch] = useReducer( + (state: number, action: 'INCREMENT' | 'CLEAR') => { + switch (action) { + case 'INCREMENT': + return state + 1; + case 'CLEAR': + return 0; + default: + return state; + } + }, + 0, + ); + + useEffect(() => { + const handleConsoleLog = (payload: ConsoleLogPayload) => { + if (payload.type === 'error') { + startTransition(() => { + dispatch('INCREMENT'); + }); + } + }; + + coreEvents.on(CoreEvent.ConsoleLog, handleConsoleLog); + return () => { + coreEvents.off(CoreEvent.ConsoleLog, handleConsoleLog); + }; + }, []); + + const clearErrorCount = useCallback(() => { + startTransition(() => { + dispatch('CLEAR'); + }); + }, []); + + return { errorCount, clearErrorCount }; +}