import React, { Component, type ErrorInfo, type ReactNode } from 'react'; import { useAppTranslation } from '@features/localization/renderer'; import { captureRendererException, isSentryRendererActive } from '@renderer/sentry'; import { useStore } from '@renderer/store'; import { type BugReportContext, buildBugReportText, buildGitHubBugReportUrl, } from '@renderer/utils/bugReportUtils'; import { createLogger } from '@shared/utils/logger'; import { AlertTriangle, Bug, Check, Copy, RefreshCw } from 'lucide-react'; const logger = createLogger('Component:ErrorBoundary'); interface Props { children: ReactNode; fallback?: ReactNode; labels?: ErrorBoundaryLabels; } interface ErrorBoundaryLabels { title: string; description: string; componentStack: string; tryAgain: string; copied: string; copyErrorDetails: string; reportBugOnGitHub: string; reloadApp: string; diagnosticsNotice: string; } interface State { hasError: boolean; copiedReport: boolean; error: Error | null; errorInfo: ErrorInfo | null; } class ErrorBoundaryInner extends Component { private copyResetTimeout: ReturnType | null = null; constructor(props: Props) { super(props); this.state = { hasError: false, copiedReport: false, error: null, errorInfo: null, }; } static getDerivedStateFromError(error: Error): Partial { return { hasError: true, error }; } componentDidCatch(error: Error, errorInfo: ErrorInfo): void { logger.error('ErrorBoundary caught an error:', error, errorInfo); this.setState({ errorInfo }); // Report to Sentry when telemetry is active if (isSentryRendererActive()) { captureRendererException(error, { componentStack: errorInfo.componentStack, ...this.getBugReportContext(), }); } } handleReload = (): void => { window.location.reload(); }; handleReset = (): void => { if (this.copyResetTimeout) { clearTimeout(this.copyResetTimeout); this.copyResetTimeout = null; } this.setState({ hasError: false, copiedReport: false, error: null, errorInfo: null, }); }; componentWillUnmount(): void { if (this.copyResetTimeout) { clearTimeout(this.copyResetTimeout); this.copyResetTimeout = null; } } getBugReportContext = (): BugReportContext => { const state = useStore.getState(); const activeTab = state.getActiveTab(); return { activeTabType: activeTab?.type ?? null, activeTabLabel: activeTab?.label ?? null, activeTeamName: activeTab?.teamName ?? null, selectedTeamName: state.selectedTeamName, taskId: state.globalTaskDetail?.taskId ?? state.pendingReviewRequest?.taskId ?? null, sessionId: activeTab?.sessionId ?? null, projectId: activeTab?.projectId ?? state.activeProjectId, }; }; handleCreateGitHubIssue = (): void => { const issueUrl = buildGitHubBugReportUrl({ error: this.state.error, componentStack: this.state.errorInfo?.componentStack ?? null, context: this.getBugReportContext(), }); if (window.electronAPI?.openExternal) { void window.electronAPI.openExternal(issueUrl); return; } window.open(issueUrl, '_blank', 'noopener,noreferrer'); }; handleCopyErrorDetails = async (): Promise => { try { await navigator.clipboard.writeText( buildBugReportText({ error: this.state.error, componentStack: this.state.errorInfo?.componentStack ?? null, context: this.getBugReportContext(), }) ); if (this.copyResetTimeout) { clearTimeout(this.copyResetTimeout); } this.setState({ copiedReport: true }); this.copyResetTimeout = setTimeout(() => { this.setState({ copiedReport: false }); this.copyResetTimeout = null; }, 2000); } catch (error) { logger.warn('Failed to copy error details:', error); } }; // eslint-disable-next-line sonarjs/function-return-type -- Error boundaries inherently return different content based on error state render(): ReactNode { const { hasError, copiedReport, error, errorInfo } = this.state; const { children, fallback, labels } = this.props; if (hasError) { if (fallback) { return fallback; } return (

{labels?.title}

{labels?.description}

{error && (

{error.message}

{errorInfo?.componentStack && (
{labels?.componentStack}
                    {errorInfo.componentStack}
                  
)}
)}

{labels?.diagnosticsNotice}

); } return children; } } export function ErrorBoundary(props: Omit): React.JSX.Element { const { t } = useAppTranslation('common'); return ( ); }