authorgravatar for maria@kuuro.netmaria <maria@kuuro.net> 2026-09-02 17:21:26-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-02 17:21:26-04:00
log46b5c66406b9942589d7e9132beeafda2434f113
tree275558d8190760e990418f3a6f391414df930e69
parent62c68dc41f9d9d7ecaf5193a6a8d997ebf671a61
signature Signed by PGP key B5690EEEBB952194

fix(chat): show single tool calls without summaries (#9267)

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", () => {
870870 status: undefined,
871871 displayName: "Click in the preview browser",
872872 liveDisplayName: "Clicking in the preview browser",
873 settledDisplayName: "Clicked in the preview browser",
873874 icon: "browser",
874875 },
875876 {
......@@ -880,11 +881,12 @@ describe("buildThreadFeed", () => {
880881 status: undefined,
881882 displayName: "Get delegated task status",
882883 liveDisplayName: "Getting delegated task status",
884 settledDisplayName: "Got delegated task status",
883885 icon: "t3-code",
884886 },
885887 ])(
886888 "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 }) => {
888890 const turnId = TurnId.make("turn-friendly-mcp");
889891 const rawCommand = "node mcp-call.js";
890892 const rawDetail = '{"provider":"raw MCP output"}';
......@@ -949,6 +951,23 @@ describe("buildThreadFeed", () => {
949951 live: true,
950952 },
951953 ]);
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 }
952971 },
953972 );
954973
......@@ -1041,7 +1060,7 @@ describe("buildThreadFeed", () => {
10411060 hasFailure: true,
10421061 },
10431062 ])(
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",
10451064 ({ status, displayName, detail, hasFailure }) => {
10461065 const turnId = TurnId.make("turn-preview-lifecycle");
10471066 const toolCallId = "preview-click";
......@@ -1176,7 +1195,7 @@ describe("buildThreadFeed", () => {
11761195 groupId,
11771196 hiddenCount: 1,
11781197 expanded: true,
1179 summary: "Used browser 1 time",
1198 summary: displayName,
11801199 summaryKind: "browser",
11811200 hasFailure,
11821201 live: false,
......@@ -1762,7 +1781,11 @@ describe("buildThreadFeed", () => {
17621781 const stoppedRows = deriveThreadFeedPresentation(feed, latestTurn, new Set());
17631782 expect(stoppedRows.filter((entry) => entry.type === "work-toggle")).toMatchObject([
17641783 { live: false, shimmer: false },
1765 { live: false, shimmer: false },
1784 {
1785 live: false,
1786 shimmer: false,
1787 summary: lifecycleStatus === "inProgress" ? "printf done" : command,
1788 },
17661789 ]);
17671790
17681791 const completedRows = deriveThreadFeedPresentation(
apps/mobile/src/lib/threadActivity.ts+21-4
......@@ -21,6 +21,7 @@ import {
2121 omitSupersededLifecycleMarkers,
2222 resolveWorkEntryToolPresentation,
2323 summarizeToolGroup,
24 toolGroupAction,
2425 toolGroupSummaryKind,
2526 type ToolGroupSummaryKind,
2627} from "@t3tools/client-runtime/work-log/presentation";
......@@ -837,6 +838,13 @@ function workEntryHeading(workEntry: WorkLogEntry): string {
837838 return capitalizePhrase(normalizeCompactToolLabel(workEntry.toolTitle));
838839}
839840
841function 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
840848function asRecord(value: unknown): Record<string, unknown> | null {
841849 return value && typeof value === "object" ? (value as Record<string, unknown>) : null;
842850}
......@@ -1518,14 +1526,23 @@ function appendToolGroupRows(
15181526 const active = latestActiveActivity !== undefined;
15191527 const live = activeTail || active;
15201528 const latestActivity = latestActiveActivity ?? activities.at(-1)!;
1529 const singleActivity = activities.length === 1 ? latestActivity : null;
15211530 const summary = live
15221531 ? 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));
15261539 const summaryToolIcon = live
15271540 ? 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;
15291546 result.push({
15301547 type: "work-toggle",
15311548 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", () => {
152152 },
153153 );
154154
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 },
167172 },
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 );
179184});
180185
181186describe("shouldPreserveAssistantLineBreaks", () => {
......@@ -1126,7 +1131,7 @@ describe("deriveMessagesTimelineRows", () => {
11261131 });
11271132 });
11281133
1129 it("summarizes a tool run after commentary starts a new run", () => {
1134 it("labels a single completed tool call without summarizing it", () => {
11301135 const rows = deriveMessagesTimelineRows({
11311136 timelineEntries: [
11321137 {
......@@ -1189,7 +1194,7 @@ describe("deriveMessagesTimelineRows", () => {
11891194 expect(rows.map((row) => row.kind)).toEqual(["working", "work-toggle", "message", "work-live"]);
11901195 expect(rows.find((row) => row.kind === "work-toggle")).toMatchObject({
11911196 hiddenCount: 1,
1192 summary: "Ran 1 command",
1197 summary: "rg toolCall",
11931198 });
11941199 });
11951200
apps/web/src/components/chat/MessagesTimeline.logic.ts+25-4
......@@ -6,6 +6,7 @@ import {
66 omitSupersededLifecycleMarkers,
77 resolveWorkEntryToolPresentation,
88 summarizeToolGroup,
9 toolGroupAction,
910 toolGroupSummaryKind,
1011 type ToolGroupSummaryKind,
1112} from "@t3tools/client-runtime/work-log/presentation";
......@@ -34,6 +35,15 @@ export const TIMELINE_MINIMAP_MAX_HEIGHT_CSS = "calc(100vh - 18rem)";
3435export const TIMELINE_CONTENT_MAX_WIDTH = 768;
3536export const TIMELINE_MINIMAP_PERSISTENT_GUTTER = 48;
3637
38function 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
3747export function workEntryDisplayLabel(entry: WorkLogEntry, workspaceRoot: string | undefined) {
3848 const toolPresentation = resolveWorkEntryToolPresentation(entry);
3949 if (toolPresentation) return toolPresentation.displayName;
......@@ -295,6 +305,7 @@ export type MessagesTimelineRow =
295305 expanded: boolean;
296306 summary: string;
297307 summaryKind: ToolGroupSummaryKind;
308 summaryToolIcon?: "browser" | "t3-code";
298309 hasFailure: boolean;
299310 }
300311 | {
......@@ -840,6 +851,15 @@ export function deriveMessagesTimelineRows(input: {
840851 const expanded = input.expandedWorkGroupIds?.has(groupId) ?? false;
841852 const summaryKind = toolGroupSummaryKind(visibleGroupedEntries);
842853 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;
843863 nextRows.push({
844864 kind: "work-toggle",
845865 id: `work-toggle:${timelineEntry.id}`,
......@@ -847,12 +867,13 @@ export function deriveMessagesTimelineRows(input: {
847867 groupId,
848868 hiddenCount: visibleGroupedEntries.length,
849869 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
854874 : summarizeToolGroup(visibleGroupedEntries),
855875 summaryKind,
876 ...(summaryToolIcon ? { summaryToolIcon } : {}),
856877 hasFailure:
857878 latestToolEntry !== undefined &&
858879 workEntryDisplayIndicatesToolFailure(latestToolEntry),
apps/web/src/components/chat/MessagesTimeline.tsx+1-1
......@@ -1851,7 +1851,7 @@ function WorkGroupToggleTimelineRow({
18511851 >
18521852 <span className="flex size-6 shrink-0 items-center justify-center text-icon-muted">
18531853 <WorkEntryIcon
1854 name={toolGroupSummaryIconName(row.summaryKind)}
1854 name={row.summaryToolIcon ?? toolGroupSummaryIconName(row.summaryKind)}
18551855 className="size-4 shrink-0 stroke-[1.8] opacity-70"
18561856 />
18571857 </span>