fix: make chat honest about live replay status

This commit is contained in:
lda
2026-07-09 16:01:57 +07:00 Verified
parent e7fb4cc087
commit d09e12d3ad
4 changed files with 86 additions and 19 deletions
@@ -2,8 +2,29 @@ import { act, renderHook } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import type { DemoTimelineController } from "../useDemoTimeline.js";
import { initialDemoTimelineState } from "../timeline/reducer.js";
import type { PresentationTargetHealth } from "../../presentation/presentation-target-status.js";
import { useTimelineAgent } from "./timelineAgent.js";
const readyStatus: PresentationTargetHealth = {
kind: "ready",
target: "http://127.0.0.1:8765/rpc",
label: "Live target ready",
detail: "127.0.0.1:8765",
};
const replayStatus: PresentationTargetHealth = {
kind: "replay",
label: "Replay evidence",
detail: "reviewed recording",
};
const failedStatus: PresentationTargetHealth = {
kind: "failed",
target: "http://127.0.0.1:8765/rpc",
label: "Replay fallback",
detail: "connection refused",
};
const demoController = (
overrides: Partial<DemoTimelineController> = {},
): DemoTimelineController => ({
@@ -32,7 +53,7 @@ describe("useTimelineAgent", () => {
const start = vi.fn();
const demo = demoController({ start });
const { result } = renderHook(() => useTimelineAgent(demo, "live"));
const { result } = renderHook(() => useTimelineAgent(demo, { mode: "live", status: readyStatus }));
await act(async () => result.current.runPreparedWorkflow());
expect(start).toHaveBeenCalledWith("live");
@@ -47,7 +68,7 @@ describe("useTimelineAgent", () => {
const start = vi.fn();
const demo = demoController({ start });
const { result } = renderHook(() => useTimelineAgent(demo, "replay"));
const { result } = renderHook(() => useTimelineAgent(demo, { mode: "replay", status: replayStatus }));
await act(async () => result.current.runPreparedWorkflow());
expect(start).toHaveBeenCalledWith("replay");
@@ -66,7 +87,7 @@ describe("useTimelineAgent", () => {
submitSelectedIssues,
});
const { result } = renderHook(() => useTimelineAgent(demo, "replay"));
const { result } = renderHook(() => useTimelineAgent(demo, { mode: "replay", status: replayStatus }));
await act(async () => result.current.submitSelectedIssues());
expect(submitSelectedIssues).toHaveBeenCalledWith(["risk-1"], "Create the selected issue.");
@@ -79,7 +100,7 @@ describe("useTimelineAgent", () => {
cancelReview,
});
const { result } = renderHook(() => useTimelineAgent(demo, "live"));
const { result } = renderHook(() => useTimelineAgent(demo, { mode: "live", status: readyStatus }));
await act(async () => result.current.cancelReview());
expect(cancelReview).toHaveBeenCalledWith("Cancelled by operator.");
@@ -87,7 +108,7 @@ describe("useTimelineAgent", () => {
it("disables run when the timeline cannot start", () => {
const demo = demoController({ canStart: false });
const { result } = renderHook(() => useTimelineAgent(demo, "live"));
const { result } = renderHook(() => useTimelineAgent(demo, { mode: "live", status: readyStatus }));
expect(result.current.canRun).toBe(false);
});
@@ -100,7 +121,7 @@ describe("useTimelineAgent", () => {
next,
});
const { result } = renderHook(() => useTimelineAgent(demo, "replay"));
const { result } = renderHook(() => useTimelineAgent(demo, { mode: "replay", status: replayStatus }));
await act(async () => result.current.cancelReview());
expect(cancelReview).toHaveBeenCalledWith("Cancelled by operator.");
@@ -111,4 +132,18 @@ describe("useTimelineAgent", () => {
]),
);
});
it("uses replay label when live target failed", () => {
const demo = demoController();
const { result } = renderHook(() =>
useTimelineAgent(demo, { mode: "live", status: failedStatus }),
);
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) }),
]),
);
});
});
@@ -1,5 +1,6 @@
import { useCallback, useMemo, useState } from "react";
import type { DemoTimelineController } from "../useDemoTimeline.js";
import type { PresentationTargetHealth } from "../../presentation/presentation-target-status.js";
import {
agentTextMessage,
agentToolCallPart,
@@ -10,6 +11,11 @@ import type { AgentToolName } from "./tools.js";
export type TimelineAgentMode = "live" | "replay";
export type TimelineAgentOptions = {
readonly mode: TimelineAgentMode;
readonly status: PresentationTargetHealth;
};
export type TimelineAgentController = {
readonly messages: ReadonlyArray<AgentMessage>;
readonly canRun: boolean;
@@ -39,17 +45,33 @@ const appendToolMessage = (
},
];
const introForStatus = (status: PresentationTargetHealth): string => {
switch (status.kind) {
case "ready":
return "Live target is ready. Direct slides still show replay evidence until I start the live run.";
case "active":
return "Live run is active. Operations are being sent to the workflow server.";
case "failed":
return "Replay fallback is active because the live target is unavailable.";
default:
return "Replay evidence is active. I can walk through the reviewed recording.";
}
};
export const useTimelineAgent = (
demo: DemoTimelineController,
modeLabel: TimelineAgentMode,
options: TimelineAgentOptions,
): TimelineAgentController => {
const modeLabel: TimelineAgentMode =
options.status.kind === "ready" || options.status.kind === "active"
? options.mode
: "replay";
const [messages, setMessages] = useState<ReadonlyArray<AgentMessage>>([
agentTextMessage(
"timeline-agent-intro",
"assistant",
modeLabel === "live"
? "Live workflow target is configured. I can run the prepared workflow now."
: "Replay fallback is active. I can still walk through the prepared workflow evidence.",
introForStatus(options.status),
),
]);
@@ -88,8 +110,6 @@ export const useTimelineAgent = (
const cancelReview = useCallback(async () => {
await demo.cancelReview("Cancelled by operator.");
// In replay the canonical recording only contains the submitted branch.
// Do not call next() or the UI would falsely advance into submitted evidence.
if (demo.state.mode === "live") {
await demo.next();
}
@@ -110,4 +130,4 @@ export const useTimelineAgent = (
submitSelectedIssues,
cancelReview,
};
};
};