| author | |
| committer | |
| log | ec44bc56f598be16ffbc22ed0e4e095447043844 |
| tree | e125223869b0e3345d20bc620b3ba6b9b33fa5f2 |
| parent | 922bd692251bc803c12a3fab159efe83c957bb70 |
| signature | Signed by PGP key B5690EEEBB952194 |
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", () => { |
| 868 | 868 | title: "Call MCP tool", |
| 869 | 869 | item: { server: "t3-code", tool: "preview_click" }, |
| 870 | 870 | status: undefined, |
| 871 | displayName: "Click in the preview browser", | |
| 871 | displayName: "Clicking in the preview browser", | |
| 872 | 872 | liveDisplayName: "Clicking in the preview browser", |
| 873 | 873 | settledDisplayName: "Clicked in the preview browser", |
| 874 | 874 | icon: "browser", |
| ... | ... | @@ -879,7 +879,7 @@ describe("buildThreadFeed", () => { |
| 879 | 879 | title: "Call MCP tool", |
| 880 | 880 | item: { server: "t3-code", tool: "task_status" }, |
| 881 | 881 | status: undefined, |
| 882 | displayName: "Get delegated task status", | |
| 882 | displayName: "Getting delegated task status", | |
| 883 | 883 | liveDisplayName: "Getting delegated task status", |
| 884 | 884 | settledDisplayName: "Got delegated task status", |
| 885 | 885 | icon: "t3-code", |
| ... | ... | @@ -1039,7 +1039,7 @@ describe("buildThreadFeed", () => { |
| 1039 | 1039 | ).toMatchObject([ |
| 1040 | 1040 | { |
| 1041 | 1041 | type: "work-toggle", |
| 1042 | summary: "Clicked in the preview browser", | |
| 1042 | summary: "Clicking in the preview browser", | |
| 1043 | 1043 | summaryToolIcon: "browser", |
| 1044 | 1044 | live: true, |
| 1045 | 1045 | }, |
| ... | ... | @@ -1050,18 +1050,20 @@ describe("buildThreadFeed", () => { |
| 1050 | 1050 | { |
| 1051 | 1051 | status: "completed", |
| 1052 | 1052 | displayName: "Clicked in the preview browser", |
| 1053 | liveDisplayName: "Clicking in the preview browser", | |
| 1053 | 1054 | detail: "Clicked Continue", |
| 1054 | 1055 | hasFailure: false, |
| 1055 | 1056 | }, |
| 1056 | 1057 | { |
| 1057 | 1058 | status: "failed", |
| 1058 | 1059 | displayName: "Failed to click in the preview browser", |
| 1060 | liveDisplayName: "Failed to click in the preview browser", | |
| 1059 | 1061 | detail: "Timed out waiting for Continue", |
| 1060 | 1062 | hasFailure: true, |
| 1061 | 1063 | }, |
| 1062 | 1064 | ])( |
| 1063 | 1065 | "uses the browser call label once its action settles as $status", |
| 1064 | ({ status, displayName, detail, hasFailure }) => { | |
| 1066 | ({ status, displayName, liveDisplayName, detail, hasFailure }) => { | |
| 1065 | 1067 | const turnId = TurnId.make("turn-preview-lifecycle"); |
| 1066 | 1068 | const toolCallId = "preview-click"; |
| 1067 | 1069 | const groupId = `work-group:tool:${turnId}:${toolCallId}`; |
| ... | ... | @@ -1156,7 +1158,7 @@ describe("buildThreadFeed", () => { |
| 1156 | 1158 | groupId, |
| 1157 | 1159 | hiddenCount: 1, |
| 1158 | 1160 | expanded: true, |
| 1159 | summary: displayName, | |
| 1161 | summary: liveDisplayName, | |
| 1160 | 1162 | summaryToolIcon: "browser", |
| 1161 | 1163 | hasFailure, |
| 1162 | 1164 | live: true, |
| ... | ... | @@ -1677,7 +1679,7 @@ describe("buildThreadFeed", () => { |
| 1677 | 1679 | ( |
| 1678 | 1680 | [ |
| 1679 | 1681 | { lifecycleStatus: "inProgress", summary: "Running pnpm", shimmer: true }, |
| 1680 | { lifecycleStatus: "completed", summary: "Ran pnpm", shimmer: false }, | |
| 1682 | { lifecycleStatus: "completed", summary: "Running pnpm", shimmer: false }, | |
| 1681 | 1683 | { lifecycleStatus: "failed", summary: "Failed pnpm", shimmer: false }, |
| 1682 | 1684 | { lifecycleStatus: "declined", summary: "Declined pnpm", shimmer: false }, |
| 1683 | 1685 | { lifecycleStatus: "stopped", summary: "Stopped pnpm", shimmer: false }, |
apps/mobile/src/lib/threadActivity.ts+8-7| ... | ... | @@ -17,6 +17,7 @@ import { |
| 17 | 17 | commandDetailRepeatsCommand, |
| 18 | 18 | extractCommandOutputText, |
| 19 | 19 | isWorktreeSetupActivity, |
| 20 | liveActivityToolStatus, | |
| 20 | 21 | normalizeCompactToolLabel, |
| 21 | 22 | omitSupersededLifecycleMarkers, |
| 22 | 23 | resolveWorkEntryToolPresentation, |
| ... | ... | @@ -1528,7 +1529,7 @@ function appendToolGroupRows( |
| 1528 | 1529 | const latestActivity = latestActiveActivity ?? activities.at(-1)!; |
| 1529 | 1530 | const singleActivity = activities.length === 1 ? latestActivity : null; |
| 1530 | 1531 | const summary = live |
| 1531 | ? liveToolActivitySummary(latestActivity, active) | |
| 1532 | ? liveToolActivitySummary(latestActivity, live) | |
| 1532 | 1533 | : singleActivity !== null && |
| 1533 | 1534 | singleActivity.toolLike && |
| 1534 | 1535 | toolGroupAction(singleActivity.workEntry) !== "edit" |
| ... | ... | @@ -1580,16 +1581,16 @@ function appendToolGroupRows( |
| 1580 | 1581 | }); |
| 1581 | 1582 | } |
| 1582 | 1583 | |
| 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 | }); | |
| 1588 | 1590 | if (presentation) return presentation.displayName; |
| 1589 | 1591 | const command = activity.workEntry.command?.trim(); |
| 1590 | 1592 | if (command) { |
| 1591 | 1593 | const program = commandProgramName(command); |
| 1592 | const status = activity.lifecycleStatus ?? (active ? "inProgress" : "completed"); | |
| 1593 | 1594 | const verb = |
| 1594 | 1595 | status === "inProgress" |
| 1595 | 1596 | ? "Running" |
apps/web/src/components/chat/MessagesTimeline.logic.test.ts+13-10| ... | ... | @@ -99,13 +99,16 @@ describe("work entry labels", () => { |
| 99 | 99 | ); |
| 100 | 100 | }); |
| 101 | 101 | |
| 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", () => { | |
| 103 | 103 | const browserEntry = { |
| 104 | 104 | ...entry, |
| 105 | 105 | toolTitle: "T3-code.preview_click", |
| 106 | 106 | toolLifecycleStatus: "completed" as const, |
| 107 | 107 | }; |
| 108 | 108 | expect(liveWorkEntryLabel(browserEntry, undefined, true)).toBe( |
| 109 | "Clicking in the preview browser", | |
| 110 | ); | |
| 111 | expect(liveWorkEntryLabel(browserEntry, undefined, false)).toBe( | |
| 109 | 112 | "Clicked in the preview browser", |
| 110 | 113 | ); |
| 111 | 114 | }); |
| ... | ... | @@ -134,21 +137,21 @@ describe("work entry labels", () => { |
| 134 | 137 | }); |
| 135 | 138 | |
| 136 | 139 | 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"], | |
| 142 | 145 | ] 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) => { | |
| 145 | 148 | const commandEntry = { |
| 146 | 149 | ...entry, |
| 147 | 150 | command: "/bin/bash -lc 'vp test run'", |
| 148 | 151 | toolLifecycleStatus, |
| 149 | 152 | }; |
| 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); | |
| 152 | 155 | }, |
| 153 | 156 | ); |
| 154 | 157 |
apps/web/src/components/chat/MessagesTimeline.logic.ts+6-5| ... | ... | @@ -2,6 +2,7 @@ import * as Equal from "effect/Equal"; |
| 2 | 2 | import { renderCodexDirectivesForCopy } from "@t3tools/client-runtime/codex-markdown-directives"; |
| 3 | 3 | import { commandProgramName } from "@t3tools/client-runtime/work-log/command-label"; |
| 4 | 4 | import { |
| 5 | liveActivityToolStatus, | |
| 5 | 6 | normalizeCompactToolLabel, |
| 6 | 7 | omitSupersededLifecycleMarkers, |
| 7 | 8 | resolveWorkEntryToolPresentation, |
| ... | ... | @@ -65,14 +66,14 @@ export function liveWorkEntryLabel( |
| 65 | 66 | workspaceRoot: string | undefined, |
| 66 | 67 | active: boolean, |
| 67 | 68 | ) { |
| 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 | }); | |
| 72 | 74 | if (toolPresentation) return toolPresentation.displayName; |
| 73 | 75 | const command = entry.command?.trim(); |
| 74 | 76 | if (command) { |
| 75 | const status = entry.toolLifecycleStatus ?? (active ? "inProgress" : "completed"); | |
| 76 | 77 | const verb = |
| 77 | 78 | status === "inProgress" |
| 78 | 79 | ? "Running" |
apps/web/src/components/chat/MessagesTimeline.test.tsx+3-3| ... | ... | @@ -1224,7 +1224,7 @@ describe("MessagesTimeline", () => { |
| 1224 | 1224 | expect(markup).toContain('data-timeline-row-id="live-activity-row"'); |
| 1225 | 1225 | }); |
| 1226 | 1226 | |
| 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", () => { | |
| 1228 | 1228 | const turnId = TurnId.make("turn-live"); |
| 1229 | 1229 | const markup = renderToStaticMarkup( |
| 1230 | 1230 | <MessagesTimeline |
| ... | ... | @@ -1259,10 +1259,10 @@ describe("MessagesTimeline", () => { |
| 1259 | 1259 | />, |
| 1260 | 1260 | ); |
| 1261 | 1261 | |
| 1262 | expect(markup).toContain("Ran pnpm"); | |
| 1262 | expect(markup).toContain("Running pnpm"); | |
| 1263 | 1263 | expect(markup).toContain("lucide-terminal"); |
| 1264 | 1264 | expect(markup).toContain("live-activity-focus"); |
| 1265 | expect(markup).not.toContain("Running pnpm"); | |
| 1265 | expect(markup).not.toContain("Ran pnpm"); | |
| 1266 | 1266 | expect(markup).not.toContain("Thinking"); |
| 1267 | 1267 | expect(markup).not.toContain('data-timeline-row-kind="thinking"'); |
| 1268 | 1268 | }); |
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. |
| 8 | 8 | Collapsing and reopening a group preserves your reading position and any open call details. |
| 9 | 9 | |
| 10 | 10 | 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. | |
| 12 | 15 | Failed, declined, and stopped calls say what happened without implying success. |
| 13 | 16 | Preview browser actions use a globe icon. Other T3 tools keep the T3 mark. |
| 14 | 17 | 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", () => { |
| 25 | 25 | "preview_click", |
| 26 | 26 | ])("recognizes browser tool names across providers: %s", (label) => { |
| 27 | 27 | expect(resolveWorkEntryToolPresentation({ label })).toEqual({ |
| 28 | displayName: "Click in the preview browser", | |
| 28 | displayName: "Clicking in the preview browser", | |
| 29 | 29 | icon: "browser", |
| 30 | 30 | }); |
| 31 | 31 | }); |
| ... | ... | @@ -37,7 +37,7 @@ describe("resolveWorkEntryToolPresentation", () => { |
| 37 | 37 | toolTitle: "Inspect the current page", |
| 38 | 38 | toolData: { server: "t3-code", tool: "preview_snapshot", result: { title: "Example" } }, |
| 39 | 39 | }), |
| 40 | ).toEqual({ displayName: "Take a snapshot of the preview page", icon: "browser" }); | |
| 40 | ).toEqual({ displayName: "Taking a snapshot of the preview page", icon: "browser" }); | |
| 41 | 41 | }); |
| 42 | 42 | |
| 43 | 43 | it.each([ |
| ... | ... | @@ -46,7 +46,7 @@ describe("resolveWorkEntryToolPresentation", () => { |
| 46 | 46 | ["failed", "Failed to click in the preview browser"], |
| 47 | 47 | ["declined", "Declined to click in the preview browser"], |
| 48 | 48 | ["stopped", "Stopped clicking in the preview browser"], |
| 49 | ["unknown", "Click in the preview browser"], | |
| 49 | ["unknown", "Clicking in the preview browser"], | |
| 50 | 50 | ])("describes the tool's own %s state", (toolLifecycleStatus, displayName) => { |
| 51 | 51 | expect( |
| 52 | 52 | resolveWorkEntryToolPresentation({ |
| ... | ... | @@ -115,7 +115,7 @@ describe("resolveWorkEntryToolPresentation", () => { |
| 115 | 115 | label: "mcp__t3_code__task_status", |
| 116 | 116 | toolTitle: "Check the child task", |
| 117 | 117 | }), |
| 118 | ).toEqual({ displayName: "Get delegated task status", icon: "t3-code" }); | |
| 118 | ).toEqual({ displayName: "Getting delegated task status", icon: "t3-code" }); | |
| 119 | 119 | }); |
| 120 | 120 | |
| 121 | 121 | 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 |
| 113 | 113 | ? `Declined to ${action.toLowerCase()}` |
| 114 | 114 | : status === "stopped" |
| 115 | 115 | ? `Stopped ${running.toLowerCase()}` |
| 116 | : action; | |
| 116 | : running; | |
| 117 | 117 | |
| 118 | 118 | return { |
| 119 | 119 | displayName: `${verb} ${detail}`, |
| ... | ... | @@ -121,6 +121,13 @@ function resolveT3McpToolPresentation(value: string | undefined, status: string |
| 121 | 121 | }; |
| 122 | 122 | } |
| 123 | 123 | |
| 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 | ||
| 124 | 131 | /** Resolves tool identity before choosing labels or icons in either client. */ |
| 125 | 132 | export function resolveWorkEntryToolPresentation( |
| 126 | 133 | entry: Pick<WorkLogPresentationEntry, "label" | "toolTitle" | "toolData" | "toolLifecycleStatus">, |