Skip to content
Closed
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
71 changes: 71 additions & 0 deletions apps/web/src/session-logic.command-output.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { deriveWorkLogEntries } from "./session-logic";
function makeCommandActivity(
id: string,
payload: Record<string, unknown>,
overrides: Partial<Pick<OrchestrationThreadActivity, "kind" | "createdAt" | "summary">> = {},
): OrchestrationThreadActivity {
return {
id: EventId.make(id),
Expand All @@ -15,9 +16,79 @@ function makeCommandActivity(
tone: "tool",
payload,
turnId: TurnId.make("turn-1"),
...overrides,
};
}

// Claude puts the command in `detail` with a `Bash: ` prefix. The server
// projects `data.command` onto in-progress updates and keeps
// `data.input.command` on the completed activity.
function makeClaudeBashLifecycle(command: string, detail: string): OrchestrationThreadActivity[] {
const toolCallId = "toolu_01XghhMudoHnUpdrKNfqgcYW";
return [
makeCommandActivity(
"claude-updated",
{
itemType: "command_execution",
toolCallId,
status: "inProgress",
detail,
data: { command },
},
{ kind: "tool.updated", createdAt: "2026-07-17T10:00:00.000Z", summary: "Command run" },
),
makeCommandActivity(
"claude-completed",
{
itemType: "command_execution",
toolCallId,
status: "completed",
detail,
data: {
toolName: "Bash",
input: { command, description: "List files" },
result: { type: "tool_result", content: "", is_error: false },
},
},
{ kind: "tool.completed", createdAt: "2026-07-17T10:00:01.000Z", summary: "Command run" },
),
];
}

describe("deriveWorkLogEntries Claude command detail", () => {
it("drops the tool-name-prefixed detail that repeats the command", () => {
const command = "cd /repo && grep -n -E \"F0[0-9][0-9]\" TRACKER.md | sed -n '1,120p'";
const entries = deriveWorkLogEntries(makeClaudeBashLifecycle(command, `Bash: ${command}`));

expect(entries).toHaveLength(1);
expect(entries[0]?.command).toBe(command);
expect(entries[0]?.detail).toBeUndefined();
});

it("drops the detail when the server truncated it", () => {
const command = `cd /repo && ${"x".repeat(200)} && ls`;
const detail = `Bash: ${command}`.slice(0, 177) + "...";
const entries = deriveWorkLogEntries(makeClaudeBashLifecycle(command, detail));

expect(entries).toHaveLength(1);
expect(entries[0]?.command).toBe(command);
expect(entries[0]?.detail).toBeUndefined();
});

it("reads the command from Claude's completed payload", () => {
const [entry] = deriveWorkLogEntries([
makeCommandActivity("claude-only-completed", {
itemType: "command_execution",
detail: "Bash: bun test",
data: { toolName: "Bash", input: { command: "bun test" } },
}),
]);

expect(entry?.command).toBe("bun test");
expect(entry?.detail).toBeUndefined();
});
});

describe("deriveWorkLogEntries command output", () => {
it("uses Codex aggregated output instead of repeating the command", () => {
const [entry] = deriveWorkLogEntries([
Expand Down
32 changes: 31 additions & 1 deletion apps/web/src/session-logic.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1410,13 +1410,16 @@ function extractToolCommand(payload: Record<string, unknown> | null): {
const item = asRecord(data?.item);
const itemResult = asRecord(item?.result);
const itemInput = asRecord(item?.input);
// Claude completed activities carry the tool input as `data.input`.
const dataInput = asRecord(data?.input);
const itemType = asTrimmedString(payload?.itemType);
const detail = asTrimmedString(payload?.detail);
const candidates: unknown[] = [
item?.command,
itemInput?.command,
itemResult?.command,
data?.command,
dataInput?.command,
itemType === "command_execution" && detail ? stripTrailingExitCode(detail).output : null,
];

Expand Down Expand Up @@ -1465,6 +1468,32 @@ function normalizePreviewForComparison(value: string | null | undefined): string
return normalizeCompactToolLabel(normalizeInlinePreview(normalized)).toLowerCase();
}

/**
* True when a normalized `detail` only repeats the normalized command.
* Claude writes the command into `detail` as `Bash: <command>`, and the server
* truncates long details with a trailing `...`, so both are allowed here.
*/
function detailRepeatsCommand(
normalizedDetail: string | null,
normalizedCommand: string | null,
): boolean {
if (!normalizedDetail || !normalizedCommand) {
return false;
}
if (normalizedDetail === normalizedCommand) {
return true;
}
const withoutToolPrefix = normalizedDetail.replace(/^[a-z][\w.-]*:\s+/, "");
if (withoutToolPrefix === normalizedCommand) {
return true;
}
const ellipsis = /\s*(?:\.\.\.|…)$/.exec(withoutToolPrefix);
if (!ellipsis || ellipsis.index === 0) {
return false;
}
return normalizedCommand.startsWith(withoutToolPrefix.slice(0, ellipsis.index));
}

function summarizeToolTextOutput(value: string): string | null {
const lines: Array<string> = [];
for (const rawLine of value.split(/\r?\n/u)) {
Expand Down Expand Up @@ -1605,7 +1634,8 @@ function extractToolDetail(
detail &&
normalizedHeading !== normalizedDetail &&
(!commandTool ||
(normalizedCommand !== normalizedDetail && normalizedRawCommand !== normalizedDetail))
(!detailRepeatsCommand(normalizedDetail, normalizedCommand) &&
!detailRepeatsCommand(normalizedDetail, normalizedRawCommand)))
) {
return detail;
}
Expand Down
Loading