feat: thread demo approval actions

This commit is contained in:
lda
2026-07-09 09:10:42 +07:00 Verified
parent 2b80ecde5a
commit e9d85dd080
6 changed files with 50 additions and 3 deletions
@@ -3,6 +3,7 @@ import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it, vi } from "vitest";
import { loadCanonicalDemoRecording } from "../demo/timeline/replay.js";
import type { DemoTimelineController } from "../demo/useDemoTimeline.js";
import type { DemoApprovalActions } from "./demo-approval-actions.js";
import { DemoWorkflowScene } from "./DemoWorkflowScene.js";
import { findBeat, findScene } from "./storyboard.js";
@@ -48,8 +49,12 @@ const requireSceneBeat = (sceneId: string, beatId: string) => {
const renderBeat = (
beatId: string,
sceneId = "workflow-demo",
openEvidence = vi.fn(),
options: {
readonly openEvidence?: () => void;
readonly approvalActions?: DemoApprovalActions;
} = {},
) => {
const openEvidence = options.openEvidence ?? vi.fn();
const { scene, beat } = requireSceneBeat(sceneId, beatId);
const rendered = render(
<DemoWorkflowScene
@@ -59,6 +64,7 @@ const renderBeat = (
selectedNodeId={null}
selectNode={noop}
openEvidence={openEvidence}
approvalActions={options.approvalActions}
/>,
);
return { ...rendered, openEvidence };
@@ -190,6 +196,21 @@ describe("DemoWorkflowScene", () => {
expect(screen.queryByLabelText("demo outcome proof")).not.toBeInTheDocument();
});
it("wires approval actions into the Scene 10 schema approval surface", () => {
renderBeat("approval", "interrupt-evidence", {
approvalActions: {
state: "ready",
canSubmit: true,
canCancel: true,
submit: vi.fn(async () => {}),
cancel: vi.fn(async () => {}),
},
});
expect(screen.getByRole("button", { name: "Submit" })).toBeEnabled();
expect(screen.getByRole("button", { name: "Cancel" })).toBeEnabled();
});
it("shows a schema approval surface for the approval beat instead of raw schema as the primary visual", () => {
renderBeat("approval", "interrupt-evidence");
@@ -6,6 +6,7 @@ import {
projectInterruptContract,
projectOperationPresentation,
} from "./demo-workflow-model.js";
import type { DemoApprovalActions } from "./demo-approval-actions.js";
import { DemoContinuityRail } from "./DemoContinuityRail.js";
import { DemoOutcomePanel } from "./DemoOutcomePanel.js";
import { InterruptContractPreview } from "./InterruptContractPreview.js";
@@ -22,6 +23,7 @@ type DemoWorkflowSceneProps = {
readonly selectedNodeId: string | null;
readonly selectNode: (nodeId: string | null) => void;
readonly openEvidence: () => void;
readonly approvalActions?: DemoApprovalActions | undefined;
};
type DemoWorkflowLayout = "operation" | "graph" | "interrupt" | "approval" | "evidence";
@@ -52,6 +54,7 @@ export const DemoWorkflowScene = ({
selectedNodeId,
selectNode,
openEvidence,
approvalActions,
}: DemoWorkflowSceneProps) => {
const runStart = findEvent(demo, "run_start");
const runResume = findEvent(demo, "run_resume");
@@ -123,6 +126,7 @@ export const DemoWorkflowScene = ({
contract={contract}
mode={contractMode}
hero={layout === "approval"}
approvalActions={approvalActions}
/>
)}
</div>
@@ -1,5 +1,6 @@
import { m } from "motion/react";
import type { InterruptContractPresentation } from "./demo-workflow-model.js";
import type { DemoApprovalActions } from "./demo-approval-actions.js";
import { SchemaApprovalSurface } from "./approval/SchemaApprovalSurface.js";
import { formatJson } from "./format.js";
@@ -7,6 +8,7 @@ type InterruptContractPreviewProps = {
readonly contract: InterruptContractPresentation;
readonly mode: "preview" | "approval";
readonly hero?: boolean;
readonly approvalActions?: DemoApprovalActions | undefined;
};
const titleForKind = (kind: string): string => `${kind.replaceAll("_", " ")} resume`;
@@ -15,6 +17,7 @@ export const InterruptContractPreview = ({
contract,
mode,
hero = false,
approvalActions,
}: InterruptContractPreviewProps) => (
<m.aside
className="interrupt-contract-preview"
@@ -48,6 +51,9 @@ export const InterruptContractPreview = ({
payload={contract.resumePayloadPreview}
outcomes={contract.outcomes}
runId={contract.runId}
state={approvalActions?.state ?? "ready"}
onSubmit={approvalActions?.canSubmit ? () => void approvalActions.submit() : undefined}
onCancel={approvalActions?.canCancel ? () => void approvalActions.cancel() : undefined}
/>
) : (
<div className="interrupt-contract-preview__schema">
@@ -2,6 +2,7 @@ import { domAnimation, LayoutGroup, LazyMotion } from "motion/react";
import type { EvidenceRecord } from "../app/state.js";
import type { AgentMessage } from "../demo/agent/events.js";
import type { TimelineAgentController } from "../demo/agent/timelineAgent.js";
import type { DemoApprovalActions } from "./demo-approval-actions.js";
import { SceneBody } from "./SceneBody.js";
import { DiscussionPanel } from "./DiscussionPanel.js";
import { EvidenceInspector } from "./evidence/EvidenceInspector.js";
@@ -18,6 +19,7 @@ type PresentationStageProps = {
readonly evidence: readonly EvidenceRecord[];
readonly messages?: ReadonlyArray<AgentMessage>;
readonly timelineAgent?: TimelineAgentController | undefined;
readonly approvalActions?: DemoApprovalActions | undefined;
readonly onApprove?: (() => void) | undefined;
readonly onDeny?: (() => void) | undefined;
readonly jump: (location: MainLocation) => void;
@@ -34,6 +36,7 @@ export const PresentationStage = ({
evidence,
messages,
timelineAgent,
approvalActions,
onApprove,
onDeny,
jump,
@@ -79,6 +82,7 @@ export const PresentationStage = ({
}
}}
motionDisabled={state.motionDisabled}
approvalActions={approvalActions}
/>
)}
</section>
@@ -1,4 +1,5 @@
import type { DemoTimelineController } from "../demo/useDemoTimeline.js";
import type { DemoApprovalActions } from "./demo-approval-actions.js";
import {
discussionBranches,
findBeat,
@@ -20,6 +21,7 @@ type SceneBodyProps = {
readonly openDiscussion: (branchId: string) => void;
readonly onFocusPathChange: (path: readonly string[]) => void;
readonly motionDisabled: boolean;
readonly approvalActions?: DemoApprovalActions | undefined;
};
const DiscussionLinks = ({
@@ -291,7 +293,7 @@ const assertNever = (value: never): never => {
throw new Error(`Unexpected view: ${value}`);
};
export const SceneBody = ({ location, demo, selectedNodeId, selectNode, openEvidence, openDiscussion, onFocusPathChange, motionDisabled }: SceneBodyProps) => {
export const SceneBody = ({ location, demo, selectedNodeId, selectNode, openEvidence, openDiscussion, onFocusPathChange, motionDisabled, approvalActions }: SceneBodyProps) => {
const sceneId = location.kind === "main" ? location.sceneId : "positioning";
const beatId = location.kind === "main" ? location.beatId : "landscape";
const scene = findScene(sceneId) ?? findScene("thesis")!;
@@ -323,7 +325,7 @@ export const SceneBody = ({ location, demo, selectedNodeId, selectNode, openEvid
return <AuthoringScene scene={scene} beat={beat} />;
case "agent":
return <AgentHandoffScene scene={scene} beat={beat} />;
case "demo":
case "demo":
return (
<DemoWorkflowScene
scene={scene}
@@ -332,6 +334,7 @@ export const SceneBody = ({ location, demo, selectedNodeId, selectNode, openEvid
selectedNodeId={selectedNodeId}
selectNode={selectNode}
openEvidence={openEvidence}
approvalActions={approvalActions}
/>
);
case "evaluation":
@@ -0,0 +1,9 @@
export type DemoApprovalUiState = "ready" | "submitted" | "cancelled";
export type DemoApprovalActions = {
readonly state: DemoApprovalUiState;
readonly canSubmit: boolean;
readonly canCancel: boolean;
readonly submit: () => Promise<void>;
readonly cancel: () => Promise<void>;
};