fix: expose live run action in agent handoff scene

This commit is contained in:
lda
2026-07-11 22:18:47 +07:00 Verified
parent 4e81950bec
commit 31fb170dba
6 changed files with 53 additions and 8 deletions
@@ -214,7 +214,13 @@ describe("PresentationRoute", () => {
const { PresentationRoute } = await import("./PresentationRoute.js"); const { PresentationRoute } = await import("./PresentationRoute.js");
render(<PresentationRoute />); render(<PresentationRoute />);
expect(await screen.findByRole("button", { name: /run prepared workflow|run replay walkthrough/i })).toBeInTheDocument(); const actions = await screen.findAllByRole("button", {
name: /run prepared workflow|run replay walkthrough/i,
});
expect(actions.length).toBeGreaterThanOrEqual(1);
expect(
document.querySelector(".agent-handoff-scene .assistant-operator-thread__action button"),
).toBeInTheDocument();
}); });
it("uses stored target for live presentation mode", async () => { it("uses stored target for live presentation mode", async () => {
@@ -75,6 +75,7 @@ export const PresentationStage = ({
<SceneBody <SceneBody
location={state.location} location={state.location}
demo={demo} demo={demo}
timelineAgent={timelineAgent}
selectedNodeId={state.selectedNodeId} selectedNodeId={state.selectedNodeId}
selectNode={selectNode} selectNode={selectNode}
openEvidence={openEvidence} openEvidence={openEvidence}
@@ -1,4 +1,5 @@
import type { DemoTimelineController } from "../demo/useDemoTimeline.js"; import type { DemoTimelineController } from "../demo/useDemoTimeline.js";
import type { TimelineAgentController } from "../demo/agent/timelineAgent.js";
import type { DemoApprovalActions } from "./demo-approval-actions.js"; import type { DemoApprovalActions } from "./demo-approval-actions.js";
import { AgentHandoffScene } from "./authoring/AgentHandoffScene.js"; import { AgentHandoffScene } from "./authoring/AgentHandoffScene.js";
import { import {
@@ -25,6 +26,7 @@ import { ProblemLoopScene } from "./opening/ProblemLoopScene.js";
type SceneBodyProps = { type SceneBodyProps = {
readonly location: PresentationLocation; readonly location: PresentationLocation;
readonly demo: DemoTimelineController; readonly demo: DemoTimelineController;
readonly timelineAgent?: TimelineAgentController | undefined;
readonly selectedNodeId: string | null; readonly selectedNodeId: string | null;
readonly selectNode: (nodeId: string | null) => void; readonly selectNode: (nodeId: string | null) => void;
readonly openEvidence: () => void; readonly openEvidence: () => void;
@@ -303,7 +305,7 @@ const assertNever = (value: never): never => {
throw new Error(`Unexpected view: ${value}`); throw new Error(`Unexpected view: ${value}`);
}; };
export const SceneBody = ({ location, demo, selectedNodeId, selectNode, openEvidence, openDiscussion, onFocusPathChange, motionDisabled, approvalActions }: SceneBodyProps) => { export const SceneBody = ({ location, demo, timelineAgent, selectedNodeId, selectNode, openEvidence, openDiscussion, onFocusPathChange, motionDisabled, approvalActions }: SceneBodyProps) => {
const sceneId = location.kind === "main" ? location.sceneId : "positioning"; const sceneId = location.kind === "main" ? location.sceneId : "positioning";
const beatId = location.kind === "main" ? location.beatId : "landscape"; const beatId = location.kind === "main" ? location.beatId : "landscape";
const scene = findScene(sceneId) ?? findScene("thesis")!; const scene = findScene(sceneId) ?? findScene("thesis")!;
@@ -339,7 +341,7 @@ export const SceneBody = ({ location, demo, selectedNodeId, selectNode, openEvid
case "authoring": case "authoring":
return <AuthoringScene scene={scene} beat={beat} />; return <AuthoringScene scene={scene} beat={beat} />;
case "agent": case "agent":
return <AgentHandoffScene scene={scene} beat={beat} />; return <AgentHandoffScene scene={scene} beat={beat} timelineAgent={timelineAgent} />;
case "demo-lifecycle": case "demo-lifecycle":
return <PreparedAuthoringLifecycleScene scene={scene} beat={beat} />; return <PreparedAuthoringLifecycleScene scene={scene} beat={beat} />;
case "demo": case "demo":
@@ -1,5 +1,6 @@
import { cleanup, render, screen } from "@testing-library/react"; import { cleanup, render, screen } from "@testing-library/react";
import { afterEach, describe, expect, it } from "vitest"; import { afterEach, describe, expect, it, vi } from "vitest";
import type { TimelineAgentController } from "../../demo/agent/timelineAgent.js";
import { findBeat, findScene } from "../storyboard.js"; import { findBeat, findScene } from "../storyboard.js";
import { AgentHandoffScene } from "./AgentHandoffScene.js"; import { AgentHandoffScene } from "./AgentHandoffScene.js";
@@ -68,4 +69,29 @@ describe("AgentHandoffScene", () => {
renderBeat("handoff"); renderBeat("handoff");
expect(screen.queryByText("prepared workflow lifecycle")).not.toBeInTheDocument(); expect(screen.queryByText("prepared workflow lifecycle")).not.toBeInTheDocument();
}); });
it("exposes the live prepared-workflow action inside the visible conversation", () => {
const runPreparedWorkflow = vi.fn();
const timelineAgent = {
messages: [],
canRun: true,
runLabel: "Run prepared workflow",
runPreparedWorkflow,
submitSelectedIssues: vi.fn(async () => {}),
cancelReview: vi.fn(async () => {}),
} as unknown as TimelineAgentController;
render(
<AgentHandoffScene
scene={findScene("agent-handoff")!}
beat={findBeat("agent-handoff", "request")!}
timelineAgent={timelineAgent}
/>,
);
const action = screen.getByRole("button", { name: "Run prepared workflow" });
expect(action).toBeEnabled();
action.click();
expect(runPreparedWorkflow).toHaveBeenCalledOnce();
});
}); });
@@ -1,10 +1,12 @@
import { AuthoringConversation } from "./AuthoringConversation.js"; import { AuthoringConversation } from "./AuthoringConversation.js";
import type { AuthoringPhaseId } from "./authoring-recording.js"; import type { AuthoringPhaseId } from "./authoring-recording.js";
import type { TimelineAgentController } from "../../demo/agent/timelineAgent.js";
import type { SceneBeatDefinition, SceneDefinition } from "../storyboard.js"; import type { SceneBeatDefinition, SceneDefinition } from "../storyboard.js";
type AgentHandoffSceneProps = { type AgentHandoffSceneProps = {
readonly scene: SceneDefinition; readonly scene: SceneDefinition;
readonly beat: SceneBeatDefinition; readonly beat: SceneBeatDefinition;
readonly timelineAgent?: TimelineAgentController | undefined;
}; };
/** /**
@@ -12,10 +14,10 @@ type AgentHandoffSceneProps = {
* *
* The request beat shows the operator asking the agent to prepare a report; * The request beat shows the operator asking the agent to prepare a report;
* the handoff beat reveals the full completed conversation with all phases. * the handoff beat reveals the full completed conversation with all phases.
* Neither run actions nor approval actions are passed because this is a * The conversation is a prepared recording, but the shared run action can
* prepared recording, not a live agent interaction. * start the configured live workflow when a target is available.
*/ */
export const AgentHandoffScene = ({ beat }: AgentHandoffSceneProps) => { export const AgentHandoffScene = ({ beat, timelineAgent }: AgentHandoffSceneProps) => {
const phase: AuthoringPhaseId = beat.id === "handoff" ? "deployment" : "discover"; const phase: AuthoringPhaseId = beat.id === "handoff" ? "deployment" : "discover";
const phases: readonly { readonly id: AuthoringPhaseId; readonly label: string }[] = [ const phases: readonly { readonly id: AuthoringPhaseId; readonly label: string }[] = [
{ id: "discover", label: "Discover" }, { id: "discover", label: "Discover" },
@@ -46,6 +48,11 @@ export const AgentHandoffScene = ({ beat }: AgentHandoffSceneProps) => {
throughPhase={phase} throughPhase={phase}
activePhase={phase} activePhase={phase}
surface="stage" surface="stage"
runAction={timelineAgent ? {
label: timelineAgent.runLabel,
disabled: !timelineAgent.canRun,
run: () => { void timelineAgent.runPreparedWorkflow(); },
} : undefined}
/> />
</div> </div>
</section> </section>
@@ -9,6 +9,7 @@ type AuthoringConversationProps = {
readonly throughPhase: AuthoringPhaseId; readonly throughPhase: AuthoringPhaseId;
readonly activePhase: AuthoringPhaseId; readonly activePhase: AuthoringPhaseId;
readonly surface: "stage" | "dock"; readonly surface: "stage" | "dock";
readonly runAction?: { readonly label: string; readonly disabled: boolean; readonly run: () => void } | undefined;
}; };
/** Renders the same prepared conversation at full-stage or compact-dock scale. */ /** Renders the same prepared conversation at full-stage or compact-dock scale. */
@@ -16,6 +17,7 @@ export const AuthoringConversation = ({
throughPhase, throughPhase,
activePhase, activePhase,
surface, surface,
runAction,
}: AuthoringConversationProps) => ( }: AuthoringConversationProps) => (
<AssistantOperatorThread <AssistantOperatorThread
mode={surface === "stage" ? "full" : "dock"} mode={surface === "stage" ? "full" : "dock"}
@@ -23,5 +25,6 @@ export const AuthoringConversation = ({
messages={projectPreparedAuthoringThread(throughPhase)} messages={projectPreparedAuthoringThread(throughPhase)}
activeToolGroupId={authoringToolGroupId(activePhase)} activeToolGroupId={authoringToolGroupId(activePhase)}
ariaLabel="prepared authoring conversation" ariaLabel="prepared authoring conversation"
runAction={runAction}
/> />
); );