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 <noreply@anthropic.com>
This commit is contained in:
Bryan Helmkamp 2026-07-25 08:42:11 -04:00
parent 73894c0f76
commit 6b9bf2be80
No known key found for this signature in database
2 changed files with 62 additions and 2 deletions

View file

@ -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(
<StageChatView
turns={[
{
kind: "tool",
ts: "2026-04-09T12:00:01Z",
toolName: "shell",
input: "{}",
result: "ok",
isError: false,
durationMs: 5,
},
{
kind: "assistant",
ts: "2026-04-09T12:00:02Z",
content: " ",
inputTokens: 0,
outputTokens: 0,
toolCallCount: 1,
},
{
kind: "tool",
ts: "2026-04-09T12:00:03Z",
toolName: "shell",
input: "{}",
result: "ok",
isError: false,
durationMs: 5,
},
{
kind: "assistant",
ts: "2026-04-09T12:00:04Z",
content: "Done",
inputTokens: 10,
outputTokens: 20,
toolCallCount: null,
},
]}
pendingTools={[]}
stage={stage()}
/>,
);
// 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(
<StageChatView

View file

@ -1586,13 +1586,21 @@ export function StageChatView({
</p>
);
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 (
<div key={`turn-${turnIndex}`} className="flex flex-col gap-1.5">
<Markdown content={turn.content} />
{isFinal && (metric || duration) && (
{hasText && <Markdown content={turn.content} />}
{showFooter && (
<div className="flex gap-3 font-mono text-[11px] text-fg-muted tabular-nums">
{metric && <span>{metric}</span>}
{duration && <span>{duration}</span>}