| author | |
| committer | |
| log | f1e6f0c9bbe527a76eaae9e3b498a5977ce3c7d6 |
| tree | ef84098d464044de3705cbe2b12a8cc237ea830c |
| parent | b982847ab201795711884d1dfadfddb3864adb23 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>7 files changed, 204 insertions(+), 35 deletions(-)
apps/mobile/src/features/threads/thread-work-log.tsx+1-1| ... | @@ -239,7 +239,7 @@ export function ThreadWorkLog(props: { | ... | @@ -239,7 +239,7 @@ export function ThreadWorkLog(props: { |
| 239 | : { ios: "minus", android: "remove" } | 239 | : { ios: "minus", android: "remove" } |
| 240 | } | 240 | } |
| 241 | size={11} | 241 | size={11} |
| 242 | tintColor={row.status === "failure" ? "#e11d48" : props.iconSubtleColor} | 242 | tintColor={props.iconSubtleColor} |
| 243 | type="monochrome" | 243 | type="monochrome" |
| 244 | /> | 244 | /> |
| 245 | ) : null} | 245 | ) : null} |
apps/mobile/src/lib/threadActivity.test.ts+32| ... | @@ -324,6 +324,38 @@ describe("buildThreadFeed", () => { | ... | @@ -324,6 +324,38 @@ describe("buildThreadFeed", () => { |
| 324 | ]); | 324 | ]); |
| 325 | }); | 325 | }); |
| 326 | 326 | ||
| 327 | it("drops runtime warnings with no displayable content", () => { | ||
| 328 | const thread = makeThread({ | ||
| 329 | id: ThreadId.make("thread-noise"), | ||
| 330 | projectId: ProjectId.make("project-1"), | ||
| 331 | title: "Warning noise thread", | ||
| 332 | activities: [ | ||
| 333 | makeActivity({ | ||
| 334 | id: EventId.make("activity-noise"), | ||
| 335 | kind: "runtime.warning", | ||
| 336 | summary: "Claude system message 'background_tasks_changed' (no displayable text content)", | ||
| 337 | createdAt: "2026-04-01T00:00:02.000Z", | ||
| 338 | turnId: TurnId.make("turn-1"), | ||
| 339 | }), | ||
| 340 | makeActivity({ | ||
| 341 | id: EventId.make("activity-signal"), | ||
| 342 | kind: "runtime.warning", | ||
| 343 | summary: "Reconnecting... 2/5", | ||
| 344 | createdAt: "2026-04-01T00:00:03.000Z", | ||
| 345 | turnId: TurnId.make("turn-1"), | ||
| 346 | }), | ||
| 347 | ], | ||
| 348 | }); | ||
| 349 | |||
| 350 | const feed = buildThreadFeed(thread); | ||
| 351 | expect(feed).toMatchObject([ | ||
| 352 | { | ||
| 353 | type: "activity-group", | ||
| 354 | activities: [{ id: "activity-signal" }], | ||
| 355 | }, | ||
| 356 | ]); | ||
| 357 | }); | ||
| 358 | |||
| 327 | it("collapses matching tool lifecycle rows like desktop", () => { | 359 | it("collapses matching tool lifecycle rows like desktop", () => { |
| 328 | const thread = makeThread({ | 360 | const thread = makeThread({ |
| 329 | id: ThreadId.make("thread-2"), | 361 | id: ThreadId.make("thread-2"), |
apps/mobile/src/lib/threadActivity.ts+12| ... | @@ -338,6 +338,7 @@ function deriveWorkLogEntries( | ... | @@ -338,6 +338,7 @@ function deriveWorkLogEntries( |
| 338 | if (activity.kind === "tool.progress") continue; | 338 | if (activity.kind === "tool.progress") continue; |
| 339 | if (activity.kind === "context-window.updated") continue; | 339 | if (activity.kind === "context-window.updated") continue; |
| 340 | if (activity.summary === "Checkpoint captured") continue; | 340 | if (activity.summary === "Checkpoint captured") continue; |
| 341 | if (isNoContentRuntimeWarning(activity)) continue; | ||
| 341 | if (isPlanBoundaryToolActivity(activity)) continue; | 342 | if (isPlanBoundaryToolActivity(activity)) continue; |
| 342 | if (isAgentInternalActivity(activity)) continue; | 343 | if (isAgentInternalActivity(activity)) continue; |
| 343 | entries.push(toDerivedWorkLogEntry(activity)); | 344 | entries.push(toDerivedWorkLogEntry(activity)); |
| ... | @@ -345,6 +346,17 @@ function deriveWorkLogEntries( | ... | @@ -345,6 +346,17 @@ function deriveWorkLogEntries( |
| 345 | return collapseDerivedWorkLogEntries(entries); | 346 | return collapseDerivedWorkLogEntries(entries); |
| 346 | } | 347 | } |
| 347 | 348 | ||
| 349 | /** Adapters forward unknown wire-only SDK messages (background_tasks_changed, | ||
| 350 | * commands_changed, ...) as runtime warnings. The suffix comes from | ||
| 351 | * describeUnknownSdkMessage in the Claude adapter; a row with no displayable | ||
| 352 | * text carries nothing a user can act on, so it does not render. */ | ||
| 353 | function isNoContentRuntimeWarning(activity: OrchestrationThreadActivity): boolean { | ||
| 354 | return ( | ||
| 355 | activity.kind === "runtime.warning" && | ||
| 356 | activity.summary.endsWith("(no displayable text content)") | ||
| 357 | ); | ||
| 358 | } | ||
| 359 | |||
| 348 | function isPlanBoundaryToolActivity(activity: OrchestrationThreadActivity): boolean { | 360 | function isPlanBoundaryToolActivity(activity: OrchestrationThreadActivity): boolean { |
| 349 | if (activity.kind !== "tool.updated" && activity.kind !== "tool.completed") { | 361 | if (activity.kind !== "tool.updated" && activity.kind !== "tool.completed") { |
| 350 | return false; | 362 | return false; |
apps/web/src/components/chat/MessagesTimeline.test.tsx+93-1| ... | @@ -973,6 +973,49 @@ describe("MessagesTimeline", () => { | ... | @@ -973,6 +973,49 @@ 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 the collapsed summary icon neutral when the group ends in a failure", () => { | ||
| 977 | const markup = renderToStaticMarkup( | ||
| 978 | <MessagesTimeline | ||
| 979 | {...buildProps()} | ||
| 980 | timelineEntries={[ | ||
| 981 | { | ||
| 982 | id: "entry-completed", | ||
| 983 | kind: "work", | ||
| 984 | createdAt: "2026-03-17T19:12:28.000Z", | ||
| 985 | entry: { | ||
| 986 | id: "work-completed", | ||
| 987 | createdAt: "2026-03-17T19:12:28.000Z", | ||
| 988 | label: "Run tests", | ||
| 989 | tone: "tool", | ||
| 990 | itemType: "command_execution", | ||
| 991 | toolLifecycleStatus: "completed", | ||
| 992 | }, | ||
| 993 | }, | ||
| 994 | { | ||
| 995 | id: "entry-failed", | ||
| 996 | kind: "work", | ||
| 997 | createdAt: "2026-03-17T19:12:29.000Z", | ||
| 998 | entry: { | ||
| 999 | id: "work-failed", | ||
| 1000 | createdAt: "2026-03-17T19:12:29.000Z", | ||
| 1001 | label: "Run lint", | ||
| 1002 | tone: "tool", | ||
| 1003 | itemType: "command_execution", | ||
| 1004 | toolLifecycleStatus: "failed", | ||
| 1005 | }, | ||
| 1006 | }, | ||
| 1007 | ]} | ||
| 1008 | />, | ||
| 1009 | ); | ||
| 1010 | |||
| 1011 | expect(markup).toContain("Ran 2 commands"); | ||
| 1012 | expect(markup).toContain("lucide-terminal"); | ||
| 1013 | expect(markup).not.toContain("lucide-x"); | ||
| 1014 | expect(markup).not.toContain("text-destructive"); | ||
| 1015 | // The failure stays discoverable for screen readers. | ||
| 1016 | expect(markup).toContain("tool call failed"); | ||
| 1017 | }); | ||
| 1018 | |||
| 976 | it("keeps mixed work logs neutral after a later tool call succeeds", () => { | 1019 | it("keeps mixed work logs neutral after a later tool call succeeds", () => { |
| 977 | const markup = renderToStaticMarkup( | 1020 | const markup = renderToStaticMarkup( |
| 978 | <MessagesTimeline | 1021 | <MessagesTimeline |
| ... | @@ -1249,11 +1292,22 @@ describe("MessagesTimeline", () => { | ... | @@ -1249,11 +1292,22 @@ describe("MessagesTimeline", () => { |
| 1249 | expect(markup).not.toContain('data-testid="file-diff"'); | 1292 | expect(markup).not.toContain('data-testid="file-diff"'); |
| 1250 | }); | 1293 | }); |
| 1251 | 1294 | ||
| 1252 | it("renders a failure marker for failed tool lifecycle entries", () => { | 1295 | it("renders a muted failure marker for failed tool lifecycle entries", () => { |
| 1253 | const markup = renderToStaticMarkup( | 1296 | const markup = renderToStaticMarkup( |
| 1254 | <MessagesTimeline | 1297 | <MessagesTimeline |
| 1255 | {...buildProps()} | 1298 | {...buildProps()} |
| 1256 | timelineEntries={[ | 1299 | timelineEntries={[ |
| 1300 | { | ||
| 1301 | id: "entry-info", | ||
| 1302 | kind: "work", | ||
| 1303 | createdAt: "2026-03-17T19:12:27.000Z", | ||
| 1304 | entry: { | ||
| 1305 | id: "work-info", | ||
| 1306 | createdAt: "2026-03-17T19:12:27.000Z", | ||
| 1307 | label: "Status updated", | ||
| 1308 | tone: "info", | ||
| 1309 | }, | ||
| 1310 | }, | ||
| 1257 | { | 1311 | { |
| 1258 | id: "entry-1", | 1312 | id: "entry-1", |
| 1259 | kind: "work", | 1313 | kind: "work", |
| ... | @@ -1273,5 +1327,43 @@ describe("MessagesTimeline", () => { | ... | @@ -1273,5 +1327,43 @@ describe("MessagesTimeline", () => { |
| 1273 | 1327 | ||
| 1274 | expect(markup).toContain("lucide-x"); | 1328 | expect(markup).toContain("lucide-x"); |
| 1275 | expect(markup).toContain('aria-label="Tool call failed"'); | 1329 | expect(markup).toContain('aria-label="Tool call failed"'); |
| 1330 | // Ordinary tool failures render muted, not red. | ||
| 1331 | expect(markup).not.toContain("text-destructive"); | ||
| 1332 | }); | ||
| 1333 | |||
| 1334 | it("keeps the red treatment for severe orchestration failures", () => { | ||
| 1335 | const markup = renderToStaticMarkup( | ||
| 1336 | <MessagesTimeline | ||
| 1337 | {...buildProps()} | ||
| 1338 | timelineEntries={[ | ||
| 1339 | { | ||
| 1340 | id: "entry-info", | ||
| 1341 | kind: "work", | ||
| 1342 | createdAt: "2026-03-17T19:12:27.000Z", | ||
| 1343 | entry: { | ||
| 1344 | id: "work-info", | ||
| 1345 | createdAt: "2026-03-17T19:12:27.000Z", | ||
| 1346 | label: "Status updated", | ||
| 1347 | tone: "info", | ||
| 1348 | }, | ||
| 1349 | }, | ||
| 1350 | { | ||
| 1351 | id: "entry-turn-failed", | ||
| 1352 | kind: "work", | ||
| 1353 | createdAt: "2026-03-17T19:12:28.000Z", | ||
| 1354 | entry: { | ||
| 1355 | id: "work-turn-failed", | ||
| 1356 | createdAt: "2026-03-17T19:12:28.000Z", | ||
| 1357 | label: "Provider turn start failed", | ||
| 1358 | tone: "error", | ||
| 1359 | sourceActivityKind: "provider.turn.start.failed", | ||
| 1360 | }, | ||
| 1361 | }, | ||
| 1362 | ]} | ||
| 1363 | />, | ||
| 1364 | ); | ||
| 1365 | |||
| 1366 | expect(markup).toContain("lucide-x"); | ||
| 1367 | expect(markup).toContain("text-destructive"); | ||
| 1276 | }); | 1368 | }); |
| 1277 | }); | 1369 | }); |
apps/web/src/components/chat/MessagesTimeline.tsx+21-33| ... | @@ -35,6 +35,7 @@ import { FileDiff } from "@pierre/diffs/react"; | ... | @@ -35,6 +35,7 @@ import { FileDiff } from "@pierre/diffs/react"; |
| 35 | import { | 35 | import { |
| 36 | deriveTimelineEntries, | 36 | deriveTimelineEntries, |
| 37 | workEntryDisplayIndicatesToolFailure, | 37 | workEntryDisplayIndicatesToolFailure, |
| 38 | workEntrySignalsSevereFailure, | ||
| 38 | workLogEntryIsToolLike, | 39 | workLogEntryIsToolLike, |
| 39 | } from "../../session-logic"; | 40 | } from "../../session-logic"; |
| 40 | import { type TurnDiffSummary } from "../../types"; | 41 | import { type TurnDiffSummary } from "../../types"; |
| ... | @@ -1479,7 +1480,7 @@ function LiveActivityContent({ | ... | @@ -1479,7 +1480,7 @@ function LiveActivityContent({ |
| 1479 | <span | 1480 | <span |
| 1480 | className={cn( | 1481 | className={cn( |
| 1481 | "flex size-6 shrink-0 items-center justify-center", | 1482 | "flex size-6 shrink-0 items-center justify-center", |
| 1482 | failed ? "text-destructive" : highlighted ? "text-foreground" : "text-icon-muted", | 1483 | highlighted ? "text-foreground" : "text-icon-muted", |
| 1483 | )} | 1484 | )} |
| 1484 | role={announceFailure ? "img" : undefined} | 1485 | role={announceFailure ? "img" : undefined} |
| 1485 | aria-label={announceFailure ? "Tool call failed" : undefined} | 1486 | aria-label={announceFailure ? "Tool call failed" : undefined} |
| ... | @@ -1556,16 +1557,9 @@ function WorkGroupToggleTimelineRow({ | ... | @@ -1556,16 +1557,9 @@ function WorkGroupToggleTimelineRow({ |
| 1556 | aria-expanded={row.expanded} | 1557 | aria-expanded={row.expanded} |
| 1557 | onClick={() => ctx.onToggleWorkGroup(row.groupId, row.id)} | 1558 | onClick={() => ctx.onToggleWorkGroup(row.groupId, row.id)} |
| 1558 | > | 1559 | > |
| 1559 | <span | 1560 | <span className="flex size-6 shrink-0 items-center justify-center text-icon-muted"> |
| 1560 | className={cn( | ||
| 1561 | "flex size-6 shrink-0 items-center justify-center", | ||
| 1562 | row.hasFailure ? "text-destructive" : "text-icon-muted", | ||
| 1563 | )} | ||
| 1564 | role={row.hasFailure ? "img" : undefined} | ||
| 1565 | aria-label={row.hasFailure ? "Tool call failed" : undefined} | ||
| 1566 | > | ||
| 1567 | <WorkEntryIconSvg | 1561 | <WorkEntryIconSvg |
| 1568 | name={row.hasFailure ? "x" : toolGroupSummaryIconName(row.summaryKind)} | 1562 | name={toolGroupSummaryIconName(row.summaryKind)} |
| 1569 | className="size-4 shrink-0 stroke-[1.8] opacity-70" | 1563 | className="size-4 shrink-0 stroke-[1.8] opacity-70" |
| 1570 | /> | 1564 | /> |
| 1571 | </span> | 1565 | </span> |
| ... | @@ -1580,33 +1574,25 @@ function WorkGroupToggleTimelineRow({ | ... | @@ -1580,33 +1574,25 @@ function WorkGroupToggleTimelineRow({ |
| 1580 | : row.hiddenCount === 1 | 1574 | : row.hiddenCount === 1 |
| 1581 | ? "log entry" | 1575 | ? "log entry" |
| 1582 | : "log entries"; | 1576 | : "log entries"; |
| 1583 | const showHiddenFailure = row.hasFailure && !row.expanded; | ||
| 1584 | |||
| 1585 | return ( | 1577 | return ( |
| 1586 | <button | 1578 | <button |
| 1587 | type="button" | 1579 | type="button" |
| 1588 | className="flex min-h-6 w-full cursor-pointer items-center gap-1.5 rounded-md px-0.5 py-0.5 text-left text-sm leading-relaxed transition-colors duration-150 hover:bg-accent/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/70" | 1580 | className="flex min-h-6 w-full cursor-pointer items-center gap-1.5 rounded-md px-0.5 py-0.5 text-left text-sm leading-relaxed transition-colors duration-150 hover:bg-accent/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/70" |
| 1581 | aria-label={ | ||
| 1582 | row.hasFailure && !row.expanded | ||
| 1583 | ? `+${row.hiddenCount} previous ${labelNoun}, includes a failure` | ||
| 1584 | : undefined | ||
| 1585 | } | ||
| 1589 | aria-expanded={row.expanded} | 1586 | aria-expanded={row.expanded} |
| 1590 | onClick={() => ctx.onToggleWorkGroup(row.groupId, row.id)} | 1587 | onClick={() => ctx.onToggleWorkGroup(row.groupId, row.id)} |
| 1591 | > | 1588 | > |
| 1592 | <span | 1589 | <span className="flex size-6 shrink-0 items-center justify-center text-icon-muted"> |
| 1593 | className={cn( | 1590 | <ChevronDownIcon |
| 1594 | "flex size-6 shrink-0 items-center justify-center", | 1591 | className={cn( |
| 1595 | showHiddenFailure ? "text-destructive" : "text-icon-muted", | 1592 | "size-4 shrink-0 opacity-70 transition-transform duration-200", |
| 1596 | )} | 1593 | row.expanded && "rotate-180", |
| 1597 | role={showHiddenFailure ? "img" : undefined} | 1594 | )} |
| 1598 | aria-label={showHiddenFailure ? "Hidden work includes a failure" : undefined} | 1595 | /> |
| 1599 | > | ||
| 1600 | {showHiddenFailure ? ( | ||
| 1601 | <WorkEntryIconSvg name="x" className="size-4 shrink-0 stroke-[1.8] opacity-70" /> | ||
| 1602 | ) : ( | ||
| 1603 | <ChevronDownIcon | ||
| 1604 | className={cn( | ||
| 1605 | "size-4 shrink-0 opacity-70 transition-transform duration-200", | ||
| 1606 | row.expanded && "rotate-180", | ||
| 1607 | )} | ||
| 1608 | /> | ||
| 1609 | )} | ||
| 1610 | </span> | 1596 | </span> |
| 1611 | {row.expanded ? ( | 1597 | {row.expanded ? ( |
| 1612 | <span className="font-medium text-foreground"> | 1598 | <span className="font-medium text-foreground"> |
| ... | @@ -2631,11 +2617,13 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: { | ... | @@ -2631,11 +2617,13 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: { |
| 2631 | const canExpand = expandedBody !== null; | 2617 | const canExpand = expandedBody !== null; |
| 2632 | const showDestructiveRowStyle = | 2618 | const showDestructiveRowStyle = |
| 2633 | showFailedIndicator && | 2619 | showFailedIndicator && |
| 2634 | (workEntry.sourceActivityKind === "runtime.error" || !workLogEntryIsToolLike(workEntry)); | 2620 | (workEntrySignalsSevereFailure(workEntry) || !workLogEntryIsToolLike(workEntry)); |
| 2621 | // Ordinary tool failures stay muted; only runtime errors and warnings get | ||
| 2622 | // color. The red treatment is reserved for severe failures. | ||
| 2635 | const iconWrapperClass = cn( | 2623 | const iconWrapperClass = cn( |
| 2636 | "flex size-6 shrink-0 items-center justify-center", | 2624 | "flex size-6 shrink-0 items-center justify-center", |
| 2637 | showWarningIndicator || showFailedIndicator | 2625 | showWarningIndicator |
| 2638 | ? "text-destructive" | 2626 | ? "text-warning" |
| 2639 | : showDestructiveRowStyle | 2627 | : showDestructiveRowStyle |
| 2640 | ? "text-destructive" | 2628 | ? "text-destructive" |
| 2641 | : workEntry.tone === "tool" || showFailedIndicator | 2629 | : workEntry.tone === "tool" || showFailedIndicator |
apps/web/src/session-logic.test.ts+22| ... | @@ -935,6 +935,28 @@ describe("deriveWorkLogEntries", () => { | ... | @@ -935,6 +935,28 @@ describe("deriveWorkLogEntries", () => { |
| 935 | expect(entries.map((entry) => entry.id)).toEqual(["tool-complete"]); | 935 | expect(entries.map((entry) => entry.id)).toEqual(["tool-complete"]); |
| 936 | }); | 936 | }); |
| 937 | 937 | ||
| 938 | it("drops runtime warnings with no displayable content, keeps ones with a preview", () => { | ||
| 939 | const activities: OrchestrationThreadActivity[] = [ | ||
| 940 | makeActivity({ | ||
| 941 | id: "warning-noise", | ||
| 942 | createdAt: "2026-02-23T00:00:01.000Z", | ||
| 943 | kind: "runtime.warning", | ||
| 944 | summary: "Claude system message 'background_tasks_changed' (no displayable text content)", | ||
| 945 | tone: "info", | ||
| 946 | }), | ||
| 947 | makeActivity({ | ||
| 948 | id: "warning-signal", | ||
| 949 | createdAt: "2026-02-23T00:00:02.000Z", | ||
| 950 | kind: "runtime.warning", | ||
| 951 | summary: "Reconnecting... 2/5", | ||
| 952 | tone: "info", | ||
| 953 | }), | ||
| 954 | ]; | ||
| 955 | |||
| 956 | const entries = deriveWorkLogEntries(activities); | ||
| 957 | expect(entries.map((entry) => entry.id)).toEqual(["warning-signal"]); | ||
| 958 | }); | ||
| 959 | |||
| 938 | it("omits task.started but shows task.progress and task.completed", () => { | 960 | it("omits task.started but shows task.progress and task.completed", () => { |
| 939 | const activities: OrchestrationThreadActivity[] = [ | 961 | const activities: OrchestrationThreadActivity[] = [ |
| 940 | makeActivity({ | 962 | makeActivity({ |
apps/web/src/session-logic.ts+23| ... | @@ -279,6 +279,17 @@ export function workEntryDisplayIndicatesToolFailure(entry: WorkLogEntry): boole | ... | @@ -279,6 +279,17 @@ export function workEntryDisplayIndicatesToolFailure(entry: WorkLogEntry): boole |
| 279 | return workEntryIndicatesToolFailureFromOutput(entry, false); | 279 | return workEntryIndicatesToolFailureFromOutput(entry, false); |
| 280 | } | 280 | } |
| 281 | 281 | ||
| 282 | /** Severe failures keep the red treatment ordinary tool failures lost: runtime | ||
| 283 | * errors and orchestration `*.failed` activities (provider.turn.start.failed, | ||
| 284 | * checkpoint.capture.failed, ...) mean the turn or a core side effect broke, | ||
| 285 | * not that a command exited nonzero. */ | ||
| 286 | export function workEntrySignalsSevereFailure(entry: WorkLogEntry): boolean { | ||
| 287 | return ( | ||
| 288 | entry.sourceActivityKind === "runtime.error" || | ||
| 289 | entry.sourceActivityKind?.endsWith(".failed") === true | ||
| 290 | ); | ||
| 291 | } | ||
| 292 | |||
| 282 | /** Tool/command row completed without failure (blue check affordance). */ | 293 | /** Tool/command row completed without failure (blue check affordance). */ |
| 283 | export function workEntryIndicatesToolSuccess(entry: WorkLogEntry): boolean { | 294 | export function workEntryIndicatesToolSuccess(entry: WorkLogEntry): boolean { |
| 284 | if (!workLogEntryIsToolLike(entry)) { | 295 | if (!workLogEntryIsToolLike(entry)) { |
| ... | @@ -870,6 +881,7 @@ export function deriveWorkLogEntries( | ... | @@ -870,6 +881,7 @@ export function deriveWorkLogEntries( |
| 870 | if (activity.kind === "tool.progress") continue; | 881 | if (activity.kind === "tool.progress") continue; |
| 871 | if (activity.kind === "context-window.updated") continue; | 882 | if (activity.kind === "context-window.updated") continue; |
| 872 | if (activity.summary === "Checkpoint captured") continue; | 883 | if (activity.summary === "Checkpoint captured") continue; |
| 884 | if (isNoContentRuntimeWarning(activity)) continue; | ||
| 873 | if (isPlanBoundaryToolActivity(activity)) continue; | 885 | if (isPlanBoundaryToolActivity(activity)) continue; |
| 874 | if (isAgentInternalActivity(activity)) continue; | 886 | if (isAgentInternalActivity(activity)) continue; |
| 875 | entries.push(toDerivedWorkLogEntry(activity)); | 887 | entries.push(toDerivedWorkLogEntry(activity)); |
| ... | @@ -877,6 +889,17 @@ export function deriveWorkLogEntries( | ... | @@ -877,6 +889,17 @@ export function deriveWorkLogEntries( |
| 877 | return collapseDerivedWorkLogEntries(entries); | 889 | return collapseDerivedWorkLogEntries(entries); |
| 878 | } | 890 | } |
| 879 | 891 | ||
| 892 | /** Adapters forward unknown wire-only SDK messages (background_tasks_changed, | ||
| 893 | * commands_changed, ...) as runtime warnings. The suffix comes from | ||
| 894 | * describeUnknownSdkMessage in the Claude adapter; a row with no displayable | ||
| 895 | * text carries nothing a user can act on, so it does not render. */ | ||
| 896 | function isNoContentRuntimeWarning(activity: OrchestrationThreadActivity): boolean { | ||
| 897 | return ( | ||
| 898 | activity.kind === "runtime.warning" && | ||
| 899 | activity.summary.endsWith("(no displayable text content)") | ||
| 900 | ); | ||
| 901 | } | ||
| 902 | |||
| 880 | function isPlanBoundaryToolActivity(activity: OrchestrationThreadActivity): boolean { | 903 | function isPlanBoundaryToolActivity(activity: OrchestrationThreadActivity): boolean { |
| 881 | if (activity.kind !== "tool.updated" && activity.kind !== "tool.completed") { | 904 | if (activity.kind !== "tool.updated" && activity.kind !== "tool.completed") { |
| 882 | return false; | 905 | return false; |