From e565a4559abbc8fd3c515e51581faff9afc00d5e Mon Sep 17 00:00:00 2001 From: Rusty Raven Date: Tue, 14 Jul 2026 15:13:20 +0800 Subject: [PATCH] fix(agents): show runtime alias + provider consistently (#5260) (#5340) * fix(agents): show runtime alias + provider consistently (#5260) The daemon bakes the provider into runtime.name ('Codex (host)') while a custom alias is stored separately in custom_name. runtimeDisplayName() returned the bare alias and dropped the provider, and the agent list and profile card rendered raw name, ignoring the alias entirely. Add runtimeDisplayLabel(): with an alias it renders 'alias (Provider)', otherwise it returns the daemon name unchanged (no duplicated provider). Route the agent personal page, list Runtime column, and profile card through it. Fixes #5260 * fix(agents): use provider display-name map for aliased runtime label Address review on #5340: a title-cased slug mislabels providers whose display name differs from the slug (traecli -> 'Traecli' instead of 'Trae') and flattens mixed-case families (CodeBuddy / OpenCode / OpenClaw). Add a provider display-name map mirroring the ProviderLogo switch, with a title-case fallback for unknown slugs. * fix(agents): align provider display map with daemon contract Follow-up on #5340 review: the previous map canonicalized codebuddy / opencode / openclaw as CodeBuddy / OpenCode / OpenClaw, but the daemon's runtimeDisplayNameOverrides only special-cases traecli and first-letter- capitalizes the rest. That recreated alias/no-alias drift for those providers ('Openclaw (host)' vs 'box (OpenClaw)'). Shrink the frontend map to mirror the daemon exactly (traecli -> Trae, first-letter fallback otherwise) and point the comment at the daemon map as the source of truth. Tests updated to lock the alignment. --- packages/core/runtimes/display.test.ts | 69 ++++++++++++++++++- packages/core/runtimes/display.ts | 43 ++++++++++++ .../agents/components/agent-detail-page.tsx | 6 +- .../agents/components/agent-profile-card.tsx | 9 +-- .../views/agents/components/agents-page.tsx | 4 +- 5 files changed, 122 insertions(+), 9 deletions(-) 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)} ) : (