diff --git a/packages/core/runtimes/display.test.ts b/packages/core/runtimes/display.test.ts index 0492060824..b838270c9e 100644 --- a/packages/core/runtimes/display.test.ts +++ b/packages/core/runtimes/display.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { runtimeDisplayName } from "./display"; +import { runtimeDisplayLabel, runtimeDisplayName } from "./display"; describe("runtimeDisplayName", () => { it("prefers a custom name when set", () => { @@ -27,3 +27,70 @@ describe("runtimeDisplayName", () => { expect(runtimeDisplayName({ name: "Claude (host)" })).toBe("Claude (host)"); }); }); + +describe("runtimeDisplayLabel", () => { + it("re-attaches the provider when a custom alias hides it", () => { + expect( + runtimeDisplayLabel({ + name: "Codex (EvaM2.local)", + custom_name: "evam2", + provider: "codex", + }), + ).toBe("evam2 (Codex)"); + }); + + it("returns the daemon name unchanged when no alias is set", () => { + expect( + runtimeDisplayLabel({ + name: "Codex (EvaM2.local)", + custom_name: "", + provider: "codex", + }), + ).toBe("Codex (EvaM2.local)"); + expect( + runtimeDisplayLabel({ + name: "Codex (EvaM2.local)", + custom_name: null, + provider: "codex", + }), + ).toBe("Codex (EvaM2.local)"); + }); + + it("omits the provider suffix when the provider is empty", () => { + expect( + runtimeDisplayLabel({ name: "host", custom_name: "evam2", provider: "" }), + ).toBe("evam2"); + }); + + it("uses the daemon's provider display name for overridden slugs", () => { + // Trae's slug is `traecli`; the label must read "Trae", matching the + // no-alias daemon name, not the title-cased slug "Traecli". + expect( + runtimeDisplayLabel({ + name: "Trae (host)", + custom_name: "box", + provider: "traecli", + }), + ).toBe("box (Trae)"); + }); + + it("first-letter-capitalizes non-overridden slugs, matching the daemon", () => { + // The daemon only overrides `traecli`; every other provider is a + // first-letter capitalization on both the alias and no-alias paths, so the + // label must match to avoid drift (e.g. no-alias name is "Openclaw (host)"). + expect( + runtimeDisplayLabel({ + name: "Openclaw (host)", + custom_name: "box", + provider: "openclaw", + }), + ).toBe("box (Openclaw)"); + expect( + runtimeDisplayLabel({ + name: "Codex (host)", + custom_name: "box", + provider: "codex", + }), + ).toBe("box (Codex)"); + }); +}); diff --git a/packages/core/runtimes/display.ts b/packages/core/runtimes/display.ts index 903a125249..d2da2dc38f 100644 --- a/packages/core/runtimes/display.ts +++ b/packages/core/runtimes/display.ts @@ -11,3 +11,46 @@ export function runtimeDisplayName( const custom = runtime.custom_name?.trim(); return custom ? custom : runtime.name; } + +/** + * A runtime label that always surfaces the provider family, even when a custom + * alias is set (#5260). The daemon bakes the provider into `name` + * ("Codex (host)"), but a user alias replaces that whole string via + * runtimeDisplayName and hides which CLI actually backs the runtime. When an + * alias is present we re-attach the provider in parentheses; without one `name` + * already carries it, so we return it unchanged to avoid a duplicated provider + * ("Codex (host) (codex)"). + */ +export function runtimeDisplayLabel( + runtime: Pick, +): string { + const display = runtimeDisplayName(runtime); + const hasCustom = !!runtime.custom_name?.trim(); + const provider = runtime.provider?.trim(); + if (!hasCustom || !provider) return display; + return `${display} (${providerDisplayName(provider)})`; +} + +/** + * Provider slugs whose human display name isn't just a capitalization of the + * slug. This MUST mirror the daemon's `runtimeDisplayNameOverrides` + * (server/internal/daemon/daemon.go): the daemon bakes that display name into + * `name` for the no-alias case ("Trae (host)"), so the aliased label has to use + * the exact same names or the two paths drift apart (#5260). Only `traecli` + * needs an override today — every other provider is a first-letter + * capitalization of its slug on both sides. Keep in sync with the daemon map. + */ +const PROVIDER_DISPLAY_NAMES: Record = { + traecli: "Trae", +}; + +/** + * Map a provider slug to its display name, matching the daemon's + * providerDisplayName: an explicit override when listed, otherwise the slug + * with its first letter capitalized. + */ +export function providerDisplayName(provider: string): string { + const known = PROVIDER_DISPLAY_NAMES[provider]; + if (known) return known; + return provider.charAt(0).toUpperCase() + provider.slice(1); +} diff --git a/packages/views/agents/components/agent-detail-page.tsx b/packages/views/agents/components/agent-detail-page.tsx index 8e7bb6ffb2..965e7bc8db 100644 --- a/packages/views/agents/components/agent-detail-page.tsx +++ b/packages/views/agents/components/agent-detail-page.tsx @@ -34,7 +34,7 @@ import { memberListOptions, workspaceKeys, } from "@multica/core/workspace/queries"; -import { runtimeListOptions } from "@multica/core/runtimes"; +import { runtimeDisplayLabel, runtimeListOptions } from "@multica/core/runtimes"; import { useAgentPermissions } from "@multica/core/permissions"; import { Button } from "@multica/ui/components/ui/button"; import { CapabilityBanner } from "@multica/ui/components/common/capability-banner"; @@ -449,7 +449,9 @@ function DetailHeader({ diff --git a/packages/views/agents/components/agent-profile-card.tsx b/packages/views/agents/components/agent-profile-card.tsx index 2d695046b1..57e48caac7 100644 --- a/packages/views/agents/components/agent-profile-card.tsx +++ b/packages/views/agents/components/agent-profile-card.tsx @@ -6,6 +6,7 @@ import { useAgentPresenceDetail } from "@multica/core/agents"; import { useWorkspaceId } from "@multica/core/hooks"; import { deriveRuntimeHealth, + runtimeDisplayLabel, type RuntimeHealth, } from "@multica/core/runtimes"; import { agentListOptions, memberListOptions } from "@multica/core/workspace/queries"; @@ -174,11 +175,11 @@ function RuntimeRow({ : runtime ? deriveRuntimeHealth(runtime, Date.now()) : "offline"; - const label = - runtime?.name ?? - (isCloud + const label = runtime + ? runtimeDisplayLabel(runtime) + : isCloud ? t(($) => $.row.fallback_runtime_cloud) - : t(($) => $.profile_card.unknown_runtime)); + : t(($) => $.profile_card.unknown_runtime); return (
{t(($) => $.profile_card.runtime_label)} diff --git a/packages/views/agents/components/agents-page.tsx b/packages/views/agents/components/agents-page.tsx index a283e4eed4..e89ac36257 100644 --- a/packages/views/agents/components/agents-page.tsx +++ b/packages/views/agents/components/agents-page.tsx @@ -44,7 +44,7 @@ import { memberListOptions, workspaceKeys, } from "@multica/core/workspace/queries"; -import { runtimeListOptions } from "@multica/core/runtimes"; +import { runtimeDisplayLabel, runtimeListOptions } from "@multica/core/runtimes"; import { Button } from "@multica/ui/components/ui/button"; import { Checkbox } from "@multica/ui/components/ui/checkbox"; import { @@ -428,7 +428,7 @@ function RuntimeCell({ row }: { row: AgentListRow }) { {runtime ? ( - {runtime.name} + {runtimeDisplayLabel(runtime)} ) : (