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.
65 lines
2.1 KiB
TypeScript
65 lines
2.1 KiB
TypeScript
import type { ReactElement } from "react";
|
|
import { useQuery } from "@tanstack/react-query";
|
|
import { getSessionDocuments } from "../api/sessions";
|
|
import type { SessionDocument } from "../api/types";
|
|
import { ArtifactView } from "../viewers/ArtifactView";
|
|
import { MarkdownView } from "../viewers/MarkdownView";
|
|
import { resolveFileArtifact } from "./gateArtifact";
|
|
|
|
type ArtifactData = { kind: string; data?: unknown; content?: unknown; [k: string]: unknown };
|
|
|
|
/**
|
|
* Body of an artifact-review gate. Most gates embed their content and render via
|
|
* ArtifactView. A phase-completion gate, however, may carry only a file reference
|
|
* (e.g. `{ file: "question.md" }`): in that case we fetch the referenced session
|
|
* document and render its content in full, with the session-relative path as a
|
|
* quiet trailing line. Anything we cannot resolve falls back to ArtifactView, so
|
|
* no existing gate regresses.
|
|
*/
|
|
export function GateArtifactBody({
|
|
artifact,
|
|
sessionId,
|
|
}: {
|
|
artifact: ArtifactData;
|
|
sessionId?: string;
|
|
}): ReactElement {
|
|
const ref = resolveFileArtifact(artifact);
|
|
if (!sessionId || !ref) return <ArtifactView artifact={artifact} />;
|
|
return (
|
|
<ResolvedDoc
|
|
sessionId={sessionId}
|
|
docKey={ref.docKey}
|
|
filename={ref.filename}
|
|
fallback={<ArtifactView artifact={artifact} />}
|
|
/>
|
|
);
|
|
}
|
|
|
|
function ResolvedDoc({
|
|
sessionId,
|
|
docKey,
|
|
filename,
|
|
fallback,
|
|
}: {
|
|
sessionId: string;
|
|
docKey: string;
|
|
filename: string;
|
|
fallback: ReactElement;
|
|
}): ReactElement {
|
|
const { data: docs, isLoading, isError } = useQuery<SessionDocument[]>({
|
|
queryKey: ["session-documents", sessionId],
|
|
queryFn: () => getSessionDocuments(sessionId),
|
|
});
|
|
if (isLoading) return <p className="text-sm text-muted-foreground">Loading…</p>;
|
|
const doc = docs?.find((d) => d.key === docKey);
|
|
if (isError || !doc || doc.format !== "markdown") return fallback;
|
|
return (
|
|
<div className="flex flex-col gap-3">
|
|
<MarkdownView source={doc.content} />
|
|
<p className="border-t border-border/50 pt-2 text-right text-xs italic text-muted-foreground">
|
|
sessions/{sessionId}/{filename}
|
|
</p>
|
|
</div>
|
|
);
|
|
}
|