Refactor: Improve console error/log display in CLI (#486)

This commit is contained in:
Jacob Richman 2025-05-22 10:36:44 -07:00 committed by GitHub
parent fb1d13d600
commit 7eaf850489
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 218 additions and 88 deletions

View File

@ -5,8 +5,21 @@
*/ */
import { useCallback, useEffect, useMemo, useState, useRef } from 'react'; import { useCallback, useEffect, useMemo, useState, useRef } from 'react';
import { Box, DOMElement, measureElement, Static, Text } from 'ink'; import {
import { StreamingState, type HistoryItem } from './types.js'; Box,
DOMElement,
measureElement,
Static,
Text,
useInput,
type Key as InkKeyType,
} from 'ink';
import {
StreamingState,
type HistoryItem,
ConsoleMessageItem,
MessageType,
} from './types.js';
import { useTerminalSize } from './hooks/useTerminalSize.js'; import { useTerminalSize } from './hooks/useTerminalSize.js';
import { useGeminiStream } from './hooks/useGeminiStream.js'; import { useGeminiStream } from './hooks/useGeminiStream.js';
import { useLoadingIndicator } from './hooks/useLoadingIndicator.js'; import { useLoadingIndicator } from './hooks/useLoadingIndicator.js';
@ -25,11 +38,11 @@ import { Help } from './components/Help.js';
import { loadHierarchicalGeminiMemory } from '../config/config.js'; import { loadHierarchicalGeminiMemory } from '../config/config.js';
import { LoadedSettings } from '../config/settings.js'; import { LoadedSettings } from '../config/settings.js';
import { Tips } from './components/Tips.js'; import { Tips } from './components/Tips.js';
import { ConsoleOutput } from './components/ConsolePatcher.js'; import { useConsolePatcher } from './components/ConsolePatcher.js';
import { DetailedMessagesDisplay } from './components/DetailedMessagesDisplay.js';
import { HistoryItemDisplay } from './components/HistoryItemDisplay.js'; import { HistoryItemDisplay } from './components/HistoryItemDisplay.js';
import { useHistory } from './hooks/useHistoryManager.js'; import { useHistory } from './hooks/useHistoryManager.js';
import process from 'node:process'; import process from 'node:process';
import { MessageType } from './types.js';
import { getErrorMessage, type Config } from '@gemini-code/server'; import { getErrorMessage, type Config } from '@gemini-code/server';
import { useLogger } from './hooks/useLogger.js'; import { useLogger } from './hooks/useLogger.js';
@ -61,6 +74,32 @@ export const App = ({
const [corgiMode, setCorgiMode] = useState(false); const [corgiMode, setCorgiMode] = useState(false);
const [shellModeActive, setShellModeActive] = useState(false); const [shellModeActive, setShellModeActive] = useState(false);
const [consoleMessages, setConsoleMessages] = useState<ConsoleMessageItem[]>(
[],
);
const [showErrorDetails, setShowErrorDetails] = useState<boolean>(false);
const errorCount = useMemo(
() => consoleMessages.filter((msg) => msg.type === 'error').length,
[consoleMessages],
);
useInput((input: string, key: InkKeyType) => {
// Check for Ctrl+D key press
if (key.ctrl && (input === 'd' || input === 'D')) {
setShowErrorDetails((prev) => !prev);
refreshStatic();
}
});
const handleNewMessage = useCallback((message: ConsoleMessageItem) => {
setConsoleMessages((prevMessages) => [...prevMessages, message]);
}, []);
useConsolePatcher({
onNewMessage: handleNewMessage,
debugMode: config.getDebugMode(),
});
const toggleCorgiMode = useCallback(() => { const toggleCorgiMode = useCallback(() => {
setCorgiMode((prev) => !prev); setCorgiMode((prev) => !prev);
}, []); }, []);
@ -72,7 +111,6 @@ export const App = ({
handleThemeHighlight, handleThemeHighlight,
} = useThemeCommand(settings, setThemeError); } = useThemeCommand(settings, setThemeError);
// useEffect to initialize geminiMdFileCount from config when config is ready
useEffect(() => { useEffect(() => {
if (config) { if (config) {
setGeminiMdFileCount(config.getGeminiMdFileCount()); setGeminiMdFileCount(config.getGeminiMdFileCount());
@ -186,12 +224,11 @@ export const App = ({
const handleClearScreen = useCallback(() => { const handleClearScreen = useCallback(() => {
clearItems(); clearItems();
setConsoleMessages([]);
console.clear(); console.clear();
refreshStatic(); refreshStatic();
}, [clearItems, refreshStatic]); }, [clearItems, refreshStatic]);
// --- Render Logic ---
const { rows: terminalHeight } = useTerminalSize(); const { rows: terminalHeight } = useTerminalSize();
const mainControlsRef = useRef<DOMElement>(null); const mainControlsRef = useRef<DOMElement>(null);
const pendingHistoryItemRef = useRef<DOMElement>(null); const pendingHistoryItemRef = useRef<DOMElement>(null);
@ -201,7 +238,7 @@ export const App = ({
const fullFooterMeasurement = measureElement(mainControlsRef.current); const fullFooterMeasurement = measureElement(mainControlsRef.current);
setFooterHeight(fullFooterMeasurement.height); setFooterHeight(fullFooterMeasurement.height);
} }
}, [terminalHeight]); }, [terminalHeight, consoleMessages, showErrorDetails]);
const availableTerminalHeight = useMemo(() => { const availableTerminalHeight = useMemo(() => {
const staticExtraHeight = /* margins and padding */ 3; const staticExtraHeight = /* margins and padding */ 3;
@ -232,6 +269,13 @@ export const App = ({
} }
}, [streamingState, refreshStatic, staticNeedsRefresh]); }, [streamingState, refreshStatic, staticNeedsRefresh]);
const filteredConsoleMessages = useMemo(() => {
if (config.getDebugMode()) {
return consoleMessages;
}
return consoleMessages.filter((msg) => msg.type !== 'debug');
}, [consoleMessages, config]);
return ( return (
<Box flexDirection="column" marginBottom={1} width="90%"> <Box flexDirection="column" marginBottom={1} width="90%">
{/* {/*
@ -339,6 +383,11 @@ export const App = ({
{shellModeActive && <ShellModeIndicator />} {shellModeActive && <ShellModeIndicator />}
</Box> </Box>
</Box> </Box>
{showErrorDetails && (
<DetailedMessagesDisplay messages={filteredConsoleMessages} />
)}
{isInputActive && ( {isInputActive && (
<InputPrompt <InputPrompt
widthFraction={0.9} widthFraction={0.9}
@ -392,8 +441,9 @@ export const App = ({
debugMessage={debugMessage} debugMessage={debugMessage}
cliVersion={cliVersion} cliVersion={cliVersion}
corgiMode={corgiMode} corgiMode={corgiMode}
errorCount={errorCount}
showErrorDetails={showErrorDetails}
/> />
<ConsoleOutput debugMode={config.getDebugMode()} />
</Box> </Box>
</Box> </Box>
); );

View File

@ -4,27 +4,19 @@
* SPDX-License-Identifier: Apache-2.0 * SPDX-License-Identifier: Apache-2.0
*/ */
import React, { useState, useEffect, Key } from 'react'; import { useEffect } from 'react';
import { Box, Text } from 'ink';
import util from 'util'; import util from 'util';
import { ConsoleMessageItem } from '../types.js';
interface ConsoleMessage { interface UseConsolePatcherParams {
id: Key; onNewMessage: (message: Omit<ConsoleMessageItem, 'id'>) => void;
type: 'log' | 'warn' | 'error' | 'debug';
content: string;
}
// Using a module-level counter for unique IDs.
// This ensures IDs are unique across messages.
let messageIdCounter = 0;
interface ConsoleOutputProps {
debugMode: boolean; debugMode: boolean;
} }
export const ConsoleOutput: React.FC<ConsoleOutputProps> = ({ debugMode }) => { export const useConsolePatcher = ({
const [messages, setMessages] = useState<ConsoleMessage[]>([]); onNewMessage,
debugMode,
}: UseConsolePatcherParams): void => {
useEffect(() => { useEffect(() => {
const originalConsoleLog = console.log; const originalConsoleLog = console.log;
const originalConsoleWarn = console.warn; const originalConsoleWarn = console.warn;
@ -32,25 +24,30 @@ export const ConsoleOutput: React.FC<ConsoleOutputProps> = ({ debugMode }) => {
const originalConsoleDebug = console.debug; const originalConsoleDebug = console.debug;
const formatArgs = (args: unknown[]): string => util.format(...args); const formatArgs = (args: unknown[]): string => util.format(...args);
const addMessage = (
type: 'log' | 'warn' | 'error' | 'debug',
args: unknown[],
) => {
setMessages((prevMessages) => [
...prevMessages,
{
id: `console-msg-${messageIdCounter++}`,
type,
content: formatArgs(args),
},
]);
};
// It's patching time const patchConsoleMethod =
console.log = (...args: unknown[]) => addMessage('log', args); (
console.warn = (...args: unknown[]) => addMessage('warn', args); type: 'log' | 'warn' | 'error' | 'debug',
console.error = (...args: unknown[]) => addMessage('error', args); originalMethod: (...args: unknown[]) => void,
console.debug = (...args: unknown[]) => addMessage('debug', args); ) =>
(...args: unknown[]) => {
if (debugMode) {
originalMethod.apply(console, args);
}
// Then, if it's not a debug message or debugMode is on, pass to onNewMessage
if (type !== 'debug' || debugMode) {
onNewMessage({
type,
content: formatArgs(args),
});
}
};
console.log = patchConsoleMethod('log', originalConsoleLog);
console.warn = patchConsoleMethod('warn', originalConsoleWarn);
console.error = patchConsoleMethod('error', originalConsoleError);
console.debug = patchConsoleMethod('debug', originalConsoleDebug);
return () => { return () => {
console.log = originalConsoleLog; console.log = originalConsoleLog;
@ -58,46 +55,5 @@ export const ConsoleOutput: React.FC<ConsoleOutputProps> = ({ debugMode }) => {
console.error = originalConsoleError; console.error = originalConsoleError;
console.debug = originalConsoleDebug; console.debug = originalConsoleDebug;
}; };
}, []); }, [onNewMessage, debugMode]);
return (
<Box flexDirection="column">
{messages.map((msg) => {
if (msg.type === 'debug' && !debugMode) {
return null;
}
const textProps: { color?: string } = {};
let prefix = '';
switch (msg.type) {
case 'warn':
textProps.color = 'yellow';
prefix = 'WARN: ';
break;
case 'error':
textProps.color = 'red';
prefix = 'ERROR: ';
break;
case 'debug':
textProps.color = 'gray';
prefix = 'DEBUG: ';
break;
case 'log':
default:
prefix = 'LOG: ';
break;
}
return (
<Box key={msg.id}>
<Text {...textProps}>
{prefix}
{msg.content}
</Text>
</Box>
);
})}
</Box>
);
}; };

View File

@ -0,0 +1,35 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import React from 'react';
import { Box, Text } from 'ink';
import { Colors } from '../colors.js';
interface ConsoleSummaryDisplayProps {
errorCount: number;
// logCount is not currently in the plan to be displayed in summary
}
export const ConsoleSummaryDisplay: React.FC<ConsoleSummaryDisplayProps> = ({
errorCount,
}) => {
if (errorCount === 0) {
return null;
}
const errorIcon = '\u2716'; // Heavy multiplication x (✖)
return (
<Box>
{errorCount > 0 && (
<Text color={Colors.AccentRed}>
{errorIcon} {errorCount} error{errorCount > 1 ? 's' : ''}{' '}
<Text color={Colors.SubtleComment}>(CTRL-D for details)</Text>
</Text>
)}
</Box>
);
};

View File

@ -0,0 +1,73 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import React from 'react';
import { Box, Text } from 'ink';
import { Colors } from '../colors.js';
import { ConsoleMessageItem } from '../types.js';
interface DetailedMessagesDisplayProps {
messages: ConsoleMessageItem[];
// debugMode is not needed here if App.tsx filters debug messages before passing them.
// If DetailedMessagesDisplay should handle filtering, add debugMode prop.
}
export const DetailedMessagesDisplay: React.FC<
DetailedMessagesDisplayProps
> = ({ messages }) => {
if (messages.length === 0) {
return null; // Don't render anything if there are no messages
}
return (
<Box
flexDirection="column"
marginTop={1}
borderStyle="round"
borderColor={Colors.SubtleComment}
paddingX={1}
>
<Box marginBottom={1}>
<Text bold color={Colors.Foreground}>
Debug Console{' '}
<Text color={Colors.SubtleComment}>(CTRL-D to close)</Text>
</Text>
</Box>
{messages.map((msg, index) => {
let textColor = Colors.Foreground;
let icon = '\u2139'; // Information source ()
switch (msg.type) {
case 'warn':
textColor = Colors.AccentYellow;
icon = '\u26A0'; // Warning sign (⚠)
break;
case 'error':
textColor = Colors.AccentRed;
icon = '\u2716'; // Heavy multiplication x (✖)
break;
case 'debug':
textColor = Colors.SubtleComment; // Or Colors.Gray
icon = '\u1F50D'; // Left-pointing magnifying glass (????)
break;
case 'log':
default:
// Default textColor and icon are already set
break;
}
return (
<Box key={index} flexDirection="row">
<Text color={textColor}>{icon} </Text>
<Text color={textColor} wrap="wrap">
{msg.content}
</Text>
</Box>
);
})}
</Box>
);
};

View File

@ -8,6 +8,7 @@ import React from 'react';
import { Box, Text } from 'ink'; import { Box, Text } from 'ink';
import { Colors } from '../colors.js'; import { Colors } from '../colors.js';
import { shortenPath, tildeifyPath, Config } from '@gemini-code/server'; import { shortenPath, tildeifyPath, Config } from '@gemini-code/server';
import { ConsoleSummaryDisplay } from './ConsoleSummaryDisplay.js';
interface FooterProps { interface FooterProps {
config: Config; config: Config;
@ -15,6 +16,8 @@ interface FooterProps {
debugMessage: string; debugMessage: string;
cliVersion: string; cliVersion: string;
corgiMode: boolean; corgiMode: boolean;
errorCount: number;
showErrorDetails: boolean;
} }
export const Footer: React.FC<FooterProps> = ({ export const Footer: React.FC<FooterProps> = ({
@ -23,8 +26,10 @@ export const Footer: React.FC<FooterProps> = ({
debugMessage, debugMessage,
cliVersion, cliVersion,
corgiMode, corgiMode,
errorCount,
showErrorDetails,
}) => ( }) => (
<Box marginTop={1}> <Box marginTop={1} justifyContent="space-between" width="100%">
<Box> <Box>
<Text color={Colors.LightBlue}> <Text color={Colors.LightBlue}>
{shortenPath(tildeifyPath(config.getTargetDir()), 70)} {shortenPath(tildeifyPath(config.getTargetDir()), 70)}
@ -56,8 +61,8 @@ export const Footer: React.FC<FooterProps> = ({
)} )}
</Box> </Box>
{/* Right Section: Gemini Label */} {/* Right Section: Gemini Label and Console Summary */}
<Box> <Box alignItems="center">
<Text color={Colors.AccentBlue}> {config.getModel()} </Text> <Text color={Colors.AccentBlue}> {config.getModel()} </Text>
<Text color={Colors.SubtleComment}>| CLI {cliVersion} </Text> <Text color={Colors.SubtleComment}>| CLI {cliVersion} </Text>
{corgiMode && ( {corgiMode && (
@ -70,6 +75,12 @@ export const Footer: React.FC<FooterProps> = ({
<Text color={Colors.AccentRed}> </Text> <Text color={Colors.AccentRed}> </Text>
</Text> </Text>
)} )}
{!showErrorDetails && errorCount > 0 && (
<Box>
<Text color={Colors.SubtleComment}>| </Text>
<ConsoleSummaryDisplay errorCount={errorCount} />
</Box>
)}
</Box> </Box>
</Box> </Box>
); );

View File

@ -119,3 +119,8 @@ export interface Message {
content: string; // Renamed from text for clarity in this context content: string; // Renamed from text for clarity in this context
timestamp: Date; // For consistency, though addItem might use its own timestamping timestamp: Date; // For consistency, though addItem might use its own timestamping
} }
export interface ConsoleMessageItem {
type: 'log' | 'warn' | 'error' | 'debug';
content: string;
}