feat: add workflow demo presentation stage

This commit is contained in:
lda
2026-07-05 06:29:52 +07:00 Verified
parent c95d71fe5d
commit a8402a4d1d
19 changed files with 1944 additions and 233 deletions
+10 -3
View File
@@ -88,11 +88,18 @@ Implementation order:
[`defense presentation storyboard`](superpowers/specs/2026-07-04-defense-presentation-storyboard-design.md). [`defense presentation storyboard`](superpowers/specs/2026-07-04-defense-presentation-storyboard-design.md).
Implementation: Implementation:
[`defense storyboard compositor plan`](historical/superpowers/plans/2026-07-04-defense-storyboard-compositor.md). [`defense storyboard compositor plan`](historical/superpowers/plans/2026-07-04-defense-storyboard-compositor.md).
10. Next: adopt source-owned AI Elements chat primitives against existing 10. Completed: make the workflow execution handoff the visual center of Scenes
9 and 10. The canonical replay now drives an interpreted operation surface,
persistent execution graph, typed interrupt contract, and raw evidence
drawer. Design:
[`workflow takes the stage`](superpowers/specs/2026-07-05-workflow-takes-stage-visual-design.md).
Implementation:
[`workflow takes the stage plan`](historical/superpowers/plans/2026-07-05-workflow-takes-stage-visual.md).
11. Next: adopt source-owned AI Elements chat primitives against existing
`AgentMessagePart` / `AgentDriver` contracts. `AgentMessagePart` / `AgentDriver` contracts.
11. Future: implement final scene visuals, motion choreography, evidence assets, 12. Future: implement final scene visuals, motion choreography, evidence assets,
and rehearsal timing. and rehearsal timing.
12. Add a static slide/appendix shell only after presentation mode is clear. 13. Add a static slide/appendix shell only after presentation mode is clear.
Astro remains an option, not the default next surface. Astro remains an option, not the default next surface.
Boundaries: this is not a production admin panel, generic visual workflow Boundaries: this is not a production admin panel, generic visual workflow
@@ -1,6 +1,6 @@
# Workflow Takes the Stage Visual Implementation Plan # Workflow Takes the Stage Visual Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [x]`) syntax for tracking.
**Goal:** Turn Scene 9 into a readable cinematic handoff from agent intent to workflow execution, then carry the same run and interrupt context into Scene 10. **Goal:** Turn Scene 9 into a readable cinematic handoff from agent intent to workflow execution, then carry the same run and interrupt context into Scene 10.
@@ -35,7 +35,7 @@
- Produces: `OperationPresentation`, `InterruptContractPresentation`, `GraphExecutionPresentation`, `projectOperationPresentation(event)`, `projectInterruptContract(event)`, and `graphExecutionForBeat(beatId)`. - Produces: `OperationPresentation`, `InterruptContractPresentation`, `GraphExecutionPresentation`, `projectOperationPresentation(event)`, `projectInterruptContract(event)`, and `graphExecutionForBeat(beatId)`.
- Later tasks must consume these projections instead of reading `event.interpreted` directly. - Later tasks must consume these projections instead of reading `event.interpreted` directly.
- [ ] **Step 1: Write failing projection tests** - [x] **Step 1: Write failing projection tests**
Create table-driven tests using the canonical `run_start` shape and malformed or sparse events: Create table-driven tests using the canonical `run_start` shape and malformed or sparse events:
@@ -122,7 +122,7 @@ describe("demo workflow presentation model", () => {
}); });
``` ```
- [ ] **Step 2: Run the tests and verify the red state** - [x] **Step 2: Run the tests and verify the red state**
Run: Run:
@@ -132,7 +132,7 @@ pnpm --dir web --filter @lda/console exec vitest run src/presentation/demo-workf
Expected: FAIL because `demo-workflow-model.ts` does not exist. Expected: FAIL because `demo-workflow-model.ts` does not exist.
- [ ] **Step 3: Implement schema-driven projections** - [x] **Step 3: Implement schema-driven projections**
Use Valibot `safeParse` at the `unknown` interpreted boundary: Use Valibot `safeParse` at the `unknown` interpreted boundary:
@@ -227,7 +227,7 @@ Keep the schemas local because they decode a presentation projection, not a new
domain contract. Add a short comment explaining why malformed replay details domain contract. Add a short comment explaining why malformed replay details
degrade to bounded display values instead of failing the entire presentation. degrade to bounded display values instead of failing the entire presentation.
- [ ] **Step 4: Run focused tests and typecheck** - [x] **Step 4: Run focused tests and typecheck**
Run: Run:
@@ -238,7 +238,7 @@ pnpm --dir web --filter @lda/console typecheck
Expected: projection tests PASS and typecheck exits zero. Expected: projection tests PASS and typecheck exits zero.
- [ ] **Step 5: Commit the projection boundary** - [x] **Step 5: Commit the projection boundary**
```powershell ```powershell
git add web/apps/console/src/presentation/demo-workflow-model.ts web/apps/console/src/presentation/demo-workflow-model.test.ts git add web/apps/console/src/presentation/demo-workflow-model.ts web/apps/console/src/presentation/demo-workflow-model.test.ts
@@ -260,7 +260,7 @@ git commit -m "feat: project workflow demo presentation state"
- Produces: `OperationBlock` props `{ event, variant, openEvidence }`, where `variant` is `"expanded" | "receipt"`. - Produces: `OperationBlock` props `{ event, variant, openEvidence }`, where `variant` is `"expanded" | "receipt"`.
- Produces: the shared Motion layout id `workflow-start-operation` on both the prepared start tool call and expanded operation. - Produces: the shared Motion layout id `workflow-start-operation` on both the prepared start tool call and expanded operation.
- [ ] **Step 1: Replace the old operation test with failing hierarchy tests** - [x] **Step 1: Replace the old operation test with failing hierarchy tests**
Test expanded and receipt variants separately: Test expanded and receipt variants separately:
@@ -293,7 +293,7 @@ it("renders a compact execution receipt", () => {
Extend `OperatorChat.test.tsx` with a prepared start tool-call message and assert Extend `OperatorChat.test.tsx` with a prepared start tool-call message and assert
that its rendered part has `data-layout-anchor="workflow-start-operation"`. that its rendered part has `data-layout-anchor="workflow-start-operation"`.
- [ ] **Step 2: Run focused tests and verify failures** - [x] **Step 2: Run focused tests and verify failures**
Run: Run:
@@ -304,7 +304,7 @@ pnpm --dir web --filter @lda/console exec vitest run src/presentation/OperationB
Expected: FAIL because the variant, callback, interpreted summary, and layout Expected: FAIL because the variant, callback, interpreted summary, and layout
anchor do not exist. anchor do not exist.
- [ ] **Step 3: Implement explicit operation variants** - [x] **Step 3: Implement explicit operation variants**
Use Motion and semantic display fields, not serialized interpreted JSON: Use Motion and semantic display fields, not serialized interpreted JSON:
@@ -367,7 +367,7 @@ In `OperatorChat`, render tool-call parts through `motion.div`. Assign
`part.call.name === "startPreparedReportRun"`. Preserve every existing message `part.call.name === "startPreparedReportRun"`. Preserve every existing message
part and approval behavior. part and approval behavior.
- [ ] **Step 4: Run operation and chat tests** - [x] **Step 4: Run operation and chat tests**
Run: Run:
@@ -378,7 +378,7 @@ pnpm --dir web --filter @lda/console typecheck
Expected: focused tests PASS and typecheck exits zero. Expected: focused tests PASS and typecheck exits zero.
- [ ] **Step 5: Commit the operation hierarchy** - [x] **Step 5: Commit the operation hierarchy**
```powershell ```powershell
git add web/apps/console/src/presentation/OperationBlock.tsx web/apps/console/src/presentation/OperationBlock.test.tsx web/apps/console/src/presentation/OperatorChat.tsx web/apps/console/src/presentation/OperatorChat.test.tsx git add web/apps/console/src/presentation/OperationBlock.tsx web/apps/console/src/presentation/OperationBlock.test.tsx web/apps/console/src/presentation/OperatorChat.tsx web/apps/console/src/presentation/OperatorChat.test.tsx
@@ -401,7 +401,7 @@ git commit -m "feat: stage interpreted workflow operations"
- Produces: `InterruptContractPreview` props `{ contract }`. - Produces: `InterruptContractPreview` props `{ contract }`.
- Graph node DOM exposes `data-execution="completed|current|future"` and preserves `data-kind`. - Graph node DOM exposes `data-execution="completed|current|future"` and preserves `data-kind`.
- [ ] **Step 1: Write failing graph-state and contract tests** - [x] **Step 1: Write failing graph-state and contract tests**
Replace the graph test setup with explicit execution state: Replace the graph test setup with explicit execution state:
@@ -447,7 +447,7 @@ it("shows the typed resume contract and persisted run", () => {
}); });
``` ```
- [ ] **Step 2: Run focused tests and verify failures** - [x] **Step 2: Run focused tests and verify failures**
Run: Run:
@@ -458,7 +458,7 @@ pnpm --dir web --filter @lda/console exec vitest run src/presentation/WorkflowGr
Expected: FAIL because the new props, connectors, execution states, and preview Expected: FAIL because the new props, connectors, execution states, and preview
component do not exist. component do not exist.
- [ ] **Step 3: Implement graph edges and semantic node state** - [x] **Step 3: Implement graph edges and semantic node state**
Add a curated edge list beside `presentationNodes`: Add a curated edge list beside `presentationNodes`:
@@ -494,7 +494,7 @@ const executionStateForNode = (
Keep each graph node as a button and retain `aria-pressed` for the spotlight Keep each graph node as a button and retain `aria-pressed` for the spotlight
selection state. selection state.
- [ ] **Step 4: Implement the contract preview** - [x] **Step 4: Implement the contract preview**
Render a Motion `aside` with kind, outcomes, run id, and a bounded formatted Render a Motion `aside` with kind, outcomes, run id, and a bounded formatted
resume schema. Reuse `formatJson` and cap the preview with CSS rather than resume schema. Reuse `formatJson` and cap the preview with CSS rather than
@@ -525,7 +525,7 @@ export const InterruptContractPreview = ({
); );
``` ```
- [ ] **Step 5: Run focused tests and typecheck** - [x] **Step 5: Run focused tests and typecheck**
Run: Run:
@@ -536,7 +536,7 @@ pnpm --dir web --filter @lda/console typecheck
Expected: focused tests PASS and typecheck exits zero. Expected: focused tests PASS and typecheck exits zero.
- [ ] **Step 6: Commit graph and interrupt semantics** - [x] **Step 6: Commit graph and interrupt semantics**
```powershell ```powershell
git add web/apps/console/src/presentation/WorkflowGraphStage.tsx web/apps/console/src/presentation/WorkflowGraphStage.test.tsx web/apps/console/src/presentation/InterruptContractPreview.tsx web/apps/console/src/presentation/InterruptContractPreview.test.tsx git add web/apps/console/src/presentation/WorkflowGraphStage.tsx web/apps/console/src/presentation/WorkflowGraphStage.test.tsx web/apps/console/src/presentation/InterruptContractPreview.tsx web/apps/console/src/presentation/InterruptContractPreview.test.tsx
@@ -560,7 +560,7 @@ git commit -m "feat: stage workflow graph execution state"
- Produces: `DemoWorkflowScene` props `{ scene, beat, demo, selectedNodeId, selectNode, openEvidence }`. - Produces: `DemoWorkflowScene` props `{ scene, beat, demo, selectedNodeId, selectNode, openEvidence }`.
- Extends `SceneBody` with `openEvidence: () => void`; `PresentationStage` passes its existing callback through without creating new state. - Extends `SceneBody` with `openEvidence: () => void`; `PresentationStage` passes its existing callback through without creating new state.
- [ ] **Step 1: Write failing beat-composition tests** - [x] **Step 1: Write failing beat-composition tests**
Build a deterministic `DemoTimelineController` fixture containing the canonical Build a deterministic `DemoTimelineController` fixture containing the canonical
`run_start` event. Test the visible composition by beat: `run_start` event. Test the visible composition by beat:
@@ -646,7 +646,7 @@ Add a `SceneBody` test that clicks `View raw evidence` and verifies the supplied
`openEvidence` callback. Update all existing `SceneBody` call sites in tests to `openEvidence` callback. Update all existing `SceneBody` call sites in tests to
pass a no-op callback. pass a no-op callback.
- [ ] **Step 2: Run focused tests and verify failures** - [x] **Step 2: Run focused tests and verify failures**
Run: Run:
@@ -656,7 +656,7 @@ pnpm --dir web --filter @lda/console exec vitest run src/presentation/DemoWorkfl
Expected: FAIL because the extracted scene and callback path do not exist. Expected: FAIL because the extracted scene and callback path do not exist.
- [ ] **Step 3: Extract and implement beat composition** - [x] **Step 3: Extract and implement beat composition**
Move `DemoWorkflowScene` out of `SceneBody.tsx`. The component must always find Move `DemoWorkflowScene` out of `SceneBody.tsx`. The component must always find
the canonical `run_start` event for the run receipt and interrupt contract, then the canonical `run_start` event for the run receipt and interrupt contract, then
@@ -706,7 +706,7 @@ while `output` retains the graph and run receipt. Preserve the existing event
selection and evidence behavior. Do not remove any existing Scene 10 content selection and evidence behavior. Do not remove any existing Scene 10 content
as collateral damage. as collateral damage.
- [ ] **Step 4: Wire the existing evidence callback through the stage** - [x] **Step 4: Wire the existing evidence callback through the stage**
Add `openEvidence` to `SceneBodyProps`, pass it from `PresentationStage`, and Add `openEvidence` to `SceneBodyProps`, pass it from `PresentationStage`, and
pass it to `DemoWorkflowScene`. This callback must continue dispatching only: pass it to `DemoWorkflowScene`. This callback must continue dispatching only:
@@ -718,7 +718,7 @@ pass it to `DemoWorkflowScene`. This callback must continue dispatching only:
Do not add event selection state in this slice. The bounded canonical replay Do not add event selection state in this slice. The bounded canonical replay
evidence already satisfies the approved fallback. evidence already satisfies the approved fallback.
- [ ] **Step 5: Run focused and presentation tests** - [x] **Step 5: Run focused and presentation tests**
Run: Run:
@@ -729,7 +729,7 @@ pnpm --dir web --filter @lda/console typecheck
Expected: focused tests PASS and typecheck exits zero. Expected: focused tests PASS and typecheck exits zero.
- [ ] **Step 6: Commit scene composition** - [x] **Step 6: Commit scene composition**
```powershell ```powershell
git add web/apps/console/src/presentation/DemoWorkflowScene.tsx web/apps/console/src/presentation/DemoWorkflowScene.test.tsx web/apps/console/src/presentation/SceneBody.tsx web/apps/console/src/presentation/SceneBody.test.tsx web/apps/console/src/presentation/PresentationStage.tsx web/apps/console/src/presentation/PresentationRoute.test.tsx git add web/apps/console/src/presentation/DemoWorkflowScene.tsx web/apps/console/src/presentation/DemoWorkflowScene.test.tsx web/apps/console/src/presentation/SceneBody.tsx web/apps/console/src/presentation/SceneBody.test.tsx web/apps/console/src/presentation/PresentationStage.tsx web/apps/console/src/presentation/PresentationRoute.test.tsx
@@ -752,7 +752,7 @@ git commit -m "feat: choreograph workflow demo beats"
- Produces: presentation-scoped color, surface, text, status, radius, and motion tokens. - Produces: presentation-scoped color, surface, text, status, radius, and motion tokens.
- Leaves `web/apps/console/src/styles/global.css` unchanged so `/console` remains stable. - Leaves `web/apps/console/src/styles/global.css` unchanged so `/console` remains stable.
- [ ] **Step 1: Add presentation token definitions** - [x] **Step 1: Add presentation token definitions**
Create a route-scoped token file. Use these roles consistently rather than Create a route-scoped token file. Use these roles consistently rather than
copying literal colors into component selectors: copying literal colors into component selectors:
@@ -782,7 +782,7 @@ copying literal colors into component selectors:
Update `DESIGN.md` with the same named presentation roles. Do not document Update `DESIGN.md` with the same named presentation roles. Do not document
every tonal literal; document the semantic token names and the one-accent rule. every tonal literal; document the semantic token names and the one-accent rule.
- [ ] **Step 2: Import styles in deterministic order** - [x] **Step 2: Import styles in deterministic order**
Update `PresentationRoute.tsx` imports in this order: Update `PresentationRoute.tsx` imports in this order:
@@ -795,7 +795,7 @@ import "./styles/demo-workflow.css";
The route owns these imports. Do not import presentation CSS from `/console` The route owns these imports. Do not import presentation CSS from `/console`
components. components.
- [ ] **Step 3: Stop global console section styles leaking into presentation** - [x] **Step 3: Stop global console section styles leaking into presentation**
In `presentation.css`, explicitly reset the primary stage container: In `presentation.css`, explicitly reset the primary stage container:
@@ -815,7 +815,7 @@ Remove the old `.operation-block`, `.operation-block__grid`,
in `demo-workflow.css`. Do not leave duplicate selector families with competing in `demo-workflow.css`. Do not leave duplicate selector families with competing
rules. rules.
- [ ] **Step 4: Style operation, graph, interrupt, and chat states** - [x] **Step 4: Style operation, graph, interrupt, and chat states**
Implement the approved hierarchy in `demo-workflow.css`: Implement the approved hierarchy in `demo-workflow.css`:
@@ -836,7 +836,7 @@ Implement the approved hierarchy in `demo-workflow.css`:
Use the semantic tokens from Step 1. Keep body text at or above `0.9rem` in the Use the semantic tokens from Step 1. Keep body text at or above `0.9rem` in the
center demo and labels at or above `0.75rem` for projector readability. center demo and labels at or above `0.75rem` for projector readability.
- [ ] **Step 5: Implement motion and reduced-motion parity** - [x] **Step 5: Implement motion and reduced-motion parity**
Use Motion component props for layout changes and CSS only for small state Use Motion component props for layout changes and CSS only for small state
transitions. Configure graph node entry with a maximum 60 ms stagger and 350 ms transitions. Configure graph node entry with a maximum 60 ms stagger and 350 ms
@@ -863,7 +863,7 @@ node transition. Ensure both existing motion-off mechanisms remain effective:
Do not animate width, height, left, or top in CSS. Motion may perform FLIP Do not animate width, height, left, or top in CSS. Motion may perform FLIP
layout animation through transforms. layout animation through transforms.
- [ ] **Step 6: Run tests, typecheck, and build** - [x] **Step 6: Run tests, typecheck, and build**
Run: Run:
@@ -875,7 +875,7 @@ pnpm --dir web --filter @lda/console build
Expected: all console tests PASS, typecheck exits zero, and Vite build succeeds. Expected: all console tests PASS, typecheck exits zero, and Vite build succeeds.
- [ ] **Step 7: Commit the scoped visual system** - [x] **Step 7: Commit the scoped visual system**
```powershell ```powershell
git add web/apps/console/src/presentation/styles/presentation-tokens.css web/apps/console/src/presentation/styles/demo-workflow.css web/apps/console/src/presentation/presentation.css web/apps/console/src/presentation/PresentationRoute.tsx web/apps/console/DESIGN.md git add web/apps/console/src/presentation/styles/presentation-tokens.css web/apps/console/src/presentation/styles/demo-workflow.css web/apps/console/src/presentation/presentation.css web/apps/console/src/presentation/PresentationRoute.tsx web/apps/console/DESIGN.md
@@ -895,7 +895,7 @@ git commit -m "style: establish cinematic workflow demo stage"
- Consumes: completed presentation behavior from Tasks 1 through 5. - Consumes: completed presentation behavior from Tasks 1 through 5.
- Produces: reproducible defense smoke instructions and a historical implementation record. - Produces: reproducible defense smoke instructions and a historical implementation record.
- [ ] **Step 1: Run the complete web verification suite** - [x] **Step 1: Run the complete web verification suite**
Run: Run:
@@ -909,7 +909,7 @@ git diff --check
Expected: all web tests PASS, all workspace typechecks exit zero, build Expected: all web tests PASS, all workspace typechecks exit zero, build
succeeds, and `git diff --check` reports no whitespace errors. succeeds, and `git diff --check` reports no whitespace errors.
- [ ] **Step 2: Run focused `1280x720` browser smoke** - [x] **Step 2: Run focused `1280x720` browser smoke**
With `pnpm --dir web dev` already running, open a named Playwright session: With `pnpm --dir web dev` already running, open a named Playwright session:
@@ -942,7 +942,7 @@ Inspect each screenshot. Verify all of the following before continuing:
- no primary action or graph node is clipped; - no primary action or graph node is clipped;
- the page has no vertical scrollbar. - the page has no vertical scrollbar.
- [ ] **Step 3: Verify keyboard, evidence, reduced motion, and `/console`** - [x] **Step 3: Verify keyboard, evidence, reduced motion, and `/console`**
In the same browser session: In the same browser session:
@@ -962,7 +962,7 @@ Close the named browser session:
pnpx --package @playwright/cli playwright-cli -s=workflow-visual close pnpx --package @playwright/cli playwright-cli -s=workflow-visual close
``` ```
- [ ] **Step 4: Update live documentation** - [x] **Step 4: Update live documentation**
In `web/README.md`, document that presentation Scenes 8 through 10 use the In `web/README.md`, document that presentation Scenes 8 through 10 use the
canonical replay, interpreted operation surface, workflow graph execution canonical replay, interpreted operation surface, workflow graph execution
@@ -973,7 +973,7 @@ In `docs/current_roadmap.md`, record the completed Scene 9 cinematic slice and
link both the live design spec and historical plan path. Keep broad remaining link both the live design spec and historical plan path. Keep broad remaining
presentation polish as the next visual follow-up. presentation polish as the next visual follow-up.
- [ ] **Step 5: Archive the completed plan** - [x] **Step 5: Archive the completed plan**
```powershell ```powershell
git mv docs/superpowers/plans/2026-07-05-workflow-takes-stage-visual.md docs/historical/superpowers/plans/2026-07-05-workflow-takes-stage-visual.md git mv docs/superpowers/plans/2026-07-05-workflow-takes-stage-visual.md docs/historical/superpowers/plans/2026-07-05-workflow-takes-stage-visual.md
@@ -981,7 +981,7 @@ git mv docs/superpowers/plans/2026-07-05-workflow-takes-stage-visual.md docs/his
Update any link added in `docs/current_roadmap.md` to the historical path. Update any link added in `docs/current_roadmap.md` to the historical path.
- [ ] **Step 6: Run final documentation and repository checks** - [x] **Step 6: Run final documentation and repository checks**
Run: Run:
@@ -995,7 +995,7 @@ Expected: the roadmap points to `historical/superpowers/plans`, no live file
points to the old active plan path, and only intended implementation and points to the old active plan path, and only intended implementation and
documentation changes remain. documentation changes remain.
- [ ] **Step 7: Commit documentation and plan archival** - [x] **Step 7: Commit documentation and plan archival**
```powershell ```powershell
git add web/README.md docs/current_roadmap.md git add web/README.md docs/current_roadmap.md
+12
View File
@@ -173,6 +173,18 @@ prepared `lda_report_workflow` story. It renders a 12-scene, multi-beat
storyboard with stable stage regions, discussion branches, act themes, a chat storyboard with stable stage regions, discussion branches, act themes, a chat
dock, and keyboard navigation. dock, and keyboard navigation.
Scenes 8 through 10 use the canonical replay as their only execution evidence.
The handoff expands an interpreted run operation into the center stage, then
keeps one workflow graph mounted while execution reaches the typed interrupt
and approval boundary. Raw protocol payloads remain in the evidence drawer.
The key defense states are directly addressable:
- `/present#scene/workflow-demo/operation`
- `/present#scene/workflow-demo/graph`
- `/present#scene/workflow-demo/interrupt`
- `/present#scene/interrupt-evidence/approval`
```powershell ```powershell
pnpm --dir web dev pnpm --dir web dev
# open http://127.0.0.1:5173/present # open http://127.0.0.1:5173/present
@@ -0,0 +1,116 @@
import { cleanup, render, screen } from "@testing-library/react";
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 { DemoWorkflowScene } from "./DemoWorkflowScene.js";
import { findBeat, findScene } from "./storyboard.js";
afterEach(() => cleanup());
const noop = () => {};
const noopAsync = async () => {};
const recording = loadCanonicalDemoRecording();
const demo: DemoTimelineController = {
state: {
mode: "replay",
phase: "review",
events: recording.events,
appliedCount: recording.events.length,
autoplay: false,
error: null,
},
inFlight: false,
interruptPayload: null,
output: null,
trace: null,
missingDeploymentMessage: null,
recordingId: recording.recordingId,
canStart: true,
setMode: noop,
start: noop,
pause: noop,
play: noop,
next: noopAsync,
submitSelectedIssues: noopAsync,
cancelReview: noopAsync,
restart: noop,
};
const requireSceneBeat = (sceneId: string, beatId: string) => {
const scene = findScene(sceneId);
const beat = findBeat(sceneId, beatId);
if (!scene || !beat) throw new Error(`missing storyboard fixture ${sceneId}/${beatId}`);
return { scene, beat };
};
const renderBeat = (
beatId: string,
sceneId = "workflow-demo",
openEvidence = vi.fn(),
) => {
const { scene, beat } = requireSceneBeat(sceneId, beatId);
const rendered = render(
<DemoWorkflowScene
scene={scene}
beat={beat}
demo={demo}
selectedNodeId={null}
selectNode={noop}
openEvidence={openEvidence}
/>,
);
return { ...rendered, openEvidence };
};
describe("DemoWorkflowScene", () => {
it("gives the start operation the stage and opens raw evidence on demand", async () => {
const { openEvidence } = renderBeat("operation");
expect(screen.getByLabelText("workflow.runs.start operation")).toBeInTheDocument();
expect(screen.queryByLabelText("workflow graph")).not.toBeInTheDocument();
await userEvent.click(screen.getByRole("button", { name: /view raw evidence/i }));
expect(openEvidence).toHaveBeenCalledOnce();
});
it("keeps the run receipt visible when the graph takes over", () => {
renderBeat("graph");
expect(screen.getByLabelText("workflow.runs.start execution receipt")).toBeInTheDocument();
expect(screen.getByText("run_recorded_lda_report")).toBeInTheDocument();
expect(screen.getByLabelText("workflow graph")).toBeInTheDocument();
});
it("renders the canonical typed interrupt contract", () => {
renderBeat("interrupt");
const contract = screen.getByLabelText("typed interrupt contract");
expect(contract).toHaveTextContent("issue_review");
expect(contract).toHaveTextContent("submitted / cancelled");
expect(contract).toHaveTextContent('"type": "object"');
expect(contract).toHaveTextContent("run_recorded_lda_report");
});
it("carries the same graph and contract into Scene 10 approval", () => {
renderBeat("approval", "interrupt-evidence");
expect(screen.getByLabelText("workflow graph")).toBeInTheDocument();
expect(screen.getByLabelText("typed interrupt contract")).toHaveTextContent(
"run_recorded_lda_report",
);
expect(screen.getByRole("button", { name: /issue review/i })).toHaveAttribute(
"data-execution-state",
"current",
);
});
it("renders resume and trace operations as expanded evidence summaries", () => {
const { unmount } = renderBeat("resume", "interrupt-evidence");
expect(screen.getByLabelText("workflow.runs.resume operation")).toBeInTheDocument();
unmount();
renderBeat("trace", "interrupt-evidence");
expect(screen.getByLabelText("workflow.runs.trace operation")).toBeInTheDocument();
});
});
@@ -0,0 +1,105 @@
import type { DemoEvent } from "../demo/timeline/models.js";
import type { DemoTimelineController } from "../demo/useDemoTimeline.js";
import {
graphExecutionForBeat,
projectInterruptContract,
} from "./demo-workflow-model.js";
import { InterruptContractPreview } from "./InterruptContractPreview.js";
import { NodeSpotlight } from "./NodeSpotlight.js";
import { OperationBlock } from "./OperationBlock.js";
import { StageCaption } from "./StageCaption.js";
import type { SceneBeatDefinition, SceneDefinition } from "./storyboard.js";
import { WorkflowGraphStage } from "./WorkflowGraphStage.js";
type DemoWorkflowSceneProps = {
readonly scene: SceneDefinition;
readonly beat: SceneBeatDefinition;
readonly demo: DemoTimelineController;
readonly selectedNodeId: string | null;
readonly selectNode: (nodeId: string) => void;
readonly openEvidence: () => void;
};
const operationStageByBeat: Readonly<Record<string, DemoEvent["stage"] | undefined>> = {
operation: "run_start",
resume: "run_resume",
trace: "trace_read",
};
const findEvent = (
demo: DemoTimelineController,
stage: DemoEvent["stage"],
): DemoEvent | null => demo.state.events.find((event) => event.stage === stage) ?? null;
export const DemoWorkflowScene = ({
scene,
beat,
demo,
selectedNodeId,
selectNode,
openEvidence,
}: DemoWorkflowSceneProps) => {
const runStart = findEvent(demo, "run_start");
const currentStage = operationStageByBeat[beat.id];
const currentEvent = currentStage ? findEvent(demo, currentStage) : null;
const contract = runStart ? projectInterruptContract(runStart) : null;
const execution = graphExecutionForBeat(beat.id);
const showExpandedOperation = beat.id === "operation" || beat.id === "resume" || beat.id === "trace";
const showGraph = beat.id === "graph" || beat.id === "interrupt" || beat.id === "approval" || beat.id === "output";
const showReceipt = showGraph;
const contractMode = beat.id === "approval"
? "approval"
: beat.id === "interrupt"
? "preview"
: null;
return (
<>
<StageCaption eyebrow="Live system walkthrough" title={scene.title}>
<p>{beat.caption}</p>
</StageCaption>
<div className="demo-workflow-stage" data-beat={beat.id}>
{showExpandedOperation && currentEvent && (
<OperationBlock
event={currentEvent}
variant="expanded"
openEvidence={openEvidence}
/>
)}
{showReceipt && runStart && (
<OperationBlock
event={runStart}
variant="receipt"
openEvidence={openEvidence}
/>
)}
{showGraph && (
<div className="demo-workflow-stage__graph">
<WorkflowGraphStage
execution={execution}
selectedNodeId={selectedNodeId}
selectNode={selectNode}
/>
{contractMode && contract && (
<InterruptContractPreview contract={contract} mode={contractMode} />
)}
</div>
)}
{showExpandedOperation && !currentEvent && (
<div className="demo-workflow-stage__pending" role="status">
Replay operation is not available yet.
</div>
)}
</div>
{selectedNodeId && (
<NodeSpotlight nodeId={selectedNodeId} close={() => selectNode("")} />
)}
</>
);
};
@@ -0,0 +1,41 @@
import { m } from "motion/react";
import type { InterruptContractPresentation } from "./demo-workflow-model.js";
import { formatJson } from "./format.js";
type InterruptContractPreviewProps = {
readonly contract: InterruptContractPresentation;
readonly mode: "preview" | "approval";
};
export const InterruptContractPreview = ({
contract,
mode,
}: InterruptContractPreviewProps) => (
<m.aside
className="interrupt-contract-preview"
data-mode={mode}
initial={{ opacity: 0, x: 10 }}
animate={{ opacity: 1, x: 0 }}
transition={{ duration: 0.22, ease: [0.16, 1, 0.3, 1] }}
aria-label="typed interrupt contract"
>
<header>
<span>{mode === "approval" ? "Operator decision" : "Execution paused"}</span>
<strong>{contract.kind}</strong>
</header>
<dl>
<div>
<dt>Persisted run</dt>
<dd><code>{contract.runId ?? "unavailable"}</code></dd>
</div>
<div>
<dt>Resume outcomes</dt>
<dd>{contract.outcomes.join(" / ")}</dd>
</div>
</dl>
<div className="interrupt-contract-preview__schema">
<span>Resume schema</span>
<pre><code>{formatJson(contract.resumeSchema)}</code></pre>
</div>
</m.aside>
);
@@ -1,5 +1,6 @@
import { cleanup, render, screen } from "@testing-library/react"; import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it } from "vitest"; import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, it, vi } from "vitest";
import type { DemoEvent } from "../demo/timeline/models.js"; import type { DemoEvent } from "../demo/timeline/models.js";
import { OperationBlock } from "./OperationBlock.js"; import { OperationBlock } from "./OperationBlock.js";
@@ -27,13 +28,42 @@ const event: DemoEvent = {
}; };
describe("OperationBlock", () => { describe("OperationBlock", () => {
it("shows command, raw response, and interpreted result", () => { it("shows command, interpreted summary, and evidence action in expanded mode", () => {
render(<OperationBlock event={event} />); render(<OperationBlock event={event} variant="expanded" openEvidence={vi.fn()} />);
expect(screen.getByText(/workflow.runs.start/i)).toBeInTheDocument(); expect(screen.getByText(/workflow.runs.start/i)).toBeInTheDocument();
expect(screen.getByText(/uv run wf run start/i)).toBeInTheDocument(); expect(screen.getByText(/uv run wf run start/i)).toBeInTheDocument();
expect(screen.getByText("Raw")).toBeInTheDocument(); expect(screen.getByText("Workflow operation")).toBeInTheDocument();
expect(screen.getByText("Interpreted")).toBeInTheDocument(); expect(screen.getAllByText(/interrupted/i).length).toBeGreaterThan(0);
expect(screen.getByText(/issue_review/i)).toBeInTheDocument();
expect(screen.getAllByText(/run_demo/i).length).toBeGreaterThan(0);
});
it("shows compact receipt with operation, status, duration, and run id", () => {
render(<OperationBlock event={event} variant="receipt" openEvidence={vi.fn()} />);
expect(screen.getByText(/workflow.runs.start/i)).toBeInTheDocument();
expect(screen.getByText(/interrupted/i)).toBeInTheDocument();
expect(screen.getByText(/88 ms/i)).toBeInTheDocument();
expect(screen.getByText(/run_demo/i)).toBeInTheDocument(); expect(screen.getByText(/run_demo/i)).toBeInTheDocument();
expect(screen.queryByText("Workflow operation")).not.toBeInTheDocument();
});
it("invokes onOpenEvidence when the evidence action is clicked", async () => {
const openEvidence = vi.fn();
render(<OperationBlock event={event} variant="expanded" openEvidence={openEvidence} />);
await userEvent.click(screen.getByText("View raw evidence"));
expect(openEvidence).toHaveBeenCalledOnce();
});
it("renders deployment and run ids as unavailable when missing", () => {
const eventWithoutIds: DemoEvent = {
...event,
resultingIds: { deploymentId: null, runId: null },
};
render(<OperationBlock event={eventWithoutIds} variant="expanded" openEvidence={vi.fn()} />);
expect(screen.getAllByText(/unavailable/i).length).toBeGreaterThan(0);
}); });
}); });
@@ -1,32 +1,97 @@
import { m } from "motion/react";
import type { DemoEvent } from "../demo/timeline/models.js"; import type { DemoEvent } from "../demo/timeline/models.js";
import { formatJson } from "./format.js"; import { projectOperationPresentation } from "./demo-workflow-model.js";
export type OperationVariant = "expanded" | "receipt";
type OperationBlockProps = { type OperationBlockProps = {
readonly event: DemoEvent; readonly event: DemoEvent;
readonly variant: OperationVariant;
readonly openEvidence: () => void;
}; };
export const OperationBlock = ({ event }: OperationBlockProps) => ( export const OperationBlock = ({
<section className="operation-block" aria-label={`${event.stage} operation`}> event,
<header> variant,
<p>{event.operation ?? event.stage}</p> openEvidence,
<small>{event.durationMs} ms</small> }: OperationBlockProps) => {
</header> const operation = projectOperationPresentation(event);
{event.equivalentCli && (
<pre className="operation-block__command"><code>{event.equivalentCli}</code></pre> if (variant === "receipt") {
)} return (
<div className="operation-block__grid"> <m.div
<section> layout
<h3>Raw</h3> layoutId="workflow-start-operation"
<pre><code>{formatJson(event.rawResponse)}</code></pre> transition={{ duration: 0.24, ease: [0.16, 1, 0.3, 1] }}
</section> className="operation-receipt"
<section> aria-label={`${operation.operation} execution receipt`}
<h3>Interpreted</h3> >
<pre><code>{formatJson(event.interpreted)}</code></pre> <div className="operation-receipt__identity">
</section> <span className="operation-receipt__pulse" aria-hidden="true" />
</div> <strong>{operation.operation}</strong>
<footer> <span className="operation-status" data-status={operation.status}>
<span>Deployment: {event.resultingIds.deploymentId}</span> {operation.status}
{event.resultingIds.runId && <span>Run: {event.resultingIds.runId}</span>} </span>
</footer> </div>
</section> <code>{operation.runId ?? "run unavailable"}</code>
); <small>{operation.durationMs} ms</small>
</m.div>
);
}
return (
<m.article
layout
layoutId="workflow-start-operation"
transition={{ duration: 0.24, ease: [0.16, 1, 0.3, 1] }}
className="operation-block operation-block--expanded"
aria-label={`${operation.operation} operation`}
>
<header className="operation-block__header">
<div>
<span className="operation-block__kicker">Workflow operation</span>
<strong>{operation.operation}</strong>
</div>
<div className="operation-block__status-group">
<span className="operation-status" data-status={operation.status}>
{operation.status}
</span>
<small>{operation.durationMs} ms</small>
</div>
</header>
{operation.command && (
<div className="operation-command">
<span aria-hidden="true">$</span>
<code>{operation.command}</code>
</div>
)}
<dl className="operation-summary">
<div>
<dt>Deployment</dt>
<dd><code>{operation.deploymentId ?? "unavailable"}</code></dd>
</div>
<div>
<dt>Run</dt>
<dd><code>{operation.runId ?? "unavailable"}</code></dd>
</div>
<div>
<dt>Boundary</dt>
<dd data-emphasis={operation.interruptKind ? "interrupt" : undefined}>
{operation.interruptKind ?? "none"}
</dd>
</div>
</dl>
<button
type="button"
className="operation-block__evidence-action"
onClick={openEvidence}
>
View raw evidence
<span aria-hidden="true">-&gt;</span>
</button>
</m.article>
);
};
@@ -1,3 +1,4 @@
import { m } from "motion/react";
import { PREPARE_THESIS_REPORT_RECIPE } from "../demo/agent/recipes.js"; import { PREPARE_THESIS_REPORT_RECIPE } from "../demo/agent/recipes.js";
import type { AgentMessage, AgentMessagePart } from "../demo/agent/events.js"; import type { AgentMessage, AgentMessagePart } from "../demo/agent/events.js";
import type { PresentationState } from "./presentation-state.js"; import type { PresentationState } from "./presentation-state.js";
@@ -29,23 +30,36 @@ const fallbackMessages = (state: PresentationState): ReadonlyArray<AgentMessage>
const renderPart = ( const renderPart = (
part: AgentMessagePart, part: AgentMessagePart,
index: number, key: string,
onApprove?: () => void, onApprove?: () => void,
onDeny?: () => void, onDeny?: () => void,
) => { ) => {
switch (part.type) { switch (part.type) {
case "text": case "text":
return <p key={index}>{part.text}</p>; return <p key={key}>{part.text}</p>;
case "tool-call": case "tool-call":
if (part.call.name === "startPreparedReportRun") {
return (
<m.div
key={key}
layout
layoutId="workflow-start-operation"
className="chat-tool-part chat-tool-part--handoff"
>
<span>Workflow operation</span>
<code>{part.call.name}</code>
</m.div>
);
}
return ( return (
<div key={index} className="chat-tool-part"> <div key={key} className="chat-tool-part">
<span>Tool call</span> <span>Tool call</span>
<code>{part.call.name}</code> <code>{part.call.name}</code>
</div> </div>
); );
case "tool-result": case "tool-result":
return ( return (
<div key={index} className="chat-tool-part chat-tool-part--result"> <div key={key} className="chat-tool-part chat-tool-part--result">
<span>Tool result</span> <span>Tool result</span>
<code>{part.result.name}</code> <code>{part.result.name}</code>
<small>{part.result.status}</small> <small>{part.result.status}</small>
@@ -53,14 +67,14 @@ const renderPart = (
); );
case "presentation-action": case "presentation-action":
return ( return (
<div key={index} className="chat-tool-part chat-tool-part--presentation"> <div key={key} className="chat-tool-part chat-tool-part--presentation">
<span>Presentation action</span> <span>Presentation action</span>
<code>{part.action.type}</code> <code>{part.action.type}</code>
</div> </div>
); );
case "approval-request": case "approval-request":
return ( return (
<div key={index} className="chat-tool-part chat-tool-part--approval"> <div key={key} className="chat-tool-part chat-tool-part--approval">
<span>Approval required</span> <span>Approval required</span>
<code>{part.name}</code> <code>{part.name}</code>
<p>{part.prompt}</p> <p>{part.prompt}</p>
@@ -71,21 +85,38 @@ const renderPart = (
</div> </div>
); );
case "error": case "error":
return <p key={index} className="chat-error">{part.message}</p>; return <p key={key} className="chat-error">{part.message}</p>;
default: default:
return null; return null;
} }
}; };
const partKey = (messageId: string, part: AgentMessagePart): string => {
switch (part.type) {
case "text":
return `${messageId}-text-${part.text}`;
case "tool-call":
return `${messageId}-call-${part.call.id}`;
case "tool-result":
return `${messageId}-result-${part.result.callId}`;
case "presentation-action":
return `${messageId}-action-${JSON.stringify(part.action)}`;
case "approval-request":
return `${messageId}-approval-${part.callId}`;
case "error":
return `${messageId}-error-${part.message}`;
}
};
export const OperatorChat = ({ state, messages, onApprove, onDeny }: OperatorChatProps) => { export const OperatorChat = ({ state, messages, onApprove, onDeny }: OperatorChatProps) => {
const visibleMessages = messages && messages.length > 0 ? messages : fallbackMessages(state); const visibleMessages = messages && messages.length > 0 ? messages : fallbackMessages(state);
const composition = compositionForState(state); const composition = compositionForState(state);
return ( return (
<aside className="operator-chat" data-mode={composition.chatMode} aria-label="scripted operator chat"> <aside className="operator-chat" data-mode={composition.chatMode} data-chat-theme={composition.chatTheme} aria-label="scripted operator chat">
{visibleMessages.map((message) => ( {visibleMessages.map((message) => (
<div key={message.id} className={`chat-message chat-message--${message.role === "user" ? "operator" : "system"}`}> <div key={message.id} className={`chat-message chat-message--${message.role === "user" ? "operator" : "system"}`}>
<strong>{message.role === "user" ? "Operator" : "lda.chat"}</strong> <strong>{message.role === "user" ? "Operator" : "lda.chat"}</strong>
{message.parts.map((part, index) => renderPart(part, index, onApprove, onDeny))} {message.parts.map((part) => renderPart(part, partKey(message.id, part), onApprove, onDeny))}
</div> </div>
))} ))}
</aside> </aside>
@@ -16,6 +16,7 @@ import { hashForLocation } from "./storyboard-navigation.js";
import { findScene } from "./storyboard.js"; import { findScene } from "./storyboard.js";
import type { PresentationLocation } from "./storyboard.js"; import type { PresentationLocation } from "./storyboard.js";
import "./presentation.css"; import "./presentation.css";
import "./styles/demo-workflow.css";
const projectRecordingToEvidence = ( const projectRecordingToEvidence = (
recording: import("../demo/timeline/models.js").DemoRecording, recording: import("../demo/timeline/models.js").DemoRecording,
@@ -1,3 +1,4 @@
import { domAnimation, LayoutGroup, LazyMotion } from "motion/react";
import type { EvidenceRecord } from "../app/state.js"; import type { EvidenceRecord } from "../app/state.js";
import type { AgentMessage } from "../demo/agent/events.js"; import type { AgentMessage } from "../demo/agent/events.js";
import { SceneBody } from "./SceneBody.js"; import { SceneBody } from "./SceneBody.js";
@@ -45,54 +46,63 @@ export const PresentationStage = ({
const composition = compositionForState(state); const composition = compositionForState(state);
const isMainScene = state.location.kind === "main"; const isMainScene = state.location.kind === "main";
const activeSceneView = isMainScene
? findScene(state.location.sceneId)?.view ?? "unknown"
: "discussion";
return ( return (
<div <LazyMotion features={domAnimation}>
className="presentation-stage" <LayoutGroup id="presentation-stage">
data-stage-theme={composition.stageTheme} <div
data-chat-theme={composition.chatTheme} className="presentation-stage"
data-chat-mode={composition.chatMode} data-stage-theme={composition.stageTheme}
data-evidence-mode={composition.evidenceMode} data-chat-theme={composition.chatTheme}
> data-chat-mode={composition.chatMode}
<aside className="presentation-stage__chat" aria-label="agent chat region"> data-evidence-mode={composition.evidenceMode}
<OperatorChat state={state} messages={messages} onApprove={onApprove} onDeny={onDeny} /> data-scene-view={activeSceneView}
</aside> >
<section className="presentation-stage__primary" aria-label="primary presentation region"> <aside className="presentation-stage__chat" aria-label="agent chat region">
{state.location.kind === "discussion" ? ( <OperatorChat state={state} messages={messages} onApprove={onApprove} onDeny={onDeny} />
<DiscussionPanel branchId={state.location.branchId} onClose={closeDiscussion} /> </aside>
) : ( <section className="presentation-stage__primary" aria-label="primary presentation region">
<> {state.location.kind === "discussion" ? (
<SceneBody <DiscussionPanel branchId={state.location.branchId} onClose={closeDiscussion} />
location={state.location} ) : (
demo={demo} <>
selectedNodeId={state.selectedNodeId} <SceneBody
selectNode={selectNode} location={state.location}
/> demo={demo}
<button selectedNodeId={state.selectedNodeId}
type="button" selectNode={selectNode}
className="presentation-stage__discussion-toggle" openEvidence={openEvidence}
onClick={toggleDiscussionIndex} />
> <button
Discussion topics type="button"
</button> className="presentation-stage__discussion-toggle"
</> onClick={toggleDiscussionIndex}
)} >
{state.discussionIndexOpen && ( Discussion topics
<DiscussionIndex </button>
onSelect={(branchId) => { </>
toggleDiscussionIndex(); )}
openDiscussion(branchId); {state.discussionIndexOpen && (
}} <DiscussionIndex
/> onSelect={(branchId) => {
)} toggleDiscussionIndex();
<p className="presentation-stage__mode"> openDiscussion(branchId);
{demo.state.mode === "replay" ? "Replay" : "Live"} · {demo.state.phase} }}
</p> />
</section> )}
<aside className="presentation-stage__evidence" aria-label="evidence region"> <p className="presentation-stage__mode">
<EvidenceDrawer records={evidence} mode={composition.evidenceMode} close={closeOverlay} /> {demo.state.mode === "replay" ? "Replay" : "Live"} · {demo.state.phase}
</aside> </p>
<SceneRail location={state.location} jump={jump} /> </section>
</div> <aside className="presentation-stage__evidence" aria-label="evidence region">
<EvidenceDrawer records={evidence} mode={composition.evidenceMode} close={closeOverlay} />
</aside>
<SceneRail location={state.location} jump={jump} />
</div>
</LayoutGroup>
</LazyMotion>
); );
}; };
@@ -1,9 +1,38 @@
import { cleanup, render, screen } from "@testing-library/react"; import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it } from "vitest"; import { afterEach, describe, expect, it } from "vitest";
import { loadCanonicalDemoRecording } from "../demo/timeline/replay.js";
import type { DemoTimelineController } from "../demo/useDemoTimeline.js";
import { SceneBody } from "./SceneBody.js"; import { SceneBody } from "./SceneBody.js";
import type { PresentationLocation } from "./storyboard.js"; import type { PresentationLocation } from "./storyboard.js";
const noop = () => {}; const noop = () => {};
const noopAsync = async () => {};
const recording = loadCanonicalDemoRecording();
const demo: DemoTimelineController = {
state: {
mode: "replay",
phase: "review",
events: recording.events,
appliedCount: recording.events.length,
autoplay: false,
error: null,
},
inFlight: false,
interruptPayload: null,
output: null,
trace: null,
missingDeploymentMessage: null,
recordingId: recording.recordingId,
canStart: true,
setMode: noop,
start: noop,
pause: noop,
play: noop,
next: noopAsync,
submitSelectedIssues: noopAsync,
cancelReview: noopAsync,
restart: noop,
};
afterEach(() => cleanup()); afterEach(() => cleanup());
@@ -13,9 +42,10 @@ describe("SceneBody", () => {
render( render(
<SceneBody <SceneBody
location={location} location={location}
demo={null as never} demo={demo}
selectedNodeId={null} selectedNodeId={null}
selectNode={noop} selectNode={noop}
openEvidence={noop}
/>, />,
); );
expect(screen.getByRole("heading", { name: /Positioning and Related Systems/i })).toBeInTheDocument(); expect(screen.getByRole("heading", { name: /Positioning and Related Systems/i })).toBeInTheDocument();
@@ -27,9 +57,10 @@ describe("SceneBody", () => {
render( render(
<SceneBody <SceneBody
location={location} location={location}
demo={null as never} demo={demo}
selectedNodeId={null} selectedNodeId={null}
selectNode={noop} selectNode={noop}
openEvidence={noop}
/>, />,
); );
expect(screen.getByLabelText(/workflow graph/i)).toBeInTheDocument(); expect(screen.getByLabelText(/workflow graph/i)).toBeInTheDocument();
@@ -1,23 +1,14 @@
import type { DemoTimelineController } from "../demo/useDemoTimeline.js"; import type { DemoTimelineController } from "../demo/useDemoTimeline.js";
import { findBeat, findScene, type PresentationLocation, type SceneDefinition, type SceneBeatDefinition } from "./storyboard.js"; import { findBeat, findScene, type PresentationLocation, type SceneDefinition, type SceneBeatDefinition } from "./storyboard.js";
import { NodeSpotlight } from "./NodeSpotlight.js"; import { DemoWorkflowScene } from "./DemoWorkflowScene.js";
import { OperationBlock } from "./OperationBlock.js";
import { StageCaption } from "./StageCaption.js"; import { StageCaption } from "./StageCaption.js";
import { WorkflowGraphStage } from "./WorkflowGraphStage.js";
type SceneBodyProps = { type SceneBodyProps = {
readonly location: PresentationLocation; readonly location: PresentationLocation;
readonly demo: DemoTimelineController; readonly demo: DemoTimelineController;
readonly selectedNodeId: string | null; readonly selectedNodeId: string | null;
readonly selectNode: (nodeId: string) => void; readonly selectNode: (nodeId: string) => void;
}; readonly openEvidence: () => void;
const operationStageByBeat: Readonly<Record<string, string | undefined>> = {
operation: "run_start",
interrupt: "interrupt",
approval: "interrupt",
resume: "run_resume",
trace: "trace_read",
}; };
const NarrativeScene = ({ scene, beat }: { scene: SceneDefinition; beat: SceneBeatDefinition }) => ( const NarrativeScene = ({ scene, beat }: { scene: SceneDefinition; beat: SceneBeatDefinition }) => (
@@ -206,41 +197,11 @@ const AgentHandoffScene = ({ scene, beat }: { scene: SceneDefinition; beat: Scen
</> </>
); );
const DemoWorkflowScene = ({
scene,
beat,
demo,
selectedNodeId,
selectNode,
}: {
scene: SceneDefinition;
beat: SceneBeatDefinition;
demo: DemoTimelineController;
selectedNodeId: string | null;
selectNode: (nodeId: string) => void;
}) => {
const operationStage = operationStageByBeat[beat.id] ?? null;
const operationEvent = operationStage
? demo.state.events.find((event) => event.stage === operationStage) ?? null
: null;
return (
<>
<StageCaption eyebrow="Demo" title={scene.title}>
<p>{beat.caption}</p>
</StageCaption>
{operationEvent && <OperationBlock event={operationEvent} />}
<WorkflowGraphStage selectedNodeId={selectedNodeId} selectNode={selectNode} />
{selectedNodeId && <NodeSpotlight nodeId={selectedNodeId} close={() => selectNode("")} />}
</>
);
};
const assertNever = (value: never): never => { const assertNever = (value: never): never => {
throw new Error(`Unexpected view: ${value}`); throw new Error(`Unexpected view: ${value}`);
}; };
export const SceneBody = ({ location, demo, selectedNodeId, selectNode }: SceneBodyProps) => { export const SceneBody = ({ location, demo, selectedNodeId, selectNode, openEvidence }: SceneBodyProps) => {
const sceneId = location.kind === "main" ? location.sceneId : "positioning"; const sceneId = location.kind === "main" ? location.sceneId : "positioning";
const beatId = location.kind === "main" ? location.beatId : "landscape"; const beatId = location.kind === "main" ? location.beatId : "landscape";
const scene = findScene(sceneId) ?? findScene("thesis")!; const scene = findScene(sceneId) ?? findScene("thesis")!;
@@ -269,6 +230,7 @@ export const SceneBody = ({ location, demo, selectedNodeId, selectNode }: SceneB
demo={demo} demo={demo}
selectedNodeId={selectedNodeId} selectedNodeId={selectedNodeId}
selectNode={selectNode} selectNode={selectNode}
openEvidence={openEvidence}
/> />
); );
case "evaluation": case "evaluation":
@@ -8,9 +8,54 @@ afterEach(() => cleanup());
describe("WorkflowGraphStage", () => { describe("WorkflowGraphStage", () => {
it("renders curated workflow nodes and allows node selection", async () => { it("renders curated workflow nodes and allows node selection", async () => {
const selectNode = vi.fn(); const selectNode = vi.fn();
render(<WorkflowGraphStage selectedNodeId={null} selectNode={selectNode} />); render(
<WorkflowGraphStage
execution={{ completedNodeIds: [], currentNodeId: null }}
selectedNodeId={null}
selectNode={selectNode}
/>,
);
await userEvent.click(screen.getByRole("button", { name: /issue review/i })); await userEvent.click(screen.getByRole("button", { name: /issue review/i }));
expect(selectNode).toHaveBeenCalledWith("review_issues"); expect(selectNode).toHaveBeenCalledWith("review_issues");
}); });
it("distinguishes completed, current interrupt, and future nodes semantically", () => {
render(
<WorkflowGraphStage
execution={{
completedNodeIds: ["read_docs", "build_report"],
currentNodeId: "review_issues",
}}
selectedNodeId={null}
selectNode={vi.fn()}
/>,
);
const readDocs = screen.getByRole("button", { name: /read documents/i });
const buildReport = screen.getByRole("button", { name: /build report/i });
const reviewIssues = screen.getByRole("button", { name: /issue review/i });
const createIssues = screen.getByRole("button", { name: /create issues/i });
expect(readDocs).toHaveAttribute("data-execution-state", "completed");
expect(buildReport).toHaveAttribute("data-execution-state", "completed");
expect(reviewIssues).toHaveAttribute("data-execution-state", "current");
expect(reviewIssues).toHaveAttribute("data-current-interrupt", "true");
expect(reviewIssues).toHaveTextContent("Current interrupt");
expect(createIssues).toHaveAttribute("data-execution-state", "future");
});
it("renders connectors between nodes", () => {
render(
<WorkflowGraphStage
execution={{ completedNodeIds: ["read_docs"], currentNodeId: "build_report" }}
selectedNodeId={null}
selectNode={vi.fn()}
/>,
);
const connectors = screen.getAllByTestId("workflow-connector");
expect(connectors).toHaveLength(4);
expect(connectors.filter((connector) => connector.dataset.active === "true")).toHaveLength(1);
});
}); });
@@ -1,39 +1,131 @@
import { m } from "motion/react";
import type { GraphExecutionPresentation } from "./demo-workflow-model.js";
export type PresentationNode = { export type PresentationNode = {
readonly id: string; readonly id: string;
readonly label: string; readonly label: string;
readonly detail: string;
readonly kind: "node" | "interrupt" | "end"; readonly kind: "node" | "interrupt" | "end";
readonly x: number; readonly x: number;
readonly y: number; readonly y: number;
}; };
export const presentationNodes: readonly PresentationNode[] = [ export const presentationNodes: ReadonlyArray<PresentationNode> = [
{ id: "read_docs", label: "Read docs", kind: "node", x: 8, y: 45 }, { id: "read_docs", label: "Read documents", detail: "5 selected", kind: "node", x: 9, y: 57 },
{ id: "build_report", label: "Build report", kind: "node", x: 30, y: 30 }, { id: "build_report", label: "Build report", detail: "Markdown", kind: "node", x: 30, y: 35 },
{ id: "review_issues", label: "Issue review", kind: "interrupt", x: 52, y: 45 }, { id: "review_issues", label: "Issue review", detail: "Typed interrupt", kind: "interrupt", x: 52, y: 57 },
{ id: "create_issues", label: "Create issues", kind: "node", x: 74, y: 30 }, { id: "create_issues", label: "Create issues", detail: "Selected only", kind: "node", x: 74, y: 35 },
{ id: "end_completed", label: "Completed", kind: "end", x: 92, y: 45 }, { id: "end_completed", label: "Completed", detail: "Persisted run", kind: "end", x: 92, y: 57 },
]; ];
type PresentationEdge = readonly [from: string, to: string];
const presentationEdges: ReadonlyArray<PresentationEdge> = [
["read_docs", "build_report"],
["build_report", "review_issues"],
["review_issues", "create_issues"],
["create_issues", "end_completed"],
];
type NodeExecutionState = "completed" | "current" | "future";
type WorkflowGraphStageProps = { type WorkflowGraphStageProps = {
readonly execution: GraphExecutionPresentation;
readonly selectedNodeId: string | null; readonly selectedNodeId: string | null;
readonly selectNode: (nodeId: string) => void; readonly selectNode: (nodeId: string) => void;
}; };
export const WorkflowGraphStage = ({ selectedNodeId, selectNode }: WorkflowGraphStageProps) => ( const executionStateForNode = (
<section className="workflow-graph-stage" aria-label="workflow graph"> nodeId: string,
{presentationNodes.map((node) => ( execution: GraphExecutionPresentation,
<button ): NodeExecutionState => {
key={node.id} if (execution.currentNodeId === nodeId) return "current";
type="button" if (execution.completedNodeIds.includes(nodeId)) return "completed";
className="workflow-graph-stage__node" return "future";
data-kind={node.kind} };
data-selected={selectedNodeId === node.id}
aria-pressed={selectedNodeId === node.id} const requireNode = (nodeId: string): PresentationNode => {
style={{ left: `${node.x}%`, top: `${node.y}%` }} const node = presentationNodes.find((candidate) => candidate.id === nodeId);
onClick={() => selectNode(node.id)} if (!node) throw new Error(`presentation edge references unknown node ${nodeId}`);
> return node;
{node.label} };
</button>
))} export const WorkflowGraphStage = ({
</section> execution,
selectedNodeId,
selectNode,
}: WorkflowGraphStageProps) => (
<div className="workflow-graph-stage" role="group" aria-label="workflow graph">
<div className="workflow-graph-stage__legend" aria-hidden="true">
<span><i data-state="completed" />Completed</span>
<span><i data-state="current" />Current</span>
<span><i data-state="interrupt" />Human boundary</span>
</div>
<svg className="workflow-graph-stage__connectors" aria-hidden="true">
<defs>
<marker id="workflow-arrow" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" />
</marker>
<marker id="workflow-arrow-active" markerWidth="8" markerHeight="6" refX="8" refY="3" orient="auto">
<polygon points="0 0, 8 3, 0 6" />
</marker>
</defs>
{presentationEdges.map(([fromId, toId]) => {
const from = requireNode(fromId);
const to = requireNode(toId);
const active = execution.completedNodeIds.includes(from.id);
return (
<line
key={`${fromId}-${toId}`}
data-testid="workflow-connector"
data-active={active}
x1={`${from.x}%`}
y1={`${from.y}%`}
x2={`${to.x}%`}
y2={`${to.y}%`}
markerEnd={active ? "url(#workflow-arrow-active)" : "url(#workflow-arrow)"}
/>
);
})}
</svg>
{presentationNodes.map((node, index) => {
const executionState = executionStateForNode(node.id, execution);
const currentInterrupt = executionState === "current" && node.kind === "interrupt";
const stateLabel = currentInterrupt
? "Current interrupt"
: executionState === "current"
? "Current"
: executionState === "completed"
? "Completed"
: "Queued";
return (
<m.div
key={node.id}
className="workflow-graph-stage__node-slot"
style={{ left: `${node.x}%`, top: `${node.y}%` }}
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 0.2, delay: index * 0.025 }}
>
<button
type="button"
className="workflow-graph-stage__node"
data-kind={node.kind}
data-execution-state={executionState}
data-current-interrupt={currentInterrupt}
data-selected={selectedNodeId === node.id}
aria-pressed={selectedNodeId === node.id}
aria-label={`${node.label}, ${stateLabel}`}
onClick={() => selectNode(node.id)}
>
<span className="workflow-graph-stage__node-state">{stateLabel}</span>
<strong>{node.label}</strong>
<small>{node.detail}</small>
</button>
</m.div>
);
})}
</div>
); );
@@ -0,0 +1,72 @@
import { describe, expect, it } from "vitest";
import type { DemoEvent } from "../demo/timeline/models.js";
import { loadCanonicalDemoRecording } from "../demo/timeline/replay.js";
import {
graphExecutionForBeat,
projectInterruptContract,
projectOperationPresentation,
} from "./demo-workflow-model.js";
const requireEvent = (stage: DemoEvent["stage"]): DemoEvent => {
const event = loadCanonicalDemoRecording().events.find((candidate) => candidate.stage === stage);
if (!event) throw new Error(`canonical recording is missing ${stage}`);
return event;
};
describe("demo workflow presentation model", () => {
it("projects the canonical run without inventing display facts", () => {
const event = requireEvent("run_start");
expect(projectOperationPresentation(event)).toEqual({
operation: "workflow.runs.start",
status: "interrupted",
durationMs: 88,
command: "uv run wf run start lda_report_case_study.default --input '<json>'",
deploymentId: "lda_report_case_study.default",
runId: "run_recorded_lda_report",
interruptKind: "issue_review",
});
});
it("projects the canonical typed interrupt contract", () => {
expect(projectInterruptContract(requireEvent("run_start"))).toEqual({
kind: "issue_review",
outcomes: ["submitted", "cancelled"],
resumeSchema: { type: "object" },
runId: "run_recorded_lda_report",
});
});
it("degrades malformed interpreted data to bounded fallbacks", () => {
const sparse: DemoEvent = {
...requireEvent("run_start"),
operation: null,
equivalentCli: null,
interpreted: "malformed",
rawResponse: null,
};
expect(projectOperationPresentation(sparse)).toMatchObject({
operation: "run_start",
status: "unknown",
command: null,
interruptKind: null,
});
expect(projectInterruptContract(sparse)).toBeNull();
});
it("maps storyboard beats to one continuous execution", () => {
expect(graphExecutionForBeat("graph")).toEqual({
completedNodeIds: ["read_docs"],
currentNodeId: "build_report",
});
expect(graphExecutionForBeat("interrupt")).toEqual({
completedNodeIds: ["read_docs", "build_report"],
currentNodeId: "review_issues",
});
expect(graphExecutionForBeat("approval")).toEqual({
completedNodeIds: ["read_docs", "build_report"],
currentNodeId: "review_issues",
});
});
});
@@ -0,0 +1,114 @@
import * as v from "valibot";
import type { DemoEvent } from "../demo/timeline/models.js";
const InterruptProjectionSchema = v.looseObject({
kind: v.string(),
outcomes: v.optional(v.array(v.string())),
resume_schema: v.optional(v.unknown()),
});
const RunInterpretationSchema = v.looseObject({
status: v.optional(v.string()),
interrupt: v.optional(v.nullable(InterruptProjectionSchema)),
});
const RawRunResponseSchema = v.looseObject({
result: v.optional(v.looseObject({
status: v.optional(v.string()),
})),
});
export type OperationPresentation = {
readonly operation: string;
readonly status: string;
readonly durationMs: number;
readonly command: string | null;
readonly deploymentId: string | null;
readonly runId: string | null;
readonly interruptKind: string | null;
};
export type InterruptContractPresentation = {
readonly kind: string;
readonly outcomes: ReadonlyArray<string>;
readonly resumeSchema: unknown;
readonly runId: string | null;
};
export type GraphExecutionPresentation = {
readonly completedNodeIds: ReadonlyArray<string>;
readonly currentNodeId: string | null;
};
const decodeInterpretation = (event: DemoEvent) => {
const decoded = v.safeParse(RunInterpretationSchema, event.interpreted);
return decoded.success ? decoded.output : null;
};
const decodeRawResponse = (event: DemoEvent) => {
const decoded = v.safeParse(RawRunResponseSchema, event.rawResponse);
return decoded.success ? decoded.output : null;
};
/**
* Produces bounded display data from replay evidence. A malformed optional
* detail must not crash the defense route, but it also must not be invented.
*/
export const projectOperationPresentation = (
event: DemoEvent,
): OperationPresentation => {
const interpretation = decodeInterpretation(event);
const rawResponse = decodeRawResponse(event);
return {
operation: event.operation ?? event.stage,
status: interpretation?.status ?? rawResponse?.result?.status ?? "unknown",
durationMs: event.durationMs,
command: event.equivalentCli,
deploymentId: event.resultingIds.deploymentId,
runId: event.resultingIds.runId,
interruptKind: interpretation?.interrupt?.kind ?? null,
};
};
export const projectInterruptContract = (
event: DemoEvent,
): InterruptContractPresentation | null => {
const interrupt = decodeInterpretation(event)?.interrupt;
if (!interrupt || !interrupt.outcomes || interrupt.resume_schema === undefined) return null;
return {
kind: interrupt.kind,
outcomes: interrupt.outcomes,
resumeSchema: interrupt.resume_schema,
runId: event.resultingIds.runId,
};
};
export const graphExecutionForBeat = (
beatId: string,
): GraphExecutionPresentation => {
switch (beatId) {
case "operation":
return { completedNodeIds: [], currentNodeId: "read_docs" };
case "graph":
return { completedNodeIds: ["read_docs"], currentNodeId: "build_report" };
case "interrupt":
case "approval":
return {
completedNodeIds: ["read_docs", "build_report"],
currentNodeId: "review_issues",
};
case "resume":
return {
completedNodeIds: ["read_docs", "build_report", "review_issues"],
currentNodeId: "create_issues",
};
case "output":
case "trace":
return {
completedNodeIds: ["read_docs", "build_report", "review_issues", "create_issues"],
currentNodeId: "end_completed",
};
default:
return { completedNodeIds: [], currentNodeId: null };
}
};
@@ -1,19 +1,30 @@
:root {
--chat-width: 18rem;
--evidence-width: 0rem;
}
body {
margin: 0;
padding: 0;
}
.presentation-route { .presentation-route {
height: 100dvh; height: 100dvh;
min-height: 0; min-height: 0;
overflow: hidden; overflow: hidden;
background: oklch(0.12 0.025 250); background: var(--stage-canvas, oklch(0.12 0.025 250));
color: oklch(0.96 0.01 250); color: var(--text-primary, oklch(0.96 0.01 250));
/* Presentation tokens scoped to presentation route */
--stage-canvas: oklch(0.12 0.025 250);
--stage-surface: oklch(0.16 0.025 250);
--stage-inset: oklch(0.14 0.025 250);
--stage-line: oklch(0.36 0.04 250);
--text-primary: oklch(0.96 0.01 250);
--text-secondary: oklch(0.78 0.035 250);
--text-muted: oklch(0.65 0.03 250);
--accent-cyan: oklch(0.72 0.17 195);
--accent-amber: oklch(0.76 0.18 70);
--success: oklch(0.7 0.16 145);
--failure: oklch(0.65 0.2 25);
--chat-width: 18rem;
--evidence-width: 0rem;
/* Paper theme tokens */
--paper-bg: oklch(0.97 0.005 80);
--paper-text: oklch(0.15 0.01 80);
--paper-muted: oklch(0.5 0.03 80);
--paper-accent: oklch(0.45 0.14 80);
--paper-border: oklch(0.75 0.04 80);
--paper-surface: oklch(0.92 0.01 80);
} }
.presentation-stage { .presentation-stage {
@@ -28,22 +39,26 @@ body {
/* Stage themes */ /* Stage themes */
.presentation-stage[data-stage-theme="paper"] { .presentation-stage[data-stage-theme="paper"] {
background: oklch(0.97 0.005 80); background: var(--paper-bg);
color: oklch(0.15 0.01 80); color: var(--paper-text);
} }
.presentation-stage[data-stage-theme="night"] { .presentation-stage[data-stage-theme="night"] {
background: oklch(0.12 0.025 250); background: var(--stage-canvas);
color: oklch(0.96 0.01 250); color: var(--text-primary);
} }
/* Paper theme child-element overrides */ /* Paper theme child-element overrides */
.presentation-stage[data-stage-theme="paper"] .stage-caption__eyebrow { .presentation-stage[data-stage-theme="paper"] .stage-caption__eyebrow {
color: oklch(0.45 0.04 80); color: var(--paper-muted);
}
.presentation-stage[data-stage-theme="paper"] .stage-caption h1 {
color: var(--paper-text);
} }
.presentation-stage[data-stage-theme="paper"] .scene-body__evidence { .presentation-stage[data-stage-theme="paper"] .scene-body__evidence {
color: oklch(0.5 0.03 80); color: var(--paper-muted);
} }
.presentation-stage[data-stage-theme="paper"] .scene-rail__scene { .presentation-stage[data-stage-theme="paper"] .scene-rail__scene {
@@ -168,16 +183,37 @@ body {
} }
/* Chat themes */ /* Chat themes */
.presentation-stage[data-chat-theme="light"] .presentation-stage__chat { .presentation-stage[data-chat-theme="light"] .presentation-stage__chat,
.operator-chat[data-chat-theme="light"] {
background: oklch(0.96 0.005 80); background: oklch(0.96 0.005 80);
color: oklch(0.18 0.01 80); color: oklch(0.18 0.01 80);
} }
.presentation-stage[data-chat-theme="dark"] .presentation-stage__chat { .presentation-stage[data-chat-theme="dark"] .presentation-stage__chat,
.operator-chat[data-chat-theme="dark"] {
background: oklch(0.14 0.025 250); background: oklch(0.14 0.025 250);
color: oklch(0.92 0.01 250); color: oklch(0.92 0.01 250);
} }
/* Light chat message overrides */
.presentation-stage[data-chat-theme="light"] .chat-message,
.operator-chat[data-chat-theme="light"] .chat-message {
border-color: oklch(0.75 0.04 80);
background: oklch(0.92 0.01 80);
color: oklch(0.18 0.01 80);
}
/* Light chat tool-part overrides */
.presentation-stage[data-chat-theme="light"] .chat-tool-part,
.operator-chat[data-chat-theme="light"] .chat-tool-part {
border-color: oklch(0.75 0.04 80);
}
.presentation-stage[data-chat-theme="light"] .chat-tool-part span,
.operator-chat[data-chat-theme="light"] .chat-tool-part span {
color: oklch(0.45 0.03 80);
}
/* Chat modes */ /* Chat modes */
.presentation-stage[data-chat-mode="hidden"] { .presentation-stage[data-chat-mode="hidden"] {
grid-template: grid-template:
@@ -331,8 +367,8 @@ body {
} }
.operation-block { .operation-block {
border: 1px solid oklch(0.36 0.04 250); border: 1px solid var(--stage-line);
background: oklch(0.16 0.025 250); background: var(--stage-surface);
padding: 0.75rem; padding: 0.75rem;
border-radius: 0.65rem; border-radius: 0.65rem;
flex-shrink: 0; flex-shrink: 0;
@@ -343,10 +379,33 @@ body {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
gap: 0.75rem; gap: 0.75rem;
color: oklch(0.78 0.035 250); color: var(--text-secondary);
font-size: 0.8rem; font-size: 0.8rem;
} }
.operation-block__name {
font-weight: 600;
margin: 0;
}
.operation-block__status {
font-size: 0.75rem;
padding: 0.1rem 0.4rem;
border-radius: 0.3rem;
text-transform: uppercase;
letter-spacing: 0.05em;
}
.operation-block__status[data-status="interrupted"] {
background: oklch(0.76 0.18 70 / 0.2);
color: var(--accent-amber);
}
.operation-block__status[data-status="completed"] {
background: oklch(0.7 0.16 145 / 0.2);
color: var(--success);
}
.operation-block__command, .operation-block__command,
.operation-block pre { .operation-block pre {
white-space: pre-wrap; white-space: pre-wrap;
@@ -354,44 +413,166 @@ body {
font-size: 0.8rem; font-size: 0.8rem;
} }
.operation-block__grid { .operation-block__command {
display: grid; margin: 0.5rem 0;
grid-template-columns: repeat(2, minmax(0, 1fr)); padding: 0.5rem;
gap: 0.75rem; background: var(--stage-inset);
border-radius: 0.4rem;
border: 1px solid var(--stage-line);
} }
@media (max-width: 800px) { .operation-block__interpreted {
.operation-block__grid { margin: 0.5rem 0;
grid-template-columns: 1fr; padding: 0.5rem;
} background: var(--stage-inset);
border-radius: 0.4rem;
border: 1px solid var(--stage-line);
}
.operation-block__interpreted-label {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--text-muted);
margin: 0 0 0.25rem;
}
.operation-block__interpreted-status,
.operation-block__interpreted-interrupt,
.operation-block__interpreted-ids {
margin: 0.15rem 0;
font-size: 0.8rem;
color: var(--text-primary);
}
.operation-block__interpreted-interrupt {
color: var(--accent-amber);
}
.operation-block__evidence-action {
display: inline-block;
margin: 0.5rem 0;
padding: 0.3rem 0.6rem;
border: 1px solid var(--stage-line);
background: var(--stage-inset);
color: var(--text-secondary);
border-radius: 0.3rem;
font-size: 0.75rem;
cursor: pointer;
}
.operation-block__evidence-action:hover {
background: var(--stage-surface);
color: var(--text-primary);
}
.operation-block--receipt {
padding: 0.5rem 0.75rem;
}
.operation-block--receipt header {
gap: 0.5rem;
}
.operation-block__run-id {
margin: 0.25rem 0 0;
font-size: 0.75rem;
color: var(--text-muted);
} }
.workflow-graph-stage { .workflow-graph-stage {
position: relative; position: relative;
min-height: 12rem; min-height: 12rem;
flex: 1; flex: 1;
border: 1px solid oklch(0.32 0.04 250); border: 1px solid var(--stage-line);
background: oklch(0.14 0.025 250); background: var(--stage-canvas);
overflow: hidden; overflow: hidden;
} }
.workflow-graph-stage__connectors {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.workflow-graph-stage__connector {
stroke: var(--stage-line);
stroke-width: 2;
stroke-dasharray: 4 4;
}
.workflow-graph-stage__connector--active {
stroke: var(--accent-cyan);
stroke-dasharray: none;
}
.workflow-graph-stage__arrowhead {
fill: var(--stage-line);
}
.workflow-graph-stage__arrowhead--active {
fill: var(--accent-cyan);
}
.workflow-graph-stage__node { .workflow-graph-stage__node {
position: absolute; position: absolute;
transform: translate(-50%, -50%); transform: translate(-50%, -50%);
border: 1px solid oklch(0.44 0.055 250); border: 1px solid var(--stage-line);
background: oklch(0.2 0.03 250); background: var(--stage-surface);
color: inherit; color: var(--text-primary);
padding: 0.5rem 0.7rem; padding: 0.5rem 0.7rem;
border-radius: 0.6rem; border-radius: 0.6rem;
font-size: 0.8rem; font-size: 0.8rem;
transition: border-color 0.2s ease, background 0.2s ease, box-shadow 0.2s ease;
}
.workflow-graph-stage__node--completed {
background: oklch(0.18 0.02 250);
border-color: oklch(0.44 0.04 250);
}
.workflow-graph-stage__node--current {
border-color: var(--accent-cyan);
box-shadow: 0 0 0 2px oklch(0.72 0.17 195 / 0.3);
}
.workflow-graph-stage__node--interrupt {
border-color: var(--accent-amber);
box-shadow: 0 0 0 2px oklch(0.76 0.18 70 / 0.3);
}
.workflow-graph-stage__node--future {
opacity: 0.6;
}
.workflow-graph-stage__node-label {
display: block;
}
.workflow-graph-stage__node-state {
display: block;
font-size: 0.65rem;
text-transform: uppercase;
letter-spacing: 0.05em;
opacity: 0.8;
}
.workflow-graph-stage__node--current .workflow-graph-stage__node-state {
color: var(--accent-cyan);
}
.workflow-graph-stage__node--interrupt .workflow-graph-stage__node-state {
color: var(--accent-amber);
} }
.workflow-graph-stage__node[data-kind="interrupt"] { .workflow-graph-stage__node[data-kind="interrupt"] {
border-color: oklch(0.76 0.18 70); border-color: var(--accent-amber);
} }
.workflow-graph-stage__node[data-selected="true"] { .workflow-graph-stage__node[data-selected="true"] {
outline: 3px solid oklch(0.72 0.17 195); outline: 3px solid var(--accent-cyan);
} }
.node-spotlight { .node-spotlight {
@@ -937,3 +1118,61 @@ body {
transition-duration: 0.01ms !important; transition-duration: 0.01ms !important;
scroll-behavior: auto !important; scroll-behavior: auto !important;
} }
/* Demo workflow scene */
.demo-workflow-scene__graph-area {
flex: 1;
display: flex;
min-height: 0;
}
.demo-workflow-scene__interrupt-preview {
padding: 0.65rem 0.75rem;
border: 1px solid var(--accent-amber);
background: oklch(0.76 0.18 70 / 0.1);
border-radius: 0.5rem;
flex-shrink: 0;
}
.demo-workflow-scene__interrupt-preview h3 {
margin: 0 0 0.35rem;
font-size: 0.85rem;
color: var(--accent-amber);
}
.demo-workflow-scene__interrupt-details {
display: grid;
grid-template-columns: auto 1fr;
gap: 0.2rem 0.75rem;
margin: 0;
font-size: 0.8rem;
}
.demo-workflow-scene__interrupt-details dt {
color: var(--text-muted);
}
.demo-workflow-scene__interrupt-details dd {
margin: 0;
color: var(--text-primary);
}
.demo-workflow-scene__approval-context {
padding: 0.65rem 0.75rem;
border: 1px solid var(--accent-cyan);
background: oklch(0.72 0.17 195 / 0.1);
border-radius: 0.5rem;
flex-shrink: 0;
}
.demo-workflow-scene__approval-context h3 {
margin: 0 0 0.35rem;
font-size: 0.85rem;
color: var(--accent-cyan);
}
.demo-workflow-scene__approval-prompt {
margin: 0 0 0.5rem;
font-size: 0.8rem;
color: var(--text-secondary);
}
@@ -0,0 +1,738 @@
/* The demo scenes use one continuous stage; global console card styles must not leak in. */
.presentation-route .presentation-stage__primary {
margin: 0;
border: 0;
border-radius: 0;
box-shadow: none;
}
.presentation-stage[data-scene-view="demo"] .presentation-stage__primary {
position: relative;
padding: 1rem 1.25rem 0.7rem;
background:
radial-gradient(circle at 76% 18%, oklch(0.7 0.13 195 / 0.08), transparent 30rem),
linear-gradient(145deg, oklch(0.13 0.025 250), oklch(0.105 0.02 250));
color: var(--text-primary);
}
.presentation-route {
position: fixed;
z-index: 100;
inset: 0;
width: 100vw;
}
.presentation-stage[data-scene-view="demo"] .stage-caption {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(15rem, 36%);
column-gap: 2rem;
align-items: end;
flex: 0 0 auto;
margin: 0;
border: 0;
border-radius: 0;
padding: 0 0 0.85rem;
background: transparent;
box-shadow: none;
}
.presentation-stage[data-scene-view="demo"] .stage-caption__eyebrow {
grid-column: 1;
margin: 0 0 0.2rem;
color: var(--accent-cyan);
font: 650 0.68rem/1 var(--font-mono, monospace);
letter-spacing: 0.14em;
}
.presentation-stage[data-scene-view="demo"] .stage-caption h1 {
grid-column: 1;
margin: 0;
max-width: 15ch;
color: var(--text-primary);
font-size: clamp(1.8rem, 3vw, 2.75rem);
font-weight: 680;
line-height: 0.98;
letter-spacing: -0.035em;
}
.presentation-stage[data-scene-view="demo"] .stage-caption > div {
grid-column: 2;
grid-row: 1 / span 2;
align-self: end;
border-left: 1px solid oklch(0.72 0.17 195 / 0.45);
padding-left: 1rem;
color: var(--text-secondary);
font-size: 0.86rem;
line-height: 1.45;
}
.presentation-stage[data-scene-view="demo"] .stage-caption > div p {
margin: 0;
}
.demo-workflow-stage {
position: relative;
display: flex;
flex: 1 1 auto;
min-height: 0;
overflow: hidden;
}
.demo-workflow-stage__pending {
display: grid;
flex: 1;
place-items: center;
border: 1px dashed var(--stage-line);
color: var(--text-muted);
font-family: var(--font-mono, monospace);
}
/* Operation handoff: a single large receipt, not a stack of dashboard cards. */
.presentation-route .operation-block--expanded {
position: relative;
display: grid;
grid-template-rows: auto auto 1fr auto;
align-self: stretch;
width: 100%;
margin: 0;
border: 1px solid oklch(0.72 0.17 195 / 0.42);
border-radius: 1rem;
padding: clamp(1.1rem, 2.5vw, 2rem);
overflow: hidden;
background:
linear-gradient(90deg, oklch(0.72 0.17 195 / 0.07), transparent 42%),
oklch(0.145 0.025 250 / 0.92);
box-shadow: 0 1.5rem 4rem oklch(0.04 0.02 250 / 0.35);
}
.presentation-route .operation-block--expanded::before {
position: absolute;
inset: 0 auto 0 0;
width: 3px;
background: var(--accent-cyan);
content: "";
}
.operation-block__header {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1.5rem;
}
.operation-block__header strong {
display: block;
margin-top: 0.3rem;
color: var(--text-primary);
font: 650 clamp(1.35rem, 2.5vw, 2.15rem)/1.1 var(--font-mono, monospace);
letter-spacing: -0.04em;
}
.operation-block__kicker {
color: var(--accent-cyan);
font: 650 0.68rem/1 var(--font-mono, monospace);
letter-spacing: 0.14em;
text-transform: uppercase;
}
.operation-block__status-group {
display: flex;
align-items: center;
gap: 0.6rem;
color: var(--text-muted);
font-family: var(--font-mono, monospace);
}
.operation-status {
display: inline-flex;
align-items: center;
min-height: 1.65rem;
border: 1px solid var(--stage-line);
border-radius: 999px;
padding: 0.25rem 0.65rem;
font: 650 0.68rem/1 var(--font-mono, monospace);
letter-spacing: 0.08em;
text-transform: uppercase;
}
.operation-status[data-status="interrupted"] {
border-color: oklch(0.76 0.18 70 / 0.55);
background: oklch(0.76 0.18 70 / 0.1);
color: var(--accent-amber);
}
.operation-status[data-status="completed"] {
border-color: oklch(0.7 0.16 145 / 0.55);
background: oklch(0.7 0.16 145 / 0.1);
color: var(--success);
}
.operation-command {
display: flex;
align-items: flex-start;
gap: 0.7rem;
margin: clamp(1rem, 3vh, 1.8rem) 0;
border: 1px solid oklch(0.4 0.04 250 / 0.75);
border-radius: 0.7rem;
padding: 0.9rem 1rem;
background: oklch(0.09 0.02 250 / 0.8);
color: oklch(0.88 0.03 195);
font-family: var(--font-mono, monospace);
font-size: clamp(0.76rem, 1.3vw, 0.92rem);
line-height: 1.55;
}
.operation-command > span {
color: var(--accent-cyan);
user-select: none;
}
.operation-command code {
overflow-wrap: anywhere;
}
.operation-summary {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
align-self: end;
margin: 0;
border-block: 1px solid var(--stage-line);
}
.operation-summary > div {
min-width: 0;
padding: 0.9rem 1rem;
}
.operation-summary > div + div {
border-left: 1px solid var(--stage-line);
}
.operation-summary dt {
margin-bottom: 0.35rem;
color: var(--text-muted);
font: 650 0.64rem/1 var(--font-mono, monospace);
letter-spacing: 0.11em;
text-transform: uppercase;
}
.operation-summary dd {
margin: 0;
overflow: hidden;
color: var(--text-primary);
font-size: 0.86rem;
text-overflow: ellipsis;
white-space: nowrap;
}
.operation-summary dd[data-emphasis="interrupt"] {
color: var(--accent-amber);
}
.operation-block__evidence-action {
justify-self: start;
margin-top: 1rem;
border: 0;
border-bottom: 1px solid oklch(0.72 0.17 195 / 0.5);
border-radius: 0;
padding: 0.3rem 0;
background: transparent;
color: var(--accent-cyan);
font: 650 0.75rem/1 var(--font-mono, monospace);
cursor: pointer;
}
.operation-block__evidence-action span {
display: inline-block;
margin-left: 0.5rem;
transition: transform 180ms ease;
}
.operation-block__evidence-action:hover span,
.operation-block__evidence-action:focus-visible span {
transform: translateX(0.25rem);
}
/* Graph beats keep the operation as a narrow provenance receipt. */
.operation-receipt {
position: absolute;
z-index: 5;
top: 0;
left: 0;
right: 0;
display: grid;
grid-template-columns: minmax(0, 1fr) auto auto;
align-items: center;
gap: 1rem;
min-height: 2.6rem;
border: 1px solid var(--stage-line);
border-radius: 0.55rem;
padding: 0.4rem 0.7rem;
background: oklch(0.12 0.022 250 / 0.9);
backdrop-filter: blur(12px);
color: var(--text-secondary);
font-family: var(--font-mono, monospace);
font-size: 0.7rem;
}
.operation-receipt__identity {
display: flex;
align-items: center;
gap: 0.55rem;
min-width: 0;
}
.operation-receipt__identity strong {
overflow: hidden;
color: var(--text-primary);
text-overflow: ellipsis;
}
.operation-receipt__pulse {
width: 0.45rem;
height: 0.45rem;
border-radius: 50%;
background: var(--accent-cyan);
box-shadow: 0 0 0 0.25rem oklch(0.72 0.17 195 / 0.12);
}
.operation-receipt .operation-status {
min-height: 1.35rem;
padding: 0.15rem 0.45rem;
font-size: 0.58rem;
}
.demo-workflow-stage__graph {
display: grid;
grid-template-columns: minmax(0, 1fr);
flex: 1;
min-width: 0;
min-height: 0;
padding-top: 3.15rem;
}
.demo-workflow-stage__graph:has(.interrupt-contract-preview) {
grid-template-columns: minmax(0, 1fr) minmax(17rem, 29%);
gap: 0.9rem;
}
.presentation-route .workflow-graph-stage {
position: relative;
min-width: 0;
min-height: 0;
border: 1px solid oklch(0.4 0.04 250 / 0.7);
border-radius: 0.85rem;
overflow: hidden;
background:
linear-gradient(oklch(0.35 0.03 250 / 0.12) 1px, transparent 1px),
linear-gradient(90deg, oklch(0.35 0.03 250 / 0.12) 1px, transparent 1px),
radial-gradient(circle at 50% 50%, oklch(0.72 0.17 195 / 0.06), transparent 60%),
oklch(0.105 0.02 250);
background-size: 1.75rem 1.75rem, 1.75rem 1.75rem, auto, auto;
}
.workflow-graph-stage__legend {
position: absolute;
z-index: 4;
top: 0.7rem;
right: 0.8rem;
display: flex;
gap: 0.9rem;
color: var(--text-muted);
font: 600 0.58rem/1 var(--font-mono, monospace);
letter-spacing: 0.05em;
text-transform: uppercase;
}
.workflow-graph-stage__legend span {
display: flex;
align-items: center;
gap: 0.3rem;
}
.workflow-graph-stage__legend i {
width: 0.42rem;
height: 0.42rem;
border-radius: 50%;
background: var(--stage-line);
}
.workflow-graph-stage__legend i[data-state="current"] {
background: var(--accent-cyan);
}
.workflow-graph-stage__legend i[data-state="interrupt"] {
background: var(--accent-amber);
}
.workflow-graph-stage__connectors {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
pointer-events: none;
}
.workflow-graph-stage__connectors line {
stroke: oklch(0.48 0.035 250 / 0.55);
stroke-width: 1.4;
stroke-dasharray: 4 6;
}
.workflow-graph-stage__connectors line[data-active="true"] {
stroke: var(--accent-cyan);
stroke-width: 2;
stroke-dasharray: none;
}
#workflow-arrow polygon {
fill: oklch(0.48 0.035 250);
}
#workflow-arrow-active polygon {
fill: var(--accent-cyan);
}
.workflow-graph-stage__node-slot {
position: absolute;
z-index: 2;
transform: translate(-50%, -50%);
}
.presentation-route .workflow-graph-stage__node {
position: relative;
display: grid;
gap: 0.25rem;
width: clamp(7.2rem, 11vw, 9.5rem);
min-height: 4.35rem;
transform: none;
border: 1px solid var(--stage-line);
border-radius: 0.7rem;
padding: 0.65rem 0.75rem;
background: oklch(0.15 0.025 250 / 0.96);
color: var(--text-primary);
text-align: left;
box-shadow: 0 0.65rem 1.5rem oklch(0.04 0.02 250 / 0.3);
cursor: pointer;
}
.workflow-graph-stage__node strong {
font-size: 0.82rem;
line-height: 1.05;
}
.workflow-graph-stage__node small {
color: var(--text-muted);
font-size: 0.67rem;
}
.workflow-graph-stage__node-state {
color: var(--text-muted);
font: 650 0.55rem/1 var(--font-mono, monospace);
letter-spacing: 0.09em;
text-transform: uppercase;
}
.workflow-graph-stage__node[data-execution-state="completed"] {
border-color: oklch(0.48 0.04 195 / 0.7);
background: oklch(0.16 0.035 210 / 0.96);
}
.workflow-graph-stage__node[data-execution-state="current"] {
border-color: var(--accent-cyan);
background: oklch(0.19 0.055 210 / 0.98);
box-shadow: 0 0 0 3px oklch(0.72 0.17 195 / 0.14), 0 1rem 2.5rem oklch(0.06 0.03 230 / 0.5);
}
.workflow-graph-stage__node[data-execution-state="current"] .workflow-graph-stage__node-state {
color: var(--accent-cyan);
}
.workflow-graph-stage__node[data-current-interrupt="true"] {
border-color: var(--accent-amber);
background: oklch(0.19 0.045 70 / 0.95);
box-shadow: 0 0 0 3px oklch(0.76 0.18 70 / 0.14), 0 1rem 2.5rem oklch(0.06 0.03 230 / 0.5);
}
.workflow-graph-stage__node[data-current-interrupt="true"] .workflow-graph-stage__node-state {
color: var(--accent-amber);
}
.workflow-graph-stage__node[data-execution-state="future"] {
opacity: 0.48;
box-shadow: none;
}
.workflow-graph-stage__node[data-selected="true"] {
outline: 2px solid var(--accent-cyan);
outline-offset: 3px;
}
.interrupt-contract-preview {
position: relative;
display: grid;
grid-template-rows: auto auto auto minmax(0, 1fr);
min-height: 0;
border: 1px solid oklch(0.76 0.18 70 / 0.48);
border-radius: 0.85rem;
padding: 1rem;
overflow: hidden;
background:
linear-gradient(180deg, oklch(0.76 0.18 70 / 0.08), transparent 42%),
oklch(0.135 0.025 250);
}
.interrupt-contract-preview[data-mode="approval"] {
box-shadow: inset 0 3px 0 var(--accent-amber);
}
.interrupt-contract-preview__eyebrow {
margin: 0 0 0.25rem;
color: var(--accent-amber);
font: 650 0.62rem/1 var(--font-mono, monospace);
letter-spacing: 0.12em;
text-transform: uppercase;
}
.interrupt-contract-preview h2 {
margin: 0 0 0.8rem;
color: var(--text-primary);
font-size: 1.2rem;
line-height: 1.05;
}
.interrupt-contract-preview > header {
display: grid;
gap: 0.25rem;
margin-bottom: 0.9rem;
}
.interrupt-contract-preview > header span {
color: var(--accent-amber);
font: 650 0.6rem/1 var(--font-mono, monospace);
letter-spacing: 0.1em;
text-transform: uppercase;
}
.interrupt-contract-preview > header strong {
color: var(--text-primary);
font: 650 1.15rem/1.1 var(--font-mono, monospace);
}
.interrupt-contract-preview dl {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 0.7rem;
margin: 0 0 0.8rem;
font-size: 0.72rem;
}
.interrupt-contract-preview dl > div {
min-width: 0;
}
.interrupt-contract-preview dt {
margin-bottom: 0.25rem;
color: var(--text-muted);
}
.interrupt-contract-preview dd {
margin: 0;
overflow: hidden;
color: var(--text-primary);
font-family: var(--font-mono, monospace);
text-overflow: ellipsis;
white-space: nowrap;
}
.interrupt-contract-preview__schema > span {
color: var(--text-muted);
font: 650 0.6rem/1 var(--font-mono, monospace);
letter-spacing: 0.1em;
text-transform: uppercase;
}
.interrupt-contract-preview__schema {
min-height: 0;
border-top: 1px solid var(--stage-line);
padding-top: 0.7rem;
}
.interrupt-contract-preview__schema strong {
color: var(--text-muted);
font: 650 0.6rem/1 var(--font-mono, monospace);
letter-spacing: 0.1em;
text-transform: uppercase;
}
.interrupt-contract-preview__schema pre {
max-height: 11rem;
margin: 0.45rem 0 0;
overflow: auto;
color: oklch(0.85 0.04 195);
font: 0.63rem/1.45 var(--font-mono, monospace);
white-space: pre-wrap;
}
/* Chat remains an app surface while the workflow stage stays cinematic. */
.presentation-stage[data-chat-theme="light"] .presentation-stage__chat {
border-right: 1px solid oklch(0.82 0.015 80);
background: oklch(0.975 0.004 80);
}
.presentation-stage[data-chat-theme="light"] .operator-chat {
gap: 1.05rem;
padding: 1rem 0.85rem;
background: transparent;
}
.presentation-stage[data-chat-theme="light"] .chat-message {
border: 0;
border-radius: 0;
padding: 0;
background: transparent;
box-shadow: none;
color: oklch(0.22 0.015 80);
font-size: 0.78rem;
line-height: 1.45;
}
.presentation-stage[data-chat-theme="light"] .chat-message > strong {
display: block;
margin-bottom: 0.35rem;
color: oklch(0.42 0.025 80);
font: 650 0.6rem/1 var(--font-mono, monospace);
letter-spacing: 0.09em;
text-transform: uppercase;
}
.presentation-stage[data-chat-theme="light"] .chat-message--operator {
margin-left: 1rem;
border-radius: 0.85rem;
padding: 0.65rem 0.75rem;
background: oklch(0.925 0.012 80);
}
.presentation-stage[data-chat-theme="light"] .chat-message p {
margin: 0.2rem 0;
}
.presentation-stage[data-chat-theme="light"] .chat-tool-part {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
gap: 0.3rem;
margin-top: 0.5rem;
border: 1px solid oklch(0.82 0.015 80);
border-radius: 0.55rem;
padding: 0.55rem 0.65rem;
background: oklch(0.95 0.006 80);
}
.presentation-stage[data-chat-theme="light"] .chat-tool-part span {
grid-column: 1 / -1;
color: oklch(0.48 0.04 195);
font: 650 0.56rem/1 var(--font-mono, monospace);
letter-spacing: 0.09em;
}
.presentation-stage[data-chat-theme="light"] .chat-tool-part code {
overflow: hidden;
color: oklch(0.26 0.025 250);
font-size: 0.68rem;
text-overflow: ellipsis;
}
.presentation-stage[data-chat-theme="light"] .chat-tool-part--handoff {
border-color: oklch(0.56 0.13 195 / 0.55);
background: oklch(0.94 0.025 195);
}
.presentation-route .presentation-stage__discussion-toggle {
position: absolute;
right: 1.25rem;
bottom: 0.7rem;
z-index: 8;
border: 0;
padding: 0.25rem 0;
background: transparent;
color: var(--text-muted);
font: 600 0.62rem/1 var(--font-mono, monospace);
cursor: pointer;
}
.presentation-route .presentation-stage__mode {
position: absolute;
bottom: 0.7rem;
left: 1.25rem;
z-index: 8;
margin: 0;
color: var(--text-muted);
font: 600 0.6rem/1 var(--font-mono, monospace);
letter-spacing: 0.06em;
text-transform: uppercase;
}
.presentation-route .scene-rail {
gap: 0.25rem;
min-height: 2.7rem;
border-top: 1px solid oklch(0.36 0.04 250 / 0.7);
padding: 0.35rem 0.45rem;
background: oklch(0.095 0.02 250);
}
.presentation-route .scene-rail__scene {
min-width: 2.5rem;
min-height: 1.9rem;
border: 0;
border-radius: 0.35rem;
padding: 0.25rem 0.35rem;
background: transparent;
color: oklch(0.62 0.025 250);
}
.presentation-route .scene-rail__scene[data-active="true"] {
background: oklch(0.72 0.17 195 / 0.13);
color: var(--text-primary);
}
.presentation-route .scene-rail__title {
display: none;
}
@media (max-width: 1050px) {
.demo-workflow-stage__graph:has(.interrupt-contract-preview) {
grid-template-columns: minmax(0, 1fr) 15rem;
}
.presentation-route .workflow-graph-stage__node {
width: 7rem;
padding-inline: 0.55rem;
}
}
@media (max-height: 760px) {
.presentation-stage[data-scene-view="demo"] .presentation-stage__primary {
gap: 0.55rem;
padding-top: 0.8rem;
}
.presentation-stage[data-scene-view="demo"] .stage-caption {
padding-bottom: 0.55rem;
}
.presentation-stage[data-scene-view="demo"] .stage-caption h1 {
font-size: clamp(1.65rem, 3vw, 2.2rem);
}
.operation-block__header strong {
font-size: clamp(1.2rem, 2.2vw, 1.75rem);
}
.operation-command {
margin: 0.8rem 0;
}
.interrupt-contract-preview__schema pre {
max-height: 8rem;
}
}