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.
This commit is contained in:
Rusty Raven
2026-07-14 15:13:20 +08:00
committed by GitHub
parent ef9b334408
commit e565a4559a
5 changed files with 122 additions and 9 deletions

View File

@@ -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)");
});
});

View File

@@ -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<AgentRuntime, "name" | "custom_name" | "provider">,
): 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<string, string> = {
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);
}

View File

@@ -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({
<span className="inline-flex min-w-0 items-center gap-1.5">
<Server className="h-3.5 w-3.5 shrink-0" aria-hidden="true" />
<span className="truncate">
{runtime?.name ?? t(($) => $.pickers.runtime_none)}
{runtime
? runtimeDisplayLabel(runtime)
: t(($) => $.pickers.runtime_none)}
</span>
</span>
<VisibilityBadge value={agent.visibility} />

View File

@@ -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 (
<div className="flex items-center gap-1.5">
<span className="w-12 shrink-0 text-muted-foreground">{t(($) => $.profile_card.runtime_label)}</span>

View File

@@ -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 }) {
<ListGridCell className="hidden @2xl:flex">
{runtime ? (
<span className="min-w-0 truncate text-xs text-muted-foreground">
{runtime.name}
{runtimeDisplayLabel(runtime)}
</span>
) : (
<span className="text-xs text-muted-foreground/40"></span>