feat: agent color cfg (#4226)

Co-authored-by: 0xrin <0xrin1@protonmail.com>
Co-authored-by: GitHub Action <action@github.com>
This commit is contained in:
Aiden Cline
2025-11-11 16:32:44 -08:00
committed by GitHub
parent 834a2c09d5
commit 0b86adbe99
7 changed files with 102 additions and 1 deletions

View File

@@ -16,6 +16,7 @@ export namespace Agent {
builtIn: z.boolean(), builtIn: z.boolean(),
topP: z.number().optional(), topP: z.number().optional(),
temperature: z.number().optional(), temperature: z.number().optional(),
color: z.string().optional(),
permission: z.object({ permission: z.object({
edit: Config.Permission, edit: Config.Permission,
bash: z.record(z.string(), Config.Permission), bash: z.record(z.string(), Config.Permission),
@@ -147,7 +148,7 @@ export namespace Agent {
tools: {}, tools: {},
builtIn: false, 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 = {
...item.options, ...item.options,
...extra, ...extra,
@@ -167,6 +168,7 @@ export namespace Agent {
if (temperature != undefined) item.temperature = temperature if (temperature != undefined) item.temperature = temperature
if (top_p != undefined) item.topP = top_p if (top_p != undefined) item.topP = top_p
if (mode) item.mode = mode if (mode) item.mode = mode
if (color) item.color = color
// just here for consistency & to prevent it from being added as an option // just here for consistency & to prevent it from being added as an option
if (name) item.name = name if (name) item.name = name

View File

@@ -90,6 +90,8 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({
}) })
}, },
color(name: string) { 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) const index = agents().findIndex((x) => x.name === name)
return colors()[index % colors().length] return colors()[index % colors().length]
}, },

View File

@@ -355,6 +355,11 @@ export namespace Config {
disable: z.boolean().optional(), disable: z.boolean().optional(),
description: z.string().optional().describe("Description of when to use the agent"), 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(), 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 permission: z
.object({ .object({
edit: Permission.optional(), edit: Permission.optional(),

View File

@@ -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`
}
}

View File

@@ -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()
})

View File

@@ -49,6 +49,7 @@ type Agent struct {
Options map[string]interface{} `json:"options,required"` Options map[string]interface{} `json:"options,required"`
Permission AgentPermission `json:"permission,required"` Permission AgentPermission `json:"permission,required"`
Tools map[string]bool `json:"tools,required"` Tools map[string]bool `json:"tools,required"`
Color string `json:"color"`
Description string `json:"description"` Description string `json:"description"`
Model AgentModel `json:"model"` Model AgentModel `json:"model"`
Prompt string `json:"prompt"` Prompt string `json:"prompt"`
@@ -65,6 +66,7 @@ type agentJSON struct {
Options apijson.Field Options apijson.Field
Permission apijson.Field Permission apijson.Field
Tools apijson.Field Tools apijson.Field
Color apijson.Field
Description apijson.Field Description apijson.Field
Model apijson.Field Model apijson.Field
Prompt apijson.Field Prompt apijson.Field

View File

@@ -190,6 +190,10 @@ export type AgentConfig = {
*/ */
description?: string description?: string
mode?: "subagent" | "primary" | "all" mode?: "subagent" | "primary" | "all"
/**
* Hex color code for the agent (e.g., #FF5733)
*/
color?: string
permission?: { permission?: {
edit?: "ask" | "allow" | "deny" edit?: "ask" | "allow" | "deny"
bash?: bash?:
@@ -1043,6 +1047,7 @@ export type Agent = {
builtIn: boolean builtIn: boolean
topP?: number topP?: number
temperature?: number temperature?: number
color?: string
permission: { permission: {
edit: "ask" | "allow" | "deny" edit: "ask" | "allow" | "deny"
bash: { bash: {