summaryrefslogtreecommitdiff
path: root/packages/cli/src
diff options
context:
space:
mode:
Diffstat (limited to 'packages/cli/src')
-rw-r--r--packages/cli/src/ui/App.tsx68
-rw-r--r--packages/cli/src/ui/components/ConsolePatcher.tsx108
-rw-r--r--packages/cli/src/ui/components/ConsoleSummaryDisplay.tsx35
-rw-r--r--packages/cli/src/ui/components/DetailedMessagesDisplay.tsx73
-rw-r--r--packages/cli/src/ui/components/Footer.tsx17
-rw-r--r--packages/cli/src/ui/types.ts5
6 files changed, 218 insertions, 88 deletions
diff --git a/packages/cli/src/ui/App.tsx b/packages/cli/src/ui/App.tsx
index 74c1ea5d..4921c93e 100644
--- a/packages/cli/src/ui/App.tsx
+++ b/packages/cli/src/ui/App.tsx
@@ -5,8 +5,21 @@
*/
import { useCallback, useEffect, useMemo, useState, useRef } from 'react';
-import { Box, DOMElement, measureElement, Static, Text } from 'ink';
-import { StreamingState, type HistoryItem } from './types.js';
+import {
+ 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 { useGeminiStream } from './hooks/useGeminiStream.js';
import { useLoadingIndicator } from './hooks/useLoadingIndicator.js';
@@ -25,11 +38,11 @@ import { Help } from './components/Help.js';
import { loadHierarchicalGeminiMemory } from '../config/config.js';
import { LoadedSettings } from '../config/settings.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 { useHistory } from './hooks/useHistoryManager.js';
import process from 'node:process';
-import { MessageType } from './types.js';
import { getErrorMessage, type Config } from '@gemini-code/server';
import { useLogger } from './hooks/useLogger.js';
@@ -61,6 +74,32 @@ export const App = ({
const [corgiMode, setCorgiMode] = 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(() => {
setCorgiMode((prev) => !prev);
}, []);
@@ -72,7 +111,6 @@ export const App = ({
handleThemeHighlight,
} = useThemeCommand(settings, setThemeError);
- // useEffect to initialize geminiMdFileCount from config when config is ready
useEffect(() => {
if (config) {
setGeminiMdFileCount(config.getGeminiMdFileCount());
@@ -186,12 +224,11 @@ export const App = ({
const handleClearScreen = useCallback(() => {
clearItems();
+ setConsoleMessages([]);
console.clear();
refreshStatic();
}, [clearItems, refreshStatic]);
- // --- Render Logic ---
-
const { rows: terminalHeight } = useTerminalSize();
const mainControlsRef = useRef<DOMElement>(null);
const pendingHistoryItemRef = useRef<DOMElement>(null);
@@ -201,7 +238,7 @@ export const App = ({
const fullFooterMeasurement = measureElement(mainControlsRef.current);
setFooterHeight(fullFooterMeasurement.height);
}
- }, [terminalHeight]);
+ }, [terminalHeight, consoleMessages, showErrorDetails]);
const availableTerminalHeight = useMemo(() => {
const staticExtraHeight = /* margins and padding */ 3;
@@ -232,6 +269,13 @@ export const App = ({
}
}, [streamingState, refreshStatic, staticNeedsRefresh]);
+ const filteredConsoleMessages = useMemo(() => {
+ if (config.getDebugMode()) {
+ return consoleMessages;
+ }
+ return consoleMessages.filter((msg) => msg.type !== 'debug');
+ }, [consoleMessages, config]);
+
return (
<Box flexDirection="column" marginBottom={1} width="90%">
{/*
@@ -339,6 +383,11 @@ export const App = ({
{shellModeActive && <ShellModeIndicator />}
</Box>
</Box>
+
+ {showErrorDetails && (
+ <DetailedMessagesDisplay messages={filteredConsoleMessages} />
+ )}
+
{isInputActive && (
<InputPrompt
widthFraction={0.9}
@@ -392,8 +441,9 @@ export const App = ({
debugMessage={debugMessage}
cliVersion={cliVersion}
corgiMode={corgiMode}
+ errorCount={errorCount}
+ showErrorDetails={showErrorDetails}
/>
- <ConsoleOutput debugMode={config.getDebugMode()} />
</Box>
</Box>
);
diff --git a/packages/cli/src/ui/components/ConsolePatcher.tsx b/packages/cli/src/ui/components/ConsolePatcher.tsx
index 366aef43..240a32e5 100644
--- a/packages/cli/src/ui/components/ConsolePatcher.tsx
+++ b/packages/cli/src/ui/components/ConsolePatcher.tsx
@@ -4,27 +4,19 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import React, { useState, useEffect, Key } from 'react';
-import { Box, Text } from 'ink';
+import { useEffect } from 'react';
import util from 'util';
+import { ConsoleMessageItem } from '../types.js';
-interface ConsoleMessage {
- id: Key;
- 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 {
+interface UseConsolePatcherParams {
+ onNewMessage: (message: Omit<ConsoleMessageItem, 'id'>) => void;
debugMode: boolean;
}
-export const ConsoleOutput: React.FC<ConsoleOutputProps> = ({ debugMode }) => {
- const [messages, setMessages] = useState<ConsoleMessage[]>([]);
-
+export const useConsolePatcher = ({
+ onNewMessage,
+ debugMode,
+}: UseConsolePatcherParams): void => {
useEffect(() => {
const originalConsoleLog = console.log;
const originalConsoleWarn = console.warn;
@@ -32,25 +24,30 @@ export const ConsoleOutput: React.FC<ConsoleOutputProps> = ({ debugMode }) => {
const originalConsoleDebug = console.debug;
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
- console.log = (...args: unknown[]) => addMessage('log', args);
- console.warn = (...args: unknown[]) => addMessage('warn', args);
- console.error = (...args: unknown[]) => addMessage('error', args);
- console.debug = (...args: unknown[]) => addMessage('debug', args);
+ const patchConsoleMethod =
+ (
+ type: 'log' | 'warn' | 'error' | 'debug',
+ originalMethod: (...args: unknown[]) => void,
+ ) =>
+ (...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 () => {
console.log = originalConsoleLog;
@@ -58,46 +55,5 @@ export const ConsoleOutput: React.FC<ConsoleOutputProps> = ({ debugMode }) => {
console.error = originalConsoleError;
console.debug = originalConsoleDebug;
};
- }, []);
-
- 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>
- );
+ }, [onNewMessage, debugMode]);
};
diff --git a/packages/cli/src/ui/components/ConsoleSummaryDisplay.tsx b/packages/cli/src/ui/components/ConsoleSummaryDisplay.tsx
new file mode 100644
index 00000000..b944f409
--- /dev/null
+++ b/packages/cli/src/ui/components/ConsoleSummaryDisplay.tsx
@@ -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>
+ );
+};
diff --git a/packages/cli/src/ui/components/DetailedMessagesDisplay.tsx b/packages/cli/src/ui/components/DetailedMessagesDisplay.tsx
new file mode 100644
index 00000000..de4f3f6e
--- /dev/null
+++ b/packages/cli/src/ui/components/DetailedMessagesDisplay.tsx
@@ -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>
+ );
+};
diff --git a/packages/cli/src/ui/components/Footer.tsx b/packages/cli/src/ui/components/Footer.tsx
index 03e85db1..7f7d058a 100644
--- a/packages/cli/src/ui/components/Footer.tsx
+++ b/packages/cli/src/ui/components/Footer.tsx
@@ -8,6 +8,7 @@ import React from 'react';
import { Box, Text } from 'ink';
import { Colors } from '../colors.js';
import { shortenPath, tildeifyPath, Config } from '@gemini-code/server';
+import { ConsoleSummaryDisplay } from './ConsoleSummaryDisplay.js';
interface FooterProps {
config: Config;
@@ -15,6 +16,8 @@ interface FooterProps {
debugMessage: string;
cliVersion: string;
corgiMode: boolean;
+ errorCount: number;
+ showErrorDetails: boolean;
}
export const Footer: React.FC<FooterProps> = ({
@@ -23,8 +26,10 @@ export const Footer: React.FC<FooterProps> = ({
debugMessage,
cliVersion,
corgiMode,
+ errorCount,
+ showErrorDetails,
}) => (
- <Box marginTop={1}>
+ <Box marginTop={1} justifyContent="space-between" width="100%">
<Box>
<Text color={Colors.LightBlue}>
{shortenPath(tildeifyPath(config.getTargetDir()), 70)}
@@ -56,8 +61,8 @@ export const Footer: React.FC<FooterProps> = ({
)}
</Box>
- {/* Right Section: Gemini Label */}
- <Box>
+ {/* Right Section: Gemini Label and Console Summary */}
+ <Box alignItems="center">
<Text color={Colors.AccentBlue}> {config.getModel()} </Text>
<Text color={Colors.SubtleComment}>| CLI {cliVersion} </Text>
{corgiMode && (
@@ -70,6 +75,12 @@ export const Footer: React.FC<FooterProps> = ({
<Text color={Colors.AccentRed}>▼ </Text>
</Text>
)}
+ {!showErrorDetails && errorCount > 0 && (
+ <Box>
+ <Text color={Colors.SubtleComment}>| </Text>
+ <ConsoleSummaryDisplay errorCount={errorCount} />
+ </Box>
+ )}
</Box>
</Box>
);
diff --git a/packages/cli/src/ui/types.ts b/packages/cli/src/ui/types.ts
index ff7515a5..d660fe16 100644
--- a/packages/cli/src/ui/types.ts
+++ b/packages/cli/src/ui/types.ts
@@ -119,3 +119,8 @@ export interface Message {
content: string; // Renamed from text for clarity in this context
timestamp: Date; // For consistency, though addItem might use its own timestamping
}
+
+export interface ConsoleMessageItem {
+ type: 'log' | 'warn' | 'error' | 'debug';
+ content: string;
+}