From a7d98e1382c7ca2f220d628ee337207564356c3b Mon Sep 17 00:00:00 2001 From: Adam Firestone Date: Mon, 10 Aug 2026 14:07:31 -0500 Subject: [PATCH 1/2] fix(web): render provider no-output sentinels as the empty placeholder The Claude Code CLI substitutes literal filler text for empty command output (e.g. "(Bash completed with no output)"), which the web extractor treated as real output, so those rows never hit the italic (No output) placeholder. Treat the known sentinels as empty for non-error command executions, and give the command preview a step more ink than the secondary-label output so wrapped commands stop blending into their output. Claude Fable 5 (Claude Code) --- .../components/chat/MessagesTimeline.test.tsx | 51 +++++++++++++++++++ .../src/components/chat/MessagesTimeline.tsx | 20 ++++++-- 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/apps/web/src/components/chat/MessagesTimeline.test.tsx b/apps/web/src/components/chat/MessagesTimeline.test.tsx index b82ef362cade..4c4b810f93ab 100644 --- a/apps/web/src/components/chat/MessagesTimeline.test.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.test.tsx @@ -519,6 +519,57 @@ describe("MessagesTimeline", () => { expect(markup).not.toContain("Copy output"); }); + it("renders provider no-output sentinels as the empty state", () => { + for (const sentinel of [ + "(Bash completed with no output)", + "(Command completed with no output)", + "(no output)", + "(No content)", + ]) { + const body = buildToolCallExpandedBody( + { + id: "work-command-sentinel-output", + createdAt: MESSAGE_CREATED_AT, + label: "Bash", + tone: "tool", + itemType: "command_execution", + command: "true", + toolData: { rawOutput: { stdout: sentinel } }, + }, + undefined, + ); + expect(body).toEqual({ + blocks: [{ kind: "empty" }], + copyableCommand: "true", + }); + } + }); + + it("keeps sentinel-like text when the command failed", () => { + const body = buildToolCallExpandedBody( + { + id: "work-command-sentinel-error", + createdAt: MESSAGE_CREATED_AT, + label: "Bash", + tone: "tool", + itemType: "command_execution", + command: "true", + toolData: { + result: { content: "(Bash completed with no output)", is_error: true }, + }, + }, + undefined, + ); + expect(body?.blocks).toEqual([ + { + kind: "output", + text: "(Bash completed with no output)", + isError: true, + truncated: false, + }, + ]); + }); + it("labels only error output and retains the truncation notice", () => { const normalBody = buildToolCallExpandedBody( { diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx index 7d398090e38c..0ed91a716a27 100644 --- a/apps/web/src/components/chat/MessagesTimeline.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.tsx @@ -2178,6 +2178,12 @@ function mcpToolCallMetadata(toolData: unknown): unknown { return metadata; } +// Some providers substitute filler text for empty command output (e.g. the +// Claude Code CLI emits "(Bash completed with no output)") instead of an empty +// result; treat those like no output so the placeholder renders consistently. +const EMPTY_COMMAND_OUTPUT_SENTINEL = + /^\((?:[^()\n]+ )?completed with no output\)$|^\(no (?:output|content)\)$/i; + type ToolCallExpandedBodyBlock = | { readonly kind: "output"; @@ -2224,7 +2230,11 @@ export function buildToolCallExpandedBody( const resultOutput = extractToolResultOutput(workEntry.toolData); if (resultOutput) { const trimmed = resultOutput.text.trim(); - if (trimmed && !seen.has(trimmed)) { + const isEmptyOutputSentinel = + workEntry.itemType === "command_execution" && + !resultOutput.isError && + EMPTY_COMMAND_OUTPUT_SENTINEL.test(trimmed); + if (trimmed && !isEmptyOutputSentinel && !seen.has(trimmed)) { seen.add(trimmed); if (resultOutput.truncated) { seen.add("Output truncated"); @@ -2562,8 +2572,12 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: { {preview && ( Date: Mon, 10 Aug 2026 14:08:19 -0500 Subject: [PATCH 2/2] style(web): emphasize command output over the command in tool rows The prior contrast step made the command the darker text, but the output is what usually matters when scanning a row. Swap the hierarchy: output renders at foreground/80 while the command preview returns to secondary-label, which also widens the gap between the two slightly. Claude Fable 5 (Claude Code) --- apps/web/src/components/chat/MessagesTimeline.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx index 0ed91a716a27..a3530005902a 100644 --- a/apps/web/src/components/chat/MessagesTimeline.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.tsx @@ -2285,7 +2285,7 @@ export const WorkEntryExpandedBody = memo(function WorkEntryExpandedBody(props: {blocks.map((block) => block.kind === "output" ? (
-
+            
               {block.isError ? "Error output\n" : null}
               {block.text}
               {block.truncated ? "\n\nOutput truncated" : null}
@@ -2572,12 +2572,10 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: {
               {preview && (