From 2006ab25ff75e83af69e74fe1478c485998bf364 Mon Sep 17 00:00:00 2001 From: claude-code-best Date: Sat, 9 May 2026 22:02:04 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E6=B7=BB=E5=8A=A0=20React=20Error=20Bou?= =?UTF-8?q?ndary=20=E9=98=B2=E6=AD=A2=E7=94=9F=E4=BA=A7=E7=8E=AF=E5=A2=83?= =?UTF-8?q?=E6=B8=B2=E6=9F=93=E5=B4=A9=E6=BA=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 增强 SentryErrorBoundary 组件,捕获渲染错误时输出诊断信息 (错误消息 + component stack)到 stderr 和终端,而非静默返回 null。在 replLauncher 根节点和 Messages 组件层级包裹 Error Boundary,防止 Ink 内部的 Error Boundary 直接终止进程。 Co-Authored-By: glm-5-turbo --- src/components/Messages.tsx | 5 ++- src/components/SentryErrorBoundary.ts | 38 ---------------- src/components/SentryErrorBoundary.tsx | 62 ++++++++++++++++++++++++++ src/replLauncher.tsx | 9 ++-- 4 files changed, 71 insertions(+), 43 deletions(-) delete mode 100644 src/components/SentryErrorBoundary.ts create mode 100644 src/components/SentryErrorBoundary.tsx diff --git a/src/components/Messages.tsx b/src/components/Messages.tsx index 638fe6c42..c7d4eb671 100644 --- a/src/components/Messages.tsx +++ b/src/components/Messages.tsx @@ -1,5 +1,6 @@ import { feature } from 'bun:bundle'; import chalk from 'chalk'; +import { SentryErrorBoundary } from './SentryErrorBoundary.js'; import type { UUID } from 'crypto'; import type { RefObject } from 'react'; import * as React from 'react'; @@ -890,7 +891,7 @@ const MessagesImpl = ({ ); return ( - <> + {/* Logo */} {!hideLogo && !(renderRange && renderRange[0] > 0) && } @@ -977,7 +978,7 @@ const MessagesImpl = ({ /> )} - + ); }; diff --git a/src/components/SentryErrorBoundary.ts b/src/components/SentryErrorBoundary.ts deleted file mode 100644 index 7380a62b0..000000000 --- a/src/components/SentryErrorBoundary.ts +++ /dev/null @@ -1,38 +0,0 @@ -import * as React from 'react' -import { captureException } from 'src/utils/sentry.js' - -interface Props { - children: React.ReactNode - /** Optional label for identifying which component boundary caught the error */ - name?: string -} - -interface State { - hasError: boolean -} - -export class SentryErrorBoundary extends React.Component { - constructor(props: Props) { - super(props) - this.state = { hasError: false } - } - - static getDerivedStateFromError(): State { - return { hasError: true } - } - - componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void { - captureException(error, { - componentBoundary: this.props.name || 'SentryErrorBoundary', - componentStack: errorInfo.componentStack, - }) - } - - render(): React.ReactNode { - if (this.state.hasError) { - return null - } - - return this.props.children - } -} diff --git a/src/components/SentryErrorBoundary.tsx b/src/components/SentryErrorBoundary.tsx new file mode 100644 index 000000000..a1a7ace0e --- /dev/null +++ b/src/components/SentryErrorBoundary.tsx @@ -0,0 +1,62 @@ +import * as React from 'react'; +import { Box, Text } from '@anthropic/ink'; +import { captureException } from 'src/utils/sentry.js'; +import { logError } from 'src/utils/log.js'; + +interface Props { + children: React.ReactNode; + /** Optional label for identifying which component boundary caught the error */ + name?: string; +} + +interface State { + hasError: boolean; + error: Error | null; + errorInfo: React.ErrorInfo | null; +} + +export class SentryErrorBoundary extends React.Component { + constructor(props: Props) { + super(props); + this.state = { hasError: false, error: null, errorInfo: null }; + } + + static getDerivedStateFromError(error: Error): Pick { + return { hasError: true, error }; + } + + componentDidCatch(error: Error, errorInfo: React.ErrorInfo): void { + this.setState({ errorInfo }); + + // Log to stderr so the diagnostic info is visible even in production builds + const boundary = this.props.name || 'SentryErrorBoundary'; + const lines = ['', `[ErrorBoundary:${boundary}] React rendering error caught`, ` Message: ${error.message}`]; + if (errorInfo.componentStack) { + lines.push(` Component stack:\n${errorInfo.componentStack}`); + } + // eslint-disable-next-line no-console -- intentional stderr diagnostic output + console.error(lines.join('\n')); + + logError(error); + captureException(error, { + componentBoundary: boundary, + componentStack: errorInfo.componentStack, + }); + } + + render(): React.ReactNode { + if (this.state.hasError) { + return ( + + + React Rendering Error + + {this.state.error?.message} + {this.props.name && Boundary: {this.props.name}} + + ); + } + + return this.props.children; + } +} diff --git a/src/replLauncher.tsx b/src/replLauncher.tsx index 0d27afe12..040636719 100644 --- a/src/replLauncher.tsx +++ b/src/replLauncher.tsx @@ -18,11 +18,14 @@ export async function launchRepl( renderAndRun: (root: Root, element: React.ReactNode) => Promise, ): Promise { const { App } = await import('./components/App.js'); + const { SentryErrorBoundary } = await import('./components/SentryErrorBoundary.js'); const { REPL } = await import('./screens/REPL.js'); await renderAndRun( root, - - - , + + + + + , ); }