Files
lda-wf/docs/historical/superpowers/plans/2026-07-09-presentation-live-replay-truth.md
T

25 KiB

Presentation Live/Replay Truth 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 mode explicitly distinguish replay evidence, configured live target, reachable live target, active live run, and replay fallback.

Architecture: Add a small presentation target-status model/hook, render it in the presentation footer, and route chat run labels through that status. Keep direct scene hashes replay-backed until the operator intentionally starts live execution.

Tech Stack: React 19, TypeScript, Vitest, Testing Library, existing console RPC operation layer, existing presentation footer/chat components.

Global Constraints

  • Do not add a new JSON-RPC transport if the existing console operation layer can call workflow.health.
  • Do not change /console connection behavior.
  • Direct scene hashes must remain replay-backed and ready.
  • Do not say "live run active" before the operator starts the timeline in live mode.
  • Health check failure must fall back to replay wording, not crash the presentation.
  • Keep UI compact; this is a truth badge, not a new hero panel.

File Structure

  • Create web/apps/console/src/presentation/presentation-target-status.ts
    • Pure status model and copy helpers.
  • Create web/apps/console/src/presentation/presentation-target-status.test.ts
    • Tests status derivation.
  • Create web/apps/console/src/presentation/usePresentationTargetStatus.ts
    • Hook that probes target health and combines it with demo timeline state.
  • Create web/apps/console/src/presentation/usePresentationTargetStatus.test.tsx
    • Tests health success/failure and active live state.
  • Create web/apps/console/src/presentation/PresentationTruthBadge.tsx
    • Compact footer badge.
  • Create web/apps/console/src/presentation/PresentationTruthBadge.test.tsx
    • Tests labels and data-state attributes.
  • Modify web/apps/console/src/presentation/PresentationRoute.tsx
    • Uses the hook and passes status to stage/chat/footer.
  • Modify web/apps/console/src/presentation/PresentationStage.tsx
    • Threads status to footer and chat.
  • Modify web/apps/console/src/presentation/PresentationFooter.tsx
    • Renders PresentationTruthBadge.
  • Modify web/apps/console/src/presentation/OperatorChat.tsx
    • Uses status-aware run label/copy.
  • Modify web/apps/console/src/demo/agent/timelineAgent.ts
    • Accepts status-derived mode/copy or receives enabled label from route.
  • Modify web/apps/console/src/presentation/presentation.css
    • Adds compact badge styles.

Task 1: Add pure presentation target status model

Files:

  • Create: web/apps/console/src/presentation/presentation-target-status.ts
  • Create: web/apps/console/src/presentation/presentation-target-status.test.ts

