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)
|
- [`defense presentation storyboard`](superpowers/specs/2026-07-04-defense-presentation-storyboard-design.md)
|
||||||
- [`adaptive presentation canvas and evidence inspector`](superpowers/specs/2026-07-05-adaptive-presentation-canvas-design.md)
|
- [`adaptive presentation canvas and evidence inspector`](superpowers/specs/2026-07-05-adaptive-presentation-canvas-design.md)
|
||||||
- [`Scene 10 guided product moment`](superpowers/specs/2026-07-09-scene-10-guided-product-moment-design.md)
|
- [`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:
|
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).
|
[`Scene 10 guided product moment`](superpowers/specs/2026-07-09-scene-10-guided-product-moment-design.md).
|
||||||
Implementation:
|
Implementation:
|
||||||
[`Scene 10 guided product moment plan`](historical/superpowers/plans/2026-07-09-scene-10-guided-product-moment.md).
|
[`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.
|
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.
|
Astro remains an option, not the default next surface.
|
||||||
|
|
||||||
Presentation wishlist / defense readiness:
|
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).
|
[`Scene 10 guided product moment`](superpowers/specs/2026-07-09-scene-10-guided-product-moment-design.md).
|
||||||
Implementation:
|
Implementation:
|
||||||
[`Scene 10 guided product moment plan`](historical/superpowers/plans/2026-07-09-scene-10-guided-product-moment.md).
|
[`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,
|
- Evidence assets and rehearsal timing: prepare fallback screenshots/recordings,
|
||||||
expected run states, and a timed walkthrough checklist for a 15-minute defense.
|
expected run states, and a timed walkthrough checklist for a 15-minute defense.
|
||||||
- Presenter companion feasibility: decide whether phone/laptop control is local
|
- Presenter companion feasibility: decide whether phone/laptop control is local
|
||||||
|
|||||||
@@ -0,0 +1,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