feat: wire scene 8 chat entry beats
This commit is contained in:
@@ -43,6 +43,7 @@ export const OperatorChat = ({ state, messages, timelineAgent, onApprove, onRequ
|
||||
? () => { timelineAgent.requestRevision().catch(console.error); }
|
||||
: onRequestRevision;
|
||||
const composition = compositionForState(state);
|
||||
const isScene8 = state.location.kind === "main" && state.location.sceneId === "agent-handoff";
|
||||
const presentationSurface = composition.chatTheme === "light" ? "editorial" : "night";
|
||||
return (
|
||||
<aside
|
||||
@@ -55,7 +56,7 @@ export const OperatorChat = ({ state, messages, timelineAgent, onApprove, onRequ
|
||||
<AssistantOperatorThread
|
||||
mode={composition.chatMode}
|
||||
messages={visibleMessages}
|
||||
runAction={timelineAgent ? {
|
||||
runAction={timelineAgent && !isScene8 ? {
|
||||
label: timelineAgent.runLabel,
|
||||
disabled: !timelineAgent.canRun,
|
||||
run: () => void timelineAgent.runPreparedWorkflow(),
|
||||
|
||||
@@ -84,7 +84,7 @@ describe("PresentationRoute", () => {
|
||||
window.location.hash = "#scene/agent-handoff/request";
|
||||
const { PresentationRoute } = await import("./PresentationRoute.js");
|
||||
render(<PresentationRoute />);
|
||||
expect(screen.getByRole("log", { name: "prepared authoring conversation" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("textbox", { name: /authoring request/i })).toBeInTheDocument();
|
||||
await userEvent.keyboard("{ArrowRight}");
|
||||
expect(await screen.findByRole("button", { name: /deployment.*2 tool calls/i })).toBeInTheDocument();
|
||||
});
|
||||
@@ -209,18 +209,13 @@ describe("PresentationRoute", () => {
|
||||
expect(screen.getByRole("button", { name: /inspect evidence/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders a chat run action on the presentation route", async () => {
|
||||
it("renders the Scene 8 composer without a workflow run action", async () => {
|
||||
window.location.hash = "#scene/agent-handoff/request";
|
||||
const { PresentationRoute } = await import("./PresentationRoute.js");
|
||||
render(<PresentationRoute />);
|
||||
|
||||
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();
|
||||
expect(await screen.findByRole("textbox", { name: /authoring request/i })).toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: "Run prepared workflow" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("uses stored target for live presentation mode", async () => {
|
||||
@@ -268,16 +263,15 @@ describe("PresentationRoute", () => {
|
||||
expect(screen.queryByText(/checking reachability/i)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows the live prepared-workflow action after a healthy target probe", async () => {
|
||||
it("keeps Scene 8 local after a healthy target probe", async () => {
|
||||
window.sessionStorage.setItem("lda.workflowConsole.target", "http://127.0.0.1:8765/rpc");
|
||||
window.location.hash = "#scene/agent-handoff/request";
|
||||
const { PresentationRoute } = await import("./PresentationRoute.js");
|
||||
render(<PresentationRoute />);
|
||||
|
||||
expect(await screen.findByText(/Live target ready/i)).toBeInTheDocument();
|
||||
const runActions = screen.getAllByRole("button", { name: "Run prepared workflow" });
|
||||
expect(runActions.length).toBeGreaterThanOrEqual(1);
|
||||
expect(runActions.every((action) => !action.hasAttribute("disabled"))).toBe(true);
|
||||
expect(screen.getByRole("button", { name: "Send" })).toBeEnabled();
|
||||
expect(screen.queryByRole("button", { name: "Run prepared workflow" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("opens Scene 10 approval from the canonical hash", async () => {
|
||||
@@ -465,6 +459,8 @@ describe("PresentationRoute", () => {
|
||||
const { PresentationRoute } = await import("./PresentationRoute.js");
|
||||
render(<PresentationRoute />);
|
||||
|
||||
const send = await screen.findByRole("button", { name: "Send" });
|
||||
await userEvent.click(send);
|
||||
expect(await screen.findByRole("log", { name: "prepared authoring conversation" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: /discover.*4 tool calls/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
@@ -340,7 +340,7 @@ export const SceneBody = ({ location, demo, timelineAgent, selectedNodeId, selec
|
||||
case "authoring":
|
||||
return <AuthoringScene scene={scene} beat={beat} />;
|
||||
case "agent":
|
||||
return <AgentHandoffScene scene={scene} beat={beat} timelineAgent={timelineAgent} />;
|
||||
return <AgentHandoffScene scene={scene} beat={beat} />;
|
||||
case "demo-lifecycle":
|
||||
return <PreparedAuthoringLifecycleScene scene={scene} beat={beat} />;
|
||||
case "demo":
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { cleanup, render, screen } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { TimelineAgentController } from "../../demo/agent/timelineAgent.js";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { findBeat, findScene } from "../storyboard.js";
|
||||
import { AgentHandoffScene } from "./AgentHandoffScene.js";
|
||||
import { SCENE8_REQUEST } from "./scene8-entry-state.js";
|
||||
|
||||
const renderBeat = (beatId: "request" | "handoff") => {
|
||||
const scene = findScene("agent-handoff");
|
||||
@@ -14,7 +15,7 @@ const renderBeat = (beatId: "request" | "handoff") => {
|
||||
afterEach(cleanup);
|
||||
|
||||
describe("AgentHandoffScene", () => {
|
||||
it("renders one prepared conversation with a phase orientation rail", () => {
|
||||
it("renders the empty request beat as a chat composer", () => {
|
||||
render(
|
||||
<AgentHandoffScene
|
||||
scene={findScene("agent-handoff")!}
|
||||
@@ -30,14 +31,10 @@ describe("AgentHandoffScene", () => {
|
||||
"data-presentation-surface",
|
||||
"editorial",
|
||||
);
|
||||
expect(screen.getByRole("log", { name: "prepared authoring conversation" })).toHaveAttribute(
|
||||
"data-surface",
|
||||
"stage",
|
||||
);
|
||||
expect(screen.getByText(/We need to author a report workflow/i)).toBeInTheDocument();
|
||||
expect(screen.getByText(/Let me inspect the available sources/i)).toBeInTheDocument();
|
||||
expect(screen.getByRole("list", { name: "prepared handoff phases" })).toBeInTheDocument();
|
||||
expect(screen.getAllByText(/workflow\.sources\.list/i).length).toBeGreaterThan(0);
|
||||
expect(screen.getByRole("heading", { name: /what should the workflow author prepare/i })).toBeInTheDocument();
|
||||
expect(screen.getByRole("textbox", { name: /authoring request/i })).toHaveValue(SCENE8_REQUEST);
|
||||
expect(screen.queryByRole("list", { name: "prepared handoff phases" })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole("button", { name: /run prepared workflow/i })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("advances the same conversation to deployment evidence", () => {
|
||||
@@ -55,10 +52,12 @@ describe("AgentHandoffScene", () => {
|
||||
expect(screen.getAllByText(/workflow\.deployments\.save/i).length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("renders separated user and assistant turns on the request beat", () => {
|
||||
it("reveals separated user and assistant turns after local submission", async () => {
|
||||
renderBeat("request");
|
||||
await userEvent.click(screen.getByRole("button", { name: "Send" }));
|
||||
expect(screen.getAllByText(/report|workflow|prepare/i).length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getAllByText(/inspect|capabilities|sources|schemas|let me/i).length).toBeGreaterThanOrEqual(1);
|
||||
expect(screen.getByRole("button", { name: /discover.*4 tool calls/i })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("interleaves prepared workflow tool groups with the handoff conversation", () => {
|
||||
@@ -76,28 +75,8 @@ describe("AgentHandoffScene", () => {
|
||||
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 () => {}),
|
||||
requestRevision: 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();
|
||||
it("does not expose a workflow run action", () => {
|
||||
renderBeat("handoff");
|
||||
expect(screen.queryByRole("button", { name: /run prepared workflow/i })).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,60 +1,43 @@
|
||||
import { useReducer } from "react";
|
||||
import { AuthoringConversation } from "./AuthoringConversation.js";
|
||||
import type { AuthoringPhaseId } from "./authoring-recording.js";
|
||||
import type { TimelineAgentController } from "../../demo/agent/timelineAgent.js";
|
||||
import { Scene8ChatEntry } from "./Scene8ChatEntry.js";
|
||||
import {
|
||||
initialScene8EntryState,
|
||||
scene8EntryReducer,
|
||||
} from "./scene8-entry-state.js";
|
||||
import type { SceneBeatDefinition, SceneDefinition } from "../storyboard.js";
|
||||
|
||||
type AgentHandoffSceneProps = {
|
||||
readonly scene: SceneDefinition;
|
||||
readonly beat: SceneBeatDefinition;
|
||||
readonly timelineAgent?: TimelineAgentController | undefined;
|
||||
};
|
||||
|
||||
/**
|
||||
* Full-screen prepared-authoring conversation for Scene 8.
|
||||
*
|
||||
* 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 conversation is a prepared recording, but the shared run action can
|
||||
* start the configured live workflow when a target is available.
|
||||
* Scene 8 request and handoff beats share one local deterministic entry state.
|
||||
*/
|
||||
export const AgentHandoffScene = ({ beat, timelineAgent }: AgentHandoffSceneProps) => {
|
||||
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);
|
||||
export const AgentHandoffScene = ({ beat }: AgentHandoffSceneProps) => {
|
||||
const [entryState, dispatch] = useReducer(scene8EntryReducer, initialScene8EntryState);
|
||||
const phase = beat.id === "handoff" ? "deployment" : "discover";
|
||||
|
||||
return (
|
||||
<section className="agent-handoff-scene" aria-label="prepared agent handoff" data-handoff-phase={phase} data-presentation-surface="editorial">
|
||||
<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"
|
||||
runAction={timelineAgent ? {
|
||||
label: timelineAgent.runLabel,
|
||||
disabled: !timelineAgent.canRun,
|
||||
run: () => { void timelineAgent.runPreparedWorkflow(); },
|
||||
} : undefined}
|
||||
/>
|
||||
</div>
|
||||
<section
|
||||
className="agent-handoff-scene"
|
||||
aria-label="prepared agent handoff"
|
||||
data-handoff-phase={phase}
|
||||
data-presentation-surface="editorial"
|
||||
>
|
||||
{beat.id === "request" ? (
|
||||
<Scene8ChatEntry state={entryState} dispatch={dispatch} />
|
||||
) : (
|
||||
<div className="agent-handoff-scene__handoff">
|
||||
<AuthoringConversation
|
||||
throughPhase="deployment"
|
||||
activePhase="deployment"
|
||||
surface="stage"
|
||||
{...(entryState.phase === "submitted" ? { requestOverride: entryState.request } : {})}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2995,90 +2995,30 @@
|
||||
}
|
||||
|
||||
.agent-handoff-scene {
|
||||
display: grid;
|
||||
grid-template-rows: auto auto minmax(0, 1fr);
|
||||
gap: 0.65rem;
|
||||
display: block;
|
||||
width: min(100%, 72rem);
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
margin-inline: auto;
|
||||
overflow: hidden;
|
||||
border-radius: 0.35rem;
|
||||
background: var(--stage-canvas);
|
||||
color: var(--authoring-ink, var(--text-primary));
|
||||
}
|
||||
|
||||
.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 {
|
||||
.agent-handoff-scene__handoff {
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.agent-handoff-scene__thread .assistant-operator-thread {
|
||||
.agent-handoff-scene__handoff .assistant-operator-thread {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.agent-handoff-scene__thread .assistant-thread__viewport {
|
||||
.agent-handoff-scene__handoff .assistant-thread__viewport {
|
||||
padding: 1rem 1.2rem;
|
||||
}
|
||||
|
||||
@@ -3187,29 +3127,6 @@
|
||||
color: var(--authoring-ink);
|
||||
}
|
||||
|
||||
.agent-handoff-scene[data-presentation-surface="editorial"] .agent-handoff-scene__header span,
|
||||
.agent-handoff-scene[data-presentation-surface="editorial"] .agent-handoff-scene__rail li[data-active="true"] span {
|
||||
color: var(--authoring-accent);
|
||||
}
|
||||
|
||||
.agent-handoff-scene[data-presentation-surface="editorial"] .agent-handoff-scene__header strong,
|
||||
.agent-handoff-scene[data-presentation-surface="editorial"] .agent-handoff-scene__rail li[data-active="true"] {
|
||||
color: var(--authoring-ink);
|
||||
}
|
||||
|
||||
.agent-handoff-scene[data-presentation-surface="editorial"] .agent-handoff-scene__header p,
|
||||
.agent-handoff-scene[data-presentation-surface="editorial"] .agent-handoff-scene__rail li {
|
||||
color: var(--authoring-muted);
|
||||
}
|
||||
|
||||
.agent-handoff-scene[data-presentation-surface="editorial"] .agent-handoff-scene__rail li {
|
||||
border-color: var(--authoring-rule);
|
||||
}
|
||||
|
||||
.agent-handoff-scene[data-presentation-surface="editorial"] .agent-handoff-scene__rail li[data-active="true"] {
|
||||
border-color: var(--authoring-accent);
|
||||
}
|
||||
|
||||
.agent-handoff-scene[data-presentation-surface="editorial"] .assistant-operator-thread[data-surface="stage"] {
|
||||
color: var(--authoring-ink);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user