Merge pull request #3260 from code-yeongyu/fix/remove-zwsp-sort-prefixes

fix(agents): remove ZWSP sort prefixes from display name helper (#3259)
This commit is contained in:
YeonGyu-Kim
2026-04-09 10:16:32 +09:00
committed by GitHub
4 changed files with 64 additions and 34 deletions
-1
View File
@@ -6,7 +6,6 @@ import { randomUUID } from "node:crypto"
import { createPluginInterface } from "./plugin-interface" import { createPluginInterface } from "./plugin-interface"
import { createAutoSlashCommandHook } from "./hooks/auto-slash-command" import { createAutoSlashCommandHook } from "./hooks/auto-slash-command"
import { createStartWorkHook } from "./hooks/start-work" import { createStartWorkHook } from "./hooks/start-work"
import { getAgentListDisplayName } from "./shared/agent-display-names"
import { readBoulderState } from "./features/boulder-state" import { readBoulderState } from "./features/boulder-state"
import { import {
_resetForTesting, _resetForTesting,
+5 -3
View File
@@ -9,7 +9,6 @@ import { createAutoSlashCommandHook } from "../hooks/auto-slash-command"
import { createStartWorkHook } from "../hooks/start-work" import { createStartWorkHook } from "../hooks/start-work"
import { readBoulderState } from "../features/boulder-state" import { readBoulderState } from "../features/boulder-state"
import { _resetForTesting, setMainSession, subagentSessions, registerAgentName, updateSessionAgent, getSessionAgent } from "../features/claude-code-session-state" import { _resetForTesting, setMainSession, subagentSessions, registerAgentName, updateSessionAgent, getSessionAgent } from "../features/claude-code-session-state"
import { getAgentListDisplayName } from "../shared/agent-display-names"
import { clearSessionModel, getSessionModel, setSessionModel } from "../shared/session-model-state" import { clearSessionModel, getSessionModel, setSessionModel } from "../shared/session-model-state"
type ChatMessagePart = { type: string; text?: string; [key: string]: unknown } type ChatMessagePart = { type: string; text?: string; [key: string]: unknown }
@@ -403,7 +402,10 @@ describe("createChatMessageHandler - TUI variant passthrough", () => {
expect(getSessionModel("test-session")).toEqual({ providerID: "openai", modelID: "gpt-5.4" }) expect(getSessionModel("test-session")).toEqual({ providerID: "openai", modelID: "gpt-5.4" })
}) })
test("treats prefixed list-display agent names as explicit model overrides", async () => { test("treats legacy ZWSP-prefixed agent names as explicit model overrides (GH-3259)", async () => {
// Users upgrading from v3.14.0-v3.16.0 may still have ZWSP-prefixed agent
// keys persisted in their session state. The handler must strip the
// prefix and resolve to the canonical display name.
//#given //#given
setMainSession("test-session") setMainSession("test-session")
setSessionModel("test-session", { providerID: "openai", modelID: "gpt-5.4" }) setSessionModel("test-session", { providerID: "openai", modelID: "gpt-5.4" })
@@ -416,7 +418,7 @@ describe("createChatMessageHandler - TUI variant passthrough", () => {
}, },
}) })
const handler = createChatMessageHandler(args) const handler = createChatMessageHandler(args)
const input = createMockInput(getAgentListDisplayName("prometheus")) const input = createMockInput("\u200B\u200B\u200BPrometheus - Plan Builder")
const output = createMockOutput() const output = createMockOutput()
//#when //#when
+30 -14
View File
@@ -183,30 +183,46 @@ describe("getAgentConfigKey", () => {
expect(getAgentConfigKey("Sisyphus-Junior")).toBe("sisyphus-junior") expect(getAgentConfigKey("Sisyphus-Junior")).toBe("sisyphus-junior")
}) })
it("resolves atlas even when the UI ordering prefix is present", () => { it("resolves atlas even when a legacy ZWSP sort prefix is present on the stored key", () => {
expect(getAgentConfigKey(getAgentListDisplayName("atlas"))).toBe("atlas") // Users who installed v3.14.0 through v3.16.0 may have ZWSP-prefixed agent
// names baked into their config.agent keys. The resolver must still find
// the canonical config key after strip.
expect(getAgentConfigKey("\u200B\u200B\u200B\u200BAtlas - Plan Executor")).toBe("atlas")
}) })
}) })
describe("getAgentListDisplayName", () => { describe("getAgentListDisplayName (deprecated alias, GH-3259)", () => {
it("applies invisible stable-sort prefixes to the core agent list", () => { it("returns plain display names without the legacy ZWSP sort prefix", () => {
expect(getAgentListDisplayName("sisyphus")).toBe("\u200BSisyphus - Ultraworker") // ZWSP prefixes were removed in #3242/#3259. This alias is retained for
expect(getAgentListDisplayName("hephaestus")).toBe("\u200B\u200BHephaestus - Deep Agent") // external callers that may still import it, but it now behaves
expect(getAgentListDisplayName("prometheus")).toBe("\u200B\u200B\u200BPrometheus - Plan Builder") // identically to getAgentDisplayName.
expect(getAgentListDisplayName("atlas")).toBe("\u200B\u200B\u200B\u200BAtlas - Plan Executor") expect(getAgentListDisplayName("sisyphus")).toBe("Sisyphus - Ultraworker")
expect(getAgentListDisplayName("hephaestus")).toBe("Hephaestus - Deep Agent")
expect(getAgentListDisplayName("prometheus")).toBe("Prometheus - Plan Builder")
expect(getAgentListDisplayName("atlas")).toBe("Atlas - Plan Executor")
}) })
it("keeps non-core agents unprefixed for list display", () => { it("matches getAgentDisplayName for unknown agents", () => {
expect(getAgentListDisplayName("oracle")).toBe("oracle") expect(getAgentListDisplayName("oracle")).toBe("oracle")
}) })
it("contains no zero-width characters in any core agent output (GH-3259)", () => {
const coreAgents = ["sisyphus", "hephaestus", "prometheus", "atlas"]
for (const agent of coreAgents) {
const result = getAgentListDisplayName(agent)
expect(result).not.toMatch(/[\u200B\u200C\u200D\uFEFF]/)
}
})
}) })
describe("normalizeAgentForPrompt", () => { describe("normalizeAgentForPrompt", () => {
it("strips core UI ordering prefixes back to canonical display names", () => { it("strips legacy ZWSP sort prefixes from stored agent keys back to canonical display names", () => {
expect(normalizeAgentForPrompt(getAgentListDisplayName("sisyphus"))).toBe("Sisyphus - Ultraworker") // Configs from v3.14.0-v3.16.0 may persist ZWSP-prefixed keys. The
expect(normalizeAgentForPrompt(getAgentListDisplayName("hephaestus"))).toBe("Hephaestus - Deep Agent") // normalizer must restore the canonical name on read.
expect(normalizeAgentForPrompt(getAgentListDisplayName("prometheus"))).toBe("Prometheus - Plan Builder") expect(normalizeAgentForPrompt("\u200BSisyphus - Ultraworker")).toBe("Sisyphus - Ultraworker")
expect(normalizeAgentForPrompt(getAgentListDisplayName("atlas"))).toBe("Atlas - Plan Executor") expect(normalizeAgentForPrompt("\u200B\u200BHephaestus - Deep Agent")).toBe("Hephaestus - Deep Agent")
expect(normalizeAgentForPrompt("\u200B\u200B\u200BPrometheus - Plan Builder")).toBe("Prometheus - Plan Builder")
expect(normalizeAgentForPrompt("\u200B\u200B\u200B\u200BAtlas - Plan Executor")).toBe("Atlas - Plan Executor")
}) })
}) })
+29 -16
View File
@@ -26,13 +26,23 @@ export const AGENT_DISPLAY_NAMES: Record<string, string> = {
"council-member": "council-member", "council-member": "council-member",
} }
const AGENT_LIST_SORT_PREFIXES: Record<string, string> = { /**
sisyphus: "\u200B", * Strip the legacy zero-width-space sort prefix from an agent name.
hephaestus: "\u200B\u200B", *
prometheus: "\u200B\u200B\u200B", * v3.14.0 through v3.16.0 prefixed the four core agents (Sisyphus,
atlas: "\u200B\u200B\u200B\u200B", * 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 { export function stripAgentListSortPrefix(agentName: string): string {
return agentName.replace(/^\u200B+/, "") return agentName.replace(/^\u200B+/, "")
} }
@@ -58,17 +68,20 @@ export function getAgentDisplayName(configKey: string): string {
} }
/** /**
* @deprecated Do NOT use for config.agent keys or API-facing names. * @deprecated Use {@link getAgentDisplayName} directly.
* ZWSP prefixes leak into the /agent API response and break prompt_async consumers. *
* Use getAgentDisplayName() instead. The `order` field injected by * Historically this returned the display name with a ZWSP sort prefix
* reorderAgentsByPriority() handles sort ordering without invisible characters. * prepended so core agents would sort ahead of user agents in the Tab
* See: https://github.com/code-yeongyu/oh-my-openagent/issues/3238 * 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 { export function getAgentListDisplayName(configKey: string): string {
const displayName = getAgentDisplayName(configKey) return getAgentDisplayName(configKey)
const prefix = AGENT_LIST_SORT_PREFIXES[configKey.toLowerCase()]
return prefix ? `${prefix}${displayName}` : displayName
} }
const REVERSE_DISPLAY_NAMES: Record<string, string> = Object.fromEntries( const REVERSE_DISPLAY_NAMES: Record<string, string> = Object.fromEntries(