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.
68 lines
3.1 KiB
TypeScript
68 lines
3.1 KiB
TypeScript
import { Menu } from "@base-ui/react/menu";
|
||
import { MoreVertical } from "lucide-react";
|
||
import type { SessionSummary } from "../api/types";
|
||
|
||
interface Props {
|
||
session: SessionSummary;
|
||
groups: string[];
|
||
onResume: () => void;
|
||
onView: () => void;
|
||
onRename: () => void;
|
||
onMove: (group: string) => void;
|
||
onNewGroup: () => void;
|
||
onArchiveToggle: () => void;
|
||
onDelete: () => void;
|
||
}
|
||
|
||
const itemCls =
|
||
"flex w-full cursor-default select-none items-center rounded-md px-2 py-1.5 text-sm outline-none data-highlighted:bg-accent data-highlighted:text-accent-foreground";
|
||
|
||
export function SessionMenu({ session, groups, onResume, onView, onRename, onMove, onNewGroup, onArchiveToggle, onDelete }: Props) {
|
||
// Resume re-enters the workflow; only meaningful while the session is still live
|
||
// (the backend returns 409 for finalized/archived, matching this guard).
|
||
const canResume = session.status !== "finalized" && !session.archived;
|
||
return (
|
||
<Menu.Root>
|
||
<Menu.Trigger
|
||
aria-label="Session actions"
|
||
className="rounded-md p-1 text-muted-foreground transition-opacity hover:bg-accent data-popup-open:opacity-100"
|
||
onClick={(e) => e.stopPropagation()}
|
||
>
|
||
<MoreVertical className="size-4" />
|
||
</Menu.Trigger>
|
||
<Menu.Portal>
|
||
<Menu.Positioner side="bottom" align="end" sideOffset={4}>
|
||
<Menu.Popup className="z-50 min-w-44 rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-none">
|
||
{canResume && (
|
||
<Menu.Item className={itemCls} onClick={onResume}>Resume</Menu.Item>
|
||
)}
|
||
<Menu.Item className={itemCls} onClick={onView}>Split view</Menu.Item>
|
||
<Menu.Item className={itemCls} onClick={onRename}>Rename</Menu.Item>
|
||
<Menu.SubmenuRoot>
|
||
<Menu.SubmenuTrigger className={itemCls}>Move to group ›</Menu.SubmenuTrigger>
|
||
<Menu.Portal>
|
||
<Menu.Positioner side="right" align="start">
|
||
<Menu.Popup className="z-50 min-w-44 rounded-lg bg-popover p-1 text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-none">
|
||
{groups.filter((g) => g !== session.group).map((g) => (
|
||
<Menu.Item key={g} className={itemCls} onClick={() => onMove(g)}>{g}</Menu.Item>
|
||
))}
|
||
{session.group && (
|
||
<Menu.Item className={itemCls} onClick={() => onMove("")}>No group</Menu.Item>
|
||
)}
|
||
<Menu.Item className={itemCls} onClick={onNewGroup}>New group…</Menu.Item>
|
||
</Menu.Popup>
|
||
</Menu.Positioner>
|
||
</Menu.Portal>
|
||
</Menu.SubmenuRoot>
|
||
<Menu.Item className={itemCls} onClick={onArchiveToggle}>
|
||
{session.archived ? "Restore" : "Archive"}
|
||
</Menu.Item>
|
||
<Menu.Separator className="my-1 h-px bg-border" />
|
||
<Menu.Item className={`${itemCls} text-destructive`} onClick={onDelete}>Delete</Menu.Item>
|
||
</Menu.Popup>
|
||
</Menu.Positioner>
|
||
</Menu.Portal>
|
||
</Menu.Root>
|
||
);
|
||
}
|