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..a3530005902a 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"); @@ -2275,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}
@@ -2562,6 +2572,8 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: {
{preview && (