A render error in one viewer or gate widget unmounted the whole React root (white screen). Add a reusable ErrorBoundary (class component, no new dep) with resetKeys + an on-brand fallback, and wire it at two surfaces: - WidgetHost: isolates the gate widget (reset on descriptor id) so a malformed gate payload no longer blanks the conversation - SessionDocumentsPanel: wraps each document (reset on doc key/content) so one crashing viewer degrades only its section; siblings and the panel survive The observed crash: a schema-linking doc that parses but lacks `candidates` makes SchemaLinkingViewer throw. Verified live via Playwright against the mock backend. TDD throughout; tsc clean, 123/123 vitest (+8 new tests). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
44 lines
1.6 KiB
TypeScript
44 lines
1.6 KiB
TypeScript
import { useSessionStore } from "../store/sessionStore";
|
|
import { resolve } from "../widgets";
|
|
import { postResponse } from "../api/sessions";
|
|
import type { UiResponse } from "../api/types";
|
|
import { ErrorBoundary } from "../components/ErrorBoundary";
|
|
|
|
export function WidgetHost({ sessionId }: { sessionId: string | null }) {
|
|
const pending = useSessionStore((s) => s.pendingWidget);
|
|
const clearPending = useSessionStore((s) => s.clearPending);
|
|
const pushToast = useSessionStore((s) => s.pushToast);
|
|
const setLastUserEntry = useSessionStore((s) => s.setLastUserEntry);
|
|
if (!pending) return null;
|
|
const Renderer = resolve(pending.widget);
|
|
const onRespond = async (r: UiResponse) => {
|
|
if (sessionId) {
|
|
try {
|
|
await postResponse(sessionId, r);
|
|
} catch (err) {
|
|
// Surface feedback and keep the widget pending so the user can retry.
|
|
pushToast({
|
|
level: "error",
|
|
text:
|
|
err instanceof Error
|
|
? `Failed to send response: ${err.message}`
|
|
: "Failed to send response.",
|
|
});
|
|
return;
|
|
}
|
|
}
|
|
setLastUserEntry({
|
|
kind: "choice",
|
|
text: r.text ?? r.choices?.join(", ") ?? r.decision?.type ?? r.control ?? "(choice)",
|
|
});
|
|
clearPending();
|
|
};
|
|
// A malformed gate payload must not white-screen the whole app: isolate the
|
|
// widget render. resetKeys on the descriptor id so the next gate starts clean.
|
|
return (
|
|
<ErrorBoundary resetKeys={[pending.id]} label="step">
|
|
<Renderer descriptor={pending} onRespond={onRespond} />
|
|
</ErrorBoundary>
|
|
);
|
|
}
|