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", () => {
14701470 });
14711471 });
14721472
1473 it("keeps a failure visible when other hidden entries succeeded", () => {
1474 const rows = deriveMessagesTimelineRows({
1475 timelineEntries: [
1476 {
1477 id: "failed-work-entry",
1478 kind: "work",
1479 createdAt: "2026-01-01T00:00:01Z",
1480 entry: {
1481 id: "failed-work",
1482 createdAt: "2026-01-01T00:00:01Z",
1483 label: "Ran command",
1484 tone: "tool",
1485 toolLifecycleStatus: "failed",
1486 },
1487 },
1488 {
1489 id: "completed-work-entry",
1490 kind: "work",
1491 createdAt: "2026-01-01T00:00:02Z",
1492 entry: {
1493 id: "completed-work",
1494 createdAt: "2026-01-01T00:00:02Z",
1495 label: "Ran command",
1496 tone: "tool",
1497 toolLifecycleStatus: "completed",
1498 },
1499 },
1500 {
1501 id: "visible-info-entry",
1502 kind: "work",
1503 createdAt: "2026-01-01T00:00:03Z",
1504 entry: {
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 });
1473 it.each([
1474 ["the later success is hidden", ["failed", "completed", "info"], false],
1475 ["the later success is visible", ["failed", "info", "completed"], false],
1476 ["an error-toned entry recovers", ["error", "info", "completed"], false],
1477 ["the final failure is hidden", ["completed", "failed", "info"], true],
1478 ["the final failure is visible", ["failed", "info", "failed"], true],
1479 ["the only failure is visible", ["completed", "info", "failed"], false],
1480 ] as const)(
1481 "uses the final tool call for mixed work groups when %s",
1482 (_, statuses, hasFailure) => {
1483 const timelineEntries = statuses.map((status, index) => {
1484 const id = `work-${index}`;
1485 const createdAt = `2026-01-01T00:00:0${index}Z`;
1486
1487 return {
1488 id: `work-entry-${index}`,
1489 kind: "work" as const,
1490 createdAt,
1491 entry:
1492 status === "info"
1493 ? { id, createdAt, label: "Status updated", tone: "info" as const }
1494 : status === "error"
1495 ? { id, createdAt, label: "Command failed", tone: "error" as const }
1496 : {
1497 id,
1498 createdAt,
1499 label: "Ran command",
1500 tone: "tool" as const,
1501 toolLifecycleStatus: status,
1502 },
1503 };
1504 });
15171505
1518 expect(rows.find((row) => row.kind === "work-toggle")).toMatchObject({
1519 hiddenCount: 2,
1520 summary: null,
1521 hasFailure: true,
1522 });
1523 });
1506 const rows = deriveMessagesTimelineRows({
1507 timelineEntries,
1508 isWorking: false,
1509 activeTurnStartedAt: null,
1510 turnDiffSummaryByAssistantMessageId: new Map(),
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 );
15241521});
15251522
15261523describe("computeStableMessagesTimelineRows", () => {
apps/web/src/components/chat/MessagesTimeline.logic.ts+6-3
......@@ -953,6 +953,8 @@ export function deriveMessagesTimelineRows(input: {
953953 }
954954
955955 if (hiddenEntries.length > 0) {
956 const latestToolEntry = visibleGroupedEntries.findLast(workLogEntryIsToolLike);
957
956958 nextRows.push({
957959 kind: "work-toggle",
958960 id: `work-toggle:${timelineEntry.id}`,
......@@ -963,9 +965,10 @@ export function deriveMessagesTimelineRows(input: {
963965 onlyToolEntries: hiddenEntries.every(workLogEntryIsToolLike),
964966 summary: null,
965967 summaryKind: null,
966 hasFailure: hiddenEntries.some((entry) =>
967 workEntryDisplayIndicatesToolFailure(entry),
968 ),
968 hasFailure:
969 latestToolEntry !== undefined &&
970 workEntryDisplayIndicatesToolFailure(latestToolEntry) &&
971 hiddenEntries.some(workEntryDisplayIndicatesToolFailure),
969972 });
970973 }
971974 }
apps/web/src/components/chat/MessagesTimeline.test.tsx+50
......@@ -973,6 +973,56 @@ describe("MessagesTimeline", () => {
973973 expect(markup).not.toContain('aria-label="Tool call failed"');
974974 });
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
9761026 it("shows the animated one-line label for a live tool group", () => {
9771027 const turnId = TurnId.make("turn-live");
9781028 const markup = renderToStaticMarkup(