/** * @license * Copyright 2025 Vybestack LLC * SPDX-License-Identifier: Apache-2.0 */ import type { ErrorInfo, ReactNode } from 'react'; import type React from 'react'; import { Component } from 'react'; import { Box, Text } from 'ink'; import { Colors } from '../colors.js'; import { firstNonEmptyString } from '../../utils/coalesce.js'; interface Props { children: ReactNode; fallback?: (error: Error, errorInfo: ErrorInfo) => ReactNode; onError?: (error: Error, errorInfo: ErrorInfo) => void; } interface State { hasError: boolean; error: Error | null; errorInfo: ErrorInfo | null; errorCount: number; } /** * Enhanced error boundary that detects and reports React errors, * including "Maximum update depth exceeded" errors. */ export class ErrorBoundary extends Component { private errorTimestamps: number[] = []; private readonly ERROR_TIME_WINDOW = 5000; // 5 seconds private readonly MAX_ERRORS_IN_WINDOW = 5; constructor(props: Props) { super(props); this.state = { hasError: false, error: null, errorInfo: null, errorCount: 0, }; } static getDerivedStateFromError(error: Error): Partial { return { hasError: true, error, }; } override componentDidCatch(error: Error, errorInfo: ErrorInfo) { const now = Date.now(); this.errorTimestamps.push(now); // Clean up old timestamps this.errorTimestamps = this.errorTimestamps.filter( (timestamp) => timestamp > now - this.ERROR_TIME_WINDOW, ); // Check if we're in an error loop const isErrorLoop = this.errorTimestamps.length > this.MAX_ERRORS_IN_WINDOW; // Special handling for Maximum update depth exceeded const isMaxUpdateDepthError = error.message.includes( 'Maximum update depth exceeded', ); if (isMaxUpdateDepthError || isErrorLoop) { globalThis.console.error('CRITICAL: Render loop detected!'); globalThis.console.error('Error:', error.message); globalThis.console.error('Component Stack:', errorInfo.componentStack); if (isMaxUpdateDepthError) { globalThis.console.error('\nThis error typically occurs when:'); globalThis.console.error('1. setState is called inside render()'); globalThis.console.error( '2. useEffect has missing or incorrect dependencies', ); globalThis.console.error( '3. Props are recreated on every render (objects, arrays, functions)', ); globalThis.console.error( '\nCheck recent changes to hooks and state updates.', ); } } // Log error details globalThis.console.error('React Error Boundary caught an error:', error); globalThis.console.error('Error Info:', errorInfo); globalThis.console.error( 'Error count in window:', this.errorTimestamps.length, ); // Call custom error handler if provided if (this.props.onError) { this.props.onError(error, errorInfo); } // Update state with error info this.setState((prevState) => ({ errorInfo, errorCount: prevState.errorCount + 1, })); // If we're in an error loop, try to break out if (isErrorLoop) { // Force a hard refresh after a delay to break the loop setTimeout(() => { globalThis.console.error('Attempting to recover from error loop...'); this.setState({ hasError: false, error: null, errorInfo: null, errorCount: 0, }); this.errorTimestamps = []; }, 1000); } } override render() { if (this.state.hasError && this.state.error) { // Use custom fallback if provided if (this.props.fallback && this.state.errorInfo) { return this.props.fallback(this.state.error, this.state.errorInfo); } // Default error UI const isMaxUpdateDepthError = this.state.error.message.includes( 'Maximum update depth exceeded', ); return ( {isMaxUpdateDepthError ? 'CRITICAL: Render Loop Error' : '❌ An error occurred'} {this.state.error.message} {this.state.errorCount > 1 && ( Error count: {this.state.errorCount} )} {isMaxUpdateDepthError && ( This error indicates an infinite render loop. Common causes: • State updates during render • Incorrect useEffect dependencies • Non-memoized props causing re-renders )} Check the console for more details. ); } return this.props.children; } } /** * Hook to wrap a component with an error boundary. * * @example * ```typescript * function MyApp() { * return ( * logError(error, info)}> * * * ); * } * ``` */ export function withErrorBoundary

( Component: React.ComponentType

, errorBoundaryProps?: Omit, ): React.ComponentType

{ const WrappedComponent = (props: P) => ( ); WrappedComponent.displayName = `withErrorBoundary(${firstNonEmptyString( Component.displayName, Component.name, )})`; return WrappedComponent; }