From 6b9bf2be8033008bf5ee142b202f921213c4bae6 Mon Sep 17 00:00:00 2001 From: Bryan Helmkamp Date: Sat, 25 Jul 2026 08:42:11 -0400 Subject: [PATCH] Skip text-free assistant turns in the Chat view A text-free agent message marks the boundary between two batches of tool calls, so it stays in the turn stream to keep those batches as separate "N tool calls" chips. But it rendered an empty prose div, which still took a slot in the gap-4 column and doubled the vertical space between the chips on either side of it. Render nothing for those turns instead. The final assistant turn still renders when it carries a token/duration footer, even with no text, so the completed-stage metrics are unchanged. Co-Authored-By: Claude Fable 5 --- .../app/routes/run-stages-chat.test.tsx | 52 +++++++++++++++++++ apps/fabro-web/app/routes/run-stages.tsx | 12 ++++- 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/apps/fabro-web/app/routes/run-stages-chat.test.tsx b/apps/fabro-web/app/routes/run-stages-chat.test.tsx index 9ca84db99..be86b89fd 100644 --- a/apps/fabro-web/app/routes/run-stages-chat.test.tsx +++ b/apps/fabro-web/app/routes/run-stages-chat.test.tsx @@ -52,6 +52,58 @@ describe("StageChatView", () => { expect(html).toContain("1m 12s"); }); + test("renders no node for a text-free assistant turn between tool batches", () => { + const html = renderToStaticMarkup( + , + ); + + // The boundary keeps the two batches as separate chips, but contributes + // no element of its own between them. + expect(html.match(/1 tool call/g)).toHaveLength(2); + expect(html).not.toContain('class="prose prose-sm max-w-none"'); + expect(html.match(/class="prose /g)).toHaveLength(1); + expect(html).toContain("Done"); + }); + test("connects a long prompt's expand button to its controlled content", () => { const html = renderToStaticMarkup( ); case "assistant": { + const hasText = turn.content.trim().length > 0; const metric = turnMetric(turn); const isFinal = turnIndex === lastAssistantTurnIndex && !stageActive; + const showFooter = isFinal && Boolean(metric || duration); + // A text-free assistant turn is the boundary between two batches + // of tool calls, kept in the turn stream so those batches stay + // separate chips. It has nothing to show, and an empty node would + // still take a slot in this gap-4 column, doubling the space + // between the chips on either side of it. + if (!hasText && !showFooter) return null; return (
- - {isFinal && (metric || duration) && ( + {hasText && } + {showFooter && (
{metric && {metric}} {duration && {duration}}