tenseleyflow/claudex / ad9ef9d

Browse files

ui: error boundaries around sidebar + viewer panes

Authored by espadonne
SHA
ad9ef9d4de0f9cbdefedc22278d7a9ffe653d242
Parents
17f83dc
Tree
53c3e80

2 changed files

StatusFile+-
M src/App.tsx 7 2
A src/components/ErrorBoundary.tsx 76 0
src/App.tsxmodified
@@ -5,6 +5,7 @@ import {
55
   PanelResizeHandle,
66
 } from "react-resizable-panels";
77
 
8
+import { ErrorBoundary } from "@/components/ErrorBoundary";
89
 import { ProjectsPane } from "@/components/ProjectsPane";
910
 import { ViewerPane } from "@/components/ViewerPane";
1011
 import { relativeTime, tildeify } from "@/lib/format";
@@ -34,11 +35,15 @@ export default function App() {
3435
         className="flex-1"
3536
       >
3637
         <Panel defaultSize={28} minSize={18} maxSize={50}>
37
-          <ProjectsPane />
38
+          <ErrorBoundary label="sidebar">
39
+            <ProjectsPane />
40
+          </ErrorBoundary>
3841
         </Panel>
3942
         <VerticalHandle />
4043
         <Panel defaultSize={72} minSize={30}>
41
-          <ViewerPane />
44
+          <ErrorBoundary label="viewer">
45
+            <ViewerPane />
46
+          </ErrorBoundary>
4247
         </Panel>
4348
       </PanelGroup>
4449
     </div>
src/components/ErrorBoundary.tsxadded
@@ -0,0 +1,76 @@
1
+// Minimal React error boundary. Wrap the viewer pane so a render
2
+// crash (e.g. inside xterm.js bootstrap) doesn't take down the
3
+// whole shell — the sidebar stays visible and we get a readable
4
+// error card instead of a black screen.
5
+//
6
+// All caught errors are forwarded to Rust's tracing via the debug
7
+// bridge, so they land in `~/Library/Logs/claudex/claudex.log.<date>`
8
+// even when the user never opens devtools.
9
+
10
+import { Component, type ErrorInfo, type ReactNode } from "react";
11
+
12
+import { logFrontend } from "@/lib/ipc/client";
13
+
14
+interface Props {
15
+  /** Short label used in the fallback UI header and the log `source`
16
+   *  tag so we can tell which boundary fired from the log file. */
17
+  label: string;
18
+  children: ReactNode;
19
+}
20
+
21
+interface State {
22
+  error: Error | null;
23
+}
24
+
25
+export class ErrorBoundary extends Component<Props, State> {
26
+  state: State = { error: null };
27
+
28
+  static getDerivedStateFromError(error: Error): State {
29
+    return { error };
30
+  }
31
+
32
+  componentDidCatch(error: Error, info: ErrorInfo) {
33
+    void logFrontend(
34
+      "error",
35
+      `ErrorBoundary:${this.props.label}`,
36
+      error.message || String(error),
37
+      `${error.stack ?? ""}\n--- componentStack ---${info.componentStack ?? ""}`,
38
+    );
39
+  }
40
+
41
+  reset = () => {
42
+    this.setState({ error: null });
43
+  };
44
+
45
+  render() {
46
+    if (this.state.error) {
47
+      return (
48
+        <div className="flex h-full flex-col items-start gap-3 overflow-auto p-4 text-xs">
49
+          <div className="text-sm font-semibold text-red-400">
50
+            {this.props.label} crashed
51
+          </div>
52
+          <div className="font-mono text-fg-1">
53
+            {this.state.error.message || String(this.state.error)}
54
+          </div>
55
+          <pre className="max-h-64 w-full overflow-auto rounded border border-red-900/40 bg-red-950/20 p-2 font-mono text-[11px] text-red-300">
56
+            {this.state.error.stack ?? "no stack"}
57
+          </pre>
58
+          <div className="text-fg-3">
59
+            full details logged to{" "}
60
+            <span className="font-mono">
61
+              ~/Library/Logs/claudex/claudex.log
62
+            </span>
63
+          </div>
64
+          <button
65
+            type="button"
66
+            onClick={this.reset}
67
+            className="rounded border border-border bg-bg-2 px-2 py-1 text-fg-2 hover:bg-bg-3"
68
+          >
69
+            reset pane
70
+          </button>
71
+        </div>
72
+      );
73
+    }
74
+    return this.props.children;
75
+  }
76
+}