From c15735dd8894f829ce396c922ce4b5a48073f05a Mon Sep 17 00:00:00 2001 From: maria Date: Wed, 2 Sep 2026 01:19:48 -0400 Subject: [PATCH] fix(chat): replace failed tools with thinking (#9165) Co-authored-by: maria-rcks <254055478+maria-rcks@users.noreply.github.com> --- .../components/chat/MessagesTimeline.logic.test.ts | 9 ++++++++- .../web/src/components/chat/MessagesTimeline.logic.ts | 11 ++++++++--- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/apps/web/src/components/chat/MessagesTimeline.logic.test.ts b/apps/web/src/components/chat/MessagesTimeline.logic.test.ts index 83d55e285e986539f9df3ca3f765078120ba7c11..de6632093047799e2a06bd2d45865b0a62f09370 100644 --- a/apps/web/src/components/chat/MessagesTimeline.logic.test.ts +++ b/apps/web/src/components/chat/MessagesTimeline.logic.test.ts @@ -1411,7 +1411,9 @@ describe("deriveMessagesTimelineRows", () => { ); it("reuses one activity row for initial thinking and the latest tool", () => { - const deriveRows = (toolLifecycleStatus: "inProgress" | "completed" | "declined" | null) => + const deriveRows = ( + toolLifecycleStatus: "inProgress" | "completed" | "failed" | "declined" | null, + ) => deriveMessagesTimelineRows({ timelineEntries: toolLifecycleStatus === null @@ -1430,6 +1432,7 @@ describe("deriveMessagesTimelineRows", () => { requestKind: "command", tone: "tool" as const, toolLifecycleStatus, + ...(toolLifecycleStatus === "inProgress" ? { detail: "exit code 1" } : {}), }, }, ], @@ -1448,6 +1451,7 @@ describe("deriveMessagesTimelineRows", () => { const initialRows = deriveRows(null); const runningRows = deriveRows("inProgress"); const completedRows = deriveRows("completed"); + const failedRows = deriveRows("failed"); const declinedRows = deriveRows("declined"); const initialActivityRow = initialRows.find((row) => row.id === "live-activity-row"); const runningActivityRow = runningRows.find((row) => row.id === "live-activity-row"); @@ -1456,11 +1460,14 @@ describe("deriveMessagesTimelineRows", () => { expect(initialActivityRow).toMatchObject({ kind: "thinking" }); expect(runningActivityRow).toMatchObject({ kind: "work-live", active: true }); expect(completedActivityRow).toMatchObject({ kind: "work-live", active: true }); + expect(failedRows.some((row) => row.kind === "work-live")).toBe(false); + expect(failedRows.at(-1)).toMatchObject({ kind: "thinking", id: "live-activity-row" }); expect(declinedRows.find((row) => row.kind === "work-live")).toMatchObject({ active: false }); expect(declinedRows.at(-1)).toMatchObject({ kind: "thinking", id: "live-activity-row" }); expect(initialRows.filter((row) => row.id === "live-activity-row")).toHaveLength(1); expect(runningRows.filter((row) => row.id === "live-activity-row")).toHaveLength(1); expect(completedRows.filter((row) => row.id === "live-activity-row")).toHaveLength(1); + expect(failedRows.filter((row) => row.id === "live-activity-row")).toHaveLength(1); expect(declinedRows.filter((row) => row.id === "live-activity-row")).toHaveLength(1); }); diff --git a/apps/web/src/components/chat/MessagesTimeline.logic.ts b/apps/web/src/components/chat/MessagesTimeline.logic.ts index 41ec77b1eb5a843d493a9801c09ffe42069d5718..7cea3803827daad14d62e6e40510dcf36ed9ebb5 100644 --- a/apps/web/src/components/chat/MessagesTimeline.logic.ts +++ b/apps/web/src/components/chat/MessagesTimeline.logic.ts @@ -691,13 +691,18 @@ export function deriveMessagesTimelineRows(input: { const latestRunningToolEntry = visibleActiveToolEntries.findLast((entry) => workEntryIsActiveTurnActivity(entry.entry), ); + const latestToolFailed = + latestRunningToolEntry === undefined && + latestVisibleToolEntry !== undefined && + latestVisibleToolEntry.entry.toolLifecycleStatus !== "declined" && + workEntryDisplayIndicatesToolFailure(latestVisibleToolEntry.entry); const latestToolKeepsActivityLive = latestRunningToolEntry !== undefined || (latestVisibleToolEntry !== undefined && workEntryIndicatesToolSuccess(latestVisibleToolEntry.entry)); const activeWorkPlacementEntryId = latestVisibleToolEntry?.id; const activeWorkRow = - activeWorkAnchor && latestVisibleToolEntry + activeWorkAnchor && latestVisibleToolEntry && !latestToolFailed ? (() => { const groupId = workGroupId(activeWorkAnchor.id, activeWorkAnchor.entry); return { @@ -715,7 +720,7 @@ export function deriveMessagesTimelineRows(input: { })() : null; const activeWorkEntryIds = new Set( - activeWorkRow === null ? [] : activeToolEntries.map((entry) => entry.id), + activeWorkRow !== null || latestToolFailed ? activeToolEntries.map((entry) => entry.id) : [], ); const appendWorkingRow = () => { nextRows.push({ @@ -912,7 +917,7 @@ export function deriveMessagesTimelineRows(input: { if (input.isWorking && activeTurnHeaderIndex === input.timelineEntries.length) { appendWorkingRow(); } - if (input.isWorking && !hasActivityRow) { + if (input.isWorking && (!hasActivityRow || latestToolFailed)) { nextRows.push({ kind: "thinking", id: LIVE_ACTIVITY_ROW_ID, -- 2.54.0