diff --git a/src/plugin-handlers/agent-config-handler.ts b/src/plugin-handlers/agent-config-handler.ts index 8c4a8b1fd..b8c7a9ee6 100644 --- a/src/plugin-handlers/agent-config-handler.ts +++ b/src/plugin-handlers/agent-config-handler.ts @@ -2,7 +2,7 @@ import { createBuiltinAgents } from "../agents"; import { createSisyphusJuniorAgentWithOverrides } from "../agents/sisyphus-junior"; import type { OhMyOpenCodeConfig } from "../config"; import { isTaskSystemEnabled, log, migrateAgentConfig } from "../shared"; -import { getAgentDisplayName } from "../shared/agent-display-names"; +import { getAgentRuntimeName } from "../shared/agent-display-names"; import { AGENT_NAME_MAP } from "../shared/migration"; import { registerAgentName } from "../features/claude-code-session-state"; import { @@ -159,10 +159,10 @@ export async function applyAgentConfig(params: { if (isSisyphusEnabled && builtinAgents.sisyphus) { if (configuredDefaultAgent) { (params.config as { default_agent?: string }).default_agent = - getAgentDisplayName(configuredDefaultAgent); + getAgentRuntimeName(configuredDefaultAgent); } else { (params.config as { default_agent?: string }).default_agent = - getAgentDisplayName("sisyphus"); + getAgentRuntimeName("sisyphus"); } // Assembly order: Sisyphus -> Hephaestus -> Prometheus -> Atlas diff --git a/src/plugin-handlers/agent-key-remapper.test.ts b/src/plugin-handlers/agent-key-remapper.test.ts index d0a6c24de..821a323a6 100644 --- a/src/plugin-handlers/agent-key-remapper.test.ts +++ b/src/plugin-handlers/agent-key-remapper.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from "bun:test" import { remapAgentKeysToDisplayNames } from "./agent-key-remapper" -import { getAgentDisplayName } from "../shared/agent-display-names" +import { getAgentDisplayName, getAgentRuntimeName } from "../shared/agent-display-names" describe("remapAgentKeysToDisplayNames", () => { it("remaps known agent keys to display names", () => { @@ -105,4 +105,47 @@ describe("remapAgentKeysToDisplayNames", () => { expect(name).not.toContain("\u200B") } }) + + it("preserves clean keys but rewrites core agent name fields to list-display names for tab cycling", () => { + // given agents with raw config-key names + const agents = { + sisyphus: { name: "sisyphus", prompt: "test", mode: "primary" }, + hephaestus: { name: "hephaestus", prompt: "test", mode: "primary" }, + prometheus: { name: "prometheus", prompt: "test", mode: "all" }, + atlas: { name: "atlas", prompt: "test", mode: "primary" }, + oracle: { name: "oracle", prompt: "test", mode: "subagent" }, + } + + // when remapping + const result = remapAgentKeysToDisplayNames(agents) + + // then keys stay HTTP-header-safe, but nested names carry stable list ordering + expect(Object.keys(result).slice(0, 4)).toEqual([ + getAgentDisplayName("sisyphus"), + getAgentDisplayName("hephaestus"), + getAgentDisplayName("prometheus"), + getAgentDisplayName("atlas"), + ]) + expect(result[getAgentDisplayName("sisyphus")]).toEqual({ + name: getAgentRuntimeName("sisyphus"), + prompt: "test", + mode: "primary", + }) + expect(result[getAgentDisplayName("hephaestus")]).toEqual({ + name: getAgentRuntimeName("hephaestus"), + prompt: "test", + mode: "primary", + }) + expect(result[getAgentDisplayName("prometheus")]).toEqual({ + name: getAgentRuntimeName("prometheus"), + prompt: "test", + mode: "all", + }) + expect(result[getAgentDisplayName("atlas")]).toEqual({ + name: getAgentRuntimeName("atlas"), + prompt: "test", + mode: "primary", + }) + expect(result.oracle).toEqual({ name: "oracle", prompt: "test", mode: "subagent" }) + }) }) diff --git a/src/plugin-handlers/agent-key-remapper.ts b/src/plugin-handlers/agent-key-remapper.ts index 54d422a4b..e6dafc1b0 100644 --- a/src/plugin-handlers/agent-key-remapper.ts +++ b/src/plugin-handlers/agent-key-remapper.ts @@ -1,4 +1,23 @@ -import { getAgentDisplayName } from "../shared/agent-display-names" +import { getAgentDisplayName, getAgentRuntimeName } from "../shared/agent-display-names" + +function rewriteAgentNameForListDisplay( + key: string, + value: unknown, +): unknown { + if (typeof value !== "object" || value === null || !("name" in value)) { + return value + } + + const agent = value as Record + if (typeof agent.name !== "string") { + return value + } + + return { + ...agent, + name: getAgentRuntimeName(key), + } +} export function remapAgentKeysToDisplayNames( agents: Record, @@ -8,7 +27,7 @@ export function remapAgentKeysToDisplayNames( for (const [key, value] of Object.entries(agents)) { const displayName = getAgentDisplayName(key) if (displayName && displayName !== key) { - result[displayName] = value + result[displayName] = rewriteAgentNameForListDisplay(key, value) // 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/plugin-handlers/config-handler.test.ts b/src/plugin-handlers/config-handler.test.ts index 3ea923af2..e8bc6873e 100644 --- a/src/plugin-handlers/config-handler.test.ts +++ b/src/plugin-handlers/config-handler.test.ts @@ -3,7 +3,7 @@ import { describe, test, expect, spyOn, beforeEach, afterEach, mock } from "bun:test" import type { CategoryConfig } from "../config/schema" import type { OhMyOpenCodeConfig } from "../config" -import { getAgentDisplayName } from "../shared/agent-display-names" +import { getAgentDisplayName, getAgentRuntimeName } from "../shared/agent-display-names" import { resolveCategoryConfig } from "./category-config-resolver" import * as agents from "../agents" @@ -479,7 +479,7 @@ describe("default_agent behavior with Sisyphus orchestration", () => { await handler(config) // then - expect(config.default_agent).toBe(getAgentDisplayName("hephaestus")) + expect(config.default_agent).toBe(getAgentRuntimeName("hephaestus")) }) test("canonicalizes configured default_agent when key uses mixed case", async () => { @@ -503,7 +503,7 @@ describe("default_agent behavior with Sisyphus orchestration", () => { await handler(config) // then - expect(config.default_agent).toBe(getAgentDisplayName("hephaestus")) + expect(config.default_agent).toBe(getAgentRuntimeName("hephaestus")) }) test("canonicalizes configured default_agent key to display name", async () => { @@ -527,7 +527,7 @@ describe("default_agent behavior with Sisyphus orchestration", () => { await handler(config) // #then - expect(config.default_agent).toBe(getAgentDisplayName("hephaestus")) + expect(config.default_agent).toBe(getAgentRuntimeName("hephaestus")) }) test("preserves existing display-name default_agent", async () => { @@ -575,7 +575,31 @@ describe("default_agent behavior with Sisyphus orchestration", () => { await handler(config) // #then - expect(config.default_agent).toBe(getAgentDisplayName("sisyphus")) + expect(config.default_agent).toBe(getAgentRuntimeName("sisyphus")) + }) + + test("uses runtime default_agent name so OpenCode matches the emitted ordered agent names", async () => { + // given + const pluginConfig = createPluginConfig({}) + const config: Record = { + model: "anthropic/claude-opus-4-6", + default_agent: "hephaestus", + agent: {}, + } + const handler = createConfigHandler({ + ctx: { directory: "/tmp" }, + pluginConfig, + modelCacheState: { + anthropicContext1MEnabled: false, + modelContextLimitsCache: new Map(), + }, + }) + + // when + await handler(config) + + // then + expect(config.default_agent).toBe(getAgentRuntimeName("hephaestus")) }) test("sets default_agent to sisyphus when configured default_agent is empty after trim", async () => { @@ -599,7 +623,7 @@ describe("default_agent behavior with Sisyphus orchestration", () => { await handler(config) // then - expect(config.default_agent).toBe(getAgentDisplayName("sisyphus")) + expect(config.default_agent).toBe(getAgentRuntimeName("sisyphus")) }) test("preserves custom default_agent names while trimming whitespace", async () => { diff --git a/src/shared/agent-display-names.ts b/src/shared/agent-display-names.ts index 426425851..6070393e4 100644 --- a/src/shared/agent-display-names.ts +++ b/src/shared/agent-display-names.ts @@ -37,6 +37,13 @@ export function stripAgentListSortPrefix(agentName: string): string { return agentName.replace(/^\u200B+/, "") } +export function getAgentRuntimeName(configKey: string): string { + const displayName = getAgentDisplayName(configKey) + const prefix = AGENT_LIST_SORT_PREFIXES[configKey.toLowerCase()] + + return prefix ? `${prefix}${displayName}` : displayName +} + /** * Get display name for an agent config key. * Uses case-insensitive lookup for backward compatibility. @@ -65,10 +72,7 @@ export function getAgentDisplayName(configKey: string): string { * See: https://github.com/code-yeongyu/oh-my-openagent/issues/3238 */ export function getAgentListDisplayName(configKey: string): string { - const displayName = getAgentDisplayName(configKey) - const prefix = AGENT_LIST_SORT_PREFIXES[configKey.toLowerCase()] - - return prefix ? `${prefix}${displayName}` : displayName + return getAgentRuntimeName(configKey) } const REVERSE_DISPLAY_NAMES: Record = Object.fromEntries(