History

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

maria committed 2 months ago

Commit ec44bc51 parent 922bd698 files · +54 −37Browse files

Unified · Split

8 files
  1. apps/mobile/src/lib/threadActivity.test.ts +8 −6
  2. apps/mobile/src/lib/threadActivity.ts +8 −7
  3. apps/web/src/components/chat/MessagesTimeline.logic.test.ts +13 −10
  4. apps/web/src/components/chat/MessagesTimeline.logic.ts +6 −5
  5. apps/web/src/components/chat/MessagesTimeline.test.tsx +3 −3
  6. docs/user/tool-activity.md +4 −1
  7. packages/client-runtime/src/work-log/presentation.test.ts +4 −4
  8. packages/client-runtime/src/work-log/presentation.ts +8 −1
apps/mobile/src/lib/threadActivity.test.ts+8−6
@@ -868,7 +868,7 @@ describe("buildThreadFeed", () => { · 867 unmodified lines
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", () => { · 4 unmodified lines
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", () => { · 153 unmodified lines
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", () => { · 4 unmodified lines
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", () => { · 88 unmodified lines
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", () => { · 514 unmodified lines
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 { · 16 unmodified lines
1717 commandDetailRepeatsCommand,
1818 extractCommandOutputText,
1919 isWorktreeSetupActivity,
20+ liveActivityToolStatus,
2021 normalizeCompactToolLabel,
2122 omitSupersededLifecycleMarkers,
2223 resolveWorkEntryToolPresentation,
@@ -1528,7 +1529,7 @@ function appendToolGroupRows( · 1505 unmodified lines
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( · 45 unmodified lines
15801581 });
15811582 }
15821583
1583−function liveToolActivitySummary(activity: ThreadFeedActivity, active: boolean): string {
1584− const presentation = resolveWorkEntryToolPresentation(
1585− activity.workEntry,
1586− active ? "inProgress" : "completed",
1587− );
1584+function 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", () => { · 98 unmodified lines
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", () => { · 22 unmodified lines
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"; · 1 unmodified line
22 import { renderCodexDirectivesForCopy } from "@t3tools/client-runtime/codex-markdown-directives";
33 import { commandProgramName } from "@t3tools/client-runtime/work-log/command-label";
44 import {
5+ liveActivityToolStatus,
56 normalizeCompactToolLabel,
67 omitSupersededLifecycleMarkers,
78 resolveWorkEntryToolPresentation,
@@ -65,14 +66,14 @@ export function liveWorkEntryLabel( · 57 unmodified lines
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", () => { · 1223 unmodified lines
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", () => { · 28 unmodified lines
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. · 7 unmodified lines
88 Collapsing and reopening a group preserves your reading position and any open call details.
99
1010 Recognized T3 tools use descriptive labels in both the running summary and individual rows.
11−Labels follow the call's state, such as "Clicking" while running and "Clicked" after success.
11+The 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.
13+Expanded rows follow the call's own state, such as "Clicked" after success.
14+When a call has not reported a state yet, the label stays in the present tense.
1215 Failed, declined, and stopped calls say what happened without implying success.
1316 Preview browser actions use a globe icon. Other T3 tools keep the T3 mark.
1417 Group 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", () => { · 24 unmodified lines
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", () => { · 5 unmodified lines
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", () => { · 2 unmodified lines
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", () => { · 62 unmodified lines
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 · 112 unmodified lines
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 · 1 unmodified line
121121 };
122122 }
123123
124+/** Latest live activity stays present-tense unless the call itself failed, declined, or stopped. */
125+export 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. */
125132 export function resolveWorkEntryToolPresentation(
126133 entry: Pick<WorkLogPresentationEntry, "label" | "toolTitle" | "toolData" | "toolLifecycleStatus">,