refactor(tui): finish V2 theme migration (#38383)

This commit is contained in:
James Long 2026-07-22 17:56:22 -04:00 committed by GitHub
commit b6e14b5a74
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 46 additions and 119 deletions

View file

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

View file

@ -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}$"
}
]
},

View file

@ -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<Info>("ConfigV2.Agent")({
model: ConfigModel.Selection.pipe(Schema.optional),

View file

@ -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",

View file

@ -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" },
})

View file

@ -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" }])

View file

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

View file

@ -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}$"
}
]
},

View file

@ -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<typeof Info> {}

View file

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

View file

@ -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() {
<go_upsell_art
width="100%"
height="100%"
backgroundPanel={theme.backgroundPanel}
primary={theme.primary}
logoBase={tint(theme.background, theme.text, 0.62)}
backgroundPanel={themeV2.background.default}
primary={themeV2.hue.interactive[mode() === "light" ? 800 : 200]}
logoBase={tint(themeV2.background.default, themeV2.text.default, 0.62)}
live
/>
)

View file

@ -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]
},
}

View file

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

View file

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

View file

@ -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}$"
}
]
},