diff --git a/src/components/Messages.tsx b/src/components/Messages.tsx index 921f93010..ce9288f89 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 91550a3bd..040636719 100644 --- a/src/replLauncher.tsx +++ b/src/replLauncher.tsx @@ -1,15 +1,15 @@ -import React from 'react' -import type { StatsStore } from './context/stats.js' -import type { Root } from '@anthropic/ink' -import type { Props as REPLProps } from './screens/REPL.js' -import type { AppState } from './state/AppStateStore.js' -import type { FpsMetrics } from './utils/fpsTracker.js' +import React from 'react'; +import type { StatsStore } from './context/stats.js'; +import type { Root } from '@anthropic/ink'; +import type { Props as REPLProps } from './screens/REPL.js'; +import type { AppState } from './state/AppStateStore.js'; +import type { FpsMetrics } from './utils/fpsTracker.js'; type AppWrapperProps = { - getFpsMetrics: () => FpsMetrics | undefined - stats?: StatsStore - initialState: AppState -} + getFpsMetrics: () => FpsMetrics | undefined; + stats?: StatsStore; + initialState: AppState; +}; export async function launchRepl( root: Root, @@ -17,12 +17,15 @@ export async function launchRepl( replProps: REPLProps, renderAndRun: (root: Root, element: React.ReactNode) => Promise, ): Promise { - const { App } = await import('./components/App.js') - const { REPL } = await import('./screens/REPL.js') + const { App } = await import('./components/App.js'); + const { SentryErrorBoundary } = await import('./components/SentryErrorBoundary.js'); + const { REPL } = await import('./screens/REPL.js'); await renderAndRun( root, - - - , - ) + + + + + , + ); }