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, - - - , + + + + + , ); }