From b6e14b5a7415ad5c92f72a295339fd0564cbe8b1 Mon Sep 17 00:00:00 2001 From: James Long Date: Wed, 22 Jul 2026 17:56:22 -0400 Subject: [PATCH] refactor(tui): finish V2 theme migration (#38383) --- .../client/src/promise/generated/types.ts | 4 +--- .../test/fixtures/opencode-v2-openapi.json | 22 +++---------------- packages/core/src/config/agent.ts | 5 +---- packages/core/src/v1/config/agent.ts | 7 ++---- packages/core/test/config/agent.test.ts | 10 ++++++--- packages/core/test/config/config.test.ts | 4 ++-- packages/docs/agents.mdx | 7 +++--- packages/docs/openapi.json | 22 +++---------------- packages/schema/src/agent.ts | 7 +++--- packages/schema/test/contract-hygiene.test.ts | 6 +++++ packages/tui/src/component/bg-pulse.tsx | 8 +++---- packages/tui/src/context/local.tsx | 19 ++-------------- packages/tui/src/context/theme.tsx | 15 +++---------- packages/www/content/docs/(docs)/agents.mdx | 7 +++--- packages/www/public/openapi.json | 22 +++---------------- 15 files changed, 46 insertions(+), 119 deletions(-) diff --git a/packages/client/src/promise/generated/types.ts b/packages/client/src/promise/generated/types.ts index 6c869b0e2e..966ec22cc0 100644 --- a/packages/client/src/promise/generated/types.ts +++ b/packages/client/src/promise/generated/types.ts @@ -8,8 +8,6 @@ export type ModelRef = { id: string; providerID: string; variant?: string } export type ProviderSettings = { [x: string]: JsonValue } -export type AgentColor = string | "primary" | "secondary" | "accent" | "success" | "warning" | "error" | "info" - export type PermissionV2Effect = "allow" | "deny" | "ask" export type PluginInfo = { id: string } @@ -2005,7 +2003,7 @@ export type AgentInfo = { description?: string mode: "subagent" | "primary" | "all" hidden: boolean - color?: AgentColor + color?: string steps?: number permissions: PermissionV2Ruleset } diff --git a/packages/codemode/test/fixtures/opencode-v2-openapi.json b/packages/codemode/test/fixtures/opencode-v2-openapi.json index 543a418173..d430f5d21d 100644 --- a/packages/codemode/test/fixtures/opencode-v2-openapi.json +++ b/packages/codemode/test/fixtures/opencode-v2-openapi.json @@ -10561,26 +10561,10 @@ "additionalProperties": false }, "Agent.Color": { - "anyOf": [ + "type": "string", + "allOf": [ { - "type": "string", - "allOf": [ - { - "pattern": "^#[0-9a-fA-F]{6}$" - } - ] - }, - { - "type": "string", - "enum": [ - "primary", - "secondary", - "accent", - "success", - "warning", - "error", - "info" - ] + "pattern": "^#[0-9a-fA-F]{6}$" } ] }, diff --git a/packages/core/src/config/agent.ts b/packages/core/src/config/agent.ts index fc5edf5119..075feea250 100644 --- a/packages/core/src/config/agent.ts +++ b/packages/core/src/config/agent.ts @@ -6,10 +6,7 @@ import { ConfigProvider } from "./provider" import { ConfigModel } from "./model" import { PositiveInt } from "../schema" -export const Color = Schema.Union([ - Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)), - Schema.Literals(["primary", "secondary", "accent", "success", "warning", "error", "info"]), -]) +export const Color = Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)) export class Info extends Schema.Class("ConfigV2.Agent")({ model: ConfigModel.Selection.pipe(Schema.optional), diff --git a/packages/core/src/v1/config/agent.ts b/packages/core/src/v1/config/agent.ts index b220bd7ef8..09838a9196 100644 --- a/packages/core/src/v1/config/agent.ts +++ b/packages/core/src/v1/config/agent.ts @@ -4,10 +4,7 @@ import { Schema, SchemaGetter } from "effect" import { PositiveInt } from "../../schema" import { ConfigPermissionV1 } from "./permission" -const Color = Schema.Union([ - Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)), - Schema.Literals(["primary", "secondary", "accent", "success", "warning", "error", "info"]), -]) +const Color = Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)) const AgentSchema = Schema.StructWithRest( Schema.Struct({ @@ -29,7 +26,7 @@ const AgentSchema = Schema.StructWithRest( }), options: Schema.optional(Schema.Record(Schema.String, Schema.Any)), color: Schema.optional(Color).annotate({ - description: "Hex color code (e.g., #FF5733) or theme color (e.g., primary)", + description: "Hex color code (e.g., #FF5733)", }), steps: Schema.optional(PositiveInt).annotate({ description: "Maximum number of agentic iterations before forcing text-only response", diff --git a/packages/core/test/config/agent.test.ts b/packages/core/test/config/agent.test.ts index 0380c38e3a..855ea370e2 100644 --- a/packages/core/test/config/agent.test.ts +++ b/packages/core/test/config/agent.test.ts @@ -1,4 +1,4 @@ -import { describe, expect } from "bun:test" +import { describe, expect, test } from "bun:test" import fs from "fs/promises" import path from "path" import { Effect, Schema } from "effect" @@ -23,6 +23,10 @@ const defaultPermissions = [ { action: "external_directory", resource: "*", effect: "ask" }, ] satisfies PermissionV2.Ruleset +test("rejects named agent color tokens", () => { + expect(() => decode({ agents: { reviewer: { color: "warning" } } })).toThrow() +}) + describe("ConfigAgentPlugin.Plugin", () => { it.effect("matches POSIX paths against home-relative permissions", () => Effect.gen(function* () { @@ -160,7 +164,7 @@ describe("ConfigAgentPlugin.Plugin", () => { description: "Reviews changes", mode: "subagent", hidden: true, - color: "warning", + color: "#ff6b6b", steps: 12, request: { headers: { first: "one", shared: "first" }, @@ -197,7 +201,7 @@ describe("ConfigAgentPlugin.Plugin", () => { description: "Reviews changes", mode: "subagent", hidden: true, - color: "warning", + color: "#ff6b6b", steps: 12, model: { providerID: "anthropic", id: "claude-sonnet" }, }) diff --git a/packages/core/test/config/config.test.ts b/packages/core/test/config/config.test.ts index 64e08b5e79..6584cc1c94 100644 --- a/packages/core/test/config/config.test.ts +++ b/packages/core/test/config/config.test.ts @@ -738,7 +738,7 @@ describe("Config", () => { system: "Find regressions.", mode: "subagent", hidden: false, - color: "warning", + color: "#ff6b6b", steps: 12, disabled: false, permissions: [{ action: "edit", resource: "*", effect: "deny" }], @@ -824,7 +824,7 @@ describe("Config", () => { expect(reviewer?.system).toBe("Find regressions.") expect(reviewer?.mode).toBe("subagent") expect(reviewer?.hidden).toBe(false) - expect(reviewer?.color).toBe("warning") + expect(reviewer?.color).toBe("#ff6b6b") expect(reviewer?.steps).toBe(12) expect(reviewer?.disabled).toBe(false) expect(reviewer?.permissions).toEqual([{ action: "edit", resource: "*", effect: "deny" }]) diff --git a/packages/docs/agents.mdx b/packages/docs/agents.mdx index a81ae6ec3c..ca7ee80a20 100644 --- a/packages/docs/agents.mdx +++ b/packages/docs/agents.mdx @@ -89,7 +89,7 @@ becomes `system`: description: Reviews changes without modifying files mode: subagent model: anthropic/claude-sonnet-4-5#high -color: warning +color: "#ff6b6b" steps: 8 permissions: - action: edit @@ -118,7 +118,7 @@ Use the `agents` field in any [OpenCode configuration file](/config): "mode": "all", "model": "anthropic/claude-sonnet-4-5#high", "system": "Review the current changes. Report findings before any summary.", - "color": "warning", + "color": "#ff6b6b", "steps": 8, "permissions": [ { "action": "edit", "resource": "*", "effect": "deny" }, @@ -250,8 +250,7 @@ security boundary. ### `color` -Sets the agent's UI color. Use a six-digit hex color such as `#ff6b6b`, or one -of `primary`, `secondary`, `accent`, `success`, `warning`, `error`, or `info`. +Sets the agent's UI color. Use a six-digit hex color such as `#ff6b6b`. ### `disabled` diff --git a/packages/docs/openapi.json b/packages/docs/openapi.json index 543a418173..d430f5d21d 100644 --- a/packages/docs/openapi.json +++ b/packages/docs/openapi.json @@ -10561,26 +10561,10 @@ "additionalProperties": false }, "Agent.Color": { - "anyOf": [ + "type": "string", + "allOf": [ { - "type": "string", - "allOf": [ - { - "pattern": "^#[0-9a-fA-F]{6}$" - } - ] - }, - { - "type": "string", - "enum": [ - "primary", - "secondary", - "accent", - "success", - "warning", - "error", - "info" - ] + "pattern": "^#[0-9a-fA-F]{6}$" } ] }, diff --git a/packages/schema/src/agent.ts b/packages/schema/src/agent.ts index 8806a9c469..399e9e9131 100644 --- a/packages/schema/src/agent.ts +++ b/packages/schema/src/agent.ts @@ -16,10 +16,9 @@ export type ID = typeof ID.Type export const Name = Schema.String.pipe(Schema.brand("Agent.Name")) export type Name = typeof Name.Type -export const Color = Schema.Union([ - Schema.String.check(Schema.isPattern(/^#[0-9a-fA-F]{6}$/)), - Schema.Literals(["primary", "secondary", "accent", "success", "warning", "error", "info"]), -]).annotate({ identifier: "Agent.Color" }) +export const Color = Schema.String.annotate({ identifier: "Agent.Color" }).check( + Schema.isPattern(/^#[0-9a-fA-F]{6}$/), +) export type Color = typeof Color.Type export interface Info extends Schema.Schema.Type {} diff --git a/packages/schema/test/contract-hygiene.test.ts b/packages/schema/test/contract-hygiene.test.ts index c441ad88c3..c662870269 100644 --- a/packages/schema/test/contract-hygiene.test.ts +++ b/packages/schema/test/contract-hygiene.test.ts @@ -20,6 +20,12 @@ import { PersistedRevert } from "../src/session-revert.js" import { optional } from "../src/schema.js" describe("contract hygiene", () => { + test("restricts agent colors to six-digit hex values", () => { + const decode = Schema.decodeUnknownSync(Agent.Color) + expect(decode("#ff6b6b")).toBe("#ff6b6b") + expect(() => decode("warning")).toThrow() + }) + test("keeps absolute costs distinct from model rates", () => { const usd = Money.USD.make(1) const rate = Money.USDPerMillionTokens.make(1) diff --git a/packages/tui/src/component/bg-pulse.tsx b/packages/tui/src/component/bg-pulse.tsx index 2112fe4420..064cc314f9 100644 --- a/packages/tui/src/component/bg-pulse.tsx +++ b/packages/tui/src/component/bg-pulse.tsx @@ -70,7 +70,7 @@ declare module "@opentui/solid" { extend({ go_upsell_art: GoUpsellArtRenderable }) export function BgPulse() { - const { theme } = useTheme() + const { themeV2, mode } = useTheme().contextual("elevated") const renderer = useRenderer() let targetFps = renderer.targetFps let maxFps = renderer.maxFps @@ -91,9 +91,9 @@ export function BgPulse() { ) diff --git a/packages/tui/src/context/local.tsx b/packages/tui/src/context/local.tsx index 03716cdf12..98faea2f9d 100644 --- a/packages/tui/src/context/local.tsx +++ b/packages/tui/src/context/local.tsx @@ -23,16 +23,6 @@ import { useRoute } from "./route" import { useData } from "./data" import { usePermission } from "./permission" -export type LocalTheme = { - secondary: RGBA - accent: RGBA - success: RGBA - warning: RGBA - primary: RGBA - error: RGBA - info: RGBA -} - export function parseModel(model: string) { const [providerID, ...rest] = model.split("/") return { @@ -60,7 +50,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ const data = useData() const client = useClient() const toast = useToast() - const { theme, themeV2, mode } = useTheme() + const { themeV2, mode } = useTheme() const route = useRoute() const paths = useTuiPaths() const args = useArgs() @@ -128,12 +118,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ if (index === -1) return colors()[0] const agent = visibleAgents()[index] - if (agent?.color) { - const color = agent.color - if (color.startsWith("#")) return RGBA.fromHex(color) - // already validated by config, just satisfying TS here - return theme[color as keyof typeof theme] as RGBA - } + if (agent?.color) return RGBA.fromHex(agent.color) return colors()[index % colors().length] }, } diff --git a/packages/tui/src/context/theme.tsx b/packages/tui/src/context/theme.tsx index 88f91cb50d..aff085e969 100644 --- a/packages/tui/src/context/theme.tsx +++ b/packages/tui/src/context/theme.tsx @@ -71,7 +71,6 @@ type State = { type ContextName = "elevated" | "overlay" type ThemeService = { - theme: Theme themeV2: ComponentTheme contextual(context: ContextName): ThemeService readonly selected: string @@ -280,7 +279,7 @@ const themeContext = createSimpleContext({ if (supported.includes(store.mode)) return store.mode return supported[0] ?? store.mode } - const values = createMemo(() => resolveTheme(source(), mode())) + const legacySyntaxTheme = createMemo(() => resolveTheme(source(), mode())) const valuesV2 = createMemo(() => resolveThemeFile(file(), mode(), sourceName())) valuesV2() themePerformance.set("Init", `${(performance.now() - initStarted).toFixed(2)} ms`) @@ -298,21 +297,13 @@ const themeContext = createSimpleContext({ }, mode), } - createEffect(() => renderer.setBackgroundColor(values().background)) + createEffect(() => renderer.setBackgroundColor(valuesV2().background.default)) - const syntax = createSyntaxStyleMemo(() => generateSyntax(values())) - - const theme = new Proxy(values(), { - get(_target, prop) { - // @ts-expect-error Properties are forwarded to the current reactive value. - return values()[prop] - }, - }) + const syntax = createSyntaxStyleMemo(() => generateSyntax(legacySyntaxTheme())) function contextual(context: ContextName) { return contextualServices[context] } const service: ThemeService = { - theme, themeV2, contextual, get selected() { diff --git a/packages/www/content/docs/(docs)/agents.mdx b/packages/www/content/docs/(docs)/agents.mdx index b1275960cb..d5668402db 100644 --- a/packages/www/content/docs/(docs)/agents.mdx +++ b/packages/www/content/docs/(docs)/agents.mdx @@ -89,7 +89,7 @@ becomes `system`: description: Reviews changes without modifying files mode: subagent model: anthropic/claude-sonnet-4-5#high -color: warning +color: "#ff6b6b" steps: 8 permissions: - action: edit @@ -118,7 +118,7 @@ Use the `agents` field in any [OpenCode configuration file](/docs/config): "mode": "all", "model": "anthropic/claude-sonnet-4-5#high", "system": "Review the current changes. Report findings before any summary.", - "color": "warning", + "color": "#ff6b6b", "steps": 8, "permissions": [ { "action": "edit", "resource": "*", "effect": "deny" }, @@ -250,8 +250,7 @@ security boundary. ### `color` -Sets the agent's UI color. Use a six-digit hex color such as `#ff6b6b`, or one -of `primary`, `secondary`, `accent`, `success`, `warning`, `error`, or `info`. +Sets the agent's UI color. Use a six-digit hex color such as `#ff6b6b`. ### `disabled` diff --git a/packages/www/public/openapi.json b/packages/www/public/openapi.json index 543a418173..d430f5d21d 100644 --- a/packages/www/public/openapi.json +++ b/packages/www/public/openapi.json @@ -10561,26 +10561,10 @@ "additionalProperties": false }, "Agent.Color": { - "anyOf": [ + "type": "string", + "allOf": [ { - "type": "string", - "allOf": [ - { - "pattern": "^#[0-9a-fA-F]{6}$" - } - ] - }, - { - "type": "string", - "enum": [ - "primary", - "secondary", - "accent", - "success", - "warning", - "error", - "info" - ] + "pattern": "^#[0-9a-fA-F]{6}$" } ] },