feat: model presentation demo chrome states
This commit is contained in:
@@ -0,0 +1,119 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import {
|
||||||
|
DEMO_CHROME_SCENE_IDS,
|
||||||
|
demoChromeFor,
|
||||||
|
isDemoChromeScene,
|
||||||
|
type DemoChromeInput,
|
||||||
|
} from "./presentation-demo-chrome.js";
|
||||||
|
|
||||||
|
const replayStatus = {
|
||||||
|
kind: "replay",
|
||||||
|
label: "Replay evidence",
|
||||||
|
detail: "reviewed recording",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const checkingStatus = {
|
||||||
|
kind: "checking",
|
||||||
|
target: "http://127.0.0.1:8765/rpc",
|
||||||
|
label: "Live target configured",
|
||||||
|
detail: "checking",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const failedStatus = {
|
||||||
|
kind: "failed",
|
||||||
|
target: "http://127.0.0.1:8765/rpc",
|
||||||
|
label: "Replay fallback",
|
||||||
|
detail: "connection refused",
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
const baseInput: DemoChromeInput = {
|
||||||
|
sceneId: "agent-handoff",
|
||||||
|
phase: "ready",
|
||||||
|
mode: "replay",
|
||||||
|
inFlight: false,
|
||||||
|
approvalState: "ready",
|
||||||
|
targetStatus: replayStatus,
|
||||||
|
liveTargetReady: false,
|
||||||
|
canRun: true,
|
||||||
|
canRunLive: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("isDemoChromeScene", () => {
|
||||||
|
it("includes every demo scene and excludes the surrounding presentation scenes", () => {
|
||||||
|
expect(DEMO_CHROME_SCENE_IDS).toEqual([
|
||||||
|
"agent-handoff",
|
||||||
|
"prepared-lifecycle",
|
||||||
|
"run-from-deployment",
|
||||||
|
"typed-human-boundary",
|
||||||
|
"resume-output-evidence",
|
||||||
|
]);
|
||||||
|
|
||||||
|
for (const sceneId of DEMO_CHROME_SCENE_IDS) {
|
||||||
|
expect(isDemoChromeScene(sceneId)).toBe(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const sceneId of ["thesis", "architecture", "authoring", "evaluation", "conclusion"] as const) {
|
||||||
|
expect(isDemoChromeScene(sceneId)).toBe(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("demoChromeFor", () => {
|
||||||
|
it("hides chrome outside the demo scenes", () => {
|
||||||
|
expect(demoChromeFor({ ...baseInput, sceneId: "thesis" }).kind).toBe("hidden");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows an action for a ready demo scene", () => {
|
||||||
|
expect(demoChromeFor({ ...baseInput, sceneId: "agent-handoff" }).kind).toBe("action");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows checking before exposing an action", () => {
|
||||||
|
expect(demoChromeFor({ ...baseInput, targetStatus: checkingStatus }).kind).toBe("checking");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows a live run while the live timeline is active", () => {
|
||||||
|
expect(demoChromeFor({ ...baseInput, mode: "live", phase: "running", inFlight: true }).kind).toBe("running");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("pauses at the typed human boundary for a ready decision", () => {
|
||||||
|
expect(demoChromeFor({ ...baseInput, sceneId: "typed-human-boundary", phase: "review" }).kind).toBe("paused");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("resumes immediately after a decision is submitted", () => {
|
||||||
|
expect(demoChromeFor({
|
||||||
|
...baseInput,
|
||||||
|
sceneId: "typed-human-boundary",
|
||||||
|
phase: "review",
|
||||||
|
approvalState: "submitted",
|
||||||
|
}).kind).toBe("resuming");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("shows completion before any other run state", () => {
|
||||||
|
expect(demoChromeFor({ ...baseInput, phase: "completed" }).kind).toBe("completed");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("falls back to the replay action when the live target is unavailable", () => {
|
||||||
|
const fallback = demoChromeFor({ ...baseInput, liveTargetReady: false, targetStatus: failedStatus });
|
||||||
|
expect(fallback.kind).toBe("action");
|
||||||
|
if (fallback.kind === "action") expect(fallback.label).toBe("Play replay walkthrough");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("projects the selected timeline capability and retry state", () => {
|
||||||
|
const liveAction = demoChromeFor({
|
||||||
|
...baseInput,
|
||||||
|
mode: "live",
|
||||||
|
targetStatus: { ...failedStatus, kind: "ready" },
|
||||||
|
liveTargetReady: true,
|
||||||
|
canRunLive: true,
|
||||||
|
canRun: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(liveAction).toMatchObject({
|
||||||
|
kind: "action",
|
||||||
|
mode: "live",
|
||||||
|
label: "Run prepared workflow",
|
||||||
|
canRun: true,
|
||||||
|
canRetry: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
import type { DemoMode, DemoTimelineState } from "../demo/timeline/reducer.js";
|
||||||
|
import type { TimelineAgentMode } from "../demo/agent/timelineAgent.js";
|
||||||
|
import type { DemoApprovalUiState } from "./demo-approval-actions.js";
|
||||||
|
import type { PresentationTargetHealth } from "./presentation-target-status.js";
|
||||||
|
import type { MainSceneId } from "./storyboard.js";
|
||||||
|
|
||||||
|
export const DEMO_CHROME_SCENE_IDS = [
|
||||||
|
"agent-handoff",
|
||||||
|
"prepared-lifecycle",
|
||||||
|
"run-from-deployment",
|
||||||
|
"typed-human-boundary",
|
||||||
|
"resume-output-evidence",
|
||||||
|
] as const satisfies readonly MainSceneId[];
|
||||||
|
|
||||||
|
export type DemoChromePresentation =
|
||||||
|
| { readonly kind: "hidden" }
|
||||||
|
| {
|
||||||
|
readonly kind: "action";
|
||||||
|
readonly mode: TimelineAgentMode;
|
||||||
|
readonly label: "Run prepared workflow" | "Play replay walkthrough";
|
||||||
|
readonly status: PresentationTargetHealth;
|
||||||
|
readonly canRun: boolean;
|
||||||
|
readonly canRetry: boolean;
|
||||||
|
}
|
||||||
|
| { readonly kind: "checking"; readonly label: "Checking live service" }
|
||||||
|
| { readonly kind: "running"; readonly label: "Running workflow..." }
|
||||||
|
| { readonly kind: "paused"; readonly label: "Run paused - review required" }
|
||||||
|
| { readonly kind: "resuming"; readonly label: "Resuming workflow..." }
|
||||||
|
| { readonly kind: "completed"; readonly label: "Run complete" };
|
||||||
|
|
||||||
|
export type DemoChromeInput = {
|
||||||
|
readonly sceneId: MainSceneId;
|
||||||
|
readonly phase: DemoTimelineState["phase"];
|
||||||
|
readonly mode: DemoMode;
|
||||||
|
readonly inFlight: boolean;
|
||||||
|
readonly approvalState: DemoApprovalUiState;
|
||||||
|
readonly targetStatus: PresentationTargetHealth;
|
||||||
|
readonly liveTargetReady: boolean;
|
||||||
|
readonly canRun: boolean;
|
||||||
|
readonly canRunLive: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isDemoChromeScene = (sceneId: MainSceneId): boolean =>
|
||||||
|
DEMO_CHROME_SCENE_IDS.includes(sceneId as (typeof DEMO_CHROME_SCENE_IDS)[number]);
|
||||||
|
|
||||||
|
export const demoChromeFor = (input: DemoChromeInput): DemoChromePresentation => {
|
||||||
|
if (!isDemoChromeScene(input.sceneId)) return { kind: "hidden" };
|
||||||
|
if (input.phase === "completed") return { kind: "completed", label: "Run complete" };
|
||||||
|
|
||||||
|
// The decision state must win over generic live-running state so clicking it removes the paused label immediately.
|
||||||
|
if (
|
||||||
|
input.sceneId === "typed-human-boundary" &&
|
||||||
|
input.phase === "review" &&
|
||||||
|
input.approvalState === "ready"
|
||||||
|
) {
|
||||||
|
return { kind: "paused", label: "Run paused - review required" };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
(input.approvalState === "submitted" || input.approvalState === "revision_requested") &&
|
||||||
|
(input.phase === "running" || input.phase === "review" || input.inFlight)
|
||||||
|
) {
|
||||||
|
return { kind: "resuming", label: "Resuming workflow..." };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
input.mode === "live" &&
|
||||||
|
(input.inFlight || input.phase === "running" || input.phase === "paused" || input.phase === "review")
|
||||||
|
) {
|
||||||
|
return { kind: "running", label: "Running workflow..." };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.targetStatus.kind === "checking") {
|
||||||
|
return { kind: "checking", label: "Checking live service" };
|
||||||
|
}
|
||||||
|
|
||||||
|
const mode: TimelineAgentMode = input.liveTargetReady ? "live" : "replay";
|
||||||
|
return {
|
||||||
|
kind: "action",
|
||||||
|
mode,
|
||||||
|
label: mode === "live" ? "Run prepared workflow" : "Play replay walkthrough",
|
||||||
|
status: input.targetStatus,
|
||||||
|
canRun: mode === "live" ? input.canRunLive : input.canRun,
|
||||||
|
canRetry: input.targetStatus.kind !== "replay",
|
||||||
|
};
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user