TypeScript · 2399 bytes Raw Blame History
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 }