feat(agents): support per-agent displayName for i18n (#4004)
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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(),
|
||||
|
||||
@@ -392,6 +392,7 @@ export async function applyAgentConfig(params: {
|
||||
if (params.config.agent) {
|
||||
params.config.agent = remapAgentKeysToDisplayNames(
|
||||
params.config.agent as Record<string, unknown>,
|
||||
params.pluginConfig.agents as Record<string, { displayName?: string } | undefined> | undefined,
|
||||
);
|
||||
params.config.agent = reorderAgentsByPriority(
|
||||
params.config.agent as Record<string, unknown>,
|
||||
|
||||
@@ -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<string, unknown>).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()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { getAgentListDisplayName } from "../shared/agent-display-names"
|
||||
|
||||
type AgentOverridesMap = Record<string, { displayName?: string } | undefined>
|
||||
|
||||
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<string, unknown>
|
||||
return {
|
||||
...agent,
|
||||
name: getAgentListDisplayName(key),
|
||||
name: getAgentListDisplayName(key, overrides),
|
||||
}
|
||||
}
|
||||
|
||||
export function remapAgentKeysToDisplayNames(
|
||||
agents: Record<string, unknown>,
|
||||
overrides?: AgentOverridesMap,
|
||||
): Record<string, unknown> {
|
||||
const result: Record<string, unknown> = {}
|
||||
|
||||
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:
|
||||
|
||||
@@ -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, { displayName?: string } | undefined>,
|
||||
): 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, { displayName?: string } | undefined>,
|
||||
): string {
|
||||
return getAgentDisplayName(configKey, overrides)
|
||||
}
|
||||
|
||||
const REVERSE_DISPLAY_NAMES: Record<string, string> = Object.fromEntries(
|
||||
|
||||
Reference in New Issue
Block a user