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.
41 lines
1.7 KiB
TypeScript
41 lines
1.7 KiB
TypeScript
import type { WidgetProps } from "./types";
|
|
import { ReservedControls } from "./ReservedControls";
|
|
import { IntroBody } from "./IntroBody";
|
|
|
|
export function SelectWidget({ descriptor, onRespond }: WidgetProps) {
|
|
return (
|
|
<div className="space-y-4 rounded-xl border border-border/70 bg-card p-4 shadow-sm">
|
|
{(descriptor.title || descriptor.intro) && (
|
|
<div className="space-y-1.5">
|
|
{descriptor.title && (
|
|
<p className="text-[0.95rem] font-semibold leading-snug text-foreground">
|
|
{descriptor.title}
|
|
</p>
|
|
)}
|
|
{descriptor.intro && <IntroBody text={descriptor.intro} />}
|
|
</div>
|
|
)}
|
|
<div className="flex flex-col gap-2">
|
|
{descriptor.options?.map((o) => (
|
|
<button
|
|
key={o.id}
|
|
className="flex items-center justify-between gap-3 rounded-lg border border-border bg-card px-4 py-3 text-left text-sm text-foreground transition-colors hover:border-primary/40 hover:bg-accent focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40"
|
|
onClick={() => onRespond({ id: descriptor.id, kind: "select", choices: [o.id] })}
|
|
>
|
|
<span>{o.label}</span>
|
|
{o.recommended && (
|
|
<span className="shrink-0 rounded-md bg-[oklch(var(--success)/0.16)] px-2 py-0.5 text-[0.65rem] font-medium text-[oklch(0.5_0.13_165)]">
|
|
consigliato
|
|
</span>
|
|
)}
|
|
</button>
|
|
))}
|
|
</div>
|
|
<ReservedControls
|
|
reserved={descriptor.reserved}
|
|
onControl={(c, t) => onRespond({ id: descriptor.id, control: c, ...(t !== undefined ? { text: t } : {}) })}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|