feat: thread demo approval actions
This commit is contained in:
@@ -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>;
|
||||
};
|
||||
Reference in New Issue
Block a user