Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions src/cli/run/output-renderer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { describe, expect, mock, spyOn, test } from "bun:test"
import { renderAgentHeader } from "./output-renderer"

describe("renderAgentHeader", () => {
test("strips zero-width characters before printing the agent label", () => {
const writeSpy = spyOn(process.stdout, "write").mockImplementation(mock(() => true))

try {
renderAgentHeader("\u200B\u200BHephaestus - Deep Agent", "gpt-5.4", null, {
"Hephaestus - Deep Agent": "#ff00ff",
})

const output = writeSpy.mock.calls.map(([chunk]) => String(chunk)).join("")
expect(output).toContain("Hephaestus - Deep Agent")
expect(output).not.toContain("\u200B\u200BHephaestus - Deep Agent")
} finally {
writeSpy.mockRestore()
}
})
})
4 changes: 3 additions & 1 deletion src/cli/run/output-renderer.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import pc from "picocolors"
import { stripInvisibleAgentCharacters } from "../../shared/agent-display-names"

export function renderAgentHeader(
agent: string | null,
Expand All @@ -8,8 +9,9 @@ export function renderAgentHeader(
): void {
if (!agent && !model) return

const cleanAgent = agent ? stripInvisibleAgentCharacters(agent) : null
const agentLabel = agent
? pc.bold(colorizeWithProfileColor(agent, agentColorsByName[agent]))
? pc.bold(colorizeWithProfileColor(cleanAgent ?? agent, agentColorsByName[agent] ?? (cleanAgent ? agentColorsByName[cleanAgent] : undefined)))
: ""
const modelBase = model ?? ""
const variantSuffix = variant ? ` (${variant})` : ""
Expand Down
4 changes: 2 additions & 2 deletions src/plugin-handlers/agent-key-remapper.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ describe("remapAgentKeysToDisplayNames", () => {
expect(result["sisyphus"]).toBeUndefined()
})

it("returns runtime core agent list names in canonical order", () => {
it("returns clean core agent display keys in canonical order", () => {
// given
const result = remapAgentKeysToDisplayNames({
atlas: {},
Expand Down Expand Up @@ -116,7 +116,7 @@ describe("remapAgentKeysToDisplayNames", () => {
// when remapping
const result = remapAgentKeysToDisplayNames(agents)

// then keys and names both use the same runtime-facing list names
// then keys stay human-readable while runtime names keep list ordering
expect(Object.keys(result).slice(0, 4)).toEqual([
getAgentListDisplayName("sisyphus"),
getAgentListDisplayName("hephaestus"),
Expand Down
4 changes: 2 additions & 2 deletions src/plugin-handlers/command-config-handler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ describe("applyCommandConfig", () => {
expect(commandConfig["agents-global-skill"]?.description).toContain("Agents global skill");
});

test("normalizes Atlas command agents to the runtime list name used by opencode command routing", async () => {
test("normalizes Atlas command agents to the clean display name used by opencode command routing", async () => {
// given
loadBuiltinCommandsSpy.mockReturnValue({
"start-work": {
Expand All @@ -133,7 +133,7 @@ describe("applyCommandConfig", () => {
expect(commandConfig["start-work"]?.agent).toBe(getAgentListDisplayName("atlas"));
});

test("normalizes legacy display-name command agents to the runtime list name", async () => {
test("normalizes legacy display-name command agents to the clean display name", async () => {
// given
loadBuiltinCommandsSpy.mockReturnValue({
"start-work": {
Expand Down
17 changes: 11 additions & 6 deletions src/shared/agent-display-names.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, it, expect } from "bun:test"
import { AGENT_DISPLAY_NAMES, getAgentConfigKey, getAgentDisplayName, getAgentListDisplayName, normalizeAgentForPrompt, normalizeAgentForPromptKey } from "./agent-display-names"
import { AGENT_DISPLAY_NAMES, getAgentConfigKey, getAgentDisplayName, getAgentListDisplayName, getAgentRuntimeName, normalizeAgentForPrompt, normalizeAgentForPromptKey } from "./agent-display-names"

describe("getAgentDisplayName", () => {
it("returns display name for lowercase config key (new format)", () => {
Expand Down Expand Up @@ -194,16 +194,21 @@ describe("getAgentConfigKey", () => {
})

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("returns clean display names for the core agent list", () => {
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", () => {
expect(getAgentListDisplayName("oracle")).toBe("oracle")
})

it("keeps runtime names identical to clean display names", () => {
expect(getAgentRuntimeName("sisyphus")).toBe("Sisyphus - Ultraworker")
expect(getAgentRuntimeName("hephaestus")).toBe("Hephaestus - Deep Agent")
})
})

describe("normalizeAgentForPrompt", () => {
Expand Down
28 changes: 9 additions & 19 deletions src/shared/agent-display-names.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,7 @@ 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",
}

const INVISIBLE_AGENT_CHARACTERS_REGEX = /[\u200B\u200C\u200D\uFEFF]/g
const INVISIBLE_AGENT_CHARACTERS_REGEX = /\u200B|\u200C|\u200D|\uFEFF/g

export function stripInvisibleAgentCharacters(agentName: string): string {
return agentName.replace(INVISIBLE_AGENT_CHARACTERS_REGEX, "")
Expand All @@ -44,10 +37,7 @@ export function stripAgentListSortPrefix(agentName: string): string {
}

export function getAgentRuntimeName(configKey: string): string {
const displayName = getAgentDisplayName(configKey)
const prefix = AGENT_LIST_SORT_PREFIXES[configKey.toLowerCase()]

return prefix ? `${prefix}${displayName}` : displayName
return getAgentDisplayName(configKey)
}

/**
Expand All @@ -56,25 +46,25 @@ export function getAgentRuntimeName(configKey: string): string {
* 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
}

/**
* Runtime-facing agent name used for OpenCode list ordering.
* List-facing agent display name.
*
* This must stay human-readable. Runtime sort prefixes belong only in the
* agent `name` field via getAgentRuntimeName().
Comment on lines +63 to +64
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Apr 11, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: Update this comment to match the new no-prefix behavior; it still says getAgentRuntimeName() adds runtime sort prefixes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At src/shared/agent-display-names.ts, line 63:

<comment>Update this comment to match the new no-prefix behavior; it still says `getAgentRuntimeName()` adds runtime sort prefixes.</comment>

<file context>
@@ -56,25 +46,25 @@ export function getAgentRuntimeName(configKey: string): string {
- * Runtime-facing agent name used for OpenCode list ordering.
+ * List-facing agent display name.
+ *
+ * This must stay human-readable. Runtime sort prefixes belong only in the
+ * agent `name` field via getAgentRuntimeName().
  */
</file context>
Suggested change
* This must stay human-readable. Runtime sort prefixes belong only in the
* agent `name` field via getAgentRuntimeName().
* This must stay human-readable.
* Runtime and list display names are intentionally identical here.
Fix with Cubic

*/
export function getAgentListDisplayName(configKey: string): string {
return getAgentRuntimeName(configKey)
return getAgentDisplayName(configKey)
}

const REVERSE_DISPLAY_NAMES: Record<string, string> = Object.fromEntries(
Expand Down
Loading