feat: finish continuous authoring workspace
This commit is contained in:
@@ -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 => ({
|
||||
|
||||
@@ -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" },
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user