feat: support explicit live demo launch

This commit is contained in:
lda
2026-07-12 10:49:56 +07:00 Verified
parent ad8c011351
commit b1fed7ebf8
4 changed files with 47 additions and 5 deletions
@@ -109,6 +109,37 @@ describe("useTimelineAgent", () => {
expect(start).toHaveBeenCalledWith("replay"); expect(start).toHaveBeenCalledWith("replay");
}); });
it("starts live explicitly from a direct replay view when the target is ready", async () => {
const start = vi.fn();
const demo = demoController({
state: { ...initialDemoTimelineState, mode: "replay" },
start,
});
const { result } = renderHook(() => useTimelineAgent(demo, {
mode: "live",
status: replayStatus,
liveTargetReady: true,
}));
expect(result.current.canRunLive).toBe(true);
await act(async () => result.current.runPreparedWorkflow("live"));
expect(start).toHaveBeenCalledWith("live");
});
it("does not offer an explicit live launch when health has failed", () => {
const start = vi.fn();
const demo = demoController({ start });
const { result } = renderHook(() => useTimelineAgent(demo, {
mode: "live",
status: failedStatus,
liveTargetReady: false,
}));
expect(result.current.canRunLive).toBe(false);
});
it("submits selected issues from the current interrupt payload", async () => { it("submits selected issues from the current interrupt payload", async () => {
const submitSelectedIssues = vi.fn(async () => {}); const submitSelectedIssues = vi.fn(async () => {});
const demo = demoController({ const demo = demoController({
@@ -14,13 +14,15 @@ export type TimelineAgentMode = "live" | "replay";
export type TimelineAgentOptions = { export type TimelineAgentOptions = {
readonly mode: TimelineAgentMode; readonly mode: TimelineAgentMode;
readonly status: PresentationTargetHealth; readonly status: PresentationTargetHealth;
readonly liveTargetReady?: boolean | undefined;
}; };
export type TimelineAgentController = { export type TimelineAgentController = {
readonly messages: ReadonlyArray<AgentMessage>; readonly messages: ReadonlyArray<AgentMessage>;
readonly canRun: boolean; readonly canRun: boolean;
readonly canRunLive: boolean;
readonly runLabel: string; readonly runLabel: string;
readonly runPreparedWorkflow: () => Promise<void>; readonly runPreparedWorkflow: (mode?: TimelineAgentMode) => Promise<void>;
readonly submitSelectedIssues: () => Promise<void>; readonly submitSelectedIssues: () => Promise<void>;
readonly requestRevision: () => Promise<void>; readonly requestRevision: () => Promise<void>;
}; };
@@ -89,18 +91,23 @@ export const useTimelineAgent = (
const runLabel = modeLabel === "live" ? "Run prepared workflow" : "Run replay walkthrough"; const runLabel = modeLabel === "live" ? "Run prepared workflow" : "Run replay walkthrough";
const canRun = demo.canStart && !demo.inFlight && demo.state.phase !== "running"; const canRun = demo.canStart && !demo.inFlight && demo.state.phase !== "running";
const canRunLive = (options.liveTargetReady ?? (options.mode === "live" && (
options.status.kind === "ready" || options.status.kind === "active"
))) && !demo.inFlight && demo.state.phase !== "running";
const runPreparedWorkflow = useCallback(async () => { const runPreparedWorkflow = useCallback(async (requestedMode?: TimelineAgentMode) => {
const mode = requestedMode ?? modeLabel;
if (mode === "live" && !canRunLive) return;
if (!demo.canStart || demo.inFlight || demo.state.phase === "running") return; if (!demo.canStart || demo.inFlight || demo.state.phase === "running") return;
demo.start(modeLabel); demo.start(mode);
setMessages((current) => appendToolMessage( setMessages((current) => appendToolMessage(
current, current,
"timeline-agent-start", "timeline-agent-start",
"startRun", "startRun",
{ mode: modeLabel }, { mode },
{ phase: "started" }, { phase: "started" },
)); ));
}, [demo, modeLabel]); }, [canRunLive, demo, modeLabel]);
const selectedIssueIds = useMemo( const selectedIssueIds = useMemo(
() => demo.interruptPayload?.proposed_issues.map((issue) => issue.id) ?? [], () => demo.interruptPayload?.proposed_issues.map((issue) => issue.id) ?? [],
@@ -135,6 +142,7 @@ export const useTimelineAgent = (
return { return {
messages, messages,
canRun, canRun,
canRunLive,
runLabel, runLabel,
runPreparedWorkflow, runPreparedWorkflow,
submitSelectedIssues, submitSelectedIssues,
@@ -191,6 +191,7 @@ describe("OperatorChat", () => {
timelineAgent={{ timelineAgent={{
messages: [], messages: [],
canRun: true, canRun: true,
canRunLive: true,
runLabel: "Run prepared workflow", runLabel: "Run prepared workflow",
runPreparedWorkflow, runPreparedWorkflow,
submitSelectedIssues: vi.fn(async () => {}), submitSelectedIssues: vi.fn(async () => {}),
@@ -236,6 +237,7 @@ describe("OperatorChat", () => {
timelineAgent={{ timelineAgent={{
messages: [], messages: [],
canRun: false, canRun: false,
canRunLive: false,
runLabel: "Run prepared workflow", runLabel: "Run prepared workflow",
runPreparedWorkflow: vi.fn(async () => {}), runPreparedWorkflow: vi.fn(async () => {}),
submitSelectedIssues, submitSelectedIssues,
@@ -59,6 +59,7 @@ export const PresentationRoute = () => {
const timelineAgent = useTimelineAgent(demo, { const timelineAgent = useTimelineAgent(demo, {
mode: presentationTarget.mode === "live" ? "live" : "replay", mode: presentationTarget.mode === "live" ? "live" : "replay",
status: targetStatus, status: targetStatus,
liveTargetReady: targetStatusController.liveTargetReady,
}); });
const requiredDemoStage = state.location.kind === "main" const requiredDemoStage = state.location.kind === "main"
? requirementForDemoBeat(state.location.sceneId, state.location.beatId).requiredStage ? requirementForDemoBeat(state.location.sceneId, state.location.beatId).requiredStage