diff --git a/desktop/src/features/agents/ui/AgentSessionToolItem/ToolItem.tsx b/desktop/src/features/agents/ui/AgentSessionToolItem/ToolItem.tsx index 85a484deb5..86a77de671 100644 --- a/desktop/src/features/agents/ui/AgentSessionToolItem/ToolItem.tsx +++ b/desktop/src/features/agents/ui/AgentSessionToolItem/ToolItem.tsx @@ -59,9 +59,10 @@ export function ToolItem({ [], ); - // On a work block's rail a posted message is a step the agent took, so it - // takes the same muted, expandable row as every other step. Everywhere else - // it keeps the bubble: see `useIsInsideWorkBlockRail`. + // Message presentations keep their readable bubble whenever they are standalone. + // The conversation grouping keeps message sends standalone rather than placing + // them on a work block rail; this guard preserves the muted row if another + // transcript composition explicitly embeds one in a rail. if (compactSummary.presentation === "message" && !insideWorkBlockRail) { return (
{ assert.deepEqual(blockItemIds(grouped[2]), ["tool:2"]); }); +test("message sends stay outside the work block as chat bubbles", () => { + const send = tool("tool:send", { + args: { content: "Posted an update" }, + buzzToolName: "send_message", + descriptor: { + renderClass: "message", + label: "Sent message", + preview: "Posted an update", + tone: "neutral", + source: "buzz", + }, + renderClass: "message", + toolName: "send_message", + }); + const grouped = groupConversationWorkBlocks( + itemSegments(tool("tool:1"), send, tool("tool:2")), + ); + + assert.deepEqual(kinds(grouped), ["work-block", "item", "work-block"]); + assert.deepEqual(blockItemIds(grouped[0]), ["tool:1"]); + assert.equal( + grouped[1].item.id, + "tool:send", + "the existing message presenter can render the send as a readable bubble", + ); + assert.deepEqual(blockItemIds(grouped[2]), ["tool:2"]); +}); + test("permission gates and errors stay loud, in position", () => { for (const renderClass of ["permission", "error", "status"]) { const grouped = groupConversationWorkBlocks( diff --git a/desktop/src/features/agents/ui/agentSessionWorkBlockGrouping.ts b/desktop/src/features/agents/ui/agentSessionWorkBlockGrouping.ts index 9d076f84f1..abdefb7109 100644 --- a/desktop/src/features/agents/ui/agentSessionWorkBlockGrouping.ts +++ b/desktop/src/features/agents/ui/agentSessionWorkBlockGrouping.ts @@ -2,6 +2,7 @@ import type { TranscriptTurnSegment, TranscriptDisplayBlock, } from "./agentSessionTranscriptGrouping"; +import { buildCompactToolSummary } from "./agentSessionToolSummary"; import type { TranscriptItem } from "./agentSessionTypes"; /** Reasoning. */ @@ -63,12 +64,14 @@ export type TranscriptConversationSegment = * Which items are *work* — the material the block absorbs. * * - **thoughts** — reasoning is a step on the rail, not a sibling disclosure. - * - **tool steps**, including failed ones. A failure belongs to the work it - * happened in; the folded line reports it (`N steps · 1 failed`) so it is - * never hidden behind a neutral count. + * - **tool steps**, including failed ones, except message sends. A failure + * belongs to the work it happened in; the folded line reports it + * (`N steps · 1 failed`) so it is never hidden behind a neutral count. + * Message sends stay outside as chat bubbles: they are communication the + * reader should be able to read and open, not generic work telemetry. * - **interim agent notes** — an assistant message that is not the turn's - * answer. A relay post the agent makes mid-turn is a tool step and lands - * here for the same reason: it reads as progress, not as the reply. + * answer. Unlike a message-send tool, a note is narration within the turn, + * so it stays on the rail as progress. * * Everything else stays outside and keeps its own row. Plans stay a sibling * (Buzz's checklist is a first-class surface, not a step), and lifecycle rows — @@ -84,7 +87,10 @@ function isWorkItem( item: TranscriptItem, finalAnswerId: string | null, ): item is WorkBlockItem { - if (item.type === "thought" || item.type === "tool") return true; + if (item.type === "thought") return true; + if (item.type === "tool") { + return buildCompactToolSummary(item).presentation !== "message"; + } return ( item.type === "message" && item.role === "assistant" &&