Merge pull request #49 from cyxer000/feat/error-boundary
Error Boundary for crash isolation. Minor fixes (i18n, icon, h-5 conflict) will follow.
This commit is contained in:
2 files changed
+155
-10
No files matched your search
@@ -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}
|
||||
/>
|
||||
<ChatListPanel open={chatListOpen} width={chatListWidth} />
|
||||
<ErrorBoundary>
|
||||
<ChatListPanel open={chatListOpen} width={chatListWidth} />
|
||||
</ErrorBoundary>
|
||||
{chatListOpen && (
|
||||
<ResizeHandle side="left" onResize={handleChatListResize} onResizeEnd={handleChatListResizeEnd} />
|
||||
)}
|
||||
@@ -263,24 +266,32 @@ export function AppShell({ children }: { children: React.ReactNode }) {
|
||||
className="h-5 w-full shrink-0"
|
||||
style={{ WebkitAppRegion: 'drag' } as React.CSSProperties}
|
||||
/>
|
||||
<main className="relative flex-1 overflow-hidden">{children}</main>
|
||||
<main className="relative flex-1 overflow-hidden">
|
||||
<ErrorBoundary>{children}</ErrorBoundary>
|
||||
</main>
|
||||
</div>
|
||||
{isChatDetailRoute && previewFile && (
|
||||
<ResizeHandle side="right" onResize={handleDocPreviewResize} onResizeEnd={handleDocPreviewResizeEnd} />
|
||||
)}
|
||||
{isChatDetailRoute && previewFile && (
|
||||
<DocPreview
|
||||
filePath={previewFile}
|
||||
viewMode={previewViewMode}
|
||||
onViewModeChange={setPreviewViewMode}
|
||||
onClose={() => setPreviewFile(null)}
|
||||
width={docPreviewWidth}
|
||||
/>
|
||||
<ErrorBoundary>
|
||||
<DocPreview
|
||||
filePath={previewFile}
|
||||
viewMode={previewViewMode}
|
||||
onViewModeChange={setPreviewViewMode}
|
||||
onClose={() => setPreviewFile(null)}
|
||||
width={docPreviewWidth}
|
||||
/>
|
||||
</ErrorBoundary>
|
||||
)}
|
||||
{isChatDetailRoute && panelOpen && (
|
||||
<ResizeHandle side="right" onResize={handleRightPanelResize} onResizeEnd={handleRightPanelResizeEnd} />
|
||||
)}
|
||||
{isChatDetailRoute && <RightPanel width={rightPanelWidth} />}
|
||||
{isChatDetailRoute && (
|
||||
<ErrorBoundary>
|
||||
<RightPanel width={rightPanelWidth} />
|
||||
</ErrorBoundary>
|
||||
)}
|
||||
</div>
|
||||
<UpdateDialog />
|
||||
</TooltipProvider>
|
||||
|
||||
@@ -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<ErrorBoundaryState> {
|
||||
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 (
|
||||
<div className="flex h-full w-full items-center justify-center bg-background p-8">
|
||||
<div className="flex max-w-md flex-col items-center gap-4 text-center">
|
||||
{/* Error icon */}
|
||||
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-destructive/10 text-destructive">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="24"
|
||||
height="24"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<line x1="12" y1="8" x2="12" y2="12" />
|
||||
<line x1="12" y1="16" x2="12.01" y2="16" />
|
||||
</svg>
|
||||
</div>
|
||||
|
||||
<h2 className="text-lg font-semibold text-foreground">
|
||||
Something went wrong
|
||||
</h2>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
An unexpected error occurred. You can try again or reload the app.
|
||||
</p>
|
||||
|
||||
{/* Expandable error details */}
|
||||
{this.state.error && (
|
||||
<button
|
||||
onClick={() =>
|
||||
this.setState((s) => ({ showDetails: !s.showDetails }))
|
||||
}
|
||||
className="text-xs text-muted-foreground underline hover:text-foreground"
|
||||
>
|
||||
{this.state.showDetails ? "Hide details" : "Show details"}
|
||||
</button>
|
||||
)}
|
||||
{this.state.showDetails && this.state.error && (
|
||||
<pre className="max-h-40 w-full overflow-auto rounded-md border border-border/50 bg-muted/30 p-3 text-left text-xs text-muted-foreground">
|
||||
{this.state.error.message}
|
||||
{this.state.error.stack && `\n\n${this.state.error.stack}`}
|
||||
</pre>
|
||||
)}
|
||||
|
||||
{/* Action buttons */}
|
||||
<div className="flex gap-2">
|
||||
<button
|
||||
onClick={this.handleReset}
|
||||
className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
||||
>
|
||||
Try Again
|
||||
</button>
|
||||
<button
|
||||
onClick={this.handleReload}
|
||||
className="rounded-md border border-border bg-background px-4 py-2 text-sm font-medium text-foreground hover:bg-muted"
|
||||
>
|
||||
Reload App
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
/** Convenience HOC to wrap any component with an ErrorBoundary */
|
||||
export function withErrorBoundary<P extends object>(
|
||||
Component: React.ComponentType<P>,
|
||||
fallback?: React.ReactNode
|
||||
) {
|
||||
const displayName = Component.displayName || Component.name || "Component";
|
||||
|
||||
const Wrapped = (props: P) => (
|
||||
<ErrorBoundary fallback={fallback}>
|
||||
<Component {...props} />
|
||||
</ErrorBoundary>
|
||||
);
|
||||
|
||||
Wrapped.displayName = `withErrorBoundary(${displayName})`;
|
||||
return Wrapped;
|
||||
}
|
||||
Reference in new issue
Block a user