feat(frontend): richer gate bodies + replay-friendly session exit
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.
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
import { parseIntroBlocks, splitLead } from "./introFormat";
|
||||
|
||||
test("splits paragraphs and bullet lists on blank lines", () => {
|
||||
const blocks = parseIntroBlocks(
|
||||
"Domanda: «X».\n\nEcco le famiglie:\n• uno → A\n• due → B\n\nConclusione."
|
||||
);
|
||||
expect(blocks).toEqual([
|
||||
{ kind: "para", text: "Domanda: «X»." },
|
||||
{ kind: "para", text: "Ecco le famiglie:" },
|
||||
{ kind: "list", items: ["uno → A", "due → B"] },
|
||||
{ kind: "para", text: "Conclusione." },
|
||||
]);
|
||||
});
|
||||
|
||||
test("joins soft-wrapped paragraph lines into one flowing paragraph", () => {
|
||||
const blocks = parseIntroBlocks("prima riga\nseconda riga");
|
||||
expect(blocks).toEqual([{ kind: "para", text: "prima riga seconda riga" }]);
|
||||
});
|
||||
|
||||
test("accepts -, * and • as bullet markers", () => {
|
||||
const blocks = parseIntroBlocks("- a\n* b\n• c");
|
||||
expect(blocks).toEqual([{ kind: "list", items: ["a", "b", "c"] }]);
|
||||
});
|
||||
|
||||
test("bolds the lead term up to an arrow separator", () => {
|
||||
expect(splitLead("fact_impianto → IMPIANTO INIZIALE (dettaglio)")).toEqual({
|
||||
lead: "fact_impianto",
|
||||
rest: " → IMPIANTO INIZIALE (dettaglio)",
|
||||
});
|
||||
});
|
||||
|
||||
test("bolds the lead term up to a colon separator", () => {
|
||||
expect(splitLead("Nota: i controlli non sono impianto")).toEqual({
|
||||
lead: "Nota",
|
||||
rest: ": i controlli non sono impianto",
|
||||
});
|
||||
});
|
||||
|
||||
test("does not bold when there is no separator", () => {
|
||||
expect(splitLead("un punto elenco semplice")).toBeNull();
|
||||
});
|
||||
|
||||
test("does not bold a long leading clause", () => {
|
||||
expect(
|
||||
splitLead("questa è una frase lunga con molte parole prima del: due punti")
|
||||
).toBeNull();
|
||||
});
|
||||
Reference in New Issue
Block a user