| author | |
| committer | |
| log | 46b5c66406b9942589d7e9132beeafda2434f113 |
| tree | 275558d8190760e990418f3a6f391414df930e69 |
| parent | 62c68dc41f9d9d7ecaf5193a6a8d997ebf671a61 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: maria-rcks <254055478+maria-rcks@users.noreply.github.com>5 files changed, 104 insertions(+), 38 deletions(-)
apps/mobile/src/lib/threadActivity.test.ts+27-4| ... | ... | @@ -870,6 +870,7 @@ describe("buildThreadFeed", () => { |
| 870 | 870 | status: undefined, |
| 871 | 871 | displayName: "Click in the preview browser", |
| 872 | 872 | liveDisplayName: "Clicking in the preview browser", |
| 873 | settledDisplayName: "Clicked in the preview browser", | |
| 873 | 874 | icon: "browser", |
| 874 | 875 | }, |
| 875 | 876 | { |
| ... | ... | @@ -880,11 +881,12 @@ describe("buildThreadFeed", () => { |
| 880 | 881 | status: undefined, |
| 881 | 882 | displayName: "Get delegated task status", |
| 882 | 883 | liveDisplayName: "Getting delegated task status", |
| 884 | settledDisplayName: "Got delegated task status", | |
| 883 | 885 | icon: "t3-code", |
| 884 | 886 | }, |
| 885 | 887 | ])( |
| 886 | 888 | "uses friendly row and running labels from $source", |
| 887 | ({ label, title, item, status, displayName, liveDisplayName, icon }) => { | |
| 889 | ({ label, title, item, status, displayName, liveDisplayName, settledDisplayName, icon }) => { | |
| 888 | 890 | const turnId = TurnId.make("turn-friendly-mcp"); |
| 889 | 891 | const rawCommand = "node mcp-call.js"; |
| 890 | 892 | const rawDetail = '{"provider":"raw MCP output"}'; |
| ... | ... | @@ -949,6 +951,23 @@ describe("buildThreadFeed", () => { |
| 949 | 951 | live: true, |
| 950 | 952 | }, |
| 951 | 953 | ]); |
| 954 | if (settledDisplayName) { | |
| 955 | const settledRows = deriveThreadFeedPresentation( | |
| 956 | feed, | |
| 957 | { | |
| 958 | ...thread.latestTurn!, | |
| 959 | state: "completed", | |
| 960 | completedAt: "2026-04-01T00:00:03.000Z", | |
| 961 | }, | |
| 962 | new Set([turnId]), | |
| 963 | new Set(), | |
| 964 | ); | |
| 965 | expect(settledRows.find((entry) => entry.type === "work-toggle")).toMatchObject({ | |
| 966 | summary: settledDisplayName, | |
| 967 | summaryToolIcon: icon, | |
| 968 | live: false, | |
| 969 | }); | |
| 970 | } | |
| 952 | 971 | }, |
| 953 | 972 | ); |
| 954 | 973 | |
| ... | ... | @@ -1041,7 +1060,7 @@ describe("buildThreadFeed", () => { |
| 1041 | 1060 | hasFailure: true, |
| 1042 | 1061 | }, |
| 1043 | 1062 | ])( |
| 1044 | "keeps a browser group expanded as its action changes from active to $status", | |
| 1063 | "uses the browser call label once its action settles as $status", | |
| 1045 | 1064 | ({ status, displayName, detail, hasFailure }) => { |
| 1046 | 1065 | const turnId = TurnId.make("turn-preview-lifecycle"); |
| 1047 | 1066 | const toolCallId = "preview-click"; |
| ... | ... | @@ -1176,7 +1195,7 @@ describe("buildThreadFeed", () => { |
| 1176 | 1195 | groupId, |
| 1177 | 1196 | hiddenCount: 1, |
| 1178 | 1197 | expanded: true, |
| 1179 | summary: "Used browser 1 time", | |
| 1198 | summary: displayName, | |
| 1180 | 1199 | summaryKind: "browser", |
| 1181 | 1200 | hasFailure, |
| 1182 | 1201 | live: false, |
| ... | ... | @@ -1762,7 +1781,11 @@ describe("buildThreadFeed", () => { |
| 1762 | 1781 | const stoppedRows = deriveThreadFeedPresentation(feed, latestTurn, new Set()); |
| 1763 | 1782 | expect(stoppedRows.filter((entry) => entry.type === "work-toggle")).toMatchObject([ |
| 1764 | 1783 | { live: false, shimmer: false }, |
| 1765 | { live: false, shimmer: false }, | |
| 1784 | { | |
| 1785 | live: false, | |
| 1786 | shimmer: false, | |
| 1787 | summary: lifecycleStatus === "inProgress" ? "printf done" : command, | |
| 1788 | }, | |
| 1766 | 1789 | ]); |
| 1767 | 1790 | |
| 1768 | 1791 | const completedRows = deriveThreadFeedPresentation( |
apps/mobile/src/lib/threadActivity.ts+21-4| ... | ... | @@ -21,6 +21,7 @@ import { |
| 21 | 21 | omitSupersededLifecycleMarkers, |
| 22 | 22 | resolveWorkEntryToolPresentation, |
| 23 | 23 | summarizeToolGroup, |
| 24 | toolGroupAction, | |
| 24 | 25 | toolGroupSummaryKind, |
| 25 | 26 | type ToolGroupSummaryKind, |
| 26 | 27 | } from "@t3tools/client-runtime/work-log/presentation"; |
| ... | ... | @@ -837,6 +838,13 @@ function workEntryHeading(workEntry: WorkLogEntry): string { |
| 837 | 838 | return capitalizePhrase(normalizeCompactToolLabel(workEntry.toolTitle)); |
| 838 | 839 | } |
| 839 | 840 | |
| 841 | function singleToolCallLabel(activity: ThreadFeedActivity): string { | |
| 842 | const presentation = resolveWorkEntryToolPresentation(activity.workEntry, "completed"); | |
| 843 | if (presentation) return presentation.displayName; | |
| 844 | const command = activity.workEntry.command?.trim(); | |
| 845 | return command || activity.summary; | |
| 846 | } | |
| 847 | ||
| 840 | 848 | function asRecord(value: unknown): Record<string, unknown> | null { |
| 841 | 849 | return value && typeof value === "object" ? (value as Record<string, unknown>) : null; |
| 842 | 850 | } |
| ... | ... | @@ -1518,14 +1526,23 @@ function appendToolGroupRows( |
| 1518 | 1526 | const active = latestActiveActivity !== undefined; |
| 1519 | 1527 | const live = activeTail || active; |
| 1520 | 1528 | const latestActivity = latestActiveActivity ?? activities.at(-1)!; |
| 1529 | const singleActivity = activities.length === 1 ? latestActivity : null; | |
| 1521 | 1530 | const summary = live |
| 1522 | 1531 | ? liveToolActivitySummary(latestActivity, active) |
| 1523 | : activities.length === 1 && !activities[0]!.toolLike | |
| 1524 | ? activities[0]!.workEntry.label | |
| 1525 | : summarizeToolGroup(activities.map((activity) => activity.workEntry)); | |
| 1532 | : singleActivity !== null && | |
| 1533 | singleActivity.toolLike && | |
| 1534 | toolGroupAction(singleActivity.workEntry) !== "edit" | |
| 1535 | ? singleToolCallLabel(singleActivity) | |
| 1536 | : singleActivity !== null && !singleActivity.toolLike | |
| 1537 | ? singleActivity.workEntry.label | |
| 1538 | : summarizeToolGroup(activities.map((activity) => activity.workEntry)); | |
| 1526 | 1539 | const summaryToolIcon = live |
| 1527 | 1540 | ? resolveWorkEntryToolPresentation(latestActivity.workEntry)?.icon |
| 1528 | : undefined; | |
| 1541 | : singleActivity !== null && | |
| 1542 | singleActivity.toolLike && | |
| 1543 | toolGroupAction(singleActivity.workEntry) !== "edit" | |
| 1544 | ? resolveWorkEntryToolPresentation(singleActivity.workEntry, "completed")?.icon | |
| 1545 | : undefined; | |
| 1529 | 1546 | result.push({ |
| 1530 | 1547 | type: "work-toggle", |
| 1531 | 1548 | id: `${live ? "work-live" : "work-toggle"}:${groupId}`, |
apps/web/src/components/chat/MessagesTimeline.logic.test.ts+30-25| ... | ... | @@ -152,30 +152,35 @@ describe("work entry labels", () => { |
| 152 | 152 | }, |
| 153 | 153 | ); |
| 154 | 154 | |
| 155 | it("gives a completed browser group its own count and summary icon category", () => { | |
| 156 | const rows = deriveMessagesTimelineRows({ | |
| 157 | timelineEntries: [ | |
| 158 | { | |
| 159 | id: "browser-entry", | |
| 160 | kind: "work", | |
| 161 | createdAt: entry.createdAt, | |
| 162 | entry: { | |
| 163 | ...entry, | |
| 164 | itemType: "mcp_tool_call", | |
| 165 | toolLifecycleStatus: "completed", | |
| 166 | toolData: { server: "t3-code", tool: "preview_click" }, | |
| 155 | it.each([ | |
| 156 | ["preview_click", "Clicked in the preview browser", "browser"], | |
| 157 | ["task_status", "Got delegated task status", "t3-code"], | |
| 158 | ] as const)( | |
| 159 | "uses the completed %s call presentation for a settled legacy tool", | |
| 160 | (tool, summary, summaryToolIcon) => { | |
| 161 | const rows = deriveMessagesTimelineRows({ | |
| 162 | timelineEntries: [ | |
| 163 | { | |
| 164 | id: "browser-entry", | |
| 165 | kind: "work", | |
| 166 | createdAt: entry.createdAt, | |
| 167 | entry: { | |
| 168 | ...entry, | |
| 169 | itemType: "mcp_tool_call", | |
| 170 | toolData: { server: "t3-code", tool }, | |
| 171 | }, | |
| 167 | 172 | }, |
| 168 | }, | |
| 169 | ], | |
| 170 | isWorking: false, | |
| 171 | activeTurnStartedAt: null, | |
| 172 | turnDiffSummaryByAssistantMessageId: new Map(), | |
| 173 | revertTurnCountByUserMessageId: new Map(), | |
| 174 | }); | |
| 175 | expect(rows).toMatchObject([ | |
| 176 | { kind: "work-toggle", summary: "Used browser 1 time", summaryKind: "browser" }, | |
| 177 | ]); | |
| 178 | }); | |
| 173 | ], | |
| 174 | isWorking: false, | |
| 175 | activeTurnStartedAt: null, | |
| 176 | turnDiffSummaryByAssistantMessageId: new Map(), | |
| 177 | revertTurnCountByUserMessageId: new Map(), | |
| 178 | }); | |
| 179 | expect(rows).toMatchObject([ | |
| 180 | { kind: "work-toggle", hiddenCount: 1, summary, summaryToolIcon }, | |
| 181 | ]); | |
| 182 | }, | |
| 183 | ); | |
| 179 | 184 | }); |
| 180 | 185 | |
| 181 | 186 | describe("shouldPreserveAssistantLineBreaks", () => { |
| ... | ... | @@ -1126,7 +1131,7 @@ describe("deriveMessagesTimelineRows", () => { |
| 1126 | 1131 | }); |
| 1127 | 1132 | }); |
| 1128 | 1133 | |
| 1129 | it("summarizes a tool run after commentary starts a new run", () => { | |
| 1134 | it("labels a single completed tool call without summarizing it", () => { | |
| 1130 | 1135 | const rows = deriveMessagesTimelineRows({ |
| 1131 | 1136 | timelineEntries: [ |
| 1132 | 1137 | { |
| ... | ... | @@ -1189,7 +1194,7 @@ describe("deriveMessagesTimelineRows", () => { |
| 1189 | 1194 | expect(rows.map((row) => row.kind)).toEqual(["working", "work-toggle", "message", "work-live"]); |
| 1190 | 1195 | expect(rows.find((row) => row.kind === "work-toggle")).toMatchObject({ |
| 1191 | 1196 | hiddenCount: 1, |
| 1192 | summary: "Ran 1 command", | |
| 1197 | summary: "rg toolCall", | |
| 1193 | 1198 | }); |
| 1194 | 1199 | }); |
| 1195 | 1200 |
apps/web/src/components/chat/MessagesTimeline.logic.ts+25-4| ... | ... | @@ -6,6 +6,7 @@ import { |
| 6 | 6 | omitSupersededLifecycleMarkers, |
| 7 | 7 | resolveWorkEntryToolPresentation, |
| 8 | 8 | summarizeToolGroup, |
| 9 | toolGroupAction, | |
| 9 | 10 | toolGroupSummaryKind, |
| 10 | 11 | type ToolGroupSummaryKind, |
| 11 | 12 | } from "@t3tools/client-runtime/work-log/presentation"; |
| ... | ... | @@ -34,6 +35,15 @@ export const TIMELINE_MINIMAP_MAX_HEIGHT_CSS = "calc(100vh - 18rem)"; |
| 34 | 35 | export const TIMELINE_CONTENT_MAX_WIDTH = 768; |
| 35 | 36 | export const TIMELINE_MINIMAP_PERSISTENT_GUTTER = 48; |
| 36 | 37 | |
| 38 | function singleToolCallLabel(entry: WorkLogEntry): string { | |
| 39 | const toolPresentation = resolveWorkEntryToolPresentation(entry, "completed"); | |
| 40 | if (toolPresentation) return toolPresentation.displayName; | |
| 41 | const command = entry.command?.trim(); | |
| 42 | if (command) return command; | |
| 43 | const heading = normalizeCompactToolLabel(entry.toolTitle || entry.label); | |
| 44 | return `${heading.charAt(0).toUpperCase()}${heading.slice(1)}`; | |
| 45 | } | |
| 46 | ||
| 37 | 47 | export function workEntryDisplayLabel(entry: WorkLogEntry, workspaceRoot: string | undefined) { |
| 38 | 48 | const toolPresentation = resolveWorkEntryToolPresentation(entry); |
| 39 | 49 | if (toolPresentation) return toolPresentation.displayName; |
| ... | ... | @@ -295,6 +305,7 @@ export type MessagesTimelineRow = |
| 295 | 305 | expanded: boolean; |
| 296 | 306 | summary: string; |
| 297 | 307 | summaryKind: ToolGroupSummaryKind; |
| 308 | summaryToolIcon?: "browser" | "t3-code"; | |
| 298 | 309 | hasFailure: boolean; |
| 299 | 310 | } |
| 300 | 311 | | { |
| ... | ... | @@ -840,6 +851,15 @@ export function deriveMessagesTimelineRows(input: { |
| 840 | 851 | const expanded = input.expandedWorkGroupIds?.has(groupId) ?? false; |
| 841 | 852 | const summaryKind = toolGroupSummaryKind(visibleGroupedEntries); |
| 842 | 853 | const latestToolEntry = visibleGroupedEntries.findLast(workLogEntryIsToolLike); |
| 854 | const singleEntry = | |
| 855 | visibleGroupedEntries.length === 1 ? (visibleGroupedEntries[0] ?? null) : null; | |
| 856 | const usesSingleToolCallLabel = | |
| 857 | singleEntry !== null && | |
| 858 | workLogEntryIsToolLike(singleEntry) && | |
| 859 | toolGroupAction(singleEntry) !== "edit"; | |
| 860 | const summaryToolIcon = usesSingleToolCallLabel | |
| 861 | ? resolveWorkEntryToolPresentation(singleEntry, "completed")?.icon | |
| 862 | : undefined; | |
| 843 | 863 | nextRows.push({ |
| 844 | 864 | kind: "work-toggle", |
| 845 | 865 | id: `work-toggle:${timelineEntry.id}`, |
| ... | ... | @@ -847,12 +867,13 @@ export function deriveMessagesTimelineRows(input: { |
| 847 | 867 | groupId, |
| 848 | 868 | hiddenCount: visibleGroupedEntries.length, |
| 849 | 869 | expanded, |
| 850 | summary: | |
| 851 | visibleGroupedEntries.length === 1 && | |
| 852 | !workLogEntryIsToolLike(visibleGroupedEntries[0]!) | |
| 853 | ? visibleGroupedEntries[0]!.label | |
| 870 | summary: usesSingleToolCallLabel | |
| 871 | ? singleToolCallLabel(singleEntry) | |
| 872 | : singleEntry !== null && !workLogEntryIsToolLike(singleEntry) | |
| 873 | ? singleEntry.label | |
| 854 | 874 | : summarizeToolGroup(visibleGroupedEntries), |
| 855 | 875 | summaryKind, |
| 876 | ...(summaryToolIcon ? { summaryToolIcon } : {}), | |
| 856 | 877 | hasFailure: |
| 857 | 878 | latestToolEntry !== undefined && |
| 858 | 879 | workEntryDisplayIndicatesToolFailure(latestToolEntry), |
apps/web/src/components/chat/MessagesTimeline.tsx+1-1| ... | ... | @@ -1851,7 +1851,7 @@ function WorkGroupToggleTimelineRow({ |
| 1851 | 1851 | > |
| 1852 | 1852 | <span className="flex size-6 shrink-0 items-center justify-center text-icon-muted"> |
| 1853 | 1853 | <WorkEntryIcon |
| 1854 | name={toolGroupSummaryIconName(row.summaryKind)} | |
| 1854 | name={row.summaryToolIcon ?? toolGroupSummaryIconName(row.summaryKind)} | |
| 1855 | 1855 | className="size-4 shrink-0 stroke-[1.8] opacity-70" |
| 1856 | 1856 | /> |
| 1857 | 1857 | </span> |