diff --git a/packages/app/src/utils/tool-call-icon.ts b/packages/app/src/utils/tool-call-icon.ts index be524faad..db4ddc953 100644 --- a/packages/app/src/utils/tool-call-icon.ts +++ b/packages/app/src/utils/tool-call-icon.ts @@ -11,6 +11,8 @@ import { Wrench, } from "lucide-react-native"; import type { ToolCallDetail, ToolCallIconName } from "@server/server/agent/agent-sdk-types"; +import { isPaseoToolName } from "@server/server/agent/tool-name-normalization"; +import { PaseoLogo } from "@/components/icons/paseo-logo"; export type ToolCallIconComponent = ComponentType<{ size?: number; color?: string }>; @@ -57,6 +59,9 @@ export function resolveToolCallIcon( if (lowerName === "speak") { return MicVocal; } + if (isPaseoToolName(lowerName)) { + return PaseoLogo; + } if (lowerName === "task") { return Bot; } diff --git a/packages/server/src/server/agent/tool-name-normalization.test.ts b/packages/server/src/server/agent/tool-name-normalization.test.ts new file mode 100644 index 000000000..4c20f2001 --- /dev/null +++ b/packages/server/src/server/agent/tool-name-normalization.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from "vitest"; + +import { getPaseoToolLeafName, isPaseoToolName } from "./tool-name-normalization.js"; + +describe("isPaseoToolName", () => { + it("detects Claude Code format", () => { + expect(isPaseoToolName("mcp__paseo__create_agent")).toBe(true); + expect(isPaseoToolName("mcp__paseo__list_agents")).toBe(true); + }); + + it("detects paseo_voice variant", () => { + expect(isPaseoToolName("mcp__paseo_voice__create_agent")).toBe(true); + expect(isPaseoToolName("paseo_voice.create_agent")).toBe(true); + }); + + it("excludes speak tools", () => { + expect(isPaseoToolName("mcp__paseo_voice__speak")).toBe(false); + expect(isPaseoToolName("mcp__paseo__speak")).toBe(false); + expect(isPaseoToolName("paseo.speak")).toBe(false); + }); + + it("detects Codex dot format", () => { + expect(isPaseoToolName("paseo.create_agent")).toBe(true); + }); + + it("rejects non-paseo tools", () => { + expect(isPaseoToolName("Bash")).toBe(false); + expect(isPaseoToolName("Read")).toBe(false); + expect(isPaseoToolName("mcp__other_server__some_tool")).toBe(false); + }); +}); + +describe("getPaseoToolLeafName", () => { + it("extracts leaf from Claude Code format", () => { + expect(getPaseoToolLeafName("mcp__paseo__create_agent")).toBe("create_agent"); + }); + + it("extracts leaf from Codex format", () => { + expect(getPaseoToolLeafName("paseo.create_agent")).toBe("create_agent"); + expect(getPaseoToolLeafName("paseo.list_agents")).toBe("list_agents"); + }); + + it("returns null for non-paseo tools", () => { + expect(getPaseoToolLeafName("Bash")).toBeNull(); + }); +}); diff --git a/packages/server/src/server/agent/tool-name-normalization.ts b/packages/server/src/server/agent/tool-name-normalization.ts index d6784e7b1..a920305d1 100644 --- a/packages/server/src/server/agent/tool-name-normalization.ts +++ b/packages/server/src/server/agent/tool-name-normalization.ts @@ -39,6 +39,49 @@ export function isLikelyNamespacedToolName(name: string): boolean { return false; } +export function isPaseoToolName(name: string): boolean { + const normalized = normalizeToolName(name); + if (isSpeakToolName(normalized)) { + return false; + } + if (normalized.includes("__")) { + const segments = normalized.split("__").filter((s) => s.length > 0); + return ( + segments.length >= 3 && + segments[0] === "mcp" && + (segments[1] === "paseo" || segments[1]!.startsWith("paseo_")) + ); + } + if (normalized.includes(".")) { + const firstSegment = normalized.split(".")[0]!; + return firstSegment === "paseo" || firstSegment.startsWith("paseo_"); + } + return false; +} + +export function getPaseoToolLeafName(name: string): string | null { + const normalized = normalizeToolName(name); + if (normalized.includes("__")) { + const segments = normalized.split("__").filter((s) => s.length > 0); + if ( + segments.length >= 3 && + segments[0] === "mcp" && + (segments[1] === "paseo" || segments[1]!.startsWith("paseo_")) + ) { + return segments.slice(2).join("__"); + } + return null; + } + if (normalized.includes(".")) { + const firstSegment = normalized.split(".")[0]!; + if (firstSegment === "paseo" || firstSegment.startsWith("paseo_")) { + return normalized.split(".").slice(1).join("."); + } + return null; + } + return null; +} + export function isLikelyExternalToolName(name: string): boolean { const normalized = normalizeToolName(name); if (!normalized) { diff --git a/packages/server/src/shared/tool-call-display.test.ts b/packages/server/src/shared/tool-call-display.test.ts index d0e80c6b2..259928848 100644 --- a/packages/server/src/shared/tool-call-display.test.ts +++ b/packages/server/src/shared/tool-call-display.test.ts @@ -141,6 +141,46 @@ describe("shared tool-call display mapping", () => { }); }); + it("humanizes Paseo MCP tool names (Claude Code format)", () => { + const display = buildToolCallDisplayModel({ + name: "mcp__paseo__create_agent", + status: "running", + error: null, + detail: { type: "unknown", input: null, output: null }, + }); + expect(display.displayName).toBe("Create Agent"); + }); + + it("humanizes Paseo MCP tool names (Codex format)", () => { + const display = buildToolCallDisplayModel({ + name: "paseo.create_agent", + status: "running", + error: null, + detail: { type: "unknown", input: null, output: null }, + }); + expect(display.displayName).toBe("Create Agent"); + }); + + it("humanizes list_agents Paseo tool", () => { + const display = buildToolCallDisplayModel({ + name: "mcp__paseo__list_agents", + status: "running", + error: null, + detail: { type: "unknown", input: null, output: null }, + }); + expect(display.displayName).toBe("List Agents"); + }); + + it("does not override speak tool display name", () => { + const display = buildToolCallDisplayModel({ + name: "speak", + status: "running", + error: null, + detail: { type: "unknown", input: null, output: null }, + }); + expect(display.displayName).toBe("Speak"); + }); + it("labels plan detail rows as Plan", () => { const display = buildToolCallDisplayModel({ name: "plan", diff --git a/packages/server/src/shared/tool-call-display.ts b/packages/server/src/shared/tool-call-display.ts index 899c54d0b..13506eed0 100644 --- a/packages/server/src/shared/tool-call-display.ts +++ b/packages/server/src/shared/tool-call-display.ts @@ -1,4 +1,8 @@ import type { ToolCallTimelineItem } from "../server/agent/agent-sdk-types.js"; +import { + getPaseoToolLeafName, + isPaseoToolName, +} from "../server/agent/tool-name-normalization.js"; import { stripCwdPrefix } from "./path-utils.js"; export type ToolCallDisplayInput = Pick< @@ -32,6 +36,12 @@ function humanizeToolName(name: string): string { if (!trimmed) { return name; } + if (isPaseoToolName(trimmed)) { + const leaf = getPaseoToolLeafName(trimmed); + if (leaf) { + return humanizeToolName(leaf); + } + } if (/[:./]/.test(trimmed) || trimmed.includes("__")) { return trimmed; }