Interfaces:

  • Produces:

    export type PresentationTargetHealth =
      | { readonly kind: "replay"; readonly label: string; readonly detail: string }
      | { readonly kind: "checking"; readonly target: string; readonly label: string; readonly detail: string }
      | { readonly kind: "ready"; readonly target: string; readonly label: string; readonly detail: string }
      | { readonly kind: "active"; readonly target: string; readonly label: string; readonly detail: string }
      | { readonly kind: "failed"; readonly target: string | null; readonly label: string; readonly detail: string };
    
    export type TargetProbeState = "none" | "checking" | "ready" | "failed";
    
    export const presentationTargetHealth = (input: {
      readonly target: string | null;
      readonly probe: TargetProbeState;
      readonly liveActive: boolean;
      readonly failureReason?: string | undefined;
    }): PresentationTargetHealth;
    
  • Step 1: Write failing tests

    Create web/apps/console/src/presentation/presentation-target-status.test.ts:

    import { describe, expect, it } from "vitest";
    import { presentationTargetHealth } from "./presentation-target-status.js";
    
    describe("presentationTargetHealth", () => {
      it("shows replay evidence when no target exists", () => {
        expect(presentationTargetHealth({
          target: null,
          probe: "none",
          liveActive: false,
        })).toMatchObject({
          kind: "replay",
          label: "Replay evidence",
        });
      });
    
      it("separates ready target from active live run", () => {
        expect(presentationTargetHealth({
          target: "http://127.0.0.1:8765/rpc",
          probe: "ready",
          liveActive: false,
        })).toMatchObject({
          kind: "ready",
          label: "Live target ready",
        });
      });
    
      it("marks live active only after live timeline starts", () => {
        expect(presentationTargetHealth({
          target: "http://127.0.0.1:8765/rpc",
          probe: "ready",
          liveActive: true,
        })).toMatchObject({
          kind: "active",
          label: "Live run active",
        });
      });
    
      it("shows replay fallback on failed health", () => {
        expect(presentationTargetHealth({
          target: "http://127.0.0.1:8765/rpc",
          probe: "failed",
          liveActive: false,
          failureReason: "connection refused",
        })).toMatchObject({
          kind: "failed",
          label: "Replay fallback",
        });
      });
    });
    
  • Step 2: Run failing tests

    pnpm --dir web --filter @lda/console test -- src/presentation/presentation-target-status.test.ts
    

    Expected: FAIL because the model file does not exist.

  • Step 3: Implement model

    Create web/apps/console/src/presentation/presentation-target-status.ts:

    export type PresentationTargetHealth =
      | { readonly kind: "replay"; readonly label: string; readonly detail: string }
      | { readonly kind: "checking"; readonly target: string; readonly label: string; readonly detail: string }
      | { readonly kind: "ready"; readonly target: string; readonly label: string; readonly detail: string }
      | { readonly kind: "active"; readonly target: string; readonly label: string; readonly detail: string }
      | { readonly kind: "failed"; readonly target: string | null; readonly label: string; readonly detail: string };
    
    export type TargetProbeState = "none" | "checking" | "ready" | "failed";
    
    const shortTarget = (target: string): string => {
      const url = new URL(target);
      return `${url.hostname}:${url.port || (url.protocol === "https:" ? "443" : "80")}`;
    };
    
    export const presentationTargetHealth = ({
      target,
      probe,
      liveActive,
      failureReason,
    }: {
      readonly target: string | null;
      readonly probe: TargetProbeState;
      readonly liveActive: boolean;
      readonly failureReason?: string | undefined;
    }): PresentationTargetHealth => {
      if (!target) {
        return {
          kind: "replay",
          label: "Replay evidence",
          detail: "reviewed recording",
        };
      }
    
      if (liveActive && probe === "ready") {
        return {
          kind: "active",
          target,
          label: "Live run active",
          detail: `operations sent to ${shortTarget(target)}`,
        };
      }
    
      if (probe === "ready") {
        return {
          kind: "ready",
          target,
          label: "Live target ready",
          detail: shortTarget(target),
        };
      }
    
      if (probe === "checking") {
        return {
          kind: "checking",
          target,
          label: "Live target configured",
          detail: "checking",
        };
      }
    
      return {
        kind: "failed",
        target,
        label: "Replay fallback",
        detail: failureReason ?? "live target unreachable",
      };
    };
    
  • Step 4: Run tests

    pnpm --dir web --filter @lda/console test -- src/presentation/presentation-target-status.test.ts
    

    Expected: PASS.

  • Step 5: Commit

    git add web/apps/console/src/presentation/presentation-target-status.ts web/apps/console/src/presentation/presentation-target-status.test.ts
    git commit -m "feat: model presentation live replay status"
    

Task 2: Add target health hook

Files:

  • Create: web/apps/console/src/presentation/usePresentationTargetStatus.ts
  • Create: web/apps/console/src/presentation/usePresentationTargetStatus.test.tsx

