From 45712c1262ea1d294a04beb47d8d93d96aebedb8 Mon Sep 17 00:00:00 2001 From: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz> Date: Tue, 25 Aug 2026 15:44:41 -0700 Subject: [PATCH] Render agent message sends as chat bubbles Co-authored-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz> Signed-off-by: Rizz <302abe414ca6e3134763d2539bfcf145aea2a63fe5f8455204ed602fd40cf381@buzz.block.builderlab.xyz> (cherry picked from commit 62b9603bac9d291829b11950758499bb17669918) Signed-off-by: ss-dev-00 --- .../ui/AgentSessionToolItem/ToolItem.tsx | 7 +++-- .../ui/agentSessionWorkBlockGrouping.test.mjs | 28 +++++++++++++++++++ .../ui/agentSessionWorkBlockGrouping.ts | 18 ++++++++---- 3 files changed, 44 insertions(+), 9 deletions(-) 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" &&