feat: finish continuous authoring workspace

This commit is contained in:
lda
2026-07-11 08:35:29 +07:00 Verified
parent c76be621d9
commit ce90b6b8a9
13 changed files with 168 additions and 77 deletions
+7 -3
View File
@@ -173,7 +173,7 @@ Implementation order:
Astro remains an option, not the default next surface.
25. Completed: presentation agent authoring story creates a canonical prepared
authoring recording, Scene 8 as an authentic full-screen conversation,
Scene 9 as a 5-phase lifecycle with trace panel, and route-level coverage
Scene 9 as a 5-phase lifecycle with synchronized chat dock, and route-level coverage
that never calls workflow authoring RPC operations during Scene 9
navigation. Implementation:
[`presentation agent authoring story`](historical/superpowers/plans/2026-07-11-presentation-agent-authoring-story.md).
@@ -200,8 +200,12 @@ Presentation visual audit, July 10:
Scenes 8 and 9 are now implemented as authentic external-agent handoff and
factual prepared-authoring proof. The canonical recording, full-screen Scene 8,
Scene 9 lifecycle with trace panel, and route coverage are all in place. Plan:
[`presentation agent authoring story`](historical/superpowers/plans/2026-07-11-presentation-agent-authoring-story.md).
Scene 9 lifecycle with a synchronized chat dock, five factual phase visuals,
and route coverage are all in place. The obsolete receipt/trace modal was
removed. Plans:
[`presentation agent authoring story`](historical/superpowers/plans/2026-07-11-presentation-agent-authoring-story.md)
and
[`continuous authoring workspace`](historical/superpowers/plans/2026-07-11-continuous-authoring-workspace.md).
Recommended next visual slices:
@@ -1,5 +1,7 @@
# Continuous Authoring Workspace Implementation Plan
Status: Completed on 2026-07-11.
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Rebuild Scenes 8 and 9 as one continuous assistant conversation that synchronizes prepared tool activity with five factual workflow-authoring projections.
+22 -13
View File
@@ -170,8 +170,10 @@ both modes. Replay is visibly labeled and does not create real issues.
The console exposes `/present`, a 720p no-scroll defense compositor for the
prepared `lda_report_workflow` story. It renders a 14-scene, multi-beat
storyboard with an adaptive aspect-ratio canvas, stable stage regions, discussion
branches, act themes, a chat dock, and keyboard navigation.
storyboard (expanded from the original 12-scene defense plan; items 13 and 14
now cover evaluation and closing) with an adaptive aspect-ratio canvas, stable
stage regions, discussion branches, act themes, a chat dock, and keyboard
navigation.
The final presentation beats frame the evaluation as bounded evidence, make
claim boundaries and future work explicit, and end on the canonical defense
@@ -180,8 +182,11 @@ 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 conversation that establishes the
external agent handoff; Scene 9 breaks the prepared authoring into five phases
with an overlay trace panel. Neither scene calls workflow authoring RPC
operations — they consume deterministic prepared data. Scenes 10 through 12 use
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
12 use
the canonical replay to show run, interrupt, approval, resume, output, and
trace. Raw protocol payloads are available through the evidence receipt and
inspector.
@@ -192,17 +197,20 @@ prepared timeline agent and approval flow; a future AI SDK driver should target
`AgentMessagePart` / `TimelineAgent`-compatible events instead of replacing the
presentation timeline.
The key defense states are directly addressable:
The key deep-link-addressable defense states include:
- `/present#scene/workflow-demo/operation`
- `/present#scene/workflow-demo/graph`
- `/present#scene/workflow-demo/interrupt`
- `/present#scene/interrupt-evidence/approval`
- `/present#scene/agent-handoff/request` — Scene 8, prepared handoff request
- `/present#scene/agent-handoff/handoff` — Scene 8, agent accepts
- `/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
- `/present#scene/run-from-deployment/operation` — Scene 10, run operation
- `/present#scene/run-from-deployment/graph` — Scene 10, workflow graph
- `/present#scene/typed-human-boundary/approval` — Scene 11, typed approval
- `/present#scene/resume-output-evidence/resume` — Scene 12, resume proof
Legacy aliases from the earlier 12-scene plan (such as `workflow-demo` and
`interrupt-evidence`) are replaced by the IDs above and no longer resolve.
Scene 12 is factual by design. It projects the reviewed/live run into visible
workflow input, interrupt payload, resume decision, output, and trace facts.
@@ -324,12 +332,13 @@ workflow authoring RPC operations.
- **Scene 8 (Agent Handoff)**: a full-screen prepared conversation that
establishes the external agent interface. The `request` beat shows the
operator requesting a report workflow; the `handoff` beat reveals the full
completed conversation across all authoring phases.
completed conversation with prepared `wf` tool groups across all authoring
phases.
- **Scene 9 (Prepared Workflow Lifecycle)**: a five-phase lifecycle
(discover, draft, validate, artifact, deployment) with a compact phase rail
and one dominant command/result projection per beat. An overlay
`AuthoringTracePanel` surfaces the full agent trace for the active phase
without competing with the lifecycle projection.
and one dominant factual product projection per beat. The Scene 8 thread
remains visible as a compact bottom dock; its active tool group follows the
current beat. There is no detached trace modal or second transcript.
The authoring scenes consume deterministic prepared data and never call
workflow authoring RPCs — only `workflow.health` infrastructure pings are
+7 -5
View File
@@ -1,4 +1,6 @@
import type { AgentToolName } from "./tools.js";
import type { AgentToolName, PreparedAuthoringToolName } from "./tools.js";
export type AgentMessageToolName = AgentToolName | PreparedAuthoringToolName;
export type AgentRole = "user" | "assistant";
@@ -9,13 +11,13 @@ export type PresentationToolAction =
export type AgentToolCall = {
readonly id: string;
readonly name: AgentToolName;
readonly name: AgentMessageToolName;
readonly input: unknown;
};
export type AgentToolResult = {
readonly callId: string;
readonly name: AgentToolName;
readonly name: AgentMessageToolName;
readonly status: "success" | "failure";
readonly output: unknown;
};
@@ -56,7 +58,7 @@ export const agentTextMessage = (id: string, role: AgentRole, text: string): Age
export const agentToolCallPart = (
id: string,
name: AgentToolName,
name: AgentMessageToolName,
input: unknown,
): AgentMessagePart => ({
type: "tool-call",
@@ -65,7 +67,7 @@ export const agentToolCallPart = (
export const agentToolResultPart = (
callId: string,
name: AgentToolName,
name: AgentMessageToolName,
status: "success" | "failure",
output: unknown,
): AgentMessagePart => ({
+17
View File
@@ -5,6 +5,23 @@ export type WorkflowToolName =
| "readRunTrace"
| "runWorkflowCommand";
/** Names emitted by the prepared authoring recording for its operation trace. */
export type PreparedAuthoringToolName =
| "workflow.sources.list"
| "workflow.capabilities.list"
| "workflow.capabilities.inspect"
| "wf schema"
| "workflow.draft_workspaces.create_from_capability"
| "workflow.draft_workspaces.add_step_from_capability"
| "workflow.draft_workspaces.get"
| "workflow.draft_workspaces.validate"
| "workflow.draft_workspaces.set_step_output_map"
| "workflow.draft_workspaces.compile"
| "workflow.draft_workspaces.create_artifact"
| "workflow.artifacts.inspect"
| "workflow.deployments.save"
| "workflow.deployments.validate";
export type PresentationToolName =
| "selectWorkflowNode"
| "focusOperation"
@@ -66,9 +66,9 @@ describe("PresentationRoute", () => {
window.location.hash = "#scene/agent-handoff/request";
const { PresentationRoute } = await import("./PresentationRoute.js");
render(<PresentationRoute />);
expect(screen.getByRole("heading", { name: /Agent Handoff/i })).toBeInTheDocument();
expect(screen.getByRole("log", { name: "prepared authoring conversation" })).toBeInTheDocument();
await userEvent.keyboard("{ArrowRight}");
expect(await screen.findByText(/The interface delegates durable work to lda\.chat/i)).toBeInTheDocument();
expect(await screen.findByRole("button", { name: /deployment.*2 tool calls/i })).toBeInTheDocument();
});
it("renders audience progress chrome without rail or mode label", async () => {
@@ -373,8 +373,8 @@ describe("PresentationRoute", () => {
const { PresentationRoute } = await import("./PresentationRoute.js");
render(<PresentationRoute />);
expect(await screen.findByRole("heading", { name: "Agent Handoff" })).toBeInTheDocument();
expect(screen.getByText(/A thin agent interface receives the report request/)).toBeInTheDocument();
expect(await screen.findByRole("log", { name: "prepared authoring conversation" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: /discover.*4 tool calls/i })).toBeInTheDocument();
});
it("navigates to Scene 8 handoff beat via hash", async () => {
@@ -382,8 +382,8 @@ describe("PresentationRoute", () => {
const { PresentationRoute } = await import("./PresentationRoute.js");
render(<PresentationRoute />);
expect(await screen.findByRole("heading", { name: "Agent Handoff" })).toBeInTheDocument();
expect(screen.getByText(/The interface delegates durable work to lda\.chat/)).toBeInTheDocument();
expect(await screen.findByRole("log", { name: "prepared authoring conversation" })).toBeInTheDocument();
expect(screen.getByRole("button", { name: /deployment.*2 tool calls/i })).toBeInTheDocument();
});
it.each([
@@ -36,7 +36,7 @@ describe("AgentHandoffScene", () => {
it("interleaves prepared workflow tool groups with the handoff conversation", () => {
renderBeat("handoff");
expect(screen.getAllByText(/runWorkflowCommand/i).length).toBeGreaterThan(0);
expect(screen.getAllByText(/workflow\.deployments\.save/i).length).toBeGreaterThan(0);
expect(screen.getByRole("button", { name: /deployment.*2 tool calls/i }))
.toHaveAttribute("aria-expanded", "true");
});
@@ -16,7 +16,7 @@ describe("AuthoringConversation", () => {
expect(screen.getByRole("log", { name: "prepared authoring conversation" }))
.toHaveAttribute("data-surface", "stage");
expect(screen.getAllByText(/runWorkflowCommand/i).length).toBeGreaterThan(0);
expect(screen.getAllByText(/workflow\.deployments\.save/i).length).toBeGreaterThan(0);
expect(screen.getAllByText(/deployment/i).length).toBeGreaterThan(0);
});
@@ -15,7 +15,7 @@ type AuthoringPhaseVisualProps = {
};
const InventoryVisual = ({ visual }: { visual: Extract<AuthoringPhaseProjection["visual"], { kind: "inventory" }> }) => (
<section className="authoring-visual authoring-visual--inventory" role="region" aria-label="discovery evidence">
<section className="authoring-visual authoring-visual--inventory" aria-label="discovery evidence">
<div className="authoring-inventory__sources">
{visual.sources.map((source) => (
<div key={source}><Database aria-hidden="true" /><code>{source}</code></div>
@@ -31,7 +31,7 @@ const InventoryVisual = ({ visual }: { visual: Extract<AuthoringPhaseProjection[
);
const GraphVisual = ({ visual }: { visual: Extract<AuthoringPhaseProjection["visual"], { kind: "graph" }> }) => (
<section className="authoring-visual authoring-visual--graph" role="region" aria-label="draft graph evidence">
<section className="authoring-visual authoring-visual--graph" aria-label="draft graph evidence">
<div className="authoring-graph__node"><Database aria-hidden="true" /><strong>{visual.nodes[0]}</strong></div>
<div className="authoring-graph__edge"><span>{visual.inputBinding}</span><ArrowRight aria-hidden="true" /></div>
<div className="authoring-graph__node"><Workflow aria-hidden="true" /><strong>{visual.nodes[1]}</strong></div>
@@ -41,7 +41,7 @@ const GraphVisual = ({ visual }: { visual: Extract<AuthoringPhaseProjection["vis
);
const RepairVisual = ({ visual }: { visual: Extract<AuthoringPhaseProjection["visual"], { kind: "repair" }> }) => (
<section className="authoring-visual authoring-visual--repair" role="region" aria-label="validation repair evidence">
<section className="authoring-visual authoring-visual--repair" aria-label="validation repair evidence">
<div className="authoring-repair__diagnostic"><AlertTriangle aria-hidden="true" /><span>Diagnostic</span><strong>{visual.diagnostic}</strong></div>
<ArrowRight aria-hidden="true" />
<div className="authoring-repair__correction"><Link2 aria-hidden="true" /><span>Repair</span><code>{visual.correction}</code></div>
@@ -50,7 +50,7 @@ const RepairVisual = ({ visual }: { visual: Extract<AuthoringPhaseProjection["vi
);
const ArtifactVisual = ({ visual }: { visual: Extract<AuthoringPhaseProjection["visual"], { kind: "artifact" }> }) => (
<section className="authoring-visual authoring-visual--artifact" role="region" aria-label="artifact evidence">
<section className="authoring-visual authoring-visual--artifact" aria-label="artifact evidence">
<LockKeyhole aria-hidden="true" />
<div><span>Immutable workflow artifact</span><strong>{visual.artifactId}</strong></div>
<dl><div><dt>Version</dt><dd>Version {visual.version}</dd></div><div><dt>Requirements</dt><dd>{visual.requiredSources} local sources</dd></div></dl>
@@ -58,7 +58,7 @@ const ArtifactVisual = ({ visual }: { visual: Extract<AuthoringPhaseProjection["
);
const BindingsVisual = ({ visual }: { visual: Extract<AuthoringPhaseProjection["visual"], { kind: "bindings" }> }) => (
<section className="authoring-visual authoring-visual--bindings" role="region" aria-label="deployment binding evidence">
<section className="authoring-visual authoring-visual--bindings" aria-label="deployment binding evidence">
<header><Link2 aria-hidden="true" /><div><span>Deployment</span><strong>{visual.deploymentId}</strong></div><b><CheckCircle2 aria-hidden="true" />{visual.status}</b></header>
<div className="authoring-bindings__rows">
{visual.bindings.map((binding) => (
@@ -67,6 +67,29 @@ describe("projectPreparedAuthoring", () => {
}
});
it("labels each prepared command with its actual operation name", () => {
const titles = projectPreparedAuthoring()
.flatMap((phase) => phase.commands)
.map((command) => command.title);
expect(titles).toEqual([
"workflow.sources.list",
"workflow.capabilities.list",
"workflow.capabilities.inspect",
"wf schema",
"workflow.draft_workspaces.create_from_capability",
"workflow.draft_workspaces.add_step_from_capability",
"workflow.draft_workspaces.get",
"workflow.draft_workspaces.validate",
"workflow.draft_workspaces.set_step_output_map",
"workflow.draft_workspaces.compile",
"workflow.draft_workspaces.create_artifact",
"workflow.artifacts.inspect",
"workflow.deployments.save",
"workflow.deployments.validate",
]);
});
it("starts every command with wf or uv run wf", () => {
const phases = projectPreparedAuthoring();
for (const phase of phases) {
@@ -118,7 +141,12 @@ describe("projectPreparedAuthoringThread", () => {
expect(calls).toHaveLength(projectPreparedAuthoring()[0]!.commands.length);
expect(results).toHaveLength(calls.length);
expect(calls.every((part) => part.call.name === "runWorkflowCommand")).toBe(true);
expect(calls.map((part) => part.call.name)).toEqual([
"workflow.sources.list",
"workflow.capabilities.list",
"workflow.capabilities.inspect",
"wf schema",
]);
});
it("uses a stable phase tool-group id", () => {
@@ -13,6 +13,7 @@ import {
agentToolResultPart,
type AgentMessage,
} from "../../demo/agent/events.js";
import type { PreparedAuthoringToolName } from "../../demo/agent/tools.js";
export type AuthoringPhaseId =
| "discover"
@@ -24,6 +25,8 @@ export type AuthoringPhaseId =
export type CommandResult = "success" | "diagnostic";
export type PreparedAuthoringCommand = {
/** JSON-RPC method, or the explicit local CLI label for non-RPC commands. */
readonly title: PreparedAuthoringToolName;
readonly command: string;
readonly summary: string;
readonly result: CommandResult;
@@ -70,24 +73,28 @@ const recording: readonly PreparedAuthoringPhase[] = [
label: "Discover",
commands: [
{
title: "workflow.sources.list",
command: "wf source list",
summary: "List available capability sources",
result: "success",
detail: "6 sources: local.lda_docs, local.lda_report, local.issue_board, and platform helpers.",
},
{
title: "workflow.capabilities.list",
command: "wf cap list --source local.lda_report --format ids",
summary: "List report workflow capabilities",
result: "success",
detail: "5 capabilities: analyze_documents, build_report, create_issue_drafts, finalise_report, record_revision_request.",
},
{
title: "workflow.capabilities.inspect",
command: "wf cap inspect local.lda_report.analyze_documents",
summary: "Inspect the report-analysis contract",
result: "success",
detail: "Input: documents. Output: analysis. Declared outcome: ok.",
},
{
title: "wf schema",
command: "wf schema",
summary: "List registered workflow schemas",
result: "success",
@@ -116,17 +123,20 @@ const recording: readonly PreparedAuthoringPhase[] = [
label: "Draft",
commands: [
{
title: "workflow.draft_workspaces.create_from_capability",
command: "wf draft create lda_report_workflow --capability local.lda_docs.read_documents",
summary: "Create a new workflow draft",
result: "success",
detail: "Draft 'lda_report_workflow' created with ID draft_a1b2c3",
},
{
title: "workflow.draft_workspaces.add_step_from_capability",
command: "wf draft add-step lda_report_workflow --revision 1 --step analyze --capability local.lda_report.analyze_documents --from-step read_documents --from-outcome ok --route ok=__end__ --input state.documents=documents",
summary: "Add the report-analysis step with a declared route",
result: "success",
},
{
title: "workflow.draft_workspaces.get",
command: "wf draft inspect lda_report_workflow --include-draft",
summary: "Inspect the prepared draft graph",
result: "success",
@@ -151,12 +161,14 @@ const recording: readonly PreparedAuthoringPhase[] = [
label: "Validate",
commands: [
{
title: "workflow.draft_workspaces.validate",
command: "wf draft validate lda_report_workflow",
summary: "Validate the workflow draft",
result: "diagnostic",
detail: "Diagnostic: analyze output 'analysis' has no state projection.",
},
{
title: "workflow.draft_workspaces.set_step_output_map",
command: "wf draft set-output lda_report_workflow --revision 2 --step analyze --map analysis=state.analysis",
summary: "Repair the missing output binding",
result: "success",
@@ -185,18 +197,21 @@ const recording: readonly PreparedAuthoringPhase[] = [
label: "Artifact",
commands: [
{
title: "workflow.draft_workspaces.compile",
command: "wf draft compile lda_report_workflow",
summary: "Compile the validated draft without mutating it",
result: "success",
detail: "Compiled raw plan requires local.lda_docs, local.lda_report, and local.issue_board.",
},
{
title: "workflow.draft_workspaces.create_artifact",
command: "wf draft save lda_report_workflow --artifact lda_report_case_study --version 1 --title \"lda.chat Report Case Study\" --outcome completed --outcome cancelled --binding local.lda_docs=local.lda_docs --binding local.lda_report=local.lda_report --binding local.issue_board=local.issue_board",
summary: "Save immutable artifact version 1",
result: "success",
detail: "Artifact lda_report_case_study v1 saved with report-workflow source requirements.",
},
{
title: "workflow.artifacts.inspect",
command: "wf artifact inspect lda_report_case_study --version 1",
summary: "Inspect the compiled artifact",
result: "success",
@@ -221,12 +236,14 @@ const recording: readonly PreparedAuthoringPhase[] = [
label: "Deployment",
commands: [
{
title: "workflow.deployments.save",
command: "wf deploy save lda_report_case_study.default --artifact lda_report_case_study --version 1 --binding local.lda_docs=local.lda_docs --binding local.lda_report=local.lda_report --binding local.issue_board=local.issue_board",
summary: "Save deployment with source bindings",
result: "success",
detail: "Deployment lda_report_case_study.default saved with all three local source bindings.",
},
{
title: "workflow.deployments.validate",
command: "wf deploy validate lda_report_case_study.default",
summary: "Validate deployment bindings",
result: "success",
@@ -275,12 +292,12 @@ export const projectPreparedAuthoringThread = (
const toolParts = phase.commands.flatMap((command, index) => {
const callId = `${groupId}-command-${index}`;
return [
agentToolCallPart(callId, "runWorkflowCommand", {
agentToolCallPart(callId, command.title, {
phase: phase.phase,
command: command.command,
summary: command.summary,
}),
agentToolResultPart(callId, "runWorkflowCommand", "success", {
agentToolResultPart(callId, command.title, "success", {
status: command.result,
detail: command.detail ?? null,
}),
@@ -298,27 +315,5 @@ export const projectPreparedAuthoringThread = (
});
};
const handoffConversation: readonly AuthoringConversationTurn[] = [
{
role: "user",
text: "We need to prepare a report workflow for the lda_report scenario. What sources and capabilities are available?",
},
{
role: "assistant",
text: "I found the local document, report, and issue-board capabilities. I can author the reusable workflow through the public wf CLI.",
},
{
role: "user",
text: "Keep the workflow inspectable and validate the source bindings before it runs.",
},
{
role: "assistant",
text: "The prepared workflow is ready for deployment: I will create the draft, repair validation diagnostics, save artifact lda_report_case_study v1, then validate its deployment bindings.",
},
];
/** Returns the full prepared authoring recording. */
export const projectPreparedAuthoring = (): readonly PreparedAuthoringPhase[] => recording;
/** Returns the compact Scene 8 handoff derived from the prepared authoring evidence. */
export const projectPreparedAuthoringHandoff = (): readonly AuthoringConversationTurn[] => handoffConversation;
@@ -157,13 +157,12 @@ const AssistantMessageBody = ({
const part = parts[index]!;
if (part.type === "text") {
rendered.push(
<p key={`text-${index}`} style={{ whiteSpace: "pre-line" }}>{part.text}</p>,
<p key={`text-${part.text}`} style={{ whiteSpace: "pre-line" }}>{part.text}</p>,
);
index += 1;
continue;
}
const toolRunStart = index;
const toolRun: ToolRenderPart[] = [];
while (index < parts.length && parts[index]!.type !== "text") {
toolRun.push(parts[index]! as ToolRenderPart);
@@ -176,7 +175,7 @@ const AssistantMessageBody = ({
if (logicalTools.length === 1 && !messageId.startsWith("authoring-")) {
rendered.push(
<div key={`tool-${toolRunStart}`}>
<div key={`tool-${logicalTools[0]!.toolCallId ?? logicalTools[0]!.toolName}`}>
{renderContentPart(logicalTools[0]!, submitApproval, cancelApproval)}
</div>,
);
@@ -189,7 +188,7 @@ const AssistantMessageBody = ({
const count = logicalTools.length;
rendered.push(
<ToolGroupRoot
key={`tool-group-${toolRunStart}`}
key={`tool-group-${groupId}`}
data-tool-group-id={groupId}
{...(phaseLabel
? {
@@ -203,13 +202,13 @@ const AssistantMessageBody = ({
{...(phaseLabel ? { label: `${phaseLabel} · ${count} tool ${count === 1 ? "call" : "calls"}` } : {})}
/>
<ToolGroupContent>
{logicalTools.map((tool, toolIndex) => {
{logicalTools.map((tool) => {
const pairedResult = tool.type === "tool-call"
? toolRun.find((candidate): candidate is Extract<ToolRenderPart, { readonly type: "tool-result" }> =>
candidate.type === "tool-result" && candidate.toolCallId === tool.toolCallId)
: undefined;
return (
<div key={`${tool.type}-${tool.toolName}-${tool.toolCallId ?? "no-id"}-${toolIndex}`}>
<div key={`${tool.type}-${tool.toolName}-${tool.toolCallId ?? "no-id"}`}>
{renderContentPart(tool, submitApproval, cancelApproval, false, pairedResult)}
</div>
);
@@ -254,13 +253,19 @@ export const AssistantOperatorThread = ({
}: AssistantOperatorThreadProps) => {
const projected = useMemo(() => projectAgentMessagesForAssistant(messages), [messages]);
const viewportRef = useRef<HTMLDivElement>(null);
const [openToolGroups, setOpenToolGroups] = useState<ReadonlySet<string>>(
() => new Set(activeToolGroupId ? [activeToolGroupId] : []),
const [toolGroupOverrides, setToolGroupOverrides] = useState<ReadonlyMap<string, boolean>>(
() => new Map(),
);
useEffect(() => {
if (activeToolGroupId) setOpenToolGroups(new Set([activeToolGroupId]));
}, [activeToolGroupId]);
const openToolGroups = useMemo(() => {
const open = new Set<string>();
if (activeToolGroupId && toolGroupOverrides.get(activeToolGroupId) !== false) {
open.add(activeToolGroupId);
}
for (const [groupId, isOpen] of toolGroupOverrides) {
if (isOpen) open.add(groupId);
}
return open;
}, [activeToolGroupId, toolGroupOverrides]);
useEffect(() => {
if (!activeToolGroupId) return;
@@ -278,10 +283,9 @@ export const AssistantOperatorThread = ({
}, [activeToolGroupId, projected]);
const setToolGroupOpen = useCallback((groupId: string, open: boolean) => {
setOpenToolGroups((current) => {
const next = new Set(current);
if (open) next.add(groupId);
else next.delete(groupId);
setToolGroupOverrides((current) => {
const next = new Map(current);
next.set(groupId, open);
return next;
});
}, []);
@@ -305,8 +309,8 @@ export const AssistantOperatorThread = ({
if (message.role === "user") {
return (
<MessageBubble key={message.id} role="user">
{message.content.filter((p) => p.type === "text").map((p, i) => (
<p key={`text-${i}`} style={{ whiteSpace: "pre-line" }}>{p.text}</p>
{message.content.filter((p) => p.type === "text").map((p) => (
<p key={`text-${p.text}`} style={{ whiteSpace: "pre-line" }}>{p.text}</p>
))}
</MessageBubble>
);
@@ -137,4 +137,34 @@ describe("assistantRuntimeProjection", () => {
{ type: "text", text: "provider failed" },
]);
});
it("assigns sequential indexes for same-type presentation actions", () => {
const messages: ReadonlyArray<AgentMessage> = [
{
id: "dual",
role: "assistant",
parts: [
{ type: "presentation-action", action: { type: "selectWorkflowNode", nodeId: "review_issues" } },
{ type: "presentation-action", action: { type: "selectWorkflowNode", nodeId: "start_step" } },
],
},
];
const projected = projectAgentMessagesForAssistant(messages);
expect(projected[0]?.content).toEqual([
{
type: "tool-call",
toolCallId: "presentation-dual-0-selectWorkflowNode",
toolName: "presentation.selectWorkflowNode",
args: { type: "selectWorkflowNode", nodeId: "review_issues" },
},
{
type: "tool-call",
toolCallId: "presentation-dual-1-selectWorkflowNode",
toolName: "presentation.selectWorkflowNode",
args: { type: "selectWorkflowNode", nodeId: "start_step" },
},
]);
});
});