Interfaces:

  • Consumes:

    • PresentationTargetState from live-target.ts
    • DemoTimelineController["state"]
    • Existing operation call path if available.
  • Produces:

    export const usePresentationTargetStatus = (
      targetState: PresentationTargetState,
      demoState: DemoTimelineState,
    ): PresentationTargetHealth;
    
  • Step 1: Inspect existing RPC operation helper

    Run:

    rg -n "workflow.health|callOperation|OperationName" web/apps/console/src web/packages/rpc/src
    

    Use the existing helper that the console already uses for lifecycle/demo calls. Do not create a separate fetch transport if callOperation can call workflow.health.

  • Step 2: Write failing hook tests

    Create web/apps/console/src/presentation/usePresentationTargetStatus.test.tsx. Mock the existing call helper. If it is callOperation, use:

    import { renderHook, waitFor } from "@testing-library/react";
    import { beforeEach, describe, expect, it, vi } from "vitest";
    import { callOperation } from "../connection/api.js";
    import { initialDemoTimelineState } from "../demo/timeline/reducer.js";
    import { usePresentationTargetStatus } from "./usePresentationTargetStatus.js";
    
    vi.mock("../connection/api.js", () => ({ callOperation: vi.fn() }));
    const mockedCallOperation = vi.mocked(callOperation);
    
    beforeEach(() => mockedCallOperation.mockReset());
    
    describe("usePresentationTargetStatus", () => {
      it("marks live target ready after workflow health succeeds", async () => {
        mockedCallOperation.mockResolvedValueOnce({
          ok: true,
          operation: "workflow.health",
          label: "Health",
          interpreted: { status: "ok", storeRoot: "store" },
          exchange: { request: {}, response: {} },
          equivalentCli: "uv run wf status",
          durationMs: 2,
        });
    
        const { result } = renderHook(() =>
          usePresentationTargetStatus(
            { mode: "live", target: "http://127.0.0.1:8765/rpc", source: "default" },
            initialDemoTimelineState,
          ),
        );
    
        await waitFor(() => expect(result.current.kind).toBe("ready"));
      });
    
      it("falls back to replay when health fails", async () => {
        mockedCallOperation.mockRejectedValueOnce(new Error("connection refused"));
    
        const { result } = renderHook(() =>
          usePresentationTargetStatus(
            { mode: "live", target: "http://127.0.0.1:8765/rpc", source: "default" },
            initialDemoTimelineState,
          ),
        );
    
        await waitFor(() => expect(result.current.kind).toBe("failed"));
        expect(result.current.label).toBe("Replay fallback");
      });
    });
    

    Adjust operation response fields to match existing callOperation types.

  • Step 3: Run failing tests

    pnpm --dir web --filter @lda/console test -- src/presentation/usePresentationTargetStatus.test.tsx
    

    Expected: FAIL because the hook does not exist.

  • Step 4: Implement hook

    Create web/apps/console/src/presentation/usePresentationTargetStatus.ts:

    import { useEffect, useState } from "react";
    import { callOperation } from "../connection/api.js";
    import type { DemoTimelineState } from "../demo/timeline/reducer.js";
    import type { PresentationTargetState } from "./live-target.js";
    import {
      presentationTargetHealth,
      type PresentationTargetHealth,
      type TargetProbeState,
    } from "./presentation-target-status.js";
    
    const liveActive = (state: DemoTimelineState): boolean =>
      state.mode === "live" && state.phase !== "ready";
    
    export const usePresentationTargetStatus = (
      targetState: PresentationTargetState,
      demoState: DemoTimelineState,
    ): PresentationTargetHealth => {
      const [probe, setProbe] = useState<TargetProbeState>(
        targetState.mode === "live" ? "checking" : "none",
      );
      const [failureReason, setFailureReason] = useState<string | undefined>(undefined);
    
      useEffect(() => {
        let cancelled = false;
        if (targetState.mode !== "live") {
          setProbe("none");
          setFailureReason(targetState.reason);
          return;
        }
    
        setProbe("checking");
        setFailureReason(undefined);
        void callOperation(targetState.target, "workflow.health", {}).then(
          () => {
            if (!cancelled) setProbe("ready");
          },
          (error: unknown) => {
            if (!cancelled) {
              setProbe("failed");
              setFailureReason(error instanceof Error ? error.message : String(error));
            }
          },
        );
        return () => {
          cancelled = true;
        };
      }, [targetState]);
    
      return presentationTargetHealth({
        target: targetState.mode === "live" ? targetState.target : null,
        probe,
        liveActive: liveActive(demoState),
        failureReason,
      });
    };
    

    If callOperation takes a different parameter shape, adapt to the existing function signature and update the test mock accordingly.

  • Step 5: Run hook tests

    pnpm --dir web --filter @lda/console test -- src/presentation/usePresentationTargetStatus.test.tsx
    

    Expected: PASS.

  • Step 6: Commit

    git add web/apps/console/src/presentation/usePresentationTargetStatus.ts web/apps/console/src/presentation/usePresentationTargetStatus.test.tsx
    git commit -m "feat: probe presentation live target"
    

