From 7bc92bcd25f3ee00bba99b0f017bf1dfa816f50a Mon Sep 17 00:00:00 2001 From: ZeyuFu Date: Sat, 16 May 2026 06:52:14 -0400 Subject: [PATCH] feat(agents): support per-agent displayName for i18n (#4004) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add optional `displayName` field to AgentOverrideConfigSchema (next to the existing `color` field) so users can specify localized agent names in oh-my-openagent.json: { "agents": { "sisyphus": { "displayName": "总指挥" } } } When set, the override takes precedence everywhere AGENT_DISPLAY_NAMES[agentName] is used — TUI agent selector, the agent list key, and the internal `name` field. When not set, behavior is identical to before (hardcoded English names from AGENT_DISPLAY_NAMES). Implementation touches: - AgentOverrideConfigSchema: adds displayName?: z.string().optional() - getAgentDisplayName / getAgentListDisplayName: accept optional overrides map and check displayName before the hardcoded table - remapAgentKeysToDisplayNames: forwards overrides map to name resolution - agent-config-handler: passes pluginConfig.agents as the overrides map Backward compatible — existing configs without displayName continue to work unchanged. Co-Authored-By: Claude Sonnet 4.6 --- src/config/schema/agent-overrides.ts | 2 + src/plugin-handlers/agent-config-handler.ts | 1 + .../agent-key-remapper.test.ts | 50 +++++++++++++++++++ src/plugin-handlers/agent-key-remapper.ts | 10 ++-- src/shared/agent-display-names.ts | 23 +++++++-- 5 files changed, 80 insertions(+), 6 deletions(-) diff --git a/src/config/schema/agent-overrides.ts b/src/config/schema/agent-overrides.ts index cbf995392..9af62cb3a 100644 --- a/src/config/schema/agent-overrides.ts +++ b/src/config/schema/agent-overrides.ts @@ -24,6 +24,8 @@ export const AgentOverrideConfigSchema = z.object({ .string() .regex(/^#[0-9A-Fa-f]{6}$/) .optional(), + /** Localized display name shown in TUI agent selector (i18n support). Falls back to hardcoded English when not set. */ + displayName: z.string().optional(), permission: AgentPermissionSchema.optional(), /** Maximum tokens for response. Passed directly to OpenCode SDK. */ maxTokens: z.number().optional(), diff --git a/src/plugin-handlers/agent-config-handler.ts b/src/plugin-handlers/agent-config-handler.ts index 9d5c3b2ca..d4b2ed5e3 100644 --- a/src/plugin-handlers/agent-config-handler.ts +++ b/src/plugin-handlers/agent-config-handler.ts @@ -392,6 +392,7 @@ export async function applyAgentConfig(params: { if (params.config.agent) { params.config.agent = remapAgentKeysToDisplayNames( params.config.agent as Record, + params.pluginConfig.agents as Record | undefined, ); params.config.agent = reorderAgentsByPriority( params.config.agent as Record, diff --git a/src/plugin-handlers/agent-key-remapper.test.ts b/src/plugin-handlers/agent-key-remapper.test.ts index 7c4ff25e4..d10f3eb21 100644 --- a/src/plugin-handlers/agent-key-remapper.test.ts +++ b/src/plugin-handlers/agent-key-remapper.test.ts @@ -197,4 +197,54 @@ describe("remapAgentKeysToDisplayNames", () => { foo: "bar", }) }) + + describe("displayName i18n override (#4004)", () => { + it("uses per-agent displayName override when set", () => { + // given sisyphus config with a Chinese displayName override + const agents = { + sisyphus: { prompt: "test", mode: "primary" }, + } + const overrides = { + sisyphus: { displayName: "总指挥" }, + } + + // when remapping with overrides + const result = remapAgentKeysToDisplayNames(agents, overrides) + + // then the localized name is used instead of "Sisyphus - Ultraworker" + expect(result["总指挥"]).toBeDefined() + expect((result["总指挥"] as Record).name).toBe("总指挥") + expect(result["Sisyphus - Ultraworker"]).toBeUndefined() + }) + + it("falls back to hardcoded English name when displayName is not set", () => { + // given sisyphus config without displayName override + const agents = { + sisyphus: { prompt: "test", mode: "primary" }, + } + const overrides = { + sisyphus: { model: "claude-opus-4-7" }, + } + + // when remapping with overrides that have no displayName + const result = remapAgentKeysToDisplayNames(agents, overrides) + + // then the legacy AGENT_DISPLAY_NAMES value is used + expect(result["Sisyphus - Ultraworker"]).toBeDefined() + expect(result["总指挥"]).toBeUndefined() + }) + + it("falls back to hardcoded English name when no overrides are passed", () => { + // given sisyphus config with no overrides at all + const agents = { + sisyphus: { prompt: "test", mode: "primary" }, + } + + // when remapping without overrides + const result = remapAgentKeysToDisplayNames(agents) + + // then the legacy AGENT_DISPLAY_NAMES value is used + expect(result["Sisyphus - Ultraworker"]).toBeDefined() + }) + }) }) diff --git a/src/plugin-handlers/agent-key-remapper.ts b/src/plugin-handlers/agent-key-remapper.ts index e75ab21b3..e712af71f 100644 --- a/src/plugin-handlers/agent-key-remapper.ts +++ b/src/plugin-handlers/agent-key-remapper.ts @@ -1,8 +1,11 @@ import { getAgentListDisplayName } from "../shared/agent-display-names" +type AgentOverridesMap = Record + function rewriteAgentNameForListDisplay( key: string, value: unknown, + overrides?: AgentOverridesMap, ): unknown { if (typeof value !== "object" || value === null) { return value @@ -11,19 +14,20 @@ function rewriteAgentNameForListDisplay( const agent = value as Record return { ...agent, - name: getAgentListDisplayName(key), + name: getAgentListDisplayName(key, overrides), } } export function remapAgentKeysToDisplayNames( agents: Record, + overrides?: AgentOverridesMap, ): Record { const result: Record = {} for (const [key, value] of Object.entries(agents)) { - const displayName = getAgentListDisplayName(key) + const displayName = getAgentListDisplayName(key, overrides) if (displayName && displayName !== key) { - result[displayName] = rewriteAgentNameForListDisplay(key, value) + result[displayName] = rewriteAgentNameForListDisplay(key, value, overrides) // Regression guard: do not also assign result[key]. // This line was repeatedly re-added and caused duplicate agent rows in the UI. // Runtime callers that previously depended on config-key aliases were fixed in: diff --git a/src/shared/agent-display-names.ts b/src/shared/agent-display-names.ts index 9a7f9c517..0ba70d61d 100644 --- a/src/shared/agent-display-names.ts +++ b/src/shared/agent-display-names.ts @@ -42,8 +42,22 @@ export function stripAgentListSortPrefix(agentName: string): string { * Get display name for an agent config key. * Uses case-insensitive lookup for backward compatibility. * Returns original key if not found. + * + * @param overrides - Optional per-agent overrides map. If the agent has a `displayName` + * field set, it takes precedence over the hardcoded AGENT_DISPLAY_NAMES entry. + * This enables i18n: `agents.sisyphus.displayName = "总指挥"` in oh-my-openagent.json. */ -export function getAgentDisplayName(configKey: string): string { +export function getAgentDisplayName( + configKey: string, + overrides?: Record, +): string { + // Check per-agent displayName override first (i18n support) + if (overrides) { + const override = overrides[configKey] + ?? Object.entries(overrides).find(([k]) => k.toLowerCase() === configKey.toLowerCase())?.[1] + if (override?.displayName) return override.displayName + } + // Try exact match first const exactMatch = AGENT_DISPLAY_NAMES[configKey] if (exactMatch !== undefined) return exactMatch @@ -67,8 +81,11 @@ export function getAgentDisplayName(configKey: string): string { * display name verbatim. Kept exported because downstream modules still * import this symbol; do not collapse the call sites without coordinating. */ -export function getAgentListDisplayName(configKey: string): string { - return getAgentDisplayName(configKey) +export function getAgentListDisplayName( + configKey: string, + overrides?: Record, +): string { + return getAgentDisplayName(configKey, overrides) } const REVERSE_DISPLAY_NAMES: Record = Object.fromEntries(