feat: add workflow demo presentation stage
This commit is contained in:
+10
-3
@@ -88,11 +88,18 @@ Implementation order:
|
||||
[`defense presentation storyboard`](superpowers/specs/2026-07-04-defense-presentation-storyboard-design.md).
|
||||
Implementation:
|
||||
[`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.
|
||||
11. Future: implement final scene visuals, motion choreography, evidence assets,
|
||||
12. Future: implement final scene visuals, motion choreography, evidence assets,
|
||||
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.
|
||||
|
||||
Boundaries: this is not a production admin panel, generic visual workflow
|
||||
|
||||
+37
-37
@@ -1,6 +1,6 @@
|
||||
# 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.
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
- Produces: `OperationPresentation`, `InterruptContractPresentation`, `GraphExecutionPresentation`, `projectOperationPresentation(event)`, `projectInterruptContract(event)`, and `graphExecutionForBeat(beatId)`.
|
||||
- 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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
- [ ] **Step 3: Implement schema-driven projections**
|
||||
- [x] **Step 3: Implement schema-driven projections**
|
||||
|
||||
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
|
||||
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:
|
||||
|
||||
@@ -238,7 +238,7 @@ pnpm --dir web --filter @lda/console typecheck
|
||||
|
||||
Expected: projection tests PASS and typecheck exits zero.
|
||||
|
||||
- [ ] **Step 5: Commit the projection boundary**
|
||||
- [x] **Step 5: Commit the projection boundary**
|
||||
|
||||
```powershell
|
||||
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: 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:
|
||||
|
||||
@@ -293,7 +293,7 @@ it("renders a compact execution receipt", () => {
|
||||
Extend `OperatorChat.test.tsx` with a prepared start tool-call message and assert
|
||||
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:
|
||||
|
||||
@@ -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
|
||||
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:
|
||||
|
||||
@@ -367,7 +367,7 @@ In `OperatorChat`, render tool-call parts through `motion.div`. Assign
|
||||
`part.call.name === "startPreparedReportRun"`. Preserve every existing message
|
||||
part and approval behavior.
|
||||
|
||||
- [ ] **Step 4: Run operation and chat tests**
|
||||
- [x] **Step 4: Run operation and chat tests**
|
||||
|
||||
Run:
|
||||
|
||||
@@ -378,7 +378,7 @@ pnpm --dir web --filter @lda/console typecheck
|
||||
|
||||
Expected: focused tests PASS and typecheck exits zero.
|
||||
|
||||
- [ ] **Step 5: Commit the operation hierarchy**
|
||||
- [x] **Step 5: Commit the operation hierarchy**
|
||||
|
||||
```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
|
||||
@@ -401,7 +401,7 @@ git commit -m "feat: stage interpreted workflow operations"
|
||||
- Produces: `InterruptContractPreview` props `{ contract }`.
|
||||
- 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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
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`:
|
||||
|
||||
@@ -494,7 +494,7 @@ const executionStateForNode = (
|
||||
Keep each graph node as a button and retain `aria-pressed` for the spotlight
|
||||
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
|
||||
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:
|
||||
|
||||
@@ -536,7 +536,7 @@ pnpm --dir web --filter @lda/console typecheck
|
||||
|
||||
Expected: focused tests PASS and typecheck exits zero.
|
||||
|
||||
- [ ] **Step 6: Commit graph and interrupt semantics**
|
||||
- [x] **Step 6: Commit graph and interrupt semantics**
|
||||
|
||||
```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
|
||||
@@ -560,7 +560,7 @@ git commit -m "feat: stage workflow graph execution state"
|
||||
- Produces: `DemoWorkflowScene` props `{ scene, beat, demo, selectedNodeId, selectNode, openEvidence }`.
|
||||
- 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
|
||||
`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
|
||||
pass a no-op callback.
|
||||
|
||||
- [ ] **Step 2: Run focused tests and verify failures**
|
||||
- [x] **Step 2: Run focused tests and verify failures**
|
||||
|
||||
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.
|
||||
|
||||
- [ ] **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
|
||||
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
|
||||
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
|
||||
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
|
||||
evidence already satisfies the approved fallback.
|
||||
|
||||
- [ ] **Step 5: Run focused and presentation tests**
|
||||
- [x] **Step 5: Run focused and presentation tests**
|
||||
|
||||
Run:
|
||||
|
||||
@@ -729,7 +729,7 @@ pnpm --dir web --filter @lda/console typecheck
|
||||
|
||||
Expected: focused tests PASS and typecheck exits zero.
|
||||
|
||||
- [ ] **Step 6: Commit scene composition**
|
||||
- [x] **Step 6: Commit scene composition**
|
||||
|
||||
```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
|
||||
@@ -752,7 +752,7 @@ git commit -m "feat: choreograph workflow demo beats"
|
||||
- Produces: presentation-scoped color, surface, text, status, radius, and motion tokens.
|
||||
- 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
|
||||
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
|
||||
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:
|
||||
|
||||
@@ -795,7 +795,7 @@ import "./styles/demo-workflow.css";
|
||||
The route owns these imports. Do not import presentation CSS from `/console`
|
||||
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:
|
||||
|
||||
@@ -815,7 +815,7 @@ Remove the old `.operation-block`, `.operation-block__grid`,
|
||||
in `demo-workflow.css`. Do not leave duplicate selector families with competing
|
||||
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`:
|
||||
|
||||
@@ -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
|
||||
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
|
||||
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
|
||||
layout animation through transforms.
|
||||
|
||||
- [ ] **Step 6: Run tests, typecheck, and build**
|
||||
- [x] **Step 6: Run tests, typecheck, and build**
|
||||
|
||||
Run:
|
||||
|
||||
@@ -875,7 +875,7 @@ pnpm --dir web --filter @lda/console build
|
||||
|
||||
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
|
||||
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.
|
||||
- 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:
|
||||
|
||||
@@ -909,7 +909,7 @@ git diff --check
|
||||
Expected: all web tests PASS, all workspace typechecks exit zero, build
|
||||
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:
|
||||
|
||||
@@ -942,7 +942,7 @@ Inspect each screenshot. Verify all of the following before continuing:
|
||||
- no primary action or graph node is clipped;
|
||||
- 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:
|
||||
|
||||
@@ -962,7 +962,7 @@ Close the named browser session:
|
||||
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
|
||||
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
|
||||
presentation polish as the next visual follow-up.
|
||||
|
||||
- [ ] **Step 5: Archive the completed plan**
|
||||
- [x] **Step 5: Archive the completed plan**
|
||||
|
||||
```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
|
||||
@@ -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.
|
||||
|
||||
- [ ] **Step 6: Run final documentation and repository checks**
|
||||
- [x] **Step 6: Run final documentation and repository checks**
|
||||
|
||||
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
|
||||
documentation changes remain.
|
||||
|
||||
- [ ] **Step 7: Commit documentation and plan archival**
|
||||
- [x] **Step 7: Commit documentation and plan archival**
|
||||
|
||||
```powershell
|
||||
git add web/README.md docs/current_roadmap.md
|
||||
@@ -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
|
||||
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
|
||||
pnpm --dir web dev
|
||||
# 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 { 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 { OperationBlock } from "./OperationBlock.js";
|
||||
|
||||
@@ -27,13 +28,42 @@ const event: DemoEvent = {
|
||||
};
|
||||
|
||||
describe("OperationBlock", () => {
|
||||
it("shows command, raw response, and interpreted result", () => {
|
||||
render(<OperationBlock event={event} />);
|
||||
it("shows command, interpreted summary, and evidence action in expanded mode", () => {
|
||||
render(<OperationBlock event={event} variant="expanded" openEvidence={vi.fn()} />);
|
||||
|
||||
expect(screen.getByText(/workflow.runs.start/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/uv run wf run start/i)).toBeInTheDocument();
|
||||
expect(screen.getByText("Raw")).toBeInTheDocument();
|
||||
expect(screen.getByText("Interpreted")).toBeInTheDocument();
|
||||
expect(screen.getByText("Workflow operation")).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.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 { formatJson } from "./format.js";
|
||||
import { projectOperationPresentation } from "./demo-workflow-model.js";
|
||||
|
||||
export type OperationVariant = "expanded" | "receipt";
|
||||
|
||||
type OperationBlockProps = {
|
||||
readonly event: DemoEvent;
|
||||
readonly variant: OperationVariant;
|
||||
readonly openEvidence: () => void;
|
||||
};
|
||||
|
||||
export const OperationBlock = ({ event }: OperationBlockProps) => (
|
||||
<section className="operation-block" aria-label={`${event.stage} operation`}>
|
||||
<header>
|
||||
<p>{event.operation ?? event.stage}</p>
|
||||
<small>{event.durationMs} ms</small>
|
||||
</header>
|
||||
{event.equivalentCli && (
|
||||
<pre className="operation-block__command"><code>{event.equivalentCli}</code></pre>
|
||||
)}
|
||||
<div className="operation-block__grid">
|
||||
<section>
|
||||
<h3>Raw</h3>
|
||||
<pre><code>{formatJson(event.rawResponse)}</code></pre>
|
||||
</section>
|
||||
<section>
|
||||
<h3>Interpreted</h3>
|
||||
<pre><code>{formatJson(event.interpreted)}</code></pre>
|
||||
</section>
|
||||
</div>
|
||||
<footer>
|
||||
<span>Deployment: {event.resultingIds.deploymentId}</span>
|
||||
{event.resultingIds.runId && <span>Run: {event.resultingIds.runId}</span>}
|
||||
</footer>
|
||||
</section>
|
||||
);
|
||||
export const OperationBlock = ({
|
||||
event,
|
||||
variant,
|
||||
openEvidence,
|
||||
}: OperationBlockProps) => {
|
||||
const operation = projectOperationPresentation(event);
|
||||
|
||||
if (variant === "receipt") {
|
||||
return (
|
||||
<m.div
|
||||
layout
|
||||
layoutId="workflow-start-operation"
|
||||
transition={{ duration: 0.24, ease: [0.16, 1, 0.3, 1] }}
|
||||
className="operation-receipt"
|
||||
aria-label={`${operation.operation} execution receipt`}
|
||||
>
|
||||
<div className="operation-receipt__identity">
|
||||
<span className="operation-receipt__pulse" aria-hidden="true" />
|
||||
<strong>{operation.operation}</strong>
|
||||
<span className="operation-status" data-status={operation.status}>
|
||||
{operation.status}
|
||||
</span>
|
||||
</div>
|
||||
<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">-></span>
|
||||
</button>
|
||||
</m.article>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { m } from "motion/react";
|
||||
import { PREPARE_THESIS_REPORT_RECIPE } from "../demo/agent/recipes.js";
|
||||
import type { AgentMessage, AgentMessagePart } from "../demo/agent/events.js";
|
||||
import type { PresentationState } from "./presentation-state.js";
|
||||
@@ -29,23 +30,36 @@ const fallbackMessages = (state: PresentationState): ReadonlyArray<AgentMessage>
|
||||
|
||||
const renderPart = (
|
||||
part: AgentMessagePart,
|
||||
index: number,
|
||||
key: string,
|
||||
onApprove?: () => void,
|
||||
onDeny?: () => void,
|
||||
) => {
|
||||
switch (part.type) {
|
||||
case "text":
|
||||
return <p key={index}>{part.text}</p>;
|
||||
return <p key={key}>{part.text}</p>;
|
||||
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 (
|
||||
<div key={index} className="chat-tool-part">
|
||||
<div key={key} className="chat-tool-part">
|
||||
<span>Tool call</span>
|
||||
<code>{part.call.name}</code>
|
||||
</div>
|
||||
);
|
||||
case "tool-result":
|
||||
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>
|
||||
<code>{part.result.name}</code>
|
||||
<small>{part.result.status}</small>
|
||||
@@ -53,14 +67,14 @@ const renderPart = (
|
||||
);
|
||||
case "presentation-action":
|
||||
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>
|
||||
<code>{part.action.type}</code>
|
||||
</div>
|
||||
);
|
||||
case "approval-request":
|
||||
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>
|
||||
<code>{part.name}</code>
|
||||
<p>{part.prompt}</p>
|
||||
@@ -71,21 +85,38 @@ const renderPart = (
|
||||
</div>
|
||||
);
|
||||
case "error":
|
||||
return <p key={index} className="chat-error">{part.message}</p>;
|
||||
return <p key={key} className="chat-error">{part.message}</p>;
|
||||
default:
|
||||
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) => {
|
||||
const visibleMessages = messages && messages.length > 0 ? messages : fallbackMessages(state);
|
||||
const composition = compositionForState(state);
|
||||
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) => (
|
||||
<div key={message.id} className={`chat-message chat-message--${message.role === "user" ? "operator" : "system"}`}>
|
||||
<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>
|
||||
))}
|
||||
</aside>
|
||||
|
||||
@@ -16,6 +16,7 @@ import { hashForLocation } from "./storyboard-navigation.js";
|
||||
import { findScene } from "./storyboard.js";
|
||||
import type { PresentationLocation } from "./storyboard.js";
|
||||
import "./presentation.css";
|
||||
import "./styles/demo-workflow.css";
|
||||
|
||||
const projectRecordingToEvidence = (
|
||||
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 { AgentMessage } from "../demo/agent/events.js";
|
||||
import { SceneBody } from "./SceneBody.js";
|
||||
@@ -45,54 +46,63 @@ export const PresentationStage = ({
|
||||
const composition = compositionForState(state);
|
||||
|
||||
const isMainScene = state.location.kind === "main";
|
||||
const activeSceneView = isMainScene
|
||||
? findScene(state.location.sceneId)?.view ?? "unknown"
|
||||
: "discussion";
|
||||
|
||||
return (
|
||||
<div
|
||||
className="presentation-stage"
|
||||
data-stage-theme={composition.stageTheme}
|
||||
data-chat-theme={composition.chatTheme}
|
||||
data-chat-mode={composition.chatMode}
|
||||
data-evidence-mode={composition.evidenceMode}
|
||||
>
|
||||
<aside className="presentation-stage__chat" aria-label="agent chat region">
|
||||
<OperatorChat state={state} messages={messages} onApprove={onApprove} onDeny={onDeny} />
|
||||
</aside>
|
||||
<section className="presentation-stage__primary" aria-label="primary presentation region">
|
||||
{state.location.kind === "discussion" ? (
|
||||
<DiscussionPanel branchId={state.location.branchId} onClose={closeDiscussion} />
|
||||
) : (
|
||||
<>
|
||||
<SceneBody
|
||||
location={state.location}
|
||||
demo={demo}
|
||||
selectedNodeId={state.selectedNodeId}
|
||||
selectNode={selectNode}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="presentation-stage__discussion-toggle"
|
||||
onClick={toggleDiscussionIndex}
|
||||
>
|
||||
Discussion topics
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{state.discussionIndexOpen && (
|
||||
<DiscussionIndex
|
||||
onSelect={(branchId) => {
|
||||
toggleDiscussionIndex();
|
||||
openDiscussion(branchId);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<p className="presentation-stage__mode">
|
||||
{demo.state.mode === "replay" ? "Replay" : "Live"} · {demo.state.phase}
|
||||
</p>
|
||||
</section>
|
||||
<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>
|
||||
<LazyMotion features={domAnimation}>
|
||||
<LayoutGroup id="presentation-stage">
|
||||
<div
|
||||
className="presentation-stage"
|
||||
data-stage-theme={composition.stageTheme}
|
||||
data-chat-theme={composition.chatTheme}
|
||||
data-chat-mode={composition.chatMode}
|
||||
data-evidence-mode={composition.evidenceMode}
|
||||
data-scene-view={activeSceneView}
|
||||
>
|
||||
<aside className="presentation-stage__chat" aria-label="agent chat region">
|
||||
<OperatorChat state={state} messages={messages} onApprove={onApprove} onDeny={onDeny} />
|
||||
</aside>
|
||||
<section className="presentation-stage__primary" aria-label="primary presentation region">
|
||||
{state.location.kind === "discussion" ? (
|
||||
<DiscussionPanel branchId={state.location.branchId} onClose={closeDiscussion} />
|
||||
) : (
|
||||
<>
|
||||
<SceneBody
|
||||
location={state.location}
|
||||
demo={demo}
|
||||
selectedNodeId={state.selectedNodeId}
|
||||
selectNode={selectNode}
|
||||
openEvidence={openEvidence}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="presentation-stage__discussion-toggle"
|
||||
onClick={toggleDiscussionIndex}
|
||||
>
|
||||
Discussion topics
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
{state.discussionIndexOpen && (
|
||||
<DiscussionIndex
|
||||
onSelect={(branchId) => {
|
||||
toggleDiscussionIndex();
|
||||
openDiscussion(branchId);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
<p className="presentation-stage__mode">
|
||||
{demo.state.mode === "replay" ? "Replay" : "Live"} · {demo.state.phase}
|
||||
</p>
|
||||
</section>
|
||||
<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 { 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 type { PresentationLocation } from "./storyboard.js";
|
||||
|
||||
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());
|
||||
|
||||
@@ -13,9 +42,10 @@ describe("SceneBody", () => {
|
||||
render(
|
||||
<SceneBody
|
||||
location={location}
|
||||
demo={null as never}
|
||||
demo={demo}
|
||||
selectedNodeId={null}
|
||||
selectNode={noop}
|
||||
openEvidence={noop}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByRole("heading", { name: /Positioning and Related Systems/i })).toBeInTheDocument();
|
||||
@@ -27,9 +57,10 @@ describe("SceneBody", () => {
|
||||
render(
|
||||
<SceneBody
|
||||
location={location}
|
||||
demo={null as never}
|
||||
demo={demo}
|
||||
selectedNodeId={null}
|
||||
selectNode={noop}
|
||||
openEvidence={noop}
|
||||
/>,
|
||||
);
|
||||
expect(screen.getByLabelText(/workflow graph/i)).toBeInTheDocument();
|
||||
|
||||
@@ -1,23 +1,14 @@
|
||||
import type { DemoTimelineController } from "../demo/useDemoTimeline.js";
|
||||
import { findBeat, findScene, type PresentationLocation, type SceneDefinition, type SceneBeatDefinition } from "./storyboard.js";
|
||||
import { NodeSpotlight } from "./NodeSpotlight.js";
|
||||
import { OperationBlock } from "./OperationBlock.js";
|
||||
import { DemoWorkflowScene } from "./DemoWorkflowScene.js";
|
||||
import { StageCaption } from "./StageCaption.js";
|
||||
import { WorkflowGraphStage } from "./WorkflowGraphStage.js";
|
||||
|
||||
type SceneBodyProps = {
|
||||
readonly location: PresentationLocation;
|
||||
readonly demo: DemoTimelineController;
|
||||
readonly selectedNodeId: string | null;
|
||||
readonly selectNode: (nodeId: string) => void;
|
||||
};
|
||||
|
||||
const operationStageByBeat: Readonly<Record<string, string | undefined>> = {
|
||||
operation: "run_start",
|
||||
interrupt: "interrupt",
|
||||
approval: "interrupt",
|
||||
resume: "run_resume",
|
||||
trace: "trace_read",
|
||||
readonly openEvidence: () => void;
|
||||
};
|
||||
|
||||
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 => {
|
||||
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 beatId = location.kind === "main" ? location.beatId : "landscape";
|
||||
const scene = findScene(sceneId) ?? findScene("thesis")!;
|
||||
@@ -269,6 +230,7 @@ export const SceneBody = ({ location, demo, selectedNodeId, selectNode }: SceneB
|
||||
demo={demo}
|
||||
selectedNodeId={selectedNodeId}
|
||||
selectNode={selectNode}
|
||||
openEvidence={openEvidence}
|
||||
/>
|
||||
);
|
||||
case "evaluation":
|
||||
|
||||
@@ -8,9 +8,54 @@ afterEach(() => cleanup());
|
||||
describe("WorkflowGraphStage", () => {
|
||||
it("renders curated workflow nodes and allows node selection", async () => {
|
||||
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 }));
|
||||
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 = {
|
||||
readonly id: string;
|
||||
readonly label: string;
|
||||
readonly detail: string;
|
||||
readonly kind: "node" | "interrupt" | "end";
|
||||
readonly x: number;
|
||||
readonly y: number;
|
||||
};
|
||||
|
||||
export const presentationNodes: readonly PresentationNode[] = [
|
||||
{ id: "read_docs", label: "Read docs", kind: "node", x: 8, y: 45 },
|
||||
{ id: "build_report", label: "Build report", kind: "node", x: 30, y: 30 },
|
||||
{ id: "review_issues", label: "Issue review", kind: "interrupt", x: 52, y: 45 },
|
||||
{ id: "create_issues", label: "Create issues", kind: "node", x: 74, y: 30 },
|
||||
{ id: "end_completed", label: "Completed", kind: "end", x: 92, y: 45 },
|
||||
export const presentationNodes: ReadonlyArray<PresentationNode> = [
|
||||
{ id: "read_docs", label: "Read documents", detail: "5 selected", kind: "node", x: 9, y: 57 },
|
||||
{ id: "build_report", label: "Build report", detail: "Markdown", kind: "node", x: 30, y: 35 },
|
||||
{ id: "review_issues", label: "Issue review", detail: "Typed interrupt", kind: "interrupt", x: 52, y: 57 },
|
||||
{ id: "create_issues", label: "Create issues", detail: "Selected only", kind: "node", x: 74, y: 35 },
|
||||
{ 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 = {
|
||||
readonly execution: GraphExecutionPresentation;
|
||||
readonly selectedNodeId: string | null;
|
||||
readonly selectNode: (nodeId: string) => void;
|
||||
};
|
||||
|
||||
export const WorkflowGraphStage = ({ selectedNodeId, selectNode }: WorkflowGraphStageProps) => (
|
||||
<section className="workflow-graph-stage" aria-label="workflow graph">
|
||||
{presentationNodes.map((node) => (
|
||||
<button
|
||||
key={node.id}
|
||||
type="button"
|
||||
className="workflow-graph-stage__node"
|
||||
data-kind={node.kind}
|
||||
data-selected={selectedNodeId === node.id}
|
||||
aria-pressed={selectedNodeId === node.id}
|
||||
style={{ left: `${node.x}%`, top: `${node.y}%` }}
|
||||
onClick={() => selectNode(node.id)}
|
||||
>
|
||||
{node.label}
|
||||
</button>
|
||||
))}
|
||||
</section>
|
||||
const executionStateForNode = (
|
||||
nodeId: string,
|
||||
execution: GraphExecutionPresentation,
|
||||
): NodeExecutionState => {
|
||||
if (execution.currentNodeId === nodeId) return "current";
|
||||
if (execution.completedNodeIds.includes(nodeId)) return "completed";
|
||||
return "future";
|
||||
};
|
||||
|
||||
const requireNode = (nodeId: string): PresentationNode => {
|
||||
const node = presentationNodes.find((candidate) => candidate.id === nodeId);
|
||||
if (!node) throw new Error(`presentation edge references unknown node ${nodeId}`);
|
||||
return node;
|
||||
};
|
||||
|
||||
export const WorkflowGraphStage = ({
|
||||
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 {
|
||||
height: 100dvh;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
background: oklch(0.12 0.025 250);
|
||||
color: oklch(0.96 0.01 250);
|
||||
background: var(--stage-canvas, oklch(0.12 0.025 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 {
|
||||
@@ -28,22 +39,26 @@ body {
|
||||
|
||||
/* Stage themes */
|
||||
.presentation-stage[data-stage-theme="paper"] {
|
||||
background: oklch(0.97 0.005 80);
|
||||
color: oklch(0.15 0.01 80);
|
||||
background: var(--paper-bg);
|
||||
color: var(--paper-text);
|
||||
}
|
||||
|
||||
.presentation-stage[data-stage-theme="night"] {
|
||||
background: oklch(0.12 0.025 250);
|
||||
color: oklch(0.96 0.01 250);
|
||||
background: var(--stage-canvas);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Paper theme child-element overrides */
|
||||
.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 {
|
||||
color: oklch(0.5 0.03 80);
|
||||
color: var(--paper-muted);
|
||||
}
|
||||
|
||||
.presentation-stage[data-stage-theme="paper"] .scene-rail__scene {
|
||||
@@ -168,16 +183,37 @@ body {
|
||||
}
|
||||
|
||||
/* 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);
|
||||
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);
|
||||
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 */
|
||||
.presentation-stage[data-chat-mode="hidden"] {
|
||||
grid-template:
|
||||
@@ -331,8 +367,8 @@ body {
|
||||
}
|
||||
|
||||
.operation-block {
|
||||
border: 1px solid oklch(0.36 0.04 250);
|
||||
background: oklch(0.16 0.025 250);
|
||||
border: 1px solid var(--stage-line);
|
||||
background: var(--stage-surface);
|
||||
padding: 0.75rem;
|
||||
border-radius: 0.65rem;
|
||||
flex-shrink: 0;
|
||||
@@ -343,10 +379,33 @@ body {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 0.75rem;
|
||||
color: oklch(0.78 0.035 250);
|
||||
color: var(--text-secondary);
|
||||
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 pre {
|
||||
white-space: pre-wrap;
|
||||
@@ -354,44 +413,166 @@ body {
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.operation-block__grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 0.75rem;
|
||||
.operation-block__command {
|
||||
margin: 0.5rem 0;
|
||||
padding: 0.5rem;
|
||||
background: var(--stage-inset);
|
||||
border-radius: 0.4rem;
|
||||
border: 1px solid var(--stage-line);
|
||||
}
|
||||
|
||||
@media (max-width: 800px) {
|
||||
.operation-block__grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
.operation-block__interpreted {
|
||||
margin: 0.5rem 0;
|
||||
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 {
|
||||
position: relative;
|
||||
min-height: 12rem;
|
||||
flex: 1;
|
||||
border: 1px solid oklch(0.32 0.04 250);
|
||||
background: oklch(0.14 0.025 250);
|
||||
border: 1px solid var(--stage-line);
|
||||
background: var(--stage-canvas);
|
||||
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 {
|
||||
position: absolute;
|
||||
transform: translate(-50%, -50%);
|
||||
border: 1px solid oklch(0.44 0.055 250);
|
||||
background: oklch(0.2 0.03 250);
|
||||
color: inherit;
|
||||
border: 1px solid var(--stage-line);
|
||||
background: var(--stage-surface);
|
||||
color: var(--text-primary);
|
||||
padding: 0.5rem 0.7rem;
|
||||
border-radius: 0.6rem;
|
||||
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"] {
|
||||
border-color: oklch(0.76 0.18 70);
|
||||
border-color: var(--accent-amber);
|
||||
}
|
||||
|
||||
.workflow-graph-stage__node[data-selected="true"] {
|
||||
outline: 3px solid oklch(0.72 0.17 195);
|
||||
outline: 3px solid var(--accent-cyan);
|
||||
}
|
||||
|
||||
.node-spotlight {
|
||||
@@ -937,3 +1118,61 @@ body {
|
||||
transition-duration: 0.01ms !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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user