diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index f1050ae7..867bc0fe 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -16,6 +16,7 @@ export namespace Agent { builtIn: z.boolean(), topP: z.number().optional(), temperature: z.number().optional(), + color: z.string().optional(), permission: z.object({ edit: Config.Permission, bash: z.record(z.string(), Config.Permission), @@ -147,7 +148,7 @@ export namespace Agent { tools: {}, builtIn: false, } - const { name, model, prompt, tools, description, temperature, top_p, mode, permission, ...extra } = value + const { name, model, prompt, tools, description, temperature, top_p, mode, permission, color, ...extra } = value item.options = { ...item.options, ...extra, @@ -167,6 +168,7 @@ export namespace Agent { if (temperature != undefined) item.temperature = temperature if (top_p != undefined) item.topP = top_p if (mode) item.mode = mode + if (color) item.color = color // just here for consistency & to prevent it from being added as an option if (name) item.name = name diff --git a/packages/opencode/src/cli/cmd/tui/context/local.tsx b/packages/opencode/src/cli/cmd/tui/context/local.tsx index fe81fd18..998739f1 100644 --- a/packages/opencode/src/cli/cmd/tui/context/local.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/local.tsx @@ -90,6 +90,8 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ }) }, color(name: string) { + const agent = agents().find((x) => x.name === name) + if (agent?.color) return agent.color const index = agents().findIndex((x) => x.name === name) return colors()[index % colors().length] }, diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index eaac1dd4..8823c866 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -355,6 +355,11 @@ export namespace Config { disable: z.boolean().optional(), description: z.string().optional().describe("Description of when to use the agent"), mode: z.union([z.literal("subagent"), z.literal("primary"), z.literal("all")]).optional(), + color: z + .string() + .regex(/^#[0-9a-fA-F]{6}$/, "Invalid hex color format") + .optional() + .describe("Hex color code for the agent (e.g., #FF5733)"), permission: z .object({ edit: Permission.optional(), diff --git a/packages/opencode/src/util/color.ts b/packages/opencode/src/util/color.ts new file mode 100644 index 00000000..b96deaec --- /dev/null +++ b/packages/opencode/src/util/color.ts @@ -0,0 +1,19 @@ +export namespace Color { + export function isValidHex(hex?: string): hex is string { + if (!hex) return false + return /^#[0-9a-fA-F]{6}$/.test(hex) + } + + export function hexToRgb(hex: string): { r: number; g: number; b: number } { + const r = parseInt(hex.slice(1, 3), 16) + const g = parseInt(hex.slice(3, 5), 16) + const b = parseInt(hex.slice(5, 7), 16) + return { r, g, b } + } + + export function hexToAnsiBold(hex?: string): string | undefined { + if (!isValidHex(hex)) return undefined + const { r, g, b } = hexToRgb(hex) + return `\x1b[38;2;${r};${g};${b}m\x1b[1m` + } +} diff --git a/packages/opencode/test/config/agent-color.test.ts b/packages/opencode/test/config/agent-color.test.ts new file mode 100644 index 00000000..a2c37429 --- /dev/null +++ b/packages/opencode/test/config/agent-color.test.ts @@ -0,0 +1,66 @@ +import { test, expect } from "bun:test" +import path from "path" +import { tmpdir } from "../fixture/fixture" +import { Instance } from "../../src/project/instance" +import { Config } from "../../src/config/config" +import { Agent as AgentSvc } from "../../src/agent/agent" +import { Color } from "../../src/util/color" + +test("agent color parsed from project config", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + agent: { + build: { color: "#FFA500" }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const cfg = await Config.get() + expect(cfg.agent?.["build"]?.color).toBe("#FFA500") + }, + }) +}) + +test("Agent.get includes color from config", async () => { + await using tmp = await tmpdir({ + init: async (dir) => { + await Bun.write( + path.join(dir, "opencode.json"), + JSON.stringify({ + $schema: "https://opencode.ai/config.json", + agent: { + plan: { color: "#A855F7" }, + }, + }), + ) + }, + }) + await Instance.provide({ + directory: tmp.path, + fn: async () => { + const plan = await AgentSvc.get("plan") + expect(plan?.color).toBe("#A855F7") + }, + }) +}) + +test("Color.hexToAnsiBold converts valid hex to ANSI", () => { + const result = Color.hexToAnsiBold("#FFA500") + expect(result).toBe("\x1b[38;2;255;165;0m\x1b[1m") +}) + +test("Color.hexToAnsiBold returns undefined for invalid hex", () => { + expect(Color.hexToAnsiBold(undefined)).toBeUndefined() + expect(Color.hexToAnsiBold("")).toBeUndefined() + expect(Color.hexToAnsiBold("#FFF")).toBeUndefined() + expect(Color.hexToAnsiBold("FFA500")).toBeUndefined() + expect(Color.hexToAnsiBold("#GGGGGG")).toBeUndefined() +}) diff --git a/packages/sdk/go/agent.go b/packages/sdk/go/agent.go index 3413b566..d1bb36e3 100644 --- a/packages/sdk/go/agent.go +++ b/packages/sdk/go/agent.go @@ -49,6 +49,7 @@ type Agent struct { Options map[string]interface{} `json:"options,required"` Permission AgentPermission `json:"permission,required"` Tools map[string]bool `json:"tools,required"` + Color string `json:"color"` Description string `json:"description"` Model AgentModel `json:"model"` Prompt string `json:"prompt"` @@ -65,6 +66,7 @@ type agentJSON struct { Options apijson.Field Permission apijson.Field Tools apijson.Field + Color apijson.Field Description apijson.Field Model apijson.Field Prompt apijson.Field diff --git a/packages/sdk/js/src/gen/types.gen.ts b/packages/sdk/js/src/gen/types.gen.ts index 19f7a3a7..3e64fc9a 100644 --- a/packages/sdk/js/src/gen/types.gen.ts +++ b/packages/sdk/js/src/gen/types.gen.ts @@ -190,6 +190,10 @@ export type AgentConfig = { */ description?: string mode?: "subagent" | "primary" | "all" + /** + * Hex color code for the agent (e.g., #FF5733) + */ + color?: string permission?: { edit?: "ask" | "allow" | "deny" bash?: @@ -1043,6 +1047,7 @@ export type Agent = { builtIn: boolean topP?: number temperature?: number + color?: string permission: { edit: "ask" | "allow" | "deny" bash: {