diff --git a/.opencode/plugins/tui-smoke.tsx b/.opencode/plugins/tui-smoke.tsx index ef24030e5b..6578156865 100644 --- a/.opencode/plugins/tui-smoke.tsx +++ b/.opencode/plugins/tui-smoke.tsx @@ -1,6 +1,7 @@ /** @jsxImportSource @opentui/solid */ import mytheme from "../themes/mytheme.json" with { type: "json" } import { useKeyboard, useTerminalDimensions } from "@opentui/solid" +import type { RGBA } from "@opentui/core" import type { TuiApi, TuiPluginInput } from "@opencode-ai/plugin/tui" const tabs = ["overview", "counter", "help"] @@ -39,6 +40,26 @@ const ui = { accent: "#5f87ff", } +type Color = RGBA | string + +const tone = (api: TuiApi) => { + const map = api.theme.current as Record + const get = (name: string, fallback: string): Color => { + const value = map[name] + if (typeof value === "string") return value + if (value && typeof value === "object") return value as RGBA + return fallback + } + return { + panel: get("backgroundPanel", ui.panel), + border: get("border", ui.border), + text: get("text", ui.text), + muted: get("textMuted", ui.muted), + accent: get("primary", ui.accent), + selected: get("selectedListItemText", ui.text), + } +} + const parse = (params: Record | undefined) => { const tab = typeof params?.tab === "number" ? params.tab : 0 const count = typeof params?.count === "number" ? params.count : 0 @@ -70,6 +91,7 @@ const Screen = (props: { }) => { const dim = useTerminalDimensions() const value = parse(props.params) + const skin = tone(props.api) useKeyboard((evt) => { if (props.api.route.current.name !== props.route.screen) return @@ -147,7 +169,7 @@ const Screen = (props: { }) return ( - + - + {props.input.label} screen - plugin route + plugin route - esc or ctrl+h home + esc or ctrl+h home @@ -171,11 +193,11 @@ const Screen = (props: { return ( props.api.route.navigate(props.route.screen, { ...value, tab: i })} - backgroundColor={on ? ui.accent : ui.border} + backgroundColor={on ? skin.accent : skin.border} paddingLeft={1} paddingRight={1} > - {item} + {item} ) })} @@ -183,7 +205,7 @@ const Screen = (props: { {value.tab === 0 ? ( - Route: {props.route.screen} - source: {value.source} - note: {value.note || "(none)"} - selected: {value.selected || "(none)"} + Route: {props.route.screen} + source: {value.source} + note: {value.note || "(none)"} + selected: {value.selected || "(none)"} ) : null} {value.tab === 1 ? ( - Counter: {value.count} - up/down or j/k change value + Counter: {value.count} + up/down or j/k change value ) : null} {value.tab === 2 ? ( - ctrl+m modal | a alert | c confirm | p prompt | s select - esc or ctrl+h returns home + ctrl+m modal | a alert | c confirm | p prompt | s select + esc or ctrl+h returns home ) : null} @@ -217,51 +239,51 @@ const Screen = (props: { props.api.route.navigate("home")} - backgroundColor={ui.border} + backgroundColor={skin.border} paddingLeft={1} paddingRight={1} > - go home + go home props.api.route.navigate(props.route.modal, value)} - backgroundColor={ui.accent} + backgroundColor={skin.accent} paddingLeft={1} paddingRight={1} > - modal + modal props.api.route.navigate(props.route.alert, value)} - backgroundColor={ui.border} + backgroundColor={skin.border} paddingLeft={1} paddingRight={1} > - alert + alert props.api.route.navigate(props.route.confirm, value)} - backgroundColor={ui.border} + backgroundColor={skin.border} paddingLeft={1} paddingRight={1} > - confirm + confirm props.api.route.navigate(props.route.prompt, value)} - backgroundColor={ui.border} + backgroundColor={skin.border} paddingLeft={1} paddingRight={1} > - prompt + prompt props.api.route.navigate(props.route.select, value)} - backgroundColor={ui.border} + backgroundColor={skin.border} paddingLeft={1} paddingRight={1} > - select + select @@ -277,6 +299,7 @@ const Modal = (props: { }) => { const Dialog = props.api.ui.Dialog const value = parse(props.params) + const skin = tone(props.api) useKeyboard((evt) => { if (props.api.route.current.name !== props.route.modal) return @@ -296,31 +319,31 @@ const Modal = (props: { }) return ( - + props.api.route.navigate("home")}> - + {props.input.label} modal - {props.api.keybind.print(props.input.modal)} modal command - {props.api.keybind.print(props.input.screen)} screen command - enter opens screen · esc closes + {props.api.keybind.print(props.input.modal)} modal command + {props.api.keybind.print(props.input.screen)} screen command + enter opens screen · esc closes props.api.route.navigate(props.route.screen, { ...value, source: "modal" })} - backgroundColor={ui.accent} + backgroundColor={skin.accent} paddingLeft={1} paddingRight={1} > - open screen + open screen props.api.route.navigate("home")} - backgroundColor={ui.border} + backgroundColor={skin.border} paddingLeft={1} paddingRight={1} > - cancel + cancel @@ -466,13 +489,17 @@ const SelectDialog = (props: { api: TuiApi; route: ReturnType; par const slot = (input: ReturnType) => ({ id: "workspace-smoke", slots: { - home_logo() { - return plugin logo:{input.label} - }, - sidebar_top(_ctx, value) { + home_logo(ctx) { return ( - plugin:{input.label} session:{value.session_id.slice(0, 8)} + plugin logo:{input.label} theme:{ctx.theme.selected}/{ctx.theme.mode()} + + ) + }, + sidebar_top(ctx, value) { + return ( + + plugin:{input.label} session:{value.session_id.slice(0, 8)} ready:{String(ctx.theme.ready)} ) }, diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 28538370d1..a52f870127 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -231,7 +231,8 @@ function App() { const keybind = useKeybind() const sdk = useSDK() const toast = useToast() - const { theme, mode, setMode } = useTheme() + const t = useTheme() + const { theme, mode, setMode } = t const sync = useSync() const exit = useExit() const promptRef = usePromptRef() @@ -391,6 +392,15 @@ function App() { get current() { return theme }, + get selected() { + return t.selected + }, + mode() { + return t.mode() + }, + get ready() { + return t.ready + }, }, } TuiPlugin.init({ diff --git a/packages/opencode/src/cli/cmd/tui/plugin.ts b/packages/opencode/src/cli/cmd/tui/plugin.ts index fa63575189..c4da7dc3ad 100644 --- a/packages/opencode/src/cli/cmd/tui/plugin.ts +++ b/packages/opencode/src/cli/cmd/tui/plugin.ts @@ -68,10 +68,12 @@ export namespace TuiPlugin { export const Slot: Slot = (props) => view(props) - function setupSlots(renderer: CliRenderer): TuiSlots { + function setupSlots(input: InitInput): TuiSlots { const reg = createSolidSlotRegistry( - renderer, - {}, + input.renderer, + { + theme: input.api.theme, + }, { onPluginError(event) { console.error("[tui.slot] plugin error", { @@ -99,7 +101,7 @@ export namespace TuiPlugin { if (loaded) return loaded loaded = load({ ...input, - slots: setupSlots(input.renderer), + slots: setupSlots(input), }) return loaded } diff --git a/packages/opencode/test/cli/tui/plugin-loader.test.ts b/packages/opencode/test/cli/tui/plugin-loader.test.ts index 41d13b9bec..5cac98505f 100644 --- a/packages/opencode/test/cli/tui/plugin-loader.test.ts +++ b/packages/opencode/test/cli/tui/plugin-loader.test.ts @@ -132,6 +132,15 @@ test("ignores function-only tui exports and loads object exports", async () => { get current() { return {} }, + get selected() { + return "opencode" + }, + mode() { + return "dark" as const + }, + get ready() { + return true + }, }, }, }) diff --git a/packages/plugin/src/tui.ts b/packages/plugin/src/tui.ts index 90b07f42d7..0a9e51ffb7 100644 --- a/packages/plugin/src/tui.ts +++ b/packages/plugin/src/tui.ts @@ -125,6 +125,13 @@ export type TuiToast = { duration?: number } +export type TuiTheme = { + readonly current: Record + readonly selected: string + mode: () => "dark" | "light" + readonly ready: boolean +} + export type TuiApi = { command: { register: (cb: () => TuiCommand[]) => void @@ -148,9 +155,7 @@ export type TuiApi = { match: (key: string, evt: ParsedKey) => boolean print: (key: string) => string } - theme: { - readonly current: Record - } + theme: TuiTheme } export type TuiSlotMap = { @@ -161,7 +166,9 @@ export type TuiSlotMap = { } } -export type TuiSlotContext = {} +export type TuiSlotContext = { + theme: TuiTheme +} export type TuiSlotPlugin = CorePlugin