# Presentation Chat Timeline Bridge 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 presentation chat operate the existing prepared workflow timeline so `/present` can start, approve/cancel, resume, and inspect the demo through the same live/replay execution path as the product console. **Architecture:** `useDemoTimeline` remains the only owner of workflow execution, RPC calls, replay progression, run IDs, interrupt payloads, output, trace, and evidence recording. The agent/chat layer becomes an adapter that emits messages around timeline commands instead of executing a parallel replay driver. `/present` resolves a live RPC target when possible, falls back to replay when unavailable, and passes one timeline controller to both the scene renderer and chat bridge. **Tech Stack:** React 19, TypeScript, Vitest, Testing Library, existing `callOperation`/`useDemoTimeline` RPC path, no new runtime dependencies. ## Global Constraints - Do not add AI SDK, AI Elements, or a new chat component library in this slice. - Do not add another RPC transport or direct `@effect/rpc` caller in presentation code; use the existing console `callOperation` path through `useDemoTimeline`. - Do not make `/present` require a live server. Replay must remain the safe default/fallback. - Chat must trigger real timeline methods when live mode is active: `start`, `next`, `submitSelectedIssues`, and `cancelReview`. - Keep `AgentDriver` as the future AI SDK seam, but do not force the live timeline bridge into the old prepared replay driver shape if a small controller adapter is clearer. - Use submitted/cancelled outcome language for workflow outcomes. Internal booleans such as `approved` may remain where existing runtime payloads require them. - Keep tests scoped; do not run the full repo unless focused web checks pass first. --- ## File Structure - Create `web/apps/console/src/presentation/live-target.ts` - Reads the persisted console target safely and decides whether `/present` can attempt live mode. - Create `web/apps/console/src/presentation/live-target.test.ts` - Tests session storage fallback, explicit URL parsing, and safe failure when storage is unavailable. - Create `web/apps/console/src/demo/agent/timelineAgent.ts` - Builds messages and actions around `DemoTimelineController` instead of replay events. - Create `web/apps/console/src/demo/agent/timelineAgent.test.tsx` - Hook/controller tests for start, live/replay mode labels, approval submit/cancel, and failure copy. - Modify `web/apps/console/src/demo/agent/events.ts` - Add a small command/result vocabulary if needed for timeline-backed chat status. - Modify `web/apps/console/src/demo/agent/useDemoAgent.ts` - Either keep as the replay-driver hook or add a sibling hook; do not contort it if timeline-backed chat has different lifecycle. - Modify `web/apps/console/src/presentation/OperatorChat.tsx` - Add a visible "Run prepared workflow" affordance in chat and route approval submit/cancel through timeline-backed handlers. - Modify `web/apps/console/src/presentation/OperatorChat.test.tsx` - Cover the run affordance, live/replay status, schema approval submit/cancel, and disabled state. - Modify `web/apps/console/src/presentation/PresentationRoute.tsx` - Resolve target, create `useDemoTimeline(target, recordEvidence, recording)`, create timeline-backed chat controller, and stop hardcoding replay-only timeline. - Modify `web/apps/console/src/presentation/PresentationRoute.test.tsx` - Cover replay default, live target use, chat run click advancing the prepared timeline, and approval flow wiring. - Modify `web/apps/console/src/presentation/presentation.css` or `styles/demo-workflow.css` - Minimal styling for the chat run affordance and live/replay status. Do not redesign chat in this slice. - Modify `docs/current_roadmap.md` - Mark this bridge completed after implementation and leave source-owned AI Elements as a later slice. --- ### Task 1: Presentation Live Target Resolution **Files:** - Create: `web/apps/console/src/presentation/live-target.ts` - Create: `web/apps/console/src/presentation/live-target.test.ts` **Interfaces:** - Produces: ```ts export type PresentationTargetState = | { readonly mode: "live"; readonly target: string; readonly source: "session-storage" | "default" } | { readonly mode: "replay"; readonly target: null; readonly reason: string }; export const DEFAULT_PRESENTATION_TARGET = "http://127.0.0.1:8765/rpc"; export const resolvePresentationTarget = ( storage?: Storage | null, ): PresentationTargetState; ``` - Consumes: no React APIs; this is pure browser-boundary logic. - [ ] **Step 1: Write the failing target tests** Create `web/apps/console/src/presentation/live-target.test.ts`: ```ts import { describe, expect, it } from "vitest"; import { DEFAULT_PRESENTATION_TARGET, resolvePresentationTarget, } from "./live-target.js"; const fakeStorage = (value: string | null): Storage => ({ length: value === null ? 0 : 1, clear() {}, getItem: (key: string) => key === "lda.workflowConsole.target" ? value : null, key: () => null, removeItem() {}, setItem() {}, }); describe("resolvePresentationTarget", () => { it("uses the console target from session storage", () => { expect(resolvePresentationTarget(fakeStorage("http://127.0.0.1:8765/rpc"))).toEqual({ mode: "live", target: "http://127.0.0.1:8765/rpc", source: "session-storage", }); }); it("falls back to the default loopback target when storage is empty", () => { expect(resolvePresentationTarget(fakeStorage(null))).toEqual({ mode: "live", target: DEFAULT_PRESENTATION_TARGET, source: "default", }); }); it("uses replay mode when storage access throws", () => { const broken = { getItem() { throw new Error("blocked"); }, } as unknown as Storage; expect(resolvePresentationTarget(broken)).toEqual({ mode: "replay", target: null, reason: "session storage is unavailable", }); }); it("uses replay mode for non-http targets", () => { expect(resolvePresentationTarget(fakeStorage("file:///tmp/rpc"))).toEqual({ mode: "replay", target: null, reason: "presentation target is not an HTTP URL", }); }); }); ``` - [ ] **Step 2: Run the tests to verify they fail** Run: ```bash pnpm --filter @lda/console test -- src/presentation/live-target.test.ts ``` Expected: FAIL because `live-target.ts` does not exist. - [ ] **Step 3: Implement target resolution** Create `web/apps/console/src/presentation/live-target.ts`: ```ts import { STORAGE_KEY } from "../app/state.js"; export type PresentationTargetState = | { readonly mode: "live"; readonly target: string; readonly source: "session-storage" | "default" } | { readonly mode: "replay"; readonly target: null; readonly reason: string }; export const DEFAULT_PRESENTATION_TARGET = "http://127.0.0.1:8765/rpc"; const isHttpUrl = (value: string): boolean => { try { const url = new URL(value); return url.protocol === "http:" || url.protocol === "https:"; } catch { return false; } }; export const resolvePresentationTarget = ( storage: Storage | null = typeof sessionStorage === "undefined" ? null : sessionStorage, ): PresentationTargetState => { let stored: string | null; try { stored = storage?.getItem(STORAGE_KEY) ?? null; } catch { return { mode: "replay", target: null, reason: "session storage is unavailable", }; } const target = stored ?? DEFAULT_PRESENTATION_TARGET; if (!isHttpUrl(target)) { return { mode: "replay", target: null, reason: "presentation target is not an HTTP URL", }; } return { mode: "live", target, source: stored ? "session-storage" : "default", }; }; ``` - [ ] **Step 4: Run the target tests** Run: ```bash pnpm --filter @lda/console test -- src/presentation/live-target.test.ts ``` Expected: PASS. - [ ] **Step 5: Commit** ```bash git add web/apps/console/src/presentation/live-target.ts web/apps/console/src/presentation/live-target.test.ts git commit -m "feat: resolve presentation live target" ``` --- ### Task 2: Timeline-Backed Chat Controller **Files:** - Create: `web/apps/console/src/demo/agent/timelineAgent.ts` - Create: `web/apps/console/src/demo/agent/timelineAgent.test.tsx` - Modify: `web/apps/console/src/demo/agent/events.ts` **Interfaces:** - Consumes: ```ts import type { DemoTimelineController } from "../useDemoTimeline.js"; ``` - Produces: ```ts export type TimelineAgentMode = "live" | "replay"; export type TimelineAgentController = { readonly messages: ReadonlyArray; readonly canRun: boolean; readonly runLabel: string; readonly runPreparedWorkflow: () => Promise; readonly submitSelectedIssues: () => Promise; readonly cancelReview: () => Promise; }; export const useTimelineAgent = ( demo: DemoTimelineController, modeLabel: TimelineAgentMode, ): TimelineAgentController; ``` - The hook may use React state internally. It must not call `callOperation` directly. - [ ] **Step 1: Add event helper if needed** Modify `web/apps/console/src/demo/agent/events.ts` only if the existing `agentTextMessage`, `agentToolCallPart`, and `agentToolResultPart` are insufficient. Prefer not changing it unless tests need a stable message shape. If you add a helper, use this exact shape: ```ts export const agentToolMessage = ( id: string, name: AgentToolName, input: unknown, status: "success" | "failure", output: unknown, ): AgentMessage => ({ id, role: "assistant", parts: [ agentToolCallPart(`${id}-call`, name, input), agentToolResultPart(`${id}-call`, name, status, output), ], }); ``` - [ ] **Step 2: Write failing timeline-agent tests** Create `web/apps/console/src/demo/agent/timelineAgent.test.tsx`: ```tsx import { act, renderHook } from "@testing-library/react"; import { describe, expect, it, vi } from "vitest"; import type { DemoTimelineController } from "../useDemoTimeline.js"; import { initialDemoTimelineState } from "../timeline/reducer.js"; import { useTimelineAgent } from "./timelineAgent.js"; const demoController = ( overrides: Partial = {}, ): DemoTimelineController => ({ state: initialDemoTimelineState, inFlight: false, interruptPayload: null, output: null, trace: null, missingDeploymentMessage: null, recordingId: null, canStart: true, setMode: vi.fn(), start: vi.fn(), pause: vi.fn(), play: vi.fn(), next: vi.fn(async () => {}), submitSelectedIssues: vi.fn(async () => {}), cancelReview: vi.fn(async () => {}), restart: vi.fn(), ...overrides, }); describe("useTimelineAgent", () => { it("starts the prepared workflow through the timeline", async () => { const start = vi.fn(); const demo = demoController({ start }); const { result } = renderHook(() => useTimelineAgent(demo, "live")); await act(async () => result.current.runPreparedWorkflow()); expect(start).toHaveBeenCalledTimes(1); expect(result.current.messages.at(-1)?.parts).toEqual( expect.arrayContaining([ expect.objectContaining({ type: "tool-result" }), ]), ); }); it("submits selected issues from the current interrupt payload", async () => { const submitSelectedIssues = vi.fn(async () => {}); const demo = demoController({ state: { ...initialDemoTimelineState, phase: "review" }, interruptPayload: { report_markdown: "# Report", proposed_issues: [ { id: "risk-1", title: "Risk", body: "Body", severity: "medium" }, ], }, submitSelectedIssues, }); const { result } = renderHook(() => useTimelineAgent(demo, "replay")); await act(async () => result.current.submitSelectedIssues()); expect(submitSelectedIssues).toHaveBeenCalledWith(["risk-1"], "Create the selected issue."); }); it("cancels review through the timeline", async () => { const cancelReview = vi.fn(async () => {}); const demo = demoController({ state: { ...initialDemoTimelineState, phase: "review" }, cancelReview, }); const { result } = renderHook(() => useTimelineAgent(demo, "live")); await act(async () => result.current.cancelReview()); expect(cancelReview).toHaveBeenCalledWith("Cancelled by operator."); }); it("disables run when the timeline cannot start", () => { const demo = demoController({ canStart: false }); const { result } = renderHook(() => useTimelineAgent(demo, "live")); expect(result.current.canRun).toBe(false); }); }); ``` - [ ] **Step 3: Run tests to verify they fail** Run: ```bash pnpm --filter @lda/console test -- src/demo/agent/timelineAgent.test.tsx ``` Expected: FAIL because `timelineAgent.ts` does not exist. - [ ] **Step 4: Implement `useTimelineAgent`** Create `web/apps/console/src/demo/agent/timelineAgent.ts`: ```ts import { useCallback, useMemo, useState } from "react"; import type { DemoTimelineController } from "../useDemoTimeline.js"; import { agentTextMessage, agentToolCallPart, agentToolResultPart, type AgentMessage, } from "./events.js"; export type TimelineAgentMode = "live" | "replay"; export type TimelineAgentController = { readonly messages: ReadonlyArray; readonly canRun: boolean; readonly runLabel: string; readonly runPreparedWorkflow: () => Promise; readonly submitSelectedIssues: () => Promise; readonly cancelReview: () => Promise; }; const DEFAULT_COMMENT = "Create the selected issue."; const appendToolMessage = ( messages: ReadonlyArray, id: string, name: "startPreparedReportRun" | "resumeIssueReview" | "readRunTrace", input: unknown, output: unknown, ): ReadonlyArray => [ ...messages, { id, role: "assistant", parts: [ agentToolCallPart(`${id}-call`, name, input), agentToolResultPart(`${id}-call`, name, "success", output), ], }, ]; export const useTimelineAgent = ( demo: DemoTimelineController, modeLabel: TimelineAgentMode, ): TimelineAgentController => { const [messages, setMessages] = useState>([ agentTextMessage( "timeline-agent-intro", "assistant", modeLabel === "live" ? "Live workflow server is available. I can run the prepared workflow now." : "Replay fallback is active. I can still walk through the prepared workflow evidence.", ), ]); const runLabel = modeLabel === "live" ? "Run prepared workflow" : "Run replay walkthrough"; const canRun = demo.canStart && !demo.inFlight && demo.state.phase !== "running"; const runPreparedWorkflow = useCallback(async () => { if (!demo.canStart || demo.inFlight) return; demo.restart(); demo.start(); setMessages((current) => appendToolMessage( current, "timeline-agent-start", "startPreparedReportRun", { mode: modeLabel }, { phase: "started" }, )); }, [demo, modeLabel]); const selectedIssueIds = useMemo( () => demo.interruptPayload?.proposed_issues.map((issue) => issue.id) ?? [], [demo.interruptPayload], ); const submitSelectedIssues = useCallback(async () => { if (selectedIssueIds.length === 0) return; await demo.submitSelectedIssues(selectedIssueIds, DEFAULT_COMMENT); await demo.next(); setMessages((current) => appendToolMessage( current, "timeline-agent-submit", "resumeIssueReview", { selectedIssueIds }, { outcome: "submitted" }, )); }, [demo, selectedIssueIds]); const cancelReview = useCallback(async () => { await demo.cancelReview("Cancelled by operator."); await demo.next(); setMessages((current) => appendToolMessage( current, "timeline-agent-cancel", "resumeIssueReview", {}, { outcome: "cancelled" }, )); }, [demo]); return { messages, canRun, runLabel, runPreparedWorkflow, submitSelectedIssues, cancelReview, }; }; ``` If TypeScript reports that the tool names are not assignable, import `type AgentToolName` and use it in `appendToolMessage`. - [ ] **Step 5: Run timeline-agent tests** Run: ```bash pnpm --filter @lda/console test -- src/demo/agent/timelineAgent.test.tsx ``` Expected: PASS. - [ ] **Step 6: Commit** ```bash git add web/apps/console/src/demo/agent/timelineAgent.ts web/apps/console/src/demo/agent/timelineAgent.test.tsx web/apps/console/src/demo/agent/events.ts git commit -m "feat: add timeline backed demo agent" ``` --- ### Task 3: Chat UI Runs The Timeline **Files:** - Modify: `web/apps/console/src/presentation/OperatorChat.tsx` - Modify: `web/apps/console/src/presentation/OperatorChat.test.tsx` - Modify: `web/apps/console/src/presentation/presentation.css` **Interfaces:** - Consumes: ```ts import type { TimelineAgentController } from "../demo/agent/timelineAgent.js"; ``` - Produces: ```ts type OperatorChatProps = { readonly state: PresentationState; readonly messages?: ReadonlyArray | undefined; readonly timelineAgent?: TimelineAgentController | undefined; readonly onApprove?: (() => void) | undefined; readonly onDeny?: (() => void) | undefined; }; ``` - [ ] **Step 1: Write failing chat tests** Modify `web/apps/console/src/presentation/OperatorChat.test.tsx`: ```tsx it("shows a chat-owned run prepared workflow action", async () => { const user = userEvent.setup(); const runPreparedWorkflow = vi.fn(async () => {}); render( {}), cancelReview: vi.fn(async () => {}), }} />, ); await user.click(screen.getByRole("button", { name: /run prepared workflow/i })); expect(runPreparedWorkflow).toHaveBeenCalledTimes(1); }); it("routes schema approval submit and cancel through the timeline agent when present", async () => { const user = userEvent.setup(); const submitSelectedIssues = vi.fn(async () => {}); const cancelReview = vi.fn(async () => {}); const messages: ReadonlyArray = [ { id: "approval", role: "assistant", parts: [ { type: "approval-request", callId: "call-1", name: "resumeIssueReview", prompt: "Submit resume request?", contract: { kind: "issue_review", outcomes: ["submitted", "cancelled"], resumeSchema: { type: "object" }, resumePayloadPreview: { selected_issue_ids: ["risk-1"] }, runId: "run_recorded_lda_report", }, }, ], }, ]; render( {}), submitSelectedIssues, cancelReview, }} />, ); await user.click(screen.getByRole("button", { name: /submit/i })); await user.click(screen.getByRole("button", { name: /cancel/i })); expect(submitSelectedIssues).toHaveBeenCalledTimes(1); expect(cancelReview).toHaveBeenCalledTimes(1); }); ``` - [ ] **Step 2: Run tests to verify they fail** Run: ```bash pnpm --filter @lda/console test -- src/presentation/OperatorChat.test.tsx ``` Expected: FAIL because `timelineAgent` prop does not exist. - [ ] **Step 3: Wire run action and timeline approval handlers** Modify `web/apps/console/src/presentation/OperatorChat.tsx`: ```tsx import type { TimelineAgentController } from "../demo/agent/timelineAgent.js"; ``` Update props: ```ts type OperatorChatProps = { readonly state: PresentationState; readonly messages?: ReadonlyArray | undefined; readonly timelineAgent?: TimelineAgentController | undefined; readonly onApprove?: (() => void) | undefined; readonly onDeny?: (() => void) | undefined; }; ``` In the component: ```tsx const visibleMessages = messages && messages.length > 0 ? messages : timelineAgent && timelineAgent.messages.length > 0 ? timelineAgent.messages : fallbackMessages(state); const submit = timelineAgent?.submitSelectedIssues ?? onApprove; const cancel = timelineAgent?.cancelReview ?? onDeny; ``` Render the action before messages: ```tsx {timelineAgent ? (
) : null} ``` Pass `submit` and `cancel` to `renderPart`. - [ ] **Step 4: Add minimal CSS** Append to `web/apps/console/src/presentation/presentation.css`: ```css .operator-chat__action { display: flex; padding: 0.65rem; border-bottom: 1px solid var(--stage-line); } .operator-chat__action button { width: 100%; border: 1px solid color-mix(in oklch, var(--accent-cyan) 56%, var(--stage-line)); border-radius: 0.55rem; padding: 0.6rem 0.75rem; background: color-mix(in oklch, var(--accent-cyan) 18%, var(--stage-surface)); color: var(--text-primary); font: 700 0.8rem/1 var(--font-mono); } .operator-chat__action button:disabled { cursor: not-allowed; opacity: 0.55; } ``` - [ ] **Step 5: Run chat tests** Run: ```bash pnpm --filter @lda/console test -- src/presentation/OperatorChat.test.tsx ``` Expected: PASS. - [ ] **Step 6: Commit** ```bash git add web/apps/console/src/presentation/OperatorChat.tsx web/apps/console/src/presentation/OperatorChat.test.tsx web/apps/console/src/presentation/presentation.css git commit -m "feat: let chat run demo timeline" ``` --- ### Task 4: Presentation Route Uses Live Timeline When Available **Files:** - Modify: `web/apps/console/src/presentation/PresentationRoute.tsx` - Modify: `web/apps/console/src/presentation/PresentationRoute.test.tsx` - Modify: `web/apps/console/src/presentation/PresentationStage.tsx` **Interfaces:** - Consumes: - `resolvePresentationTarget()` - `useTimelineAgent(demo, modeLabel)` - existing `useDemoTimeline(target, recordEvidence, recording)` - Produces: - `/present` no longer hardcodes `useDemoTimeline(null, ...)`. - Chat run button drives the same `demo` used by `DemoWorkflowScene`. - `PresentationRoute` no longer creates the old recording-only prepared replay agent; the timeline-backed chat controller owns the run affordance. - [ ] **Step 1: Write failing route tests** Modify `web/apps/console/src/presentation/PresentationRoute.test.tsx`: ```tsx it("renders a chat run action on the presentation route", async () => { window.location.hash = "#scene/agent-handoff/request"; const { PresentationRoute } = await import("./PresentationRoute.js"); render(); expect(await screen.findByRole("button", { name: /run prepared workflow|run replay walkthrough/i })).toBeInTheDocument(); }); it("uses stored target for live presentation mode", async () => { window.sessionStorage.setItem("lda.workflowConsole.target", "http://127.0.0.1:8765/rpc"); const { PresentationRoute } = await import("./PresentationRoute.js"); render(); expect(await screen.findByRole("button", { name: /run prepared workflow/i })).toBeInTheDocument(); }); ``` If tests share session state, add `window.sessionStorage.clear()` in `afterEach`. - [ ] **Step 2: Run tests to verify they fail** Run: ```bash pnpm --filter @lda/console test -- src/presentation/PresentationRoute.test.tsx ``` Expected: FAIL because route does not create or pass `timelineAgent`. - [ ] **Step 3: Wire live target and timeline agent** Modify imports in `web/apps/console/src/presentation/PresentationRoute.tsx`: ```ts import { useTimelineAgent } from "../demo/agent/timelineAgent.js"; import { resolvePresentationTarget } from "./live-target.js"; ``` Replace: ```ts const demo = useDemoTimeline(null, recordEvidence, recording); ``` With: ```ts const presentationTarget = useMemo(() => resolvePresentationTarget(), []); const demo = useDemoTimeline(presentationTarget.target, recordEvidence, recording); const timelineAgent = useTimelineAgent( demo, presentationTarget.mode === "live" ? "live" : "replay", ); ``` Replace the auto-forced replay effects with target-aware behavior: ```ts useEffect(() => { if (demo.state.phase === "ready" && presentationTarget.mode === "replay" && demo.state.mode !== "replay") { demo.setMode("replay"); } if (demo.state.phase === "ready" && presentationTarget.mode === "live" && demo.state.mode !== "live") { demo.setMode("live"); } }, [demo.state.phase, demo.state.mode, demo.setMode, presentationTarget.mode]); ``` Keep the existing replay auto-start only for baseline visual content, but do not let it fight live mode: ```ts useEffect(() => { if (presentationTarget.mode === "replay" && demo.state.phase === "ready" && demo.state.mode === "replay") { demo.start(); } }, [demo.state.phase, demo.state.mode, demo.start, presentationTarget.mode]); ``` Remove these old prepared-agent lines from `PresentationRoute.tsx`: ```ts import { createPreparedRecipeDriver, assertNever } from "../demo/agent/preparedRecipeDriver.js"; import { useDemoAgent } from "../demo/agent/useDemoAgent.js"; ``` Remove the `agentDriver`, `agent`, `handleApprove`, `handleDeny`, and `agent.pendingActions` effect. The prepared recording-only agent is still available for tests and future comparison, but `/present` should use the timeline-backed chat controller. Pass `timelineAgent` to `PresentationStage`: ```tsx timelineAgent={timelineAgent} ``` Remove `messages={agent.messages}`, `onApprove={...}`, and `onDeny={...}` from the `PresentationStage` call in `PresentationRoute.tsx`. - [ ] **Step 4: Thread the prop through `PresentationStage`** Modify `web/apps/console/src/presentation/PresentationStage.tsx`: ```ts import type { TimelineAgentController } from "../demo/agent/timelineAgent.js"; ``` Add prop: ```ts readonly timelineAgent?: TimelineAgentController | undefined; ``` Pass to chat: ```tsx ``` - [ ] **Step 5: Run route tests** Run: ```bash pnpm --filter @lda/console test -- src/presentation/PresentationRoute.test.tsx src/presentation/OperatorChat.test.tsx ``` Expected: PASS. - [ ] **Step 6: Commit** ```bash git add web/apps/console/src/presentation/PresentationRoute.tsx web/apps/console/src/presentation/PresentationRoute.test.tsx web/apps/console/src/presentation/PresentationStage.tsx git commit -m "feat: connect presentation chat to timeline" ``` --- ### Task 5: End-To-End Behavior Polish And Verification **Files:** - Modify: `web/apps/console/src/presentation/PresentationRoute.test.tsx` - Modify: `docs/current_roadmap.md` - Move after completion: `docs/superpowers/plans/2026-07-09-presentation-chat-timeline-bridge.md` -> `docs/historical/superpowers/plans/2026-07-09-presentation-chat-timeline-bridge.md` **Interfaces:** - Consumes all previous task outputs. - Produces completed roadmap entry and archived plan. - [ ] **Step 1: Add one integrated replay smoke test** Modify `web/apps/console/src/presentation/PresentationRoute.test.tsx`: ```tsx it("chat run action advances the replay timeline when no live server is configured", async () => { window.sessionStorage.setItem("lda.workflowConsole.target", "file:///invalid"); window.location.hash = "#scene/workflow-demo/operation"; const user = userEvent.setup(); const { PresentationRoute } = await import("./PresentationRoute.js"); render(); await user.click(await screen.findByRole("button", { name: /run replay walkthrough/i })); // Replay mode is timer-driven after the chat starts the timeline. await userEvent.keyboard("{ArrowRight}"); expect(await screen.findByLabelText("workflow.runs.start operation")).toBeInTheDocument(); }); ``` - [ ] **Step 2: Run presentation and demo tests** Run: ```bash pnpm --filter @lda/console test -- src/demo src/presentation ``` Expected: PASS. - [ ] **Step 3: Run broad web checks** Run: ```bash pnpm --dir web test pnpm --dir web typecheck pnpm --dir web build ``` Expected: - Tests pass. - Typecheck clean. - Build passes with only the known Vite chunk-size warning if it appears. - [ ] **Step 4: Browser smoke** With `pnpm dev` running, check: ```text http://127.0.0.1:5173/present#scene/agent-handoff/request http://127.0.0.1:5173/present#scene/interrupt-evidence/approval ``` Expected: - Chat shows a run action. - Replay fallback button says "Run replay walkthrough" when target resolution fails. - Live button says "Run prepared workflow" when a valid target is in `sessionStorage`. - Approval submit/cancel routes through the timeline agent. - Scene evidence and graph still update from the same `demo` controller. - [ ] **Step 5: Update roadmap** In `docs/current_roadmap.md`: - Add a completed item after the schema approval surface entry. - Keep AI Elements/chat-library adoption as future work. Suggested wording: ```md 16. Completed: presentation chat now drives the prepared workflow timeline. The chat run action, schema approval submit/cancel, graph, evidence, and live/replay execution all share `useDemoTimeline`; AI SDK remains a later driver for the same seam. Implementation: [`presentation chat timeline bridge`](historical/superpowers/plans/2026-07-09-presentation-chat-timeline-bridge.md). ``` - [ ] **Step 6: Archive the plan** Run: ```bash git mv docs/superpowers/plans/2026-07-09-presentation-chat-timeline-bridge.md docs/historical/superpowers/plans/2026-07-09-presentation-chat-timeline-bridge.md ``` - [ ] **Step 7: Commit** ```bash git add docs/current_roadmap.md docs/historical/superpowers/plans/2026-07-09-presentation-chat-timeline-bridge.md web/apps/console/src/presentation/PresentationRoute.test.tsx git commit -m "docs: complete presentation chat timeline bridge" ``` --- ## Self-Review Checklist - [ ] `/present` no longer hardcodes `useDemoTimeline(null, ...)`. - [ ] Chat run action calls timeline methods, not direct RPC and not the old recording-only driver. - [ ] Live mode uses the existing `callOperation` path through `useDemoTimeline`. - [ ] Replay fallback remains available and test-covered. - [ ] Approval submit/cancel uses timeline review methods. - [ ] Existing schema approval surface remains reusable and does not become presentation-only. - [ ] No new dependency or AI SDK package was added. - [ ] Roadmap keeps AI Elements/source-owned chat primitive work as a later slice.