diff --git a/src/components/layout/AppShell.tsx b/src/components/layout/AppShell.tsx index b1663b04e..f41c4d7a6 100644 --- a/src/components/layout/AppShell.tsx +++ b/src/components/layout/AppShell.tsx @@ -11,6 +11,7 @@ import { UpdateDialog } from "./UpdateDialog"; import { DocPreview } from "./DocPreview"; import { PanelContext, type PanelContent, type PreviewViewMode } from "@/hooks/usePanel"; import { UpdateContext, type UpdateInfo } from "@/hooks/useUpdate"; +import { ErrorBoundary } from "./ErrorBoundary"; const CHATLIST_MIN = 180; const CHATLIST_MAX = 400; @@ -253,7 +254,9 @@ export function AppShell({ children }: { children: React.ReactNode }) { hasUpdate={updateInfo?.updateAvailable ?? false} skipPermissionsActive={skipPermissionsActive} /> - + + + {chatListOpen && ( )} @@ -263,24 +266,32 @@ export function AppShell({ children }: { children: React.ReactNode }) { className="h-11 w-full shrink-0" style={{ WebkitAppRegion: 'drag' } as React.CSSProperties} /> -
{children}
+
+ {children} +
{isChatDetailRoute && previewFile && ( )} {isChatDetailRoute && previewFile && ( - setPreviewFile(null)} - width={docPreviewWidth} - /> + + setPreviewFile(null)} + width={docPreviewWidth} + /> + )} {isChatDetailRoute && panelOpen && ( )} - {isChatDetailRoute && } + {isChatDetailRoute && ( + + + + )} diff --git a/src/components/layout/ErrorBoundary.tsx b/src/components/layout/ErrorBoundary.tsx new file mode 100644 index 000000000..651a83788 --- /dev/null +++ b/src/components/layout/ErrorBoundary.tsx @@ -0,0 +1,134 @@ +"use client"; + +import React from "react"; + +interface ErrorBoundaryProps { + children: React.ReactNode; + fallback?: React.ReactNode; +} + +interface ErrorBoundaryState { + hasError: boolean; + error: Error | null; + showDetails: boolean; +} + +export class ErrorBoundary extends React.Component< + ErrorBoundaryProps, + ErrorBoundaryState +> { + constructor(props: ErrorBoundaryProps) { + super(props); + this.state = { hasError: false, error: null, showDetails: false }; + } + + static getDerivedStateFromError(error: Error): Partial { + return { hasError: true, error }; + } + + componentDidCatch(error: Error, errorInfo: React.ErrorInfo) { + console.error("[ErrorBoundary] Uncaught error:", error); + console.error("[ErrorBoundary] Component stack:", errorInfo.componentStack); + } + + handleReset = () => { + this.setState({ hasError: false, error: null, showDetails: false }); + }; + + handleReload = () => { + window.location.reload(); + }; + + render() { + if (this.state.hasError) { + if (this.props.fallback) { + return this.props.fallback; + } + + return ( +
+
+ {/* Error icon */} +
+ + + + + +
+ +

+ Something went wrong +

+

+ An unexpected error occurred. You can try again or reload the app. +

+ + {/* Expandable error details */} + {this.state.error && ( + + )} + {this.state.showDetails && this.state.error && ( +
+                {this.state.error.message}
+                {this.state.error.stack && `\n\n${this.state.error.stack}`}
+              
+ )} + + {/* Action buttons */} +
+ + +
+
+
+ ); + } + + return this.props.children; + } +} + +/** Convenience HOC to wrap any component with an ErrorBoundary */ +export function withErrorBoundary

( + Component: React.ComponentType

, + fallback?: React.ReactNode +) { + const displayName = Component.displayName || Component.name || "Component"; + + const Wrapped = (props: P) => ( + + + + ); + + Wrapped.displayName = `withErrorBoundary(${displayName})`; + return Wrapped; +}