34 KiB
Presentation Scene Composition 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.
Goal: Recompose presentation Scenes 3, 4, and 5 so they use the 720p stage, and lightly tune Scene 10 graph/interrupt readability.
Architecture: Keep the existing storyboard, route, reducer, and demo state intact. Rewrite only the scene renderers inside SceneBody.tsx and tune CSS in the existing presentation stylesheets. Scene 10 remains the same data flow; this slice changes hierarchy and readability, not demo behavior.
Tech Stack: React, TypeScript, Vitest, Testing Library, CSS, existing Motion wrappers, existing Vite presentation route.
Global Constraints
- Follow design spec:
docs/superpowers/specs/2026-07-08-presentation-scene-composition-design.md. - Do not introduce a generic diagram framework in this slice.
- Do not replace the chat UI, Q&A panel, evidence inspector, storyboard catalog, or recursive architecture figure.
- Do not add demo RPC calls, recording events, or new runtime state.
- Preserve existing keyboard/hash navigation behavior.
- Screenshot smoke output belongs under
web/apps/console/.visual-smoke/, which is ignored. - Do not commit
.superpowers/local progress files.
File Structure
- Modify:
web/apps/console/src/presentation/SceneBody.tsx- Owns Scene 3, Scene 4, and Scene 5 markup.
- Modify:
web/apps/console/src/presentation/SceneBody.test.tsx- Adds structural tests for the recomposed scenes.
- Modify:
web/apps/console/src/presentation/presentation.css- Replaces the small-card scene CSS for positioning, boundary, and lifecycle.
- Modify:
web/apps/console/src/presentation/styles/demo-workflow.css- Tunes Scene 10 graph/contract readability and proportions.
- Modify:
web/apps/console/src/presentation/DemoWorkflowScene.test.tsx- Pins approval-contract readability without changing demo behavior.
- Modify:
docs/current_roadmap.md- Marks the composition pass complete after verification.
- Move:
docs/superpowers/plans/2026-07-08-presentation-scene-composition.md- To
docs/historical/superpowers/plans/2026-07-08-presentation-scene-composition.mdafter implementation.
- To
Task 1: Recompose Scene 3 Positioning Map
Files:
- Modify:
web/apps/console/src/presentation/SceneBody.tsx - Modify:
web/apps/console/src/presentation/SceneBody.test.tsx - Modify:
web/apps/console/src/presentation/presentation.css
Interfaces:
-
Consumes:
SceneDefinition,SceneBeatDefinition,DiscussionLinks, existingStageCaption. -
Produces: a
PositioningScenethat rendersaria-label="positioning map"and marks the center substrate withdata-positioning-active="true"onlda-position. -
Step 1: Write failing tests for the positioning map
Add these tests inside describe("SceneBody", () => { ... }) in SceneBody.test.tsx:
it("renders Scene 3 as a full positioning map", () => {
const location: PresentationLocation = { kind: "main", sceneId: "positioning", beatId: "landscape", focusPath: [] };
render(
<SceneBody
location={location}
demo={demo}
selectedNodeId={null}
selectNode={noop}
openEvidence={noop}
openDiscussion={noop}
onFocusPathChange={noop}
motionDisabled={false}
/>,
);
const map = screen.getByLabelText("positioning map");
expect(map).toHaveAttribute("data-positioning-active-region", "landscape");
expect(screen.getByText("Tool loops")).toBeInTheDocument();
expect(screen.getByText("Generated scripts")).toBeInTheDocument();
expect(screen.getByText("lda.chat")).toBeInTheDocument();
expect(screen.getByText("Agent graphs")).toBeInTheDocument();
expect(screen.getByText("MCP")).toBeInTheDocument();
});
it("emphasizes lda.chat in the positioning beat", () => {
const location: PresentationLocation = { kind: "main", sceneId: "positioning", beatId: "lda-position", focusPath: [] };
render(
<SceneBody
location={location}
demo={demo}
selectedNodeId={null}
selectNode={noop}
openEvidence={noop}
openDiscussion={noop}
onFocusPathChange={noop}
motionDisabled={false}
/>,
);
const substrate = screen.getByText("lda.chat").closest("[data-positioning-role='substrate']");
expect(substrate).toHaveAttribute("data-positioning-active", "true");
expect(screen.getByLabelText("positioning map")).toHaveAttribute("data-positioning-active-region", "lda");
});
- Step 2: Run positioning tests and verify they fail
Run:
pnpm --dir web --filter @lda/console test -- src/presentation/SceneBody.test.tsx
Expected: FAIL because positioning map, Generated scripts, and the new data attributes do not exist.
- Step 3: Replace
PositioningScenemarkup
In SceneBody.tsx, replace the current PositioningScene implementation with:
const PositioningScene = ({ scene, beat }: { scene: SceneDefinition; beat: SceneBeatDefinition }) => {
const highlightLda = beat.id === "lda-position";
return (
<>
<StageCaption eyebrow={`Act I · ${scene.claimClass}`} title={scene.title}>
<p>{beat.caption}</p>
</StageCaption>
<div
className="scene-body__positioning-map"
aria-label="positioning map"
data-positioning-active-region={highlightLda ? "lda" : "landscape"}
>
<section className="scene-body__positioning-column" aria-label="direct action patterns">
<p className="scene-body__positioning-label">Direct action</p>
<article className="scene-body__positioning-tile">
<strong>Tool loops</strong>
<span>Fast action, no durable lifecycle</span>
</article>
<article className="scene-body__positioning-tile">
<strong>Generated scripts</strong>
<span>Inspectable code, weak deployment records</span>
</article>
</section>
<article
className="scene-body__positioning-substrate"
data-positioning-role="substrate"
data-positioning-active={highlightLda ? "true" : "false"}
>
<span className="scene-body__positioning-label">This thesis</span>
<strong>lda.chat</strong>
<p>Typed workflow substrate for external agents and human operators.</p>
<ul>
<li>Lifecycle</li>
<li>Validation</li>
<li>Persisted records</li>
</ul>
</article>
<section className="scene-body__positioning-column" aria-label="adjacent systems">
<p className="scene-body__positioning-label">Adjacent systems</p>
<article className="scene-body__positioning-tile">
<strong>Hosted automation</strong>
<span>Managed triggers and app integrations</span>
</article>
<article className="scene-body__positioning-tile">
<strong>Agent graphs</strong>
<span>Durable planner loops</span>
</article>
<article className="scene-body__positioning-tile">
<strong>MCP</strong>
<span>Capability protocol boundary</span>
</article>
</section>
</div>
<p className="scene-body__evidence">{scene.evidencePointer}</p>
</>
);
};
- Step 4: Replace positioning CSS
In presentation.css, replace the existing .scene-body__positioning-grid, .scene-body__positioning-card, and .scene-body__positioning-card--active rules with:
.scene-body__positioning-map {
display: grid;
grid-template-columns: minmax(13rem, 0.9fr) minmax(20rem, 1.35fr) minmax(13rem, 0.9fr);
gap: 1rem;
align-items: stretch;
margin-top: 1.1rem;
min-height: 20rem;
}
.scene-body__positioning-column {
display: grid;
align-content: start;
gap: 0.7rem;
}
.scene-body__positioning-label {
margin: 0;
color: var(--color-editorial-muted, oklch(0.48 0.025 65));
font: 700 0.68rem/1 var(--font-mono, monospace);
letter-spacing: 0.08em;
text-transform: uppercase;
}
.scene-body__positioning-tile,
.scene-body__positioning-substrate {
border: 1px solid color-mix(in oklch, var(--color-editorial-muted, oklch(0.48 0.025 65)) 30%, transparent);
border-radius: 0.85rem;
background: color-mix(in oklch, var(--color-editorial-paper, oklch(0.975 0.012 82)) 78%, white);
color: var(--color-editorial-ink, oklch(0.19 0.015 65));
}
.scene-body__positioning-tile {
min-height: 5.8rem;
padding: 0.85rem;
}
.scene-body__positioning-tile strong {
display: block;
margin-bottom: 0.35rem;
color: var(--color-editorial-ink, oklch(0.19 0.015 65));
}
.scene-body__positioning-tile span {
color: color-mix(in oklch, var(--color-editorial-ink, oklch(0.19 0.015 65)) 72%, var(--color-editorial-muted, oklch(0.48 0.025 65)));
font-size: 0.82rem;
line-height: 1.35;
}
.scene-body__positioning-substrate {
display: grid;
align-content: center;
gap: 0.8rem;
padding: 1.15rem;
box-shadow: inset 0 0 0 1px color-mix(in oklch, var(--accent-cyan, oklch(0.7 0.16 195)) 18%, transparent);
}
.scene-body__positioning-substrate strong {
color: var(--color-editorial-ink, oklch(0.19 0.015 65));
font-size: 2rem;
line-height: 0.95;
}
.scene-body__positioning-substrate p {
max-width: 28ch;
margin: 0;
color: color-mix(in oklch, var(--color-editorial-ink, oklch(0.19 0.015 65)) 82%, var(--color-editorial-muted, oklch(0.48 0.025 65)));
}
.scene-body__positioning-substrate ul {
display: flex;
flex-wrap: wrap;
gap: 0.45rem;
margin: 0;
padding: 0;
list-style: none;
}
.scene-body__positioning-substrate li {
border: 1px solid color-mix(in oklch, var(--accent-cyan, oklch(0.7 0.16 195)) 42%, transparent);
border-radius: 999px;
padding: 0.25rem 0.5rem;
color: var(--color-editorial-ink, oklch(0.19 0.015 65));
font-size: 0.72rem;
font-weight: 700;
}
.scene-body__positioning-map[data-positioning-active-region="lda"] .scene-body__positioning-tile {
opacity: 0.58;
}
.scene-body__positioning-substrate[data-positioning-active="true"] {
transform: translateY(-0.25rem);
border-color: color-mix(in oklch, var(--accent-cyan, oklch(0.7 0.16 195)) 68%, transparent);
background: color-mix(in oklch, var(--accent-cyan, oklch(0.7 0.16 195)) 8%, var(--color-editorial-paper, oklch(0.975 0.012 82)));
}
- Step 5: Run tests and commit Task 1
Run:
pnpm --dir web --filter @lda/console test -- src/presentation/SceneBody.test.tsx
pnpm --dir web --filter @lda/console typecheck
Expected: PASS.
Commit:
git add web/apps/console/src/presentation/SceneBody.tsx web/apps/console/src/presentation/SceneBody.test.tsx web/apps/console/src/presentation/presentation.css
git commit -m "feat: recompose positioning scene"
Task 2: Recompose Scene 4 Planner Runtime Boundary
Files:
- Modify:
web/apps/console/src/presentation/SceneBody.tsx - Modify:
web/apps/console/src/presentation/SceneBody.test.tsx - Modify:
web/apps/console/src/presentation/presentation.css
Interfaces:
-
Consumes:
BoundaryScenebeat IDsplanner,runtime, andboundary. -
Produces: a boundary region with
aria-label="planner runtime boundary"anddata-boundary-active. -
Step 1: Write failing boundary tests
Add these tests to SceneBody.test.tsx:
it("renders Scene 4 as a planner runtime boundary", () => {
const location: PresentationLocation = { kind: "main", sceneId: "planner-runtime", beatId: "boundary", focusPath: [] };
render(
<SceneBody
location={location}
demo={demo}
selectedNodeId={null}
selectNode={noop}
openEvidence={noop}
openDiscussion={noop}
onFocusPathChange={noop}
motionDisabled={false}
/>,
);
const boundary = screen.getByLabelText("planner runtime boundary");
expect(boundary).toHaveAttribute("data-boundary-active", "boundary");
expect(screen.getByText("Planner")).toBeInTheDocument();
expect(screen.getByText("Runtime")).toBeInTheDocument();
expect(screen.getByText(/CLI/)).toBeInTheDocument();
expect(screen.getByText(/JSON-RPC/)).toBeInTheDocument();
});
it("marks the planner side active on the planner beat", () => {
const location: PresentationLocation = { kind: "main", sceneId: "planner-runtime", beatId: "planner", focusPath: [] };
render(
<SceneBody
location={location}
demo={demo}
selectedNodeId={null}
selectNode={noop}
openEvidence={noop}
openDiscussion={noop}
onFocusPathChange={noop}
motionDisabled={false}
/>,
);
expect(screen.getByLabelText("planner runtime boundary")).toHaveAttribute("data-boundary-active", "planner");
expect(screen.getByText("Planner").closest("[data-boundary-side='planner']")).toHaveAttribute("data-boundary-emphasis", "active");
});
- Step 2: Run boundary tests and verify they fail
Run:
pnpm --dir web --filter @lda/console test -- src/presentation/SceneBody.test.tsx
Expected: FAIL because the new aria label and data attributes do not exist.
- Step 3: Replace
BoundaryScenemarkup
In SceneBody.tsx, replace the current BoundaryScene with:
const boundaryActiveForBeat = (beatId: string): "planner" | "runtime" | "boundary" => {
if (beatId === "runtime") return "runtime";
if (beatId === "boundary") return "boundary";
return "planner";
};
const BoundaryScene = ({ scene, beat }: { scene: SceneDefinition; beat: SceneBeatDefinition }) => {
const active = boundaryActiveForBeat(beat.id);
const plannerActive = active === "planner" || active === "boundary";
const runtimeActive = active === "runtime" || active === "boundary";
return (
<>
<StageCaption eyebrow="Act II · implemented" title={scene.title}>
<p>{beat.caption}</p>
</StageCaption>
<div className="scene-body__boundary" aria-label="planner runtime boundary" data-boundary-active={active}>
<section
className="scene-body__boundary-pane"
data-boundary-side="planner"
data-boundary-emphasis={plannerActive ? "active" : "reduced"}
>
<span>External operator</span>
<h3>Planner</h3>
<ul>
<li>Proposes workflow structure</li>
<li>Revises steps and bindings</li>
<li>Chooses public operations</li>
</ul>
</section>
<div className="scene-body__boundary-seam" aria-label="workflow operation boundary">
<strong>CLI / JSON-RPC</strong>
<span>typed workflow operations</span>
</div>
<section
className="scene-body__boundary-pane"
data-boundary-side="runtime"
data-boundary-emphasis={runtimeActive ? "active" : "reduced"}
>
<span>lda.chat substrate</span>
<h3>Runtime</h3>
<ul>
<li>Validates schemas and routes</li>
<li>Executes deterministic nodes</li>
<li>Records traces and resume state</li>
</ul>
</section>
</div>
<p className="scene-body__evidence">{scene.evidencePointer}</p>
</>
);
};
- Step 4: Replace boundary CSS
In presentation.css, replace .scene-body__boundary, .scene-body__boundary-side, .scene-body__boundary-divider, and related dim rules with:
.scene-body__boundary {
display: grid;
grid-template-columns: minmax(0, 1fr) minmax(8rem, 0.26fr) minmax(0, 1fr);
gap: 1rem;
align-items: stretch;
margin-top: 1.2rem;
min-height: 20rem;
}
.scene-body__boundary-pane {
display: grid;
align-content: center;
gap: 0.85rem;
border: 1px solid color-mix(in oklch, var(--color-editorial-muted, oklch(0.48 0.025 65)) 32%, transparent);
border-radius: 1rem;
padding: 1.1rem;
background: color-mix(in oklch, var(--color-editorial-paper, oklch(0.975 0.012 82)) 82%, white);
color: var(--color-editorial-ink, oklch(0.19 0.015 65));
}
.scene-body__boundary-pane[data-boundary-emphasis="reduced"] {
opacity: 0.58;
}
.scene-body__boundary-pane h3 {
margin: 0;
color: var(--color-editorial-ink, oklch(0.19 0.015 65));
font-size: 1.8rem;
line-height: 1;
}
.scene-body__boundary-pane > span {
color: var(--color-editorial-muted, oklch(0.48 0.025 65));
font: 700 0.68rem/1 var(--font-mono, monospace);
letter-spacing: 0.08em;
text-transform: uppercase;
}
.scene-body__boundary-pane ul {
display: grid;
gap: 0.45rem;
margin: 0;
padding-left: 1rem;
color: color-mix(in oklch, var(--color-editorial-ink, oklch(0.19 0.015 65)) 78%, var(--color-editorial-muted, oklch(0.48 0.025 65)));
}
.scene-body__boundary-seam {
display: grid;
place-content: center;
gap: 0.35rem;
border-inline: 2px solid color-mix(in oklch, var(--accent-cyan, oklch(0.7 0.16 195)) 58%, transparent);
color: var(--color-editorial-ink, oklch(0.19 0.015 65));
text-align: center;
}
.scene-body__boundary-seam strong {
font-size: 0.85rem;
}
.scene-body__boundary-seam span {
color: var(--color-editorial-muted, oklch(0.48 0.025 65));
font-size: 0.72rem;
line-height: 1.25;
}
.scene-body__boundary[data-boundary-active="boundary"] .scene-body__boundary-seam {
background: color-mix(in oklch, var(--accent-cyan, oklch(0.7 0.16 195)) 7%, transparent);
}
- Step 5: Run tests and commit Task 2
Run:
pnpm --dir web --filter @lda/console test -- src/presentation/SceneBody.test.tsx
pnpm --dir web --filter @lda/console typecheck
Expected: PASS.
Commit:
git add web/apps/console/src/presentation/SceneBody.tsx web/apps/console/src/presentation/SceneBody.test.tsx web/apps/console/src/presentation/presentation.css
git commit -m "feat: recompose planner runtime scene"
Task 3: Recompose Scene 5 Lifecycle Rail
Files:
- Modify:
web/apps/console/src/presentation/SceneBody.tsx - Modify:
web/apps/console/src/presentation/SceneBody.test.tsx - Modify:
web/apps/console/src/presentation/presentation.css
Interfaces:
-
Consumes: lifecycle beat IDs
draft,artifact,deployment, andrun. -
Produces:
aria-label="workflow lifecycle rail"and a changing current-state explanation panel. -
Step 1: Write failing lifecycle tests
Add these tests to SceneBody.test.tsx:
it("renders Scene 5 as a full workflow lifecycle rail", () => {
const location: PresentationLocation = { kind: "main", sceneId: "lifecycle", beatId: "deployment", focusPath: [] };
render(
<SceneBody
location={location}
demo={demo}
selectedNodeId={null}
selectNode={noop}
openEvidence={noop}
openDiscussion={noop}
onFocusPathChange={noop}
motionDisabled={false}
/>,
);
const rail = screen.getByLabelText("workflow lifecycle rail");
expect(rail).toHaveAttribute("data-lifecycle-active-stage", "deployment");
expect(screen.getByText("Draft")).toBeInTheDocument();
expect(screen.getByText("Artifact")).toBeInTheDocument();
expect(screen.getByText("Deployment")).toBeInTheDocument();
expect(screen.getByText("Run")).toBeInTheDocument();
expect(screen.getByText("Source binding")).toBeInTheDocument();
});
it("updates the lifecycle explanation with the active beat", () => {
const { rerender } = render(
<SceneBody
location={{ kind: "main", sceneId: "lifecycle", beatId: "draft", focusPath: [] }}
demo={demo}
selectedNodeId={null}
selectNode={noop}
openEvidence={noop}
openDiscussion={noop}
onFocusPathChange={noop}
motionDisabled={false}
/>,
);
expect(screen.getByLabelText("current lifecycle state")).toHaveTextContent("Mutable authoring state");
rerender(
<SceneBody
location={{ kind: "main", sceneId: "lifecycle", beatId: "run", focusPath: [] }}
demo={demo}
selectedNodeId={null}
selectNode={noop}
openEvidence={noop}
openDiscussion={noop}
onFocusPathChange={noop}
motionDisabled={false}
/>,
);
expect(screen.getByLabelText("current lifecycle state")).toHaveTextContent("Execution record and trace");
});
- Step 2: Run lifecycle tests and verify they fail
Run:
pnpm --dir web --filter @lda/console test -- src/presentation/SceneBody.test.tsx
Expected: FAIL because the rail label and current-state panel do not exist.
- Step 3: Extend lifecycle data and replace
LifecycleScene
In SceneBody.tsx, replace lifecycleStages and LifecycleScene with:
const lifecycleStages = [
{ id: "draft", label: "Draft", role: "Mutable authoring state", detail: "Iterate before freezing a workflow definition." },
{ id: "artifact", label: "Artifact", role: "Immutable workflow definition", detail: "Save a versioned plan that can be deployed." },
{ id: "deployment", label: "Deployment", role: "Source binding", detail: "Bind workflow requirements to configured runtime sources." },
{ id: "run", label: "Run", role: "Execution record and trace", detail: "Persist status, outputs, interrupts, and trace evidence." },
] as const;
const LifecycleScene = ({ scene, beat }: { scene: SceneDefinition; beat: SceneBeatDefinition }) => {
const activeIndex = Math.max(0, lifecycleStages.findIndex((stage) => stage.id === beat.id));
const activeStage = lifecycleStages[activeIndex] ?? lifecycleStages[0];
return (
<>
<StageCaption eyebrow="Act II · implemented" title={scene.title}>
<p>{beat.caption}</p>
</StageCaption>
<div className="scene-body__lifecycle" aria-label="workflow lifecycle rail" data-lifecycle-active-stage={activeStage.id}>
{lifecycleStages.map((stage, i) => (
<article
key={stage.id}
className="scene-body__lifecycle-stage"
data-lifecycle-active={i === activeIndex ? "true" : "false"}
data-lifecycle-complete={i < activeIndex ? "true" : "false"}
>
<span className="scene-body__lifecycle-number">{i + 1}</span>
<strong>{stage.label}</strong>
<small>{stage.role}</small>
{i < lifecycleStages.length - 1 && <span className="scene-body__lifecycle-arrow">→</span>}
</article>
))}
</div>
<aside className="scene-body__lifecycle-current" aria-label="current lifecycle state">
<span>{activeStage.label}</span>
<strong>{activeStage.role}</strong>
<p>{activeStage.detail}</p>
</aside>
<p className="scene-body__evidence">{scene.evidencePointer}</p>
</>
);
};
- Step 4: Replace lifecycle CSS
In presentation.css, replace .scene-body__lifecycle, .scene-body__lifecycle-stage, .scene-body__lifecycle-number, .scene-body__lifecycle-arrow, and active-state lifecycle rules with:
.scene-body__lifecycle {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 0.65rem;
align-items: stretch;
margin-top: 1.2rem;
}
.scene-body__lifecycle-stage {
position: relative;
display: grid;
align-content: start;
gap: 0.5rem;
min-height: 9rem;
border: 1px solid color-mix(in oklch, var(--color-editorial-muted, oklch(0.48 0.025 65)) 32%, transparent);
border-radius: 0.9rem;
padding: 0.9rem;
background: color-mix(in oklch, var(--color-editorial-paper, oklch(0.975 0.012 82)) 82%, white);
color: var(--color-editorial-ink, oklch(0.19 0.015 65));
}
.scene-body__lifecycle-stage[data-lifecycle-active="false"] {
opacity: 0.68;
}
.scene-body__lifecycle-stage[data-lifecycle-active="true"] {
opacity: 1;
transform: translateY(-0.25rem);
border-color: color-mix(in oklch, var(--accent-cyan, oklch(0.7 0.16 195)) 58%, transparent);
background: color-mix(in oklch, var(--accent-cyan, oklch(0.7 0.16 195)) 8%, var(--color-editorial-paper, oklch(0.975 0.012 82)));
}
.scene-body__lifecycle-stage strong {
color: var(--color-editorial-ink, oklch(0.19 0.015 65));
font-size: 1.2rem;
}
.scene-body__lifecycle-stage small {
color: color-mix(in oklch, var(--color-editorial-ink, oklch(0.19 0.015 65)) 72%, var(--color-editorial-muted, oklch(0.48 0.025 65)));
line-height: 1.3;
}
.scene-body__lifecycle-number {
display: inline-flex;
align-items: center;
justify-content: center;
width: 1.5rem;
height: 1.5rem;
border-radius: 50%;
background: color-mix(in oklch, var(--accent-cyan, oklch(0.7 0.16 195)) 74%, white);
color: var(--color-editorial-ink, oklch(0.19 0.015 65));
font: 800 0.72rem/1 var(--font-mono, monospace);
}
.scene-body__lifecycle-arrow {
position: absolute;
right: -0.55rem;
top: 50%;
color: var(--color-editorial-muted, oklch(0.48 0.025 65));
transform: translateY(-50%);
}
.scene-body__lifecycle-current {
display: grid;
gap: 0.3rem;
margin-top: 0.9rem;
border: 1px solid color-mix(in oklch, var(--color-editorial-muted, oklch(0.48 0.025 65)) 28%, transparent);
border-radius: 0.85rem;
padding: 0.8rem 0.9rem;
background: color-mix(in oklch, var(--color-editorial-paper, oklch(0.975 0.012 82)) 86%, white);
color: var(--color-editorial-ink, oklch(0.19 0.015 65));
}
.scene-body__lifecycle-current span {
color: var(--color-editorial-muted, oklch(0.48 0.025 65));
font: 700 0.66rem/1 var(--font-mono, monospace);
letter-spacing: 0.08em;
text-transform: uppercase;
}
.scene-body__lifecycle-current strong,
.scene-body__lifecycle-current p {
margin: 0;
}
.scene-body__lifecycle-current p {
color: color-mix(in oklch, var(--color-editorial-ink, oklch(0.19 0.015 65)) 76%, var(--color-editorial-muted, oklch(0.48 0.025 65)));
}
Keep the existing .scene-body__authoring-number styles by splitting any combined selector that used .scene-body__lifecycle-number, .scene-body__authoring-number. After this task, .scene-body__authoring-number must still render the Scene 7 numbered circles.
- Step 5: Run tests and commit Task 3
Run:
pnpm --dir web --filter @lda/console test -- src/presentation/SceneBody.test.tsx
pnpm --dir web --filter @lda/console typecheck
Expected: PASS.
Commit:
git add web/apps/console/src/presentation/SceneBody.tsx web/apps/console/src/presentation/SceneBody.test.tsx web/apps/console/src/presentation/presentation.css
git commit -m "feat: recompose lifecycle scene"
Task 4: Tune Scene 10 Demo Graph And Interrupt Contract
Files:
- Modify:
web/apps/console/src/presentation/DemoWorkflowScene.test.tsx - Modify:
web/apps/console/src/presentation/styles/demo-workflow.css
Interfaces:
-
Consumes: existing
DemoWorkflowSceneapproval beat andInterruptContractPreview. -
Produces: a readable graph/contract layout without changing data or component props.
-
Step 1: Add an approval readability test
In DemoWorkflowScene.test.tsx, extend the existing "makes the Scene 10 approval contract the primary visual" test:
it("makes the Scene 10 approval contract the primary visual", () => {
renderBeat("approval", "interrupt-evidence");
const stage = screen.getByLabelText("demo workflow stage");
expect(stage).toHaveAttribute("data-demo-layout", "approval");
expect(screen.getByLabelText("typed interrupt contract")).toHaveAttribute("data-hero", "true");
expect(screen.getByLabelText("typed interrupt contract")).toHaveTextContent("Operator decision");
expect(screen.getByLabelText("typed interrupt contract")).toHaveTextContent("Resume outcomes");
expect(screen.getByLabelText("workflow graph")).toBeInTheDocument();
});
If the test already contains the first three assertions, replace it with the block above.
- Step 2: Run demo scene tests and verify they pass before CSS
Run:
pnpm --dir web --filter @lda/console test -- src/presentation/DemoWorkflowScene.test.tsx
Expected: PASS. This confirms the task is visual/readability only.
- Step 3: Tune demo workflow CSS
In web/apps/console/src/presentation/styles/demo-workflow.css, update only existing selectors. Use these target values:
.demo-workflow-stage[data-demo-layout="approval"] .demo-workflow-stage__graph,
.demo-workflow-stage[data-demo-layout="interrupt"] .demo-workflow-stage__graph {
grid-template-columns: minmax(18rem, 0.78fr) minmax(26rem, 1.22fr);
align-items: stretch;
}
.demo-workflow-stage[data-demo-layout="approval"] .interrupt-contract-preview,
.demo-workflow-stage[data-demo-layout="interrupt"] .interrupt-contract-preview {
min-width: 0;
color: var(--text-primary);
}
.demo-workflow-stage[data-demo-layout="approval"] .workflow-graph-stage,
.demo-workflow-stage[data-demo-layout="interrupt"] .workflow-graph-stage {
min-height: 18rem;
}
.presentation-route .workflow-graph-stage__node {
color: var(--text-primary);
}
.workflow-graph-stage__node strong {
color: var(--text-primary);
}
.workflow-graph-stage__node small,
.workflow-graph-stage__node-state {
color: color-mix(in oklch, var(--text-primary) 76%, var(--text-muted));
}
.workflow-graph-stage__node[data-execution-state="future"] {
opacity: 0.72;
}
.workflow-graph-stage__node[data-execution-state="completed"] {
opacity: 0.9;
}
.interrupt-contract-preview__schema pre {
max-height: 7.5rem;
color: color-mix(in oklch, var(--text-primary) 82%, var(--text-muted));
}
Do not duplicate selectors if they already exist. Merge these values into the current blocks.
- Step 4: Run tests and commit Task 4
Run:
pnpm --dir web --filter @lda/console test -- src/presentation/DemoWorkflowScene.test.tsx
pnpm --dir web --filter @lda/console typecheck
Expected: PASS.
Commit:
git add web/apps/console/src/presentation/DemoWorkflowScene.test.tsx web/apps/console/src/presentation/styles/demo-workflow.css
git commit -m "fix: tune demo graph contract readability"
Task 5: Screenshot Smoke, Roadmap, And Archive
Files:
- Modify:
docs/current_roadmap.md - Move:
docs/superpowers/plans/2026-07-08-presentation-scene-composition.mdtodocs/historical/superpowers/plans/2026-07-08-presentation-scene-composition.md
Interfaces:
-
Consumes: completed scene composition tasks.
-
Produces: verified screenshot set and live roadmap entry pointing to the archived plan.
-
Step 1: Run focused verification
Run:
pnpm --dir web --filter @lda/console test -- src/presentation/SceneBody.test.tsx src/presentation/DemoWorkflowScene.test.tsx
pnpm --dir web --filter @lda/console typecheck
pnpm --dir web --filter @lda/console build
Expected:
-
SceneBody and DemoWorkflowScene tests pass.
-
Typecheck prints no TypeScript errors.
-
Build succeeds with the existing Vite chunk-size warning only.
-
Step 2: Capture screenshot smoke
With pnpm dev running, use the existing browser smoke method used by prior presentation slices. Capture these routes at 1280x720:
http://127.0.0.1:5173/present#scene/positioning/landscape
http://127.0.0.1:5173/present#scene/positioning/lda-position
http://127.0.0.1:5173/present#scene/planner-runtime/planner
http://127.0.0.1:5173/present#scene/planner-runtime/boundary
http://127.0.0.1:5173/present#scene/lifecycle/draft
http://127.0.0.1:5173/present#scene/lifecycle/run
http://127.0.0.1:5173/present#scene/workflow-demo/approval
Save screenshots under:
web/apps/console/.visual-smoke/
Expected visual checks:
-
Scene 3 uses the center of the canvas and reads as a map.
-
Scene 4 has a clear central boundary and two large panes.
-
Scene 5 has a readable Draft -> Artifact -> Deployment -> Run rail.
-
Scene 10 graph labels and interrupt-contract labels are readable.
-
No stage scrollbars are visible.
-
Step 3: Update roadmap
In docs/current_roadmap.md, update the presentation visual slices list so item 3 reads:
3. Completed: scene composition pass expanded the positioning map,
planner/runtime boundary, lifecycle rail, and Scene 10 demo
graph/contract readability. Implementation:
[`presentation scene composition`](historical/superpowers/plans/2026-07-08-presentation-scene-composition.md).
4. Presentation craft pass: tune motion, discussion-chip placement, Q&A modal
hierarchy, evidence receipt placement, and remaining generic-slide styling
after the compositions are stable.
- Step 4: Archive the plan
Run:
Move-Item docs/superpowers/plans/2026-07-08-presentation-scene-composition.md docs/historical/superpowers/plans/2026-07-08-presentation-scene-composition.md
- Step 5: Run docs/status checks
Run:
rg -n '2026-07-08-presentation-scene-composition' docs/current_roadmap.md docs/historical/superpowers/plans docs/superpowers/plans
git status --short
git diff --check
Expected:
-
docs/current_roadmap.mdpoints to the historical plan path. -
No active plan with this filename remains in
docs/superpowers/plans/. -
.visual-smoke/is ignored. -
.superpowers/is not staged. -
git diff --checkreports no whitespace errors. -
Step 6: Commit Task 5
Commit:
git add docs/current_roadmap.md docs/historical/superpowers/plans/2026-07-08-presentation-scene-composition.md
git commit -m "docs: record presentation scene composition"
Final Verification
Run:
pnpm --dir web test
pnpm --dir web typecheck
pnpm --dir web build
git diff --check
git status --short
Expected:
- All web tests pass, except any explicitly reported pre-existing flaky timeout must be named in the final report with the failing test.
- Typecheck is clean.
- Build succeeds with only the existing Vite chunk-size warning.
- Whitespace check is clean.
- Git status has no unintended files;
.superpowers/and.visual-smoke/are not staged.
Self-Review
- Spec coverage: Tasks 1-3 cover Scenes 3, 4, and 5. Task 4 covers Scene 10. Task 5 covers screenshots, roadmap, and plan archival.
- Completion-word scan: no unresolved implementation markers are present.
- Type consistency: all new attributes and labels referenced by tests are produced by the matching scene renderers.
- Scope check: no chat replacement, Q&A rewrite, evidence-inspector redesign, route change, or demo state change is included.