| author | |
| committer | |
| log | 590a579f2e9292ce314c69e459e19620004578fe |
| tree | bfd334c0383510f569cb97d4442f03ff84327eb5 |
| parent | 0222aa255d11babd242dbe5ed0947e5fc5eaefee |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: maria-rcks <254055478+maria-rcks@users.noreply.github.com>4 files changed, 118 insertions(+), 60 deletions(-)
apps/web/src/components/chat/MessagesTimeline.logic.test.ts+57-38| ... | @@ -826,7 +826,7 @@ describe("deriveMessagesTimelineRows", () => { | ... | @@ -826,7 +826,7 @@ describe("deriveMessagesTimelineRows", () => { |
| 826 | "assistant-final-entry", | 826 | "assistant-final-entry", |
| 827 | "user-followup-entry", | 827 | "user-followup-entry", |
| 828 | "working-indicator-row", | 828 | "working-indicator-row", |
| 829 | "thinking-indicator-row", | 829 | "live-activity-row", |
| 830 | ]); | 830 | ]); |
| 831 | const finalRow = rows.find((row) => row.id === "assistant-final-entry"); | 831 | const finalRow = rows.find((row) => row.id === "assistant-final-entry"); |
| 832 | expect(finalRow?.kind === "message" && finalRow.showAssistantMeta).toBe(true); | 832 | expect(finalRow?.kind === "message" && finalRow.showAssistantMeta).toBe(true); |
| ... | @@ -879,11 +879,11 @@ describe("deriveMessagesTimelineRows", () => { | ... | @@ -879,11 +879,11 @@ describe("deriveMessagesTimelineRows", () => { |
| 879 | expect(rows.map((row) => row.id)).toEqual([ | 879 | expect(rows.map((row) => row.id)).toEqual([ |
| 880 | "working-indicator-row", | 880 | "working-indicator-row", |
| 881 | "assistant-thought-entry", | 881 | "assistant-thought-entry", |
| 882 | "work-live:work-entry-1", | 882 | "live-activity-row", |
| 883 | ]); | 883 | ]); |
| 884 | }); | 884 | }); |
| 885 | 885 | ||
| 886 | it("keeps adjacent active tool calls in one replacing row", () => { | 886 | it("keeps an actually running tool in the shared activity row", () => { |
| 887 | const rows = deriveMessagesTimelineRows({ | 887 | const rows = deriveMessagesTimelineRows({ |
| 888 | timelineEntries: [ | 888 | timelineEntries: [ |
| 889 | { | 889 | { |
| ... | @@ -948,6 +948,7 @@ describe("deriveMessagesTimelineRows", () => { | ... | @@ -948,6 +948,7 @@ describe("deriveMessagesTimelineRows", () => { |
| 948 | expect(rows.some((row) => row.kind === "thinking")).toBe(false); | 948 | expect(rows.some((row) => row.kind === "thinking")).toBe(false); |
| 949 | expect(rows.find((row) => row.kind === "work-live")).toMatchObject({ | 949 | expect(rows.find((row) => row.kind === "work-live")).toMatchObject({ |
| 950 | entry: { id: "running-command" }, | 950 | entry: { id: "running-command" }, |
| 951 | active: true, | ||
| 951 | groupedEntries: [ | 952 | groupedEntries: [ |
| 952 | { id: "running-command" }, | 953 | { id: "running-command" }, |
| 953 | { id: "completed-edit" }, | 954 | { id: "completed-edit" }, |
| ... | @@ -1196,40 +1197,58 @@ describe("deriveMessagesTimelineRows", () => { | ... | @@ -1196,40 +1197,58 @@ describe("deriveMessagesTimelineRows", () => { |
| 1196 | ]); | 1197 | ]); |
| 1197 | }); | 1198 | }); |
| 1198 | 1199 | ||
| 1199 | it("shows thinking after the latest tool call completes while the turn is running", () => { | 1200 | it("reuses one activity row for initial thinking and the latest tool", () => { |
| 1200 | const rows = deriveMessagesTimelineRows({ | 1201 | const deriveRows = (toolLifecycleStatus: "inProgress" | "completed" | "declined" | null) => |
| 1201 | timelineEntries: [ | 1202 | deriveMessagesTimelineRows({ |
| 1202 | { | 1203 | timelineEntries: |
| 1203 | id: "latest-command-entry", | 1204 | toolLifecycleStatus === null |
| 1204 | kind: "work", | 1205 | ? [] |
| 1205 | createdAt: "2026-01-01T00:00:05Z", | 1206 | : [ |
| 1206 | entry: { | 1207 | { |
| 1207 | id: "latest-command", | 1208 | id: "latest-command-entry", |
| 1208 | createdAt: "2026-01-01T00:00:05Z", | 1209 | kind: "work", |
| 1209 | turnId: "turn-1" as never, | 1210 | createdAt: "2026-01-01T00:00:05Z", |
| 1210 | label: "Ran rg", | 1211 | entry: { |
| 1211 | command: "rg toolCall", | 1212 | id: "latest-command", |
| 1212 | requestKind: "command", | 1213 | createdAt: "2026-01-01T00:00:05Z", |
| 1213 | tone: "tool" as const, | 1214 | turnId: "turn-1" as never, |
| 1214 | toolLifecycleStatus: "completed" as const, | 1215 | label: toolLifecycleStatus === "inProgress" ? "Running rg" : "Ran rg", |
| 1215 | }, | 1216 | command: "rg toolCall", |
| 1217 | requestKind: "command", | ||
| 1218 | tone: "tool" as const, | ||
| 1219 | toolLifecycleStatus, | ||
| 1220 | }, | ||
| 1221 | }, | ||
| 1222 | ], | ||
| 1223 | latestTurn: { | ||
| 1224 | turnId: "turn-1" as never, | ||
| 1225 | state: "running", | ||
| 1226 | startedAt: "2026-01-01T00:00:00Z", | ||
| 1227 | completedAt: null, | ||
| 1216 | }, | 1228 | }, |
| 1217 | ], | 1229 | isWorking: true, |
| 1218 | latestTurn: { | 1230 | activeTurnStartedAt: "2026-01-01T00:00:00Z", |
| 1219 | turnId: "turn-1" as never, | 1231 | turnDiffSummaryByAssistantMessageId: new Map(), |
| 1220 | state: "running", | 1232 | revertTurnCountByUserMessageId: new Map(), |
| 1221 | startedAt: "2026-01-01T00:00:00Z", | 1233 | }); |
| 1222 | completedAt: null, | ||
| 1223 | }, | ||
| 1224 | isWorking: true, | ||
| 1225 | activeTurnStartedAt: "2026-01-01T00:00:00Z", | ||
| 1226 | turnDiffSummaryByAssistantMessageId: new Map(), | ||
| 1227 | revertTurnCountByUserMessageId: new Map(), | ||
| 1228 | }); | ||
| 1229 | 1234 | ||
| 1230 | expect(rows.map((row) => row.kind)).toEqual(["working", "work-live", "thinking"]); | 1235 | const initialRows = deriveRows(null); |
| 1231 | expect(rows.find((row) => row.kind === "work-live")).toMatchObject({ active: false }); | 1236 | const runningRows = deriveRows("inProgress"); |
| 1232 | expect(rows.at(-1)).toMatchObject({ kind: "thinking" }); | 1237 | const completedRows = deriveRows("completed"); |
| 1238 | const declinedRows = deriveRows("declined"); | ||
| 1239 | const initialActivityRow = initialRows.find((row) => row.id === "live-activity-row"); | ||
| 1240 | const runningActivityRow = runningRows.find((row) => row.id === "live-activity-row"); | ||
| 1241 | const completedActivityRow = completedRows.find((row) => row.id === "live-activity-row"); | ||
| 1242 | |||
| 1243 | expect(initialActivityRow).toMatchObject({ kind: "thinking" }); | ||
| 1244 | expect(runningActivityRow).toMatchObject({ kind: "work-live", active: true }); | ||
| 1245 | expect(completedActivityRow).toMatchObject({ kind: "work-live", active: true }); | ||
| 1246 | expect(declinedRows.find((row) => row.kind === "work-live")).toMatchObject({ active: false }); | ||
| 1247 | expect(declinedRows.at(-1)).toMatchObject({ kind: "thinking", id: "live-activity-row" }); | ||
| 1248 | expect(initialRows.filter((row) => row.id === "live-activity-row")).toHaveLength(1); | ||
| 1249 | expect(runningRows.filter((row) => row.id === "live-activity-row")).toHaveLength(1); | ||
| 1250 | expect(completedRows.filter((row) => row.id === "live-activity-row")).toHaveLength(1); | ||
| 1251 | expect(declinedRows.filter((row) => row.id === "live-activity-row")).toHaveLength(1); | ||
| 1233 | }); | 1252 | }); |
| 1234 | 1253 | ||
| 1235 | it("does not fold the session's running turn when latestTurn regresses", () => { | 1254 | it("does not fold the session's running turn when latestTurn regresses", () => { |
| ... | @@ -1290,7 +1309,7 @@ describe("deriveMessagesTimelineRows", () => { | ... | @@ -1290,7 +1309,7 @@ describe("deriveMessagesTimelineRows", () => { |
| 1290 | expect(rows.filter((row) => row.kind === "turn-fold").map((row) => row.turnId)).toEqual([ | 1309 | expect(rows.filter((row) => row.kind === "turn-fold").map((row) => row.turnId)).toEqual([ |
| 1291 | "turn-1", | 1310 | "turn-1", |
| 1292 | ]); | 1311 | ]); |
| 1293 | expect(rows.map((row) => row.id)).toContain("work-live:running-work-entry"); | 1312 | expect(rows.map((row) => row.id)).toContain("live-activity-row"); |
| 1294 | }); | 1313 | }); |
| 1295 | 1314 | ||
| 1296 | it("only shows assistant metadata on the terminal assistant message", () => { | 1315 | it("only shows assistant metadata on the terminal assistant message", () => { |
| ... | @@ -1587,8 +1606,8 @@ describe("computeStableMessagesTimelineRows", () => { | ... | @@ -1587,8 +1606,8 @@ describe("computeStableMessagesTimelineRows", () => { |
| 1587 | initial, | 1606 | initial, |
| 1588 | ); | 1607 | ); |
| 1589 | 1608 | ||
| 1590 | const initialThinking = initial.byId.get("thinking-indicator-row"); | 1609 | const initialThinking = initial.byId.get("live-activity-row"); |
| 1591 | const updatedThinking = updated.byId.get("thinking-indicator-row"); | 1610 | const updatedThinking = updated.byId.get("live-activity-row"); |
| 1592 | expect(initialThinking).toMatchObject({ kind: "thinking" }); | 1611 | expect(initialThinking).toMatchObject({ kind: "thinking" }); |
| 1593 | expect(updatedThinking).toBe(initialThinking); | 1612 | expect(updatedThinking).toBe(initialThinking); |
| 1594 | expect(updated.result.at(-1)).toBe(updatedThinking); | 1613 | expect(updated.result.at(-1)).toBe(updatedThinking); |
apps/web/src/components/chat/MessagesTimeline.logic.ts+18-10| ... | @@ -15,6 +15,7 @@ export { | ... | @@ -15,6 +15,7 @@ export { |
| 15 | import { | 15 | import { |
| 16 | formatDuration, | 16 | formatDuration, |
| 17 | workEntryDisplayIndicatesToolFailure, | 17 | workEntryDisplayIndicatesToolFailure, |
| 18 | workEntryIndicatesToolSuccess, | ||
| 18 | workEntryIndicatesToolNeutralStatus, | 19 | workEntryIndicatesToolNeutralStatus, |
| 19 | workLogEntryIsToolLike, | 20 | workLogEntryIsToolLike, |
| 20 | type TimelineEntry, | 21 | type TimelineEntry, |
| ... | @@ -188,6 +189,8 @@ export type TimelineLatestTurn = Pick< | ... | @@ -188,6 +189,8 @@ export type TimelineLatestTurn = Pick< |
| 188 | "turnId" | "state" | "startedAt" | "completedAt" | 189 | "turnId" | "state" | "startedAt" | "completedAt" |
| 189 | >; | 190 | >; |
| 190 | 191 | ||
| 192 | const LIVE_ACTIVITY_ROW_ID = "live-activity-row"; | ||
| 193 | |||
| 191 | export type MessagesTimelineRow = | 194 | export type MessagesTimelineRow = |
| 192 | | { | 195 | | { |
| 193 | kind: "work"; | 196 | kind: "work"; |
| ... | @@ -598,21 +601,26 @@ export function deriveMessagesTimelineRows(input: { | ... | @@ -598,21 +601,26 @@ export function deriveMessagesTimelineRows(input: { |
| 598 | const latestRunningToolEntry = visibleActiveToolEntries.findLast((entry) => | 601 | const latestRunningToolEntry = visibleActiveToolEntries.findLast((entry) => |
| 599 | workEntryIsActiveTurnActivity(entry.entry), | 602 | workEntryIsActiveTurnActivity(entry.entry), |
| 600 | ); | 603 | ); |
| 601 | const displayedToolEntry = latestRunningToolEntry ?? latestVisibleToolEntry; | 604 | const latestToolKeepsActivityLive = |
| 605 | latestRunningToolEntry !== undefined || | ||
| 606 | (latestVisibleToolEntry !== undefined && | ||
| 607 | workEntryIndicatesToolSuccess(latestVisibleToolEntry.entry)); | ||
| 602 | const activeWorkPlacementEntryId = latestVisibleToolEntry?.id; | 608 | const activeWorkPlacementEntryId = latestVisibleToolEntry?.id; |
| 603 | const activeWorkRow = | 609 | const activeWorkRow = |
| 604 | activeWorkAnchor && displayedToolEntry | 610 | activeWorkAnchor && latestVisibleToolEntry |
| 605 | ? (() => { | 611 | ? (() => { |
| 606 | const groupId = workGroupId(activeWorkAnchor.id, activeWorkAnchor.entry); | 612 | const groupId = workGroupId(activeWorkAnchor.id, activeWorkAnchor.entry); |
| 607 | return { | 613 | return { |
| 608 | kind: "work-live" as const, | 614 | kind: "work-live" as const, |
| 609 | id: `work-live:${workGroupIdentity(activeWorkAnchor.id, activeWorkAnchor.entry)}`, | 615 | id: latestToolKeepsActivityLive |
| 616 | ? LIVE_ACTIVITY_ROW_ID | ||
| 617 | : `work-live:${workGroupIdentity(activeWorkAnchor.id, activeWorkAnchor.entry)}`, | ||
| 610 | createdAt: activeWorkAnchor.createdAt, | 618 | createdAt: activeWorkAnchor.createdAt, |
| 611 | entry: displayedToolEntry.entry, | 619 | entry: (latestRunningToolEntry ?? latestVisibleToolEntry).entry, |
| 612 | groupedEntries: visibleActiveToolEntries.map((entry) => entry.entry), | 620 | groupedEntries: visibleActiveToolEntries.map((entry) => entry.entry), |
| 613 | groupId, | 621 | groupId, |
| 614 | expanded: input.expandedWorkGroupIds?.has(groupId) ?? false, | 622 | expanded: input.expandedWorkGroupIds?.has(groupId) ?? false, |
| 615 | active: latestRunningToolEntry !== undefined, | 623 | active: latestToolKeepsActivityLive, |
| 616 | }; | 624 | }; |
| 617 | })() | 625 | })() |
| 618 | : null; | 626 | : null; |
| ... | @@ -626,11 +634,11 @@ export function deriveMessagesTimelineRows(input: { | ... | @@ -626,11 +634,11 @@ export function deriveMessagesTimelineRows(input: { |
| 626 | createdAt: input.activeTurnStartedAt, | 634 | createdAt: input.activeTurnStartedAt, |
| 627 | }); | 635 | }); |
| 628 | }; | 636 | }; |
| 629 | let hasLiveWorkRow = false; | 637 | let hasActivityRow = false; |
| 630 | const appendActiveWorkRows = () => { | 638 | const appendActiveWorkRows = () => { |
| 631 | if (activeWorkRow === null) return; | 639 | if (activeWorkRow === null) return; |
| 632 | nextRows.push(activeWorkRow); | 640 | nextRows.push(activeWorkRow); |
| 633 | hasLiveWorkRow ||= activeWorkRow.active; | 641 | hasActivityRow ||= activeWorkRow.active; |
| 634 | if (!activeWorkRow.expanded) return; | 642 | if (!activeWorkRow.expanded) return; |
| 635 | for (const [entryIndex, workEntry] of activeWorkRow.groupedEntries.entries()) { | 643 | for (const [entryIndex, workEntry] of activeWorkRow.groupedEntries.entries()) { |
| 636 | nextRows.push({ | 644 | nextRows.push({ |
| ... | @@ -730,7 +738,7 @@ export function deriveMessagesTimelineRows(input: { | ... | @@ -730,7 +738,7 @@ export function deriveMessagesTimelineRows(input: { |
| 730 | expanded, | 738 | expanded, |
| 731 | active: true, | 739 | active: true, |
| 732 | }); | 740 | }); |
| 733 | hasLiveWorkRow = true; | 741 | hasActivityRow = true; |
| 734 | if (expanded) { | 742 | if (expanded) { |
| 735 | for (const [entryIndex, workEntry] of visibleGroupedEntries.entries()) { | 743 | for (const [entryIndex, workEntry] of visibleGroupedEntries.entries()) { |
| 736 | nextRows.push({ | 744 | nextRows.push({ |
| ... | @@ -832,10 +840,10 @@ export function deriveMessagesTimelineRows(input: { | ... | @@ -832,10 +840,10 @@ export function deriveMessagesTimelineRows(input: { |
| 832 | if (input.isWorking && activeTurnHeaderIndex === input.timelineEntries.length) { | 840 | if (input.isWorking && activeTurnHeaderIndex === input.timelineEntries.length) { |
| 833 | appendWorkingRow(); | 841 | appendWorkingRow(); |
| 834 | } | 842 | } |
| 835 | if (input.isWorking && !hasLiveWorkRow) { | 843 | if (input.isWorking && !hasActivityRow) { |
| 836 | nextRows.push({ | 844 | nextRows.push({ |
| 837 | kind: "thinking", | 845 | kind: "thinking", |
| 838 | id: "thinking-indicator-row", | 846 | id: LIVE_ACTIVITY_ROW_ID, |
| 839 | createdAt: input.activeTurnStartedAt, | 847 | createdAt: input.activeTurnStartedAt, |
| 840 | }); | 848 | }); |
| 841 | } | 849 | } |
apps/web/src/components/chat/MessagesTimeline.test.tsx+34-8| ... | @@ -1308,7 +1308,30 @@ describe("MessagesTimeline", () => { | ... | @@ -1308,7 +1308,30 @@ describe("MessagesTimeline", () => { |
| 1308 | expect(markup).not.toContain("tool call failed"); | 1308 | expect(markup).not.toContain("tool call failed"); |
| 1309 | }); | 1309 | }); |
| 1310 | 1310 | ||
| 1311 | it("keeps declined command copy visible while thinking continues", () => { | 1311 | it("renders initial thinking as the shared live activity row", () => { |
| 1312 | const turnId = TurnId.make("turn-live"); | ||
| 1313 | const markup = renderToStaticMarkup( | ||
| 1314 | <MessagesTimeline | ||
| 1315 | {...buildProps()} | ||
| 1316 | isWorking | ||
| 1317 | activeTurnStartedAt={MESSAGE_CREATED_AT} | ||
| 1318 | latestTurn={{ | ||
| 1319 | turnId, | ||
| 1320 | state: "running", | ||
| 1321 | startedAt: MESSAGE_CREATED_AT, | ||
| 1322 | completedAt: null, | ||
| 1323 | }} | ||
| 1324 | runningTurnId={turnId} | ||
| 1325 | timelineEntries={[]} | ||
| 1326 | />, | ||
| 1327 | ); | ||
| 1328 | |||
| 1329 | expect(markup).toContain("Thinking"); | ||
| 1330 | expect(markup).toContain("lucide-brain"); | ||
| 1331 | expect(markup).toContain('data-timeline-row-id="live-activity-row"'); | ||
| 1332 | }); | ||
| 1333 | |||
| 1334 | it("keeps the completed command in the shared activity row", () => { | ||
| 1312 | const turnId = TurnId.make("turn-live"); | 1335 | const turnId = TurnId.make("turn-live"); |
| 1313 | const markup = renderToStaticMarkup( | 1336 | const markup = renderToStaticMarkup( |
| 1314 | <MessagesTimeline | 1337 | <MessagesTimeline |
| ... | @@ -1324,28 +1347,31 @@ describe("MessagesTimeline", () => { | ... | @@ -1324,28 +1347,31 @@ describe("MessagesTimeline", () => { |
| 1324 | runningTurnId={turnId} | 1347 | runningTurnId={turnId} |
| 1325 | timelineEntries={[ | 1348 | timelineEntries={[ |
| 1326 | { | 1349 | { |
| 1327 | id: "entry-declined", | 1350 | id: "entry-completed", |
| 1328 | kind: "work", | 1351 | kind: "work", |
| 1329 | createdAt: MESSAGE_CREATED_AT, | 1352 | createdAt: MESSAGE_CREATED_AT, |
| 1330 | entry: { | 1353 | entry: { |
| 1331 | id: "work-declined", | 1354 | id: "work-completed", |
| 1332 | createdAt: MESSAGE_CREATED_AT, | 1355 | createdAt: MESSAGE_CREATED_AT, |
| 1333 | turnId, | 1356 | turnId, |
| 1334 | toolCallId: "call-declined", | 1357 | toolCallId: "call-completed", |
| 1335 | label: "Run lint", | 1358 | label: "Run lint", |
| 1336 | tone: "tool", | 1359 | tone: "tool", |
| 1337 | itemType: "command_execution", | 1360 | itemType: "command_execution", |
| 1338 | command: "pnpm lint", | 1361 | command: "pnpm lint", |
| 1339 | toolLifecycleStatus: "declined", | 1362 | toolLifecycleStatus: "completed", |
| 1340 | }, | 1363 | }, |
| 1341 | }, | 1364 | }, |
| 1342 | ]} | 1365 | ]} |
| 1343 | />, | 1366 | />, |
| 1344 | ); | 1367 | ); |
| 1345 | 1368 | ||
| 1346 | expect(markup).toContain("Declined pnpm"); | 1369 | expect(markup).toContain("Running pnpm"); |
| 1347 | expect(markup).toContain("Thinking"); | 1370 | expect(markup).toContain("lucide-terminal"); |
| 1348 | expect(markup).toContain("tool call failed"); | 1371 | expect(markup).toContain("live-activity-focus"); |
| 1372 | expect(markup).not.toContain("Ran pnpm"); | ||
| 1373 | expect(markup).not.toContain("Thinking"); | ||
| 1374 | expect(markup).not.toContain('data-timeline-row-kind="thinking"'); | ||
| 1349 | }); | 1375 | }); |
| 1350 | 1376 | ||
| 1351 | it("renders review comment contexts as structured cards instead of raw tags", () => { | 1377 | it("renders review comment contexts as structured cards instead of raw tags", () => { |
apps/web/src/components/chat/MessagesTimeline.tsx+9-4| ... | @@ -62,6 +62,7 @@ import { | ... | @@ -62,6 +62,7 @@ import { |
| 62 | import ChatMarkdown, { ChatMarkdownAssetImage } from "../ChatMarkdown"; | 62 | import ChatMarkdown, { ChatMarkdownAssetImage } from "../ChatMarkdown"; |
| 63 | import { | 63 | import { |
| 64 | BotIcon, | 64 | BotIcon, |
| 65 | BrainIcon, | ||
| 65 | CheckIcon, | 66 | CheckIcon, |
| 66 | ChevronDownIcon, | 67 | ChevronDownIcon, |
| 67 | ChevronRightIcon, | 68 | ChevronRightIcon, |
| ... | @@ -985,14 +986,15 @@ const TimelineRowContent = memo(function TimelineRowContent({ row }: { row: Time | ... | @@ -985,14 +986,15 @@ const TimelineRowContent = memo(function TimelineRowContent({ row }: { row: Time |
| 985 | : "pb-0" | 986 | : "pb-0" |
| 986 | : isExpandedToolGroupHeader | 987 | : isExpandedToolGroupHeader |
| 987 | ? "pb-0" | 988 | ? "pb-0" |
| 988 | : row.kind === "turn-fold" || row.kind === "working" || row.kind === "thinking" | 989 | : row.kind === "turn-fold" || row.kind === "working" |
| 989 | ? "pb-1.5" | 990 | ? "pb-1.5" |
| 990 | : (row.kind === "message" && | 991 | : (row.kind === "message" && |
| 991 | row.message.role === "assistant" && | 992 | row.message.role === "assistant" && |
| 992 | !row.showAssistantMeta) || | 993 | !row.showAssistantMeta) || |
| 993 | row.kind === "work" || | 994 | row.kind === "work" || |
| 994 | row.kind === "work-live" || | 995 | row.kind === "work-live" || |
| 995 | row.kind === "work-toggle" | 996 | row.kind === "work-toggle" || |
| 997 | row.kind === "thinking" | ||
| 996 | ? "pb-2" | 998 | ? "pb-2" |
| 997 | : "pb-4", | 999 | : "pb-4", |
| 998 | row.kind === "message" && row.message.role === "assistant" ? "group/assistant" : null, | 1000 | row.kind === "message" && row.message.role === "assistant" ? "group/assistant" : null, |
| ... | @@ -1364,7 +1366,7 @@ function ThinkingTimelineRow() { | ... | @@ -1364,7 +1366,7 @@ function ThinkingTimelineRow() { |
| 1364 | // Reserve the activity row during setup so the handoff keeps the same height. | 1366 | // Reserve the activity row during setup so the handoff keeps the same height. |
| 1365 | return ( | 1367 | return ( |
| 1366 | <div className="min-h-7"> | 1368 | <div className="min-h-7"> |
| 1367 | {isPreparingWorktree ? null : <LiveActivityRow label="Thinking" />} | 1369 | {isPreparingWorktree ? null : <LiveActivityRow label="Thinking" iconName="brain" />} |
| 1368 | </div> | 1370 | </div> |
| 1369 | ); | 1371 | ); |
| 1370 | } | 1372 | } |
| ... | @@ -2123,6 +2125,7 @@ function formatWorkingTimerNow(startIso: string): string { | ... | @@ -2123,6 +2125,7 @@ function formatWorkingTimerNow(startIso: string): string { |
| 2123 | 2125 | ||
| 2124 | type WorkEntryIconName = | 2126 | type WorkEntryIconName = |
| 2125 | | "bot" | 2127 | | "bot" |
| 2128 | | "brain" | ||
| 2126 | | "check" | 2129 | | "check" |
| 2127 | | "circle-alert" | 2130 | | "circle-alert" |
| 2128 | | "eye" | 2131 | | "eye" |
| ... | @@ -2140,6 +2143,8 @@ function WorkEntryIconSvg({ name, className }: { name: WorkEntryIconName; classN | ... | @@ -2140,6 +2143,8 @@ function WorkEntryIconSvg({ name, className }: { name: WorkEntryIconName; classN |
| 2140 | switch (name) { | 2143 | switch (name) { |
| 2141 | case "bot": | 2144 | case "bot": |
| 2142 | return <BotIcon className={className} aria-hidden />; | 2145 | return <BotIcon className={className} aria-hidden />; |
| 2146 | case "brain": | ||
| 2147 | return <BrainIcon className={className} aria-hidden />; | ||
| 2143 | case "check": | 2148 | case "check": |
| 2144 | return <CheckIcon className={className} aria-hidden />; | 2149 | return <CheckIcon className={className} aria-hidden />; |
| 2145 | case "circle-alert": | 2150 | case "circle-alert": |
| ... | @@ -2179,7 +2184,7 @@ function workToneIcon(tone: TimelineWorkEntry["tone"]): { | ... | @@ -2179,7 +2184,7 @@ function workToneIcon(tone: TimelineWorkEntry["tone"]): { |
| 2179 | } | 2184 | } |
| 2180 | if (tone === "thinking") { | 2185 | if (tone === "thinking") { |
| 2181 | return { | 2186 | return { |
| 2182 | iconName: "bot", | 2187 | iconName: "brain", |
| 2183 | className: "text-foreground", | 2188 | className: "text-foreground", |
| 2184 | }; | 2189 | }; |
| 2185 | } | 2190 | } |