Files:

  • Create: web/apps/console/src/presentation/PresentationTruthBadge.tsx
  • Create: web/apps/console/src/presentation/PresentationTruthBadge.test.tsx
  • Modify: web/apps/console/src/presentation/PresentationFooter.tsx
  • Modify: web/apps/console/src/presentation/PresentationStage.tsx
  • Modify: web/apps/console/src/presentation/PresentationRoute.tsx
  • Modify: web/apps/console/src/presentation/presentation.css

Interfaces:

  • Consumes:

    • PresentationTargetHealth
  • Produces:

    • Visible badge in presentation footer.
  • Step 1: Write failing badge tests

    Create web/apps/console/src/presentation/PresentationTruthBadge.test.tsx:

    import { render, screen } from "@testing-library/react";
    import { describe, expect, it } from "vitest";
    import { PresentationTruthBadge } from "./PresentationTruthBadge.js";
    
    describe("PresentationTruthBadge", () => {
      it("renders status label and detail", () => {
        render(
          <PresentationTruthBadge
            status={{
              kind: "ready",
              target: "http://127.0.0.1:8765/rpc",
              label: "Live target ready",
              detail: "127.0.0.1:8765",
            }}
          />,
        );
    
        expect(screen.getByLabelText("presentation evidence mode")).toHaveAttribute("data-status", "ready");
        expect(screen.getByText("Live target ready")).toBeInTheDocument();
        expect(screen.getByText("127.0.0.1:8765")).toBeInTheDocument();
      });
    });
    
  • Step 2: Run failing tests

    pnpm --dir web --filter @lda/console test -- src/presentation/PresentationTruthBadge.test.tsx
    

    Expected: FAIL because component does not exist.

  • Step 3: Implement badge

    Create web/apps/console/src/presentation/PresentationTruthBadge.tsx:

    import type { PresentationTargetHealth } from "./presentation-target-status.js";
    
    export const PresentationTruthBadge = ({
      status,
    }: {
      readonly status: PresentationTargetHealth;
    }) => (
      <aside
        className="presentation-truth-badge"
        data-status={status.kind}
        aria-label="presentation evidence mode"
      >
        <strong>{status.label}</strong>
        <span>{status.detail}</span>
      </aside>
    );
    
  • Step 4: Thread status through stage/footer

    In PresentationRoute.tsx, import and call hook:

    import { usePresentationTargetStatus } from "./usePresentationTargetStatus.js";
    
    const targetStatus = usePresentationTargetStatus(presentationTarget, demo.state);
    

    Pass to PresentationStage.

    In PresentationStage.tsx, add prop:

    readonly targetStatus: PresentationTargetHealth;
    

    Pass to PresentationFooter.

    In PresentationFooter.tsx, add prop:

    readonly targetStatus: PresentationTargetHealth;
    

    Render:

    <PresentationTruthBadge status={targetStatus} />
    

    Place it between SceneProgress and EvidenceReceipt.

  • Step 5: Add compact CSS

    In presentation.css, add:

    .presentation-truth-badge {
      display: inline-flex;
      align-items: baseline;
      gap: 0.45rem;
      min-width: 0;
      padding: 0.3rem 0.55rem;
      border: 1px solid var(--stage-line);
      border-radius: 999px;
      background: color-mix(in oklch, var(--stage-surface) 88%, transparent);
      color: var(--text-primary);
      font-family: var(--font-mono);
      font-size: 0.68rem;
    }
    
    .presentation-truth-badge span {
      color: var(--text-muted);
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
    }
    
    .presentation-truth-badge[data-status="failed"] {
      border-color: color-mix(in oklch, var(--accent-amber) 70%, var(--stage-line));
    }
    
    .presentation-truth-badge[data-status="active"],
    .presentation-truth-badge[data-status="ready"] {
      border-color: color-mix(in oklch, var(--accent-cyan) 65%, var(--stage-line));
    }
    

    Adapt token names to the existing file if necessary.

  • Step 6: Run tests

    pnpm --dir web --filter @lda/console test -- src/presentation/PresentationTruthBadge.test.tsx src/presentation/PresentationRoute.test.tsx
    

    Expected: PASS.

  • Step 7: Commit

    git add web/apps/console/src/presentation/PresentationTruthBadge.tsx web/apps/console/src/presentation/PresentationTruthBadge.test.tsx web/apps/console/src/presentation/PresentationFooter.tsx web/apps/console/src/presentation/PresentationStage.tsx web/apps/console/src/presentation/PresentationRoute.tsx web/apps/console/src/presentation/presentation.css
    git commit -m "feat: show presentation live replay status"
    

