feat: compose continuous authoring workspace
This commit is contained in:
@@ -429,7 +429,14 @@ describe("SceneBody", () => {
|
|||||||
it("renders Scene 9 artifact beat with compiled artifact evidence", () => {
|
it("renders Scene 9 artifact beat with compiled artifact evidence", () => {
|
||||||
renderSceneBodyAtMainLocation("prepared-lifecycle", "artifact");
|
renderSceneBodyAtMainLocation("prepared-lifecycle", "artifact");
|
||||||
expect(screen.getByText("Artifact")).toBeInTheDocument();
|
expect(screen.getByText("Artifact")).toBeInTheDocument();
|
||||||
expect(screen.getByText(/art_x9y8z7/i)).toBeInTheDocument();
|
expect(screen.getAllByText(/lda_report_case_study/i).length).toBeGreaterThanOrEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("routes Scene 8 through the prepared authoring conversation", () => {
|
||||||
|
renderSceneBodyAtMainLocation("agent-handoff", "handoff");
|
||||||
|
|
||||||
|
expect(screen.getByRole("log", { name: "prepared authoring conversation" })).toBeInTheDocument();
|
||||||
|
expect(screen.getByRole("button", { name: /deployment.*2 tool calls/i })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders evidence before discussion links so the chip lane cannot cover evidence text", () => {
|
it("renders evidence before discussion links so the chip lane cannot cover evidence text", () => {
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { DemoTimelineController } from "../demo/useDemoTimeline.js";
|
import type { DemoTimelineController } from "../demo/useDemoTimeline.js";
|
||||||
import type { DemoApprovalActions } from "./demo-approval-actions.js";
|
import type { DemoApprovalActions } from "./demo-approval-actions.js";
|
||||||
|
import { AgentHandoffScene } from "./authoring/AgentHandoffScene.js";
|
||||||
import {
|
import {
|
||||||
discussionBranches,
|
discussionBranches,
|
||||||
findBeat,
|
findBeat,
|
||||||
@@ -260,15 +261,6 @@ const AuthoringScene = ({ scene, beat }: { scene: SceneDefinition; beat: SceneBe
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
const AgentHandoffScene = ({ scene, beat }: { scene: SceneDefinition; beat: SceneBeatDefinition }) => (
|
|
||||||
<>
|
|
||||||
<StageCaption eyebrow="Agent handoff" title={scene.title}>
|
|
||||||
<p>{beat.caption}</p>
|
|
||||||
</StageCaption>
|
|
||||||
<p className="scene-body__evidence">{scene.evidencePointer}</p>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
|
|
||||||
const assertNever = (value: never): never => {
|
const assertNever = (value: never): never => {
|
||||||
throw new Error(`Unexpected view: ${value}`);
|
throw new Error(`Unexpected view: ${value}`);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { render, screen } from "@testing-library/react";
|
import { cleanup, render, screen } from "@testing-library/react";
|
||||||
import { describe, expect, it } from "vitest";
|
import { afterEach, describe, expect, it } from "vitest";
|
||||||
import { findBeat, findScene } from "../storyboard.js";
|
import { findBeat, findScene } from "../storyboard.js";
|
||||||
import { AgentHandoffScene } from "./AgentHandoffScene.js";
|
import { AgentHandoffScene } from "./AgentHandoffScene.js";
|
||||||
|
|
||||||
@@ -11,6 +11,8 @@ const renderBeat = (beatId: "request" | "handoff") => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
describe("AgentHandoffScene", () => {
|
describe("AgentHandoffScene", () => {
|
||||||
|
afterEach(cleanup);
|
||||||
|
|
||||||
it("renders a log region named prepared authoring conversation", () => {
|
it("renders a log region named prepared authoring conversation", () => {
|
||||||
renderBeat("request");
|
renderBeat("request");
|
||||||
expect(screen.getByRole("log", { name: "prepared authoring conversation" })).toBeInTheDocument();
|
expect(screen.getByRole("log", { name: "prepared authoring conversation" })).toBeInTheDocument();
|
||||||
@@ -32,6 +34,13 @@ describe("AgentHandoffScene", () => {
|
|||||||
expect(assistantMessages.length).toBeGreaterThanOrEqual(2);
|
expect(assistantMessages.length).toBeGreaterThanOrEqual(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("interleaves prepared workflow tool groups with the handoff conversation", () => {
|
||||||
|
renderBeat("handoff");
|
||||||
|
expect(screen.getAllByText(/runWorkflowCommand/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", () => {
|
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();
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
import { useMemo } from "react";
|
import { AuthoringConversation } from "./AuthoringConversation.js";
|
||||||
import { agentTextMessage } from "../../demo/agent/events.js";
|
|
||||||
import { AssistantOperatorThread } from "../chat/AssistantOperatorThread.js";
|
|
||||||
import { projectPreparedAuthoring } from "./authoring-recording.js";
|
|
||||||
import type { SceneBeatDefinition, SceneDefinition } from "../storyboard.js";
|
import type { SceneBeatDefinition, SceneDefinition } from "../storyboard.js";
|
||||||
|
|
||||||
type AgentHandoffSceneProps = {
|
type AgentHandoffSceneProps = {
|
||||||
@@ -9,11 +6,6 @@ type AgentHandoffSceneProps = {
|
|||||||
readonly beat: SceneBeatDefinition;
|
readonly beat: SceneBeatDefinition;
|
||||||
};
|
};
|
||||||
|
|
||||||
const requestMessages = [
|
|
||||||
agentTextMessage("handoff-user-1", "user", "We need to prepare a report workflow for the lda_report scenario. Use the available CLI tools to inspect, author, and deploy it."),
|
|
||||||
agentTextMessage("handoff-assistant-1", "assistant", "Let me inspect the available sources, capabilities, and schemas first."),
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Full-screen prepared-authoring conversation for Scene 8.
|
* Full-screen prepared-authoring conversation for Scene 8.
|
||||||
*
|
*
|
||||||
@@ -23,28 +15,13 @@ const requestMessages = [
|
|||||||
* 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 messages = useMemo(() => {
|
const phase = beat.id === "handoff" ? "deployment" : "discover";
|
||||||
if (beat.id === "handoff") {
|
|
||||||
const recording = projectPreparedAuthoring();
|
|
||||||
const result: ReturnType<typeof agentTextMessage>[] = [];
|
|
||||||
let index = 0;
|
|
||||||
for (const phase of recording) {
|
|
||||||
for (const turn of phase.conversation) {
|
|
||||||
result.push(
|
|
||||||
agentTextMessage(`msg-${index++}`, turn.role, turn.text),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
return requestMessages;
|
|
||||||
}, [beat.id]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AssistantOperatorThread
|
<AuthoringConversation
|
||||||
mode="full"
|
throughPhase={phase}
|
||||||
messages={messages}
|
activePhase={phase}
|
||||||
ariaLabel="prepared authoring conversation"
|
surface="stage"
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,92 +0,0 @@
|
|||||||
import { cleanup, render, screen } from "@testing-library/react";
|
|
||||||
import userEvent from "@testing-library/user-event";
|
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
||||||
import { AuthoringTracePanel } from "./AuthoringTracePanel.js";
|
|
||||||
import { type AuthoringPhaseId } from "./authoring-recording.js";
|
|
||||||
|
|
||||||
afterEach(() => cleanup());
|
|
||||||
|
|
||||||
const renderPanel = (
|
|
||||||
phase: AuthoringPhaseId,
|
|
||||||
open = true,
|
|
||||||
onOpen = vi.fn(),
|
|
||||||
onClose = vi.fn(),
|
|
||||||
) => render(
|
|
||||||
<AuthoringTracePanel phase={phase} open={open} onOpen={onOpen} onClose={onClose} />,
|
|
||||||
);
|
|
||||||
|
|
||||||
describe("AuthoringTracePanel", () => {
|
|
||||||
it("renders an Agent trace trigger", () => {
|
|
||||||
renderPanel("discover", false);
|
|
||||||
expect(screen.getByRole("button", { name: "Agent trace" })).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("calls onOpen when trigger is clicked", async () => {
|
|
||||||
const user = userEvent.setup();
|
|
||||||
const onOpen = vi.fn();
|
|
||||||
renderPanel("discover", false, onOpen);
|
|
||||||
await user.click(screen.getByRole("button", { name: "Agent trace" }));
|
|
||||||
expect(onOpen).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("shows the dialog overlay when open is true", () => {
|
|
||||||
renderPanel("discover", true);
|
|
||||||
expect(screen.getByRole("dialog", { name: "Authoring trace" })).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("hides the dialog overlay when open is false", () => {
|
|
||||||
renderPanel("discover", false);
|
|
||||||
expect(screen.queryByRole("dialog", { name: "Authoring trace" })).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders the selected phase expanded in the dialog", () => {
|
|
||||||
renderPanel("draft", true);
|
|
||||||
expect(screen.getByText("Draft")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders all five phases in the panel", () => {
|
|
||||||
renderPanel("discover", true);
|
|
||||||
expect(screen.getByText("Discover")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("Draft")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("Validate")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("Artifact")).toBeInTheDocument();
|
|
||||||
expect(screen.getByText("Deployment")).toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("renders command blocks in the selected phase", () => {
|
|
||||||
renderPanel("validate", true);
|
|
||||||
const commands = screen.getAllByText(/wf bind|wf validate/i);
|
|
||||||
expect(commands.length).toBeGreaterThanOrEqual(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("closes the dialog when Escape is pressed", async () => {
|
|
||||||
const user = userEvent.setup();
|
|
||||||
const onClose = vi.fn();
|
|
||||||
render(<AuthoringTracePanel phase="discover" open={true} onOpen={vi.fn()} onClose={onClose} />);
|
|
||||||
await user.keyboard("{Escape}");
|
|
||||||
expect(onClose).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("closes the dialog when backdrop is clicked", async () => {
|
|
||||||
const user = userEvent.setup();
|
|
||||||
const onClose = vi.fn();
|
|
||||||
render(<AuthoringTracePanel phase="discover" open={true} onOpen={vi.fn()} onClose={onClose} />);
|
|
||||||
const backdrop = document.querySelector(".authoring-trace-panel__backdrop");
|
|
||||||
expect(backdrop).toBeInTheDocument();
|
|
||||||
await user.click(backdrop!);
|
|
||||||
expect(onClose).toHaveBeenCalledTimes(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("restores focus to the trigger after closing", async () => {
|
|
||||||
const user = userEvent.setup();
|
|
||||||
const onClose = vi.fn();
|
|
||||||
const { rerender } = render(
|
|
||||||
<AuthoringTracePanel phase="discover" open={true} onOpen={vi.fn()} onClose={onClose} />,
|
|
||||||
);
|
|
||||||
await user.keyboard("{Escape}");
|
|
||||||
rerender(
|
|
||||||
<AuthoringTracePanel phase="discover" open={false} onOpen={vi.fn()} onClose={onClose} />,
|
|
||||||
);
|
|
||||||
expect(screen.getByRole("button", { name: "Agent trace" })).toHaveFocus();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
|
||||||
import { projectPreparedAuthoringPhase } from "./authoring-projection.js";
|
|
||||||
import type { AuthoringPhaseId } from "./authoring-recording.js";
|
|
||||||
|
|
||||||
type AuthoringTracePanelProps = {
|
|
||||||
readonly phase: AuthoringPhaseId;
|
|
||||||
readonly open: boolean;
|
|
||||||
readonly onOpen: () => void;
|
|
||||||
readonly onClose: () => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
const allPhases: readonly AuthoringPhaseId[] = [
|
|
||||||
"discover",
|
|
||||||
"draft",
|
|
||||||
"validate",
|
|
||||||
"artifact",
|
|
||||||
"deployment",
|
|
||||||
];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Dialog-style overlay showing the prepared authoring trace.
|
|
||||||
*
|
|
||||||
* The trigger button is always rendered so it can serve as the focus anchor
|
|
||||||
* after the dialog closes (the DOM node persists across open/close cycles;
|
|
||||||
* removing it would lose the focus target).
|
|
||||||
*/
|
|
||||||
export const AuthoringTracePanel = ({ phase, open, onOpen, onClose }: AuthoringTracePanelProps) => {
|
|
||||||
const triggerRef = useRef<HTMLButtonElement>(null);
|
|
||||||
const [expandedPhase, setExpandedPhase] = useState<AuthoringPhaseId>(phase);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!open) {
|
|
||||||
triggerRef.current?.focus();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const handleKey = (e: KeyboardEvent) => {
|
|
||||||
if (e.key === "Escape") onClose();
|
|
||||||
};
|
|
||||||
document.addEventListener("keydown", handleKey);
|
|
||||||
return () => document.removeEventListener("keydown", handleKey);
|
|
||||||
}, [open, onClose]);
|
|
||||||
|
|
||||||
const togglePhase = useCallback((p: AuthoringPhaseId) => {
|
|
||||||
setExpandedPhase((prev) => (prev === p ? prev : p));
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
ref={triggerRef}
|
|
||||||
type="button"
|
|
||||||
className="authoring-trace-panel__trigger"
|
|
||||||
onClick={onOpen}
|
|
||||||
aria-haspopup="dialog"
|
|
||||||
aria-expanded={open}
|
|
||||||
>
|
|
||||||
Agent trace
|
|
||||||
</button>
|
|
||||||
{open && (
|
|
||||||
<>
|
|
||||||
<div className="authoring-trace-panel__backdrop" onClick={onClose} />
|
|
||||||
<div
|
|
||||||
className="authoring-trace-panel__dialog"
|
|
||||||
role="dialog"
|
|
||||||
aria-label="Authoring trace"
|
|
||||||
aria-modal="true"
|
|
||||||
>
|
|
||||||
<div className="authoring-trace-panel__header">
|
|
||||||
<strong>Authoring trace</strong>
|
|
||||||
<button type="button" onClick={onClose} aria-label="Close trace">×</button>
|
|
||||||
</div>
|
|
||||||
<div className="authoring-trace-panel__phases">
|
|
||||||
{allPhases.map((p) => {
|
|
||||||
const projection = projectPreparedAuthoringPhase(p);
|
|
||||||
const isExpanded = p === expandedPhase;
|
|
||||||
return (
|
|
||||||
<div key={p} className="authoring-trace-panel__phase" data-expanded={isExpanded}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="authoring-trace-panel__phase-header"
|
|
||||||
onClick={() => togglePhase(p)}
|
|
||||||
aria-expanded={isExpanded}
|
|
||||||
>
|
|
||||||
<span>{projection.label}</span>
|
|
||||||
<small>{projection.commands.length} commands</small>
|
|
||||||
</button>
|
|
||||||
{isExpanded && (
|
|
||||||
<div className="authoring-trace-panel__commands">
|
|
||||||
{projection.commands.map((cmd, i) => (
|
|
||||||
<div key={`${p}-${i}`} className="authoring-trace-panel__command">
|
|
||||||
<div className="authoring-trace-panel__command-line">
|
|
||||||
<code>$ {cmd.command}</code>
|
|
||||||
</div>
|
|
||||||
<p className="authoring-trace-panel__command-summary">{cmd.summary}</p>
|
|
||||||
<span
|
|
||||||
className="authoring-trace-panel__command-result"
|
|
||||||
data-result={cmd.result}
|
|
||||||
>
|
|
||||||
{cmd.result === "success" ? "✓" : "⚡"} {cmd.result}
|
|
||||||
</span>
|
|
||||||
{cmd.detail && (
|
|
||||||
<pre className="authoring-trace-panel__command-detail">
|
|
||||||
{cmd.detail}
|
|
||||||
</pre>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
+25
-17
@@ -1,6 +1,5 @@
|
|||||||
import { cleanup, render, screen } from "@testing-library/react";
|
import { cleanup, render, screen } from "@testing-library/react";
|
||||||
import userEvent from "@testing-library/user-event";
|
import { afterEach, describe, expect, it } from "vitest";
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
||||||
import { findBeat, findScene } from "../storyboard.js";
|
import { findBeat, findScene } from "../storyboard.js";
|
||||||
import { PreparedAuthoringLifecycleScene } from "./PreparedAuthoringLifecycleScene.js";
|
import { PreparedAuthoringLifecycleScene } from "./PreparedAuthoringLifecycleScene.js";
|
||||||
|
|
||||||
@@ -35,30 +34,39 @@ describe("PreparedAuthoringLifecycleScene", () => {
|
|||||||
it("artifact shows immutable ID and version", () => {
|
it("artifact shows immutable ID and version", () => {
|
||||||
renderBeat("artifact");
|
renderBeat("artifact");
|
||||||
expect(screen.getByText("Artifact")).toBeInTheDocument();
|
expect(screen.getByText("Artifact")).toBeInTheDocument();
|
||||||
expect(screen.getByText(/art_x9y8z7/i)).toBeInTheDocument();
|
expect(screen.getAllByText(/lda_report_case_study/i).length).toBeGreaterThanOrEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("deployment shows bindings and validation", () => {
|
it("deployment shows bindings and validation", () => {
|
||||||
renderBeat("deployment");
|
renderBeat("deployment");
|
||||||
expect(screen.getByText("Deployment")).toBeInTheDocument();
|
expect(screen.getAllByText("Deployment").length).toBeGreaterThanOrEqual(1);
|
||||||
expect(screen.getByText(/dep_m4n5p6/i)).toBeInTheDocument();
|
expect(screen.getAllByText(/lda_report_case_study\.default/i).length).toBeGreaterThanOrEqual(1);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders an Agent trace trigger", () => {
|
it.each([
|
||||||
renderBeat("discover");
|
["discover", "discovery evidence"],
|
||||||
expect(screen.getByRole("button", { name: "Agent trace" })).toBeInTheDocument();
|
["draft", "draft graph evidence"],
|
||||||
|
["validate", "validation repair evidence"],
|
||||||
|
["artifact", "artifact evidence"],
|
||||||
|
["deployment", "deployment binding evidence"],
|
||||||
|
] as const)("renders %s as the primary phase visual", (beatId, label) => {
|
||||||
|
renderBeat(beatId);
|
||||||
|
expect(screen.getByRole("region", { name: label })).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("trace is initially closed", () => {
|
it("keeps the same conversation as a synchronized bottom dock", () => {
|
||||||
renderBeat("discover");
|
|
||||||
expect(screen.queryByRole("dialog", { name: "Authoring trace" })).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
|
|
||||||
it("opens the trace panel when Agent trace is clicked", async () => {
|
|
||||||
const user = userEvent.setup();
|
|
||||||
renderBeat("draft");
|
renderBeat("draft");
|
||||||
await user.click(screen.getByRole("button", { name: "Agent trace" }));
|
const chat = screen.getByRole("log", { name: "prepared authoring conversation" });
|
||||||
expect(screen.getByRole("dialog", { name: "Authoring trace" })).toBeInTheDocument();
|
expect(chat).toHaveAttribute("data-surface", "dock");
|
||||||
|
expect(screen.getByRole("button", { name: /draft.*3 tool calls/i }))
|
||||||
|
.toHaveAttribute("aria-expanded", "true");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not render the obsolete trace modal or receipt", () => {
|
||||||
|
renderBeat("validate");
|
||||||
|
expect(screen.queryByRole("button", { name: "Agent trace" })).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByRole("dialog", { name: "Authoring trace" })).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByLabelText("prepared authoring receipt")).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders a compact orientation rail", () => {
|
it("renders a compact orientation rail", () => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useState } from "react";
|
|
||||||
import { projectPreparedAuthoringPhase } from "./authoring-projection.js";
|
import { projectPreparedAuthoringPhase } from "./authoring-projection.js";
|
||||||
import { AuthoringTracePanel } from "./AuthoringTracePanel.js";
|
import { AuthoringConversation } from "./AuthoringConversation.js";
|
||||||
|
import { AuthoringPhaseVisual } from "./AuthoringPhaseVisual.js";
|
||||||
import type { AuthoringPhaseId } from "./authoring-recording.js";
|
import type { AuthoringPhaseId } from "./authoring-recording.js";
|
||||||
import type { SceneBeatDefinition, SceneDefinition } from "../storyboard.js";
|
import type { SceneBeatDefinition, SceneDefinition } from "../storyboard.js";
|
||||||
import { StageCaption } from "../StageCaption.js";
|
import { StageCaption } from "../StageCaption.js";
|
||||||
@@ -22,15 +22,11 @@ const phases: readonly { readonly id: AuthoringPhaseId; readonly label: string }
|
|||||||
* Scene 9 — Prepared workflow authoring lifecycle.
|
* Scene 9 — Prepared workflow authoring lifecycle.
|
||||||
*
|
*
|
||||||
* Each beat shows a compact orientation rail and one dominant phase projection
|
* Each beat shows a compact orientation rail and one dominant phase projection
|
||||||
* sourced from the prepared authoring recording. A persistent "Agent trace"
|
* sourced from the prepared authoring recording. The same Scene 8 conversation
|
||||||
* trigger opens the AuthoringTracePanel overlay.
|
* remains below the canvas as a beat-synchronized assistant dock.
|
||||||
*
|
|
||||||
* The receipt bridges Scene 8's full conversation and Scene 9's separate
|
|
||||||
* trace panel without morphing runtime-owned components.
|
|
||||||
*/
|
*/
|
||||||
export const PreparedAuthoringLifecycleScene = ({ scene, beat }: PreparedAuthoringLifecycleSceneProps) => {
|
export const PreparedAuthoringLifecycleScene = ({ scene, beat }: PreparedAuthoringLifecycleSceneProps) => {
|
||||||
const beatId = beat.id as AuthoringPhaseId;
|
const beatId = beat.id as AuthoringPhaseId;
|
||||||
const [traceOpen, setTraceOpen] = useState(false);
|
|
||||||
const projection = projectPreparedAuthoringPhase(beatId);
|
const projection = projectPreparedAuthoringPhase(beatId);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -51,30 +47,17 @@ export const PreparedAuthoringLifecycleScene = ({ scene, beat }: PreparedAuthori
|
|||||||
))}
|
))}
|
||||||
</ol>
|
</ol>
|
||||||
|
|
||||||
<article className="prepared-lifecycle-scene__projection">
|
<article className="prepared-lifecycle-scene__projection" key={beatId}>
|
||||||
<div className="prepared-lifecycle-scene__summary">
|
<AuthoringPhaseVisual projection={projection} />
|
||||||
<p>{projection.summary}</p>
|
|
||||||
</div>
|
|
||||||
<div className="prepared-lifecycle-scene__commands">
|
|
||||||
{projection.commands.map((cmd, i) => (
|
|
||||||
<div key={i} className="prepared-lifecycle-scene__command">
|
|
||||||
<code>$ {cmd.command}</code>
|
|
||||||
<span className="prepared-lifecycle-scene__command-result" data-result={cmd.result}>
|
|
||||||
{cmd.result === "success" ? "✓" : "⚡"} {cmd.result}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</article>
|
</article>
|
||||||
</section>
|
<div className="prepared-lifecycle-scene__dock">
|
||||||
<div className="prepared-lifecycle-scene__receipt">
|
<AuthoringConversation
|
||||||
<AuthoringTracePanel
|
throughPhase={beatId}
|
||||||
phase={beatId}
|
activePhase={beatId}
|
||||||
open={traceOpen}
|
surface="dock"
|
||||||
onOpen={() => setTraceOpen(true)}
|
|
||||||
onClose={() => setTraceOpen(false)}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { cleanup, render, screen } from "@testing-library/react";
|
import { cleanup, render, screen, waitFor } from "@testing-library/react";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||||
import type { AgentMessage } from "../../demo/agent/events.js";
|
import type { AgentMessage } from "../../demo/agent/events.js";
|
||||||
@@ -140,6 +140,36 @@ describe("AssistantOperatorThread", () => {
|
|||||||
.toHaveAttribute("aria-expanded", "true");
|
.toHaveAttribute("aria-expanded", "true");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("scrolls the active authoring group into the dock viewport", async () => {
|
||||||
|
const setScrollTop = vi.fn();
|
||||||
|
Object.defineProperty(HTMLDivElement.prototype, "scrollTop", {
|
||||||
|
configurable: true,
|
||||||
|
get: () => 0,
|
||||||
|
set: setScrollTop,
|
||||||
|
});
|
||||||
|
const messages: ReadonlyArray<AgentMessage> = [
|
||||||
|
{
|
||||||
|
id: "authoring-draft-tools",
|
||||||
|
role: "assistant",
|
||||||
|
parts: [
|
||||||
|
{ type: "tool-call", call: { id: "authoring-draft-command-0", name: "runWorkflowCommand", input: {} } },
|
||||||
|
{ type: "tool-result", result: { callId: "authoring-draft-command-0", name: "runWorkflowCommand", status: "success", output: {} } },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
render(
|
||||||
|
<AssistantOperatorThread
|
||||||
|
mode="dock"
|
||||||
|
surface="dock"
|
||||||
|
messages={messages}
|
||||||
|
activeToolGroupId="authoring-draft"
|
||||||
|
/>,
|
||||||
|
);
|
||||||
|
|
||||||
|
await waitFor(() => expect(setScrollTop).toHaveBeenCalledWith(0));
|
||||||
|
});
|
||||||
|
|
||||||
it("renders structured tool results through the generated fallback result slot", () => {
|
it("renders structured tool results through the generated fallback result slot", () => {
|
||||||
const messages: ReadonlyArray<AgentMessage> = [
|
const messages: ReadonlyArray<AgentMessage> = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useCallback, useEffect, useMemo, useState, type ReactNode } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react";
|
||||||
import type { ToolCallMessagePartStatus } from "@assistant-ui/react";
|
import type { ToolCallMessagePartStatus } from "@assistant-ui/react";
|
||||||
import {
|
import {
|
||||||
ToolFallbackArgs,
|
ToolFallbackArgs,
|
||||||
@@ -190,6 +190,7 @@ const AssistantMessageBody = ({
|
|||||||
rendered.push(
|
rendered.push(
|
||||||
<ToolGroupRoot
|
<ToolGroupRoot
|
||||||
key={`tool-group-${toolRunStart}`}
|
key={`tool-group-${toolRunStart}`}
|
||||||
|
data-tool-group-id={groupId}
|
||||||
{...(phaseLabel
|
{...(phaseLabel
|
||||||
? {
|
? {
|
||||||
open: openToolGroups.has(groupId),
|
open: openToolGroups.has(groupId),
|
||||||
@@ -252,6 +253,7 @@ export const AssistantOperatorThread = ({
|
|||||||
activeToolGroupId,
|
activeToolGroupId,
|
||||||
}: AssistantOperatorThreadProps) => {
|
}: AssistantOperatorThreadProps) => {
|
||||||
const projected = useMemo(() => projectAgentMessagesForAssistant(messages), [messages]);
|
const projected = useMemo(() => projectAgentMessagesForAssistant(messages), [messages]);
|
||||||
|
const viewportRef = useRef<HTMLDivElement>(null);
|
||||||
const [openToolGroups, setOpenToolGroups] = useState<ReadonlySet<string>>(
|
const [openToolGroups, setOpenToolGroups] = useState<ReadonlySet<string>>(
|
||||||
() => new Set(activeToolGroupId ? [activeToolGroupId] : []),
|
() => new Set(activeToolGroupId ? [activeToolGroupId] : []),
|
||||||
);
|
);
|
||||||
@@ -260,6 +262,21 @@ export const AssistantOperatorThread = ({
|
|||||||
if (activeToolGroupId) setOpenToolGroups(new Set([activeToolGroupId]));
|
if (activeToolGroupId) setOpenToolGroups(new Set([activeToolGroupId]));
|
||||||
}, [activeToolGroupId]);
|
}, [activeToolGroupId]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!activeToolGroupId) return;
|
||||||
|
// The same transcript can be much taller than the Scene 9 dock. Keep the
|
||||||
|
// beat-owned group visible without maintaining a second scroll-state model.
|
||||||
|
const viewport = viewportRef.current;
|
||||||
|
const activeGroup = viewport
|
||||||
|
?.querySelector<HTMLElement>(`[data-tool-group-id="${activeToolGroupId}"]`);
|
||||||
|
if (!viewport || !activeGroup) return;
|
||||||
|
const top = Math.max(
|
||||||
|
0,
|
||||||
|
activeGroup.offsetTop + activeGroup.offsetHeight - viewport.clientHeight,
|
||||||
|
);
|
||||||
|
viewport.scrollTop = top;
|
||||||
|
}, [activeToolGroupId, projected]);
|
||||||
|
|
||||||
const setToolGroupOpen = useCallback((groupId: string, open: boolean) => {
|
const setToolGroupOpen = useCallback((groupId: string, open: boolean) => {
|
||||||
setOpenToolGroups((current) => {
|
setOpenToolGroups((current) => {
|
||||||
const next = new Set(current);
|
const next = new Set(current);
|
||||||
@@ -283,7 +300,7 @@ export const AssistantOperatorThread = ({
|
|||||||
aria-label={ariaLabel}
|
aria-label={ariaLabel}
|
||||||
>
|
>
|
||||||
<div className="assistant-thread">
|
<div className="assistant-thread">
|
||||||
<div className="assistant-thread__viewport">
|
<div ref={viewportRef} className="assistant-thread__viewport">
|
||||||
{projected.map((message) => {
|
{projected.map((message) => {
|
||||||
if (message.role === "user") {
|
if (message.role === "user") {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -2626,159 +2626,17 @@
|
|||||||
max-height: 100%;
|
max-height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
Authoring Trace Panel — dialog overlay with backdrop.
|
|
||||||
The backdrop fills the viewport behind the dialog and closes on click.
|
|
||||||
The trigger persists in the DOM as a focus anchor.
|
|
||||||
*/
|
|
||||||
.authoring-trace-panel__trigger {
|
|
||||||
border: 1px solid var(--accent-cyan);
|
|
||||||
border-radius: 0.45rem;
|
|
||||||
background: color-mix(in oklch, var(--accent-cyan) 11%, transparent);
|
|
||||||
color: var(--accent-cyan);
|
|
||||||
font: 600 0.7rem/1 var(--font-interface);
|
|
||||||
letter-spacing: 0.03em;
|
|
||||||
padding: 0.3rem 0.55rem;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.authoring-trace-panel__backdrop {
|
|
||||||
position: fixed;
|
|
||||||
inset: 0;
|
|
||||||
z-index: 100;
|
|
||||||
background: color-mix(in oklch, #000 52%, transparent);
|
|
||||||
}
|
|
||||||
|
|
||||||
.authoring-trace-panel__dialog {
|
|
||||||
position: fixed;
|
|
||||||
inset: 10%;
|
|
||||||
z-index: 101;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
background: var(--stage-surface);
|
|
||||||
border: 1px solid var(--stage-line);
|
|
||||||
border-radius: 0.65rem;
|
|
||||||
overflow: hidden;
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.authoring-trace-panel__header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 0.65rem 0.85rem;
|
|
||||||
border-bottom: 1px solid var(--stage-line);
|
|
||||||
font: 700 0.85rem/1 var(--font-interface);
|
|
||||||
}
|
|
||||||
|
|
||||||
.authoring-trace-panel__header button {
|
|
||||||
border: none;
|
|
||||||
background: transparent;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: 1.1rem;
|
|
||||||
cursor: pointer;
|
|
||||||
padding: 0.2rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.authoring-trace-panel__phases {
|
|
||||||
flex: 1;
|
|
||||||
overflow: auto;
|
|
||||||
padding: 0.5rem;
|
|
||||||
scrollbar-width: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.authoring-trace-panel__phase {
|
|
||||||
border: 1px solid color-mix(in oklch, var(--stage-line) 70%, transparent);
|
|
||||||
border-radius: 0.45rem;
|
|
||||||
margin-bottom: 0.35rem;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.authoring-trace-panel__phase-header {
|
|
||||||
width: 100%;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 0.5rem 0.65rem;
|
|
||||||
border: none;
|
|
||||||
background: color-mix(in oklch, var(--stage-surface) 65%, var(--stage-inset));
|
|
||||||
color: var(--text-primary);
|
|
||||||
font: 600 0.78rem/1 var(--font-interface);
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.authoring-trace-panel__phase-header small {
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: 0.65rem;
|
|
||||||
font-weight: 400;
|
|
||||||
}
|
|
||||||
|
|
||||||
.authoring-trace-panel__commands {
|
|
||||||
padding: 0.45rem;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.4rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.authoring-trace-panel__command {
|
|
||||||
padding: 0.4rem 0.55rem;
|
|
||||||
background: color-mix(in oklch, var(--stage-inset) 70%, transparent);
|
|
||||||
border-radius: 0.35rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.authoring-trace-panel__command-line code {
|
|
||||||
display: block;
|
|
||||||
font-family: var(--font-evidence);
|
|
||||||
font-size: 0.7rem;
|
|
||||||
line-height: 1.4;
|
|
||||||
color: var(--accent-cyan);
|
|
||||||
white-space: pre-wrap;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
.authoring-trace-panel__command-summary {
|
|
||||||
margin: 0.2rem 0;
|
|
||||||
font-size: 0.72rem;
|
|
||||||
line-height: 1.3;
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.authoring-trace-panel__command-result {
|
|
||||||
font-size: 0.65rem;
|
|
||||||
font-weight: 600;
|
|
||||||
letter-spacing: 0.02em;
|
|
||||||
text-transform: uppercase;
|
|
||||||
}
|
|
||||||
|
|
||||||
.authoring-trace-panel__command-result[data-result="success"] {
|
|
||||||
color: var(--accent-green);
|
|
||||||
}
|
|
||||||
|
|
||||||
.authoring-trace-panel__command-result[data-result="diagnostic"] {
|
|
||||||
color: var(--accent-yellow);
|
|
||||||
}
|
|
||||||
|
|
||||||
.authoring-trace-panel__command-detail {
|
|
||||||
margin: 0.25rem 0 0;
|
|
||||||
padding: 0.3rem;
|
|
||||||
background: color-mix(in oklch, #000 25%, transparent);
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
font-family: var(--font-evidence);
|
|
||||||
font-size: 0.65rem;
|
|
||||||
line-height: 1.3;
|
|
||||||
white-space: pre-wrap;
|
|
||||||
overflow-x: auto;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
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: flex;
|
display: grid;
|
||||||
flex-direction: column;
|
grid-template-rows: auto minmax(0, 1fr) 11rem;
|
||||||
gap: 0.75rem;
|
gap: 0.55rem;
|
||||||
|
min-height: 0;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.prepared-lifecycle-scene__rail {
|
.prepared-lifecycle-scene__rail {
|
||||||
@@ -2810,63 +2668,49 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.prepared-lifecycle-scene__projection {
|
.prepared-lifecycle-scene__projection {
|
||||||
display: flex;
|
min-height: 0;
|
||||||
flex-direction: column;
|
padding: 0.9rem 1rem;
|
||||||
gap: 0.55rem;
|
|
||||||
padding: 0.55rem 0.65rem;
|
|
||||||
background: color-mix(in oklch, var(--stage-surface) 60%, var(--stage-inset));
|
background: color-mix(in oklch, var(--stage-surface) 60%, var(--stage-inset));
|
||||||
border: 1px solid var(--stage-line);
|
border: 1px solid var(--stage-line);
|
||||||
border-radius: 0.45rem;
|
border-radius: 0.65rem;
|
||||||
|
overflow: auto;
|
||||||
|
animation: authoring-canvas-enter 180ms cubic-bezier(0.22, 1, 0.36, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
.prepared-lifecycle-scene__summary p {
|
.stage-caption:has(+ .prepared-lifecycle-scene) {
|
||||||
|
margin-bottom: 0.55rem;
|
||||||
|
padding: 0.75rem 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-caption:has(+ .prepared-lifecycle-scene) h1 {
|
||||||
|
margin-block: 0.15rem 0.25rem;
|
||||||
|
font-size: 1.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stage-caption:has(+ .prepared-lifecycle-scene) p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: 0.78rem;
|
|
||||||
line-height: 1.4;
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.prepared-lifecycle-scene__proof {
|
.prepared-lifecycle-scene__dock {
|
||||||
display: grid;
|
min-height: 0;
|
||||||
grid-template-columns: repeat(auto-fit, minmax(12rem, 1fr));
|
overflow: hidden;
|
||||||
gap: 0.5rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.prepared-lifecycle-scene__proof code {
|
@keyframes authoring-canvas-enter {
|
||||||
min-width: 0;
|
from { opacity: 0; }
|
||||||
padding: 0.7rem;
|
to { opacity: 1; }
|
||||||
border: 1px solid color-mix(in oklch, var(--accent-cyan) 35%, var(--stage-line));
|
|
||||||
border-radius: 0.35rem;
|
|
||||||
background: color-mix(in oklch, var(--stage-inset) 76%, black);
|
|
||||||
color: var(--accent-cyan);
|
|
||||||
font: 600 0.72rem/1.3 var(--font-evidence);
|
|
||||||
overflow-wrap: anywhere;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.prepared-lifecycle-scene__receipt {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
display: flex;
|
.prepared-lifecycle-scene__projection {
|
||||||
align-items: center;
|
animation: none;
|
||||||
justify-content: center;
|
|
||||||
gap: 0.5rem;
|
|
||||||
padding: 0.45rem 0.65rem;
|
|
||||||
border: 1px solid color-mix(in oklch, var(--stage-line) 80%, transparent);
|
|
||||||
border-radius: 0.45rem;
|
|
||||||
background: color-mix(in oklch, var(--stage-inset) 72%, transparent);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.prepared-lifecycle-scene__receipt strong {
|
|
||||||
color: var(--text-primary);
|
|
||||||
font: 700 0.72rem/1 var(--font-interface);
|
|
||||||
}
|
|
||||||
|
|
||||||
.prepared-lifecycle-scene__receipt > span {
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font: 0.66rem/1 var(--font-evidence);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.authoring-visual {
|
.authoring-visual {
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
background: transparent;
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -3081,7 +2925,7 @@
|
|||||||
.authoring-bindings__rows {
|
.authoring-bindings__rows {
|
||||||
display: grid;
|
display: grid;
|
||||||
align-content: center;
|
align-content: center;
|
||||||
gap: 0.55rem;
|
gap: 0.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.authoring-bindings__rows > div {
|
.authoring-bindings__rows > div {
|
||||||
@@ -3089,8 +2933,8 @@
|
|||||||
grid-template-columns: minmax(12rem, 1fr) auto minmax(12rem, 1fr);
|
grid-template-columns: minmax(12rem, 1fr) auto minmax(12rem, 1fr);
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.8rem;
|
gap: 0.8rem;
|
||||||
min-height: 2.9rem;
|
min-height: 2.25rem;
|
||||||
padding-inline: 0.8rem;
|
padding: 0.35rem 0.8rem;
|
||||||
background: var(--stage-inset);
|
background: var(--stage-inset);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ describe("defense storyboard catalog", () => {
|
|||||||
expect(mainScenes.slice(0, 3).every((scene) => scene.stageTheme === "paper")).toBe(true);
|
expect(mainScenes.slice(0, 3).every((scene) => scene.stageTheme === "paper")).toBe(true);
|
||||||
expect(mainScenes.slice(3, 12).every((scene) => scene.stageTheme === "night")).toBe(true);
|
expect(mainScenes.slice(3, 12).every((scene) => scene.stageTheme === "night")).toBe(true);
|
||||||
expect(mainScenes.slice(12).every((scene) => scene.stageTheme === "paper")).toBe(true);
|
expect(mainScenes.slice(12).every((scene) => scene.stageTheme === "paper")).toBe(true);
|
||||||
expect(findBeat("agent-handoff", "request")?.chatMode).toBe("full");
|
expect(findBeat("agent-handoff", "request")?.chatMode).toBe("hidden");
|
||||||
expect(findBeat("resume-output-evidence", "trace")?.chatMode).toBe("hidden");
|
expect(findBeat("resume-output-evidence", "trace")?.chatMode).toBe("hidden");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -169,8 +169,10 @@ export const mainScenes = defineScenes([
|
|||||||
stageTheme: "night",
|
stageTheme: "night",
|
||||||
view: "agent",
|
view: "agent",
|
||||||
beats: [
|
beats: [
|
||||||
sceneBeat("request", "Operator request", "A thin agent interface receives the report request.", { chatMode: "full", chatTheme: "light" }),
|
// Scene 8 renders its own full-screen prepared transcript in the primary
|
||||||
sceneBeat("handoff", "Prepared operation", "The interface delegates durable work to lda.chat.", { chatMode: "full", chatTheme: "light" }),
|
// 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" }),
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user