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. Astro remains an option, not the default next surface.
25. Completed: presentation agent authoring story creates a canonical prepared 25. Completed: presentation agent authoring story creates a canonical prepared
authoring recording, Scene 8 as an authentic full-screen conversation, 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 that never calls workflow authoring RPC operations during Scene 9
navigation. Implementation: navigation. Implementation:
[`presentation agent authoring story`](historical/superpowers/plans/2026-07-11-presentation-agent-authoring-story.md). [`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 Scenes 8 and 9 are now implemented as authentic external-agent handoff and
factual prepared-authoring proof. The canonical recording, full-screen Scene 8, 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: Scene 9 lifecycle with a synchronized chat dock, five factual phase visuals,
[`presentation agent authoring story`](historical/superpowers/plans/2026-07-11-presentation-agent-authoring-story.md). 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: Recommended next visual slices:
@@ -1,5 +1,7 @@
# Continuous Authoring Workspace Implementation Plan # 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. > **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. **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 The console exposes `/present`, a 720p no-scroll defense compositor for the
prepared `lda_report_workflow` story. It renders a 14-scene, multi-beat prepared `lda_report_workflow` story. It renders a 14-scene, multi-beat
storyboard with an adaptive aspect-ratio canvas, stable stage regions, discussion storyboard (expanded from the original 12-scene defense plan; items 13 and 14
branches, act themes, a chat dock, and keyboard navigation. 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 The final presentation beats frame the evaluation as bounded evidence, make
claim boundaries and future work explicit, and end on the canonical defense 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 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 execution evidence. Scene 8 is a full-screen conversation that establishes the
external agent handoff; Scene 9 breaks the prepared authoring into five phases external agent handoff; Scene 9 breaks the prepared authoring into five phases
with an overlay trace panel. Neither scene calls workflow authoring RPC with the same conversation contracted into a synchronized bottom dock. Each
operations — they consume deterministic prepared data. Scenes 10 through 12 use 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 the canonical replay to show run, interrupt, approval, resume, output, and
trace. Raw protocol payloads are available through the evidence receipt and trace. Raw protocol payloads are available through the evidence receipt and
inspector. 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 `AgentMessagePart` / `TimelineAgent`-compatible events instead of replacing the
presentation timeline. 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/request` — Scene 8, prepared handoff request
- `/present#scene/agent-handoff/handoff` — Scene 8, agent accepts - `/present#scene/agent-handoff/handoff` — Scene 8, agent accepts
- `/present#scene/prepared-lifecycle/discover` — Scene 9, discover phase - `/present#scene/prepared-lifecycle/discover` — Scene 9, discover phase
- `/present#scene/prepared-lifecycle/draft` — Scene 9, draft phase - `/present#scene/prepared-lifecycle/draft` — Scene 9, draft phase
- `/present#scene/prepared-lifecycle/deployment` — Scene 9, deployment 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 Scene 12 is factual by design. It projects the reviewed/live run into visible
workflow input, interrupt payload, resume decision, output, and trace facts. 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 - **Scene 8 (Agent Handoff)**: a full-screen prepared conversation that
establishes the external agent interface. The `request` beat shows the establishes the external agent interface. The `request` beat shows the
operator requesting a report workflow; the `handoff` beat reveals the full 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 - **Scene 9 (Prepared Workflow Lifecycle)**: a five-phase lifecycle
(discover, draft, validate, artifact, deployment) with a compact phase rail (discover, draft, validate, artifact, deployment) with a compact phase rail
and one dominant command/result projection per beat. An overlay and one dominant factual product projection per beat. The Scene 8 thread
`AuthoringTracePanel` surfaces the full agent trace for the active phase remains visible as a compact bottom dock; its active tool group follows the
without competing with the lifecycle projection. current beat. There is no detached trace modal or second transcript.
The authoring scenes consume deterministic prepared data and never call The authoring scenes consume deterministic prepared data and never call
workflow authoring RPCs — only `workflow.health` infrastructure pings are 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"; export type AgentRole = "user" | "assistant";
@@ -9,13 +11,13 @@ export type PresentationToolAction =
export type AgentToolCall = { export type AgentToolCall = {
readonly id: string; readonly id: string;
readonly name: AgentToolName; readonly name: AgentMessageToolName;
readonly input: unknown; readonly input: unknown;
}; };
export type AgentToolResult = { export type AgentToolResult = {
readonly callId: string; readonly callId: string;
readonly name: AgentToolName; readonly name: AgentMessageToolName;
readonly status: "success" | "failure"; readonly status: "success" | "failure";
readonly output: unknown; readonly output: unknown;
}; };
@@ -56,7 +58,7 @@ export const agentTextMessage = (id: string, role: AgentRole, text: string): Age
export const agentToolCallPart = ( export const agentToolCallPart = (
id: string, id: string,
name: AgentToolName, name: AgentMessageToolName,
input: unknown, input: unknown,
): AgentMessagePart => ({ ): AgentMessagePart => ({
type: "tool-call", type: "tool-call",
@@ -65,7 +67,7 @@ export const agentToolCallPart = (
export const agentToolResultPart = ( export const agentToolResultPart = (
callId: string, callId: string,
name: AgentToolName, name: AgentMessageToolName,
status: "success" | "failure", status: "success" | "failure",
output: unknown, output: unknown,
): AgentMessagePart => ({ ): AgentMessagePart => ({
+17
View File
@@ -5,6 +5,23 @@ export type WorkflowToolName =
| "readRunTrace" | "readRunTrace"
| "runWorkflowCommand"; | "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 = export type PresentationToolName =
| "selectWorkflowNode" | "selectWorkflowNode"
| "focusOperation" | "focusOperation"
@@ -66,9 +66,9 @@ describe("PresentationRoute", () => {
window.location.hash = "#scene/agent-handoff/request"; window.location.hash = "#scene/agent-handoff/request";
const { PresentationRoute } = await import("./PresentationRoute.js"); const { PresentationRoute } = await import("./PresentationRoute.js");
render(<PresentationRoute />); render(<PresentationRoute />);
expect(screen.getByRole("heading", { name: /Agent Handoff/i })).toBeInTheDocument(); expect(screen.getByRole("log", { name: "prepared authoring conversation" })).toBeInTheDocument();
await userEvent.keyboard("{ArrowRight}"); 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 () => { it("renders audience progress chrome without rail or mode label", async () => {
@@ -373,8 +373,8 @@ describe("PresentationRoute", () => {
const { PresentationRoute } = await import("./PresentationRoute.js"); const { PresentationRoute } = await import("./PresentationRoute.js");
render(<PresentationRoute />); render(<PresentationRoute />);
expect(await screen.findByRole("heading", { name: "Agent Handoff" })).toBeInTheDocument(); expect(await screen.findByRole("log", { name: "prepared authoring conversation" })).toBeInTheDocument();
expect(screen.getByText(/A thin agent interface receives the report request/)).toBeInTheDocument(); expect(screen.getByRole("button", { name: /discover.*4 tool calls/i })).toBeInTheDocument();
}); });
it("navigates to Scene 8 handoff beat via hash", async () => { it("navigates to Scene 8 handoff beat via hash", async () => {
@@ -382,8 +382,8 @@ describe("PresentationRoute", () => {
const { PresentationRoute } = await import("./PresentationRoute.js"); const { PresentationRoute } = await import("./PresentationRoute.js");
render(<PresentationRoute />); render(<PresentationRoute />);
expect(await screen.findByRole("heading", { name: "Agent Handoff" })).toBeInTheDocument(); expect(await screen.findByRole("log", { name: "prepared authoring conversation" })).toBeInTheDocument();
expect(screen.getByText(/The interface delegates durable work to lda\.chat/)).toBeInTheDocument(); expect(screen.getByRole("button", { name: /deployment.*2 tool calls/i })).toBeInTheDocument();
}); });
it.each([ it.each([
@@ -36,7 +36,7 @@ describe("AgentHandoffScene", () => {
it("interleaves prepared workflow tool groups with the handoff conversation", () => { it("interleaves prepared workflow tool groups with the handoff conversation", () => {
renderBeat("handoff"); 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 })) expect(screen.getByRole("button", { name: /deployment.*2 tool calls/i }))
.toHaveAttribute("aria-expanded", "true"); .toHaveAttribute("aria-expanded", "true");
}); });
@@ -16,7 +16,7 @@ describe("AuthoringConversation", () => {
expect(screen.getByRole("log", { name: "prepared authoring conversation" })) expect(screen.getByRole("log", { name: "prepared authoring conversation" }))
.toHaveAttribute("data-surface", "stage"); .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); expect(screen.getAllByText(/deployment/i).length).toBeGreaterThan(0);
}); });
@@ -15,7 +15,7 @@ type AuthoringPhaseVisualProps = {
}; };
const InventoryVisual = ({ visual }: { visual: Extract<AuthoringPhaseProjection["visual"], { kind: "inventory" }> }) => ( 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"> <div className="authoring-inventory__sources">
{visual.sources.map((source) => ( {visual.sources.map((source) => (
<div key={source}><Database aria-hidden="true" /><code>{source}</code></div> <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" }> }) => ( 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__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__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> <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" }> }) => ( 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> <div className="authoring-repair__diagnostic"><AlertTriangle aria-hidden="true" /><span>Diagnostic</span><strong>{visual.diagnostic}</strong></div>
<ArrowRight aria-hidden="true" /> <ArrowRight aria-hidden="true" />
<div className="authoring-repair__correction"><Link2 aria-hidden="true" /><span>Repair</span><code>{visual.correction}</code></div> <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" }> }) => ( 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" /> <LockKeyhole aria-hidden="true" />
<div><span>Immutable workflow artifact</span><strong>{visual.artifactId}</strong></div> <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> <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" }> }) => ( 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> <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"> <div className="authoring-bindings__rows">
{visual.bindings.map((binding) => ( {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", () => { it("starts every command with wf or uv run wf", () => {
const phases = projectPreparedAuthoring(); const phases = projectPreparedAuthoring();
for (const phase of phases) { for (const phase of phases) {
@@ -118,7 +141,12 @@ describe("projectPreparedAuthoringThread", () => {
expect(calls).toHaveLength(projectPreparedAuthoring()[0]!.commands.length); expect(calls).toHaveLength(projectPreparedAuthoring()[0]!.commands.length);
expect(results).toHaveLength(calls.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", () => { it("uses a stable phase tool-group id", () => {
@@ -13,6 +13,7 @@ import {
agentToolResultPart, agentToolResultPart,
type AgentMessage, type AgentMessage,
} from "../../demo/agent/events.js"; } from "../../demo/agent/events.js";
import type { PreparedAuthoringToolName } from "../../demo/agent/tools.js";
export type AuthoringPhaseId = export type AuthoringPhaseId =
| "discover" | "discover"
@@ -24,6 +25,8 @@ export type AuthoringPhaseId =
export type CommandResult = "success" | "diagnostic"; export type CommandResult = "success" | "diagnostic";
export type PreparedAuthoringCommand = { export type PreparedAuthoringCommand = {
/** JSON-RPC method, or the explicit local CLI label for non-RPC commands. */
readonly title: PreparedAuthoringToolName;
readonly command: string; readonly command: string;
readonly summary: string; readonly summary: string;
readonly result: CommandResult; readonly result: CommandResult;
@@ -70,24 +73,28 @@ const recording: readonly PreparedAuthoringPhase[] = [
label: "Discover", label: "Discover",
commands: [ commands: [
{ {
title: "workflow.sources.list",
command: "wf source list", command: "wf source list",
summary: "List available capability sources", summary: "List available capability sources",
result: "success", result: "success",
detail: "6 sources: local.lda_docs, local.lda_report, local.issue_board, and platform helpers.", 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", command: "wf cap list --source local.lda_report --format ids",
summary: "List report workflow capabilities", summary: "List report workflow capabilities",
result: "success", result: "success",
detail: "5 capabilities: analyze_documents, build_report, create_issue_drafts, finalise_report, record_revision_request.", 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", command: "wf cap inspect local.lda_report.analyze_documents",
summary: "Inspect the report-analysis contract", summary: "Inspect the report-analysis contract",
result: "success", result: "success",
detail: "Input: documents. Output: analysis. Declared outcome: ok.", detail: "Input: documents. Output: analysis. Declared outcome: ok.",
}, },
{ {
title: "wf schema",
command: "wf schema", command: "wf schema",
summary: "List registered workflow schemas", summary: "List registered workflow schemas",
result: "success", result: "success",
@@ -116,17 +123,20 @@ const recording: readonly PreparedAuthoringPhase[] = [
label: "Draft", label: "Draft",
commands: [ commands: [
{ {
title: "workflow.draft_workspaces.create_from_capability",
command: "wf draft create lda_report_workflow --capability local.lda_docs.read_documents", command: "wf draft create lda_report_workflow --capability local.lda_docs.read_documents",
summary: "Create a new workflow draft", summary: "Create a new workflow draft",
result: "success", result: "success",
detail: "Draft 'lda_report_workflow' created with ID draft_a1b2c3", 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", 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", summary: "Add the report-analysis step with a declared route",
result: "success", result: "success",
}, },
{ {
title: "workflow.draft_workspaces.get",
command: "wf draft inspect lda_report_workflow --include-draft", command: "wf draft inspect lda_report_workflow --include-draft",
summary: "Inspect the prepared draft graph", summary: "Inspect the prepared draft graph",
result: "success", result: "success",
@@ -151,12 +161,14 @@ const recording: readonly PreparedAuthoringPhase[] = [
label: "Validate", label: "Validate",
commands: [ commands: [
{ {
title: "workflow.draft_workspaces.validate",
command: "wf draft validate lda_report_workflow", command: "wf draft validate lda_report_workflow",
summary: "Validate the workflow draft", summary: "Validate the workflow draft",
result: "diagnostic", result: "diagnostic",
detail: "Diagnostic: analyze output 'analysis' has no state projection.", 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", command: "wf draft set-output lda_report_workflow --revision 2 --step analyze --map analysis=state.analysis",
summary: "Repair the missing output binding", summary: "Repair the missing output binding",
result: "success", result: "success",
@@ -185,18 +197,21 @@ const recording: readonly PreparedAuthoringPhase[] = [
label: "Artifact", label: "Artifact",
commands: [ commands: [
{ {
title: "workflow.draft_workspaces.compile",
command: "wf draft compile lda_report_workflow", command: "wf draft compile lda_report_workflow",
summary: "Compile the validated draft without mutating it", summary: "Compile the validated draft without mutating it",
result: "success", result: "success",
detail: "Compiled raw plan requires local.lda_docs, local.lda_report, and local.issue_board.", 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", 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", summary: "Save immutable artifact version 1",
result: "success", result: "success",
detail: "Artifact lda_report_case_study v1 saved with report-workflow source requirements.", 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", command: "wf artifact inspect lda_report_case_study --version 1",
summary: "Inspect the compiled artifact", summary: "Inspect the compiled artifact",
result: "success", result: "success",
@@ -221,12 +236,14 @@ const recording: readonly PreparedAuthoringPhase[] = [
label: "Deployment", label: "Deployment",
commands: [ 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", 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", summary: "Save deployment with source bindings",
result: "success", result: "success",
detail: "Deployment lda_report_case_study.default saved with all three local source bindings.", 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", command: "wf deploy validate lda_report_case_study.default",
summary: "Validate deployment bindings", summary: "Validate deployment bindings",
result: "success", result: "success",
@@ -275,12 +292,12 @@ export const projectPreparedAuthoringThread = (
const toolParts = phase.commands.flatMap((command, index) => { const toolParts = phase.commands.flatMap((command, index) => {
const callId = `${groupId}-command-${index}`; const callId = `${groupId}-command-${index}`;
return [ return [
agentToolCallPart(callId, "runWorkflowCommand", { agentToolCallPart(callId, command.title, {
phase: phase.phase, phase: phase.phase,
command: command.command, command: command.command,
summary: command.summary, summary: command.summary,
}), }),
agentToolResultPart(callId, "runWorkflowCommand", "success", { agentToolResultPart(callId, command.title, "success", {
status: command.result, status: command.result,
detail: command.detail ?? null, 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. */ /** Returns the full prepared authoring recording. */
export const projectPreparedAuthoring = (): readonly PreparedAuthoringPhase[] => 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]!; const part = parts[index]!;
if (part.type === "text") { if (part.type === "text") {
rendered.push( 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; index += 1;
continue; continue;
} }
const toolRunStart = index;
const toolRun: ToolRenderPart[] = []; const toolRun: ToolRenderPart[] = [];
while (index < parts.length && parts[index]!.type !== "text") { while (index < parts.length && parts[index]!.type !== "text") {
toolRun.push(parts[index]! as ToolRenderPart); toolRun.push(parts[index]! as ToolRenderPart);
@@ -176,7 +175,7 @@ const AssistantMessageBody = ({
if (logicalTools.length === 1 && !messageId.startsWith("authoring-")) { if (logicalTools.length === 1 && !messageId.startsWith("authoring-")) {
rendered.push( rendered.push(
<div key={`tool-${toolRunStart}`}> <div key={`tool-${logicalTools[0]!.toolCallId ?? logicalTools[0]!.toolName}`}>
{renderContentPart(logicalTools[0]!, submitApproval, cancelApproval)} {renderContentPart(logicalTools[0]!, submitApproval, cancelApproval)}
</div>, </div>,
); );
@@ -189,7 +188,7 @@ const AssistantMessageBody = ({
const count = logicalTools.length; const count = logicalTools.length;
rendered.push( rendered.push(
<ToolGroupRoot <ToolGroupRoot
key={`tool-group-${toolRunStart}`} key={`tool-group-${groupId}`}
data-tool-group-id={groupId} data-tool-group-id={groupId}
{...(phaseLabel {...(phaseLabel
? { ? {
@@ -203,13 +202,13 @@ const AssistantMessageBody = ({
{...(phaseLabel ? { label: `${phaseLabel} · ${count} tool ${count === 1 ? "call" : "calls"}` } : {})} {...(phaseLabel ? { label: `${phaseLabel} · ${count} tool ${count === 1 ? "call" : "calls"}` } : {})}
/> />
<ToolGroupContent> <ToolGroupContent>
{logicalTools.map((tool, toolIndex) => { {logicalTools.map((tool) => {
const pairedResult = tool.type === "tool-call" const pairedResult = tool.type === "tool-call"
? toolRun.find((candidate): candidate is Extract<ToolRenderPart, { readonly type: "tool-result" }> => ? toolRun.find((candidate): candidate is Extract<ToolRenderPart, { readonly type: "tool-result" }> =>
candidate.type === "tool-result" && candidate.toolCallId === tool.toolCallId) candidate.type === "tool-result" && candidate.toolCallId === tool.toolCallId)
: undefined; : undefined;
return ( 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)} {renderContentPart(tool, submitApproval, cancelApproval, false, pairedResult)}
</div> </div>
); );
@@ -254,13 +253,19 @@ export const AssistantOperatorThread = ({
}: AssistantOperatorThreadProps) => { }: AssistantOperatorThreadProps) => {
const projected = useMemo(() => projectAgentMessagesForAssistant(messages), [messages]); const projected = useMemo(() => projectAgentMessagesForAssistant(messages), [messages]);
const viewportRef = useRef<HTMLDivElement>(null); const viewportRef = useRef<HTMLDivElement>(null);
const [openToolGroups, setOpenToolGroups] = useState<ReadonlySet<string>>( const [toolGroupOverrides, setToolGroupOverrides] = useState<ReadonlyMap<string, boolean>>(
() => new Set(activeToolGroupId ? [activeToolGroupId] : []), () => new Map(),
); );
const openToolGroups = useMemo(() => {
useEffect(() => { const open = new Set<string>();
if (activeToolGroupId) setOpenToolGroups(new Set([activeToolGroupId])); if (activeToolGroupId && toolGroupOverrides.get(activeToolGroupId) !== false) {
}, [activeToolGroupId]); open.add(activeToolGroupId);
}
for (const [groupId, isOpen] of toolGroupOverrides) {
if (isOpen) open.add(groupId);
}
return open;
}, [activeToolGroupId, toolGroupOverrides]);
useEffect(() => { useEffect(() => {
if (!activeToolGroupId) return; if (!activeToolGroupId) return;
@@ -278,10 +283,9 @@ export const AssistantOperatorThread = ({
}, [activeToolGroupId, projected]); }, [activeToolGroupId, projected]);
const setToolGroupOpen = useCallback((groupId: string, open: boolean) => { const setToolGroupOpen = useCallback((groupId: string, open: boolean) => {
setOpenToolGroups((current) => { setToolGroupOverrides((current) => {
const next = new Set(current); const next = new Map(current);
if (open) next.add(groupId); next.set(groupId, open);
else next.delete(groupId);
return next; return next;
}); });
}, []); }, []);
@@ -305,8 +309,8 @@ export const AssistantOperatorThread = ({
if (message.role === "user") { if (message.role === "user") {
return ( return (
<MessageBubble key={message.id} role="user"> <MessageBubble key={message.id} role="user">
{message.content.filter((p) => p.type === "text").map((p, i) => ( {message.content.filter((p) => p.type === "text").map((p) => (
<p key={`text-${i}`} style={{ whiteSpace: "pre-line" }}>{p.text}</p> <p key={`text-${p.text}`} style={{ whiteSpace: "pre-line" }}>{p.text}</p>
))} ))}
</MessageBubble> </MessageBubble>
); );
@@ -137,4 +137,34 @@ describe("assistantRuntimeProjection", () => {
{ type: "text", text: "provider failed" }, { 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" },
},
]);
});
}); });