Task 4: Align chat copy and run labels with status

Files:

  • Modify: web/apps/console/src/demo/agent/timelineAgent.ts
  • Modify: web/apps/console/src/demo/agent/timelineAgent.test.tsx
  • Modify: web/apps/console/src/presentation/OperatorChat.tsx
  • Modify: web/apps/console/src/presentation/OperatorChat.test.tsx

Interfaces:

  • Consumes:

    • PresentationTargetHealth
  • Produces:

    • Chat run label and intro copy do not overclaim live availability.
  • Step 1: Add tests for status-driven labels

    In timelineAgent.test.tsx, add:

    it("uses replay label when live target failed", () => {
      const demo = demoController();
      const { result } = renderHook(() =>
        useTimelineAgent(demo, {
          mode: "replay",
          status: { kind: "failed", target: "http://127.0.0.1:8765/rpc", label: "Replay fallback", detail: "connection refused" },
        }),
      );
    
      expect(result.current.runLabel).toBe("Run replay walkthrough");
      expect(result.current.messages[0]?.parts).toEqual(
        expect.arrayContaining([
          expect.objectContaining({ text: expect.stringMatching(/Replay fallback/i) }),
        ]),
      );
    });
    

    This intentionally changes useTimelineAgent signature from (demo, modeLabel) to (demo, options).

  • Step 2: Run failing test

    pnpm --dir web --filter @lda/console test -- src/demo/agent/timelineAgent.test.tsx
    

    Expected: FAIL because signature still uses modeLabel.

  • Step 3: Update timeline agent options

    In timelineAgent.ts, change:

    export type TimelineAgentMode = "live" | "replay";
    

    to:

    import type { PresentationTargetHealth } from "../../presentation/presentation-target-status.js";
    
    export type TimelineAgentMode = "live" | "replay";
    
    export type TimelineAgentOptions = {
      readonly mode: TimelineAgentMode;
      readonly status: PresentationTargetHealth;
    };
    

    Change hook signature:

    export const useTimelineAgent = (
      demo: DemoTimelineController,
      options: TimelineAgentOptions,
    ): TimelineAgentController => {
      const modeLabel = options.status.kind === "ready" || options.status.kind === "active"
        ? options.mode
        : "replay";
    

    Intro message:

    const introText = options.status.kind === "ready"
      ? "Live target is ready. Direct slides still show replay evidence until I start the live run."
      : options.status.kind === "active"
        ? "Live run is active. Operations are being sent to the workflow server."
        : options.status.kind === "failed"
          ? "Replay fallback is active because the live target is unavailable."
          : "Replay evidence is active. I can walk through the reviewed recording.";
    

    Run label:

    const runLabel = modeLabel === "live" ? "Run prepared workflow" : "Run replay walkthrough";
    

    Keep demo.start(modeLabel) unchanged.

  • Step 4: Update route caller

    In PresentationRoute.tsx, pass:

    const timelineAgent = useTimelineAgent(demo, {
      mode: presentationTarget.mode === "live" ? "live" : "replay",
      status: targetStatus,
    });
    

    Ensure targetStatus is defined before useTimelineAgent.

  • Step 5: Run tests

    pnpm --dir web --filter @lda/console test -- src/demo/agent/timelineAgent.test.tsx src/presentation/OperatorChat.test.tsx src/presentation/PresentationRoute.test.tsx
    

    Expected: PASS.

  • Step 6: Commit

    git add web/apps/console/src/demo/agent/timelineAgent.ts web/apps/console/src/demo/agent/timelineAgent.test.tsx web/apps/console/src/presentation/PresentationRoute.tsx web/apps/console/src/presentation/OperatorChat.tsx web/apps/console/src/presentation/OperatorChat.test.tsx
    git commit -m "fix: make chat honest about live replay status"
    

Task 5: Docs, verification, and archive

Files:

  • Modify: docs/current_roadmap.md

  • Move: docs/superpowers/plans/2026-07-09-presentation-live-replay-truth.md to docs/historical/superpowers/plans/2026-07-09-presentation-live-replay-truth.md

  • Step 1: Run focused tests

    pnpm --dir web --filter @lda/console test -- src/presentation/presentation-target-status.test.ts src/presentation/usePresentationTargetStatus.test.tsx src/presentation/PresentationTruthBadge.test.tsx src/demo/agent/timelineAgent.test.tsx src/presentation/PresentationRoute.test.tsx
    

    Expected: PASS.

  • Step 2: Run typecheck

    pnpm --dir web --filter @lda/console typecheck
    

    Expected: PASS.

  • Step 3: Run build

    pnpm --dir web --filter @lda/console build
    

    Expected: PASS. Existing chunk-size warning is acceptable.

  • Step 4: Browser smoke

    With the example server running:

    uv run wf-rpc-server --config examples/lda_report_workflow/wf.config.json --host 127.0.0.1 --port 8765
    

    Open:

    http://127.0.0.1:5173/present#scene/interrupt-evidence/approval
    

    Expected:

    • Badge says Live target ready.
    • Slide still shows replay approval state.
    • Chat says direct slides are replay until live run starts.

    Then set session storage target to an invalid URL or use existing replay helper in tests. Expected:

    • Badge says Replay fallback.
    • Chat run label says Run replay walkthrough.
  • Step 5: Update roadmap

    In docs/current_roadmap.md, add completed item:

    20. Completed: presentation live/replay truth surface distinguishes reviewed
        replay evidence, live target readiness, live active run state, and replay
        fallback. Design:
        [`presentation live/replay truth`](superpowers/specs/2026-07-09-presentation-live-replay-truth-design.md).
        Implementation:
        [`presentation live/replay truth plan`](historical/superpowers/plans/2026-07-09-presentation-live-replay-truth.md).
    

    Renumber following future items.

  • Step 6: Archive plan

    git mv docs/superpowers/plans/2026-07-09-presentation-live-replay-truth.md docs/historical/superpowers/plans/2026-07-09-presentation-live-replay-truth.md
    
  • Step 7: Diff hygiene

    git diff --check
    git status --short
    

    Expected: no whitespace errors; only intended files listed.

  • Step 8: Commit

    git add docs/current_roadmap.md docs/historical/superpowers/plans/2026-07-09-presentation-live-replay-truth.md
    git commit -m "docs: complete presentation live replay truth"
    

Self-Review

  • Spec coverage: health check, badge, chat copy, replay fallback, live active state, tests, and smoke are covered.
  • Placeholder scan: No TODO/TBD placeholders remain.
  • Type consistency: PresentationTargetHealth, TargetProbeState, presentationTargetHealth, and usePresentationTargetStatus signatures are consistent.
  • Scope check: This avoids AI Elements/chat replacement and stays focused on truthfulness.