Rizz
2026-08-25 15:44:41 -07:00
committed by ss-dev-00
parent 8cea5e15ef
commit 45712c1262
3 changed files with 44 additions and 9 deletions

View File

@@ -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 (
<div

View File

@@ -210,6 +210,34 @@ test("the plan stays a sibling and splits the work around it", () => {
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(

View File

@@ -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" &&