Explorar o código

feat(config): add agent colors

Erman HAVUÇ hai 3 semanas
pai
achega
68c5789999

+ 37 - 0
docs/configuration.md

@@ -116,6 +116,7 @@ Presets can also be switched at runtime without restarting using the `/preset` c
 | `presets.<name>.<agent>.temperature` | number | - | Optional temperature (0–2); when omitted, OpenCode chooses its default |
 | `presets.<name>.<agent>.variant` | string | - | Reasoning effort: `"low"`, `"medium"`, `"high"`, or `"max"` (provider-specific) |
 | `presets.<name>.<agent>.displayName` | string | - | Custom user-facing alias for the agent (e.g. `"advisor"` for `oracle`) |
+| `presets.<name>.<agent>.color` | string | built-in agent default | Agent display color as `#RRGGBB` or a theme color: `primary`, `secondary`, `accent`, `success`, `warning`, `error`, or `info` |
 | `presets.<name>.<agent>.skills` | string[] | - | Skills the agent can use (`"*"`, `"!item"`, explicit list) |
 | `presets.<name>.<agent>.mcps` | string[] | - | MCPs the agent can use (`"*"`, `"!item"`, explicit list) |
 | `presets.<name>.<agent>.options` | object | - | Provider-specific model options passed to the AI SDK (e.g., `textVerbosity`, `thinking` budget) |
