authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-08-23 06:34:48-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-08-23 06:34:48-07:00
logb1670ac7d9b5b7bb9d7ebd969f27384daee22813
treed307e7390de24259986bedf122cada67d4e395d9
parentea8c9e5ca3ace89cbf6cf0a2aa03047aab1d3ef9
signature Signed by PGP key B5690EEEBB952194

fix(web): stop recovered tool failures from marking work logs red (#7999)


3 files changed, 103 insertions(+), 53 deletions(-)

apps/web/src/components/chat/MessagesTimeline.logic.test.ts+47-50
...@@ -1470,57 +1470,54 @@ describe("deriveMessagesTimelineRows", () => {...@@ -1470,57 +1470,54 @@ describe("deriveMessagesTimelineRows", () => {
1470 });1470 });
1471 });1471 });
14721472
1473 it("keeps a failure visible when other hidden entries succeeded", () => {1473 it.each([
1474 const rows = deriveMessagesTimelineRows({1474 ["the later success is hidden", ["failed", "completed", "info"], false],
1475 timelineEntries: [1475 ["the later success is visible", ["failed", "info", "completed"], false],
1476 {1476 ["an error-toned entry recovers", ["error", "info", "completed"], false],
1477 id: "failed-work-entry",1477 ["the final failure is hidden", ["completed", "failed", "info"], true],
1478 kind: "work",1478 ["the final failure is visible", ["failed", "info", "failed"], true],
1479 createdAt: "2026-01-01T00:00:01Z",1479 ["the only failure is visible", ["completed", "info", "failed"], false],
1480 entry: {1480 ] as const)(
1481 id: "failed-work",1481 "uses the final tool call for mixed work groups when %s",
1482 createdAt: "2026-01-01T00:00:01Z",1482 (_, statuses, hasFailure) => {
1483 label: "Ran command",1483 const timelineEntries = statuses.map((status, index) => {
1484 tone: "tool",1484 const id = `work-${index}`;
1485 toolLifecycleStatus: "failed",1485 const createdAt = `2026-01-01T00:00:0${index}Z`;
1486 },1486
1487 },1487 return {
1488 {1488 id: `work-entry-${index}`,
1489 id: "completed-work-entry",1489 kind: "work" as const,
1490 kind: "work",1490 createdAt,
1491 createdAt: "2026-01-01T00:00:02Z",1491 entry:
1492 entry: {1492 status === "info"
1493 id: "completed-work",1493 ? { id, createdAt, label: "Status updated", tone: "info" as const }
1494 createdAt: "2026-01-01T00:00:02Z",1494 : status === "error"
1495 label: "Ran command",1495 ? { id, createdAt, label: "Command failed", tone: "error" as const }
1496 tone: "tool",1496 : {
1497 toolLifecycleStatus: "completed",1497 id,
1498 },1498 createdAt,
1499 },1499 label: "Ran command",
1500 {1500 tone: "tool" as const,
1501 id: "visible-info-entry",1501 toolLifecycleStatus: status,
1502 kind: "work",1502 },
1503 createdAt: "2026-01-01T00:00:03Z",1503 };
1504 entry: {1504 });
1505 id: "visible-info",
1506 createdAt: "2026-01-01T00:00:03Z",
1507 label: "Status updated",
1508 tone: "info",
1509 },
1510 },
1511 ],
1512 isWorking: false,
1513 activeTurnStartedAt: null,
1514 turnDiffSummaryByAssistantMessageId: new Map(),
1515 revertTurnCountByUserMessageId: new Map(),
1516 });
15171505
1518 expect(rows.find((row) => row.kind === "work-toggle")).toMatchObject({1506 const rows = deriveMessagesTimelineRows({
1519 hiddenCount: 2,1507 timelineEntries,
1520 summary: null,1508 isWorking: false,
1521 hasFailure: true,1509 activeTurnStartedAt: null,
1522 });1510 turnDiffSummaryByAssistantMessageId: new Map(),
1523 });1511 revertTurnCountByUserMessageId: new Map(),
1512 });
1513
1514 expect(rows.find((row) => row.kind === "work-toggle")).toMatchObject({
1515 hiddenCount: 2,
1516 summary: null,
1517 hasFailure,
1518 });
1519 },
1520 );
1524});1521});
15251522
1526describe("computeStableMessagesTimelineRows", () => {1523describe("computeStableMessagesTimelineRows", () => {
apps/web/src/components/chat/MessagesTimeline.logic.ts+6-3
...@@ -953,6 +953,8 @@ export function deriveMessagesTimelineRows(input: {...@@ -953,6 +953,8 @@ export function deriveMessagesTimelineRows(input: {
953 }953 }
954954
955 if (hiddenEntries.length > 0) {955 if (hiddenEntries.length > 0) {
956 const latestToolEntry = visibleGroupedEntries.findLast(workLogEntryIsToolLike);
957
956 nextRows.push({958 nextRows.push({
957 kind: "work-toggle",959 kind: "work-toggle",
958 id: `work-toggle:${timelineEntry.id}`,960 id: `work-toggle:${timelineEntry.id}`,
...@@ -963,9 +965,10 @@ export function deriveMessagesTimelineRows(input: {...@@ -963,9 +965,10 @@ export function deriveMessagesTimelineRows(input: {
963 onlyToolEntries: hiddenEntries.every(workLogEntryIsToolLike),965 onlyToolEntries: hiddenEntries.every(workLogEntryIsToolLike),
964 summary: null,966 summary: null,
965 summaryKind: null,967 summaryKind: null,
966 hasFailure: hiddenEntries.some((entry) =>968 hasFailure:
967 workEntryDisplayIndicatesToolFailure(entry),969 latestToolEntry !== undefined &&
968 ),970 workEntryDisplayIndicatesToolFailure(latestToolEntry) &&
971 hiddenEntries.some(workEntryDisplayIndicatesToolFailure),
969 });972 });
970 }973 }
971 }974 }
apps/web/src/components/chat/MessagesTimeline.test.tsx+50
...@@ -973,6 +973,56 @@ describe("MessagesTimeline", () => {...@@ -973,6 +973,56 @@ describe("MessagesTimeline", () => {
973 expect(markup).not.toContain('aria-label="Tool call failed"');973 expect(markup).not.toContain('aria-label="Tool call failed"');
974 });974 });
975975
976 it("keeps mixed work logs neutral after a later tool call succeeds", () => {
977 const markup = renderToStaticMarkup(
978 <MessagesTimeline
979 {...buildProps()}
980 timelineEntries={[
981 {
982 id: "entry-failed",
983 kind: "work",
984 createdAt: "2026-03-17T19:12:28.000Z",
985 entry: {
986 id: "work-failed",
987 createdAt: "2026-03-17T19:12:28.000Z",
988 label: "Run search",
989 tone: "tool",
990 itemType: "command_execution",
991 toolLifecycleStatus: "failed",
992 },
993 },
994 {
995 id: "entry-info",
996 kind: "work",
997 createdAt: "2026-03-17T19:12:29.000Z",
998 entry: {
999 id: "work-info",
1000 createdAt: "2026-03-17T19:12:29.000Z",
1001 label: "Status updated",
1002 tone: "info",
1003 },
1004 },
1005 {
1006 id: "entry-completed",
1007 kind: "work",
1008 createdAt: "2026-03-17T19:12:30.000Z",
1009 entry: {
1010 id: "work-completed",
1011 createdAt: "2026-03-17T19:12:30.000Z",
1012 label: "Run tests",
1013 tone: "tool",
1014 itemType: "command_execution",
1015 toolLifecycleStatus: "completed",
1016 },
1017 },
1018 ]}
1019 />,
1020 );
1021
1022 expect(markup).toContain("+2 previous log entries");
1023 expect(markup).not.toContain('aria-label="Hidden work includes a failure"');
1024 });
1025
976 it("shows the animated one-line label for a live tool group", () => {1026 it("shows the animated one-line label for a live tool group", () => {
977 const turnId = TurnId.make("turn-live");1027 const turnId = TurnId.make("turn-live");
978 const markup = renderToStaticMarkup(1028 const markup = renderToStaticMarkup(