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