| 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,6 +870,7 @@ describe("buildThreadFeed", () => { |
| 870 | status: undefined, | 870 | status: undefined, |
| 871 | displayName: "Click in the preview browser", | 871 | displayName: "Click in the preview browser", |
| 872 | liveDisplayName: "Clicking in the preview browser", | 872 | liveDisplayName: "Clicking in the preview browser", |
| 873 | settledDisplayName: "Clicked in the preview browser", | ||
| 873 | icon: "browser", | 874 | icon: "browser", |
| 874 | }, | 875 | }, |
| 875 | { | 876 | { |
| ... | @@ -880,11 +881,12 @@ describe("buildThreadFeed", () => { | ... | @@ -880,11 +881,12 @@ describe("buildThreadFeed", () => { |
| 880 | status: undefined, | 881 | status: undefined, |
| 881 | displayName: "Get delegated task status", | 882 | displayName: "Get delegated task status", |
| 882 | liveDisplayName: "Getting delegated task status", | 883 | liveDisplayName: "Getting delegated task status", |
| 884 | settledDisplayName: "Got delegated task status", | ||
| 883 | icon: "t3-code", | 885 | icon: "t3-code", |
| 884 | }, | 886 | }, |
| 885 | ])( | 887 | ])( |
| 886 | "uses friendly row and running labels from $source", | 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 | const turnId = TurnId.make("turn-friendly-mcp"); | 890 | const turnId = TurnId.make("turn-friendly-mcp"); |
| 889 | const rawCommand = "node mcp-call.js"; | 891 | const rawCommand = "node mcp-call.js"; |
| 890 | const rawDetail = '{"provider":"raw MCP output"}'; | 892 | const rawDetail = '{"provider":"raw MCP output"}'; |
| ... | @@ -949,6 +951,23 @@ describe("buildThreadFeed", () => { | ... | @@ -949,6 +951,23 @@ describe("buildThreadFeed", () => { |
| 949 | live: true, | 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,7 +1060,7 @@ describe("buildThreadFeed", () => { |
| 1041 | hasFailure: true, | 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 | ({ status, displayName, detail, hasFailure }) => { | 1064 | ({ status, displayName, detail, hasFailure }) => { |
| 1046 | const turnId = TurnId.make("turn-preview-lifecycle"); | 1065 | const turnId = TurnId.make("turn-preview-lifecycle"); |
| 1047 | const toolCallId = "preview-click"; | 1066 | const toolCallId = "preview-click"; |
| ... | @@ -1176,7 +1195,7 @@ describe("buildThreadFeed", () => { | ... | @@ -1176,7 +1195,7 @@ describe("buildThreadFeed", () => { |
| 1176 | groupId, | 1195 | groupId, |
| 1177 | hiddenCount: 1, | 1196 | hiddenCount: 1, |
| 1178 | expanded: true, | 1197 | expanded: true, |
| 1179 | summary: "Used browser 1 time", | 1198 | summary: displayName, |
| 1180 | summaryKind: "browser", | 1199 | summaryKind: "browser", |
| 1181 | hasFailure, | 1200 | hasFailure, |
| 1182 | live: false, | 1201 | live: false, |
| ... | @@ -1762,7 +1781,11 @@ describe("buildThreadFeed", () => { | ... | @@ -1762,7 +1781,11 @@ describe("buildThreadFeed", () => { |
| 1762 | const stoppedRows = deriveThreadFeedPresentation(feed, latestTurn, new Set()); | 1781 | const stoppedRows = deriveThreadFeedPresentation(feed, latestTurn, new Set()); |
| 1763 | expect(stoppedRows.filter((entry) => entry.type === "work-toggle")).toMatchObject([ | 1782 | expect(stoppedRows.filter((entry) => entry.type === "work-toggle")).toMatchObject([ |
| 1764 | { live: false, shimmer: false }, | 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 | const completedRows = deriveThreadFeedPresentation( | 1791 | const completedRows = deriveThreadFeedPresentation( |
apps/mobile/src/lib/threadActivity.ts+21-4| ... | @@ -21,6 +21,7 @@ import { | ... | @@ -21,6 +21,7 @@ import { |
| 21 | omitSupersededLifecycleMarkers, | 21 | omitSupersededLifecycleMarkers, |
| 22 | resolveWorkEntryToolPresentation, | 22 | resolveWorkEntryToolPresentation, |
| 23 | summarizeToolGroup, | 23 | summarizeToolGroup, |
| 24 | toolGroupAction, | ||
| 24 | toolGroupSummaryKind, | 25 | toolGroupSummaryKind, |
| 25 | type ToolGroupSummaryKind, | 26 | type ToolGroupSummaryKind, |
| 26 | } from "@t3tools/client-runtime/work-log/presentation"; | 27 | } from "@t3tools/client-runtime/work-log/presentation"; |
| ... | @@ -837,6 +838,13 @@ function workEntryHeading(workEntry: WorkLogEntry): string { | ... | @@ -837,6 +838,13 @@ function workEntryHeading(workEntry: WorkLogEntry): string { |
| 837 | return capitalizePhrase(normalizeCompactToolLabel(workEntry.toolTitle)); | 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 | function asRecord(value: unknown): Record<string, unknown> | null { | 848 | function asRecord(value: unknown): Record<string, unknown> | null { |
| 841 | return value && typeof value === "object" ? (value as Record<string, unknown>) : null; | 849 | return value && typeof value === "object" ? (value as Record<string, unknown>) : null; |
| 842 | } | 850 | } |
| ... | @@ -1518,14 +1526,23 @@ function appendToolGroupRows( | ... | @@ -1518,14 +1526,23 @@ function appendToolGroupRows( |
| 1518 | const active = latestActiveActivity !== undefined; | 1526 | const active = latestActiveActivity !== undefined; |
| 1519 | const live = activeTail || active; | 1527 | const live = activeTail || active; |
| 1520 | const latestActivity = latestActiveActivity ?? activities.at(-1)!; | 1528 | const latestActivity = latestActiveActivity ?? activities.at(-1)!; |
| 1529 | const singleActivity = activities.length === 1 ? latestActivity : null; | ||
| 1521 | const summary = live | 1530 | const summary = live |
| 1522 | ? liveToolActivitySummary(latestActivity, active) | 1531 | ? liveToolActivitySummary(latestActivity, active) |
| 1523 | : activities.length === 1 && !activities[0]!.toolLike | 1532 | : singleActivity !== null && |
| 1524 | ? activities[0]!.workEntry.label | 1533 | singleActivity.toolLike && |
| 1525 | : summarizeToolGroup(activities.map((activity) => activity.workEntry)); | 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 | const summaryToolIcon = live | 1539 | const summaryToolIcon = live |
| 1527 | ? resolveWorkEntryToolPresentation(latestActivity.workEntry)?.icon | 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 | result.push({ | 1546 | result.push({ |
| 1530 | type: "work-toggle", | 1547 | type: "work-toggle", |
| 1531 | id: `${live ? "work-live" : "work-toggle"}:${groupId}`, | 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,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", () => { | 155 | it.each([ |
| 156 | const rows = deriveMessagesTimelineRows({ | 156 | ["preview_click", "Clicked in the preview browser", "browser"], |
| 157 | timelineEntries: [ | 157 | ["task_status", "Got delegated task status", "t3-code"], |
| 158 | { | 158 | ] as const)( |
| 159 | id: "browser-entry", | 159 | "uses the completed %s call presentation for a settled legacy tool", |
| 160 | kind: "work", | 160 | (tool, summary, summaryToolIcon) => { |
| 161 | createdAt: entry.createdAt, | 161 | const rows = deriveMessagesTimelineRows({ |
| 162 | entry: { | 162 | timelineEntries: [ |
| 163 | ...entry, | 163 | { |
| 164 | itemType: "mcp_tool_call", | 164 | id: "browser-entry", |
| 165 | toolLifecycleStatus: "completed", | 165 | kind: "work", |
| 166 | toolData: { server: "t3-code", tool: "preview_click" }, | 166 | createdAt: entry.createdAt, |
| 167 | entry: { | ||
| 168 | ...entry, | ||
| 169 | itemType: "mcp_tool_call", | ||
| 170 | toolData: { server: "t3-code", tool }, | ||
| 171 | }, | ||
| 167 | }, | 172 | }, |
| 168 | }, | 173 | ], |
| 169 | ], | 174 | isWorking: false, |
| 170 | isWorking: false, | 175 | activeTurnStartedAt: null, |
| 171 | activeTurnStartedAt: null, | 176 | turnDiffSummaryByAssistantMessageId: new Map(), |
| 172 | turnDiffSummaryByAssistantMessageId: new Map(), | 177 | revertTurnCountByUserMessageId: new Map(), |
| 173 | revertTurnCountByUserMessageId: new Map(), | 178 | }); |
| 174 | }); | 179 | expect(rows).toMatchObject([ |
| 175 | expect(rows).toMatchObject([ | 180 | { kind: "work-toggle", hiddenCount: 1, summary, summaryToolIcon }, |
| 176 | { kind: "work-toggle", summary: "Used browser 1 time", summaryKind: "browser" }, | 181 | ]); |
| 177 | ]); | 182 | }, |
| 178 | }); | 183 | ); |
| 179 | }); | 184 | }); |
| 180 | 185 | ||
| 181 | describe("shouldPreserveAssistantLineBreaks", () => { | 186 | describe("shouldPreserveAssistantLineBreaks", () => { |
| ... | @@ -1126,7 +1131,7 @@ describe("deriveMessagesTimelineRows", () => { | ... | @@ -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 | const rows = deriveMessagesTimelineRows({ | 1135 | const rows = deriveMessagesTimelineRows({ |
| 1131 | timelineEntries: [ | 1136 | timelineEntries: [ |
| 1132 | { | 1137 | { |
| ... | @@ -1189,7 +1194,7 @@ describe("deriveMessagesTimelineRows", () => { | ... | @@ -1189,7 +1194,7 @@ describe("deriveMessagesTimelineRows", () => { |
| 1189 | expect(rows.map((row) => row.kind)).toEqual(["working", "work-toggle", "message", "work-live"]); | 1194 | expect(rows.map((row) => row.kind)).toEqual(["working", "work-toggle", "message", "work-live"]); |
| 1190 | expect(rows.find((row) => row.kind === "work-toggle")).toMatchObject({ | 1195 | expect(rows.find((row) => row.kind === "work-toggle")).toMatchObject({ |
| 1191 | hiddenCount: 1, | 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 +6,7 @@ import { |
| 6 | omitSupersededLifecycleMarkers, | 6 | omitSupersededLifecycleMarkers, |
| 7 | resolveWorkEntryToolPresentation, | 7 | resolveWorkEntryToolPresentation, |
| 8 | summarizeToolGroup, | 8 | summarizeToolGroup, |
| 9 | toolGroupAction, | ||
| 9 | toolGroupSummaryKind, | 10 | toolGroupSummaryKind, |
| 10 | type ToolGroupSummaryKind, | 11 | type ToolGroupSummaryKind, |
| 11 | } from "@t3tools/client-runtime/work-log/presentation"; | 12 | } from "@t3tools/client-runtime/work-log/presentation"; |
| ... | @@ -34,6 +35,15 @@ export const TIMELINE_MINIMAP_MAX_HEIGHT_CSS = "calc(100vh - 18rem)"; | ... | @@ -34,6 +35,15 @@ export const TIMELINE_MINIMAP_MAX_HEIGHT_CSS = "calc(100vh - 18rem)"; |
| 34 | export const TIMELINE_CONTENT_MAX_WIDTH = 768; | 35 | export const TIMELINE_CONTENT_MAX_WIDTH = 768; |
| 35 | export const TIMELINE_MINIMAP_PERSISTENT_GUTTER = 48; | 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 | export function workEntryDisplayLabel(entry: WorkLogEntry, workspaceRoot: string | undefined) { | 47 | export function workEntryDisplayLabel(entry: WorkLogEntry, workspaceRoot: string | undefined) { |
| 38 | const toolPresentation = resolveWorkEntryToolPresentation(entry); | 48 | const toolPresentation = resolveWorkEntryToolPresentation(entry); |
| 39 | if (toolPresentation) return toolPresentation.displayName; | 49 | if (toolPresentation) return toolPresentation.displayName; |
| ... | @@ -295,6 +305,7 @@ export type MessagesTimelineRow = | ... | @@ -295,6 +305,7 @@ export type MessagesTimelineRow = |
| 295 | expanded: boolean; | 305 | expanded: boolean; |
| 296 | summary: string; | 306 | summary: string; |
| 297 | summaryKind: ToolGroupSummaryKind; | 307 | summaryKind: ToolGroupSummaryKind; |
| 308 | summaryToolIcon?: "browser" | "t3-code"; | ||
| 298 | hasFailure: boolean; | 309 | hasFailure: boolean; |
| 299 | } | 310 | } |
| 300 | | { | 311 | | { |
| ... | @@ -840,6 +851,15 @@ export function deriveMessagesTimelineRows(input: { | ... | @@ -840,6 +851,15 @@ export function deriveMessagesTimelineRows(input: { |
| 840 | const expanded = input.expandedWorkGroupIds?.has(groupId) ?? false; | 851 | const expanded = input.expandedWorkGroupIds?.has(groupId) ?? false; |
| 841 | const summaryKind = toolGroupSummaryKind(visibleGroupedEntries); | 852 | const summaryKind = toolGroupSummaryKind(visibleGroupedEntries); |
| 842 | const latestToolEntry = visibleGroupedEntries.findLast(workLogEntryIsToolLike); | 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 | nextRows.push({ | 863 | nextRows.push({ |
| 844 | kind: "work-toggle", | 864 | kind: "work-toggle", |
| 845 | id: `work-toggle:${timelineEntry.id}`, | 865 | id: `work-toggle:${timelineEntry.id}`, |
| ... | @@ -847,12 +867,13 @@ export function deriveMessagesTimelineRows(input: { | ... | @@ -847,12 +867,13 @@ export function deriveMessagesTimelineRows(input: { |
| 847 | groupId, | 867 | groupId, |
| 848 | hiddenCount: visibleGroupedEntries.length, | 868 | hiddenCount: visibleGroupedEntries.length, |
| 849 | expanded, | 869 | expanded, |
| 850 | summary: | 870 | summary: usesSingleToolCallLabel |
| 851 | visibleGroupedEntries.length === 1 && | 871 | ? singleToolCallLabel(singleEntry) |
| 852 | !workLogEntryIsToolLike(visibleGroupedEntries[0]!) | 872 | : singleEntry !== null && !workLogEntryIsToolLike(singleEntry) |
| 853 | ? visibleGroupedEntries[0]!.label | 873 | ? singleEntry.label |
| 854 | : summarizeToolGroup(visibleGroupedEntries), | 874 | : summarizeToolGroup(visibleGroupedEntries), |
| 855 | summaryKind, | 875 | summaryKind, |
| 876 | ...(summaryToolIcon ? { summaryToolIcon } : {}), | ||
| 856 | hasFailure: | 877 | hasFailure: |
| 857 | latestToolEntry !== undefined && | 878 | latestToolEntry !== undefined && |
| 858 | workEntryDisplayIndicatesToolFailure(latestToolEntry), | 879 | workEntryDisplayIndicatesToolFailure(latestToolEntry), |
apps/web/src/components/chat/MessagesTimeline.tsx+1-1| ... | @@ -1851,7 +1851,7 @@ function WorkGroupToggleTimelineRow({ | ... | @@ -1851,7 +1851,7 @@ function WorkGroupToggleTimelineRow({ |
| 1851 | > | 1851 | > |
| 1852 | <span className="flex size-6 shrink-0 items-center justify-center text-icon-muted"> | 1852 | <span className="flex size-6 shrink-0 items-center justify-center text-icon-muted"> |
| 1853 | <WorkEntryIcon | 1853 | <WorkEntryIcon |
| 1854 | name={toolGroupSummaryIconName(row.summaryKind)} | 1854 | name={row.summaryToolIcon ?? toolGroupSummaryIconName(row.summaryKind)} |
| 1855 | className="size-4 shrink-0 stroke-[1.8] opacity-70" | 1855 | className="size-4 shrink-0 stroke-[1.8] opacity-70" |
| 1856 | /> | 1856 | /> |
| 1857 | </span> | 1857 | </span> |