Files
oh-my-opencode/src/shared/agent-display-names.ts
T
YeonGyu-Kim 333ad3aadd refactor(agents): drop ZWSP prefixes from agent display names
The sort shim from the previous commit enforces canonical core ordering at runtime, so ZWSP prefixes are no longer needed. Removing them eliminates the Bun.stringWidth vs terminal-width drift that broke the TUI status bar (#3259).

Drop AGENT_LIST_SORT_PREFIXES and getAgentRuntimeName from agent-display-names; switch all call sites to getAgentDisplayName. getAgentListDisplayName stays as a thin alias for external importers.

Keep stripInvisibleAgentCharacters and the ZWSP regex paths so legacy session state and configs from v3.14.0-v3.16.0 still resolve.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-27 18:59:49 +09:00

144 lines
5.0 KiB
TypeScript

/**
* 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",
}
const INVISIBLE_AGENT_CHARACTERS_REGEX = /[\u200B\u200C\u200D\uFEFF]/g
export function stripInvisibleAgentCharacters(agentName: string): string {
return agentName.replace(INVISIBLE_AGENT_CHARACTERS_REGEX, "")
}
export function stripAgentListSortPrefix(agentName: string): string {
return stripInvisibleAgentCharacters(agentName)
}
/**
* 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
}
/**
* Thin alias for `getAgentDisplayName` preserved for external imports.
*
* Earlier versions injected zero-width prefixes here to bias OpenCode's
* `agent.name` sort. Sort ordering is now enforced by
* `src/shared/agent-sort-shim.ts`, so this function emits the canonical
* 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)
}
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",
}
function resolveKnownAgentConfigKey(agentName: string): string | undefined {
const lower = stripAgentListSortPrefix(agentName).trim().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 undefined
}
/**
* 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).trim().toLowerCase()
return resolveKnownAgentConfigKey(agentName) ?? 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 configKey = resolveKnownAgentConfigKey(trimmed)
if (configKey !== undefined) {
return AGENT_DISPLAY_NAMES[configKey] ?? trimmed
}
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
}
return resolveKnownAgentConfigKey(trimmed) ?? trimmed
}