feat: add demo outcome proof panel

This commit is contained in:
lda
2026-07-08 23:16:08 +07:00 Verified
parent fff918489d
commit 00950c58ab
3 changed files with 225 additions and 0 deletions
@@ -0,0 +1,83 @@
import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it } from "vitest";
import {
demoBeatLensForBeat,
type InterruptContractPresentation,
type OperationPresentation,
} from "./demo-workflow-model.js";
import { DemoOutcomePanel } from "./DemoOutcomePanel.js";
afterEach(() => cleanup());
const operation: OperationPresentation = {
operation: "workflow.runs.resume",
status: "completed",
durationMs: 42,
command: "uv run wf run resume run_recorded_lda_report",
deploymentId: "lda_report_workflow.default",
runId: "run_recorded_lda_report",
interruptKind: null,
};
const contract: InterruptContractPresentation = {
kind: "issue_review",
outcomes: ["submitted", "cancelled"],
resumeSchema: { type: "object" },
runId: "run_recorded_lda_report",
};
describe("DemoOutcomePanel", () => {
it("explains the approval beat as a schema-backed decision", () => {
render(
<DemoOutcomePanel
beatId="approval"
lens={demoBeatLensForBeat("approval")}
operation={null}
contract={contract}
/>,
);
expect(screen.getByLabelText("demo outcome proof")).toHaveTextContent("schema-backed");
expect(screen.getByText("submitted / cancelled")).toBeInTheDocument();
expect(screen.getByText("run_recorded_lda_report")).toBeInTheDocument();
});
it("explains resume as same-run continuation", () => {
render(
<DemoOutcomePanel
beatId="resume"
lens={demoBeatLensForBeat("resume")}
operation={operation}
contract={contract}
/>,
);
expect(screen.getByText("Same persisted run")).toBeInTheDocument();
expect(screen.getByText("workflow.runs.resume")).toBeInTheDocument();
expect(screen.getByText("completed")).toBeInTheDocument();
});
it("explains output and trace without pretending to run live services", () => {
const { rerender } = render(
<DemoOutcomePanel
beatId="output"
lens={demoBeatLensForBeat("output")}
operation={null}
contract={contract}
/>,
);
expect(screen.getByText("Report markdown")).toBeInTheDocument();
expect(screen.getByText("Issue board changes")).toBeInTheDocument();
rerender(
<DemoOutcomePanel
beatId="trace"
lens={demoBeatLensForBeat("trace")}
operation={operation}
contract={contract}
/>,
);
expect(screen.getByText("Trace frames")).toBeInTheDocument();
expect(screen.getByText("Protocol evidence")).toBeInTheDocument();
});
});
@@ -0,0 +1,74 @@
import type {
DemoBeatLens,
InterruptContractPresentation,
OperationPresentation,
} from "./demo-workflow-model.js";
type DemoOutcomePanelProps = {
readonly beatId: string;
readonly lens: DemoBeatLens;
readonly operation: OperationPresentation | null;
readonly contract: InterruptContractPresentation | null;
};
const outcomesText = (contract: InterruptContractPresentation | null): string =>
contract?.outcomes.join(" / ") ?? "submitted / cancelled";
export const DemoOutcomePanel = ({
beatId,
lens,
operation,
contract,
}: DemoOutcomePanelProps) => {
const runId = operation?.runId ?? contract?.runId ?? "run unavailable";
if (beatId === "approval") {
return (
<aside className="demo-outcome-panel" aria-label="demo outcome proof">
<span>{lens.eyebrow}</span>
<strong>Operator sees a schema-backed request</strong>
<dl>
<div><dt>Run</dt><dd><code>{runId}</code></dd></div>
<div><dt>Allowed outcomes</dt><dd>{outcomesText(contract)}</dd></div>
</dl>
</aside>
);
}
if (beatId === "resume") {
return (
<aside className="demo-outcome-panel" aria-label="demo outcome proof">
<span>{lens.eyebrow}</span>
<strong>Same persisted run</strong>
<dl>
<div><dt>Operation</dt><dd><code>{operation?.operation ?? lens.proofLabel}</code></dd></div>
<div><dt>Status</dt><dd>{operation?.status ?? "completed"}</dd></div>
</dl>
</aside>
);
}
if (beatId === "output") {
return (
<aside className="demo-outcome-panel demo-outcome-panel--split" aria-label="demo outcome proof">
<span>{lens.eyebrow}</span>
<strong>Product state, not chat-only text</strong>
<ul>
<li><b>Report markdown</b><small>Generated from selected documents.</small></li>
<li><b>Issue board changes</b><small>Created only after approval.</small></li>
</ul>
</aside>
);
}
return (
<aside className="demo-outcome-panel demo-outcome-panel--split" aria-label="demo outcome proof">
<span>{lens.eyebrow}</span>
<strong>Auditable after the moment passes</strong>
<ul>
<li><b>Trace frames</b><small>Node-level execution history.</small></li>
<li><b>Protocol evidence</b><small>Raw and interpreted JSON-RPC records.</small></li>
</ul>
</aside>
);
};
@@ -736,6 +736,74 @@
font: 600 0.6rem/1.1 var(--font-mono, monospace);
}
.demo-outcome-panel {
display: grid;
gap: 0.65rem;
align-content: start;
border: 1px solid oklch(0.76 0.18 70 / 0.38);
border-radius: 0.85rem;
padding: 0.85rem;
background:
linear-gradient(145deg, oklch(0.76 0.18 70 / 0.08), transparent 55%),
oklch(0.13 0.025 250 / 0.9);
color: var(--text-primary);
}
.demo-outcome-panel > span {
color: var(--accent-amber);
font: 700 0.62rem/1 var(--font-mono, monospace);
letter-spacing: 0.11em;
text-transform: uppercase;
}
.demo-outcome-panel > strong {
font-size: 1rem;
line-height: 1.18;
}
.demo-outcome-panel dl {
display: grid;
gap: 0.45rem;
margin: 0;
}
.demo-outcome-panel dt {
color: var(--text-muted);
font: 700 0.58rem/1 var(--font-mono, monospace);
letter-spacing: 0.08em;
text-transform: uppercase;
}
.demo-outcome-panel dd {
margin: 0.2rem 0 0;
color: var(--text-secondary);
font-size: 0.78rem;
}
.demo-outcome-panel ul {
display: grid;
gap: 0.5rem;
margin: 0;
padding: 0;
list-style: none;
}
.demo-outcome-panel li {
border: 1px solid var(--stage-line);
border-radius: 0.65rem;
padding: 0.6rem;
background: oklch(0.11 0.022 250 / 0.72);
}
.demo-outcome-panel b {
display: block;
margin-bottom: 0.25rem;
}
.demo-outcome-panel small {
color: var(--text-secondary);
}
@container presentation-canvas (max-width: 1050px) {
.demo-continuity-rail {
grid-template-columns: minmax(0, 1fr);