Skip to content

Commit cd807ef

Browse files
authored
Merge pull request #49 from cyxer000/feat/error-boundary
Error Boundary for crash isolation. Minor fixes (i18n, icon, h-5 conflict) will follow.
2 parents 791baff + 11ece8d commit cd807ef

File tree

2 files changed

+155
-10
lines changed

2 files changed

+155
-10
lines changed

src/components/layout/AppShell.tsx

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { UpdateDialog } from "./UpdateDialog";
1111
import { DocPreview } from "./DocPreview";
1212
import { PanelContext, type PanelContent, type PreviewViewMode } from "@/hooks/usePanel";
1313
import { UpdateContext, type UpdateInfo } from "@/hooks/useUpdate";
14+
import { ErrorBoundary } from "./ErrorBoundary";
1415

1516
const CHATLIST_MIN = 180;
1617
const CHATLIST_MAX = 400;
@@ -253,7 +254,9 @@ export function AppShell({ children }: { children: React.ReactNode }) {
253254
hasUpdate={updateInfo?.updateAvailable ?? false}
254255
skipPermissionsActive={skipPermissionsActive}
255256
/>
256-
<ChatListPanel open={chatListOpen} width={chatListWidth} />
257+
<ErrorBoundary>
258+
<ChatListPanel open={chatListOpen} width={chatListWidth} />
259+
</ErrorBoundary>
257260
{chatListOpen && (
258261
<ResizeHandle side="left" onResize={handleChatListResize} onResizeEnd={handleChatListResizeEnd} />
259262
)}
@@ -263,24 +266,32 @@ export function AppShell({ children }: { children: React.ReactNode }) {
263266
className="h-5 w-full shrink-0"
264267
style={{ WebkitAppRegion: 'drag' } as React.CSSProperties}
265268
/>
266-
<main className="relative flex-1 overflow-hidden">{children}</main>
269+
<main className="relative flex-1 overflow-hidden">
270+
<ErrorBoundary>{children}</ErrorBoundary>
271+
</main>
267272
</div>
268273
{isChatDetailRoute && previewFile && (
269274
<ResizeHandle side="right" onResize={handleDocPreviewResize} onResizeEnd={handleDocPreviewResizeEnd} />
270275
)}
271276
{isChatDetailRoute && previewFile && (
272-
<DocPreview
273-
filePath={previewFile}
274-
viewMode={previewViewMode}
275-
onViewModeChange={setPreviewViewMode}
276-
onClose={() => setPreviewFile(null)}
277-
width={docPreviewWidth}
278-
/>
277+
<ErrorBoundary>
278+
<DocPreview
279+
filePath={previewFile}
280+
viewMode={previewViewMode}
281+
onViewModeChange={setPreviewViewMode}
282+
onClose={() => setPreviewFile(null)}
283+
width={docPreviewWidth}
284+
/>
285+
</ErrorBoundary>
279286
)}
280287
{isChatDetailRoute && panelOpen && (
281288
<ResizeHandle side="right" onResize={handleRightPanelResize} onResizeEnd={handleRightPanelResizeEnd} />
282289
)}
283-
{isChatDetailRoute && <RightPanel width={rightPanelWidth} />}
290+
{isChatDetailRoute && (
291+
<ErrorBoundary>
292+
<RightPanel width={rightPanelWidth} />
293+
</ErrorBoundary>
294+
)}
284295
</div>
285296
<UpdateDialog />
286297
</TooltipProvider>
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
"use client";
2+
3+
import React from "react";
4+
5+
interface ErrorBoundaryProps {
6+
children: React.ReactNode;
7+
fallback?: React.ReactNode;
8+
}
9+
10+
interface ErrorBoundaryState {
11+
hasError: boolean;
12+
error: Error | null;
13+
showDetails: boolean;
14+
}
15+
16+
export class ErrorBoundary extends React.Component<
17+
ErrorBoundaryProps,
18+
ErrorBoundaryState
19+
> {
20+
constructor(props: ErrorBoundaryProps) {
21+
super(props);
22+
this.state = { hasError: false, error: null, showDetails: false };
23+
}
24+
25+
static getDerivedStateFromError(error: Error): Partial<ErrorBoundaryState> {
26+
return { hasError: true, error };
27+
}
28+
29+
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
30+
console.error("[ErrorBoundary] Uncaught error:", error);
31+
console.error("[ErrorBoundary] Component stack:", errorInfo.componentStack);
32+
}
33+
34+
handleReset = () => {
35+
this.setState({ hasError: false, error: null, showDetails: false });
36+
};
37+
38+
handleReload = () => {
39+
window.location.reload();
40+
};
41+
42+
render() {
43+
if (this.state.hasError) {
44+
if (this.props.fallback) {
45+
return this.props.fallback;
46+
}
47+
48+
return (
49+
<div className="flex h-full w-full items-center justify-center bg-background p-8">
50+
<div className="flex max-w-md flex-col items-center gap-4 text-center">
51+
{/* Error icon */}
52+
<div className="flex h-12 w-12 items-center justify-center rounded-full bg-destructive/10 text-destructive">
53+
<svg
54+
xmlns="http://www.w3.org/2000/svg"
55+
width="24"
56+
height="24"
57+
viewBox="0 0 24 24"
58+
fill="none"
59+
stroke="currentColor"
60+
strokeWidth="2"
61+
strokeLinecap="round"
62+
strokeLinejoin="round"
63+
>
64+
<circle cx="12" cy="12" r="10" />
65+
<line x1="12" y1="8" x2="12" y2="12" />
66+
<line x1="12" y1="16" x2="12.01" y2="16" />
67+
</svg>
68+
</div>
69+
70+
<h2 className="text-lg font-semibold text-foreground">
71+
Something went wrong
72+
</h2>
73+
<p className="text-sm text-muted-foreground">
74+
An unexpected error occurred. You can try again or reload the app.
75+
</p>
76+
77+
{/* Expandable error details */}
78+
{this.state.error && (
79+
<button
80+
onClick={() =>
81+
this.setState((s) => ({ showDetails: !s.showDetails }))
82+
}
83+
className="text-xs text-muted-foreground underline hover:text-foreground"
84+
>
85+
{this.state.showDetails ? "Hide details" : "Show details"}
86+
</button>
87+
)}
88+
{this.state.showDetails && this.state.error && (
89+
<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">
90+
{this.state.error.message}
91+
{this.state.error.stack && `\n\n${this.state.error.stack}`}
92+
</pre>
93+
)}
94+
95+
{/* Action buttons */}
96+
<div className="flex gap-2">
97+
<button
98+
onClick={this.handleReset}
99+
className="rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
100+
>
101+
Try Again
102+
</button>
103+
<button
104+
onClick={this.handleReload}
105+
className="rounded-md border border-border bg-background px-4 py-2 text-sm font-medium text-foreground hover:bg-muted"
106+
>
107+
Reload App
108+
</button>
109+
</div>
110+
</div>
111+
</div>
112+
);
113+
}
114+
115+
return this.props.children;
116+
}
117+
}
118+
119+
/** Convenience HOC to wrap any component with an ErrorBoundary */
120+
export function withErrorBoundary<P extends object>(
121+
Component: React.ComponentType<P>,
122+
fallback?: React.ReactNode
123+
) {
124+
const displayName = Component.displayName || Component.name || "Component";
125+
126+
const Wrapped = (props: P) => (
127+
<ErrorBoundary fallback={fallback}>
128+
<Component {...props} />
129+
</ErrorBoundary>
130+
);
131+
132+
Wrapped.displayName = `withErrorBoundary(${displayName})`;
133+
return Wrapped;
134+
}

0 commit comments

Comments
 (0)