docs: plan scene 10 guided product moment

This commit is contained in:
lda
2026-07-09 09:39:53 +07:00 Verified
parent 8de2885cfd
commit e879f18c4f
3 changed files with 1230 additions and 9 deletions
+23 -9
View File
@@ -38,6 +38,7 @@ Design contracts:
- [`demo autoplay and replay`](superpowers/specs/2026-07-03-demo-autoplay-replay.md) - [`demo autoplay and replay`](superpowers/specs/2026-07-03-demo-autoplay-replay.md)
- [`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)
- [`adaptive presentation canvas and evidence inspector`](superpowers/specs/2026-07-05-adaptive-presentation-canvas-design.md) - [`adaptive presentation canvas and evidence inspector`](superpowers/specs/2026-07-05-adaptive-presentation-canvas-design.md)
- [`Scene 10 guided product moment`](superpowers/specs/2026-07-09-scene-10-guided-product-moment-design.md)
Implementation order: Implementation order:
@@ -129,9 +130,15 @@ Implementation order:
approval, graph focus, and evidence transitions into one deterministic approval, graph focus, and evidence transitions into one deterministic
presenter sequence. Implementation: presenter sequence. Implementation:
[`guided run beat gates`](historical/superpowers/plans/2026-07-09-guided-run-beat-gates.md). [`guided run beat gates`](historical/superpowers/plans/2026-07-09-guided-run-beat-gates.md).
18. Future: presenter companion, Scene 10 product graph, final scene visuals, 18. Active: make Scene 10 a guided product moment with direct-link replay
readiness and one dominant proof surface per approval/resume/output/trace
beat. Design:
[`Scene 10 guided product moment`](superpowers/specs/2026-07-09-scene-10-guided-product-moment-design.md).
Plan:
[`Scene 10 guided product moment plan`](superpowers/plans/2026-07-09-scene-10-guided-product-moment.md).
19. Future: presenter companion, Scene 10 product graph, final scene visuals,
evidence assets, and rehearsal timing. evidence assets, and rehearsal timing.
19. Add a static slide/appendix shell only after presentation mode is clear. 20. 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.
Presentation wishlist / defense readiness: Presentation wishlist / defense readiness:
@@ -177,9 +184,15 @@ Presentation wishlist / defense readiness:
routes stage-aware, with body/support/action regions instead of plain routes stage-aware, with body/support/action regions instead of plain
document cards. Implementation: document cards. Implementation:
[`presentation discussion modal composition`](historical/superpowers/plans/2026-07-09-presentation-discussion-modal-composition.md). [`presentation discussion modal composition`](historical/superpowers/plans/2026-07-09-presentation-discussion-modal-composition.md).
10. Presentation craft pass: tune remaining motion, evidence receipt 10. Active: Scene 10 guided product moment primes replay state for direct
hashes and stages approval, resume, output, and trace as one readable
product flow. Design:
[`Scene 10 guided product moment`](superpowers/specs/2026-07-09-scene-10-guided-product-moment-design.md).
Plan:
[`Scene 10 guided product moment plan`](superpowers/plans/2026-07-09-scene-10-guided-product-moment.md).
11. Future: presentation craft pass for remaining motion, evidence receipt
placement, route-level caption contrast, and graph visual language after placement, route-level caption contrast, and graph visual language after
the discussion layer is stable. Scene 10 is stable.
- Presenter runbook: - Presenter runbook:
[`defense presentation runbook`](runbooks/defense-presentation.md) covers [`defense presentation runbook`](runbooks/defense-presentation.md) covers
exact URLs, keyboard controls, live RPC/demo fallback steps, story spine, exact URLs, keyboard controls, live RPC/demo fallback steps, story spine,
@@ -195,15 +208,16 @@ Presentation wishlist / defense readiness:
"run prepared agent" affordance, render tool calls/results with a standard "run prepared agent" affordance, render tool calls/results with a standard
modern AI-app vocabulary, and expose approval requests as normal chat events modern AI-app vocabulary, and expose approval requests as normal chat events
that can also drive presentation actions. that can also drive presentation actions.
- Schema form approval surface: render typed interrupt request/resume schemas as
reusable form UI shared by chat, Scene 10, and future console approval flows.
The current `{ "type": "object" }` proof is enough for architecture evidence,
but the product demo needs a real schema-derived approval form with submitted
and cancelled outcomes.
- Completed: guided run beat gates connect Scene 10 schema approval, chat - Completed: guided run beat gates connect Scene 10 schema approval, chat
approval, graph focus, and evidence transitions into one deterministic approval, graph focus, and evidence transitions into one deterministic
presenter sequence. Implementation: presenter sequence. Implementation:
[`guided run beat gates`](historical/superpowers/plans/2026-07-09-guided-run-beat-gates.md). [`guided run beat gates`](historical/superpowers/plans/2026-07-09-guided-run-beat-gates.md).
- Active: Scene 10 guided product moment removes delayed direct-link readiness
and gives approval, resume, output, and trace one dominant proof surface each.
Design:
[`Scene 10 guided product moment`](superpowers/specs/2026-07-09-scene-10-guided-product-moment-design.md).
Plan:
[`Scene 10 guided product moment plan`](superpowers/plans/2026-07-09-scene-10-guided-product-moment.md).
- Evidence assets and rehearsal timing: prepare fallback screenshots/recordings, - Evidence assets and rehearsal timing: prepare fallback screenshots/recordings,
expected run states, and a timed walkthrough checklist for a 15-minute defense. expected run states, and a timed walkthrough checklist for a 15-minute defense.
- Presenter companion feasibility: decide whether phone/laptop control is local - Presenter companion feasibility: decide whether phone/laptop control is local
@@ -0,0 +1,996 @@
# Scene 10 Guided Product Moment 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:** Make Scene 10 behave like a guided product flow: direct links are immediately ready, approval decisions clearly change state, and each beat has one dominant proof surface.
**Architecture:** Add presentation-owned beat readiness projection, extend `useDemoTimeline` with replay priming, and recompose Scene 10 around a guided product moment component. Replay priming is a display projection over the canonical recording; it must not fabricate live runtime evidence.
**Tech Stack:** React 19, TypeScript, Vitest, Testing Library, Playwright smoke, existing `useDemoTimeline`, existing `SchemaApprovalSurface`, existing presentation storyboard.
## Global Constraints
- Do not add a new transport, store, graph engine, or agent runtime.
- Do not fabricate cancelled replay evidence; the canonical recording only contains the submitted branch.
- Direct hashes for approval, resume, output, and trace must be immediately informative.
- Live execution must still run sequentially when the chat action starts a live workflow.
- `/console` must remain unaffected.
- Add comments around replay priming because it is presentation projection, not runtime execution.
- Keep styling within existing presentation CSS files and tokens.
---
## File Structure
- Create `web/apps/console/src/presentation/demo-beat-requirements.ts`
- Maps demo scene beats to the minimum replay event stage needed for direct-link readiness.
- Create `web/apps/console/src/presentation/demo-beat-requirements.test.ts`
- Pins requirement mapping for Scene 9 and Scene 10 beats.
- Modify `web/apps/console/src/demo/useDemoTimeline.ts`
- Adds `primeReplayToStage(stage)` to project the canonical recording to a display-ready state.
- Modify `web/apps/console/src/demo/useDemoTimeline.test.tsx`
- Tests replay priming and live-mode non-interference.
- Modify `web/apps/console/src/presentation/PresentationRoute.tsx`
- Calls replay priming when the current route enters a demo beat.
- Modify `web/apps/console/src/presentation/PresentationRoute.test.tsx`
- Tests immediate readiness and decision consequences from direct hashes.
- Create `web/apps/console/src/presentation/GuidedProductMoment.tsx`
- Owns Scene 10 hierarchy for approval/resume/output/trace.
- Create `web/apps/console/src/presentation/GuidedProductMoment.test.tsx`
- Pins hero surface selection and decision copy.
- Modify `web/apps/console/src/presentation/DemoWorkflowScene.tsx`
- Delegates Scene 10 beats to `GuidedProductMoment`.
- Modify `web/apps/console/src/presentation/styles/demo-workflow.css`
- Adds guided moment layout styles.
- Modify `docs/current_roadmap.md`
- Marks this plan active/completed at the right time.
---
### Task 1: Add beat readiness requirements
**Files:**
- Create: `web/apps/console/src/presentation/demo-beat-requirements.ts`
- Create: `web/apps/console/src/presentation/demo-beat-requirements.test.ts`
**Interfaces:**
- Produces:
```ts
import type { DemoEvent } from "../demo/timeline/models.js";
export type DemoBeatRequirement = {
readonly requiredStage: DemoEvent["stage"] | null;
readonly reason: string;
};
export const requirementForDemoBeat = (
sceneId: string,
beatId: string,
): DemoBeatRequirement;
```
- Consumes:
- `DemoEvent["stage"]` from existing replay model.
- [ ] **Step 1: Write failing model tests**
Create `web/apps/console/src/presentation/demo-beat-requirements.test.ts`:
```ts
import { describe, expect, it } from "vitest";
import { requirementForDemoBeat } from "./demo-beat-requirements.js";
describe("requirementForDemoBeat", () => {
it.each([
["workflow-demo", "operation", "run_start"],
["workflow-demo", "graph", "run_start"],
["workflow-demo", "interrupt", "interrupt"],
["interrupt-evidence", "approval", "interrupt"],
["interrupt-evidence", "resume", "run_resume"],
["interrupt-evidence", "output", "run_resume"],
["interrupt-evidence", "trace", "trace_read"],
] as const)("maps %s/%s to %s", (sceneId, beatId, stage) => {
expect(requirementForDemoBeat(sceneId, beatId).requiredStage).toBe(stage);
});
it("returns no required stage for non-demo beats", () => {
expect(requirementForDemoBeat("thesis", "title")).toEqual({
requiredStage: null,
reason: "No demo replay state needed.",
});
});
});
```
- [ ] **Step 2: Run test to verify it fails**
Run:
```bash
pnpm --dir web --filter @lda/console test -- src/presentation/demo-beat-requirements.test.ts
```
Expected: FAIL because the file does not exist.
- [ ] **Step 3: Implement mapping**
Create `web/apps/console/src/presentation/demo-beat-requirements.ts`:
```ts
import type { DemoEvent } from "../demo/timeline/models.js";
export type DemoBeatRequirement = {
readonly requiredStage: DemoEvent["stage"] | null;
readonly reason: string;
};
const requirements: Readonly<Record<string, DemoBeatRequirement>> = {
"workflow-demo/operation": {
requiredStage: "run_start",
reason: "Operation beat needs the recorded run start.",
},
"workflow-demo/graph": {
requiredStage: "run_start",
reason: "Graph beat needs the persisted run id.",
},
"workflow-demo/interrupt": {
requiredStage: "interrupt",
reason: "Interrupt beat needs the typed review payload.",
},
"interrupt-evidence/approval": {
requiredStage: "interrupt",
reason: "Approval beat needs the typed review payload before controls can act.",
},
"interrupt-evidence/resume": {
requiredStage: "run_resume",
reason: "Resume beat needs the recorded resume operation.",
},
"interrupt-evidence/output": {
requiredStage: "run_resume",
reason: "Output beat needs the resumed run output.",
},
"interrupt-evidence/trace": {
requiredStage: "trace_read",
reason: "Trace beat needs the recorded trace read.",
},
};
export const requirementForDemoBeat = (
sceneId: string,
beatId: string,
): DemoBeatRequirement =>
requirements[`${sceneId}/${beatId}`] ?? {
requiredStage: null,
reason: "No demo replay state needed.",
};
```
- [ ] **Step 4: Run test to verify it passes**
Run:
```bash
pnpm --dir web --filter @lda/console test -- src/presentation/demo-beat-requirements.test.ts
```
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
git add web/apps/console/src/presentation/demo-beat-requirements.ts web/apps/console/src/presentation/demo-beat-requirements.test.ts
git commit -m "feat: map demo beats to replay readiness"
```
---
### Task 2: Add replay priming to the demo timeline
**Files:**
- Modify: `web/apps/console/src/demo/useDemoTimeline.ts`
- Test: `web/apps/console/src/demo/useDemoTimeline.test.tsx`
**Interfaces:**
- Consumes:
- `DemoEvent["stage"]`
- canonical `DemoRecording`
- Produces:
```ts
readonly primeReplayToStage: (stage: DemoEvent["stage"] | null) => void;
```
- [ ] **Step 1: Write failing timeline tests**
In `web/apps/console/src/demo/useDemoTimeline.test.tsx`, add:
```tsx
it("primes replay to the interrupt stage immediately", () => {
const { result } = renderHook(() => useDemoTimeline("http://127.0.0.1:8765/rpc", vi.fn()));
act(() => result.current.start("replay"));
act(() => result.current.primeReplayToStage("interrupt"));
expect(result.current.state.mode).toBe("replay");
expect(result.current.state.phase).toBe("review");
expect(result.current.interruptPayload).not.toBeNull();
expect(result.current.state.events[result.current.state.appliedCount - 1]?.stage).toBe("interrupt");
});
it("primes replay to the resume stage with output projection", () => {
const { result } = renderHook(() => useDemoTimeline("http://127.0.0.1:8765/rpc", vi.fn()));
act(() => result.current.start("replay"));
act(() => result.current.primeReplayToStage("run_resume"));
expect(result.current.state.mode).toBe("replay");
expect(result.current.state.phase).toBe("paused");
expect(result.current.output?.created_issues).toHaveLength(1);
expect(result.current.state.events[result.current.state.appliedCount - 1]?.stage).toBe("run_resume");
});
it("does not prime live timelines", () => {
const { result } = renderHook(() => useDemoTimeline("http://127.0.0.1:8765/rpc", vi.fn()));
act(() => result.current.start("live"));
act(() => result.current.primeReplayToStage("interrupt"));
expect(result.current.state.mode).toBe("live");
expect(result.current.state.events).toEqual([]);
expect(result.current.interruptPayload).toBeNull();
});
```
- [ ] **Step 2: Run tests to verify they fail**
Run:
```bash
pnpm --dir web --filter @lda/console test -- src/demo/useDemoTimeline.test.tsx
```
Expected: FAIL because `primeReplayToStage` does not exist.
- [ ] **Step 3: Extend controller type**
In `web/apps/console/src/demo/useDemoTimeline.ts`, import the event type if
not already imported as a value type:
```ts
import type { DemoEvent, DemoRecording } from "./timeline/models.js";
```
Add to `DemoTimelineController`:
```ts
readonly primeReplayToStage: (stage: DemoEvent["stage"] | null) => void;
```
- [ ] **Step 4: Add projection helpers**
In `useDemoTimeline.ts`, add helper functions near `deriveMissingMessage`:
```ts
const phaseForPrimedStage = (
stage: DemoEvent["stage"] | null,
): DemoTimelineState["phase"] => {
if (stage === "interrupt") return "review";
if (stage === "completed") return "completed";
if (stage === "failed") return "failed";
return "paused";
};
const appliedCountForStage = (
events: ReadonlyArray<DemoEvent>,
stage: DemoEvent["stage"],
): number => {
const index = events.findIndex((event) => event.stage === stage);
return index === -1 ? 0 : index + 1;
};
```
Add a local helper inside the hook after state setters are declared:
```ts
const projectTransientState = useCallback((events: ReadonlyArray<DemoEvent>, appliedCount: number) => {
const appliedEvents = events.slice(0, appliedCount);
const interruptEvent = appliedEvents.find((event) => event.stage === "interrupt");
const resumeEvent = appliedEvents.find((event) => event.stage === "run_resume");
const traceEvent = appliedEvents.find((event) => event.stage === "trace_read");
const completedEvent = appliedEvents.find((event) => event.stage === "completed");
if (interruptEvent?.interpreted) {
const interpreted = interruptEvent.interpreted as { payload: LdaReportInterruptPayload };
setInterruptPayload(parseLdaReportInterruptPayload(interpreted.payload));
}
if (resumeEvent?.interpreted) {
const interpreted = resumeEvent.interpreted as { output: LdaReportOutput };
setOutput(parseLdaReportOutput(interpreted.output));
}
if (traceEvent?.interpreted) {
setTrace(decodeTracePage(traceEvent.interpreted));
}
if (completedEvent?.interpreted) {
const interpreted = completedEvent.interpreted as { output: LdaReportOutput; trace: TracePage };
setOutput(parseLdaReportOutput(interpreted.output));
setTrace(decodeTracePage(interpreted.trace));
}
}, []);
```
If `parseLdaReportInterruptPayload` is not currently used in this file, use it
here instead of a raw cast so malformed replay data fails visibly in tests.
- [ ] **Step 5: Add reducer action for priming**
In `web/apps/console/src/demo/timeline/reducer.ts`, add action:
```ts
| {
readonly type: "prime_replay";
readonly events: ReadonlyArray<DemoEvent>;
readonly appliedCount: number;
readonly phase: DemoTimelinePhase;
}
```
Add reducer case:
```ts
case "prime_replay":
return {
mode: "replay",
phase: action.phase,
events: action.events,
appliedCount: action.appliedCount,
autoplay: false,
error: null,
};
```
- [ ] **Step 6: Implement `primeReplayToStage`**
In `useDemoTimeline.ts`, add:
```ts
const primeReplayToStage = useCallback((stage: DemoEvent["stage"] | null) => {
if (stage === null || state.mode !== "replay") return;
const recording = activeRecording.current;
if (!recording) return;
const appliedCount = appliedCountForStage(recording.events, stage);
if (appliedCount === 0) return;
// Presentation priming projects the reviewed recording to the current beat.
// It is not runtime execution and must never run while the live timeline is active.
resetRuntime();
projectTransientState(recording.events, appliedCount);
dispatch({
type: "prime_replay",
events: recording.events,
appliedCount,
phase: phaseForPrimedStage(stage),
});
}, [projectTransientState, resetRuntime, state.mode]);
```
Return it from the controller.
- [ ] **Step 7: Run tests**
Run:
```bash
pnpm --dir web --filter @lda/console test -- src/demo/useDemoTimeline.test.tsx
```
Expected: PASS.
- [ ] **Step 8: Commit**
```bash
git add web/apps/console/src/demo/useDemoTimeline.ts web/apps/console/src/demo/useDemoTimeline.test.tsx web/apps/console/src/demo/timeline/reducer.ts
git commit -m "feat: prime replay timeline for demo beats"
```
---
### Task 3: Prime direct demo routes from PresentationRoute
**Files:**
- Modify: `web/apps/console/src/presentation/PresentationRoute.tsx`
- Test: `web/apps/console/src/presentation/PresentationRoute.test.tsx`
**Interfaces:**
- Consumes:
- `requirementForDemoBeat(sceneId, beatId)`
- `demo.primeReplayToStage(stage)`
- Produces:
- Direct demo hashes are immediately display-ready.
- [ ] **Step 1: Add failing direct-route readiness tests**
In `web/apps/console/src/presentation/PresentationRoute.test.tsx`, add:
```tsx
it("opens approval with enabled approval controls immediately after priming", async () => {
window.location.hash = "#scene/interrupt-evidence/approval";
const { PresentationRoute } = await import("./PresentationRoute.js");
render(<PresentationRoute />);
expect(await screen.findByRole("button", { name: "Submit" })).toBeEnabled();
expect(screen.getByRole("button", { name: "Cancel" })).toBeEnabled();
});
it("opens resume with resume operation proof immediately after priming", async () => {
window.location.hash = "#scene/interrupt-evidence/resume";
const { PresentationRoute } = await import("./PresentationRoute.js");
render(<PresentationRoute />);
expect(await screen.findByLabelText("workflow.runs.resume operation")).toBeInTheDocument();
});
```
Do not use arbitrary timeouts in these tests. `findBy*` is enough for React
effects; the UI should not wait for the replay autoplay timer.
- [ ] **Step 2: Run tests to verify they fail**
Run:
```bash
pnpm --dir web --filter @lda/console test -- src/presentation/PresentationRoute.test.tsx
```
Expected: FAIL because direct routes still depend on autoplay progression.
- [ ] **Step 3: Wire route priming**
In `PresentationRoute.tsx`, import:
```ts
import { requirementForDemoBeat } from "./demo-beat-requirements.js";
```
Add effect after replay start effect:
```ts
useEffect(() => {
if (state.location.kind !== "main") return;
if (demo.state.mode !== "replay") return;
const requirement = requirementForDemoBeat(
state.location.sceneId,
state.location.beatId,
);
demo.primeReplayToStage(requirement.requiredStage);
}, [demo.state.mode, demo.primeReplayToStage, state.location]);
```
Keep this effect separate from the replay `start("replay")` effect. The
separation makes the unusual presentation projection easier to reason about.
- [ ] **Step 4: Run route tests**
Run:
```bash
pnpm --dir web --filter @lda/console test -- src/presentation/PresentationRoute.test.tsx
```
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
git add web/apps/console/src/presentation/PresentationRoute.tsx web/apps/console/src/presentation/PresentationRoute.test.tsx
git commit -m "feat: prime presentation demo routes"
```
---
### Task 4: Add guided Scene 10 product moment component
**Files:**
- Create: `web/apps/console/src/presentation/GuidedProductMoment.tsx`
- Create: `web/apps/console/src/presentation/GuidedProductMoment.test.tsx`
- Modify: `web/apps/console/src/presentation/DemoWorkflowScene.tsx`
**Interfaces:**
- Consumes:
- `SceneBeatDefinition`
- `DemoTimelineController`
- `InterruptContractPresentation | null`
- `OperationPresentation | null`
- `DemoApprovalActions | undefined`
- `openEvidence: () => void`
- Produces:
```tsx
export type GuidedProductMomentProps = {
readonly beat: SceneBeatDefinition;
readonly demo: DemoTimelineController;
readonly contract: InterruptContractPresentation | null;
readonly operation: OperationPresentation | null;
readonly approvalActions?: DemoApprovalActions | undefined;
readonly openEvidence: () => void;
};
```
- [ ] **Step 1: Write failing component tests**
Create `web/apps/console/src/presentation/GuidedProductMoment.test.tsx`:
```tsx
import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { loadCanonicalDemoRecording } from "../demo/timeline/replay.js";
import type { DemoTimelineController } from "../demo/useDemoTimeline.js";
import { projectInterruptContract, projectOperationPresentation } from "./demo-workflow-model.js";
import { GuidedProductMoment } from "./GuidedProductMoment.js";
import { findBeat } from "./storyboard.js";
afterEach(() => cleanup());
const recording = loadCanonicalDemoRecording();
const runStart = recording.events.find((event) => event.stage === "run_start")!;
const runResume = recording.events.find((event) => event.stage === "run_resume")!;
const contract = projectInterruptContract(runStart, runResume);
const resumeOperation = projectOperationPresentation(runResume);
const demo = {
state: { mode: "replay", phase: "review", events: recording.events, appliedCount: 3, autoplay: false, error: null },
inFlight: false,
} as DemoTimelineController;
describe("GuidedProductMoment", () => {
it("makes approval the primary product decision", () => {
render(
<GuidedProductMoment
beat={findBeat("interrupt-evidence", "approval")!}
demo={demo}
contract={contract}
operation={null}
approvalActions={{
state: "ready",
canSubmit: true,
canCancel: true,
submit: vi.fn(async () => {}),
cancel: vi.fn(async () => {}),
}}
openEvidence={vi.fn()}
/>,
);
expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-moment", "approval");
expect(screen.getByText(/Run is paused/i)).toBeInTheDocument();
expect(screen.getByRole("button", { name: "Submit" })).toBeEnabled();
});
it("makes resume operation proof primary on resume beat", () => {
render(
<GuidedProductMoment
beat={findBeat("interrupt-evidence", "resume")!}
demo={demo}
contract={contract}
operation={resumeOperation}
openEvidence={vi.fn()}
/>,
);
expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-moment", "resume");
expect(screen.getByLabelText("workflow.runs.resume operation")).toBeInTheDocument();
});
});
```
- [ ] **Step 2: Run tests to verify they fail**
Run:
```bash
pnpm --dir web --filter @lda/console test -- src/presentation/GuidedProductMoment.test.tsx
```
Expected: FAIL because the component does not exist.
- [ ] **Step 3: Implement `GuidedProductMoment`**
Create `web/apps/console/src/presentation/GuidedProductMoment.tsx`:
```tsx
import type { DemoTimelineController } from "../demo/useDemoTimeline.js";
import type { DemoApprovalActions } from "./demo-approval-actions.js";
import type {
InterruptContractPresentation,
OperationPresentation,
} from "./demo-workflow-model.js";
import { DemoOutcomePanel } from "./DemoOutcomePanel.js";
import { InterruptContractPreview } from "./InterruptContractPreview.js";
import { OperationBlock } from "./OperationBlock.js";
import type { SceneBeatDefinition } from "./storyboard.js";
import { demoBeatLensForBeat } from "./demo-workflow-model.js";
export type GuidedProductMomentProps = {
readonly beat: SceneBeatDefinition;
readonly demo: DemoTimelineController;
readonly contract: InterruptContractPresentation | null;
readonly operation: OperationPresentation | null;
readonly approvalActions?: DemoApprovalActions | undefined;
readonly openEvidence: () => void;
};
const momentForBeat = (beatId: string): "approval" | "resume" | "output" | "trace" =>
beatId === "resume" || beatId === "output" || beatId === "trace" ? beatId : "approval";
const statusCopy = (
moment: ReturnType<typeof momentForBeat>,
approvalActions?: DemoApprovalActions,
): string => {
if (moment !== "approval") return "Same persisted run; inspect the proof below.";
if (approvalActions?.state === "submitted") return "Submitted. Same run resumed.";
if (approvalActions?.state === "cancelled") {
return "Cancelled in presentation replay. No resume evidence is shown.";
}
return "Run is paused. Submit resumes this same run.";
};
export const GuidedProductMoment = ({
beat,
demo,
contract,
operation,
approvalActions,
openEvidence,
}: GuidedProductMomentProps) => {
const moment = momentForBeat(beat.id);
const lens = demoBeatLensForBeat(beat.id);
return (
<section className="guided-product-moment" aria-label="current product moment" data-moment={moment}>
<header className="guided-product-moment__header">
<span>{lens.eyebrow}</span>
<strong>{lens.headline}</strong>
<p>{statusCopy(moment, approvalActions)}</p>
</header>
<div className="guided-product-moment__primary">
{moment === "approval" && contract ? (
<InterruptContractPreview
contract={contract}
mode="approval"
hero
approvalActions={approvalActions}
/>
) : null}
{moment === "resume" && demo.state.events.find((event) => event.stage === "run_resume") ? (
<OperationBlock
event={demo.state.events.find((event) => event.stage === "run_resume")!}
variant="expanded"
openEvidence={openEvidence}
/>
) : null}
{(moment === "output" || moment === "trace") ? (
<DemoOutcomePanel
beatId={beat.id}
lens={lens}
operation={operation}
contract={contract}
/>
) : null}
</div>
</section>
);
};
```
After writing this, remove duplicate `find()` calls by assigning `runResume`
before the return if the linter or reviewer flags it.
- [ ] **Step 4: Delegate Scene 10 beats**
In `DemoWorkflowScene.tsx`, import:
```ts
import { GuidedProductMoment } from "./GuidedProductMoment.js";
```
Before the existing return, add:
```ts
const isGuidedScene10 = scene.id === "interrupt-evidence";
```
Inside the stage div, render the guided moment for Scene 10 beats:
```tsx
{isGuidedScene10 ? (
<GuidedProductMoment
beat={beat}
demo={demo}
contract={contract}
operation={currentOperation}
approvalActions={approvalActions}
openEvidence={openEvidence}
/>
) : (
<>
{/* existing graph/operation/outcome rendering for Scene 9 */}
</>
)}
```
Keep the continuity rail and discussion links unchanged.
- [ ] **Step 5: Run tests**
Run:
```bash
pnpm --dir web --filter @lda/console test -- src/presentation/GuidedProductMoment.test.tsx src/presentation/DemoWorkflowScene.test.tsx
```
Expected: PASS. Update existing Scene 10 tests only where they assert old layout internals that are intentionally replaced.
- [ ] **Step 6: Commit**
```bash
git add web/apps/console/src/presentation/GuidedProductMoment.tsx web/apps/console/src/presentation/GuidedProductMoment.test.tsx web/apps/console/src/presentation/DemoWorkflowScene.tsx web/apps/console/src/presentation/DemoWorkflowScene.test.tsx
git commit -m "feat: stage scene 10 as guided product moment"
```
---
### Task 5: Style the guided moment and pin visual hierarchy
**Files:**
- Modify: `web/apps/console/src/presentation/styles/demo-workflow.css`
- Test: `web/apps/console/src/presentation/GuidedProductMoment.test.tsx`
**Interfaces:**
- Consumes:
- `.guided-product-moment`
- `data-moment="approval" | "resume" | "output" | "trace"`
- Produces:
- One dominant primary surface per beat.
- [ ] **Step 1: Add class contract tests**
In `GuidedProductMoment.test.tsx`, add:
```tsx
it("marks the primary surface for visual hierarchy", () => {
render(
<GuidedProductMoment
beat={findBeat("interrupt-evidence", "approval")!}
demo={demo}
contract={contract}
operation={null}
approvalActions={{
state: "ready",
canSubmit: true,
canCancel: true,
submit: vi.fn(async () => {}),
cancel: vi.fn(async () => {}),
}}
openEvidence={vi.fn()}
/>,
);
expect(screen.getByRole("region", { name: /current product moment/i })).toHaveClass("guided-product-moment");
expect(screen.getByLabelText("typed interrupt contract")).toHaveAttribute("data-hero", "true");
});
```
- [ ] **Step 2: Run test**
Run:
```bash
pnpm --dir web --filter @lda/console test -- src/presentation/GuidedProductMoment.test.tsx
```
Expected: PASS if Task 4 already produced the class and hero attribute.
- [ ] **Step 3: Add CSS**
In `web/apps/console/src/presentation/styles/demo-workflow.css`, add:
```css
.guided-product-moment {
display: grid;
grid-template-rows: auto minmax(0, 1fr);
gap: 0.75rem;
min-height: 0;
}
.guided-product-moment__header {
display: grid;
grid-template-columns: minmax(0, 0.55fr) minmax(0, 1fr);
gap: 0.35rem 1rem;
align-items: end;
padding: 0.75rem 0.9rem;
border: 1px solid var(--stage-line);
border-radius: 0.75rem;
background: color-mix(in oklch, var(--stage-surface) 86%, transparent);
}
.guided-product-moment__header span {
font-family: var(--font-mono);
font-size: 0.72rem;
color: var(--text-muted);
}
.guided-product-moment__header strong {
color: var(--text-primary);
font-size: 1.2rem;
line-height: 1.05;
}
.guided-product-moment__header p {
grid-column: 2;
margin: 0;
color: var(--text-muted);
}
.guided-product-moment__primary {
min-height: 0;
display: grid;
align-items: stretch;
}
.guided-product-moment[data-moment="approval"] .interrupt-contract-preview {
max-width: min(58rem, 100%);
justify-self: center;
}
.guided-product-moment[data-moment="resume"] .operation-block {
max-width: min(62rem, 100%);
justify-self: center;
}
```
Adjust class names to match current CSS tokens if the file uses different
variables. Do not add a third presentation palette.
- [ ] **Step 4: Run CSS-related tests**
Run:
```bash
pnpm --dir web --filter @lda/console test -- src/presentation/GuidedProductMoment.test.tsx src/presentation/DemoWorkflowScene.test.tsx
```
Expected: PASS.
- [ ] **Step 5: Commit**
```bash
git add web/apps/console/src/presentation/styles/demo-workflow.css web/apps/console/src/presentation/GuidedProductMoment.test.tsx
git commit -m "style: focus scene 10 product moment hierarchy"
```
---
### Task 6: Docs, verification, and plan archive
**Files:**
- Modify: `docs/current_roadmap.md`
- Move: `docs/superpowers/plans/2026-07-09-scene-10-guided-product-moment.md` to `docs/historical/superpowers/plans/2026-07-09-scene-10-guided-product-moment.md`
**Interfaces:**
- Produces:
- Roadmap completion entry.
- Archived plan after implementation.
- [ ] **Step 1: Run focused tests**
Run:
```bash
pnpm --dir web --filter @lda/console test -- src/demo/useDemoTimeline.test.tsx src/presentation/demo-beat-requirements.test.ts src/presentation/PresentationRoute.test.tsx src/presentation/GuidedProductMoment.test.tsx src/presentation/DemoWorkflowScene.test.tsx
```
Expected: PASS.
- [ ] **Step 2: Run typecheck**
Run:
```bash
pnpm --dir web --filter @lda/console typecheck
```
Expected: PASS.
- [ ] **Step 3: Run build**
Run:
```bash
pnpm --dir web --filter @lda/console build
```
Expected: PASS. Existing chunk-size warning is acceptable.
- [ ] **Step 4: Browser smoke**
With `pnpm dev` running, inspect or screenshot:
```text
http://127.0.0.1:5173/present#scene/interrupt-evidence/approval
http://127.0.0.1:5173/present#scene/interrupt-evidence/resume
http://127.0.0.1:5173/present#scene/interrupt-evidence/output
http://127.0.0.1:5173/present#scene/interrupt-evidence/trace
```
Required checks:
- Approval controls are enabled without waiting for autoplay.
- Resume operation is visible immediately on the resume hash.
- Output beat shows result proof, not approval controls.
- Trace beat shows trace/evidence proof, not approval controls.
- Cancel still does not show resume evidence in replay.
- [ ] **Step 5: Update roadmap**
In `docs/current_roadmap.md`, mark this slice completed:
```md
18. Completed: Scene 10 guided product moment primes replay state for direct
hashes and stages approval, resume, output, and trace as one readable
product flow. Design:
[`Scene 10 guided product moment`](superpowers/specs/2026-07-09-scene-10-guided-product-moment-design.md).
Implementation:
[`Scene 10 guided product moment plan`](historical/superpowers/plans/2026-07-09-scene-10-guided-product-moment.md).
```
Renumber following items.
- [ ] **Step 6: Archive plan**
Run:
```bash
git mv docs/superpowers/plans/2026-07-09-scene-10-guided-product-moment.md docs/historical/superpowers/plans/2026-07-09-scene-10-guided-product-moment.md
```
- [ ] **Step 7: Diff hygiene**
Run:
```bash
git diff --check
git status --short
```
Expected: no whitespace errors; only intended files listed.
- [ ] **Step 8: Commit**
```bash
git add docs/current_roadmap.md docs/historical/superpowers/plans/2026-07-09-scene-10-guided-product-moment.md
git commit -m "docs: complete scene 10 guided product moment"
```
---
## Self-Review
- Spec coverage: Direct readiness, approval consequences, replay truthfulness,
hierarchy, tests, docs, and smoke verification are covered.
- Placeholder scan: No TODO/TBD placeholders remain.
- Type consistency: `DemoBeatRequirement`, `requirementForDemoBeat`,
`primeReplayToStage`, and `GuidedProductMoment` signatures are consistent.
- Scope check: This is one focused slice. Chat replacement and presenter
companion remain future work.
@@ -0,0 +1,211 @@
# Scene 10 Guided Product Moment Design
## Purpose
Scene 10 should be the strongest product proof in the defense presentation. It
shows that `lda.chat` owns a durable run, pauses at a typed human boundary,
accepts an operator decision, resumes the same run, and leaves inspectable
output/trace evidence.
The current implementation is functionally correct but still reads like several
widgets placed on a slide. The next slice should turn it into a guided product
moment: one dominant visual per beat, immediate readiness on deep links, and
clear transitions from decision to proof.
## Problem
The approval beat currently has three UX problems:
1. **Delayed readiness:** a direct link to
`#scene/interrupt-evidence/approval` starts replay from the beginning, so the
approval controls remain disabled until autoplay reaches the interrupt.
2. **Weak hierarchy:** graph, receipt, schema approval, continuity rail, and
discussion links compete. The viewer must infer which part matters now.
3. **Unclear consequence:** after Submit or Cancel, the UI changes technically,
but it does not strongly explain what happened to the persisted run.
These are not backend problems. They are presentation-state and composition
problems.
## Design Goal
Make Scene 10 feel like a product flow:
```text
paused persisted run
-> schema-backed operator decision
-> same run resumes
-> output appears outside chat
-> trace/evidence remains inspectable
```
The presenter should be able to say less because the screen already answers:
- What is waiting?
- What decision is required?
- What happens after Submit?
- Why is Cancel not fake evidence?
- Where is the proof?
## Scope
In scope:
- Scene 10 and immediately related demo timeline projection.
- Replay readiness for direct hashes in Scenes 9 and 10.
- Visual hierarchy for approval/resume/output/trace beats.
- Clear copy for live vs replay proof.
- Tests that pin direct-link readiness and post-decision outcomes.
Out of scope:
- Replacing chat with AI Elements or AI SDK.
- Building a presenter companion.
- Adding a new workflow graph engine.
- Changing Python workflow runtime behavior.
- Adding a new cancelled replay recording.
## Core Interaction
### Direct route readiness
When the route opens a demo beat, replay state should be primed to the minimum
event boundary needed by that beat.
Examples:
- `workflow-demo/operation`: needs `run_start`.
- `workflow-demo/interrupt`: needs `interrupt`.
- `interrupt-evidence/approval`: needs `interrupt`.
- `interrupt-evidence/resume`: needs `run_resume`.
- `interrupt-evidence/output`: needs `completed` or `run_resume` output.
- `interrupt-evidence/trace`: needs `trace_read` or `completed` trace.
This is a presentation projection, not a runtime mutation. Live mode still
executes sequentially when the operator starts the prepared workflow.
### Approval
On the approval beat:
- Submit is enabled immediately when replay has been primed to the interrupt.
- Submit applies the existing submitted replay branch and jumps to `resume`.
- Cancel marks the decision as cancelled and stays on approval.
- Cancel must not display `workflow.runs.resume` in replay.
- The UI should state that replay has no cancelled run evidence.
### Hierarchy
Each beat gets one dominant visual:
- Approval: schema-backed decision surface.
- Resume: operation block for `workflow.runs.resume`.
- Output: result/output panel.
- Trace: trace/evidence receipt.
The graph is context, not the hero, during approval and resume. It may remain as
a compact strip or mini-map, but it must not compete with the decision or
operation proof.
## Components
### Beat Readiness Projection
Add a small model that maps beat IDs to required replay stages.
```ts
export type DemoBeatRequirement = {
readonly requiredStage: DemoEvent["stage"] | null;
readonly reason: string;
};
```
This model should live near the existing demo presentation model, not in the
generic timeline reducer. It is presentation-specific.
### Guided Product Moment
Add a presentation component for Scene 10 composition. It can reuse existing
building blocks but owns the hierarchy.
```tsx
<GuidedProductMoment
beat={beat}
demo={demo}
contract={contract}
operation={operation}
approvalActions={approvalActions}
openEvidence={openEvidence}
/>
```
It should avoid nested cards. Use a clear primary region, a slim context region,
and a proof footer/receipt.
### Decision Status Copy
The approval surface should show one of these states:
- Ready: "Run is paused. Submit resumes this same run."
- Submitted: "Submitted. Same run resumed."
- Cancelled: "Cancelled in presentation replay. No resume evidence is shown."
Do not describe replay cancellation as a real persisted cancelled run unless a
live run actually produced that evidence.
## Data Flow
```text
PresentationRoute location
-> beat readiness requirement
-> useDemoTimeline primes replay events for that beat
-> SceneBody/DemoWorkflowScene receives ready demo state
-> GuidedProductMoment renders the single dominant proof
```
Approval actions stay route-owned:
```text
SchemaApprovalSurface
-> DemoApprovalActions
-> PresentationRoute
-> useDemoTimeline submit/cancel
-> hash jump or local cancelled state
```
## Testing Requirements
Route tests:
- Direct approval hash renders enabled Submit/Cancel without waiting for
autoplay.
- Direct resume hash renders `workflow.runs.resume` without waiting for
autoplay.
- Submit advances to resume and shows resume proof.
- Cancel stays on approval, shows cancelled copy, and does not show resume
evidence after a delay.
Model tests:
- Beat requirement mapping covers all demo beats.
- Unknown beat returns no requirement.
Component tests:
- Approval beat marks the decision surface as the hero.
- Resume beat marks the operation block as the hero.
- Output/trace beats do not show approval controls.
Visual smoke:
- Capture approval, resume, output, and trace at 1280x720.
- Approval should have one dominant decision surface.
- Resume should have one dominant operation proof.
## Success Criteria
- A direct link to approval is immediately interactive.
- Submit and Cancel consequences are clear without verbal explanation.
- No fake cancelled replay evidence appears.
- Scene 10 reads as one product flow rather than a collection of widgets.
- Existing `/console` behavior is unchanged.