refactor: make scene 8 a single chat entry beat

This commit is contained in:
lda
2026-07-12 06:15:19 +07:00 Verified
parent f22077da37
commit c5f9bed26d
12 changed files with 42 additions and 120 deletions
+10 -12
View File
@@ -180,12 +180,12 @@ claim boundaries and future work explicit, and end on the canonical defense
discussion index rather than a benchmark or generic conclusion.
Scenes 8 and 9 use the canonical prepared-authoring recording as their only
execution evidence. Scene 8 is a full-screen chat entry: its prefilled request
is submitted locally, then reveals the first deterministic user, assistant, and
Discover tool group. It is deterministic replay, not a live LLM chat, and does
not start a workflow run. The handoff beat reveals the full prepared conversation;
Scene 9 breaks the prepared authoring into five phases
with the same conversation contracted into a synchronized bottom dock. Each
execution evidence. Scene 8 is a single full-screen chat-entry beat: its
prefilled request is submitted locally, then reveals the first deterministic
user, assistant, and Discover tool group. It is deterministic replay, not a live
LLM chat, and does not start a workflow run. Scene 9 breaks the prepared
authoring into five phases with the same conversation contracted into a
synchronized bottom dock. Each
phase opens its matching prepared tool group beneath a factual source, graph,
repair, artifact, or deployment view. Neither scene calls workflow authoring
RPC operations — they consume deterministic prepared data. Scenes 10 through
@@ -203,8 +203,7 @@ presentation timeline.
The key deep-link-addressable defense states include:
- `/present#scene/agent-handoff/request` — Scene 8, prepared handoff request
- `/present#scene/agent-handoff/handoff` — Scene 8, agent accepts
- `/present#scene/agent-handoff/request` — Scene 8, prepared authoring request
- `/present#scene/prepared-lifecycle/discover` — Scene 9, discover phase
- `/present#scene/prepared-lifecycle/draft` — Scene 9, draft phase
- `/present#scene/prepared-lifecycle/deployment` — Scene 9, deployment phase
@@ -333,11 +332,10 @@ Scenes 8 and 9 are the prepared authoring story. They use deterministic data
from the committed `projectPreparedAuthoring()` recording and never call
workflow authoring RPC operations.
- **Scene 8 (Agent Handoff)**: a full-screen deterministic chat entry that
- **Scene 8 (Agent Request)**: a single full-screen deterministic chat entry that
pre-fills the report-authoring request. Send is local presentation state and
reveals the first prepared Discover tool group; the `handoff` beat reveals
the full completed conversation with prepared `wf` tool groups across all
authoring phases. This is not a live LLM chat or workflow run.
reveals the first prepared Discover tool group. This is not a live LLM chat
or workflow run.
- **Scene 9 (Prepared Workflow Lifecycle)**: a five-phase lifecycle
(discover, draft, validate, artifact, deployment) with a compact phase rail
and one dominant factual product projection per beat. The Scene 8 thread
@@ -86,7 +86,7 @@ describe("PresentationRoute", () => {
render(<PresentationRoute />);
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();
expect(await screen.findByLabelText("authoring phase rail")).toBeInTheDocument();
});
it("renders audience progress chrome without rail or mode label", async () => {
@@ -467,15 +467,6 @@ describe("PresentationRoute", () => {
expect(screen.getByRole("button", { name: /discover.*4 tool calls/i })).toBeInTheDocument();
});
it("navigates to Scene 8 handoff beat via hash", async () => {
window.location.hash = "#scene/agent-handoff/handoff";
const { PresentationRoute } = await import("./PresentationRoute.js");
render(<PresentationRoute />);
expect(await screen.findByRole("log", { name: "prepared authoring conversation" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: /deployment.*2 tool calls/i })).toBeInTheDocument();
});
it.each([
"#scene/prepared-lifecycle/discover",
"#scene/prepared-lifecycle/draft",
@@ -439,11 +439,11 @@ describe("SceneBody", () => {
expect(screen.getAllByText(/lda_report_case_study/i).length).toBeGreaterThanOrEqual(1);
});
it("routes Scene 8 through the prepared authoring conversation", () => {
renderSceneBodyAtMainLocation("agent-handoff", "handoff");
it("renders Scene 8 as the request entry beat", () => {
renderSceneBodyAtMainLocation("agent-handoff", "request");
expect(screen.getByRole("log", { name: "prepared authoring conversation" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: /deployment.*2 tool calls/i })).toBeInTheDocument();
expect(screen.getByRole("textbox", { name: /authoring request/i })).toBeInTheDocument();
expect(screen.queryByRole("button", { name: /deployment.*2 tool calls/i })).not.toBeInTheDocument();
});
it("renders evidence before discussion links so the chip lane cannot cover evidence text", () => {
@@ -5,10 +5,10 @@ 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 renderRequestBeat = () => {
const scene = findScene("agent-handoff");
const beat = findBeat("agent-handoff", beatId);
if (!scene || !beat) throw new Error(`missing agent-handoff/${beatId}`);
const beat = findBeat("agent-handoff", "request");
if (!scene || !beat) throw new Error("missing agent-handoff/request");
return render(<AgentHandoffScene scene={scene} beat={beat} />);
};
@@ -23,11 +23,11 @@ describe("AgentHandoffScene", () => {
/>,
);
expect(screen.getByRole("region", { name: "prepared agent handoff" })).toHaveAttribute(
expect(screen.getByRole("region", { name: "prepared agent request" })).toHaveAttribute(
"data-handoff-phase",
"discover",
);
expect(screen.getByRole("region", { name: "prepared agent handoff" })).toHaveAttribute(
expect(screen.getByRole("region", { name: "prepared agent request" })).toHaveAttribute(
"data-presentation-surface",
"editorial",
);
@@ -37,46 +37,17 @@ describe("AgentHandoffScene", () => {
expect(screen.queryByRole("button", { name: /run prepared workflow/i })).not.toBeInTheDocument();
});
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("reveals separated user and assistant turns after local submission", async () => {
renderBeat("request");
renderRequestBeat();
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", () => {
renderBeat("handoff");
expect(screen.getAllByText(/workflow\.deployments\.save/i).length).toBeGreaterThan(0);
expect(screen.getByRole("button", { name: /deployment.*2 tool calls/i }))
.toHaveAttribute("aria-expanded", "true");
});
it("does not render prepared workflow lifecycle content", () => {
renderBeat("request");
renderRequestBeat();
expect(screen.queryByText("prepared workflow lifecycle")).not.toBeInTheDocument();
cleanup();
renderBeat("handoff");
expect(screen.queryByText("prepared workflow lifecycle")).not.toBeInTheDocument();
});
it("does not expose a workflow run action", () => {
renderBeat("handoff");
expect(screen.queryByRole("button", { name: /run prepared workflow/i })).not.toBeInTheDocument();
});
});
@@ -1,5 +1,4 @@
import { useReducer } from "react";
import { AuthoringConversation } from "./AuthoringConversation.js";
import { Scene8ChatEntry } from "./Scene8ChatEntry.js";
import {
initialScene8EntryState,
@@ -12,32 +11,17 @@ type AgentHandoffSceneProps = {
readonly beat: SceneBeatDefinition;
};
/**
* Scene 8 request and handoff beats share one local deterministic entry state.
*/
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}
aria-label="prepared agent request"
data-handoff-phase="discover"
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>
)}
{beat.id === "request" && <Scene8ChatEntry state={entryState} dispatch={dispatch} />}
</section>
);
};
@@ -157,7 +157,6 @@ const beatContracts = {
"authoring/diagnose": { mode: "evidence", primarySurface: "authoring-diagnostic", supportSurface: "authoring-loop" },
"authoring/repair": { mode: "evidence", primarySurface: "authoring-repair", supportSurface: "authoring-loop" },
"agent-handoff/request": { mode: "conversation", primarySurface: "prepared-conversation", supportSurface: "none" },
"agent-handoff/handoff": { mode: "conversation", primarySurface: "prepared-conversation", supportSurface: "none" },
"evaluation/cohort": { mode: "evidence", primarySurface: "evaluation-cohort", supportSurface: "none" },
"evaluation/validity": { mode: "evidence", primarySurface: "evaluation-validity", supportSurface: "audit-reconciliation" },
"evaluation/findings": { mode: "evidence", primarySurface: "evaluation-findings", supportSurface: "validity-boundary" },
@@ -3006,22 +3006,6 @@
color: var(--authoring-ink, var(--text-primary));
}
.agent-handoff-scene__handoff {
min-height: 0;
height: 100%;
overflow: hidden;
}
.agent-handoff-scene__handoff .assistant-operator-thread {
width: 100%;
height: 100%;
max-height: none;
}
.agent-handoff-scene__handoff .assistant-thread__viewport {
padding: 1rem 1.2rem;
}
.agent-handoff-scene__composer {
display: grid;
gap: 0.75rem;
@@ -35,6 +35,7 @@ describe("storyboard navigation", () => {
it("falls back for unknown scene, beat, and branch hashes", () => {
expect(locationFromHash("#scene/missing/nope")).toEqual(defaultMainLocation);
expect(locationFromHash("#scene/lifecycle/nope")).toEqual(defaultMainLocation);
expect(locationFromHash("#scene/agent-handoff/handoff")).toEqual(defaultMainLocation);
expect(locationFromHash("#discuss/nope")).toEqual(defaultMainLocation);
});
@@ -43,6 +43,7 @@ describe("defense storyboard catalog", () => {
});
it("uses one editorial canvas theme and independent chat composition", () => {
expect(findScene("agent-handoff")?.beats).toHaveLength(1);
expect(findBeat("agent-handoff", "request")?.chatMode).toBe("hidden");
expect(findBeat("resume-output-evidence", "trace")?.chatMode).toBe("hidden");
});
@@ -154,15 +154,12 @@ export const mainScenes = defineScenes([
{
id: "agent-handoff",
number: 8,
title: "Agent Handoff",
title: "Agent Request",
claimClass: "implemented",
evidencePointer: "Constrained demo agent and prepared replay recipe",
view: "agent",
beats: [
// Scene 8 renders its own full-screen prepared transcript in the primary
// region, so the persistent stage chat rail must stay out of the way.
sceneBeat("request", "Operator request", "A thin agent interface receives the report request.", { chatMode: "hidden", chatTheme: "light" }),
sceneBeat("handoff", "Prepared operation", "The interface delegates durable work to lda.chat.", { chatMode: "hidden", chatTheme: "light" }),
],
},
{