| author | |
| committer | |
| log | b1670ac7d9b5b7bb9d7ebd969f27384daee22813 |
| tree | d307e7390de24259986bedf122cada67d4e395d9 |
| parent | ea8c9e5ca3ace89cbf6cf0a2aa03047aab1d3ef9 |
| signature | Signed by PGP key B5690EEEBB952194 |
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 | }); |
| 1472 | 1472 | ||
| 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 | }); | ||
| 1517 | 1505 | ||
| 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 | }); |
| 1525 | 1522 | ||
| 1526 | describe("computeStableMessagesTimelineRows", () => { | 1523 | describe("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 | } |
| 954 | 954 | ||
| 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 | }); |
| 975 | 975 | ||
| 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( |