docs: plan presentation live replay truth
This commit is contained in:
+14
-2
@@ -39,6 +39,7 @@ Design contracts:
|
||||
- [`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)
|
||||
- [`Scene 10 guided product moment`](superpowers/specs/2026-07-09-scene-10-guided-product-moment-design.md)
|
||||
- [`presentation live/replay truth`](superpowers/specs/2026-07-09-presentation-live-replay-truth-design.md)
|
||||
|
||||
Implementation order:
|
||||
|
||||
@@ -136,9 +137,14 @@ Implementation order:
|
||||
[`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).
|
||||
19. Future: presenter companion, Scene 10 product graph, final scene visuals,
|
||||
19. Active: distinguish reviewed replay evidence, live target readiness, live
|
||||
active run state, and replay fallback in presentation mode. Design:
|
||||
[`presentation live/replay truth`](superpowers/specs/2026-07-09-presentation-live-replay-truth-design.md).
|
||||
Plan:
|
||||
[`presentation live/replay truth plan`](superpowers/plans/2026-07-09-presentation-live-replay-truth.md).
|
||||
20. Future: presenter companion, Scene 10 product graph, final scene visuals,
|
||||
evidence assets, and rehearsal timing.
|
||||
20. Add a static slide/appendix shell only after presentation mode is clear.
|
||||
21. Add a static slide/appendix shell only after presentation mode is clear.
|
||||
Astro remains an option, not the default next surface.
|
||||
|
||||
Presentation wishlist / defense readiness:
|
||||
@@ -218,6 +224,12 @@ Presentation wishlist / defense readiness:
|
||||
[`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).
|
||||
- Active: presentation live/replay truth distinguishes reviewed replay evidence,
|
||||
live target readiness, live active run state, and replay fallback so the demo
|
||||
can look credible without implying fake live execution. Design:
|
||||
[`presentation live/replay truth`](superpowers/specs/2026-07-09-presentation-live-replay-truth-design.md).
|
||||
Plan:
|
||||
[`presentation live/replay truth plan`](superpowers/plans/2026-07-09-presentation-live-replay-truth.md).
|
||||
- Evidence assets and rehearsal timing: prepare fallback screenshots/recordings,
|
||||
expected run states, and a timed walkthrough checklist for a 15-minute defense.
|
||||
- Presenter companion feasibility: decide whether phone/laptop control is local
|
||||
|
||||
@@ -0,0 +1,821 @@
|
||||
# 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:
|
||||
|
||||
```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";
|
||||
|
||||
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`:
|
||||
|
||||
```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**
|
||||
|
||||
```bash
|
||||
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`:
|
||||
|
||||
```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**
|
||||
|
||||
```bash
|
||||
pnpm --dir web --filter @lda/console test -- src/presentation/presentation-target-status.test.ts
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 5: Commit**
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```ts
|
||||
export const usePresentationTargetStatus = (
|
||||
targetState: PresentationTargetState,
|
||||
demoState: DemoTimelineState,
|
||||
): PresentationTargetHealth;
|
||||
```
|
||||
|
||||
- [ ] **Step 1: Inspect existing RPC operation helper**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```tsx
|
||||
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**
|
||||
|
||||
```bash
|
||||
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`:
|
||||
|
||||
```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**
|
||||
|
||||
```bash
|
||||
pnpm --dir web --filter @lda/console test -- src/presentation/usePresentationTargetStatus.test.tsx
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
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"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Task 3: Render the truth badge in the footer
|
||||
|
||||
**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`:
|
||||
|
||||
```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**
|
||||
|
||||
```bash
|
||||
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`:
|
||||
|
||||
```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:
|
||||
|
||||
```ts
|
||||
import { usePresentationTargetStatus } from "./usePresentationTargetStatus.js";
|
||||
|
||||
const targetStatus = usePresentationTargetStatus(presentationTarget, demo.state);
|
||||
```
|
||||
|
||||
Pass to `PresentationStage`.
|
||||
|
||||
In `PresentationStage.tsx`, add prop:
|
||||
|
||||
```ts
|
||||
readonly targetStatus: PresentationTargetHealth;
|
||||
```
|
||||
|
||||
Pass to `PresentationFooter`.
|
||||
|
||||
In `PresentationFooter.tsx`, add prop:
|
||||
|
||||
```ts
|
||||
readonly targetStatus: PresentationTargetHealth;
|
||||
```
|
||||
|
||||
Render:
|
||||
|
||||
```tsx
|
||||
<PresentationTruthBadge status={targetStatus} />
|
||||
```
|
||||
|
||||
Place it between `SceneProgress` and `EvidenceReceipt`.
|
||||
|
||||
- [ ] **Step 5: Add compact CSS**
|
||||
|
||||
In `presentation.css`, add:
|
||||
|
||||
```css
|
||||
.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**
|
||||
|
||||
```bash
|
||||
pnpm --dir web --filter @lda/console test -- src/presentation/PresentationTruthBadge.test.tsx src/presentation/PresentationRoute.test.tsx
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```tsx
|
||||
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**
|
||||
|
||||
```bash
|
||||
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:
|
||||
|
||||
```ts
|
||||
export type TimelineAgentMode = "live" | "replay";
|
||||
```
|
||||
|
||||
to:
|
||||
|
||||
```ts
|
||||
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:
|
||||
|
||||
```ts
|
||||
export const useTimelineAgent = (
|
||||
demo: DemoTimelineController,
|
||||
options: TimelineAgentOptions,
|
||||
): TimelineAgentController => {
|
||||
const modeLabel = options.status.kind === "ready" || options.status.kind === "active"
|
||||
? options.mode
|
||||
: "replay";
|
||||
```
|
||||
|
||||
Intro message:
|
||||
|
||||
```ts
|
||||
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:
|
||||
|
||||
```ts
|
||||
const runLabel = modeLabel === "live" ? "Run prepared workflow" : "Run replay walkthrough";
|
||||
```
|
||||
|
||||
Keep `demo.start(modeLabel)` unchanged.
|
||||
|
||||
- [ ] **Step 4: Update route caller**
|
||||
|
||||
In `PresentationRoute.tsx`, pass:
|
||||
|
||||
```ts
|
||||
const timelineAgent = useTimelineAgent(demo, {
|
||||
mode: presentationTarget.mode === "live" ? "live" : "replay",
|
||||
status: targetStatus,
|
||||
});
|
||||
```
|
||||
|
||||
Ensure `targetStatus` is defined before `useTimelineAgent`.
|
||||
|
||||
- [ ] **Step 5: Run tests**
|
||||
|
||||
```bash
|
||||
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**
|
||||
|
||||
```bash
|
||||
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**
|
||||
|
||||
```bash
|
||||
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**
|
||||
|
||||
```bash
|
||||
pnpm --dir web --filter @lda/console typecheck
|
||||
```
|
||||
|
||||
Expected: PASS.
|
||||
|
||||
- [ ] **Step 3: Run build**
|
||||
|
||||
```bash
|
||||
pnpm --dir web --filter @lda/console build
|
||||
```
|
||||
|
||||
Expected: PASS. Existing chunk-size warning is acceptable.
|
||||
|
||||
- [ ] **Step 4: Browser smoke**
|
||||
|
||||
With the example server running:
|
||||
|
||||
```bash
|
||||
uv run wf-rpc-server --config examples/lda_report_workflow/wf.config.json --host 127.0.0.1 --port 8765
|
||||
```
|
||||
|
||||
Open:
|
||||
|
||||
```text
|
||||
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:
|
||||
|
||||
```md
|
||||
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**
|
||||
|
||||
```bash
|
||||
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**
|
||||
|
||||
```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-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.
|
||||
@@ -0,0 +1,138 @@
|
||||
# Presentation Live/Replay Truth Design
|
||||
|
||||
## Purpose
|
||||
|
||||
The defense presentation can now show a prepared workflow as replay evidence and
|
||||
can also run against a local `wf-rpc-server`. The UI must make that distinction
|
||||
obvious. A viewer should never have to infer whether the screen is showing
|
||||
recorded evidence, a configured live target, or an actually reachable live
|
||||
server.
|
||||
|
||||
## Problem
|
||||
|
||||
The presentation currently resolves `http://127.0.0.1:8765/rpc` as a live target
|
||||
by default, even before a health check. At the same time, direct scene hashes are
|
||||
replay-backed so the slides are immediately ready. This is technically useful
|
||||
but semantically blurry:
|
||||
|
||||
- The chat says a live workflow target is configured.
|
||||
- The screen often shows replay evidence.
|
||||
- The run button can say "Run prepared workflow" even when the visible evidence
|
||||
is replayed.
|
||||
- When the example server is running, the fake/replay path can look real enough
|
||||
to be misleading.
|
||||
|
||||
## Design Goal
|
||||
|
||||
Add a small truth layer that distinguishes:
|
||||
|
||||
1. **Replay evidence**: the slide is showing the reviewed recording.
|
||||
2. **Live target configured**: a URL exists, but reachability has not been
|
||||
proven in this presentation session.
|
||||
3. **Live target reachable**: `workflow.health` succeeded.
|
||||
4. **Live run active**: the operator clicked the run action and the timeline is
|
||||
executing live operations.
|
||||
5. **Live unavailable**: health check failed or target is invalid; replay is the
|
||||
fallback.
|
||||
|
||||
This is not a large connection redesign. It is a presentation honesty layer.
|
||||
|
||||
## Scope
|
||||
|
||||
In scope:
|
||||
|
||||
- Health probing the presentation target.
|
||||
- A visible presentation status badge or strip.
|
||||
- Clear chat copy for replay vs configured vs reachable vs live active.
|
||||
- Route tests and component tests for status rendering.
|
||||
- Browser smoke for reachable local example server and replay fallback.
|
||||
|
||||
Out of scope:
|
||||
|
||||
- New connection UI.
|
||||
- Replacing chat with AI Elements.
|
||||
- Changing `/console` connection behavior.
|
||||
- Adding remote/VPS presenter companion behavior.
|
||||
- Full live/replay recording reconciliation.
|
||||
|
||||
## UX Rules
|
||||
|
||||
- Direct scene hashes remain replay-backed until the operator intentionally
|
||||
starts a live run.
|
||||
- A reachable live server should be shown as "Live target ready", not "Live run
|
||||
active".
|
||||
- "Live run active" only appears after `Run prepared workflow` starts a live
|
||||
timeline.
|
||||
- If health fails, the run button should either use replay wording or explain
|
||||
why live is unavailable.
|
||||
- Replay evidence should be labelled calmly, not apologetically.
|
||||
|
||||
Recommended copy:
|
||||
|
||||
- Replay: `Replay evidence · reviewed recording`
|
||||
- Configured: `Live target configured · checking`
|
||||
- Reachable: `Live target ready · 127.0.0.1:8765`
|
||||
- Active: `Live run active · operations sent to wf-rpc-server`
|
||||
- Failed: `Replay fallback · live target unreachable`
|
||||
|
||||
## Architecture
|
||||
|
||||
Add a small presentation target status hook:
|
||||
|
||||
```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 };
|
||||
```
|
||||
|
||||
The hook should not own timeline state. It consumes target resolution and demo
|
||||
state, then exposes display status and whether live run actions should be
|
||||
enabled.
|
||||
|
||||
Health check should use the existing RPC client/operation layer if one is
|
||||
already available in the console package. Do not hand-roll a second JSON-RPC
|
||||
transport unless the existing operation layer cannot call `workflow.health`.
|
||||
|
||||
## UI Placement
|
||||
|
||||
Add the status to the footer near scene progress/evidence receipt. It should be
|
||||
visible but quiet: a compact pill or strip. The audience should see it without
|
||||
it becoming a new hero element.
|
||||
|
||||
The chat intro should use the same status language. It should not claim "server
|
||||
is available" before health succeeds.
|
||||
|
||||
## Testing Requirements
|
||||
|
||||
Model/hook tests:
|
||||
|
||||
- Invalid target produces replay/failed fallback.
|
||||
- Default target starts as checking, then ready when health succeeds.
|
||||
- Health failure produces failed fallback.
|
||||
- Demo state `live` + running/review/completed after live start produces active.
|
||||
|
||||
Presentation tests:
|
||||
|
||||
- Direct approval hash shows replay evidence status and ready live target status
|
||||
separately if health succeeds.
|
||||
- Chat run label uses live wording only when health is ready.
|
||||
- If health fails, chat run label uses replay wording.
|
||||
|
||||
Smoke:
|
||||
|
||||
- With `wf-rpc-server --config examples/lda_report_workflow/wf.config.json
|
||||
--host 127.0.0.1 --port 8765`, `/present#scene/interrupt-evidence/approval`
|
||||
shows the live target as ready while the slide remains replay-backed.
|
||||
- With an invalid target in session storage, the same route shows replay
|
||||
fallback.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- No screen implies a live run is active before the operator starts it.
|
||||
- The audience can distinguish replay evidence from live server reachability.
|
||||
- Live target success makes the demo feel credible without hiding replay
|
||||
fallback.
|
||||
- Existing direct-hash readiness remains intact.
|
||||
Reference in New Issue
Block a user