feat: give the agent handoff and demo one visual spine

This commit is contained in:
lda
2026-07-11 09:27:03 +07:00 Verified
parent 4951d2487c
commit 5b1368d5ee
6 changed files with 188 additions and 28 deletions
@@ -10,28 +10,48 @@ const renderBeat = (beatId: "request" | "handoff") => {
return render(<AgentHandoffScene scene={scene} beat={beat} />);
};
describe("AgentHandoffScene", () => {
afterEach(cleanup);
afterEach(cleanup);
it("renders a log region named prepared authoring conversation", () => {
renderBeat("request");
expect(screen.getByRole("log", { name: "prepared authoring conversation" })).toBeInTheDocument();
describe("AgentHandoffScene", () => {
it("renders one prepared conversation with a phase orientation rail", () => {
render(
<AgentHandoffScene
scene={findScene("agent-handoff")!}
beat={findBeat("agent-handoff", "request")!}
/>,
);
expect(screen.getByRole("region", { name: "prepared agent handoff" })).toHaveAttribute(
"data-handoff-phase",
"discover",
);
expect(screen.getByRole("log", { name: "prepared authoring conversation" })).toHaveAttribute(
"data-surface",
"stage",
);
expect(screen.getByRole("list", { name: "prepared handoff phases" })).toBeInTheDocument();
expect(screen.getAllByText(/workflow\.sources\.list/i).length).toBeGreaterThan(0);
});
it("advances the same conversation to deployment evidence", () => {
render(
<AgentHandoffScene
scene={findScene("agent-handoff")!}
beat={findBeat("agent-handoff", "handoff")!}
/>,
);
expect(screen.getByRole("region", { name: "prepared agent handoff" })).toHaveAttribute(
"data-handoff-phase",
"deployment",
);
expect(screen.getAllByText(/workflow\.deployments\.save/i).length).toBeGreaterThan(0);
});
it("renders separated user and assistant turns on the request beat", () => {
renderBeat("request");
const userMessages = screen.getAllByText(/report|workflow|prepare/i);
expect(userMessages.length).toBeGreaterThanOrEqual(1);
const assistantMessages = screen.getAllByText(/inspect|capabilities|sources|schemas|let me/i);
expect(assistantMessages.length).toBeGreaterThanOrEqual(1);
});
it("renders the full conversation on the handoff beat", () => {
renderBeat("handoff");
const userMessages = screen.getAllByText(/report|workflow|prepare|save/i);
expect(userMessages.length).toBeGreaterThanOrEqual(1);
const assistantMessages = screen.getAllByText(/inspect|sources|capabilities|compile|deployment|artifact/i);
expect(assistantMessages.length).toBeGreaterThanOrEqual(2);
expect(screen.getAllByText(/report|workflow|prepare/i).length).toBeGreaterThanOrEqual(1);
expect(screen.getAllByText(/inspect|capabilities|sources|schemas|let me/i).length).toBeGreaterThanOrEqual(1);
});
it("interleaves prepared workflow tool groups with the handoff conversation", () => {
@@ -44,9 +64,7 @@ describe("AgentHandoffScene", () => {
it("does not render prepared workflow lifecycle content", () => {
renderBeat("request");
expect(screen.queryByText("prepared workflow lifecycle")).not.toBeInTheDocument();
});
it("does not include prepared workflow lifecycle content on handoff", () => {
cleanup();
renderBeat("handoff");
expect(screen.queryByText("prepared workflow lifecycle")).not.toBeInTheDocument();
});
@@ -1,4 +1,5 @@
import { AuthoringConversation } from "./AuthoringConversation.js";
import type { AuthoringPhaseId } from "./authoring-recording.js";
import type { SceneBeatDefinition, SceneDefinition } from "../storyboard.js";
type AgentHandoffSceneProps = {
@@ -15,13 +16,38 @@ type AgentHandoffSceneProps = {
* prepared recording, not a live agent interaction.
*/
export const AgentHandoffScene = ({ beat }: AgentHandoffSceneProps) => {
const phase = beat.id === "handoff" ? "deployment" : "discover";
const phase: AuthoringPhaseId = beat.id === "handoff" ? "deployment" : "discover";
const phases: readonly { readonly id: AuthoringPhaseId; readonly label: string }[] = [
{ id: "discover", label: "Discover" },
{ id: "draft", label: "Draft" },
{ id: "validate", label: "Validate" },
{ id: "artifact", label: "Artifact" },
{ id: "deployment", label: "Deployment" },
];
const activeIndex = phases.findIndex(({ id }) => id === phase);
return (
<AuthoringConversation
throughPhase={phase}
activePhase={phase}
surface="stage"
/>
<section className="agent-handoff-scene" aria-label="prepared agent handoff" data-handoff-phase={phase}>
<header className="agent-handoff-scene__header">
<span>Prepared replay</span>
<strong>External agent workflow substrate</strong>
<p>One conversation, with public operations and interpreted results.</p>
</header>
<ol className="agent-handoff-scene__rail" aria-label="prepared handoff phases">
{phases.map((item, index) => (
<li key={item.id} data-active={index === activeIndex ? "true" : "false"}>
<span>{index + 1}</span>
<strong>{item.label}</strong>
</li>
))}
</ol>
<div className="agent-handoff-scene__thread">
<AuthoringConversation
throughPhase={phase}
activePhase={phase}
surface="stage"
/>
</div>
</section>
);
};
@@ -38,6 +38,8 @@ export const PreparedAuthoringLifecycleScene = ({ scene, beat }: PreparedAuthori
className="prepared-lifecycle-scene"
aria-label="prepared workflow authoring lifecycle"
data-active-phase={beatId}
data-primary-surface="authoring-phase"
data-support-surface="prepared-chat"
>
<ol className="prepared-lifecycle-scene__rail" aria-label="authoring phase rail">
{phases.map((phase) => (