refactor: polish assistant-ui chat surface and rename tools

- Remove useExternalStoreRuntime (caused duplicate ID crashes on re-render)
- Render messages directly from projected data for read-only transcripts
- ToolCard open by default for single tools, collapsed inside ToolGroupCard
- Remove result bubble from ToolCard (was unstyled)
- Move run button below thread (chat-style layout)
- Add user/assistant visual distinction (right-aligned cyan bubble for user)
- Rename startPreparedReportRun -> startRun
- Add 3 workflow.run_once tool calls to Scene 2
- Add overflow handling with max-height + hidden overflow
- Update all tests for new behavior
This commit is contained in:
lda
2026-07-10 07:36:42 +07:00 Verified
parent 029da57f03
commit df026b553a
13 changed files with 137 additions and 127 deletions
@@ -18,19 +18,19 @@ describe("agent events", () => {
}); });
it("creates tool call, tool result, and presentation action parts", () => { it("creates tool call, tool result, and presentation action parts", () => {
expect(agentToolCallPart("c1", "startPreparedReportRun", { deploymentId: "lda_report_case_study.default" })).toEqual({ expect(agentToolCallPart("c1", "startRun", { deploymentId: "lda_report_case_study.default" })).toEqual({
type: "tool-call", type: "tool-call",
call: { call: {
id: "c1", id: "c1",
name: "startPreparedReportRun", name: "startRun",
input: { deploymentId: "lda_report_case_study.default" }, input: { deploymentId: "lda_report_case_study.default" },
}, },
}); });
expect(agentToolResultPart("c1", "startPreparedReportRun", "success", { runId: "run_1" })).toEqual({ expect(agentToolResultPart("c1", "startRun", "success", { runId: "run_1" })).toEqual({
type: "tool-result", type: "tool-result",
result: { result: {
callId: "c1", callId: "c1",
name: "startPreparedReportRun", name: "startRun",
status: "success", status: "success",
output: { runId: "run_1" }, output: { runId: "run_1" },
}, },
@@ -16,7 +16,7 @@ describe("prepared recipe driver", () => {
const messages = await collect(runPreparedRecipeReplay(recording, signal, async () => ({ approved: true, comment: "test" }))); const messages = await collect(runPreparedRecipeReplay(recording, signal, async () => ({ approved: true, comment: "test" })));
expect(messages[0]?.role).toBe("user"); expect(messages[0]?.role).toBe("user");
expect(messages.some((message) => expect(messages.some((message) =>
message.parts.some((part) => part.type === "tool-call" && part.call.name === "startPreparedReportRun"), message.parts.some((part) => part.type === "tool-call" && part.call.name === "startRun"),
)).toBe(true); )).toBe(true);
expect(messages.some((message) => expect(messages.some((message) =>
message.parts.some((part) => part.type === "presentation-action" && part.action.type === "selectWorkflowNode"), message.parts.some((part) => part.type === "presentation-action" && part.action.type === "selectWorkflowNode"),
@@ -35,7 +35,7 @@ describe("prepared recipe driver", () => {
); );
expect(toolNames).toEqual([ expect(toolNames).toEqual([
"inspectDeployment", "inspectDeployment",
"startPreparedReportRun", "startRun",
"selectWorkflowNode", "selectWorkflowNode",
"resumeIssueReview", "resumeIssueReview",
"readRunTrace", "readRunTrace",
@@ -92,7 +92,7 @@ describe("prepared recipe driver", () => {
); );
expect(toolCalls).toEqual([ expect(toolCalls).toEqual([
"inspectDeployment", "inspectDeployment",
"startPreparedReportRun", "startRun",
"selectWorkflowNode", "selectWorkflowNode",
"resumeIssueReview", "resumeIssueReview",
]); ]);
@@ -58,7 +58,7 @@ export async function* runPreparedRecipeReplay(
case "inspectDeployment": case "inspectDeployment":
yield emitToolStep(step.id, step.toolName, { deploymentId }, { deploymentId }); yield emitToolStep(step.id, step.toolName, { deploymentId }, { deploymentId });
break; break;
case "startPreparedReportRun": case "startRun":
yield emitToolStep(step.id, step.toolName, { deploymentId }, { yield emitToolStep(step.id, step.toolName, { deploymentId }, {
runId, runId,
eventId: runStart?.id ?? null, eventId: runStart?.id ?? null,
+2 -2
View File
@@ -2,7 +2,7 @@ import { LDA_REPORT_DEPLOYMENT_ID } from "../ldaReportDemoConfig.js";
import type { PresentationToolName, WorkflowToolName } from "./tools.js"; import type { PresentationToolName, WorkflowToolName } from "./tools.js";
export type RecipeTool = export type RecipeTool =
| Extract<WorkflowToolName, "inspectDeployment" | "startPreparedReportRun" | "resumeIssueReview" | "readRunTrace"> | Extract<WorkflowToolName, "inspectDeployment" | "startRun" | "resumeIssueReview" | "readRunTrace">
| Extract<PresentationToolName, "selectWorkflowNode">; | Extract<PresentationToolName, "selectWorkflowNode">;
type SelectWorkflowNodeStep = { type SelectWorkflowNodeStep = {
@@ -47,7 +47,7 @@ export const PREPARE_THESIS_REPORT_RECIPE: PreparedRecipe = {
{ {
id: "start-run", id: "start-run",
narration: "I will start the prepared workflow run.", narration: "I will start the prepared workflow run.",
toolName: "startPreparedReportRun", toolName: "startRun",
}, },
{ {
id: "focus-interrupt", id: "focus-interrupt",
@@ -96,7 +96,7 @@ export const useTimelineAgent = (
setMessages((current) => appendToolMessage( setMessages((current) => appendToolMessage(
current, current,
"timeline-agent-start", "timeline-agent-start",
"startPreparedReportRun", "startRun",
{ mode: modeLabel }, { mode: modeLabel },
{ phase: "started" }, { phase: "started" },
)); ));
@@ -4,7 +4,7 @@ import { AGENT_TOOLS, isAllowedAgentToolName } from "./tools.js";
describe("agent tools", () => { describe("agent tools", () => {
it("separates workflow tools from presentation tools", () => { it("separates workflow tools from presentation tools", () => {
expect(AGENT_TOOLS.inspectDeployment.kind).toBe("workflow"); expect(AGENT_TOOLS.inspectDeployment.kind).toBe("workflow");
expect(AGENT_TOOLS.startPreparedReportRun.kind).toBe("workflow"); expect(AGENT_TOOLS.startRun.kind).toBe("workflow");
expect(AGENT_TOOLS.resumeIssueReview.kind).toBe("workflow"); expect(AGENT_TOOLS.resumeIssueReview.kind).toBe("workflow");
expect(AGENT_TOOLS.readRunTrace.kind).toBe("workflow"); expect(AGENT_TOOLS.readRunTrace.kind).toBe("workflow");
expect(AGENT_TOOLS.selectWorkflowNode.kind).toBe("presentation"); expect(AGENT_TOOLS.selectWorkflowNode.kind).toBe("presentation");
+3 -3
View File
@@ -1,6 +1,6 @@
export type WorkflowToolName = export type WorkflowToolName =
| "inspectDeployment" | "inspectDeployment"
| "startPreparedReportRun" | "startRun"
| "resumeIssueReview" | "resumeIssueReview"
| "readRunTrace"; | "readRunTrace";
@@ -23,8 +23,8 @@ export const AGENT_TOOLS = {
kind: "workflow", kind: "workflow",
description: "Inspect the prepared report deployment.", description: "Inspect the prepared report deployment.",
}, },
startPreparedReportRun: { startRun: {
name: "startPreparedReportRun", name: "startRun",
kind: "workflow", kind: "workflow",
description: "Start the prepared report workflow run.", description: "Start the prepared report workflow run.",
}, },
@@ -77,7 +77,7 @@ describe("OperatorChat", () => {
expect(screen.getByRole("button", { name: /selectWorkflowNode/i })).toBeInTheDocument(); expect(screen.getByRole("button", { name: /selectWorkflowNode/i })).toBeInTheDocument();
}); });
it("renders tool calls as collapsed tool cards", async () => { it("renders tool calls as open tool cards", async () => {
const user = userEvent.setup(); const user = userEvent.setup();
const messages: ReadonlyArray<AgentMessage> = [ const messages: ReadonlyArray<AgentMessage> = [
{ {
@@ -92,9 +92,10 @@ describe("OperatorChat", () => {
render(<OperatorChat state={initialPresentationState} messages={messages} />); render(<OperatorChat state={initialPresentationState} messages={messages} />);
const tool = screen.getByRole("button", { name: /readRunTrace/i }); const tool = screen.getByRole("button", { name: /readRunTrace/i });
expect(screen.queryByText(/run_1/)).not.toBeInTheDocument(); expect(tool).toHaveAttribute("aria-expanded", "true");
await user.click(tool);
expect(screen.getByText(/run_1/)).toBeInTheDocument(); expect(screen.getByText(/run_1/)).toBeInTheDocument();
await user.click(tool);
expect(screen.queryByText(/run_1/)).not.toBeInTheDocument();
}); });
it("renders schema approval surface inside chat approval request", async () => { it("renders schema approval surface inside chat approval request", async () => {
@@ -157,8 +158,9 @@ describe("OperatorChat", () => {
const tool = screen.getByRole("button", { name: /resumeIssueReview/i }); const tool = screen.getByRole("button", { name: /resumeIssueReview/i });
expect(screen.queryByRole("button", { name: /Approve/i })).not.toBeInTheDocument(); expect(screen.queryByRole("button", { name: /Approve/i })).not.toBeInTheDocument();
expect(screen.queryByRole("button", { name: /Deny/i })).not.toBeInTheDocument(); expect(screen.queryByRole("button", { name: /Deny/i })).not.toBeInTheDocument();
await user.click(tool);
expect(screen.getByLabelText("tool input")).toBeInTheDocument(); expect(screen.getByLabelText("tool input")).toBeInTheDocument();
await user.click(tool);
expect(screen.queryByLabelText("tool input")).not.toBeInTheDocument();
}); });
it("renders error and presentation action parts", () => { it("renders error and presentation action parts", () => {
@@ -256,7 +258,7 @@ describe("OperatorChat", () => {
parts: [ parts: [
{ {
type: "tool-call", type: "tool-call",
call: { id: "call-1", name: "startPreparedReportRun", input: { deploymentId: "demo" } }, call: { id: "call-1", name: "startRun", input: { deploymentId: "demo" } },
}, },
], ],
}, },
@@ -264,7 +266,7 @@ describe("OperatorChat", () => {
render(<OperatorChat state={initialPresentationState} messages={messages} />); render(<OperatorChat state={initialPresentationState} messages={messages} />);
expect(screen.getByRole("button", { name: /startPreparedReportRun/i })).toBeInTheDocument(); expect(screen.getByRole("button", { name: /startRun/i })).toBeInTheDocument();
}); });
it("disables schema approval buttons when approval actions are unavailable", () => { it("disables schema approval buttons when approval actions are unavailable", () => {
@@ -7,7 +7,7 @@ import { AssistantOperatorThread } from "./AssistantOperatorThread.js";
afterEach(() => cleanup()); afterEach(() => cleanup());
describe("AssistantOperatorThread", () => { describe("AssistantOperatorThread", () => {
it("renders text interleaved with a collapsed tool call", async () => { it("renders text interleaved with an open tool call", async () => {
const user = userEvent.setup(); const user = userEvent.setup();
const messages: ReadonlyArray<AgentMessage> = [ const messages: ReadonlyArray<AgentMessage> = [
{ {
@@ -30,9 +30,10 @@ describe("AssistantOperatorThread", () => {
expect(screen.getByText("I will inspect the run.")).toBeInTheDocument(); expect(screen.getByText("I will inspect the run.")).toBeInTheDocument();
expect(screen.getByText("The trace is inspectable.")).toBeInTheDocument(); expect(screen.getByText("The trace is inspectable.")).toBeInTheDocument();
const tool = screen.getByRole("button", { name: /readRunTrace/i }); const tool = screen.getByRole("button", { name: /readRunTrace/i });
expect(screen.queryByText(/run_1/)).not.toBeInTheDocument(); expect(tool).toHaveAttribute("aria-expanded", "true");
await user.click(tool);
expect(screen.getByText(/run_1/)).toBeInTheDocument(); expect(screen.getByText(/run_1/)).toBeInTheDocument();
await user.click(tool);
expect(screen.queryByText(/run_1/)).not.toBeInTheDocument();
}); });
it("renders grouped consecutive tool calls", () => { it("renders grouped consecutive tool calls", () => {
@@ -1,11 +1,4 @@
import { useCallback, useMemo, useState } from "react"; import { useCallback, useMemo, useState } from "react";
import {
AssistantRuntimeProvider,
MessagePrimitive,
ThreadPrimitive,
useExternalStoreRuntime,
type AppendMessage,
} from "@assistant-ui/react";
import type { AgentMessage } from "../../demo/agent/events.js"; import type { AgentMessage } from "../../demo/agent/events.js";
import { SchemaApprovalSurface } from "../approval/SchemaApprovalSurface.js"; import { SchemaApprovalSurface } from "../approval/SchemaApprovalSurface.js";
import { projectAgentMessagesForAssistant, type AssistantProjectedMessage } from "./assistantRuntimeProjection.js"; import { projectAgentMessagesForAssistant, type AssistantProjectedMessage } from "./assistantRuntimeProjection.js";
@@ -31,15 +24,13 @@ const formatJson = (value: unknown): string => {
const ToolCard = ({ const ToolCard = ({
toolName, toolName,
args, args,
result, defaultOpen = true,
isError,
}: { }: {
readonly toolName: string; readonly toolName: string;
readonly args?: unknown; readonly args?: unknown;
readonly result?: unknown; readonly defaultOpen?: boolean;
readonly isError?: boolean;
}) => { }) => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(defaultOpen);
return ( return (
<div className="assistant-tool-card" data-open={open ? "true" : "false"}> <div className="assistant-tool-card" data-open={open ? "true" : "false"}>
<button <button
@@ -48,19 +39,13 @@ const ToolCard = ({
aria-expanded={open} aria-expanded={open}
onClick={() => setOpen((c) => !c)} onClick={() => setOpen((c) => !c)}
> >
<span className="assistant-tool-card__label">Used tool: <b>{toolName}</b></span> <span className="assistant-tool-card__label">{toolName}</span>
</button> </button>
{open ? ( {open ? (
<div className="assistant-tool-card__body"> <div className="assistant-tool-card__body">
{args !== undefined ? ( {args !== undefined ? (
<pre className="assistant-tool-card__io" aria-label="tool input">{formatJson(args)}</pre> <pre className="assistant-tool-card__io" aria-label="tool input">{formatJson(args)}</pre>
) : null} ) : null}
{result !== undefined ? (
<div className="assistant-tool-card__result" data-error={isError ? "true" : undefined}>
<span>{isError ? "error" : "success"}</span>
<pre className="assistant-tool-card__io" aria-label="tool output">{formatJson(result)}</pre>
</div>
) : null}
</div> </div>
) : null} ) : null}
</div> </div>
@@ -100,9 +85,10 @@ const renderContentPart = (
part: ContentPart, part: ContentPart,
submitApproval?: (() => void) | undefined, submitApproval?: (() => void) | undefined,
cancelApproval?: (() => void) | undefined, cancelApproval?: (() => void) | undefined,
defaultOpen?: boolean,
): React.ReactNode => { ): React.ReactNode => {
if (part.type === "text") { if (part.type === "text") {
return <MessagePrimitive.Content />; return <p style={{ whiteSpace: "pre-line" }}>{part.text}</p>;
} }
const toolName = part.toolName; const toolName = part.toolName;
const args = part.args as Record<string, unknown> | undefined; const args = part.args as Record<string, unknown> | undefined;
@@ -130,7 +116,7 @@ const renderContentPart = (
</div> </div>
); );
} }
return <ToolCard toolName={toolName} args={part.args} result={part.result} isError={part.isError ?? false} />; return <ToolCard toolName={toolName} args={part.args} {...(defaultOpen !== undefined ? { defaultOpen } : {})} />;
}; };
const AssistantMessageBody = ({ const AssistantMessageBody = ({
@@ -142,11 +128,16 @@ const AssistantMessageBody = ({
readonly submitApproval?: (() => void) | undefined; readonly submitApproval?: (() => void) | undefined;
readonly cancelApproval?: (() => void) | undefined; readonly cancelApproval?: (() => void) | undefined;
}) => { }) => {
const textParts = parts.filter((p) => p.type === "text");
const toolParts = parts.filter((p) => p.type === "tool-call"); const toolParts = parts.filter((p) => p.type === "tool-call");
if (toolParts.length === 0) { if (toolParts.length === 0) {
return <MessagePrimitive.Content />; return (
<>
{parts.filter((p) => p.type === "text").map((p, i) => (
<p key={`text-${i}`} style={{ whiteSpace: "pre-line" }}>{p.text}</p>
))}
</>
);
} }
if (toolParts.length === 1) { if (toolParts.length === 1) {
@@ -164,54 +155,38 @@ const AssistantMessageBody = ({
} }
const firstToolIndex = parts.findIndex((p) => p.type === "tool-call"); const firstToolIndex = parts.findIndex((p) => p.type === "tool-call");
const lastToolIndex = parts.findLastIndex((p) => p.type === "tool-call");
const beforeText = parts.slice(0, firstToolIndex).filter((p) => p.type === "text"); const beforeText = parts.slice(0, firstToolIndex).filter((p) => p.type === "text");
const afterText = parts.slice(lastToolIndex + 1).filter((p) => p.type === "text");
return ( return (
<> <>
{beforeText.map((p, i) => <p key={`before-${i}`} style={{ whiteSpace: "pre-line" }}>{p.text}</p>)} {beforeText.map((p, i) => <p key={`before-${i}`} style={{ whiteSpace: "pre-line" }}>{p.text}</p>)}
<ToolGroupCard toolCount={toolParts.length}> <ToolGroupCard toolCount={toolParts.length}>
{toolParts.map((tool, i) => ( {toolParts.map((tool, i) => (
<div key={i}> <div key={i}>
{renderContentPart(tool, submitApproval, cancelApproval)} {renderContentPart(tool, submitApproval, cancelApproval, false)}
</div> </div>
))} ))}
</ToolGroupCard> </ToolGroupCard>
{afterText.map((p, i) => <p key={`after-${i}`} style={{ whiteSpace: "pre-line" }}>{p.text}</p>)}
</> </>
); );
}; };
const ThreadBody = ({ const MessageBubble = ({
submitApproval, role,
cancelApproval, children,
}: { }: {
readonly submitApproval?: (() => void) | undefined; readonly role: "user" | "assistant";
readonly cancelApproval?: (() => void) | undefined; readonly children: React.ReactNode;
}) => ( }) => (
<ThreadPrimitive.Root className="assistant-thread"> <div className="assistant-message" data-role={role}>
<ThreadPrimitive.Viewport className="assistant-thread__viewport"> {role === "user" ? (
<ThreadPrimitive.Messages> <div className="assistant-message__user-bubble">{children}</div>
{({ message }) => { ) : (
const content = (message as unknown as { content?: unknown }).content; children
const parts: ContentPart[] = Array.isArray(content) )}
? (content as ContentPart[]) </div>
: typeof content === "string"
? [{ type: "text" as const, text: content }]
: [];
return (
<MessagePrimitive.Root
className="assistant-message"
data-role={message.role}
>
<AssistantMessageBody
parts={parts}
submitApproval={submitApproval}
cancelApproval={cancelApproval}
/>
</MessagePrimitive.Root>
);
}}
</ThreadPrimitive.Messages>
</ThreadPrimitive.Viewport>
</ThreadPrimitive.Root>
); );
export const AssistantOperatorThread = ({ export const AssistantOperatorThread = ({
@@ -223,41 +198,53 @@ export const AssistantOperatorThread = ({
ariaLabel = "operator conversation", ariaLabel = "operator conversation",
}: AssistantOperatorThreadProps) => { }: AssistantOperatorThreadProps) => {
const projected = useMemo(() => projectAgentMessagesForAssistant(messages), [messages]); const projected = useMemo(() => projectAgentMessagesForAssistant(messages), [messages]);
const [localMessages, setLocalMessages] = useState<AssistantProjectedMessage[]>([]);
const runtimeMessages = projected.length > 0 ? projected : localMessages;
const onNew = useCallback(async (message: AppendMessage) => { const handleRun = useCallback(() => {
const text = message.content.find((part) => part.type === "text")?.text ?? ""; if (!runAction || runAction.disabled) return;
setLocalMessages((current) => [ runAction.run();
...current, }, [runAction]);
{
id: `local-${current.length + 1}`,
role: "user" as const,
content: [{ type: "text" as const, text }],
},
]);
}, []);
const runtime = useExternalStoreRuntime({
messages: runtimeMessages,
setMessages: (msgs) => setLocalMessages([...msgs]),
onNew,
convertMessage: (message) => message,
isRunning: false,
});
return ( return (
<section className="assistant-operator-thread" data-mode={mode} role="log" aria-label={ariaLabel}> <section className="assistant-operator-thread" data-mode={mode} role="log" aria-label={ariaLabel}>
<div className="assistant-thread">
<div className="assistant-thread__viewport">
{projected.map((message) => {
const content = (message as unknown as { content?: unknown }).content;
const parts: ContentPart[] = Array.isArray(content)
? (content as ContentPart[])
: typeof content === "string"
? [{ type: "text" as const, text: content }]
: [];
if (message.role === "user") {
return (
<MessageBubble key={message.id} role="user">
{parts.filter((p) => p.type === "text").map((p, i) => (
<p key={`text-${i}`} style={{ whiteSpace: "pre-line" }}>{p.text}</p>
))}
</MessageBubble>
);
}
return (
<MessageBubble key={message.id} role="assistant">
<AssistantMessageBody
parts={parts}
submitApproval={submitApproval}
cancelApproval={cancelApproval}
/>
</MessageBubble>
);
})}
</div>
</div>
{runAction ? ( {runAction ? (
<div className="assistant-operator-thread__action"> <div className="assistant-operator-thread__action">
<button type="button" disabled={runAction.disabled} onClick={runAction.run}> <button type="button" disabled={runAction.disabled} onClick={handleRun}>
{runAction.label} {runAction.label}
</button> </button>
</div> </div>
) : null} ) : null}
<AssistantRuntimeProvider runtime={runtime}>
<ThreadBody submitApproval={submitApproval} cancelApproval={cancelApproval} />
</AssistantRuntimeProvider>
</section> </section>
); );
}; };
@@ -10,17 +10,14 @@ afterEach(() => cleanup());
describe("ProblemLoopScene", () => { describe("ProblemLoopScene", () => {
it("uses the assistant transcript surface for the direct-action side", async () => { it("uses the assistant transcript surface for the direct-action side", async () => {
const user = userEvent.setup();
render(<ProblemLoopScene scene={problemScene} beat={findBeat("problem", "direct-actions")!} />); render(<ProblemLoopScene scene={problemScene} beat={findBeat("problem", "direct-actions")!} />);
const transcript = screen.getByRole("log", { name: /one-off assistant transcript/i }); const transcript = screen.getByRole("log", { name: /one-off assistant transcript/i });
expect(transcript).toHaveClass("assistant-operator-thread"); expect(transcript).toHaveClass("assistant-operator-thread");
expect(within(transcript).getByText("Can you finish this workspace task?")).toBeInTheDocument(); expect(within(transcript).getByText("Can you finish this workspace task?")).toBeInTheDocument();
expect(within(transcript).getByRole("button", { name: /workspace.run_once/i })).toBeInTheDocument(); const toolButtons = within(transcript).getAllByRole("button", { name: /workflow.run_once/i });
expect(within(transcript).getByText("Reports success, but leaves no reusable workflow behind.")).toBeInTheDocument(); expect(toolButtons).toHaveLength(3);
expect(within(transcript).getByText("Done. But none of this is recorded in a durable workflow.")).toBeInTheDocument();
await user.click(within(transcript).getByRole("button", { name: /workspace.run_once/i }));
expect(within(transcript).getByText(/ephemeral/i)).toBeInTheDocument();
}); });
it("renders reusable automation as a durable workflow blueprint", () => { it("renders reusable automation as a durable workflow blueprint", () => {
@@ -19,12 +19,28 @@ const oneOffToolLoopMessages: ReadonlyArray<AgentMessage> = [
{ {
type: "tool-call", type: "tool-call",
call: { call: {
id: "scene-2-tool", id: "scene-2-tool-1",
name: "workspace.run_once" as AgentToolName, name: "workflow.run_once" as AgentToolName,
input: { persistence: "ephemeral", reusable_workflow: false }, input: { source: "api", endpoint: "/tasks" },
}, },
}, },
{ type: "text", text: "Reports success, but leaves no reusable workflow behind." }, {
type: "tool-call",
call: {
id: "scene-2-tool-2",
name: "workflow.run_once" as AgentToolName,
input: { format: "json", validate: true },
},
},
{
type: "tool-call",
call: {
id: "scene-2-tool-3",
name: "workflow.run_once" as AgentToolName,
input: { destination: "file", path: "/tmp/result.json" },
},
},
{ type: "text", text: "Done. But none of this is recorded in a durable workflow." },
], ],
}, },
]; ];
@@ -1639,6 +1639,24 @@
margin-bottom: 0.65rem; margin-bottom: 0.65rem;
} }
.assistant-message[data-role="user"] {
justify-items: end;
}
.assistant-message__user-bubble {
background: color-mix(in oklch, var(--accent-cyan) 14%, var(--stage-surface));
border: 1px solid color-mix(in oklch, var(--accent-cyan) 30%, var(--stage-line));
border-radius: 0.65rem;
padding: 0.55rem 0.75rem;
max-width: 80%;
color: var(--text-primary);
font: 0.82rem/1.5 var(--font-interface);
}
.assistant-message[data-role="assistant"] {
justify-items: start;
}
.assistant-tool-card { .assistant-tool-card {
border: 1px solid var(--stage-line); border: 1px solid var(--stage-line);
border-radius: 0.65rem; border-radius: 0.65rem;
@@ -1680,19 +1698,6 @@
overflow-x: auto; overflow-x: auto;
} }
.assistant-tool-card__result {
margin-top: 0.35rem;
font: 600 0.7rem/1 var(--font-interface);
}
.assistant-tool-card__result span {
color: var(--accent-cyan);
}
.assistant-tool-card__result[data-error="true"] span {
color: var(--accent-error);
}
.assistant-tool-group { .assistant-tool-group {
border: 1px solid var(--stage-line); border: 1px solid var(--stage-line);
border-radius: 0.65rem; border-radius: 0.65rem;
@@ -1723,6 +1728,8 @@
flex-direction: column; flex-direction: column;
gap: 0.35rem; gap: 0.35rem;
padding: 0 0.65rem 0.55rem; padding: 0 0.65rem 0.55rem;
max-height: 14rem;
overflow: hidden;
} }
.assistant-tool-group__body .assistant-tool-card { .assistant-tool-group__body .assistant-tool-card {