Skip to content

Commit 490318a

Browse files
fix(web): keep failed tool items in the collapsed group summaries
An ordinary exit-code failure knocked its whole tool group out of the "Ran N commands" summary (and the live pill mid-run) into the raw "+N previous tool calls" clamp: v2's derived tone marked any status=failed item as "error", which the grouping treats as a non-tool row. That inverts v1's semantics and #8395's muted-failure rule — the failed lifecycle status already carries the X marker and the summary's includes-a-failure hint, so the tone override goes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent d6ed793 commit 490318a

3 files changed

Lines changed: 91 additions & 1 deletion

File tree

‎apps/web/src/components/chat/MessagesTimeline.logic.test.ts‎

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1251,6 +1251,53 @@ describe("deriveMessagesTimelineRows", () => {
12511251
expect(assistantRow?.showAssistantCopyButton).toBe(false);
12521252
});
12531253

1254+
it("summarizes a settled tool group even when a command in it failed", () => {
1255+
const rows = deriveMessagesTimelineRows({
1256+
timelineEntries: [
1257+
{
1258+
id: "work-entry-ok",
1259+
kind: "work" as const,
1260+
createdAt: "2026-01-01T00:00:01Z",
1261+
entry: {
1262+
id: "work-ok",
1263+
createdAt: "2026-01-01T00:00:01Z",
1264+
label: "Ran command",
1265+
command: "true",
1266+
tone: "tool" as const,
1267+
itemType: "command_execution" as const,
1268+
toolLifecycleStatus: "completed" as const,
1269+
},
1270+
},
1271+
{
1272+
id: "work-entry-failed",
1273+
kind: "work" as const,
1274+
createdAt: "2026-01-01T00:00:02Z",
1275+
entry: {
1276+
id: "work-failed",
1277+
createdAt: "2026-01-01T00:00:02Z",
1278+
label: "Ran command",
1279+
command: "ssh host true",
1280+
detail: "connection refused",
1281+
tone: "tool" as const,
1282+
itemType: "command_execution" as const,
1283+
toolLifecycleStatus: "failed" as const,
1284+
},
1285+
},
1286+
],
1287+
isWorking: false,
1288+
activeTurnStartedAt: null,
1289+
turnDiffSummaryByAssistantMessageId: new Map(),
1290+
revertTurnCountByUserMessageId: new Map(),
1291+
});
1292+
1293+
expect(rows).toHaveLength(1);
1294+
expect(rows[0]).toMatchObject({
1295+
kind: "work-toggle",
1296+
summary: "Ran 2 commands",
1297+
hasFailure: true,
1298+
});
1299+
});
1300+
12541301
it("collapses contiguous settled tool entries behind a summary toggle", () => {
12551302
const timelineEntries = [
12561303
{

‎apps/web/src/session-logic.test.ts‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,50 @@ describe("V2 session presentation", () => {
379379
}
380380
});
381381

382+
it("keeps failed tool items tool-toned so groups still summarize", () => {
383+
const failedCommand = {
384+
id: TurnItemId.make("item-failed-command"),
385+
threadId: ThreadId.make("thread-1"),
386+
runId: RunId.make("run-1"),
387+
nodeId: null,
388+
providerThreadId: null,
389+
providerTurnId: null,
390+
nativeItemRef: null,
391+
parentItemId: null,
392+
ordinal: 0,
393+
status: "failed" as const,
394+
title: null,
395+
startedAt: null,
396+
completedAt: null,
397+
updatedAt: DateTime.nowUnsafe(),
398+
type: "command_execution" as const,
399+
input: "ssh host true",
400+
output: "connection refused",
401+
exitCode: 255,
402+
} satisfies OrchestrationV2TurnItem;
403+
const entries = deriveTimelineEntriesFromVisibleTurnItems({
404+
visibleTurnItems: [
405+
{
406+
position: 0,
407+
visibility: "local" as const,
408+
sourceThreadId: ThreadId.make("thread-1"),
409+
sourceItemId: failedCommand.id,
410+
item: failedCommand,
411+
} as never,
412+
],
413+
optimisticMessages: [],
414+
});
415+
const entry = entries[0];
416+
expect(entry?.kind).toBe("work");
417+
if (entry?.kind === "work") {
418+
// An exit-code failure is still an ordinary tool row: the failed
419+
// lifecycle status carries the marker, and an "error" tone here would
420+
// knock the whole group out of the "Ran N commands" summary.
421+
expect(entry.entry.tone).toBe("tool");
422+
expect(entry.entry.toolLifecycleStatus).toBe("failed");
423+
}
424+
});
425+
382426
it("waits for a dispatched turn item before adding queued input to the timeline", () => {
383427
const projection = makeThreadProjectionFixture();
384428
const now = DateTime.makeUnsafe("2026-06-20T00:00:00.000Z");

‎apps/web/src/session-logic.ts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -468,7 +468,6 @@ function projectedWorkEntryStatus(
468468

469469
function projectedWorkEntryTone(item: OrchestrationV2TurnItem): WorkLogEntry["tone"] {
470470
if (item.type === "error") return "info";
471-
if (item.status === "failed") return "error";
472471
if (item.type === "reasoning") return "thinking";
473472
switch (item.type) {
474473
case "command_execution":

0 commit comments

Comments
 (0)