diff --git a/packages/opencode/src/cli/cmd/tui/context/theme.tsx b/packages/opencode/src/cli/cmd/tui/context/theme.tsx index 4daed8cb28..008f1bf806 100644 --- a/packages/opencode/src/cli/cmd/tui/context/theme.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/theme.tsx @@ -43,66 +43,12 @@ import { Global } from "@/global" import { Filesystem } from "@/util/filesystem" import { useTuiConfig } from "./tui-config" import { isRecord } from "@/util/record" +import type { TuiThemeCurrent } from "@opencode-ai/plugin/tui" -type ThemeColors = { - primary: RGBA - secondary: RGBA - accent: RGBA - error: RGBA - warning: RGBA - success: RGBA - info: RGBA - text: RGBA - textMuted: RGBA - selectedListItemText: RGBA - background: RGBA - backgroundPanel: RGBA - backgroundElement: RGBA - backgroundMenu: RGBA - border: RGBA - borderActive: RGBA - borderSubtle: RGBA - diffAdded: RGBA - diffRemoved: RGBA - diffContext: RGBA - diffHunkHeader: RGBA - diffHighlightAdded: RGBA - diffHighlightRemoved: RGBA - diffAddedBg: RGBA - diffRemovedBg: RGBA - diffContextBg: RGBA - diffLineNumber: RGBA - diffAddedLineNumberBg: RGBA - diffRemovedLineNumberBg: RGBA - markdownText: RGBA - markdownHeading: RGBA - markdownLink: RGBA - markdownLinkText: RGBA - markdownCode: RGBA - markdownBlockQuote: RGBA - markdownEmph: RGBA - markdownStrong: RGBA - markdownHorizontalRule: RGBA - markdownListItem: RGBA - markdownListEnumeration: RGBA - markdownImage: RGBA - markdownImageText: RGBA - markdownCodeBlock: RGBA - syntaxComment: RGBA - syntaxKeyword: RGBA - syntaxFunction: RGBA - syntaxVariable: RGBA - syntaxString: RGBA - syntaxNumber: RGBA - syntaxType: RGBA - syntaxOperator: RGBA - syntaxPunctuation: RGBA -} - -type Theme = ThemeColors & { +type Theme = TuiThemeCurrent & { _hasSelectedListItemText: boolean - thinkingOpacity: number } +type ThemeColor = Exclude export function selectedForeground(theme: Theme, bg?: RGBA): RGBA { // If theme explicitly defines selectedListItemText, use it @@ -132,7 +78,7 @@ type ColorValue = HexColor | RefName | Variant | RGBA export type ThemeJson = { $schema?: string defs?: Record - theme: Omit, "selectedListItemText" | "backgroundMenu"> & { + theme: Omit, "selectedListItemText" | "backgroundMenu"> & { selectedListItemText?: ColorValue backgroundMenu?: ColorValue thinkingOpacity?: number @@ -250,7 +196,7 @@ export function resolveTheme(theme: ThemeJson, mode: "dark" | "light") { throw new Error(`Circular color reference: ${[...chain, c].join(" -> ")}`) } - const next = defs[c] ?? theme.theme[c as keyof ThemeColors] + const next = defs[c] ?? theme.theme[c as ThemeColor] if (next === undefined) { throw new Error(`Color reference "${c}" not found in defs or theme`) } @@ -268,7 +214,7 @@ export function resolveTheme(theme: ThemeJson, mode: "dark" | "light") { .map(([key, value]) => { return [key, resolveColor(value as ColorValue)] }), - ) as Partial + ) as Partial> // Handle selectedListItemText separately since it's optional const hasSelectedListItemText = theme.theme.selectedListItemText !== undefined diff --git a/packages/opencode/test/fixture/tui-plugin.ts b/packages/opencode/test/fixture/tui-plugin.ts index 753dd8f0b1..f884ea7179 100644 --- a/packages/opencode/test/fixture/tui-plugin.ts +++ b/packages/opencode/test/fixture/tui-plugin.ts @@ -1,5 +1,5 @@ import { createOpencodeClient } from "@opencode-ai/sdk/v2" -import type { CliRenderer } from "@opentui/core" +import { RGBA, type CliRenderer } from "@opentui/core" import { createPluginKeybind } from "../../src/cli/cmd/tui/context/plugin-keybinds" import type { HostPluginApi } from "../../src/cli/cmd/tui/plugin/slots" @@ -12,6 +12,74 @@ type Count = { command_drop: number } +function themeCurrent(): HostPluginApi["theme"]["current"] { + const a = RGBA.fromInts(0, 120, 240) + const b = RGBA.fromInts(120, 120, 120) + const c = RGBA.fromInts(230, 230, 230) + const d = RGBA.fromInts(120, 30, 30) + const e = RGBA.fromInts(140, 100, 40) + const f = RGBA.fromInts(20, 140, 80) + const g = RGBA.fromInts(20, 80, 160) + const h = RGBA.fromInts(40, 40, 40) + const i = RGBA.fromInts(60, 60, 60) + const j = RGBA.fromInts(80, 80, 80) + return { + primary: a, + secondary: b, + accent: a, + error: d, + warning: e, + success: f, + info: g, + text: c, + textMuted: b, + selectedListItemText: h, + background: h, + backgroundPanel: h, + backgroundElement: i, + backgroundMenu: i, + border: j, + borderActive: c, + borderSubtle: i, + diffAdded: f, + diffRemoved: d, + diffContext: b, + diffHunkHeader: b, + diffHighlightAdded: f, + diffHighlightRemoved: d, + diffAddedBg: h, + diffRemovedBg: h, + diffContextBg: h, + diffLineNumber: b, + diffAddedLineNumberBg: h, + diffRemovedLineNumberBg: h, + markdownText: c, + markdownHeading: c, + markdownLink: a, + markdownLinkText: g, + markdownCode: f, + markdownBlockQuote: e, + markdownEmph: e, + markdownStrong: c, + markdownHorizontalRule: b, + markdownListItem: a, + markdownListEnumeration: g, + markdownImage: a, + markdownImageText: g, + markdownCodeBlock: c, + syntaxComment: b, + syntaxKeyword: a, + syntaxFunction: g, + syntaxVariable: c, + syntaxString: f, + syntaxNumber: e, + syntaxType: a, + syntaxOperator: a, + syntaxPunctuation: c, + thinkingOpacity: 0.6, + } +} + type Opts = { client?: HostPluginApi["client"] renderer?: HostPluginApi["renderer"] @@ -185,18 +253,7 @@ export function createTuiPluginApi(opts: Opts = {}): HostPluginApi { }, theme: { get current() { - return ( - opts.theme?.current ?? { - text: "#ffffff", - textMuted: "#999999", - success: "#00ff00", - error: "#ff0000", - warning: "#ffff00", - backgroundElement: "#111111", - diffAdded: "#00ff00", - diffRemoved: "#ff0000", - } - ) + return opts.theme?.current ?? themeCurrent() }, get selected() { return selected diff --git a/packages/plugin/src/tui.ts b/packages/plugin/src/tui.ts index 8b4ba9c072..b92ece0542 100644 --- a/packages/plugin/src/tui.ts +++ b/packages/plugin/src/tui.ts @@ -136,15 +136,59 @@ export type TuiToast = { } export type TuiThemeCurrent = { - readonly text: RGBA | string - readonly textMuted: RGBA | string - readonly success: RGBA | string - readonly error: RGBA | string - readonly warning: RGBA | string - readonly backgroundElement: RGBA | string - readonly diffAdded: RGBA | string - readonly diffRemoved: RGBA | string - readonly [key: string]: RGBA | string | number | boolean + readonly primary: RGBA + readonly secondary: RGBA + readonly accent: RGBA + readonly error: RGBA + readonly warning: RGBA + readonly success: RGBA + readonly info: RGBA + readonly text: RGBA + readonly textMuted: RGBA + readonly selectedListItemText: RGBA + readonly background: RGBA + readonly backgroundPanel: RGBA + readonly backgroundElement: RGBA + readonly backgroundMenu: RGBA + readonly border: RGBA + readonly borderActive: RGBA + readonly borderSubtle: RGBA + readonly diffAdded: RGBA + readonly diffRemoved: RGBA + readonly diffContext: RGBA + readonly diffHunkHeader: RGBA + readonly diffHighlightAdded: RGBA + readonly diffHighlightRemoved: RGBA + readonly diffAddedBg: RGBA + readonly diffRemovedBg: RGBA + readonly diffContextBg: RGBA + readonly diffLineNumber: RGBA + readonly diffAddedLineNumberBg: RGBA + readonly diffRemovedLineNumberBg: RGBA + readonly markdownText: RGBA + readonly markdownHeading: RGBA + readonly markdownLink: RGBA + readonly markdownLinkText: RGBA + readonly markdownCode: RGBA + readonly markdownBlockQuote: RGBA + readonly markdownEmph: RGBA + readonly markdownStrong: RGBA + readonly markdownHorizontalRule: RGBA + readonly markdownListItem: RGBA + readonly markdownListEnumeration: RGBA + readonly markdownImage: RGBA + readonly markdownImageText: RGBA + readonly markdownCodeBlock: RGBA + readonly syntaxComment: RGBA + readonly syntaxKeyword: RGBA + readonly syntaxFunction: RGBA + readonly syntaxVariable: RGBA + readonly syntaxString: RGBA + readonly syntaxNumber: RGBA + readonly syntaxType: RGBA + readonly syntaxOperator: RGBA + readonly syntaxPunctuation: RGBA + readonly thinkingOpacity: number } export type TuiTheme = {