refactor(debug): replace ConsolePatcher with debugLogger and update error reporting

- Replace ConsolePatcher with centralized debugLogger utility
- Refactor errorReporting to use debugLogger instead of file-based reporting
- Remove user-facing console message components:
  - Delete ConsolePatcher.ts, useConsoleMessages.ts/hook
  - Delete ConsoleSummaryDisplay.tsx, DetailedMessagesDisplay.tsx
- Update all tests in packages/core and packages/cli:
  - Mock debugLogger where needed
  - Remove assertions for console output on non-critical errors
  - Keep debugLogger assertions for fatal/network errors
  - Use HOME directory mocking for hermetic file system tests

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
tanzhenxin 2026-02-02 17:37:54 +08:00
parent 135df54f27
commit 89e3c2cd7a
64 changed files with 1240 additions and 2416 deletions

View file

@ -43,10 +43,6 @@ vi.mock('./ShellModeIndicator.js', () => ({
ShellModeIndicator: () => <Text>ShellModeIndicator</Text>,
}));
vi.mock('./DetailedMessagesDisplay.js', () => ({
DetailedMessagesDisplay: () => <Text>DetailedMessagesDisplay</Text>,
}));
vi.mock('./InputPrompt.js', () => ({
InputPrompt: () => <Text>InputPrompt</Text>,
calculatePromptWidths: vi.fn(() => ({
@ -60,10 +56,6 @@ vi.mock('./Footer.js', () => ({
Footer: () => <Text>Footer</Text>,
}));
vi.mock('./ShowMoreLines.js', () => ({
ShowMoreLines: () => <Text>ShowMoreLines</Text>,
}));
vi.mock('./QueuedMessageDisplay.js', () => ({
QueuedMessageDisplay: ({ messageQueue }: { messageQueue: string[] }) => {
if (messageQueue.length === 0) {
@ -91,7 +83,6 @@ const createMockUIState = (overrides: Partial<UIState> = {}): UIState =>
contextFileNames: [],
showAutoAcceptIndicator: ApprovalMode.DEFAULT,
messageQueue: [],
showErrorDetails: false,
constrainHeight: false,
isInputActive: true,
buffer: '',
@ -111,7 +102,6 @@ const createMockUIState = (overrides: Partial<UIState> = {}): UIState =>
ideContextState: null,
geminiMdFileCount: 0,
showToolDescriptions: false,
filteredConsoleMessages: [],
sessionStats: {
lastPromptTokenCount: 0,
sessionTokenCount: 0,
@ -119,7 +109,6 @@ const createMockUIState = (overrides: Partial<UIState> = {}): UIState =>
},
branchName: 'main',
debugMessage: '',
errorCount: 0,
nightly: false,
isTrustedFolder: true,
...overrides,
@ -354,31 +343,4 @@ describe('Composer', () => {
expect(lastFrame()).toContain('Footer');
});
});
describe('Error Details Display', () => {
it('shows DetailedMessagesDisplay when showErrorDetails is true', () => {
const uiState = createMockUIState({
showErrorDetails: true,
filteredConsoleMessages: [
{ level: 'error', message: 'Test error', timestamp: new Date() },
// eslint-disable-next-line @typescript-eslint/no-explicit-any
] as any,
});
const { lastFrame } = renderComposer(uiState);
expect(lastFrame()).toContain('DetailedMessagesDisplay');
expect(lastFrame()).toContain('ShowMoreLines');
});
it('does not show error details when showErrorDetails is false', () => {
const uiState = createMockUIState({
showErrorDetails: false,
});
const { lastFrame } = renderComposer(uiState);
expect(lastFrame()).not.toContain('DetailedMessagesDisplay');
});
});
});

View file

@ -5,15 +5,12 @@
*/
import { Box, useIsScreenReaderEnabled } from 'ink';
import { useCallback, useMemo, useState } from 'react';
import { useCallback, useState } from 'react';
import { LoadingIndicator } from './LoadingIndicator.js';
import { DetailedMessagesDisplay } from './DetailedMessagesDisplay.js';
import { InputPrompt, calculatePromptWidths } from './InputPrompt.js';
import { InputPrompt } from './InputPrompt.js';
import { Footer } from './Footer.js';
import { ShowMoreLines } from './ShowMoreLines.js';
import { QueuedMessageDisplay } from './QueuedMessageDisplay.js';
import { KeyboardShortcuts } from './KeyboardShortcuts.js';
import { OverflowProvider } from '../contexts/OverflowContext.js';
import { useUIState } from '../contexts/UIStateContext.js';
import { useUIActions } from '../contexts/UIActionsContext.js';
import { useVimMode } from '../contexts/VimModeContext.js';
@ -29,8 +26,6 @@ export const Composer = () => {
const uiState = useUIState();
const uiActions = useUIActions();
const { vimEnabled } = useVimMode();
const terminalWidth = process.stdout.columns;
const debugConsoleMaxHeight = Math.floor(Math.max(terminalWidth * 0.2, 5));
const { showAutoAcceptIndicator } = uiState;
@ -46,12 +41,6 @@ export const Composer = () => {
setShowSuggestions(visible);
}, []);
// Use the container width of InputPrompt for width of DetailedMessagesDisplay
const { containerWidth } = useMemo(
() => calculatePromptWidths(uiState.terminalWidth),
[uiState.terminalWidth],
);
return (
<Box flexDirection="column" marginTop={1}>
{!uiState.embeddedShellFocused && (
@ -75,21 +64,6 @@ export const Composer = () => {
<QueuedMessageDisplay messageQueue={uiState.messageQueue} />
{uiState.showErrorDetails && (
<OverflowProvider>
<Box flexDirection="column">
<DetailedMessagesDisplay
messages={uiState.filteredConsoleMessages}
maxHeight={
uiState.constrainHeight ? debugConsoleMaxHeight : undefined
}
width={containerWidth}
/>
<ShowMoreLines constrainHeight={uiState.constrainHeight} />
</Box>
</OverflowProvider>
)}
{uiState.isFeedbackDialogOpen && <FeedbackDialog />}
{uiState.isInputActive && (

View file

@ -1,35 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import { Box, Text } from 'ink';
import { theme } from '../semantic-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={theme.status.error}>
{errorIcon} {errorCount} error{errorCount > 1 ? 's' : ''}{' '}
<Text color={theme.text.secondary}>(ctrl+o for details)</Text>
</Text>
)}
</Box>
);
};

View file

@ -1,83 +0,0 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/
import type React from 'react';
import { Box, Text } from 'ink';
import { theme } from '../semantic-colors.js';
import type { ConsoleMessageItem } from '../types.js';
import { MaxSizedBox } from './shared/MaxSizedBox.js';
interface DetailedMessagesDisplayProps {
messages: ConsoleMessageItem[];
maxHeight: number | undefined;
width: number;
// 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, maxHeight, width }) => {
if (messages.length === 0) {
return null; // Don't render anything if there are no messages
}
const borderAndPadding = 4;
return (
<Box
flexDirection="column"
marginTop={1}
borderStyle="round"
borderColor={theme.border.default}
paddingX={1}
width={width}
>
<Box marginBottom={1}>
<Text bold color={theme.text.primary}>
Debug Console{' '}
<Text color={theme.text.secondary}>(ctrl+o to close)</Text>
</Text>
</Box>
<MaxSizedBox maxHeight={maxHeight} maxWidth={width - borderAndPadding}>
{messages.map((msg, index) => {
let textColor = theme.text.primary;
let icon = '\u2139'; // Information source ()
switch (msg.type) {
case 'warn':
textColor = theme.status.warning;
icon = '\u26A0'; // Warning sign (⚠)
break;
case 'error':
textColor = theme.status.error;
icon = '\u2716'; // Heavy multiplication x (✖)
break;
case 'debug':
textColor = theme.text.secondary; // Or theme.text.secondary
icon = '\u{1F50D}'; // 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}
{msg.count && msg.count > 1 && (
<Text color={theme.text.secondary}> (x{msg.count})</Text>
)}
</Text>
</Box>
);
})}
</MaxSizedBox>
</Box>
);
};

View file

@ -7,7 +7,6 @@
import type React from 'react';
import { Box, Text } from 'ink';
import { theme } from '../semantic-colors.js';
import { ConsoleSummaryDisplay } from './ConsoleSummaryDisplay.js';
import { ContextUsageDisplay } from './ContextUsageDisplay.js';
import { useTerminalSize } from '../hooks/useTerminalSize.js';
import { AutoAcceptIndicator } from './AutoAcceptIndicator.js';
@ -25,20 +24,11 @@ export const Footer: React.FC = () => {
const config = useConfig();
const { vimEnabled, vimMode } = useVimMode();
const {
errorCount,
showErrorDetails,
promptTokenCount,
showAutoAcceptIndicator,
} = {
errorCount: uiState.errorCount,
showErrorDetails: uiState.showErrorDetails,
const { promptTokenCount, showAutoAcceptIndicator } = {
promptTokenCount: uiState.sessionStats.lastPromptTokenCount,
showAutoAcceptIndicator: uiState.showAutoAcceptIndicator,
};
const showErrorIndicator = !showErrorDetails && errorCount > 0;
const { columns: terminalWidth } = useTerminalSize();
const isNarrow = isNarrowWidth(terminalWidth);
@ -103,13 +93,6 @@ export const Footer: React.FC = () => {
),
});
}
if (showErrorIndicator) {
rightItems.push({
key: 'errors',
node: <ConsoleSummaryDisplay errorCount={errorCount} />,
});
}
return (
<Box
justifyContent="space-between"

View file

@ -38,19 +38,13 @@ describe('IdeTrustChangeDialog', () => {
expect(frameText).toContain("Press 'r' to restart Gemini");
});
it('renders a generic message and logs an error for NONE reason', () => {
const consoleErrorSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
it('renders a generic message for NONE reason', () => {
const { lastFrame } = renderWithProviders(
<IdeTrustChangeDialog reason="NONE" />,
);
const frameText = lastFrame();
expect(frameText).toContain('Workspace trust has changed.');
expect(consoleErrorSpy).toHaveBeenCalledWith(
'IdeTrustChangeDialog rendered with unexpected reason "NONE"',
);
});
it('calls relaunchApp when "r" is pressed', () => {

View file

@ -475,10 +475,7 @@ describe('InputPrompt', () => {
unmount();
});
it('should handle errors during clipboard operations', async () => {
const consoleErrorSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
it('should handle errors during clipboard operations gracefully', async () => {
vi.mocked(clipboardUtils.clipboardHasImage).mockRejectedValue(
new Error('Clipboard error'),
);
@ -491,13 +488,9 @@ describe('InputPrompt', () => {
stdin.write('\x16'); // Ctrl+V
await wait();
expect(consoleErrorSpy).toHaveBeenCalledWith(
'Error handling clipboard image:',
expect.any(Error),
);
// Should not throw and should not set buffer text on error
expect(mockBuffer.setText).not.toHaveBeenCalled();
consoleErrorSpy.mockRestore();
unmount();
});
});

View file

@ -394,10 +394,6 @@ describe('QwenOAuthProgress', () => {
it('should handle QR code generation errors gracefully', async () => {
const qrcode = await import('qrcode-terminal');
const mockGenerate = vi.mocked(qrcode.default.generate);
const consoleErrorSpy = vi
.spyOn(console, 'error')
.mockImplementation(() => {});
mockGenerate.mockImplementation(() => {
throw new Error('QR Code generation failed');
});
@ -413,12 +409,6 @@ describe('QwenOAuthProgress', () => {
// Should not crash and should not show QR code section since QR generation failed
const output = lastFrame();
expect(output).not.toContain('Or scan the QR code below:');
expect(consoleErrorSpy).toHaveBeenCalledWith(
'Failed to generate QR code:',
expect.any(Error),
);
consoleErrorSpy.mockRestore();
});
it('should not generate QR code when deviceAuth is null', async () => {