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