authorgravatar for maria@kuuro.netmaria <maria@kuuro.net> 2026-09-02 01:19:48-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-02 01:19:48-04:00
logc15735dd8894f829ce396c922ce4b5a48073f05a
treef49662a6a5ce16139a0d2ed26c74b5dc8add0244
parent133db22fae34cfb2e62a806e99d22674b0376f35
signature Signed by PGP key B5690EEEBB952194

fix(chat): replace failed tools with thinking (#9165)

Co-authored-by: maria-rcks <254055478+maria-rcks@users.noreply.github.com>

2 files changed, 16 insertions(+), 4 deletions(-)

apps/web/src/components/chat/MessagesTimeline.logic.test.ts+8-1
...@@ -1411,7 +1411,9 @@ describe("deriveMessagesTimelineRows", () => {...@@ -1411,7 +1411,9 @@ describe("deriveMessagesTimelineRows", () => {
1411 );1411 );
14121412
1413 it("reuses one activity row for initial thinking and the latest tool", () => {1413 it("reuses one activity row for initial thinking and the latest tool", () => {
1414 const deriveRows = (toolLifecycleStatus: "inProgress" | "completed" | "declined" | null) =>1414 const deriveRows = (
1415 toolLifecycleStatus: "inProgress" | "completed" | "failed" | "declined" | null,
1416 ) =>
1415 deriveMessagesTimelineRows({1417 deriveMessagesTimelineRows({
1416 timelineEntries:1418 timelineEntries:
1417 toolLifecycleStatus === null1419 toolLifecycleStatus === null
...@@ -1430,6 +1432,7 @@ describe("deriveMessagesTimelineRows", () => {...@@ -1430,6 +1432,7 @@ describe("deriveMessagesTimelineRows", () => {
1430 requestKind: "command",1432 requestKind: "command",
1431 tone: "tool" as const,1433 tone: "tool" as const,
1432 toolLifecycleStatus,1434 toolLifecycleStatus,
1435 ...(toolLifecycleStatus === "inProgress" ? { detail: "exit code 1" } : {}),
1433 },1436 },
1434 },1437 },
1435 ],1438 ],
...@@ -1448,6 +1451,7 @@ describe("deriveMessagesTimelineRows", () => {...@@ -1448,6 +1451,7 @@ describe("deriveMessagesTimelineRows", () => {
1448 const initialRows = deriveRows(null);1451 const initialRows = deriveRows(null);
1449 const runningRows = deriveRows("inProgress");1452 const runningRows = deriveRows("inProgress");
1450 const completedRows = deriveRows("completed");1453 const completedRows = deriveRows("completed");
1454 const failedRows = deriveRows("failed");
1451 const declinedRows = deriveRows("declined");1455 const declinedRows = deriveRows("declined");
1452 const initialActivityRow = initialRows.find((row) => row.id === "live-activity-row");1456 const initialActivityRow = initialRows.find((row) => row.id === "live-activity-row");
1453 const runningActivityRow = runningRows.find((row) => row.id === "live-activity-row");1457 const runningActivityRow = runningRows.find((row) => row.id === "live-activity-row");
...@@ -1456,11 +1460,14 @@ describe("deriveMessagesTimelineRows", () => {...@@ -1456,11 +1460,14 @@ describe("deriveMessagesTimelineRows", () => {
1456 expect(initialActivityRow).toMatchObject({ kind: "thinking" });1460 expect(initialActivityRow).toMatchObject({ kind: "thinking" });
1457 expect(runningActivityRow).toMatchObject({ kind: "work-live", active: true });1461 expect(runningActivityRow).toMatchObject({ kind: "work-live", active: true });
1458 expect(completedActivityRow).toMatchObject({ kind: "work-live", active: true });1462 expect(completedActivityRow).toMatchObject({ kind: "work-live", active: true });
1463 expect(failedRows.some((row) => row.kind === "work-live")).toBe(false);
1464 expect(failedRows.at(-1)).toMatchObject({ kind: "thinking", id: "live-activity-row" });
1459 expect(declinedRows.find((row) => row.kind === "work-live")).toMatchObject({ active: false });1465 expect(declinedRows.find((row) => row.kind === "work-live")).toMatchObject({ active: false });
1460 expect(declinedRows.at(-1)).toMatchObject({ kind: "thinking", id: "live-activity-row" });1466 expect(declinedRows.at(-1)).toMatchObject({ kind: "thinking", id: "live-activity-row" });
1461 expect(initialRows.filter((row) => row.id === "live-activity-row")).toHaveLength(1);1467 expect(initialRows.filter((row) => row.id === "live-activity-row")).toHaveLength(1);
1462 expect(runningRows.filter((row) => row.id === "live-activity-row")).toHaveLength(1);1468 expect(runningRows.filter((row) => row.id === "live-activity-row")).toHaveLength(1);
1463 expect(completedRows.filter((row) => row.id === "live-activity-row")).toHaveLength(1);1469 expect(completedRows.filter((row) => row.id === "live-activity-row")).toHaveLength(1);
1470 expect(failedRows.filter((row) => row.id === "live-activity-row")).toHaveLength(1);
1464 expect(declinedRows.filter((row) => row.id === "live-activity-row")).toHaveLength(1);1471 expect(declinedRows.filter((row) => row.id === "live-activity-row")).toHaveLength(1);
1465 });1472 });
14661473
apps/web/src/components/chat/MessagesTimeline.logic.ts+8-3
...@@ -691,13 +691,18 @@ export function deriveMessagesTimelineRows(input: {...@@ -691,13 +691,18 @@ export function deriveMessagesTimelineRows(input: {
691 const latestRunningToolEntry = visibleActiveToolEntries.findLast((entry) =>691 const latestRunningToolEntry = visibleActiveToolEntries.findLast((entry) =>
692 workEntryIsActiveTurnActivity(entry.entry),692 workEntryIsActiveTurnActivity(entry.entry),
693 );693 );
694 const latestToolFailed =
695 latestRunningToolEntry === undefined &&
696 latestVisibleToolEntry !== undefined &&
697 latestVisibleToolEntry.entry.toolLifecycleStatus !== "declined" &&
698 workEntryDisplayIndicatesToolFailure(latestVisibleToolEntry.entry);
694 const latestToolKeepsActivityLive =699 const latestToolKeepsActivityLive =
695 latestRunningToolEntry !== undefined ||700 latestRunningToolEntry !== undefined ||
696 (latestVisibleToolEntry !== undefined &&701 (latestVisibleToolEntry !== undefined &&
697 workEntryIndicatesToolSuccess(latestVisibleToolEntry.entry));702 workEntryIndicatesToolSuccess(latestVisibleToolEntry.entry));
698 const activeWorkPlacementEntryId = latestVisibleToolEntry?.id;703 const activeWorkPlacementEntryId = latestVisibleToolEntry?.id;
699 const activeWorkRow =704 const activeWorkRow =
700 activeWorkAnchor && latestVisibleToolEntry705 activeWorkAnchor && latestVisibleToolEntry && !latestToolFailed
701 ? (() => {706 ? (() => {
702 const groupId = workGroupId(activeWorkAnchor.id, activeWorkAnchor.entry);707 const groupId = workGroupId(activeWorkAnchor.id, activeWorkAnchor.entry);
703 return {708 return {
...@@ -715,7 +720,7 @@ export function deriveMessagesTimelineRows(input: {...@@ -715,7 +720,7 @@ export function deriveMessagesTimelineRows(input: {
715 })()720 })()
716 : null;721 : null;
717 const activeWorkEntryIds = new Set(722 const activeWorkEntryIds = new Set(
718 activeWorkRow === null ? [] : activeToolEntries.map((entry) => entry.id),723 activeWorkRow !== null || latestToolFailed ? activeToolEntries.map((entry) => entry.id) : [],
719 );724 );
720 const appendWorkingRow = () => {725 const appendWorkingRow = () => {
721 nextRows.push({726 nextRows.push({
...@@ -912,7 +917,7 @@ export function deriveMessagesTimelineRows(input: {...@@ -912,7 +917,7 @@ export function deriveMessagesTimelineRows(input: {
912 if (input.isWorking && activeTurnHeaderIndex === input.timelineEntries.length) {917 if (input.isWorking && activeTurnHeaderIndex === input.timelineEntries.length) {
913 appendWorkingRow();918 appendWorkingRow();
914 }919 }
915 if (input.isWorking && !hasActivityRow) {920 if (input.isWorking && (!hasActivityRow || latestToolFailed)) {
916 nextRows.push({921 nextRows.push({
917 kind: "thinking",922 kind: "thinking",
918 id: LIVE_ACTIVITY_ROW_ID,923 id: LIVE_ACTIVITY_ROW_ID,