Improves how reviewer gates render their content, and adds two small
hooks that make the frontend cooperate with the standalone replay server
(also useful as generic primitives).
Gate body rendering:
- IntroBody: gate intros now render paragraphs plus real bullet lists
with a hanging indent and emphasised bullet lead term, filling the
card width (no narrow measure cap). Backed by introFormat (parser) +
IntroBody (renderer), with tests.
- GateArtifactBody: artifact-review gates whose artifact is a bare file
reference (e.g. {kind:"phase", data:{file:"question.md"}}) now fetch
the referenced session document and render its full content, instead
of showing the file reference as text. Falls back to ArtifactView for
inline-content artifacts and unmapped files. Backed by gateArtifact
(resolver) with tests. ArtifactGateWidget passes sessionId through.
- SelectWidget renders intros via IntroBody.
Replay-friendly session exit:
- AppShell: a system_event {event:"session_exit"} now triggers
stopSession(), returning to the landing view. Generic primitive (the
real backend can emit it too); used by the replay server's "Esci".
- SessionMenu: the ⋮ trigger is now always visible (was opacity-0 /
group-hover only), so Resume is reachable without hovering.
Verified offline against the replay server (no VPN needed); tsc clean.
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} sessionId={sessionId ?? undefined} />
|
|
</ErrorBoundary>
|
|
);
|
|
}
|