diff --git a/src/components/TerminalView.tsx b/src/components/TerminalView.tsx index 7246a1b3..e2c12499 100644 --- a/src/components/TerminalView.tsx +++ b/src/components/TerminalView.tsx @@ -129,7 +129,7 @@ import { } from '@/components/terminal/terminal-write-queue' import { nanoid } from 'nanoid' import { cn } from '@/lib/utils' -import { Terminal } from '@xterm/xterm' +import { Terminal, type ILogger } from '@xterm/xterm' import { Loader2 } from 'lucide-react' import { ConfirmModal } from '@/components/ui/confirm-modal' import type { PaneContent, PaneContentInput, PaneRefreshRequest, TerminalPaneContent } from '@/store/paneTypes' @@ -175,6 +175,51 @@ const TERMINAL_OUTPUT_BATCH_BARRIER_REASONS = new Set([ 'geometry', ]) +function isXtermGroundDelParserNoise(message: string | Error, args: unknown[]): boolean { + if (typeof message !== 'string' || message.trim() !== 'Parsing error:') return false + const state = args[0] + return !!state + && typeof state === 'object' + && (state as { code?: unknown }).code === 0x7f + && (state as { currentState?: unknown }).currentState === 0 +} + +function forwardXtermLog( + method: keyof ILogger, + message: string | Error, + args: unknown[], +): void { + const prefixedMessage = `xterm.js: ${String(message)}` + switch (method) { + case 'trace': + console.trace(prefixedMessage, ...args) + break + case 'debug': + console.debug(prefixedMessage, ...args) + break + case 'info': + console.info(prefixedMessage, ...args) + break + case 'warn': + console.warn(prefixedMessage, ...args) + break + case 'error': + console.error(prefixedMessage, ...args) + break + } +} + +const xtermLogger: ILogger = { + trace: (message, ...args) => forwardXtermLog('trace', message, args), + debug: (message, ...args) => forwardXtermLog('debug', message, args), + info: (message, ...args) => forwardXtermLog('info', message, args), + warn: (message, ...args) => forwardXtermLog('warn', message, args), + error: (message, ...args) => { + if (isXtermGroundDelParserNoise(message, args)) return + forwardXtermLog('error', message, args) + }, +} + function viewportHydrateReplayOptions(content?: TerminalPaneContent | null): { maxReplayBytes: number } | undefined { return content?.mode === 'opencode' ? undefined @@ -1682,6 +1727,7 @@ function TerminalView({ tabId, paneId, paneContent, hidden }: TerminalViewProps) fontSize: settings.terminal.fontSize, fontFamily: resolveTerminalFontFamily(settings.terminal.fontFamily), lineHeight: settings.terminal.lineHeight, + logger: xtermLogger, scrollback: settings.terminal.scrollback, theme: resolvedTheme, minimumContrastRatio: resolveMinimumContrastRatio(resolvedTheme), diff --git a/test/unit/client/components/TerminalView.search.test.tsx b/test/unit/client/components/TerminalView.search.test.tsx index acf22f2c..b45918df 100644 --- a/test/unit/client/components/TerminalView.search.test.tsx +++ b/test/unit/client/components/TerminalView.search.test.tsx @@ -354,4 +354,42 @@ describe('TerminalView search', () => { expect.objectContaining({ allowProposedApi: true }), ) }) + + it('suppresses xterm DEL parser noise while forwarding other errors', async () => { + const { store, tabId, paneId, paneContent } = createTestStore() + + render( + + + , + ) + + await waitFor(() => { + expect(capturedTerminalOptions).not.toBeNull() + }) + + const logger = capturedTerminalOptions?.logger as { + error?: (message: string, ...args: unknown[]) => void + } | undefined + expect(logger?.error).toBeTypeOf('function') + + const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {}) + try { + logger!.error!('Parsing error: ', { code: 127, currentState: 0 }) + logger!.error!('Parsing error: ', { code: 127, currentState: 4 }) + logger!.error!('Something else', { code: 127, currentState: 0 }) + + expect(consoleErrorSpy).toHaveBeenCalledTimes(2) + expect(consoleErrorSpy).toHaveBeenNthCalledWith(1, 'xterm.js: Parsing error: ', { + code: 127, + currentState: 4, + }) + expect(consoleErrorSpy).toHaveBeenNthCalledWith(2, 'xterm.js: Something else', { + code: 127, + currentState: 0, + }) + } finally { + consoleErrorSpy.mockRestore() + } + }) })