History

Render T3 MCP tools with branded timeline labels

- Pretty-print known T3 MCP tool names in work rows - Show the T3 Code logo for recognized MCP tool entries - Add logic and UI tests for the new tool presentation

Julius Marminge committed 3 months ago

Commit dc41f471 parent 1bc63844 files · +174 −195Browse files

Unified · Split

4 files
  1. apps/web/src/components/chat/MessagesTimeline.logic.test.ts +28 −180
  2. apps/web/src/components/chat/MessagesTimeline.logic.ts +57 −10
  3. apps/web/src/components/chat/MessagesTimeline.test.tsx +63 −0
  4. apps/web/src/components/chat/MessagesTimeline.tsx +26 −5
apps/web/src/components/chat/MessagesTimeline.logic.test.ts+28−180
@@ -8,10 +8,7 @@ import { · 7 unmodified lines
88 liveWorkEntryLabel,
99 normalizeCompactToolLabel,
1010 resolveAssistantMessageCopyState,
11− resolveWorkGroupScrollIndex,
12− shouldFollowWorkGroupAppend,
13− shouldPreserveAssistantLineBreaks,
14− workEntryDisplayLabel,
11+ resolveTimelineToolPresentation,
1512 } from "./MessagesTimeline.logic";
1613 import {
1714 createMessageAttachmentPreviewProjector,
@@ -354,182 +351,6 @@ describe("streaming row projection", () => { · 336 unmodified lines
354351 });
355352 });
356353
357−describe("expanded tool group scrolling", () => {
358− const entries = [{ id: "first" }, { id: "second" }];
359−
360− it("follows appended calls only at the hard end", () => {
361− const appended = [...entries, { id: "third" }];
362− expect(shouldFollowWorkGroupAppend(entries, appended, 0)).toBe(true);
363− expect(shouldFollowWorkGroupAppend(entries, appended, 0.5)).toBe(true);
364− expect(shouldFollowWorkGroupAppend(entries, appended, 1)).toBe(true);
365− expect(shouldFollowWorkGroupAppend(entries, appended, 1.01)).toBe(false);
366− expect(shouldFollowWorkGroupAppend(entries, appended, 10)).toBe(false);
367− expect(shouldFollowWorkGroupAppend(entries, appended, Infinity)).toBe(false);
368− });
369−
370− it("does not follow output updates, prepends, or replacements", () => {
371− expect(
372− shouldFollowWorkGroupAppend(
373− entries,
374− entries.map((entry) => ({ ...entry })),
375− 0,
376− ),
377− ).toBe(false);
378− expect(shouldFollowWorkGroupAppend(entries, [{ id: "older" }, ...entries], 0)).toBe(false);
379− expect(
380− shouldFollowWorkGroupAppend(
381− entries,
382− [{ id: "replacement" }, entries[1]!, { id: "third" }],
383− 0,
384− ),
385− ).toBe(false);
386− expect(shouldFollowWorkGroupAppend([], entries, 0)).toBe(false);
387− });
388−
389− it("restores the visible tool and its offset inside expanded output", () => {
390− const anchor = { entryId: "second", offset: 120 };
391− expect(resolveWorkGroupScrollIndex(entries, anchor)).toEqual({ index: 1, viewOffset: -120 });
392− expect(resolveWorkGroupScrollIndex([{ id: "older" }, ...entries], anchor)).toEqual({
393− index: 2,
394− viewOffset: -120,
395− });
396− });
397−
398− it("starts normally when the saved tool no longer exists", () => {
399− expect(resolveWorkGroupScrollIndex(entries, undefined)).toBeUndefined();
400− expect(
401− resolveWorkGroupScrollIndex(entries, { entryId: "removed", offset: 120 }),
402− ).toBeUndefined();
403− });
404−});
405−
406−describe("work entry labels", () => {
407− const entry = {
408− id: "tool-1",
409− createdAt: "2026-09-01T12:00:00Z",
410− label: "Tool call",
411− tone: "tool" as const,
412− };
413−
414− it.each([
415− ["inProgress", "Clicking in the preview browser"],
416− ["completed", "Clicked in the preview browser"],
417− ["failed", "Failed to click in the preview browser"],
418− ["declined", "Declined to click in the preview browser"],
419− ["stopped", "Stopped clicking in the preview browser"],
420− ] as const)("uses the same friendly %s label in both views", (toolLifecycleStatus, label) => {
421− const browserEntry = {
422− ...entry,
423− toolTitle: "T3-code.preview_click",
424− detail: '{"ok":true}',
425− toolLifecycleStatus,
426− };
427− expect(liveWorkEntryLabel(browserEntry, undefined, toolLifecycleStatus === "inProgress")).toBe(
428− label,
429− );
430− expect(workEntryDisplayLabel(browserEntry, undefined)).toBe(label);
431− });
432−
433− it("uses the active summary state for legacy tools without a lifecycle status", () => {
434− const browserEntry = { ...entry, toolTitle: "T3-code.preview_click" };
435− expect(liveWorkEntryLabel(browserEntry, undefined, true)).toBe(
436− "Clicking in the preview browser",
437− );
438− expect(liveWorkEntryLabel(browserEntry, undefined, false)).toBe(
439− "Clicked in the preview browser",
440− );
441− });
442−
443− it("does not describe a finished call as still running while the turn continues", () => {
444− const browserEntry = {
445− ...entry,
446− toolTitle: "T3-code.preview_click",
447− toolLifecycleStatus: "completed" as const,
448− };
449− expect(liveWorkEntryLabel(browserEntry, undefined, true)).toBe(
450− "Clicked in the preview browser",
451− );
452− });
453−
454− it("keeps custom titles and output for unrecognized tools", () => {
455− const unknownEntry = { ...entry, toolTitle: "mcp__github__search_issues" };
456− expect(liveWorkEntryLabel(unknownEntry, undefined, true)).toBe("Mcp__github__search_issues");
457− expect(workEntryDisplayLabel({ ...unknownEntry, detail: "Found 3 issues" }, undefined)).toBe(
458− "Found 3 issues",
459− );
460− });
461−
462− it("keeps command summaries compact without replacing the full command in expanded rows", () => {
463− const commandEntry = { ...entry, command: "vp test run", detail: "All tests passed" };
464− expect(liveWorkEntryLabel(commandEntry, undefined, true)).toBe("Running vp");
465− expect(liveWorkEntryLabel(commandEntry, undefined, false)).toBe("Ran vp");
466− expect(workEntryDisplayLabel(commandEntry, undefined)).toBe("vp test run");
467− });
468−
469− it("summarizes the program inside a shell wrapper while preserving the expanded command", () => {
470− const command = "/bin/zsh -lc 'vp test run apps/web/src/session-logic.test.ts'";
471− const commandEntry = { ...entry, command };
472− expect(liveWorkEntryLabel(commandEntry, undefined, true)).toBe("Running vp");
473− expect(liveWorkEntryLabel(commandEntry, undefined, false)).toBe("Ran vp");
474− expect(workEntryDisplayLabel(commandEntry, undefined)).toBe(command);
475− });
476−
477− it.each([
478− ["inProgress", "Running vp"],
479− ["completed", "Ran vp"],
480− ["failed", "Failed vp"],
481− ["declined", "Declined vp"],
482− ["stopped", "Stopped vp"],
483− ] as const)(
484− "uses the command's %s outcome even while the turn continues",
485− (toolLifecycleStatus, label) => {
486− const commandEntry = {
487− ...entry,
488− command: "/bin/bash -lc 'vp test run'",
489− toolLifecycleStatus,
490− };
491− expect(liveWorkEntryLabel(commandEntry, undefined, true)).toBe(label);
492− expect(liveWorkEntryLabel(commandEntry, undefined, false)).toBe(label);
493− },
494− );
495−
496− it("gives a completed browser group its own count and summary icon category", () => {
497− const rows = deriveMessagesTimelineRows({
498− timelineEntries: [
499− {
500− id: "browser-entry",
501− kind: "work",
502− createdAt: entry.createdAt,
503− entry: {
504− ...entry,
505− itemType: "mcp_tool_call",
506− toolLifecycleStatus: "completed",
507− toolData: { server: "t3-code", tool: "preview_click" },
508− },
509− },
510− ],
511− isWorking: false,
512− activeTurnStartedAt: null,
513− turnDiffSummaryByAssistantMessageId: new Map(),
514− revertTurnCountByUserMessageId: new Map(),
515− });
516− expect(rows).toMatchObject([
517− { kind: "work-toggle", summary: "Used browser 1 time", summaryKind: "browser" },
518− ]);
519− });
520−});
521−
522−describe("shouldPreserveAssistantLineBreaks", () => {
523− it("preserves Claude insight formatting without changing regular markdown", () => {
524− expect(
525− shouldPreserveAssistantLineBreaks(
526− "★ Insight ─────────────────\\nFirst observation\\nSecond observation\\n─────────────────",
527− ),
528− ).toBe(true);
529− expect(shouldPreserveAssistantLineBreaks("A normal\\nmarkdown paragraph")).toBe(false);
530− });
531−});
532−
533354 describe("computeMessageDurationStart", () => {
534355 it("returns message createdAt when there is no preceding user message", () => {
535356 const result = computeMessageDurationStart([
@@ -729,6 +550,33 @@ describe("normalizeCompactToolLabel", () => { · 193 unmodified lines
729550 });
730551 });
731552
553+describe("resolveTimelineToolPresentation", () => {
554+ it("pretty prints Claude and Cursor T3 MCP tool names", () => {
555+ expect(resolveTimelineToolPresentation("mcp__t3-code__t3_thread_read")).toEqual({
556+ displayName: "Read a T3 thread",
557+ logo: "t3-code",
558+ });
559+ });
560+
561+ it("pretty prints Codex T3 MCP tool names", () => {
562+ expect(resolveTimelineToolPresentation("t3-code.create_threads")).toEqual({
563+ displayName: "Create T3 threads",
564+ logo: "t3-code",
565+ });
566+ });
567+
568+ it("pretty prints bare T3 MCP toolkit names", () => {
569+ expect(resolveTimelineToolPresentation("list_scheduled_tasks")).toEqual({
570+ displayName: "List scheduled tasks",
571+ logo: "t3-code",
572+ });
573+ });
574+
575+ it("keeps unknown MCP tools on the generic renderer path", () => {
576+ expect(resolveTimelineToolPresentation("mcp__github__search_issues")).toBeNull();
577+ });
578+});
579+
732580 describe("resolveAssistantMessageCopyState", () => {
733581 it("returns enabled copy state for completed assistant messages", () => {
734582 expect(
apps/web/src/components/chat/MessagesTimeline.logic.ts+57−10
@@ -279,17 +279,64 @@ export function normalizeCompactToolLabel(value: string): string { · 278 unmodified lines
279279 return value.replace(/\s+(?:complete|completed)\s*$/i, "").trim();
280280 }
281281
282−function expandedWorkGroupRow(
283− groupId: string,
284− createdAt: string,
285− groupedEntries: WorkLogEntry[],
286−): Extract<MessagesTimelineRow, { kind: "work" }> {
282+export interface TimelineToolPresentation {
283+ readonly displayName: string;
284+ readonly logo: "t3-code";
285+}
286+
287+const T3_MCP_SERVER_ALIASES = new Set(["t3-code", "t3_code", "t3code"]);
288+
289+const T3_MCP_TOOL_DISPLAY_NAMES: Record<string, string> = {
290+ orchestrator_capabilities: "Get orchestration capabilities",
291+ delegate_task: "Delegate a child task",
292+ task_status: "Get delegated task status",
293+ task_cancel: "Cancel delegated task",
294+ schedule_task: "Schedule a recurring task",
295+ list_scheduled_tasks: "List scheduled tasks",
296+ update_scheduled_task: "Update a scheduled task",
297+ delete_scheduled_task: "Delete a scheduled task",
298+ create_threads: "Create T3 threads",
299+ t3_thread_start: "Start a T3 thread",
300+ t3_thread_list: "List T3 threads",
301+ t3_thread_read: "Read a T3 thread",
302+ t3_thread_send: "Send to a T3 thread",
303+ t3_thread_wait: "Wait for a T3 thread",
304+ t3_thread_interrupt: "Interrupt a T3 thread",
305+};
306+
307+function resolveT3McpToolName(value: string): string | null {
308+ const label = normalizeCompactToolLabel(value);
309+ const mcpMatch = /^mcp__(?<server>.+?)__(?<tool>.+)$/.exec(label);
310+ if (mcpMatch?.groups) {
311+ const { server, tool } = mcpMatch.groups;
312+ return server !== undefined && tool !== undefined && T3_MCP_SERVER_ALIASES.has(server)
313+ ? tool
314+ : null;
315+ }
316+
317+ const namespaceMatch = /^(?<server>t3-code|t3_code|t3code)[.:/](?<tool>.+)$/.exec(label);
318+ if (namespaceMatch?.groups) {
319+ return namespaceMatch.groups.tool ?? null;
320+ }
321+
322+ return Object.hasOwn(T3_MCP_TOOL_DISPLAY_NAMES, label) ? label : null;
323+}
324+
325+export function resolveTimelineToolPresentation(
326+ toolName: string | null | undefined,
327+): TimelineToolPresentation | null {
328+ const resolvedToolName =
329+ toolName === undefined || toolName === null ? null : resolveT3McpToolName(toolName);
330+ if (resolvedToolName === null) {
331+ return null;
332+ }
333+ const displayName = T3_MCP_TOOL_DISPLAY_NAMES[resolvedToolName];
334+ if (displayName === undefined) {
335+ return null;
336+ }
287337 return {
288− kind: "work",
289− id: `${groupId}:details`,
290− createdAt,
291− groupedEntries,
292− isExpandedToolGroup: true,
338+ displayName,
339+ logo: "t3-code",
293340 };
294341 }
295342
apps/web/src/components/chat/MessagesTimeline.test.tsx+63−0
@@ -1034,6 +1034,69 @@ describe("MessagesTimeline", () => { · 1033 unmodified lines
10341034 expect(markup).toContain("Inherited");
10351035 });
10361036
1037+ it("renders T3 MCP dynamic tools with the product logo and pretty name", async () => {
1038+ const { MessagesTimeline } = await import("./MessagesTimeline");
1039+ const item = {
1040+ id: "tool-t3-thread-read",
1041+ threadId: "thread-source",
1042+ runId: null,
1043+ nodeId: null,
1044+ providerThreadId: null,
1045+ providerTurnId: null,
1046+ nativeItemRef: null,
1047+ parentItemId: null,
1048+ ordinal: 0,
1049+ status: "completed",
1050+ title: null,
1051+ startedAt: null,
1052+ completedAt: null,
1053+ updatedAt: {},
1054+ type: "dynamic_tool",
1055+ toolName: "mcp__t3-code__t3_thread_read",
1056+ input: { threadId: "thread-child" },
1057+ output: { messages: [] },
1058+ } as const;
1059+ const projectedItem = {
1060+ position: 0,
1061+ visibility: "local",
1062+ sourceThreadId: "thread-source",
1063+ sourceItemId: item.id,
1064+ item,
1065+ } as const;
1066+ const markup = renderToStaticMarkup(
1067+ <MessagesTimeline
1068+ {...buildProps()}
1069+ timelineEntries={
1070+ [
1071+ {
1072+ id: item.id,
1073+ kind: "work",
1074+ createdAt: MESSAGE_CREATED_AT,
1075+ entry: {
1076+ id: item.id,
1077+ createdAt: MESSAGE_CREATED_AT,
1078+ runId: null,
1079+ label: item.toolName,
1080+ tone: "tool",
1081+ itemType: item.type,
1082+ toolTitle: item.toolName,
1083+ toolLifecycleStatus: "completed",
1084+ toolData: { input: item.input, output: item.output },
1085+ structuredPayload: item,
1086+ projectedItem,
1087+ },
1088+ },
1089+ ] as never
1090+ }
1091+ />,
1092+ );
1093+
1094+ expect(markup).toContain('data-tool-logo="t3-code"');
1095+ expect(markup).toContain('src="/apple-touch-icon.png"');
1096+ expect(markup).toContain("Read a T3 thread");
1097+ expect(markup).not.toContain("mcp__t3-code__t3_thread_read");
1098+ });
1099+
10371100 it("formats changed file paths from the workspace root", async () => {
10381101 const { MessagesTimeline } = await import("./MessagesTimeline");
10391102 const markup = renderToStaticMarkup(
apps/web/src/components/chat/MessagesTimeline.tsx+26−5
@@ -73,6 +73,7 @@ import { · 72 unmodified lines
7373 computeStableMessagesTimelineRows,
7474 deriveMessagesTimelineRows,
7575 normalizeCompactToolLabel,
76+ resolveTimelineToolPresentation,
7677 resolveAssistantMessageCopyState,
7778 resolveTimelineIsAtEnd,
7879 resolveTimelineMinimapHasPersistentGutter,
@@ -2266,6 +2267,20 @@ function WorkEntryIconSvg({ name, className }: { name: WorkEntryIconName; classN · 2187 unmodified lines
22662267 }
22672268 }
22682269
2270+function T3CodeToolLogo({ className }: { className?: string }) {
2271+ return (
2272+ <span
2273+ className={cn(
2274+ "flex size-4 shrink-0 items-center justify-center overflow-hidden rounded-[4px] bg-background ring-1 ring-border/65",
2275+ className,
2276+ )}
2277+ title="T3 Code MCP tool"
2278+ >
2279+ <img alt="" aria-hidden="true" className="size-4 object-cover" src="/apple-touch-icon.png" />
2280+ </span>
2281+ );
2282+}
2283+
22692284 function workToneIcon(tone: TimelineWorkEntry["tone"]): {
22702285 iconName: WorkEntryIconName;
22712286 className: string;
@@ -2515,7 +2530,8 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: { · 243 unmodified lines
25152530 const iconConfig = workToneIcon(workEntry.tone);
25162531 const showWarningIndicator = false;
25172532 const entryIconName = showWarningIndicator ? "x" : workEntryIconName(workEntry);
2518− const heading = toolWorkEntryHeading(workEntry);
2533+ const toolPresentation = resolveTimelineToolPresentation(workEntry.toolTitle ?? workEntry.label);
2534+ const heading = toolPresentation?.displayName ?? toolWorkEntryHeading(workEntry);
25192535 const rawPreview = workEntryPreview(workEntry, workspaceRoot);
25202536 const preview =
25212537 rawPreview &&
@@ -2572,16 +2588,21 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: { · 50 unmodified lines
25722588 canExpand &&
25732589 "cursor-pointer hover:bg-accent/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/70",
25742590 )}
2591+ data-tool-logo={toolPresentation?.logo}
25752592 data-v2-item-type={workEntry.projectedItem?.item.type}
25762593 data-v2-item-visibility={workEntry.projectedItem?.visibility}
25772594 {...rowToggleProps}
25782595 >
25792596 <div className="flex select-none items-center gap-1.5 transition-[opacity,translate] duration-200">
25802597 <span className={iconWrapperClass}>
2581− <WorkEntryIconSvg
2582− name={entryIconName}
2583− className="block size-3.5 shrink-0 stroke-[1.8] opacity-80"
2584− />
2598+ {toolPresentation?.logo === "t3-code" ? (
2599+ <T3CodeToolLogo />
2600+ ) : (
2601+ <WorkEntryIconSvg
2602+ name={entryIconName}
2603+ className="block size-3.5 shrink-0 stroke-[1.8] opacity-80"
2604+ />
2605+ )}
25852606 </span>
25862607 <div className="flex min-w-0 flex-1 items-center gap-1.5">
25872608 <div className="min-w-0 flex-1 overflow-hidden">

Keyboard shortcuts

General

Show keyboard shortcuts
?
Focus the current search
/

Issues and pull requests

Open filters
f

Issues, pull requests and Inbox

Select next item
j↓
Select previous item
k↑
Open selected item
Enter
Dismiss filters or clear selection
Esc

Repository code

Go to file
t