@@ -124,6 +125,7 @@ Presets can also be switched at runtime without restarting using the `/preset` c
 | `agents.<customAgent>.orchestratorPrompt` | string | - | Exact `@agent` block injected into the orchestrator prompt; must start with `@<agent-name>` |
 | `agents.<agent>.permission` | object \| string | - | Tool-level permission rules enforced by the SDK. See [Agent Permissions](#agent-permissions) |
 | `agents.<agent>.displayName` | string | - | Custom user-facing alias for the agent in the active config |
+| `agents.<agent>.color` | string | built-in agent default | Agent display color as `#RRGGBB` or a theme color: `primary`, `secondary`, `accent`, `success`, `warning`, `error`, or `info` |
 | `agents.<agent>.description` | string | generated | Description shown to OpenCode and the orchestrator; defaults to `Custom subagent '<name>'` for custom agents |
 | `acpAgents.<name>.command` | string | - | Command for an external ACP-compatible agent; creates a wrapper subagent named `<name>` See [ACP-connected agents](#acp-connected-agents). |
 | `acpAgents.<name>.args` | string[] | `[]` | Arguments for the ACP agent command See [ACP-connected agents](#acp-connected-agents). |
@@ -402,6 +404,41 @@ Model selection follows these rules:
 
 The setting works in both root `agents` overrides and preset agent overrides.
 
+### Agent Colors
+
+Built-in agents use theme-aware colors by default:
+
+| Agent | Default color |
+|-------|---------------|
+| `orchestrator` | `primary` |
+| `explorer` | `info` |
+| `librarian` | `secondary` |
+| `oracle` | `accent` |
+| `designer` | `success` |
+| `fixer` | `warning` |
+| `observer` | `info` |
+| `council` and councillors | `accent` |
+
+Override a built-in color or color a custom agent with a six-digit hex value
+or an OpenCode theme color:
+
+```jsonc
+{
+  "agents": {
+    "oracle": { "color": "#FF5733" },
+    "reviewer": {
+      "model": "openai/gpt-5.6",
+      "color": "info"
+    }
+  }
+}
+```
+
+Theme colors adapt to the active OpenCode theme. Dynamic councillors inherit
+the configured `council` color unless `agents.councillor.color` overrides it.
+Custom agents have no default color. `color` works in top-level `agents`
+overrides and inside `presets`.
+
 ### Per-preset agent configuration
 
 To get per-preset behavior for any agent, built-in (`council`, `oracle`,

+ 42 - 0
oh-my-opencode-slim.schema.json

@@ -111,6 +111,27 @@
               "type": "string",
               "minLength": 1
             },
+            "color": {
+              "description": "Agent display color as #RRGGBB or an OpenCode theme color",
+              "anyOf": [
+                {
+                  "type": "string",
+                  "pattern": "^#[0-9a-fA-F]{6}$"
+                },
+                {
+                  "type": "string",
+                  "enum": [
+                    "primary",
+                    "secondary",
+                    "accent",
+                    "success",
+                    "warning",
+                    "error",
+                    "info"
+                  ]
+                }
+              ]
+            },
             "description": {
               "type": "string",
               "minLength": 1
@@ -557,6 +578,27 @@
             "type": "string",
             "minLength": 1
           },
+          "color": {
+            "description": "Agent display color as #RRGGBB or an OpenCode theme color",
+            "anyOf": [
+              {
+                "type": "string",
+                "pattern": "^#[0-9a-fA-F]{6}$"
+              },
+              {
+                "type": "string",
+                "enum": [
+                  "primary",
+                  "secondary",
+                  "accent",
+                  "success",
+                  "warning",
+                  "error",
+                  "info"
+                ]
+              }
+            ]
+          },
           "description": {
             "type": "string",
             "minLength": 1

+ 1 - 0
src/agents/codemap.md

@@ -26,6 +26,7 @@ Each agent is a **prompt-driven specialist** with a factory function that create
 
 - **Default prompts**: Each agent factory has a base prompt defined in its file (e.g., `explorer.ts`, `oracle.ts`)
 - **User overrides**: From `~/.config/opencode/oh-my-opencode-slim.json` via `loadAgentPrompt()`
+- **Agent colors**: Built-in theme-aware defaults with per-agent hex or theme-color overrides
 - **Permission wildcards**: Applied via `applyDefaultPermissions()` in `index.ts`
 - **Model resolution**: Supports string models, explicit `inheritModelFrom` policies, and priority-ordered arrays (`_modelArray`) for runtime fallback
 - **Skill permissions**: Per-agent MCP and tool access controlled via `getSkillPermissionsForAgent()`

+ 69 - 0
src/agents/index.test.ts

@@ -3,6 +3,7 @@ import type { PluginConfig } from '../config';
 import {
   AgentOverrideConfigSchema,
   CouncilConfigSchema,
+  DEFAULT_AGENT_COLORS,
   DEFAULT_DISABLED_AGENTS,
   DEFAULT_MODELS,
   PluginConfigSchema,
@@ -903,6 +904,74 @@ describe('getAgentConfigs', () => {
     expect(configs.explorer.temperature).toBe(0.5);
     expect(configs.fixer.temperature).toBe(0);
   });
+
+  test('applies default colors to every built-in agent', () => {
+    const configs = getAgentConfigs(
+      runtimeFor({ disabled_agents: [], council: councilConfig() }),
+    );
+
+    for (const [name, color] of Object.entries(DEFAULT_AGENT_COLORS)) {
+      expect(configs[name]?.color).toBe(color);
+    }
+    expect(configs['councillor-alpha']?.color).toBe(
+      DEFAULT_AGENT_COLORS.councillor,
+    );
+  });
+
+  test('configured colors override defaults and flow to custom agents', () => {
+    const configs = getAgentConfigs(
+      runtimeFor({
+        agents: {
+          oracle: { color: '#A1b2C3' },
+          reviewer: {
+            model: 'openai/gpt-5.6',
+            color: 'warning',
+          },
+        },
+      }),
+    );
+
+    expect(configs.oracle.color).toBe('#A1b2C3');
+    expect(configs.reviewer.color).toBe('warning');
+  });
+
+  test('dynamic councillors inherit configured council color', () => {
+    const configs = getAgentConfigs(
+      runtimeFor({
+        council: councilConfig(),
+        agents: { council: { color: '#123ABC' } },
+      }),
+    );
+
+    expect(configs.council.color).toBe('#123ABC');
+    expect(configs['councillor-alpha']?.color).toBe('#123ABC');
+  });
+});
+
+describe('AgentOverrideConfigSchema color validation', () => {
+  test('accepts OpenCode theme colors and six-digit hex colors', () => {
+    for (const color of [
+      'primary',
+      'secondary',
+      'accent',
+      'success',
+      'warning',
+      'error',
+      'info',
+      '#FF5733',
+      '#a1B2c3',
+    ]) {
+      expect(AgentOverrideConfigSchema.safeParse({ color }).success).toBe(true);
+    }
+  });
+
+  test('rejects unsupported color formats', () => {
+    for (const color of ['red', '#FFF', '#GG5733', 'FF5733']) {
+      expect(AgentOverrideConfigSchema.safeParse({ color }).success).toBe(
+        false,
+      );
+    }
+  });
 });
 
 describe('council agent model resolution', () => {

+ 9 - 0
src/agents/index.ts

@@ -4,6 +4,7 @@ import {
   AGENT_ALIASES,
   type AgentOverrideConfig,
   ALL_AGENT_NAMES,
+  DEFAULT_AGENT_COLORS,
   DEFAULT_DISABLED_AGENTS,
   DEFAULT_MODELS,
   loadAgentPrompt,
@@ -180,6 +181,7 @@ function applyOverrides(
   if (override.variant) agent.config.variant = override.variant;
   if (override.temperature !== undefined)
     agent.config.temperature = override.temperature;
+  if (override.color) agent.config.color = override.color;
   if (override.options) {
     agent.config.options = {
       ...agent.config.options,
@@ -476,6 +478,7 @@ export function createAgents(
     .map(([name, factory]) => {
       // Get base agent definition using the subagent factory with undefined prompts
       const agent = factory(getModelForAgent(name), undefined, undefined);
+      agent.config.color ??= DEFAULT_AGENT_COLORS[name];
 
       const customPrompts = loadAgentPrompt(name, {
         preset: runtime.preset,
@@ -600,8 +603,13 @@ export function createAgents(
   // Build dynamic councillor agents from council config (flatten mode).
   // Each councillor becomes a dispatchable subagent with its own model,
   // so the orchestrator can task() them with native panes at depth 1.
+  const councillorColor =
+    getOverrideFromAgents(mergedAgents, 'councillor')?.color ??
+    getOverrideFromAgents(mergedAgents, 'council')?.color ??
+    DEFAULT_AGENT_COLORS.councillor;
   const councillorAgents = buildCouncillorAgents(runtime, disabled).map(
     (agent) => {
+      agent.config.color ??= councillorColor;
       applyDefaultPermissions(agent, undefined, runtime.disabledSkills);
       return agent;
     },
@@ -638,6 +646,7 @@ export function createAgents(
     !runtime.disabledTools.includes('wait_for_user'),
     runtime.backgroundJobs.orchestratorWake.enabled,
   );
+  orchestrator.config.color ??= DEFAULT_AGENT_COLORS.orchestrator;
 
   const inlineOrchestratorPrompt = orchestratorOverride?.prompt;
   const defaultOrchestratorPrompt = orchestrator.config.prompt ?? '';

+ 1 - 0
src/config/codemap.md

@@ -179,6 +179,7 @@ This allows consumers to import directly from `src/config` rather than individua
 - `orchestratorPrompt`: Custom orchestrator prompt override
 - `options`: Provider-specific model options
 - `displayName`: Custom display name for the agent
+- `color`: Agent display color as a six-digit hex value or OpenCode theme color
 
 ### CouncilConfig
 - `presets`: Named council presets (map of presetName → CouncillorConfig[])

+ 25 - 0
src/config/constants.ts

@@ -20,6 +20,31 @@ export const ALL_AGENT_NAMES = ['orchestrator', ...SUBAGENT_NAMES] as const;
 // Agent name type (for use in DEFAULT_MODELS)
 export type AgentName = (typeof ALL_AGENT_NAMES)[number];
 
+export const AGENT_THEME_COLORS = [
+  'primary',
+  'secondary',
+  'accent',
+  'success',
+  'warning',
+  'error',
+  'info',
+] as const;
+
+export type AgentThemeColor = (typeof AGENT_THEME_COLORS)[number];
+
+/** Theme-aware colors used for built-in agents unless users override them. */
+export const DEFAULT_AGENT_COLORS: Record<AgentName, AgentThemeColor> = {
+  orchestrator: 'primary',
+  explorer: 'info',
+  librarian: 'secondary',
+  oracle: 'accent',
+  designer: 'success',
+  fixer: 'warning',
+  observer: 'info',
+  council: 'accent',
+  councillor: 'accent',
+};
+
 /** Agents that cannot be disabled even if listed in disabled_agents config. */
 export const PROTECTED_AGENTS = new Set(['orchestrator', 'councillor']);
 

+ 14 - 1
src/config/schema.ts

@@ -1,5 +1,8 @@
 import { z } from 'zod';
-import { DEFAULT_MAX_RETAINED_SNAPSHOTS } from './constants';
+import {
+  AGENT_THEME_COLORS,
+  DEFAULT_MAX_RETAINED_SNAPSHOTS,
+} from './constants';
 import { CouncilConfigSchema } from './council-schema';
 
 export const ProviderModelIdSchema = z
@@ -49,6 +52,13 @@ export const PermissionConfigSchema = z.union([
   PermissionObjectSchema,
 ]);
 
+export const AgentColorSchema = z.union([
+  z
+    .string()
+    .regex(/^#[0-9a-fA-F]{6}$/, 'Expected a six-digit hex color (#RRGGBB)'),
+  z.enum(AGENT_THEME_COLORS),
+]);
+
 // Agent override configuration (distinct from SDK's AgentConfig)
 export const ModelInheritanceSourceSchema = z.enum(['session', 'orchestrator']);
 export type ModelInheritanceSource = z.infer<
@@ -82,6 +92,9 @@ export const AgentOverrideConfigSchema = z
     orchestratorPrompt: z.string().min(1).optional(),
     options: z.record(z.string(), z.unknown()).optional(), // provider-specific model options (e.g., textVerbosity, thinking budget)
     displayName: z.string().min(1).optional(),
+    color: AgentColorSchema.optional().describe(
+      'Agent display color as #RRGGBB or an OpenCode theme color',
+    ),
     description: z.string().min(1).optional(),
     permission: PermissionConfigSchema.optional(), // tool-level permission rules enforced by the SDK
   })