feat: give the agent handoff and demo one visual spine
This commit is contained in:
@@ -72,6 +72,7 @@ describe("GuidedProductMoment", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-moment", "approval");
|
expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-moment", "approval");
|
||||||
|
expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-primary-surface", "interrupt-approval");
|
||||||
expect(screen.getByText(/Run is paused/i)).toBeInTheDocument();
|
expect(screen.getByText(/Run is paused/i)).toBeInTheDocument();
|
||||||
expect(screen.getByText("Workflow input")).toBeInTheDocument();
|
expect(screen.getByText("Workflow input")).toBeInTheDocument();
|
||||||
expect(screen.getByText("project-brief.md")).toBeInTheDocument();
|
expect(screen.getByText("project-brief.md")).toBeInTheDocument();
|
||||||
@@ -92,6 +93,7 @@ describe("GuidedProductMoment", () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-moment", "resume");
|
expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-moment", "resume");
|
||||||
|
expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-primary-surface", "resume-output");
|
||||||
expect(screen.getByLabelText("workflow.runs.resume operation")).toBeInTheDocument();
|
expect(screen.getByLabelText("workflow.runs.resume operation")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -257,6 +259,7 @@ describe("GuidedProductMoment", () => {
|
|||||||
/>,
|
/>,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
expect(screen.getByRole("region", { name: /current product moment/i })).toHaveAttribute("data-support-surface", "output-summary");
|
||||||
expect(screen.getByRole("region", { name: /workflow trace proof/i })).toBeInTheDocument();
|
expect(screen.getByRole("region", { name: /workflow trace proof/i })).toBeInTheDocument();
|
||||||
expect(screen.getByRole("region", { name: /workflow output summary/i })).toHaveAttribute("data-output-priority", "summary");
|
expect(screen.getByRole("region", { name: /workflow output summary/i })).toHaveAttribute("data-output-priority", "summary");
|
||||||
expect(screen.queryByText("No trace frames captured.")).not.toBeInTheDocument();
|
expect(screen.queryByText("No trace frames captured.")).not.toBeInTheDocument();
|
||||||
|
|||||||
@@ -41,6 +41,19 @@ const statusCopy = (
|
|||||||
return "Run is paused. Submit resumes this same run.";
|
return "Run is paused. Submit resumes this same run.";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const hierarchyForMoment = (moment: ReturnType<typeof momentForBeat>) => {
|
||||||
|
switch (moment) {
|
||||||
|
case "approval":
|
||||||
|
return { primary: "interrupt-approval", support: "input-facts" };
|
||||||
|
case "resume":
|
||||||
|
return { primary: "resume-output", support: "resume-operation" };
|
||||||
|
case "output":
|
||||||
|
return { primary: "workflow-output", support: "none" };
|
||||||
|
case "trace":
|
||||||
|
return { primary: "trace-evidence", support: "output-summary" };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const GuidedProductMoment = ({
|
export const GuidedProductMoment = ({
|
||||||
beat,
|
beat,
|
||||||
demo,
|
demo,
|
||||||
@@ -50,12 +63,19 @@ export const GuidedProductMoment = ({
|
|||||||
openEvidence,
|
openEvidence,
|
||||||
}: GuidedProductMomentProps) => {
|
}: GuidedProductMomentProps) => {
|
||||||
const moment = momentForBeat(beat.id);
|
const moment = momentForBeat(beat.id);
|
||||||
|
const hierarchy = hierarchyForMoment(moment);
|
||||||
const lens = demoBeatLensForBeat(beat.id);
|
const lens = demoBeatLensForBeat(beat.id);
|
||||||
const facts = projectDemoRunFacts(demo);
|
const facts = projectDemoRunFacts(demo);
|
||||||
const runResume = demo.state.events.find((event) => event.stage === "run_resume");
|
const runResume = demo.state.events.find((event) => event.stage === "run_resume");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<section className="guided-product-moment" aria-label="current product moment" data-moment={moment}>
|
<section
|
||||||
|
className="guided-product-moment"
|
||||||
|
aria-label="current product moment"
|
||||||
|
data-moment={moment}
|
||||||
|
data-primary-surface={hierarchy.primary}
|
||||||
|
data-support-surface={hierarchy.support}
|
||||||
|
>
|
||||||
<header className="guided-product-moment__header">
|
<header className="guided-product-moment__header">
|
||||||
<span>{lens.eyebrow}</span>
|
<span>{lens.eyebrow}</span>
|
||||||
<strong>{lens.headline}</strong>
|
<strong>{lens.headline}</strong>
|
||||||
|
|||||||
@@ -10,28 +10,48 @@ const renderBeat = (beatId: "request" | "handoff") => {
|
|||||||
return render(<AgentHandoffScene scene={scene} beat={beat} />);
|
return render(<AgentHandoffScene scene={scene} beat={beat} />);
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("AgentHandoffScene", () => {
|
afterEach(cleanup);
|
||||||
afterEach(cleanup);
|
|
||||||
|
|
||||||
it("renders a log region named prepared authoring conversation", () => {
|
describe("AgentHandoffScene", () => {
|
||||||
renderBeat("request");
|
it("renders one prepared conversation with a phase orientation rail", () => {
|
||||||
expect(screen.getByRole("log", { name: "prepared authoring conversation" })).toBeInTheDocument();
|
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", () => {
|
it("renders separated user and assistant turns on the request beat", () => {
|
||||||
renderBeat("request");
|
renderBeat("request");
|
||||||
const userMessages = screen.getAllByText(/report|workflow|prepare/i);
|
expect(screen.getAllByText(/report|workflow|prepare/i).length).toBeGreaterThanOrEqual(1);
|
||||||
expect(userMessages.length).toBeGreaterThanOrEqual(1);
|
expect(screen.getAllByText(/inspect|capabilities|sources|schemas|let me/i).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);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("interleaves prepared workflow tool groups with the handoff conversation", () => {
|
it("interleaves prepared workflow tool groups with the handoff conversation", () => {
|
||||||
@@ -44,9 +64,7 @@ describe("AgentHandoffScene", () => {
|
|||||||
it("does not render prepared workflow lifecycle content", () => {
|
it("does not render prepared workflow lifecycle content", () => {
|
||||||
renderBeat("request");
|
renderBeat("request");
|
||||||
expect(screen.queryByText("prepared workflow lifecycle")).not.toBeInTheDocument();
|
expect(screen.queryByText("prepared workflow lifecycle")).not.toBeInTheDocument();
|
||||||
});
|
cleanup();
|
||||||
|
|
||||||
it("does not include prepared workflow lifecycle content on handoff", () => {
|
|
||||||
renderBeat("handoff");
|
renderBeat("handoff");
|
||||||
expect(screen.queryByText("prepared workflow lifecycle")).not.toBeInTheDocument();
|
expect(screen.queryByText("prepared workflow lifecycle")).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { AuthoringConversation } from "./AuthoringConversation.js";
|
import { AuthoringConversation } from "./AuthoringConversation.js";
|
||||||
|
import type { AuthoringPhaseId } from "./authoring-recording.js";
|
||||||
import type { SceneBeatDefinition, SceneDefinition } from "../storyboard.js";
|
import type { SceneBeatDefinition, SceneDefinition } from "../storyboard.js";
|
||||||
|
|
||||||
type AgentHandoffSceneProps = {
|
type AgentHandoffSceneProps = {
|
||||||
@@ -15,13 +16,38 @@ type AgentHandoffSceneProps = {
|
|||||||
* prepared recording, not a live agent interaction.
|
* prepared recording, not a live agent interaction.
|
||||||
*/
|
*/
|
||||||
export const AgentHandoffScene = ({ beat }: AgentHandoffSceneProps) => {
|
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 (
|
return (
|
||||||
<AuthoringConversation
|
<section className="agent-handoff-scene" aria-label="prepared agent handoff" data-handoff-phase={phase}>
|
||||||
throughPhase={phase}
|
<header className="agent-handoff-scene__header">
|
||||||
activePhase={phase}
|
<span>Prepared replay</span>
|
||||||
surface="stage"
|
<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"
|
className="prepared-lifecycle-scene"
|
||||||
aria-label="prepared workflow authoring lifecycle"
|
aria-label="prepared workflow authoring lifecycle"
|
||||||
data-active-phase={beatId}
|
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">
|
<ol className="prepared-lifecycle-scene__rail" aria-label="authoring phase rail">
|
||||||
{phases.map((phase) => (
|
{phases.map((phase) => (
|
||||||
|
|||||||
@@ -2753,13 +2753,104 @@
|
|||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.presentation-stage[data-scene-view="agent"] .presentation-stage__primary {
|
||||||
|
align-items: stretch;
|
||||||
|
justify-content: stretch;
|
||||||
|
padding: 1.15rem 1.5rem 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-handoff-scene {
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto auto minmax(0, 1fr);
|
||||||
|
gap: 0.65rem;
|
||||||
|
width: min(100%, 72rem);
|
||||||
|
min-height: 0;
|
||||||
|
height: 100%;
|
||||||
|
margin-inline: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-handoff-scene__header {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: auto minmax(0, 1fr);
|
||||||
|
gap: 0.2rem 0.7rem;
|
||||||
|
align-items: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-handoff-scene__header span {
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
font: 700 0.66rem/1 var(--font-evidence);
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
text-transform: uppercase;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-handoff-scene__header strong {
|
||||||
|
color: var(--text-primary);
|
||||||
|
font: 700 1.08rem/1.1 var(--font-interface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-handoff-scene__header p {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
margin: 0;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font: 0.78rem/1.3 var(--font-interface);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-handoff-scene__rail {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.35rem;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-handoff-scene__rail li {
|
||||||
|
display: flex;
|
||||||
|
flex: 1 1 0;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.35rem;
|
||||||
|
min-width: 0;
|
||||||
|
border-bottom: 1px solid var(--stage-line);
|
||||||
|
padding: 0.35rem 0.45rem;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font: 700 0.68rem/1 var(--font-evidence);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-handoff-scene__rail li span {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-handoff-scene__rail li[data-active="true"] {
|
||||||
|
border-color: var(--accent-cyan);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-handoff-scene__rail li[data-active="true"] span {
|
||||||
|
color: var(--accent-cyan);
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-handoff-scene__thread {
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-handoff-scene__thread .assistant-operator-thread {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
max-height: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.agent-handoff-scene__thread .assistant-thread__viewport {
|
||||||
|
padding: 1rem 1.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
Scene 9 — Prepared Workflow Authoring Lifecycle.
|
Scene 9 — Prepared Workflow Authoring Lifecycle.
|
||||||
Compact orientation rail plus dominant phase projection.
|
Compact orientation rail plus dominant phase projection.
|
||||||
*/
|
*/
|
||||||
.prepared-lifecycle-scene {
|
.prepared-lifecycle-scene {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: auto minmax(0, 1fr) 11rem;
|
grid-template-rows: auto minmax(0, 1fr) 8.5rem;
|
||||||
gap: 0.55rem;
|
gap: 0.55rem;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|||||||
Reference in New Issue
Block a user