fix: clarify presentation demo proof surfaces

This commit is contained in:
lda
2026-07-09 21:15:15 +07:00 Verified
parent fe8e4cee40
commit 7ce81c8f23
11 changed files with 349 additions and 31 deletions
@@ -95,6 +95,8 @@ describe("DemoWorkflowScene", () => {
const contract = screen.getByLabelText("typed interrupt contract");
expect(contract).toHaveTextContent("issue_review");
expect(contract).toHaveTextContent("submitted / cancelled");
expect(contract).toHaveTextContent("lda.chat Thesis And Project Readiness Report");
expect(contract).toHaveTextContent("Prepare the defense walkthrough");
expect(contract).toHaveTextContent('"type": "object"');
expect(contract).toHaveTextContent("run_recorded_lda_report");
});
@@ -133,7 +135,7 @@ describe("DemoWorkflowScene", () => {
it("passes run proof into full graph beats", () => {
const { unmount } = renderBeat("graph");
expect(screen.getByLabelText("workflow graph proof")).toHaveTextContent("run_recorded_lda_report");
expect(screen.getByLabelText("workflow graph proof")).toHaveTextContent("5 workflow nodes");
expect(screen.getByLabelText("workflow graph proof")).toHaveTextContent("9 workflow nodes");
unmount();
renderBeat("output", "interrupt-evidence");
@@ -6,6 +6,7 @@ import {
projectInterruptContract,
projectOperationPresentation,
} from "./demo-workflow-model.js";
import { projectDemoRunFacts } from "./demo-run-facts.js";
import type { DemoApprovalActions } from "./demo-approval-actions.js";
import { DemoContinuityRail } from "./DemoContinuityRail.js";
import { DemoOutcomePanel } from "./DemoOutcomePanel.js";
@@ -71,12 +72,13 @@ export const DemoWorkflowScene = ({
const runProof = {
runId: runStart?.resultingIds.runId ?? null,
traceLabel: "5 workflow nodes",
traceLabel: "9 workflow nodes",
evidenceLabel: "JSON-RPC evidence",
};
const lens = demoBeatLensForBeat(beat.id);
const currentOperation = currentEvent ? projectOperationPresentation(currentEvent) : null;
const facts = projectDemoRunFacts(demo);
const showOutcomePanel = beat.id === "resume" || beat.id === "output" || beat.id === "trace";
const showExpandedOperation = beat.id === "operation" || beat.id === "resume" || beat.id === "trace";
@@ -141,6 +143,7 @@ export const DemoWorkflowScene = ({
mode={contractMode}
hero={layout === "approval"}
approvalActions={approvalActions}
interrupt={facts.interrupt}
/>
)}
</div>
@@ -68,7 +68,7 @@ describe("GuidedProductMoment", () => {
expect(screen.getByText("project-brief.md")).toBeInTheDocument();
expect(screen.getByText("issue-board.json")).toBeInTheDocument();
expect(screen.getByRole("group", { name: /operator resume decision/i })).toBeInTheDocument();
expect(screen.getByText("Output not created yet")).toBeInTheDocument();
expect(screen.queryByText("Output not created yet")).not.toBeInTheDocument();
});
it("makes resume operation proof primary on resume beat", () => {
@@ -68,7 +68,6 @@ export const GuidedProductMoment = ({
terminalOutcome={approvalActions?.state === "submitted" ? "submitted" :
approvalActions?.state === "cancelled" ? "cancelled" : undefined}
/>
<RunOutputFacts facts={facts} />
</div>
) : null}
{moment === "resume" && runResume ? (
@@ -1,6 +1,7 @@
import { m } from "motion/react";
import type { InterruptContractPresentation } from "./demo-workflow-model.js";
import type { DemoApprovalActions } from "./demo-approval-actions.js";
import type { RunFactsInterrupt } from "./demo-run-facts.js";
import { SchemaApprovalSurface } from "./approval/SchemaApprovalSurface.js";
import { formatJson } from "./format.js";
@@ -9,6 +10,7 @@ type InterruptContractPreviewProps = {
readonly mode: "preview" | "approval";
readonly hero?: boolean;
readonly approvalActions?: DemoApprovalActions | undefined;
readonly interrupt?: RunFactsInterrupt | undefined;
};
const titleForKind = (kind: string): string => `${kind.replaceAll("_", " ")} resume`;
@@ -18,6 +20,7 @@ export const InterruptContractPreview = ({
mode,
hero = false,
approvalActions,
interrupt,
}: InterruptContractPreviewProps) => (
<m.aside
className="interrupt-contract-preview"
@@ -56,9 +59,30 @@ export const InterruptContractPreview = ({
onCancel={approvalActions?.canCancel ? () => void approvalActions.cancel() : undefined}
/>
) : (
<div className="interrupt-contract-preview__schema">
<span>Resume schema</span>
<pre><code>{formatJson(contract.resumeSchema)}</code></pre>
<div className="interrupt-contract-preview__details">
{interrupt?.reportMarkdownPreview ? (
<section className="interrupt-contract-preview__payload">
<span>Interrupt payload</span>
<pre><code>{interrupt.reportMarkdownPreview}</code></pre>
</section>
) : null}
{interrupt && interrupt.proposedIssues.length > 0 ? (
<section className="interrupt-contract-preview__issues">
<span>Proposed issues</span>
<ul>
{interrupt.proposedIssues.map((issue) => (
<li key={issue.id}>
<strong>{issue.title}</strong>
<small>{issue.severity}</small>
</li>
))}
</ul>
</section>
) : null}
<section className="interrupt-contract-preview__schema">
<span>Resume schema</span>
<pre><code>{formatJson(contract.resumeSchema)}</code></pre>
</section>
</div>
)}
</m.aside>
@@ -205,7 +205,7 @@ describe("PresentationRoute", () => {
expect(screen.getByText("project-brief.md")).toBeInTheDocument();
expect(screen.getByText("issue-board.json")).toBeInTheDocument();
expect(screen.getByRole("group", { name: /operator resume decision/i })).toBeInTheDocument();
expect(screen.getByText("Output not created yet")).toBeInTheDocument();
expect(screen.queryByText("Output not created yet")).not.toBeInTheDocument();
});
it("chat run action advances the replay timeline when no live server is configured", async () => {
@@ -238,6 +238,27 @@ describe("PresentationRoute", () => {
expect(screen.getByLabelText("workflow.runs.resume operation")).toBeInTheDocument();
});
it("reopens approval controls after returning from submitted resume", async () => {
const user = userEvent.setup();
setReplayMode();
window.location.hash = "#scene/interrupt-evidence/approval";
const { PresentationRoute } = await import("./PresentationRoute.js");
render(<PresentationRoute />);
const submitButton = await screen.findByRole("button", { name: "Submit" });
await waitFor(() => expect(submitButton).toBeEnabled(), { timeout: 10000 });
await act(async () => {
await user.click(submitButton);
});
expect(window.location.hash).toBe("#scene/interrupt-evidence/resume");
window.location.hash = "#scene/interrupt-evidence/approval";
window.dispatchEvent(new HashChangeEvent("hashchange"));
expect(await screen.findByRole("button", { name: "Submit" })).toBeEnabled();
expect(screen.getByRole("button", { name: "Cancel" })).toBeEnabled();
});
it("cancels Scene 10 approval in replay without applying submitted evidence", async () => {
const user = userEvent.setup();
setReplayMode();
@@ -119,6 +119,10 @@ export const PresentationRoute = () => {
demo.primeReplayToStage(requirement.requiredStage);
}, [demo.state.mode, demo.primeReplayToStage, state.location]);
const isApprovalBeat = state.location.kind === "main"
&& state.location.sceneId === "interrupt-evidence"
&& state.location.beatId === "approval";
useEffect(() => {
dispatch({ type: "set_playback_mode", mode: demo.state.mode });
}, [demo.state.mode]);
@@ -173,10 +177,13 @@ export const PresentationRoute = () => {
}), [approvalState, demo.state.phase, demo.interruptPayload, handleSubmitApproval, handleCancelApproval]);
useEffect(() => {
if ((demo.state.phase === "ready" || demo.state.phase === "running") && approvalState === "ready") {
setApprovalState("ready");
}
}, [demo.state.phase, approvalState]);
if (!isApprovalBeat || demo.state.phase !== "review") return;
// The approval form is route-scoped presentation state. A presenter can
// submit, revisit the approval beat, and should see the decision form
// again because the replay is re-primed to the interrupt stage.
setApprovalState("ready");
}, [demo.state.phase, isApprovalBeat]);
const handleJump = useCallback(
(location: MainLocation) => dispatch({ type: "jump", location }),
@@ -73,6 +73,16 @@ describe("demo-run-facts", () => {
expect(facts.interrupt.reportMarkdownPreview).toContain("workflow substrate is ready");
});
it("projects interrupt payload from recorded events when transient state is absent", () => {
const facts = projectDemoRunFacts(controller({ interruptPayload: null }));
expect(facts.interrupt.proposedIssues[0]).toMatchObject({
id: "risk-1",
title: "Prepare the defense walkthrough",
});
expect(facts.interrupt.reportMarkdownPreview).toContain("workflow substrate is ready");
});
it("projects resume payload and output after resume", () => {
const recording = loadCanonicalDemoRecording();
const facts = projectDemoRunFacts(controller({
@@ -90,15 +90,21 @@ const readInterruptFacts = (
const runStart = findEvent(events, "run_start") as
| {
interpreted: {
interrupt?: { kind?: string; typed?: boolean; outcomes?: unknown };
interrupt?: {
kind?: string;
typed?: boolean;
outcomes?: unknown;
payload?: DemoTimelineController["interruptPayload"];
};
};
}
| undefined;
const interruptEvent = findEvent(events, "interrupt") as
| { interpreted: { outcomes?: unknown } }
| { interpreted: { outcomes?: unknown; payload?: DemoTimelineController["interruptPayload"] } }
| undefined;
const ri = runStart?.interpreted?.interrupt;
const payload = interruptPayload ?? interruptEvent?.interpreted?.payload ?? ri?.payload ?? null;
const outcomes = Array.isArray(interruptEvent?.interpreted?.outcomes)
? (interruptEvent!.interpreted.outcomes as ReadonlyArray<string>)
: Array.isArray(ri?.outcomes)
@@ -109,8 +115,8 @@ const readInterruptFacts = (
kind: typeof ri?.kind === "string" ? ri.kind : "unknown",
typed: ri?.typed === true,
outcomes,
proposedIssues: interruptPayload?.proposed_issues ?? [],
reportMarkdownPreview: interruptPayload?.report_markdown ?? "",
proposedIssues: payload?.proposed_issues ?? [],
reportMarkdownPreview: payload?.report_markdown ?? "",
};
};
@@ -1507,6 +1507,7 @@
border: 2px solid var(--accent-cyan);
border-radius: 10px;
background: var(--stage-surface);
box-sizing: border-box;
}
.interrupt-decision-form__header {
@@ -1672,16 +1673,169 @@
/* Factual Scene 10 layout grids */
.guided-product-moment__approval-grid {
display: grid;
grid-template-columns: 1fr 1.4fr 1fr;
gap: 1rem;
align-items: start;
grid-template-columns: minmax(13rem, 0.56fr) minmax(0, 1.44fr);
gap: 0.75rem;
align-items: stretch;
min-height: 0;
max-height: 100%;
overflow: hidden;
}
.guided-product-moment__approval-grid > *,
.guided-product-moment__resume-grid > *,
.guided-product-moment[data-moment="output"] > .guided-product-moment__primary > * {
min-height: 0;
}
.guided-product-moment[data-moment="approval"] .run-facts-card,
.guided-product-moment[data-moment="approval"] .interrupt-decision-form,
.guided-product-moment[data-moment="resume"] .run-facts-card,
.guided-product-moment[data-moment="output"] .run-facts-card {
scrollbar-width: none;
}
.guided-product-moment[data-moment="approval"] .run-facts-card::-webkit-scrollbar,
.guided-product-moment[data-moment="approval"] .interrupt-decision-form::-webkit-scrollbar,
.guided-product-moment[data-moment="resume"] .run-facts-card::-webkit-scrollbar,
.guided-product-moment[data-moment="output"] .run-facts-card::-webkit-scrollbar {
display: none;
}
.guided-product-moment[data-moment="approval"] .run-facts-card {
max-height: 100%;
overflow: auto;
padding: 0.75rem 0.85rem;
}
.guided-product-moment[data-moment="approval"] .run-facts-card h3 {
margin-bottom: 0.45rem;
font-size: 0.78rem;
}
.guided-product-moment[data-moment="approval"] .run-facts-dl {
gap: 0.2rem 0.55rem;
font-size: 0.76rem;
line-height: 1.35;
}
.guided-product-moment[data-moment="approval"] .run-facts-list li {
padding: 0.18rem 0;
font-size: 0.76rem;
}
.guided-product-moment[data-moment="approval"] .interrupt-decision-form {
display: grid;
max-height: 100%;
overflow: auto;
padding: 0.75rem 0.9rem;
}
.guided-product-moment[data-moment="approval"] .interrupt-decision-form__header {
margin-bottom: 0.45rem;
}
.guided-product-moment[data-moment="approval"] .interrupt-decision-form__kind {
font-size: 0.92rem;
}
.guided-product-moment[data-moment="approval"] .interrupt-decision-form__run-id {
font-size: 0.7rem;
}
.guided-product-moment[data-moment="approval"] .interrupt-decision-form__meta {
grid-template-columns: auto minmax(0, 1fr) auto minmax(0, 1fr);
gap: 0.15rem 0.45rem;
margin-bottom: 0.45rem;
font-size: 0.74rem;
}
.guided-product-moment[data-moment="approval"] .interrupt-decision-form__report-preview {
max-height: 11.2rem;
margin-bottom: 0.45rem;
padding: 0.65rem 0.75rem;
font-size: 0.76rem;
line-height: 1.45;
overflow: auto;
scrollbar-width: none;
}
.guided-product-moment[data-moment="approval"] .interrupt-decision-form__report-preview::-webkit-scrollbar {
display: none;
}
.guided-product-moment[data-moment="approval"] .interrupt-decision-form__issues {
margin-bottom: 0.45rem;
padding: 0.45rem 0.55rem;
}
.guided-product-moment[data-moment="approval"] .interrupt-decision-form__issues legend {
font-size: 0.72rem;
}
.guided-product-moment[data-moment="approval"] .interrupt-decision-form__issue-row {
gap: 0.45rem;
padding: 0.25rem 0;
font-size: 0.76rem;
}
.guided-product-moment[data-moment="approval"] .interrupt-decision-form__issue-row input[type="checkbox"] {
width: 0.95rem;
height: 0.95rem;
}
.guided-product-moment[data-moment="approval"] .interrupt-decision-form__issue-severity {
font-size: 0.62rem;
}
.guided-product-moment[data-moment="approval"] .interrupt-decision-form__comment-label {
margin-bottom: 0.45rem;
font-size: 0.74rem;
}
.guided-product-moment[data-moment="approval"] .interrupt-decision-form__comment {
box-sizing: border-box;
height: 3.1rem;
padding: 0.35rem 0.5rem;
font-size: 0.72rem;
resize: none;
}
.guided-product-moment[data-moment="approval"] .interrupt-decision-form__actions {
gap: 0.55rem;
}
.guided-product-moment[data-moment="approval"] .interrupt-decision-form__submit,
.guided-product-moment[data-moment="approval"] .interrupt-decision-form__cancel {
padding: 0.45rem 1rem;
font-size: 0.78rem;
}
.guided-product-moment__resume-grid {
display: grid;
grid-template-columns: 1.4fr 1fr;
gap: 1rem;
align-items: start;
align-items: stretch;
min-height: 0;
max-height: 100%;
overflow: hidden;
}
.guided-product-moment[data-moment="resume"] .run-facts-card,
.guided-product-moment[data-moment="output"] .run-facts-card {
max-height: 100%;
overflow: auto;
}
.guided-product-moment[data-moment="resume"] .run-facts-markdown-preview,
.guided-product-moment[data-moment="output"] .run-facts-markdown-preview {
max-height: 16rem;
overflow: auto;
scrollbar-width: none;
}
.guided-product-moment[data-moment="resume"] .run-facts-markdown-preview::-webkit-scrollbar,
.guided-product-moment[data-moment="output"] .run-facts-markdown-preview::-webkit-scrollbar {
display: none;
}
@media (max-width: 900px) {
@@ -552,7 +552,7 @@
}
.demo-workflow-stage[data-demo-layout="interrupt"] .demo-workflow-stage__graph {
grid-template-columns: minmax(20rem, 0.95fr) minmax(24rem, 1.05fr);
grid-template-columns: minmax(28rem, 1.25fr) minmax(20rem, 0.75fr);
align-items: stretch;
gap: 1rem;
}
@@ -599,7 +599,7 @@
@media (max-width: 1050px) {
.demo-workflow-stage[data-demo-layout="interrupt"] .demo-workflow-stage__graph {
grid-template-columns: minmax(17rem, 40%) minmax(0, 1fr);
grid-template-columns: minmax(24rem, 1.15fr) minmax(0, 0.85fr);
}
}
@@ -666,7 +666,34 @@
white-space: nowrap;
}
.interrupt-contract-preview__schema > span {
.interrupt-contract-preview__details {
display: grid;
grid-template-columns: minmax(0, 1.15fr) minmax(0, 0.85fr);
gap: 0.8rem;
min-height: 0;
border-top: 1px solid var(--stage-line);
padding-top: 0.7rem;
overflow: hidden;
}
.interrupt-contract-preview__schema,
.interrupt-contract-preview__payload,
.interrupt-contract-preview__issues {
min-height: 0;
border: 1px solid color-mix(in oklch, var(--stage-line) 72%, transparent);
border-radius: 0.6rem;
padding: 0.75rem 0.85rem;
overflow: hidden;
background: oklch(0.09 0.022 250 / 0.92);
}
.interrupt-contract-preview__payload {
grid-row: span 2;
}
.interrupt-contract-preview__schema > span,
.interrupt-contract-preview__payload > span,
.interrupt-contract-preview__issues > span {
color: var(--text-muted);
font: 650 0.6rem/1 var(--font-mono, monospace);
letter-spacing: 0.1em;
@@ -674,9 +701,7 @@
}
.interrupt-contract-preview__schema {
min-height: 0;
border-top: 1px solid var(--stage-line);
padding-top: 0.7rem;
border-top: 0;
}
.interrupt-contract-preview__schema strong {
@@ -686,18 +711,66 @@
text-transform: uppercase;
}
.interrupt-contract-preview__schema pre {
max-height: 7.5rem;
.interrupt-contract-preview__schema pre,
.interrupt-contract-preview__payload pre {
max-height: 16rem;
margin: 0.45rem 0 0;
overflow: auto;
color: color-mix(in oklch, var(--text-primary) 82%, var(--text-muted));
font: 0.63rem/1.45 var(--font-mono, monospace);
font: 0.74rem/1.5 var(--font-mono, monospace);
white-space: pre-wrap;
scrollbar-width: none;
}
.interrupt-contract-preview__schema code,
.interrupt-contract-preview__payload code {
color: color-mix(in oklch, var(--text-primary) 92%, white);
}
.interrupt-contract-preview__schema pre::-webkit-scrollbar,
.interrupt-contract-preview__payload pre::-webkit-scrollbar {
display: none;
}
.interrupt-contract-preview__issues ul {
display: grid;
gap: 0.45rem;
margin: 0.45rem 0 0;
padding: 0;
list-style: none;
}
.interrupt-contract-preview__issues li {
display: flex;
gap: 0.5rem;
align-items: center;
justify-content: space-between;
border: 1px solid color-mix(in oklch, var(--stage-line) 72%, transparent);
border-radius: 0.55rem;
padding: 0.45rem 0.55rem;
background: oklch(0.12 0.026 250 / 0.9);
}
.interrupt-contract-preview__issues strong {
min-width: 0;
overflow: hidden;
color: var(--text-primary);
font-size: 0.78rem;
text-overflow: ellipsis;
white-space: nowrap;
}
.interrupt-contract-preview__issues small {
border-radius: 0.35rem;
padding: 0.1rem 0.35rem;
background: color-mix(in oklch, var(--accent-amber) 24%, transparent);
color: var(--accent-amber);
font: 650 0.62rem/1.3 var(--font-mono, monospace);
}
@media (max-width: 1050px) {
.demo-workflow-stage__graph:has(.interrupt-contract-preview) {
grid-template-columns: minmax(0, 1fr) 15rem;
grid-template-columns: minmax(0, 1fr) minmax(14rem, 0.7fr);
}
.presentation-route .workflow-graph-stage__node {
@@ -767,7 +840,7 @@
}
.interrupt-contract-preview__schema pre {
max-height: 8rem;
max-height: 12rem;
}
}
@@ -1062,6 +1135,25 @@
min-height: 0;
display: grid;
align-items: stretch;
overflow: hidden;
}
.guided-product-moment[data-moment="approval"] {
gap: 0.55rem;
}
.guided-product-moment[data-moment="approval"] .guided-product-moment__header {
grid-template-columns: minmax(0, 0.42fr) minmax(0, 1fr);
padding: 0.55rem 0.75rem;
}
.guided-product-moment[data-moment="approval"] .guided-product-moment__header strong {
font-size: 1rem;
}
.guided-product-moment[data-moment="approval"] .guided-product-moment__header p {
font-size: 0.82rem;
line-height: 1.35;
}
.guided-product-moment[data-moment="approval"] .interrupt-contract-preview {