Files
oh-my-opencode/src/shared/agent-display-names.ts
T

175 lines
6.1 KiB
TypeScript
Raw Normal View History

/**
* Agent config keys to display names mapping.
* Config keys are lowercase (e.g., "sisyphus", "atlas").
* Display names include suffixes for UI/logs (e.g., "Sisyphus - Ultraworker").
*
* IMPORTANT: Display names MUST NOT contain parentheses or other characters
* that are invalid in HTTP header values per RFC 7230. OpenCode passes the
* agent name in the `x-opencode-agent-name` header, and parentheses cause
* header validation failures that prevent agents from appearing in the UI
* type selector dropdown. Use ` - ` (space-dash-space) instead of `(...)`.
*/
export const AGENT_DISPLAY_NAMES: Record<string, string> = {
sisyphus: "Sisyphus - Ultraworker",
hephaestus: "Hephaestus - Deep Agent",
prometheus: "Prometheus - Plan Builder",
atlas: "Atlas - Plan Executor",
"sisyphus-junior": "Sisyphus-Junior",
metis: "Metis - Plan Consultant",
momus: "Momus - Plan Critic",
athena: "Athena - Council",
"athena-junior": "Athena-Junior - Council",
oracle: "oracle",
librarian: "librarian",
explore: "explore",
"multimodal-looker": "multimodal-looker",
"council-member": "council-member",
}
/**
* Strip the legacy zero-width-space sort prefix from an agent name.
*
* v3.14.0 through v3.16.0 prefixed the four core agents (Sisyphus,
* Hephaestus, Prometheus, Atlas) with U+200B Zero Width Space characters
* so they would sort ahead of user agents in the Tab cycle. Some terminal
* emulators (Ghostty, certain Windows Terminal builds) render ZWSP as a
* visible box or extra space, breaking the status bar layout (#3259), and
* the prefixes also leaked through the plugin API and broke prompt_async
* consumers (#3238).
*
* The prefixes are no longer injected anywhere (#3242 removed all call
* sites and #3259 removed the constant table). This helper remains so
* existing user configs that still have the ZWSP baked into their
* `config.agent` keys from an older install continue to resolve
* correctly after upgrading.
*/
export function stripAgentListSortPrefix(agentName: string): string {
return agentName.replace(/^\u200B+/, "")
}
/**
* Get display name for an agent config key.
* Uses case-insensitive lookup for backward compatibility.
* Returns original key if not found.
*/
export function getAgentDisplayName(configKey: string): string {
// Try exact match first
const exactMatch = AGENT_DISPLAY_NAMES[configKey]
if (exactMatch !== undefined) return exactMatch
// Fall back to case-insensitive search
const lowerKey = configKey.toLowerCase()
for (const [k, v] of Object.entries(AGENT_DISPLAY_NAMES)) {
if (k.toLowerCase() === lowerKey) return v
}
// Unknown agent: return original key
return configKey
}
/**
* @deprecated Use {@link getAgentDisplayName} directly.
*
* Historically this returned the display name with a ZWSP sort prefix
* prepended so core agents would sort ahead of user agents in the Tab
* cycle. The ZWSP prefixes caused visible rendering artifacts in some
* terminals (#3259) and leaked into the plugin API surface (#3238), so
* they were removed in #3242/#3259. This function is now a thin alias
* over {@link getAgentDisplayName} that exists only for external
* callers that may still import it. Sort ordering is now handled by
* the `order` field injection in `reorderAgentsByPriority()` plus the
* core-first insertion order in the same helper.
*/
export function getAgentListDisplayName(configKey: string): string {
return getAgentDisplayName(configKey)
}
const REVERSE_DISPLAY_NAMES: Record<string, string> = Object.fromEntries(
Object.entries(AGENT_DISPLAY_NAMES).map(([key, displayName]) => [displayName.toLowerCase(), key]),
)
// Legacy parenthesized display names for backward compatibility.
// Old configs/sessions may reference these names; resolve them to config keys.
const LEGACY_DISPLAY_NAMES: Record<string, string> = {
"sisyphus (ultraworker)": "sisyphus",
"hephaestus (deep agent)": "hephaestus",
"prometheus (plan builder)": "prometheus",
"atlas (plan executor)": "atlas",
"metis (plan consultant)": "metis",
"momus (plan critic)": "momus",
"athena (council)": "athena",
"athena-junior (council)": "athena-junior",
}
/**
* Resolve an agent name (display name or config key) to its lowercase config key.
* "Atlas - Plan Executor" -> "atlas", "Atlas (Plan Executor)" -> "atlas", "atlas" -> "atlas"
*/
export function getAgentConfigKey(agentName: string): string {
const lower = stripAgentListSortPrefix(agentName).toLowerCase()
const reversed = REVERSE_DISPLAY_NAMES[lower]
if (reversed !== undefined) return reversed
const legacy = LEGACY_DISPLAY_NAMES[lower]
if (legacy !== undefined) return legacy
if (AGENT_DISPLAY_NAMES[lower] !== undefined) return lower
return lower
}
/**
* Normalize an agent name for prompt APIs.
* - Known display names -> canonical display names
* - Known config keys (any case) -> canonical display names
* - Unknown/custom names -> preserved as-is (trimmed)
*/
export function normalizeAgentForPrompt(agentName: string | undefined): string | undefined {
if (typeof agentName !== "string") {
return undefined
}
const trimmed = stripAgentListSortPrefix(agentName.trim())
if (!trimmed) {
return undefined
}
const lower = trimmed.toLowerCase()
const reversed = REVERSE_DISPLAY_NAMES[lower]
if (reversed !== undefined) {
return AGENT_DISPLAY_NAMES[reversed] ?? trimmed
}
const legacy = LEGACY_DISPLAY_NAMES[lower]
if (legacy !== undefined) {
return AGENT_DISPLAY_NAMES[legacy] ?? trimmed
}
if (AGENT_DISPLAY_NAMES[lower] !== undefined) {
return AGENT_DISPLAY_NAMES[lower]
}
return trimmed
}
export function normalizeAgentForPromptKey(agentName: string | undefined): string | undefined {
if (typeof agentName !== "string") {
return undefined
}
const trimmed = stripAgentListSortPrefix(agentName.trim())
if (!trimmed) {
return undefined
}
const lower = trimmed.toLowerCase()
const reversed = REVERSE_DISPLAY_NAMES[lower]
if (reversed !== undefined) {
return reversed
}
const legacy = LEGACY_DISPLAY_NAMES[lower]
if (legacy !== undefined) {
return legacy
}
if (AGENT_DISPLAY_NAMES[lower] !== undefined) {
return lower
}
return trimmed
}