1import { describe, expect, it } from "vite-plus/test";
2
3import { ThreadId, TurnItemId, type OrchestrationV2TurnItem } from "@t3tools/contracts";
4import * as DateTime from "effect/DateTime";
5import { T3_MCP_TOOL_NAMES } from "@t3tools/shared/t3McpToolPresentation";
6
7import {
8 commandDetailRepeatsCommand,
9 contextCompactionLabel,
10 extractCommandOutputText,
11 liveThoughtLine,
12 resolveViewedImageAsset,
13 resolveWorkEntryToolPresentation,
14 summarizeToolGroup,
15 toolGroupAction,
16 toolGroupSummaryKind,
17 toolItemForDisplay,
18 type WorkLogPresentationEntry,
19 type WorkLogToolLifecycleStatus,
20 workEntryViewedImagePath,
21 workEntryIndicatesToolFailure,
22 workEntryDisplayIndicatesToolFailure,
23 workEntryIndicatesToolSuccess,
24} from "./presentation.js";
25
26describe("contextCompactionLabel", () => {
27 it.each(["pending", "running", "waiting"] as const)("reports %s as active", (status) => {
28 expect(contextCompactionLabel({ status })).toBe("Compacting context");
29 });
30
31 it.each(["failed", "cancelled", "interrupted"] as const)(
32 "does not report %s compaction as successful even with token counts",
33 (status) => {
34 expect(contextCompactionLabel({ status, beforeTokenCount: 1000, afterTokenCount: 100 })).toBe(
35 status === "failed" ? "Context compaction failed" : "Context compaction stopped",
36 );
37 },
38 );
39});
40
41function commandItem(
42 fields: Partial<Extract<OrchestrationV2TurnItem, { type: "command_execution" }>> = {},
43): OrchestrationV2TurnItem {
44 return {
45 id: TurnItemId.make("command"),
46 threadId: ThreadId.make("thread"),
47 runId: null,
48 nodeId: null,
49 providerThreadId: null,
50 providerTurnId: null,
51 nativeItemRef: null,
52 parentItemId: null,
53 ordinal: 1,
54 status: "completed",
55 title: null,
56 startedAt: null,
57 completedAt: null,
58 updatedAt: DateTime.makeUnsafe("2026-09-08T00:00:00.000Z"),
59 type: "command_execution",
60 input: 'rg "command not found"',
61 exitCode: 0,
62 ...fields,
63 };
64}
65
66describe("workEntryIndicatesToolFailure", () => {
67 const base = {
68 id: "w1",
69 createdAt: "2026-01-01T00:00:00.000Z",
70 label: "Read",
71 };
72
73 it.each([
74 [{ outputIndicatesFailure: true }, true],
75 [{ exitCode: 2 }, true],
76 [{ output: "sh: missing-command: command not found" }, true],
77 [{ output: "Found 3 matches" }, false],
78 [{ output: `${"x".repeat(32_768)} command not found` }, false],
79 [{}, false],
80 ] as const)(
81 "preserves command failure state after removing displayed output: %j",
82 (fields, failed) => {
83 const structuredPayload = commandItem(fields);
84 const entry: WorkLogPresentationEntry = {
85 ...base,
86 tone: "tool",
87 itemType: "command_execution",
88 toolLifecycleStatus: "completed",
89 structuredPayload,
90 };
91 expect(workEntryDisplayIndicatesToolFailure(entry)).toBe(failed);
92 expect(workEntryIndicatesToolSuccess(entry)).toBe(!failed);
93 expect(JSON.stringify(toolItemForDisplay(structuredPayload))).not.toContain('"output":');
94 },
95 );
96
97 it("is true for error tone", () => {
98 expect(
99 workEntryIndicatesToolFailure({
100 ...base,
101 tone: "error",
102 detail: "nothing special",
103 }),
104 ).toBe(true);
105 });
106
107 it("is true when lifecycle says failed even if detail is empty", () => {
108 expect(
109 workEntryIndicatesToolFailure({
110 ...base,
111 tone: "tool",
112 toolLifecycleStatus: "failed",
113 }),
114 ).toBe(true);
115 });
116
117 it("detects file-not-found style tool output with completed lifecycle", () => {
118 expect(
119 workEntryIndicatesToolFailure({
120 ...base,
121 tone: "tool",
122 toolLifecycleStatus: "completed",
123 detail: "File not found: C:\\foo\\nonexistent.ts",
124 }),
125 ).toBe(true);
126 });
127
128 it("detects glob no files and PowerShell command errors", () => {
129 expect(
130 workEntryIndicatesToolFailure({
131 ...base,
132 label: "Glob",
133 tone: "tool",
134 detail: "No files found",
135 }),
136 ).toBe(true);
137 expect(
138 workEntryIndicatesToolFailure({
139 ...base,
140 label: "Bash",
141 tone: "tool",
142 detail:
143 "The term 'this_is_not_a_command' is not recognized as the name of a cmdlet, function, script file, or operable program.",
144 }),
145 ).toBe(true);
146 });
147
148 it("is false for successful completed tools", () => {
149 expect(
150 workEntryIndicatesToolFailure({
151 ...base,
152 tone: "tool",
153 toolLifecycleStatus: "completed",
154 detail: "Found 3 matching files",
155 }),
156 ).toBe(false);
157 });
158
159 it("does not treat error text in a command as rendered failure", () => {
160 const entry = {
161 ...base,
162 label: "Ran command",
163 tone: "tool",
164 toolLifecycleStatus: "completed",
165 command: 'rg "file not found"',
166 detail: "Found 3 matches",
167 } satisfies WorkLogPresentationEntry;
168
169 expect(workEntryDisplayIndicatesToolFailure(entry)).toBe(false);
170 // Older activities can store output in this field, so that path stays separate.
171 expect(workEntryIndicatesToolFailure(entry)).toBe(true);
172 expect(workEntryDisplayIndicatesToolFailure({ ...entry, detail: "File not found" })).toBe(true);
173 });
174
175 it("treats successful tool rows as success candidates", () => {
176 expect(
177 workEntryIndicatesToolSuccess({
178 ...base,
179 tone: "tool",
180 toolLifecycleStatus: "completed",
181 detail: "ok",
182 }),
183 ).toBe(true);
184 expect(
185 workEntryIndicatesToolSuccess({
186 ...base,
187 tone: "tool",
188 toolLifecycleStatus: "inProgress",
189 detail: "…",
190 }),
191 ).toBe(false);
192 expect(workEntryIndicatesToolSuccess({ ...base, tone: "thinking", detail: "…" })).toBe(false);
193 expect(
194 workEntryIndicatesToolSuccess({ ...base, tone: "tool", toolLifecycleStatus: "stopped" }),
195 ).toBe(false);
196 expect(
197 workEntryIndicatesToolSuccess({ ...base, tone: "tool", toolLifecycleStatus: "idle" }),
198 ).toBe(false);
199 });
200
201 it("does not run heuristics on non-tool info rows", () => {
202 expect(
203 workEntryIndicatesToolFailure({
204 ...base,
205 label: "Context compacted",
206 tone: "info",
207 detail: "File not found in conversation",
208 }),
209 ).toBe(false);
210 });
211});
212
213describe("summarizeToolGroup", () => {
214 const entry = (
215 id: string,
216 overrides: Partial<WorkLogPresentationEntry>,
217 ): WorkLogPresentationEntry => ({
218 id,
219 createdAt: "2026-09-01T00:00:00Z",
220 label: "Tool call",
221 tone: "tool",
222 ...overrides,
223 });
224
225 it("excludes reasoning from mixed tool counts and icons", () => {
226 const thought = entry("thought", {
227 itemType: "reasoning",
228 tone: "thinking",
229 detail: "Check the source",
230 });
231 const command = entry("command", { itemType: "command_execution", command: "vp test run" });
232 expect(summarizeToolGroup([thought, command, { ...thought, id: "thought-2" }])).toEqual({
233 summary: "Ran 1 command",
234 hasFailure: false,
235 });
236 expect(toolGroupSummaryKind([thought, command])).toBe("command");
237 expect(summarizeToolGroup([thought]).summary).toBe("Thought");
238 expect(summarizeToolGroup([thought, { ...thought, id: "thought-2" }]).summary).toBe(
239 "Thought (×2)",
240 );
241 expect(toolGroupSummaryKind([thought])).toBe("reasoning");
242 });
243
244 it("counts created threads alongside adjacent commands", () => {
245 expect(
246 summarizeToolGroup([
247 entry("command", { itemType: "command_execution", command: "vp test run" }),
248 entry("created", {
249 itemType: "thread_created",
250
251 label: "Created thread",
252 }),
253 ]).summary,
254 ).toBe("Ran 1 command and created 1 thread");
255 });
256
257 it("deduplicates named sources ahead of ordinary actions", () => {
258 const source = { key: "browser-use:chrome", name: "Chrome", kind: "integration" as const };
259 expect(
260 summarizeToolGroup([
261 entry("open", { label: "Open page", toolSource: source }),
262 entry("inspect", { label: "Inspect page", toolSource: source }),
263 entry("command", {
264 label: "Ran command",
265 itemType: "command_execution",
266 command: "git status",
267 }),
268 ]).summary,
269 ).toBe("Used Chrome integration and ran 1 command");
270 });
271
272 it.each([
273 [["Foo", "Foo"], "Used Foo integration and ran 1 command"],
274 [["Foo", "Bar", "Foo"], "Used Foo and Bar integrations and ran 1 command"],
275 ] as const)(
276 "uses the displayed source names for integration plurality: %j",
277 (names, expected) => {
278 expect(
279 summarizeToolGroup([
280 ...names.map((name, index) =>
281 entry(`integration-${index}`, {
282 toolSource: { key: `integration-${index}`, name, kind: "integration" },
283 }),
284 ),
285 entry("command", {
286 itemType: "command_execution",
287 command: "git status",
288 }),
289 ]).summary,
290 ).toBe(expected);
291 },
292 );
293
294 it("omits the integration suffix for special browser and computer sources", () => {
295 expect(
296 summarizeToolGroup([
297 entry("inspect", {
298 label: "Inspect page",
299 toolSource: { key: "browser-use", name: "Browser", kind: "browser" },
300 }),
301 entry("click", {
302 label: "Click",
303 toolSource: { key: "computer-use", name: "Computer Use", kind: "computer" },
304 }),
305 ]).summary,
306 ).toBe("Used Browser and Computer Use");
307 });
308
309 it("names Computer Use once when native-app and fallback sources share its label", () => {
310 expect(
311 summarizeToolGroup([
312 entry("inspect-app", {
313 label: "Inspect app",
314 toolSource: {
315 key: "native-app:com.t3tools.t3code",
316 name: "Computer Use",
317 kind: "computer",
318 },
319 }),
320 entry("failed-sky", {
321 label: "Restrict the breakpoint to Git actions",
322 toolSource: { key: "computer-use", name: "Computer Use", kind: "computer" },
323 }),
324 ]).summary,
325 ).toBe("Used Computer Use");
326 });
327});
328
329describe("resolveWorkEntryToolPresentation", () => {
330 it("presents and summarizes every T3 tool using the same structured identity", () => {
331 for (const tool of T3_MCP_TOOL_NAMES) {
332 const entry: WorkLogPresentationEntry = {
333 id: tool,
334 createdAt: "2026-09-19T00:00:00.000Z",
335 tone: "tool",
336 label: "Custom provider title",
337 toolData: { server: "t3-code", tool },
338 toolLifecycleStatus: "completed",
339 itemType: "dynamic_tool",
340 toolSource: { key: "t3-code", name: "T3 Code", kind: "integration" },
341 };
342 const presentation = resolveWorkEntryToolPresentation(entry);
343 expect(presentation, tool).not.toBeNull();
344 expect(presentation?.displayName, tool).not.toContain(tool);
345 const summary = summarizeToolGroup([entry]);
346 expect(summary.summary, tool).not.toMatch(/Used (?:1 tool|T3 Code integration)/);
347 expect(summary.hasFailure, tool).toBe(false);
348 const failed = { ...entry, toolLifecycleStatus: "failed" as const };
349 expect(resolveWorkEntryToolPresentation(failed)?.displayName, tool).toMatch(/^Failed to /);
350 expect(summarizeToolGroup([failed]).hasFailure, tool).toBe(true);
351 expect(summarizeToolGroup([failed]).summary, tool).toMatch(
352 /^(?:Tried to |Requested thread creation)/,
353 );
354 }
355 });
356
357 it.each([
358 ["t3_project_list", "Listing projects", "Listed projects"],
359 ["t3_project_clone", "Cloning a repository", "Cloned a repository"],
360 ["t3_project_create", "Registering a project", "Registered a project"],
361 ["t3_thread_launch", "Launching a project thread", "Launched a project thread"],
362 ["t3_queue_edit", "Editing a queued message", "Edited a queued message"],
363 ["t3_pending_request_respond", "Answering pending questions", "Answered pending questions"],
364 ["t3_thread_configure", "Setting thread model", "Set thread model"],
365 ["t3_thread_fork", "Forking this thread", "Requested a fork of this thread"],
366 ["t3_thread_send_attachments", "Sending attachments", "Sent attachments"],
367 ["run_scheduled_task_now", "Running a scheduled task", "Requested a run of a scheduled task"],
368 ["t3_workflow_stop", "Stopping a workflow", "Requested a stop for a workflow"],
369 ])("labels %s through its lifecycle", (tool, running, completed) => {
370 expect(resolveWorkEntryToolPresentation({ label: `T3-code.${tool}` })?.displayName).toBe(
371 running,
372 );
373 expect(
374 resolveWorkEntryToolPresentation({
375 label: `T3-code.${tool}`,
376 toolLifecycleStatus: "completed",
377 })?.displayName,
378 ).toBe(completed);
379 });
380
381 it("summarizes workflow stop targets separately from their parent thread", () => {
382 const entry = {
383 id: "workflow-stop",
384 createdAt: "2026-10-07T00:00:00.000Z",
385 tone: "tool",
386 label: "Custom title",
387 itemType: "dynamic_tool",
388 toolLifecycleStatus: "completed",
389 toolData: {
390 server: "t3-code",
391 tool: "t3_workflow_stop",
392 arguments: { threadId: "parent", subagentId: "workflow-1" },
393 result: {},
394 },
395 } satisfies WorkLogPresentationEntry;
396 expect(
397 summarizeToolGroup([
398 entry,
399 entry,
400 {
401 ...entry,
402 toolData: {
403 ...entry.toolData,
404 arguments: { threadId: "parent", subagentId: "workflow-2" },
405 },
406 },
407 {
408 ...entry,
409 toolData: {
410 ...entry.toolData,
411 tool: "t3_thread_interrupt",
412 arguments: { threadId: "parent" },
413 },
414 },
415 ]),
416 ).toEqual({
417 summary: "Requested stops for 2 workflows and requested interrupts for 1 thread",
418 hasFailure: false,
419 });
420 });
421
422 it("summarizes project tools from MCP arguments and results without claiming failed effects", () => {
423 const entry: WorkLogPresentationEntry = {
424 id: "clone",
425 createdAt: "2026-09-19T00:00:00.000Z",
426 tone: "tool",
427 label: "Custom title",
428 itemType: "dynamic_tool",
429 toolLifecycleStatus: "completed",
430 toolData: {
431 server: "t3-code",
432 tool: "t3_project_clone",
433 arguments: { url: "https://github.com/acme/repo" },
434 result: { cwd: "/tmp/repo" },
435 },
436 };
437 const list = { ...entry, toolData: { server: "t3-code", tool: "t3_project_list" } };
438 expect(summarizeToolGroup([list, entry])).toEqual({
439 summary: "Listed projects 1 time and cloned 1 repository",
440 hasFailure: false,
441 });
442 const failed = {
443 ...entry,
444 toolData: { toolName: "T3-code.t3_project_clone", rawOutput: { isError: true } },
445 };
446 expect(summarizeToolGroup([entry, failed])).toEqual({
447 summary: "Cloned 1 repository",
448 hasFailure: true,
449 });
450 });
451
452 it("does not summarize a foreign structured identity as T3 work", () => {
453 const entry: WorkLogPresentationEntry = {
454 id: "foreign",
455 createdAt: "2026-09-19T00:00:00.000Z",
456 tone: "tool",
457 label: "t3_project_clone",
458 toolLifecycleStatus: "completed",
459 toolData: { server: "another-server", tool: "t3_project_clone" },
460 };
461 expect(summarizeToolGroup([entry]).summary).toBe("Used 1 tool");
462 });
463
464 it("shows returned MCP errors as failures even in the live activity row", () => {
465 const entry: WorkLogPresentationEntry = {
466 id: "clone",
467 createdAt: "2026-09-19T00:00:00.000Z",
468 tone: "tool",
469 label: "T3-code.t3_project_clone",
470 toolLifecycleStatus: "inProgress",
471 itemType: "dynamic_tool",
472 toolData: { output: { isError: true } },
473 };
474 expect(resolveWorkEntryToolPresentation(entry)?.displayName).toBe(
475 "Failed to clone a repository",
476 );
477 expect(workEntryDisplayIndicatesToolFailure(entry)).toBe(true);
478 expect(workEntryIndicatesToolSuccess(entry)).toBe(false);
479 const childFailure = {
480 ...entry,
481 label: "T3-code.task_status",
482 toolLifecycleStatus: "completed" as const,
483 toolData: { output: { taskId: "child", status: "failed", summary: "command not found" } },
484 };
485 expect(workEntryDisplayIndicatesToolFailure(childFailure)).toBe(false);
486 expect(workEntryIndicatesToolSuccess(childFailure)).toBe(true);
487 });
488 it.each([
489 "mcp__t3-code__preview_click",
490 "mcp__t3_code__preview_click",
491 "mcp__t3code__preview_click",
492 "T3-code.preview_click",
493 "t3-code · preview_click completed",
494 "t3_code/preview_click",
495 "preview_click",
496 ])("recognizes browser tool names across providers: %s", (label) => {
497 expect(resolveWorkEntryToolPresentation({ label })).toEqual({
498 displayName: "Clicking in the preview browser",
499 icon: "browser",
500 });
501 });
502
503 it("labels device tools with the device icon", () => {
504 expect(
505 resolveWorkEntryToolPresentation({
506 label: "mcp__t3-code__device_open",
507 toolLifecycleStatus: "completed",
508 }),
509 ).toEqual({ displayName: "Opened a device in the Device panel", icon: "device" });
510 expect(resolveWorkEntryToolPresentation({ label: "t3-code · device_screenshot" })).toEqual({
511 displayName: "Taking a screenshot of the device",
512 icon: "device",
513 });
514 });
515
516 it("uses structured MCP identity when the provider supplies a custom title", () => {
517 expect(
518 resolveWorkEntryToolPresentation({
519 label: "Tool call complete",
520 toolTitle: "Inspect the current page",
521 toolData: { server: "t3-code", tool: "preview_snapshot", result: { title: "Example" } },
522 }),
523 ).toEqual({ displayName: "Taking a snapshot of the preview page", icon: "browser" });
524 });
525
526 it.each([
527 ["inProgress", "Clicking in the preview browser"],
528 ["completed", "Clicked in the preview browser"],
529 ["failed", "Failed to click in the preview browser"],
530 ["declined", "Declined to click in the preview browser"],
531 ["stopped", "Stopped clicking in the preview browser"],
532 ["unknown", "Clicking in the preview browser"],
533 ] as const)("describes the tool's own %s state", (toolLifecycleStatus, displayName) => {
534 expect(
535 resolveWorkEntryToolPresentation({
536 label: "T3-code.preview_click",
537 toolLifecycleStatus: toolLifecycleStatus as WorkLogToolLifecycleStatus,
538 }),
539 ).toEqual({ displayName, icon: "browser" });
540 });
541
542 it("uses the summary's state only when the provider omitted a lifecycle status", () => {
543 const entry = { label: "T3-code.preview_click" };
544 expect(resolveWorkEntryToolPresentation(entry, "inProgress")?.displayName).toBe(
545 "Clicking in the preview browser",
546 );
547 expect(resolveWorkEntryToolPresentation(entry, "completed")?.displayName).toBe(
548 "Clicked in the preview browser",
549 );
550 expect(
551 resolveWorkEntryToolPresentation({ ...entry, toolLifecycleStatus: "completed" }, "inProgress")
552 ?.displayName,
553 ).toBe("Clicked in the preview browser");
554 expect(
555 resolveWorkEntryToolPresentation({ ...entry, toolLifecycleStatus: "failed" }, "completed")
556 ?.displayName,
557 ).toBe("Failed to click in the preview browser");
558 });
559
560 it.each([
561 ["preview_type", "Typing in the preview browser", "Typed in the preview browser"],
562 [
563 "preview_set_appearance",
564 "Setting preview browser appearance",
565 "Set preview browser appearance",
566 ],
567 [
568 "preview_snapshot",
569 "Taking a snapshot of the preview page",
570 "Took a snapshot of the preview page",
571 ],
572 [
573 "preview_recording_stop",
574 "Stopping recording the preview browser",
575 "Stopped recording the preview browser",
576 ],
577 ["t3_thread_read", "Reading a T3 thread", "Read a T3 thread"],
578 ["t3_thread_send", "Sending to a T3 thread", "Sent to a T3 thread"],
579 [
580 "t3_worktree_handoff",
581 "Handing off thread to a git worktree",
582 "Handed off thread to a git worktree",
583 ],
584 ])("preserves verb forms and the rest of %s's label", (tool, running, completed) => {
585 const entry = { label: `t3-code.${tool}` };
586 expect(
587 resolveWorkEntryToolPresentation({ ...entry, toolLifecycleStatus: "inProgress" })
588 ?.displayName,
589 ).toBe(running);
590 expect(
591 resolveWorkEntryToolPresentation({ ...entry, toolLifecycleStatus: "completed" })?.displayName,
592 ).toBe(completed);
593 });
594
595 it("keeps T3 branding for non-browser tools and falls back to the original tool label", () => {
596 expect(
597 resolveWorkEntryToolPresentation({
598 label: "mcp__t3_code__task_status",
599 toolTitle: "Check the child task",
600 }),
601 ).toEqual({ displayName: "Getting delegated task status", icon: "t3-code" });
602 });
603
604 it("does not brand unknown tools or another server's matching tool name", () => {
605 for (const label of [
606 "mcp__github__preview_click",
607 "t3-code.unknown_tool",
608 "t3-code.toString",
609 "Search files",
610 ]) {
611 expect(resolveWorkEntryToolPresentation({ label })).toBeNull();
612 }
613 expect(
614 resolveWorkEntryToolPresentation({
615 label: "preview_click",
616 toolData: { server: "another-server", tool: "preview_click" },
617 }),
618 ).toBeNull();
619 });
620});
621
622describe("browser group summaries", () => {
623 const summarizeGroupLabel = (entries: ReadonlyArray<WorkLogPresentationEntry>) =>
624 summarizeToolGroup(entries).summary;
625 const browserEntry: WorkLogPresentationEntry = {
626 id: "browser",
627 createdAt: "2026-09-01T00:00:00Z",
628 label: "MCP tool call",
629 toolData: { server: "t3-code", tool: "preview_click" },
630 itemType: "dynamic_tool",
631 toolLifecycleStatus: "completed",
632 tone: "tool",
633 };
634 const commandEntry: WorkLogPresentationEntry = {
635 id: "command",
636 createdAt: "2026-09-01T00:00:00Z",
637 label: "Ran command",
638 command: "/bin/bash -lc 'vp test run'",
639 itemType: "command_execution",
640 toolLifecycleStatus: "completed",
641 tone: "tool",
642 };
643
644 it.each([1, 18])("counts %s browser calls separately from generic tools", (count) => {
645 const entries = Array.from({ length: count }, (_, index) => ({
646 ...browserEntry,
647 toolCallId: `browser-${index}`,
648 }));
649 expect(summarizeGroupLabel(entries)).toBe(
650 `Used browser ${count} ${count === 1 ? "time" : "times"}`,
651 );
652 expect(toolGroupSummaryKind(entries)).toBe("browser");
653 });
654
655 it("combines command and browser counts in a single sentence", () => {
656 const entries = [
657 ...Array.from({ length: 4 }, () => commandEntry),
658 ...Array.from({ length: 15 }, () => browserEntry),
659 ];
660 expect(summarizeGroupLabel(entries)).toBe("Ran 4 commands and used browser 15 times");
661 expect(toolGroupSummaryKind(entries)).toBe("mixed");
662 });
663
664 it("preserves first-seen action ordering alongside non-browser tools", () => {
665 expect(
666 summarizeGroupLabel([
667 browserEntry,
668 commandEntry,
669 {
670 ...browserEntry,
671 toolData: { server: "t3-code", tool: "task_status" },
672 },
673 ]),
674 ).toBe("Used browser 1 time, ran 1 command, and performed 1 other action");
675 });
676
677 it("recognizes Claude browser identity without treating script metadata as a shell command", () => {
678 expect(
679 summarizeGroupLabel([
680 {
681 ...browserEntry,
682 command: "node inspect-page.js",
683 toolData: { toolName: "mcp__t3_code__preview_evaluate" },
684 },
685 ]),
686 ).toBe("Used browser 1 time");
687 });
688
689 it("keeps foreign tools and web searches out of the browser count", () => {
690 expect(
691 summarizeGroupLabel([
692 browserEntry,
693 {
694 ...browserEntry,
695 label: "preview_click",
696 toolData: { server: "another-server", tool: "preview_click" },
697 },
698 {
699 id: "search",
700 createdAt: "2026-09-01T00:00:00Z",
701 label: "Search",
702 tone: "tool",
703 itemType: "web_search",
704 },
705 ]),
706 ).toBe("Used browser 1 time, searched the web 1 time, and performed 1 other action");
707 });
708
709 it("keeps browser screenshots in the browser count while preserving their image path", () => {
710 const entry = { ...browserEntry, viewedImagePath: "/workspace/page.png" };
711 expect(summarizeGroupLabel([entry])).toBe("Used browser 1 time");
712 expect(workEntryViewedImagePath(entry)).toBe("/workspace/page.png");
713 });
714});
715
716describe("command work-log details", () => {
717 it("extracts Claude result blocks and projected output", () => {
718 expect(
719 extractCommandOutputText({
720 result: {
721 content: [
722 { type: "text", text: "first" },
723 { type: "text", text: "second" },
724 ],
725 },
726 }),
727 ).toBe("first\nsecond");
728 expect(extractCommandOutputText({ rawOutput: { content: "projected summary" } })).toBe(
729 "projected summary",
730 );
731 });
732
733 it("only removes a detail with the matching tool-name prefix", () => {
734 expect(
735 commandDetailRepeatsCommand({
736 detail: "Bash: printf hello",
737 command: "printf hello",
738 rawCommand: null,
739 toolName: "Bash",
740 data: { toolName: "Bash", command: "printf hello" },
741 }),
742 ).toBe(true);
743 expect(
744 commandDetailRepeatsCommand({
745 detail: "warning: printf hello",
746 command: "printf hello",
747 rawCommand: null,
748 toolName: "Bash",
749 data: { toolName: "Bash", command: "printf hello" },
750 }),
751 ).toBe(false);
752 });
753
754 it("treats an ingestion-truncated echo of a long command as a repeat", () => {
755 const command = `git add -A && git commit -m "${"x".repeat(200)}"`;
756 const truncated = `Bash: ${command}`.slice(0, 177) + "...";
757 expect(
758 commandDetailRepeatsCommand({
759 detail: truncated,
760 command,
761 rawCommand: null,
762 toolName: "Bash",
763 data: { toolName: "Bash", command },
764 }),
765 ).toBe(true);
766 expect(
767 commandDetailRepeatsCommand({
768 detail: "Bash: printf hello...",
769 command: "printf goodbye",
770 rawCommand: null,
771 toolName: "Bash",
772 data: { toolName: "Bash", command: "printf goodbye" },
773 }),
774 ).toBe(false);
775 });
776
777 it("treats ACP command echoes as synthetic even without a tool kind", () => {
778 expect(
779 commandDetailRepeatsCommand({
780 detail: "pnpm test",
781 command: "pnpm test",
782 rawCommand: null,
783 toolName: undefined,
784 data: { toolCallId: "tool-1", command: "pnpm test" },
785 }),
786 ).toBe(true);
787 expect(
788 commandDetailRepeatsCommand({
789 detail: "pnpm test",
790 command: "pnpm test",
791 rawCommand: null,
792 toolName: undefined,
793 data: { command: "pnpm test" },
794 }),
795 ).toBe(false);
796 });
797});
798
799describe("workEntryViewedImagePath", () => {
800 const entry = {
801 id: "entry-1",
802 createdAt: "2026-01-01T00:00:00Z",
803 label: "Read",
804 tone: "tool",
805 } as const;
806
807 it("returns a single image path from supported read entries", () => {
808 expect(
809 workEntryViewedImagePath({ ...entry, requestKind: "file-read", detail: " assets/a.png " }),
810 ).toBe("assets/a.png");
811 expect(
812 workEntryViewedImagePath({
813 ...entry,
814 itemType: "dynamic_tool",
815 toolTitle: "Read file",
816 detail: "C:\\workspace\\a.webp",
817 }),
818 ).toBe("C:\\workspace\\a.webp");
819 expect(
820 workEntryViewedImagePath({
821 ...entry,
822 itemType: "dynamic_tool",
823 detail: 'Read: {"file_path":"truncated..."}',
824 viewedImagePath: " /workspace/reference image.webp ",
825 }),
826 ).toBe("/workspace/reference image.webp");
827 });
828
829 it("rejects non-image, multi-line, and non-read details", () => {
830 expect(
831 workEntryViewedImagePath({ ...entry, requestKind: "file-read", detail: "a.txt" }),
832 ).toBeNull();
833 expect(
834 workEntryViewedImagePath({ ...entry, requestKind: "file-read", detail: "a.png\nb.png" }),
835 ).toBeNull();
836 expect(workEntryViewedImagePath({ ...entry, detail: "a.png" })).toBeNull();
837 });
838});
839
840describe("toolGroupAction", () => {
841 it("groups legacy Claude image reads with other reads", () => {
842 expect(
843 toolGroupAction({
844 id: "legacy-read",
845 createdAt: "2026-09-01T00:00:00Z",
846 label: "Tool call",
847 tone: "tool",
848 itemType: "dynamic_tool",
849 viewedImagePath: "/workspace/reference.png",
850 }),
851 ).toBe("read");
852 });
853
854 it("groups Claude Read and Grep from toolName, not file contents", () => {
855 expect(
856 toolGroupAction({
857 id: "read",
858 createdAt: "2026-09-01T00:00:00Z",
859 label: "Read",
860 tone: "tool",
861 itemType: "dynamic_tool",
862 toolTitle: "Read",
863 toolData: { input: { file_path: "src/env.ts" } },
864 structuredPayload: {
865 type: "dynamic_tool",
866 toolName: "Read",
867 input: { file_path: "src/env.ts" },
868 } as NonNullable<WorkLogPresentationEntry["structuredPayload"]>,
869 }),
870 ).toBe("read");
871 expect(
872 toolGroupAction({
873 id: "grep",
874 createdAt: "2026-09-01T00:00:00Z",
875 label: "Grep",
876 tone: "tool",
877 itemType: "dynamic_tool",
878 toolTitle: "Grep",
879 toolData: { input: { pattern: "TODO", path: "apps/web" } },
880 structuredPayload: {
881 type: "dynamic_tool",
882 toolName: "Grep",
883 input: { pattern: "TODO", path: "apps/web" },
884 } as NonNullable<WorkLogPresentationEntry["structuredPayload"]>,
885 }),
886 ).toBe("code-search");
887 });
888});
889
890describe("resolveViewedImageAsset", () => {
891 const threadId = ThreadId.make("thread-1");
892
893 it("serves t3 attachment paths in place like any other host path", () => {
894 const path = "/Users/demo/.t3/dev/attachments/11111111-1111-4111-8111-111111111111.png";
895 expect(resolveViewedImageAsset(path, { threadId, workspaceRoot: "/workspace" })).toEqual({
896 resource: { _tag: "media-file", threadId, path },
897 alt: "11111111-1111-4111-8111-111111111111.png",
898 srcFragment: "",
899 });
900 });
901
902 it("normalizes workspace image sources", () => {
903 expect(
904 resolveViewedImageAsset("screens/logo.svg?v=2#mark", {
905 threadId,
906 workspaceRoot: "/workspace",
907 }),
908 ).toEqual({
909 resource: {
910 _tag: "media-file",
911 threadId,
912 path: "/workspace/screens/logo.svg",
913 },
914 alt: "logo.svg",
915 srcFragment: "#mark",
916 });
917 expect(resolveViewedImageAsset("https://example.com/logo.png", { threadId })).toBeNull();
918 });
919});
920
921describe("pull request tool presentation", () => {
922 it.each([
923 "mcp__t3-code__link_pull_request",
924 "mcp__t3_code__link_pull_request",
925 "T3-code · link_pull_request",
926 "t3code/link_pull_request",
927 "link_pull_request",
928 ])("recognizes the native linking tool: %s", (label) => {
929 const entry: WorkLogPresentationEntry = {
930 id: "link",
931 createdAt: "2026-09-10T00:00:00.000Z",
932 label,
933 tone: "tool",
934 toolLifecycleStatus: "completed",
935 };
936 expect(resolveWorkEntryToolPresentation(entry)).toMatchObject({
937 displayName: "Linked a pull request",
938 icon: "pull-request",
939 });
940 expect(toolGroupAction(entry)).toBe("link-pr");
941 });
942
943 it.each([
944 ["inProgress", "Linking PR #42"],
945 ["completed", "Linked PR #42"],
946 ["failed", "Failed to link PR #42"],
947 ["declined", "Declined to link PR #42"],
948 ["stopped", "Stopped linking PR #42"],
949 ] as const)("describes the target and %s status", (toolLifecycleStatus, displayName) => {
950 expect(
951 resolveWorkEntryToolPresentation({
952 label: "MCP tool call",
953 toolTitle: "Custom title",
954 toolLifecycleStatus,
955 toolData: {
956 server: "t3-code",
957 tool: "link_pull_request",
958 arguments: { url: "https://github.com/acme/web/pull/42" },
959 },
960 })?.displayName,
961 ).toBe(displayName);
962 });
963
964 it("recognizes unlink targets supplied as repository and number", () => {
965 expect(
966 resolveWorkEntryToolPresentation({
967 label: "MCP tool call",
968 toolLifecycleStatus: "completed",
969 toolData: {
970 toolName: "mcp__t3-code__unlink_pull_request",
971 rawInput: { repository: "acme/web", number: 42 },
972 },
973 }),
974 ).toMatchObject({ displayName: "Unlinked PR #42", icon: "pull-request", action: "unlink-pr" });
975 });
976
977 it("summarizes native PR work separately from ordinary tools and integration metadata", () => {
978 const link: WorkLogPresentationEntry = {
979 id: "link",
980 createdAt: "2026-09-10T00:00:00.000Z",
981 label: "T3-code · link_pull_request",
982 tone: "tool",
983 itemType: "dynamic_tool",
984 toolLifecycleStatus: "completed",
985 toolSource: { key: "t3-code", name: "T3 Code", kind: "integration" },
986 };
987 const list: WorkLogPresentationEntry = {
988 ...link,
989 label: "T3-code · list_thread_pull_requests",
990 };
991 expect(summarizeToolGroup([link, link, list]).summary).toBe(
992 "Linked 2 pull requests and checked linked pull requests",
993 );
994 expect(summarizeToolGroup([{ ...link, label: "T3-code · unlink_pull_request" }]).summary).toBe(
995 "Unlinked 1 pull request",
996 );
997 expect(toolGroupSummaryKind([link, link, list])).toBe("pull-request");
998 expect(summarizeToolGroup([list, list]).summary).toBe("Checked linked pull requests 2 times");
999 expect(
1000 resolveWorkEntryToolPresentation({ label: "mcp__another-server__link_pull_request" }),
1001 ).toBeNull();
1002 });
1003});
1004
1005describe("device group summaries", () => {
1006 const deviceEntry = (tool: string): WorkLogPresentationEntry => ({
1007 id: tool,
1008 createdAt: "2026-09-10T00:00:00.000Z",
1009 label: "MCP tool call",
1010 toolData: { server: "t3-code", tool },
1011 itemType: "dynamic_tool",
1012 toolLifecycleStatus: "completed",
1013 tone: "tool",
1014 });
1015
1016 it.each(["device_list", "device_open", "device_screenshot", "device_close"])(
1017 "recognizes %s as device controls",
1018 (tool) => {
1019 const entry = deviceEntry(tool);
1020 expect(summarizeToolGroup([entry]).summary).toBe("Used device controls 1 time");
1021 expect(toolGroupSummaryKind([entry])).toBe("device");
1022 },
1023 );
1024
1025 it("summarizes device calls alongside shell commands", () => {
1026 expect(
1027 summarizeToolGroup([
1028 {
1029 id: "command",
1030 createdAt: "2026-09-10T00:00:00.000Z",
1031 label: "Ran command",
1032 itemType: "command_execution",
1033 command: "pwd",
1034 tone: "tool",
1035 },
1036 deviceEntry("device_list"),
1037 deviceEntry("device_open"),
1038 ]).summary,
1039 ).toBe("Ran 1 command and used device controls 2 times");
1040 });
1041
1042 it("recognizes Claude tool names and preserves screenshot previews", () => {
1043 const entry = {
1044 ...deviceEntry("device_screenshot"),
1045 toolData: { toolName: "mcp__t3_code__device_screenshot" },
1046 viewedImagePath: "/workspace/device.png",
1047 };
1048 expect(summarizeToolGroup([entry]).summary).toBe("Used device controls 1 time");
1049 expect(workEntryViewedImagePath(entry)).toBe("/workspace/device.png");
1050 });
1051
1052 it("does not classify another server's tools as T3 device controls", () => {
1053 expect(
1054 summarizeToolGroup([
1055 {
1056 ...deviceEntry("device_open"),
1057 toolData: { server: "another-server", tool: "device_open" },
1058 },
1059 ]).summary,
1060 ).toBe("Used 1 tool");
1061 });
1062});
1063
1064describe("liveThoughtLine", () => {
1065 it("keeps the first sentence, including a closing quote", () => {
1066 expect(
1067 liveThoughtLine(
1068 'Found the cause: the repo has no commits, so `git worktree add` fails with "invalid reference: main." Now checking the UI.',
1069 ),
1070 ).toBe(
1071 'Found the cause: the repo has no commits, so git worktree add fails with "invalid reference: main."',
1072 );
1073 });
1074
1075 it("does not cut at dots inside file names or long dashes", () => {
1076 expect(
1077 liveThoughtLine("I read ThreadLaunchService.ts \u2014 it skips the fetch. Next step."),
1078 ).toBe("I read ThreadLaunchService.ts \u2014 it skips the fetch.");
1079 });
1080
1081 it("uses a bold-only opening line as the whole line", () => {
1082 expect(
1083 liveThoughtLine("**Narrowing dispatch files**\n\nI should check the adapter. Then more."),
1084 ).toBe("Narrowing dispatch files");
1085 expect(liveThoughtLine("**Narrowing dispatch files**\r\n\r\nI should check the adapter.")).toBe(
1086 "Narrowing dispatch files",
1087 );
1088 });
1089
1090 it("returns unpunctuated text whole and flattens markdown", () => {
1091 expect(liveThoughtLine("- Checking [the docs](https://x.dev) for **limits**")).toBe(
1092 "Checking the docs for limits",
1093 );
1094 expect(liveThoughtLine("This is *really* ~~not~~ _fine_ in snake_case_names.")).toBe(
1095 "This is really not fine in snake_case_names.",
1096 );
1097 expect(liveThoughtLine(" ")).toBe("");
1098 });
1099});