authorgravatar for maria@kuuro.netmaria <maria@kuuro.net> 2026-09-02 22:39:09-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-02 22:39:09-04:00
logec44bc56f598be16ffbc22ed0e4e095447043844
treee125223869b0e3345d20bc620b3ba6b9b33fa5f2
parent922bd692251bc803c12a3fab159efe83c957bb70
signature Signed by PGP key B5690EEEBB952194

fix(chat): keep live tool labels in present tense (#9316)

Co-authored-by: maria-rcks <254055478+maria-rcks@users.noreply.github.com>

8 files changed, 54 insertions(+), 37 deletions(-)

apps/mobile/src/lib/threadActivity.test.ts+8-6
......@@ -868,7 +868,7 @@ describe("buildThreadFeed", () => {
868868 title: "Call MCP tool",
869869 item: { server: "t3-code", tool: "preview_click" },
870870 status: undefined,
871 displayName: "Click in the preview browser",
871 displayName: "Clicking in the preview browser",
872872 liveDisplayName: "Clicking in the preview browser",
873873 settledDisplayName: "Clicked in the preview browser",
874874 icon: "browser",
......@@ -879,7 +879,7 @@ describe("buildThreadFeed", () => {
879879 title: "Call MCP tool",
880880 item: { server: "t3-code", tool: "task_status" },
881881 status: undefined,
882 displayName: "Get delegated task status",
882 displayName: "Getting delegated task status",
883883 liveDisplayName: "Getting delegated task status",
884884 settledDisplayName: "Got delegated task status",
885885 icon: "t3-code",
......@@ -1039,7 +1039,7 @@ describe("buildThreadFeed", () => {
10391039 ).toMatchObject([
10401040 {
10411041 type: "work-toggle",
1042 summary: "Clicked in the preview browser",
1042 summary: "Clicking in the preview browser",
10431043 summaryToolIcon: "browser",
10441044 live: true,
10451045 },
......@@ -1050,18 +1050,20 @@ describe("buildThreadFeed", () => {
10501050 {
10511051 status: "completed",
10521052 displayName: "Clicked in the preview browser",
1053 liveDisplayName: "Clicking in the preview browser",
10531054 detail: "Clicked Continue",
10541055 hasFailure: false,
10551056 },
10561057 {
10571058 status: "failed",
10581059 displayName: "Failed to click in the preview browser",
1060 liveDisplayName: "Failed to click in the preview browser",
10591061 detail: "Timed out waiting for Continue",
10601062 hasFailure: true,
10611063 },
10621064 ])(
10631065 "uses the browser call label once its action settles as $status",
1064 ({ status, displayName, detail, hasFailure }) => {
1066 ({ status, displayName, liveDisplayName, detail, hasFailure }) => {
10651067 const turnId = TurnId.make("turn-preview-lifecycle");
10661068 const toolCallId = "preview-click";
10671069 const groupId = `work-group:tool:${turnId}:${toolCallId}`;
......@@ -1156,7 +1158,7 @@ describe("buildThreadFeed", () => {
11561158 groupId,
11571159 hiddenCount: 1,
11581160 expanded: true,
1159 summary: displayName,
1161 summary: liveDisplayName,
11601162 summaryToolIcon: "browser",
11611163 hasFailure,
11621164 live: true,
......@@ -1677,7 +1679,7 @@ describe("buildThreadFeed", () => {
16771679 (
16781680 [
16791681 { lifecycleStatus: "inProgress", summary: "Running pnpm", shimmer: true },
1680 { lifecycleStatus: "completed", summary: "Ran pnpm", shimmer: false },
1682 { lifecycleStatus: "completed", summary: "Running pnpm", shimmer: false },
16811683 { lifecycleStatus: "failed", summary: "Failed pnpm", shimmer: false },
16821684 { lifecycleStatus: "declined", summary: "Declined pnpm", shimmer: false },
16831685 { lifecycleStatus: "stopped", summary: "Stopped pnpm", shimmer: false },
apps/mobile/src/lib/threadActivity.ts+8-7
......@@ -17,6 +17,7 @@ import {
1717 commandDetailRepeatsCommand,
1818 extractCommandOutputText,
1919 isWorktreeSetupActivity,
20 liveActivityToolStatus,
2021 normalizeCompactToolLabel,
2122 omitSupersededLifecycleMarkers,
2223 resolveWorkEntryToolPresentation,
......@@ -1528,7 +1529,7 @@ function appendToolGroupRows(
15281529 const latestActivity = latestActiveActivity ?? activities.at(-1)!;
15291530 const singleActivity = activities.length === 1 ? latestActivity : null;
15301531 const summary = live
1531 ? liveToolActivitySummary(latestActivity, active)
1532 ? liveToolActivitySummary(latestActivity, live)
15321533 : singleActivity !== null &&
15331534 singleActivity.toolLike &&
15341535 toolGroupAction(singleActivity.workEntry) !== "edit"
......@@ -1580,16 +1581,16 @@ function appendToolGroupRows(
15801581 });
15811582}
15821583
1583function liveToolActivitySummary(activity: ThreadFeedActivity, active: boolean): string {
1584 const presentation = resolveWorkEntryToolPresentation(
1585 activity.workEntry,
1586 active ? "inProgress" : "completed",
1587 );
1584function liveToolActivitySummary(activity: ThreadFeedActivity, presentTense: boolean): string {
1585 const status = liveActivityToolStatus(activity.lifecycleStatus, presentTense);
1586 const presentation = resolveWorkEntryToolPresentation({
1587 ...activity.workEntry,
1588 toolLifecycleStatus: status,
1589 });
15881590 if (presentation) return presentation.displayName;
15891591 const command = activity.workEntry.command?.trim();
15901592 if (command) {
15911593 const program = commandProgramName(command);
1592 const status = activity.lifecycleStatus ?? (active ? "inProgress" : "completed");
15931594 const verb =
15941595 status === "inProgress"
15951596 ? "Running"
apps/web/src/components/chat/MessagesTimeline.logic.test.ts+13-10
......@@ -99,13 +99,16 @@ describe("work entry labels", () => {
9999 );
100100 });
101101
102 it("does not describe a finished call as still running while the turn continues", () => {
102 it("keeps the latest live activity in the present tense after the call completes", () => {
103103 const browserEntry = {
104104 ...entry,
105105 toolTitle: "T3-code.preview_click",
106106 toolLifecycleStatus: "completed" as const,
107107 };
108108 expect(liveWorkEntryLabel(browserEntry, undefined, true)).toBe(
109 "Clicking in the preview browser",
110 );
111 expect(liveWorkEntryLabel(browserEntry, undefined, false)).toBe(
109112 "Clicked in the preview browser",
110113 );
111114 });
......@@ -134,21 +137,21 @@ describe("work entry labels", () => {
134137 });
135138
136139 it.each([
137 ["inProgress", "Running vp"],
138 ["completed", "Ran vp"],
139 ["failed", "Failed vp"],
140 ["declined", "Declined vp"],
141 ["stopped", "Stopped vp"],
140 ["inProgress", "Running vp", "Running vp"],
141 ["completed", "Running vp", "Ran vp"],
142 ["failed", "Failed vp", "Failed vp"],
143 ["declined", "Declined vp", "Declined vp"],
144 ["stopped", "Stopped vp", "Stopped vp"],
142145 ] as const)(
143 "uses the command's %s outcome even while the turn continues",
144 (toolLifecycleStatus, label) => {
146 "uses present tense for a live %s command and the outcome once it is no longer live",
147 (toolLifecycleStatus, liveLabel, settledLabel) => {
145148 const commandEntry = {
146149 ...entry,
147150 command: "/bin/bash -lc 'vp test run'",
148151 toolLifecycleStatus,
149152 };
150 expect(liveWorkEntryLabel(commandEntry, undefined, true)).toBe(label);
151 expect(liveWorkEntryLabel(commandEntry, undefined, false)).toBe(label);
153 expect(liveWorkEntryLabel(commandEntry, undefined, true)).toBe(liveLabel);
154 expect(liveWorkEntryLabel(commandEntry, undefined, false)).toBe(settledLabel);
152155 },
153156 );
154157
apps/web/src/components/chat/MessagesTimeline.logic.ts+6-5
......@@ -2,6 +2,7 @@ import * as Equal from "effect/Equal";
22import { renderCodexDirectivesForCopy } from "@t3tools/client-runtime/codex-markdown-directives";
33import { commandProgramName } from "@t3tools/client-runtime/work-log/command-label";
44import {
5 liveActivityToolStatus,
56 normalizeCompactToolLabel,
67 omitSupersededLifecycleMarkers,
78 resolveWorkEntryToolPresentation,
......@@ -65,14 +66,14 @@ export function liveWorkEntryLabel(
6566 workspaceRoot: string | undefined,
6667 active: boolean,
6768) {
68 const toolPresentation = resolveWorkEntryToolPresentation(
69 entry,
70 active ? "inProgress" : "completed",
71 );
69 const status = liveActivityToolStatus(entry.toolLifecycleStatus, active);
70 const toolPresentation = resolveWorkEntryToolPresentation({
71 ...entry,
72 toolLifecycleStatus: status,
73 });
7274 if (toolPresentation) return toolPresentation.displayName;
7375 const command = entry.command?.trim();
7476 if (command) {
75 const status = entry.toolLifecycleStatus ?? (active ? "inProgress" : "completed");
7677 const verb =
7778 status === "inProgress"
7879 ? "Running"
apps/web/src/components/chat/MessagesTimeline.test.tsx+3-3
......@@ -1224,7 +1224,7 @@ describe("MessagesTimeline", () => {
12241224 expect(markup).toContain('data-timeline-row-id="live-activity-row"');
12251225 });
12261226
1227 it("keeps the completed command in the shared activity row with a past-tense label", () => {
1227 it("keeps the completed command in the shared activity row with a present-tense label", () => {
12281228 const turnId = TurnId.make("turn-live");
12291229 const markup = renderToStaticMarkup(
12301230 <MessagesTimeline
......@@ -1259,10 +1259,10 @@ describe("MessagesTimeline", () => {
12591259 />,
12601260 );
12611261
1262 expect(markup).toContain("Ran pnpm");
1262 expect(markup).toContain("Running pnpm");
12631263 expect(markup).toContain("lucide-terminal");
12641264 expect(markup).toContain("live-activity-focus");
1265 expect(markup).not.toContain("Running pnpm");
1265 expect(markup).not.toContain("Ran pnpm");
12661266 expect(markup).not.toContain("Thinking");
12671267 expect(markup).not.toContain('data-timeline-row-kind="thinking"');
12681268 });
docs/user/tool-activity.md+4-1
......@@ -8,7 +8,10 @@ indicate more calls above or below. Short groups use only the space they need.
88Collapsing and reopening a group preserves your reading position and any open call details.
99
1010Recognized T3 tools use descriptive labels in both the running summary and individual rows.
11Labels follow the call's state, such as "Clicking" while running and "Clicked" after success.
11The latest live activity stays in the present tense while the turn continues, such as
12"Running vp" or "Clicking in the preview browser", even after that call has completed.
13Expanded rows follow the call's own state, such as "Clicked" after success.
14When a call has not reported a state yet, the label stays in the present tense.
1215Failed, declined, and stopped calls say what happened without implying success.
1316Preview browser actions use a globe icon. Other T3 tools keep the T3 mark.
1417Group summaries count browser actions separately, such as "Used browser 18 times" or
packages/client-runtime/src/work-log/presentation.test.ts+4-4
......@@ -25,7 +25,7 @@ describe("resolveWorkEntryToolPresentation", () => {
2525 "preview_click",
2626 ])("recognizes browser tool names across providers: %s", (label) => {
2727 expect(resolveWorkEntryToolPresentation({ label })).toEqual({
28 displayName: "Click in the preview browser",
28 displayName: "Clicking in the preview browser",
2929 icon: "browser",
3030 });
3131 });
......@@ -37,7 +37,7 @@ describe("resolveWorkEntryToolPresentation", () => {
3737 toolTitle: "Inspect the current page",
3838 toolData: { server: "t3-code", tool: "preview_snapshot", result: { title: "Example" } },
3939 }),
40 ).toEqual({ displayName: "Take a snapshot of the preview page", icon: "browser" });
40 ).toEqual({ displayName: "Taking a snapshot of the preview page", icon: "browser" });
4141 });
4242
4343 it.each([
......@@ -46,7 +46,7 @@ describe("resolveWorkEntryToolPresentation", () => {
4646 ["failed", "Failed to click in the preview browser"],
4747 ["declined", "Declined to click in the preview browser"],
4848 ["stopped", "Stopped clicking in the preview browser"],
49 ["unknown", "Click in the preview browser"],
49 ["unknown", "Clicking in the preview browser"],
5050 ])("describes the tool's own %s state", (toolLifecycleStatus, displayName) => {
5151 expect(
5252 resolveWorkEntryToolPresentation({
......@@ -115,7 +115,7 @@ describe("resolveWorkEntryToolPresentation", () => {
115115 label: "mcp__t3_code__task_status",
116116 toolTitle: "Check the child task",
117117 }),
118 ).toEqual({ displayName: "Get delegated task status", icon: "t3-code" });
118 ).toEqual({ displayName: "Getting delegated task status", icon: "t3-code" });
119119 });
120120
121121 it("does not brand unknown tools or another server's matching tool name", () => {
packages/client-runtime/src/work-log/presentation.ts+8-1
......@@ -113,7 +113,7 @@ function resolveT3McpToolPresentation(value: string | undefined, status: string
113113 ? `Declined to ${action.toLowerCase()}`
114114 : status === "stopped"
115115 ? `Stopped ${running.toLowerCase()}`
116 : action;
116 : running;
117117
118118 return {
119119 displayName: `${verb} ${detail}`,
......@@ -121,6 +121,13 @@ function resolveT3McpToolPresentation(value: string | undefined, status: string
121121 };
122122}
123123
124/** Latest live activity stays present-tense unless the call itself failed, declined, or stopped. */
125export function liveActivityToolStatus(status: string | undefined, presentTense: boolean) {
126 if (status === "failed" || status === "declined" || status === "stopped") return status;
127 if (presentTense || status === "inProgress") return "inProgress";
128 return "completed";
129}
130
124131/** Resolves tool identity before choosing labels or icons in either client. */
125132export function resolveWorkEntryToolPresentation(
126133 entry: Pick<WorkLogPresentationEntry, "label" | "toolTitle" | "toolData" | "toolLifecycleStatus">,