From 0d5b0874409cda6e56fef8a9d2f0b7488fd6eef9 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Thu, 9 Apr 2026 12:21:02 +0900 Subject: [PATCH] Revert "Merge pull request #3260 from code-yeongyu/fix/remove-zwsp-sort-prefixes" This reverts commit c3be4c2793be1c82a50ef2568bb2d0caac07edef, reversing changes made to d2bb5d57d1362a5a49f204b5482485ce1293012b. --- src/plugin-interface.test.ts | 1 + src/plugin/chat-message.test.ts | 8 ++--- src/shared/agent-display-names.test.ts | 44 ++++++++----------------- src/shared/agent-display-names.ts | 45 +++++++++----------------- 4 files changed, 34 insertions(+), 64 deletions(-) diff --git a/src/plugin-interface.test.ts b/src/plugin-interface.test.ts index fea4752e2..4dac3f7be 100644 --- a/src/plugin-interface.test.ts +++ b/src/plugin-interface.test.ts @@ -6,6 +6,7 @@ import { randomUUID } from "node:crypto" import { createPluginInterface } from "./plugin-interface" import { createAutoSlashCommandHook } from "./hooks/auto-slash-command" import { createStartWorkHook } from "./hooks/start-work" +import { getAgentListDisplayName } from "./shared/agent-display-names" import { readBoulderState } from "./features/boulder-state" import { _resetForTesting, diff --git a/src/plugin/chat-message.test.ts b/src/plugin/chat-message.test.ts index 2d96f6065..6dd7a0397 100644 --- a/src/plugin/chat-message.test.ts +++ b/src/plugin/chat-message.test.ts @@ -9,6 +9,7 @@ import { createAutoSlashCommandHook } from "../hooks/auto-slash-command" import { createStartWorkHook } from "../hooks/start-work" import { readBoulderState } from "../features/boulder-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" type ChatMessagePart = { type: string; text?: string; [key: string]: unknown } @@ -402,10 +403,7 @@ describe("createChatMessageHandler - TUI variant passthrough", () => { expect(getSessionModel("test-session")).toEqual({ providerID: "openai", modelID: "gpt-5.4" }) }) - 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. + test("treats prefixed list-display agent names as explicit model overrides", async () => { //#given setMainSession("test-session") setSessionModel("test-session", { providerID: "openai", modelID: "gpt-5.4" }) @@ -418,7 +416,7 @@ describe("createChatMessageHandler - TUI variant passthrough", () => { }, }) const handler = createChatMessageHandler(args) - const input = createMockInput("\u200B\u200B\u200BPrometheus - Plan Builder") + const input = createMockInput(getAgentListDisplayName("prometheus")) const output = createMockOutput() //#when diff --git a/src/shared/agent-display-names.test.ts b/src/shared/agent-display-names.test.ts index b77a5e1ff..353bfb31e 100644 --- a/src/shared/agent-display-names.test.ts +++ b/src/shared/agent-display-names.test.ts @@ -183,46 +183,30 @@ describe("getAgentConfigKey", () => { expect(getAgentConfigKey("Sisyphus-Junior")).toBe("sisyphus-junior") }) - it("resolves atlas even when a legacy ZWSP sort prefix is present on the stored key", () => { - // 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") + it("resolves atlas even when the UI ordering prefix is present", () => { + expect(getAgentConfigKey(getAgentListDisplayName("atlas"))).toBe("atlas") }) }) -describe("getAgentListDisplayName (deprecated alias, GH-3259)", () => { - it("returns plain display names without the legacy ZWSP sort prefix", () => { - // ZWSP prefixes were removed in #3242/#3259. This alias is retained for - // external callers that may still import it, but it now behaves - // identically to getAgentDisplayName. - 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") +describe("getAgentListDisplayName", () => { + it("applies invisible stable-sort prefixes to the core agent list", () => { + expect(getAgentListDisplayName("sisyphus")).toBe("\u200BSisyphus - Ultraworker") + expect(getAgentListDisplayName("hephaestus")).toBe("\u200B\u200BHephaestus - Deep Agent") + expect(getAgentListDisplayName("prometheus")).toBe("\u200B\u200B\u200BPrometheus - Plan Builder") + expect(getAgentListDisplayName("atlas")).toBe("\u200B\u200B\u200B\u200BAtlas - Plan Executor") }) - it("matches getAgentDisplayName for unknown agents", () => { + it("keeps non-core agents unprefixed for list display", () => { 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", () => { - it("strips legacy ZWSP sort prefixes from stored agent keys back to canonical display names", () => { - // Configs from v3.14.0-v3.16.0 may persist ZWSP-prefixed keys. The - // normalizer must restore the canonical name on read. - expect(normalizeAgentForPrompt("\u200BSisyphus - Ultraworker")).toBe("Sisyphus - Ultraworker") - 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") + it("strips core UI ordering prefixes back to canonical display names", () => { + expect(normalizeAgentForPrompt(getAgentListDisplayName("sisyphus"))).toBe("Sisyphus - Ultraworker") + expect(normalizeAgentForPrompt(getAgentListDisplayName("hephaestus"))).toBe("Hephaestus - Deep Agent") + expect(normalizeAgentForPrompt(getAgentListDisplayName("prometheus"))).toBe("Prometheus - Plan Builder") + expect(normalizeAgentForPrompt(getAgentListDisplayName("atlas"))).toBe("Atlas - Plan Executor") }) }) diff --git a/src/shared/agent-display-names.ts b/src/shared/agent-display-names.ts index 2ccb545ad..426425851 100644 --- a/src/shared/agent-display-names.ts +++ b/src/shared/agent-display-names.ts @@ -26,23 +26,13 @@ export const AGENT_DISPLAY_NAMES: Record = { "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. - */ +const AGENT_LIST_SORT_PREFIXES: Record = { + sisyphus: "\u200B", + hephaestus: "\u200B\u200B", + prometheus: "\u200B\u200B\u200B", + atlas: "\u200B\u200B\u200B\u200B", +} + export function stripAgentListSortPrefix(agentName: string): string { return agentName.replace(/^\u200B+/, "") } @@ -68,20 +58,17 @@ export function getAgentDisplayName(configKey: string): string { } /** - * @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. + * @deprecated Do NOT use for config.agent keys or API-facing names. + * ZWSP prefixes leak into the /agent API response and break prompt_async consumers. + * Use getAgentDisplayName() instead. The `order` field injected by + * reorderAgentsByPriority() handles sort ordering without invisible characters. + * See: https://github.com/code-yeongyu/oh-my-openagent/issues/3238 */ export function getAgentListDisplayName(configKey: string): string { - return getAgentDisplayName(configKey) + const displayName = getAgentDisplayName(configKey) + const prefix = AGENT_LIST_SORT_PREFIXES[configKey.toLowerCase()] + + return prefix ? `${prefix}${displayName}` : displayName } const REVERSE_DISPLAY_NAMES: Record = Object.fromEntries(