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
8 liveWorkEntryLabel,8 liveWorkEntryLabel,
9 normalizeCompactToolLabel,9 normalizeCompactToolLabel,
10 resolveAssistantMessageCopyState,10 resolveAssistantMessageCopyState,
11 resolveWorkGroupScrollIndex,11 resolveTimelineToolPresentation,
12 shouldFollowWorkGroupAppend,
13 shouldPreserveAssistantLineBreaks,
14 workEntryDisplayLabel,
15} from "./MessagesTimeline.logic";12} from "./MessagesTimeline.logic";
16import {13import {
17 createMessageAttachmentPreviewProjector,14 createMessageAttachmentPreviewProjector,
@@ -354,182 +351,6 @@ describe("streaming row projection", () => { · 336 unmodified lines
354 });351 });
355});352});
356 353
357describe("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
406describe("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
522describe("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
533describe("computeMessageDurationStart", () => {354describe("computeMessageDurationStart", () => {
534 it("returns message createdAt when there is no preceding user message", () => {355 it("returns message createdAt when there is no preceding user message", () => {
535 const result = computeMessageDurationStart([356 const result = computeMessageDurationStart([
@@ -729,6 +550,33 @@ describe("normalizeCompactToolLabel", () => { · 193 unmodified lines
729 });550 });
730});551});
731 552
553describe("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
732describe("resolveAssistantMessageCopyState", () => {580describe("resolveAssistantMessageCopyState", () => {
733 it("returns enabled copy state for completed assistant messages", () => {581 it("returns enabled copy state for completed assistant messages", () => {
734 expect(582 expect(
apps/web/src/components/chat/MessagesTimeline.logic.ts+57−10
@@ -279,17 +279,64 @@ export function normalizeCompactToolLabel(value: string): string { · 278 unmodified lines
279 return value.replace(/\s+(?:complete|completed)\s*$/i, "").trim();279 return value.replace(/\s+(?:complete|completed)\s*$/i, "").trim();
280}280}
281 281
282function expandedWorkGroupRow(282export interface TimelineToolPresentation {
283 groupId: string,283 readonly displayName: string;
284 createdAt: string,284 readonly logo: "t3-code";
285 groupedEntries: WorkLogEntry[],285}
286): Extract<MessagesTimelineRow, { kind: "work" }> {286
287const T3_MCP_SERVER_ALIASES = new Set(["t3-code", "t3_code", "t3code"]);
288
289const 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
307function 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
325export 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 }
287 return {337 return {
288 kind: "work",338 displayName,
289 id: `${groupId}:details`,339 logo: "t3-code",
290 createdAt,
291 groupedEntries,
292 isExpandedToolGroup: true,
293 };340 };
294}341}
295 342
apps/web/src/components/chat/MessagesTimeline.test.tsx+63−0
@@ -1034,6 +1034,69 @@ describe("MessagesTimeline", () => { · 1033 unmodified lines
1034 expect(markup).toContain("Inherited");1034 expect(markup).toContain("Inherited");
1035 });1035 });
1036 1036
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
1037 it("formats changed file paths from the workspace root", async () => {1100 it("formats changed file paths from the workspace root", async () => {
1038 const { MessagesTimeline } = await import("./MessagesTimeline");1101 const { MessagesTimeline } = await import("./MessagesTimeline");
1039 const markup = renderToStaticMarkup(1102 const markup = renderToStaticMarkup(
apps/web/src/components/chat/MessagesTimeline.tsx+26−5
@@ -73,6 +73,7 @@ import { · 72 unmodified lines
73 computeStableMessagesTimelineRows,73 computeStableMessagesTimelineRows,
74 deriveMessagesTimelineRows,74 deriveMessagesTimelineRows,
75 normalizeCompactToolLabel,75 normalizeCompactToolLabel,
76 resolveTimelineToolPresentation,
76 resolveAssistantMessageCopyState,77 resolveAssistantMessageCopyState,
77 resolveTimelineIsAtEnd,78 resolveTimelineIsAtEnd,
78 resolveTimelineMinimapHasPersistentGutter,79 resolveTimelineMinimapHasPersistentGutter,
@@ -2266,6 +2267,20 @@ function WorkEntryIconSvg({ name, className }: { name: WorkEntryIconName; classN · 2187 unmodified lines
2266 }2267 }
2267}2268}
2268 2269
2270function 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
2269function workToneIcon(tone: TimelineWorkEntry["tone"]): {2284function workToneIcon(tone: TimelineWorkEntry["tone"]): {
2270 iconName: WorkEntryIconName;2285 iconName: WorkEntryIconName;
2271 className: string;2286 className: string;
@@ -2515,7 +2530,8 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: { · 243 unmodified lines
2515 const iconConfig = workToneIcon(workEntry.tone);2530 const iconConfig = workToneIcon(workEntry.tone);
2516 const showWarningIndicator = false;2531 const showWarningIndicator = false;
2517 const entryIconName = showWarningIndicator ? "x" : workEntryIconName(workEntry);2532 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);
2519 const rawPreview = workEntryPreview(workEntry, workspaceRoot);2535 const rawPreview = workEntryPreview(workEntry, workspaceRoot);
2520 const preview =2536 const preview =
2521 rawPreview &&2537 rawPreview &&
@@ -2572,16 +2588,21 @@ const PlainWorkEntryRow = memo(function PlainWorkEntryRow(props: { · 50 unmodified lines
2572 canExpand &&2588 canExpand &&
2573 "cursor-pointer hover:bg-accent/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/70",2589 "cursor-pointer hover:bg-accent/20 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring/70",
2574 )}2590 )}
2591 data-tool-logo={toolPresentation?.logo}
2575 data-v2-item-type={workEntry.projectedItem?.item.type}2592 data-v2-item-type={workEntry.projectedItem?.item.type}
2576 data-v2-item-visibility={workEntry.projectedItem?.visibility}2593 data-v2-item-visibility={workEntry.projectedItem?.visibility}
2577 {...rowToggleProps}2594 {...rowToggleProps}
2578 >2595 >
2579 <div className="flex select-none items-center gap-1.5 transition-[opacity,translate] duration-200">2596 <div className="flex select-none items-center gap-1.5 transition-[opacity,translate] duration-200">
2580 <span className={iconWrapperClass}>2597 <span className={iconWrapperClass}>
2581 <WorkEntryIconSvg2598 {toolPresentation?.logo === "t3-code" ? (
2582 name={entryIconName}2599 <T3CodeToolLogo />
2583 className="block size-3.5 shrink-0 stroke-[1.8] opacity-80"2600 ) : (
2584 />2601 <WorkEntryIconSvg
2602 name={entryIconName}
2603 className="block size-3.5 shrink-0 stroke-[1.8] opacity-80"
2604 />
2605 )}
2585 </span>2606 </span>
2586 <div className="flex min-w-0 flex-1 items-center gap-1.5">2607 <div className="flex min-w-0 flex-1 items-center gap-1.5">
2587 <div className="min-w-0 flex-1 overflow-hidden">2608 <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