Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions apps/web/src/components/chat/MessagesTimeline.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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(
{
Expand Down
16 changes: 14 additions & 2 deletions apps/web/src/components/chat/MessagesTimeline.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -2275,7 +2285,7 @@ export const WorkEntryExpandedBody = memo(function WorkEntryExpandedBody(props:
{blocks.map((block) =>
block.kind === "output" ? (
<div key={`output:${block.text}`}>
<pre className="max-h-64 cursor-text overflow-auto whitespace-pre-wrap break-words font-mono text-secondary-label text-[11px] leading-relaxed select-text">
<pre className="max-h-64 cursor-text overflow-auto whitespace-pre-wrap break-words font-mono text-[11px] text-foreground/80 leading-relaxed select-text">
{block.isError ? "Error output\n" : null}
{block.text}
{block.truncated ? "\n\nOutput truncated" : null}
Expand Down Expand Up @@ -2562,6 +2572,8 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: {
{preview && (
<span
className={cn(
// Command text sits a step below the foreground output
// under it so wrapped commands don't blend into the output.
"min-w-0 flex-1 text-secondary-label",
previewIsCommand && "font-mono text-[11px]",
commandUnfurled
Expand Down
Loading