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:
@@ -6,7 +6,6 @@ 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,
|
||||
|
||||
@@ -9,7 +9,6 @@ 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 }
|
||||
@@ -403,7 +402,10 @@ describe("createChatMessageHandler - TUI variant passthrough", () => {
|
||||
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
|
||||
setMainSession("test-session")
|
||||
setSessionModel("test-session", { providerID: "openai", modelID: "gpt-5.4" })
|
||||
@@ -416,7 +418,7 @@ describe("createChatMessageHandler - TUI variant passthrough", () => {
|
||||
},
|
||||
})
|
||||
const handler = createChatMessageHandler(args)
|
||||
const input = createMockInput(getAgentListDisplayName("prometheus"))
|
||||
const input = createMockInput("\u200B\u200B\u200BPrometheus - Plan Builder")
|
||||
const output = createMockOutput()
|
||||
|
||||
//#when
|
||||
|
||||
@@ -183,30 +183,46 @@ describe("getAgentConfigKey", () => {
|
||||
expect(getAgentConfigKey("Sisyphus-Junior")).toBe("sisyphus-junior")
|
||||
})
|
||||
|
||||
it("resolves atlas even when the UI ordering prefix is present", () => {
|
||||
expect(getAgentConfigKey(getAgentListDisplayName("atlas"))).toBe("atlas")
|
||||
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")
|
||||
})
|
||||
})
|
||||
|
||||
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")
|
||||
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")
|
||||
})
|
||||
|
||||
it("keeps non-core agents unprefixed for list display", () => {
|
||||
it("matches getAgentDisplayName for unknown agents", () => {
|
||||
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 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")
|
||||
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")
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -26,13 +26,23 @@ export const AGENT_DISPLAY_NAMES: Record<string, string> = {
|
||||
"council-member": "council-member",
|
||||
}
|
||||
|
||||
const AGENT_LIST_SORT_PREFIXES: Record<string, string> = {
|
||||
sisyphus: "\u200B",
|
||||
hephaestus: "\u200B\u200B",
|
||||
prometheus: "\u200B\u200B\u200B",
|
||||
atlas: "\u200B\u200B\u200B\u200B",
|
||||
}
|
||||
|
||||
/**
|
||||
* 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+/, "")
|
||||
}
|
||||
@@ -58,17 +68,20 @@ export function getAgentDisplayName(configKey: string): string {
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
* @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 {
|
||||
const displayName = getAgentDisplayName(configKey)
|
||||
const prefix = AGENT_LIST_SORT_PREFIXES[configKey.toLowerCase()]
|
||||
|
||||
return prefix ? `${prefix}${displayName}` : displayName
|
||||
return getAgentDisplayName(configKey)
|
||||
}
|
||||
|
||||
const REVERSE_DISPLAY_NAMES: Record<string, string> = Object.fromEntries(
|
||||
|
||||
Reference in New Issue
Block a user