From a2f702000de0783f1e86d88e94d66ee0857689a6 Mon Sep 17 00:00:00 2001 From: Rohan Godha Date: Thu, 16 Oct 2025 22:39:59 -0400 Subject: [PATCH 1/7] port over themes to opentui --- packages/opencode/src/cli/cmd/tui/app.tsx | 25 +- .../src/cli/cmd/tui/component/border.tsx | 2 +- .../cli/cmd/tui/component/dialog-model.tsx | 2 +- .../cmd/tui/component/dialog-session-list.tsx | 2 +- .../cli/cmd/tui/component/dialog-status.tsx | 16 +- .../cmd/tui/component/dialog-theme-list.tsx | 38 ++ .../src/cli/cmd/tui/component/logo.tsx | 6 +- .../cmd/tui/component/prompt/autocomplete.tsx | 8 +- .../cli/cmd/tui/component/prompt/index.tsx | 28 +- .../src/cli/cmd/tui/context/local.tsx | 2 +- .../src/cli/cmd/tui/context/theme.tsx | 369 ++++++------------ .../opencode/src/cli/cmd/tui/routes/home.tsx | 8 +- .../src/cli/cmd/tui/routes/session/header.tsx | 10 +- .../src/cli/cmd/tui/routes/session/index.tsx | 80 ++-- .../cli/cmd/tui/routes/session/sidebar.tsx | 26 +- .../src/cli/cmd/tui/ui/dialog-alert.tsx | 8 +- .../src/cli/cmd/tui/ui/dialog-confirm.tsx | 8 +- .../src/cli/cmd/tui/ui/dialog-select.tsx | 36 +- .../opencode/src/cli/cmd/tui/ui/dialog.tsx | 4 +- .../opencode/src/cli/cmd/tui/ui/shimmer.tsx | 6 +- .../tui/internal/theme/themes/vesper.json | 2 +- 21 files changed, 304 insertions(+), 382 deletions(-) create mode 100644 packages/opencode/src/cli/cmd/tui/component/dialog-theme-list.tsx diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 3a6c2e3a16..387bc2acd5 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -14,6 +14,7 @@ import { DialogStatus } from "@tui/component/dialog-status" import { CommandProvider, useCommandDialog } from "@tui/component/dialog-command" import { DialogAgent } from "@tui/component/dialog-agent" import { DialogSessionList } from "@tui/component/dialog-session-list" +import { DialogThemeList } from "@tui/component/dialog-theme-list" import { KeybindProvider, useKeybind } from "@tui/context/keybind" import { Theme } from "@tui/context/theme" import { Home } from "@tui/routes/home" @@ -151,6 +152,14 @@ function App() { }, category: "System", }, + { + title: "Switch theme", + value: "theme.switch", + onSelect: () => { + dialog.replace(() => ) + }, + category: "System", + }, ]) createEffect(() => { @@ -174,7 +183,7 @@ function App() { { const text = renderer.getSelection()?.getSelectedText() if (text && text.length > 0) { @@ -200,27 +209,27 @@ function App() { - - open + + open code - v{Installation.VERSION} + v{Installation.VERSION} - {process.cwd().replace(Global.Path.home, "~")} + {process.cwd().replace(Global.Path.home, "~")} - + tab {""} - + {local.agent.current().name.toUpperCase()} AGENT diff --git a/packages/opencode/src/cli/cmd/tui/component/border.tsx b/packages/opencode/src/cli/cmd/tui/component/border.tsx index 82b942c94f..936fb23c61 100644 --- a/packages/opencode/src/cli/cmd/tui/component/border.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/border.tsx @@ -2,7 +2,7 @@ import { Theme } from "@tui/context/theme" export const SplitBorder = { border: ["left" as const, "right" as const], - borderColor: Theme.border, + borderColor: Theme().border, customBorderChars: { topLeft: "", bottomLeft: "", diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx index cbf4912de7..04f2f6523b 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-model.tsx @@ -9,7 +9,7 @@ export function DialogModel() { const local = useLocal() const sync = useSync() const dialog = useDialog() - const [ref, setRef] = createSignal() + const [ref, setRef] = createSignal>() const options = createMemo(() => { return [ diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx index 3def8f6811..18ae5caa0b 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx @@ -29,7 +29,7 @@ export function DialogSessionList() { const isDeleting = toDelete() === x.id return { title: isDeleting ? "Press delete again to confirm" : x.title, - bg: isDeleting ? Theme.error : undefined, + bg: isDeleting ? Theme().error : undefined, value: x.id, category, footer: Locale.time(x.time.updated), diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx index 8833886f2d..718a0b4882 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx @@ -12,7 +12,7 @@ export function DialogStatus() { Status - esc + esc 0}> @@ -24,9 +24,9 @@ export function DialogStatus() { flexShrink={0} style={{ fg: { - connected: Theme.success, - failed: Theme.error, - disabled: Theme.textMuted, + connected: Theme().success, + failed: Theme().error, + disabled: Theme().textMuted, }[item.status], }} > @@ -34,7 +34,7 @@ export function DialogStatus() { {key}{" "} - + Connected {(val) => val().error} @@ -57,15 +57,15 @@ export function DialogStatus() { flexShrink={0} style={{ fg: { - connected: Theme.success, - error: Theme.error, + connected: Theme().success, + error: Theme().error, }[item.status], }} > • - {item.id} {item.root} + {item.id} {item.root} )} diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-theme-list.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-theme-list.tsx new file mode 100644 index 0000000000..56e929fbbf --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-theme-list.tsx @@ -0,0 +1,38 @@ +import { DialogSelect, type DialogSelectRef } from "../ui/dialog-select" +import { THEMES, selectedTheme, setSelectedTheme } from "../context/theme" +import { useDialog } from "../ui/dialog" +import { onCleanup } from "solid-js" + +export function DialogThemeList() { + const options = Object.keys(THEMES).map((theme) => ({ + title: theme, + value: theme as keyof typeof THEMES, + })) + const initialTheme = selectedTheme() + const dialog = useDialog() + let confimed = false + onCleanup(() => { + if (!confimed) setSelectedTheme(initialTheme) + }) + let ref: DialogSelectRef + + return ( + { + setSelectedTheme(opt.value) + }} + onSelect={(opt) => { + setSelectedTheme(opt.value) + confimed = true + dialog.clear() + }} + ref={(r) => (ref = r)} + onFilter={(query) => { + if (query.length === 0) setSelectedTheme(initialTheme) + else if (ref.filtered[0].value) setSelectedTheme(ref.filtered[0].value) + }} + /> + ) +} diff --git a/packages/opencode/src/cli/cmd/tui/component/logo.tsx b/packages/opencode/src/cli/cmd/tui/component/logo.tsx index 02ec9df6bc..15e71c789b 100644 --- a/packages/opencode/src/cli/cmd/tui/component/logo.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/logo.tsx @@ -13,15 +13,15 @@ export function Logo() { {(line, index) => ( - {line} - + {line} + {LOGO_RIGHT[index()]} )} - {Installation.VERSION} + {Installation.VERSION} ) diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx index 45d19657e6..6ce71d34a4 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx @@ -300,7 +300,7 @@ export function Autocomplete(props: { zIndex={100} {...SplitBorder} > - + - {option.display} + {option.display} - {option.description} + {option.description} )} diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index c7cd50dd1c..48254e19b2 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -110,8 +110,8 @@ export function Prompt(props: PromptProps) { }) createEffect(() => { - if (props.disabled) input.cursorColor = Theme.backgroundElement - if (!props.disabled) input.cursorColor = Theme.primary + if (props.disabled) input.cursorColor = Theme().backgroundElement + if (!props.disabled) input.cursorColor = Theme().primary }) const [store, setStore] = createStore<{ @@ -251,14 +251,14 @@ export function Prompt(props: PromptProps) { - - + + {store.mode === "normal" ? ">" : "!"} - + (input = r)} onMouseDown={(r) => r.target?.focus()} - focusedBackgroundColor={Theme.backgroundElement} - cursorColor={Theme.primary} - backgroundColor={Theme.backgroundElement} + focusedBackgroundColor={Theme().backgroundElement} + cursorColor={Theme().primary} + backgroundColor={Theme().backgroundElement} /> - + - {local.model.parsed().provider}{" "} + {local.model.parsed().provider}{" "} {local.model.parsed().model} - compacting... + compacting... - esc interrupt + esc interrupt {props.hint!} - ctrl+p commands + ctrl+p commands diff --git a/packages/opencode/src/cli/cmd/tui/context/local.tsx b/packages/opencode/src/cli/cmd/tui/context/local.tsx index ff9ef3552e..47458bda6a 100644 --- a/packages/opencode/src/cli/cmd/tui/context/local.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/local.tsx @@ -44,7 +44,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ }, color(name: string) { const index = agents().findIndex((x) => x.name === name) - const colors = [Theme.secondary, Theme.accent, Theme.success, Theme.warning, Theme.primary, Theme.error] + const colors = [Theme().secondary, Theme().accent, Theme().success, Theme().warning, Theme().primary, Theme().error] return colors[index % colors.length] }, } diff --git a/packages/opencode/src/cli/cmd/tui/context/theme.tsx b/packages/opencode/src/cli/cmd/tui/context/theme.tsx index 20a133482d..0a3d539554 100644 --- a/packages/opencode/src/cli/cmd/tui/context/theme.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/theme.tsx @@ -1,256 +1,122 @@ -import { SyntaxStyle } from "@opentui/core" - -const OPENCODE_THEME = { - primary: { - dark: "#fab283", - light: "#3b7dd8", - }, - secondary: { - dark: "#5c9cf5", - light: "#7b5bb6", - }, - accent: { - dark: "#9d7cd8", - light: "#d68c27", - }, - error: { - dark: "#e06c75", - light: "#d1383d", - }, - warning: { - dark: "#f5a742", - light: "#d68c27", - }, - success: { - dark: "#7fd88f", - light: "#3d9a57", - }, - info: { - dark: "#56b6c2", - light: "#318795", - }, - text: { - dark: "#eeeeee", - light: "#1a1a1a", - }, - textMuted: { - dark: "#808080", - light: "#8a8a8a", - }, - background: { - dark: "#0a0a0a", - light: "#ffffff", - }, - backgroundPanel: { - dark: "#141414", - light: "#fafafa", - }, - backgroundElement: { - dark: "#1e1e1e", - light: "#f5f5f5", - }, - border: { - dark: "#484848", - light: "#b8b8b8", - }, - borderActive: { - dark: "#606060", - light: "#a0a0a0", - }, - borderSubtle: { - dark: "#3c3c3c", - light: "#d4d4d4", - }, - diffAdded: { - dark: "#4fd6be", - light: "#1e725c", - }, - diffRemoved: { - dark: "#c53b53", - light: "#c53b53", - }, - diffContext: { - dark: "#828bb8", - light: "#7086b5", - }, - diffHunkHeader: { - dark: "#828bb8", - light: "#7086b5", - }, - diffHighlightAdded: { - dark: "#b8db87", - light: "#4db380", - }, - diffHighlightRemoved: { - dark: "#e26a75", - light: "#f52a65", - }, - diffAddedBg: { - dark: "#20303b", - light: "#d5e5d5", - }, - diffRemovedBg: { - dark: "#37222c", - light: "#f7d8db", - }, - diffContextBg: { - dark: "#141414", - light: "#fafafa", - }, - diffLineNumber: { - dark: "#1e1e1e", - light: "#f5f5f5", - }, - diffAddedLineNumberBg: { - dark: "#1b2b34", - light: "#c5d5c5", - }, - diffRemovedLineNumberBg: { - dark: "#2d1f26", - light: "#e7c8cb", - }, - markdownText: { - dark: "#eeeeee", - light: "#1a1a1a", - }, - markdownHeading: { - dark: "#9d7cd8", - light: "#d68c27", - }, - markdownLink: { - dark: "#fab283", - light: "#3b7dd8", - }, - markdownLinkText: { - dark: "#56b6c2", - light: "#318795", - }, - markdownCode: { - dark: "#7fd88f", - light: "#3d9a57", - }, - markdownBlockQuote: { - dark: "#e5c07b", - light: "#b0851f", - }, - markdownEmph: { - dark: "#e5c07b", - light: "#b0851f", - }, - markdownStrong: { - dark: "#f5a742", - light: "#d68c27", - }, - markdownHorizontalRule: { - dark: "#808080", - light: "#8a8a8a", - }, - markdownListItem: { - dark: "#fab283", - light: "#3b7dd8", - }, - markdownListEnumeration: { - dark: "#56b6c2", - light: "#318795", - }, - markdownImage: { - dark: "#fab283", - light: "#3b7dd8", - }, - markdownImageText: { - dark: "#56b6c2", - light: "#318795", - }, - markdownCodeBlock: { - dark: "#eeeeee", - light: "#1a1a1a", - }, - syntaxComment: { - dark: "#808080", - light: "#8a8a8a", - }, - syntaxKeyword: { - dark: "#9d7cd8", - light: "#d68c27", - }, - syntaxFunction: { - dark: "#fab283", - light: "#3b7dd8", - }, - syntaxVariable: { - dark: "#e06c75", - light: "#d1383d", - }, - syntaxString: { - dark: "#7fd88f", - light: "#3d9a57", - }, - syntaxNumber: { - dark: "#f5a742", - light: "#d68c27", - }, - syntaxType: { - dark: "#e5c07b", - light: "#b0851f", - }, - syntaxOperator: { - dark: "#56b6c2", - light: "#318795", - }, - syntaxPunctuation: { - dark: "#eeeeee", - light: "#1a1a1a", - }, -} as const +import { SyntaxStyle, RGBA } from "@opentui/core" +import { createSignal, createMemo } from "solid-js" +import aura from "../../../../../../tui/internal/theme/themes/aura.json" with { type: "json" } +import ayu from "../../../../../../tui/internal/theme/themes/ayu.json" with { type: "json" } +import catppuccin from "../../../../../../tui/internal/theme/themes/catppuccin.json" with { type: "json" } +import cobalt2 from "../../../../../../tui/internal/theme/themes/cobalt2.json" with { type: "json" } +import dracula from "../../../../../../tui/internal/theme/themes/dracula.json" with { type: "json" } +import everforest from "../../../../../../tui/internal/theme/themes/everforest.json" with { type: "json" } +import github from "../../../../../../tui/internal/theme/themes/github.json" with { type: "json" } +import gruvbox from "../../../../../../tui/internal/theme/themes/gruvbox.json" with { type: "json" } +import kanagawa from "../../../../../../tui/internal/theme/themes/kanagawa.json" with { type: "json" } +import material from "../../../../../../tui/internal/theme/themes/material.json" with { type: "json" } +import matrix from "../../../../../../tui/internal/theme/themes/matrix.json" with { type: "json" } +import monokai from "../../../../../../tui/internal/theme/themes/monokai.json" with { type: "json" } +import nord from "../../../../../../tui/internal/theme/themes/nord.json" with { type: "json" } +import onedark from "../../../../../../tui/internal/theme/themes/one-dark.json" with { type: "json" } +import opencode from "../../../../../../tui/internal/theme/themes/opencode.json" with { type: "json" } +import palenight from "../../../../../../tui/internal/theme/themes/palenight.json" with { type: "json" } +import rosepine from "../../../../../../tui/internal/theme/themes/rosepine.json" with { type: "json" } +import solarized from "../../../../../../tui/internal/theme/themes/solarized.json" with { type: "json" } +import synthwave84 from "../../../../../../tui/internal/theme/themes/synthwave84.json" with { type: "json" } +import tokyonight from "../../../../../../tui/internal/theme/themes/tokyonight.json" with { type: "json" } +import vesper from "../../../../../../tui/internal/theme/themes/vesper.json" with { type: "json" } +import zenburn from "../../../../../../tui/internal/theme/themes/zenburn.json" with { type: "json" } type Theme = { - primary: string - secondary: string - accent: string - error: string - warning: string - success: string - info: string - text: string - textMuted: string - background: string - backgroundPanel: string - backgroundElement: string - border: string - borderActive: string - borderSubtle: string - diffAdded: string - diffRemoved: string - diffContext: string - diffHunkHeader: string - diffHighlightAdded: string - diffHighlightRemoved: string - diffAddedBg: string - diffRemovedBg: string - diffContextBg: string - diffLineNumber: string - diffAddedLineNumberBg: string - diffRemovedLineNumberBg: string - markdownText: string - markdownHeading: {} - markdownLink: string - markdownLinkText: string - markdownCode: string - markdownBlockQuote: string - markdownEmph: string - markdownStrong: string - markdownHorizontalRule: string - markdownListItem: string - markdownListEnumeration: {} - markdownImage: string - markdownImageText: string - markdownCodeBlock: string + primary: RGBA + secondary: RGBA + accent: RGBA + error: RGBA + warning: RGBA + success: RGBA + info: RGBA + text: RGBA + textMuted: RGBA + background: RGBA + backgroundPanel: RGBA + backgroundElement: 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 +} + +type HexColor = `#${string}` +type RefName = string +type ColorModeObj = { + dark: HexColor | RefName + light: HexColor | RefName +} +type ColorValue = HexColor | RefName | ColorModeObj +type ThemeJson = { + $schema?: string + defs?: Record + theme: Record } -export const Theme = Object.entries(OPENCODE_THEME).reduce((acc, [key, value]) => { - acc[key as keyof Theme] = value.dark - return acc -}, {} as Theme) +export const THEMES = { + aura: resolveTheme(aura), + ayu: resolveTheme(ayu), + catppuccin: resolveTheme(catppuccin), + cobalt2: resolveTheme(cobalt2), + dracula: resolveTheme(dracula), + everforest: resolveTheme(everforest), + github: resolveTheme(github), + gruvbox: resolveTheme(gruvbox), + kanagawa: resolveTheme(kanagawa), + material: resolveTheme(material), + matrix: resolveTheme(matrix), + monokai: resolveTheme(monokai), + nord: resolveTheme(nord), + onedark: resolveTheme(onedark), + opencode: resolveTheme(opencode), + palenight: resolveTheme(palenight), + rosepine: resolveTheme(rosepine), + solarized: resolveTheme(solarized), + synthwave84: resolveTheme(synthwave84), + tokyonight: resolveTheme(tokyonight), + vesper: resolveTheme(vesper), + zenburn: resolveTheme(zenburn), +} + +function resolveTheme(theme: ThemeJson) { + const defs = theme.defs ?? {} + function resolveColor(c: ColorValue): RGBA { + if (typeof c === "string") return c.startsWith("#") ? RGBA.fromHex(c) : resolveColor(defs[c]) + else return resolveColor(c.dark) // TODO: opentui doesn't have the equivalent of lipgloss adaptiveColor yet + } + return Object.fromEntries( + Object.entries(theme.theme).map(([key, value]) => { + return [key, resolveColor(value)] + }), + ) as Theme +} const syntaxThemeDark = [ { @@ -432,4 +298,7 @@ const syntaxThemeDark = [ }, ] -export const syntaxTheme = SyntaxStyle.fromTheme(syntaxThemeDark) +export const [selectedTheme, setSelectedTheme] = createSignal("tokyonight") +export const Theme = createMemo(() => THEMES[selectedTheme()]) +// TODO: switch this +export const syntaxTheme = createMemo(() => SyntaxStyle.fromTheme(syntaxThemeDark)) diff --git a/packages/opencode/src/cli/cmd/tui/routes/home.tsx b/packages/opencode/src/cli/cmd/tui/routes/home.tsx index 78e310e7c2..b1e1d95645 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/home.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/home.tsx @@ -19,11 +19,11 @@ export function Home() { - mcp errors{" "} - ctrl+x s + mcp errors{" "} + ctrl+x s - {" "} + {" "} {Locale.pluralize(Object.values(sync.data.mcp).length, "{} mcp server", "{} mcp servers")} @@ -53,7 +53,7 @@ function HelpRow(props: ParentProps<{ keybind: keyof KeybindsConfig }>) { return ( {props.children} - {keybind.print(props.keybind)} + {keybind.print(props.keybind)} ) } diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx index 875e54036c..ab0871b2f5 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx @@ -37,23 +37,23 @@ export function Header() { }) return ( - + - # {session().title} + # {session().title} - {session().share!.url} + {session().share!.url} - /share to create a shareable link + /share to create a shareable link - + {context()} ({cost()}) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 88c326d398..29779d315f 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -420,17 +420,17 @@ export function Session() { flexShrink={0} border={["left"]} customBorderChars={SplitBorder.customBorderChars} - borderColor={Theme.backgroundPanel} + borderColor={Theme().backgroundPanel} > - {revert()!.reverted.length} message reverted - - {keybind.print("messages_redo")} or /redo to + {revert()!.reverted.length} message reverted + + {keybind.print("messages_redo")} or /redo to restore @@ -440,10 +440,10 @@ export function Session() { {file.filename} 0}> - +{file.additions} + +{file.additions} 0}> - -{file.deletions} + -{file.deletions} )} @@ -531,9 +531,9 @@ function UserMessage(props: { message: UserMessage; parts: Part[]; onMouseUp: () paddingBottom={1} paddingLeft={2} marginTop={props.index === 0 ? 0 : 1} - backgroundColor={hover() ? Theme.backgroundElement : Theme.backgroundPanel} + backgroundColor={hover() ? Theme().backgroundElement : Theme().backgroundPanel} customBorderChars={SplitBorder.customBorderChars} - borderColor={Theme.secondary} + borderColor={Theme().secondary} flexShrink={0} > {text()?.text} @@ -542,14 +542,14 @@ function UserMessage(props: { message: UserMessage; parts: Part[]; onMouseUp: () {(file) => { const bg = createMemo(() => { - if (file.mime.startsWith("image/")) return Theme.accent - if (file.mime === "application/pdf") return Theme.primary - return Theme.secondary + if (file.mime.startsWith("image/")) return Theme().accent + if (file.mime === "application/pdf") return Theme().primary + return Theme().secondary }) return ( - {MIME_BADGE[file.mime] ?? file.mime} - {file.filename} + {MIME_BADGE[file.mime] ?? file.mime} + {file.filename} ) }} @@ -558,7 +558,7 @@ function UserMessage(props: { message: UserMessage; parts: Part[]; onMouseUp: () {sync.data.config.username ?? "You"}{" "} - ({Locale.time(props.message.time.created)}) + ({Locale.time(props.message.time.created)}) @@ -586,11 +586,11 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las paddingBottom={1} paddingLeft={2} marginTop={1} - backgroundColor={Theme.backgroundPanel} + backgroundColor={Theme().backgroundPanel} customBorderChars={SplitBorder.customBorderChars} - borderColor={Theme.error} + borderColor={Theme().error} > - {props.message.error?.data.message} + {props.message.error?.data.message} @@ -601,17 +601,17 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las gap={1} border={["left"]} customBorderChars={SplitBorder.customBorderChars} - borderColor={Theme.backgroundElement} + borderColor={Theme().backgroundElement} > {Locale.titlecase(props.message.mode)} - + {Locale.titlecase(props.message.mode)}{" "} - {props.message.modelID} + {props.message.modelID} @@ -634,9 +634,9 @@ function ReasoningPart(props: { part: ReasoningPart; message: AssistantMessage } flexShrink={0} border={["left"]} customBorderChars={SplitBorder.customBorderChars} - borderColor={Theme.backgroundPanel} + borderColor={Theme().backgroundPanel} > - + {props.part.text.trim()} @@ -678,9 +678,9 @@ function ToolPart(props: { part: ToolPart; message: AssistantMessage }) { paddingLeft: 2, marginTop: 1, gap: 1, - backgroundColor: Theme.backgroundPanel, + backgroundColor: Theme().backgroundPanel, customBorderChars: SplitBorder.customBorderChars, - borderColor: permissionIndex === 0 ? Theme.warning : Theme.background, + borderColor: permissionIndex === 0 ? Theme().warning : Theme().background, } : { paddingLeft: 3, @@ -723,24 +723,24 @@ function ToolPart(props: { part: ToolPart; message: AssistantMessage }) { /> {props.part.state.status === "error" && ( - {props.part.state.error.replace("Error: ", "")} + {props.part.state.error.replace("Error: ", "")} )} {permission && ( - Permission required to run this tool: + Permission required to run this tool: enter - accept + accept a - accept always + accept always d - deny + deny @@ -790,7 +790,7 @@ const ToolRegistry = (() => { function ToolTitle(props: { fallback: string; when: any; icon: string; children: JSX.Element }) { return ( - + ~ {props.fallback}} when={props.when}> {props.icon} {props.children} @@ -809,11 +809,11 @@ ToolRegistry.register({ {props.input.description || "Shell"} - $ {props.input.command} + $ {props.input.command} - {output()} + {output()} @@ -862,10 +862,10 @@ ToolRegistry.register({ - {(value) => {value}} + {(value) => {value}} - + @@ -934,7 +934,7 @@ ToolRegistry.register({ {(task) => ( - + ∟ {task.tool} {task.state.status === "completed" ? task.state.title : ""} )} @@ -1047,16 +1047,16 @@ ToolRegistry.register({ - + - + - + @@ -1092,7 +1092,7 @@ ToolRegistry.register({ {(todo) => ( - + [{todo.status === "completed" ? "✓" : " "}] {todo.content} )} diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx index 48910dfb32..c7f341174d 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx @@ -52,16 +52,16 @@ export function Sidebar(props: { sessionID: string }) { {session().title} - {session().share!.url} + {session().share!.url} Context - {context()?.tokens ?? 0} tokens - {context()?.percentage ?? 0}% used - {cost()} spent + {context()?.tokens ?? 0} tokens + {context()?.percentage ?? 0}% used + {cost()} spent 0}> @@ -75,9 +75,9 @@ export function Sidebar(props: { sessionID: string }) { flexShrink={0} style={{ fg: { - connected: Theme.success, - failed: Theme.error, - disabled: Theme.textMuted, + connected: Theme().success, + failed: Theme().error, + disabled: Theme().textMuted, }[item.status], }} > @@ -85,7 +85,7 @@ export function Sidebar(props: { sessionID: string }) { {key}{" "} - + Connected {(val) => {val().error}} @@ -110,14 +110,14 @@ export function Sidebar(props: { sessionID: string }) { flexShrink={0} style={{ fg: { - connected: Theme.success, - error: Theme.error, + connected: Theme().success, + error: Theme().error, }[item.status], }} > • - + {item.id} {item.root} @@ -130,7 +130,7 @@ export function Sidebar(props: { sessionID: string }) { Modified Files - {(file) => {Locale.truncateMiddle(file, 40)}} + {(file) => {Locale.truncateMiddle(file, 40)}} 0}> @@ -140,7 +140,7 @@ export function Sidebar(props: { sessionID: string }) { {(todo) => ( - + [{todo.status === "completed" ? "✓" : " "}] {todo.content} )} diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-alert.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-alert.tsx index 31eee9f4bb..d64b14855f 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-alert.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-alert.tsx @@ -22,22 +22,22 @@ export function DialogAlert(props: DialogAlertProps) { {props.title} - esc + esc - {props.message} + {props.message} { props.onConfirm?.() dialog.clear() }} > - ok + ok diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx index 021331484d..4c48181621 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx @@ -34,10 +34,10 @@ export function DialogConfirm(props: DialogConfirmProps) { {props.title} - esc + esc - {props.message} + {props.message} @@ -45,14 +45,14 @@ export function DialogConfirm(props: DialogConfirmProps) { { if (key === "confirm") props.onConfirm?.() if (key === "cancel") props.onCancel?.() dialog.clear() }} > - {Locale.titlecase(key)} + {Locale.titlecase(key)} )} diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx index 5a8722aab6..1277bf60e1 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx @@ -14,7 +14,7 @@ import { Locale } from "@/util/locale" export interface DialogSelectProps { title: string options: DialogSelectOption[] - ref?: (ref: DialogSelectRef) => void + ref?: (ref: DialogSelectRef) => void onMove?: (option: DialogSelectOption) => void onFilter?: (query: string) => void onSelect?: (option: DialogSelectOption) => void @@ -34,12 +34,13 @@ export interface DialogSelectOption { footer?: string category?: string disabled?: boolean - bg?: string + bg?: RGBA onSelect?: (ctx: DialogContext) => void } -export type DialogSelectRef = { +export type DialogSelectRef = { filter: string + filtered: DialogSelectOption[] } export function DialogSelect(props: DialogSelectProps) { @@ -139,10 +140,13 @@ export function DialogSelect(props: DialogSelectProps) { }) let scroll: ScrollBoxRenderable - const ref: DialogSelectRef = { + const ref: DialogSelectRef = { get filter() { return store.filter }, + get filtered() { + return filtered() + }, } props.ref?.(ref) @@ -151,7 +155,7 @@ export function DialogSelect(props: DialogSelectProps) { {props.title} - esc + esc (props: DialogSelectProps) { props.onFilter?.(e) }) }} - focusedBackgroundColor={Theme.backgroundPanel} - cursorColor={Theme.primary} - focusedTextColor={Theme.textMuted} + focusedBackgroundColor={Theme().backgroundPanel} + cursorColor={Theme().primary} + focusedTextColor={Theme().textMuted} ref={(r) => { input = r input.focus() @@ -184,7 +188,7 @@ export function DialogSelect(props: DialogSelectProps) { <> 0 ? 1 : 0} paddingLeft={1}> - + {category} @@ -205,7 +209,7 @@ export function DialogSelect(props: DialogSelectProps) { if (index === -1) return moveTo(index) }} - backgroundColor={active() ? (option.bg ?? Theme.primary) : RGBA.fromInts(0, 0, 0, 0)} + backgroundColor={active() ? (option.bg ?? Theme().primary) : RGBA.fromInts(0, 0, 0, 0)} paddingLeft={1} paddingRight={1} gap={1} @@ -229,8 +233,10 @@ export function DialogSelect(props: DialogSelectProps) { {(item) => ( - {Keybind.toString(item.keybind)} - {item.title} + + {Keybind.toString(item.keybind)} + + {item.title} )} @@ -251,17 +257,17 @@ function Option(props: { <> {Locale.truncate(props.title, 62)} - {props.description} + {props.description} - {props.footer} + {props.footer} diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx index 32d6682bcc..a86d964f26 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx @@ -46,8 +46,8 @@ export function Dialog( customBorderChars={Border} width={props.size === "large" ? 80 : 60} maxWidth={dimensions().width - 2} - backgroundColor={Theme.backgroundPanel} - borderColor={Theme.border} + backgroundColor={Theme().backgroundPanel} + borderColor={Theme().border} paddingTop={1} > {props.children} diff --git a/packages/opencode/src/cli/cmd/tui/ui/shimmer.tsx b/packages/opencode/src/cli/cmd/tui/ui/shimmer.tsx index a669cecdd0..6c5629b8a7 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/shimmer.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/shimmer.tsx @@ -4,7 +4,7 @@ import { createMemo, createSignal } from "solid-js" export type ShimmerProps = { text: string - color: string + color: RGBA } const DURATION = 2_500 @@ -15,7 +15,7 @@ export function Shimmer(props: ShimmerProps) { loop: true, }) const characters = props.text.split("") - const color = createMemo(() => RGBA.fromHex(props.color)) + const color = props.color const shimmerSignals = characters.map((_, i) => { const [shimmer, setShimmer] = createSignal(0.4) @@ -47,7 +47,7 @@ export function Shimmer(props: ShimmerProps) { {(() => { return characters.map((ch, i) => { const shimmer = shimmerSignals[i] - const fg = RGBA.fromInts(color().r * 255, color().g * 255, color().b * 255, shimmer() * 255) + const fg = RGBA.fromInts(color.r * 255, color.g * 255, color.b * 255, shimmer() * 255) return {ch} }) })()} diff --git a/packages/tui/internal/theme/themes/vesper.json b/packages/tui/internal/theme/themes/vesper.json index cb19ff1782..758c8f20c1 100644 --- a/packages/tui/internal/theme/themes/vesper.json +++ b/packages/tui/internal/theme/themes/vesper.json @@ -3,7 +3,7 @@ "defs": { "vesperBg": "#101010", "vesperFg": "#FFF", - "vesperComment": "#8b8b8b94", + "vesperComment": "#8b8b8b", "vesperKeyword": "#A0A0A0", "vesperFunction": "#FFC799", "vesperString": "#99FFE4", From 2cd9cf67e5a0cff204e028e49dc4ecf27857d3ea Mon Sep 17 00:00:00 2001 From: Rohan Godha Date: Thu, 16 Oct 2025 23:02:28 -0400 Subject: [PATCH 2/7] wip: put themes in a context --- .../src/cli/cmd/tui/component/border.tsx | 3 --- .../cmd/tui/component/dialog-session-list.tsx | 5 ++-- .../cli/cmd/tui/component/dialog-status.tsx | 19 ++++++++------- .../cmd/tui/component/dialog-theme-list.tsx | 3 ++- .../src/cli/cmd/tui/component/logo.tsx | 9 +++---- .../src/cli/cmd/tui/context/local.tsx | 5 ++-- .../src/cli/cmd/tui/context/theme.tsx | 24 +++++++++++++++---- 7 files changed, 43 insertions(+), 25 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/border.tsx b/packages/opencode/src/cli/cmd/tui/component/border.tsx index 936fb23c61..9cbb96068d 100644 --- a/packages/opencode/src/cli/cmd/tui/component/border.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/border.tsx @@ -1,8 +1,5 @@ -import { Theme } from "@tui/context/theme" - export const SplitBorder = { border: ["left" as const, "right" as const], - borderColor: Theme().border, customBorderChars: { topLeft: "", bottomLeft: "", diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx index 18ae5caa0b..605eb2bff7 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx @@ -5,12 +5,13 @@ import { useSync } from "@tui/context/sync" import { createMemo, createSignal, onMount } from "solid-js" import { Locale } from "@/util/locale" import { Keybind } from "@/util/keybind" -import { Theme } from "../context/theme" +import { useTheme } from "../context/theme" import { useSDK } from "../context/sdk" export function DialogSessionList() { const dialog = useDialog() const sync = useSync() + const { theme } = useTheme() const route = useRoute() const sdk = useSDK() @@ -29,7 +30,7 @@ export function DialogSessionList() { const isDeleting = toDelete() === x.id return { title: isDeleting ? "Press delete again to confirm" : x.title, - bg: isDeleting ? Theme().error : undefined, + bg: isDeleting ? theme.error : undefined, value: x.id, category, footer: Locale.time(x.time.updated), diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx index 718a0b4882..732aa45730 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx @@ -1,5 +1,5 @@ import { TextAttributes } from "@opentui/core" -import { Theme } from "../context/theme" +import { useTheme } from "../context/theme" import { useSync } from "@tui/context/sync" import { For, Match, Switch, Show } from "solid-js" @@ -7,12 +7,13 @@ export type DialogStatusProps = {} export function DialogStatus() { const sync = useSync() + const { theme } = useTheme() return ( Status - esc + esc 0}> @@ -24,9 +25,9 @@ export function DialogStatus() { flexShrink={0} style={{ fg: { - connected: Theme().success, - failed: Theme().error, - disabled: Theme().textMuted, + connected: theme.success, + failed: theme.error, + disabled: theme.textMuted, }[item.status], }} > @@ -34,7 +35,7 @@ export function DialogStatus() { {key}{" "} - + Connected {(val) => val().error} @@ -57,15 +58,15 @@ export function DialogStatus() { flexShrink={0} style={{ fg: { - connected: Theme().success, - error: Theme().error, + connected: theme.success, + error: theme.error, }[item.status], }} > • - {item.id} {item.root} + {item.id} {item.root} )} diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-theme-list.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-theme-list.tsx index 56e929fbbf..0c49c73999 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-theme-list.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-theme-list.tsx @@ -1,9 +1,10 @@ import { DialogSelect, type DialogSelectRef } from "../ui/dialog-select" -import { THEMES, selectedTheme, setSelectedTheme } from "../context/theme" +import { THEMES, useTheme } from "../context/theme" import { useDialog } from "../ui/dialog" import { onCleanup } from "solid-js" export function DialogThemeList() { + const { selectedTheme, setSelectedTheme } = useTheme() const options = Object.keys(THEMES).map((theme) => ({ title: theme, value: theme as keyof typeof THEMES, diff --git a/packages/opencode/src/cli/cmd/tui/component/logo.tsx b/packages/opencode/src/cli/cmd/tui/component/logo.tsx index 15e71c789b..59db5fe7d1 100644 --- a/packages/opencode/src/cli/cmd/tui/component/logo.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/logo.tsx @@ -1,27 +1,28 @@ import { Installation } from "@/installation" import { TextAttributes } from "@opentui/core" import { For } from "solid-js" -import { Theme } from "@tui/context/theme" +import { useTheme } from "@tui/context/theme" const LOGO_LEFT = [` `, `█▀▀█ █▀▀█ █▀▀█ █▀▀▄`, `█░░█ █░░█ █▀▀▀ █░░█`, `▀▀▀▀ █▀▀▀ ▀▀▀▀ ▀ ▀`] const LOGO_RIGHT = [` ▄ `, `█▀▀▀ █▀▀█ █▀▀█ █▀▀█`, `█░░░ █░░█ █░░█ █▀▀▀`, `▀▀▀▀ ▀▀▀▀ ▀▀▀▀ ▀▀▀▀`] export function Logo() { + const { theme } = useTheme() return ( {(line, index) => ( - {line} - + {line} + {LOGO_RIGHT[index()]} )} - {Installation.VERSION} + {Installation.VERSION} ) diff --git a/packages/opencode/src/cli/cmd/tui/context/local.tsx b/packages/opencode/src/cli/cmd/tui/context/local.tsx index 47458bda6a..dfdb6aaa06 100644 --- a/packages/opencode/src/cli/cmd/tui/context/local.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/local.tsx @@ -1,7 +1,7 @@ import { createStore } from "solid-js/store" import { batch, createEffect, createMemo, createSignal } from "solid-js" import { useSync } from "@tui/context/sync" -import { Theme } from "@tui/context/theme" +import { useTheme } from "@tui/context/theme" import { uniqueBy } from "remeda" import path from "path" import { Global } from "@/global" @@ -20,6 +20,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ }>({ current: agents()[0].name, }) + const { theme } = useTheme() return { list() { return agents() @@ -44,7 +45,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ }, color(name: string) { const index = agents().findIndex((x) => x.name === name) - const colors = [Theme().secondary, Theme().accent, Theme().success, Theme().warning, Theme().primary, Theme().error] + const colors = [theme.secondary, theme.accent, theme.success, theme.warning, theme.primary, theme.error] return colors[index % colors.length] }, } diff --git a/packages/opencode/src/cli/cmd/tui/context/theme.tsx b/packages/opencode/src/cli/cmd/tui/context/theme.tsx index 0a3d539554..69e873e73a 100644 --- a/packages/opencode/src/cli/cmd/tui/context/theme.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/theme.tsx @@ -1,5 +1,6 @@ import { SyntaxStyle, RGBA } from "@opentui/core" import { createSignal, createMemo } from "solid-js" +import { createSimpleContext } from "./helper" import aura from "../../../../../../tui/internal/theme/themes/aura.json" with { type: "json" } import ayu from "../../../../../../tui/internal/theme/themes/ayu.json" with { type: "json" } import catppuccin from "../../../../../../tui/internal/theme/themes/catppuccin.json" with { type: "json" } @@ -298,7 +299,22 @@ const syntaxThemeDark = [ }, ] -export const [selectedTheme, setSelectedTheme] = createSignal("tokyonight") -export const Theme = createMemo(() => THEMES[selectedTheme()]) -// TODO: switch this -export const syntaxTheme = createMemo(() => SyntaxStyle.fromTheme(syntaxThemeDark)) +export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({ + name: "Theme", + init: () => { + const [selectedTheme, setSelectedTheme] = createSignal("tokyonight") + const themeValue = createMemo(() => THEMES[selectedTheme()]) + const syntaxThemeValue = createMemo(() => SyntaxStyle.fromTheme(syntaxThemeDark)) + + return { + get theme() { + return themeValue() + }, + get syntaxTheme() { + return syntaxThemeValue() + }, + selectedTheme, + setSelectedTheme, + } + }, +}) From bdcc47c519b222c5a1f67b8eb5bd44ab114534ce Mon Sep 17 00:00:00 2001 From: Rohan Godha Date: Thu, 16 Oct 2025 23:08:05 -0400 Subject: [PATCH 3/7] finish refactoring to theme context --- packages/opencode/src/cli/cmd/tui/app.tsx | 41 ++++---- .../cmd/tui/component/prompt/autocomplete.tsx | 11 ++- .../cli/cmd/tui/component/prompt/index.tsx | 31 ++++--- .../opencode/src/cli/cmd/tui/routes/home.tsx | 12 ++- .../src/cli/cmd/tui/routes/session/header.tsx | 13 +-- .../src/cli/cmd/tui/routes/session/index.tsx | 93 +++++++++++-------- .../cli/cmd/tui/routes/session/sidebar.tsx | 29 +++--- .../src/cli/cmd/tui/ui/dialog-alert.tsx | 11 ++- .../src/cli/cmd/tui/ui/dialog-confirm.tsx | 11 ++- .../src/cli/cmd/tui/ui/dialog-select.tsx | 28 +++--- .../opencode/src/cli/cmd/tui/ui/dialog.tsx | 7 +- 11 files changed, 155 insertions(+), 132 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 387bc2acd5..d182b4097b 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -16,7 +16,7 @@ import { DialogAgent } from "@tui/component/dialog-agent" import { DialogSessionList } from "@tui/component/dialog-session-list" import { DialogThemeList } from "@tui/component/dialog-theme-list" import { KeybindProvider, useKeybind } from "@tui/context/keybind" -import { Theme } from "@tui/context/theme" +import { ThemeProvider, useTheme } from "@tui/context/theme" import { Home } from "@tui/routes/home" import { Session } from "@tui/routes/session" import { PromptHistoryProvider } from "./component/prompt/history" @@ -32,15 +32,17 @@ export async function tui(input: { url: string; onExit?: () => Promise }) - - - - - - - - - + + + + + + + + + + + @@ -66,6 +68,7 @@ function App() { const local = useLocal() const command = useCommandDialog() const { event } = useSDK() + const { theme } = useTheme() useKeyboard(async (evt) => { if (evt.meta && evt.name === "t") { @@ -183,7 +186,7 @@ function App() { { const text = renderer.getSelection()?.getSelectedText() if (text && text.length > 0) { @@ -209,27 +212,27 @@ function App() { - - open + + open code - v{Installation.VERSION} + v{Installation.VERSION} - {process.cwd().replace(Global.Path.home, "~")} + {process.cwd().replace(Global.Path.home, "~")} - + tab - {""} - + {""} + {local.agent.current().name.toUpperCase()} AGENT diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx index 6ce71d34a4..bbc8a4fd00 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx @@ -5,7 +5,7 @@ import { createMemo, createResource, createEffect, onMount, For, Show } from "so import { createStore } from "solid-js/store" import { useSDK } from "@tui/context/sdk" import { useSync } from "@tui/context/sync" -import { Theme } from "@tui/context/theme" +import { useTheme } from "@tui/context/theme" import { SplitBorder } from "@tui/component/border" import { useCommandDialog } from "@tui/component/dialog-command" import type { PromptInfo } from "./history" @@ -34,6 +34,7 @@ export function Autocomplete(props: { const sdk = useSDK() const sync = useSync() const command = useCommandDialog() + const { theme } = useTheme() const [store, setStore] = createStore({ index: 0, @@ -300,7 +301,7 @@ export function Autocomplete(props: { zIndex={100} {...SplitBorder} > - + - {option.display} + {option.display} - {option.description} + {option.description} )} diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 48254e19b2..cacba3fdc2 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -1,7 +1,7 @@ import { InputRenderable, TextAttributes, BoxRenderable } from "@opentui/core" import { createEffect, createMemo, Match, Switch, type JSX } from "solid-js" import { useLocal } from "@tui/context/local" -import { Theme } from "@tui/context/theme" +import { useTheme } from "@tui/context/theme" import { SplitBorder } from "@tui/component/border" import { useSDK } from "@tui/context/sdk" import { useRoute } from "@tui/context/route" @@ -47,6 +47,7 @@ export function Prompt(props: PromptProps) { const history = usePromptHistory() const command = useCommandDialog() const renderer = useRenderer() + const { theme } = useTheme() command.register(() => { return [ @@ -110,8 +111,8 @@ export function Prompt(props: PromptProps) { }) createEffect(() => { - if (props.disabled) input.cursorColor = Theme().backgroundElement - if (!props.disabled) input.cursorColor = Theme().primary + if (props.disabled) input.cursorColor = theme.backgroundElement + if (!props.disabled) input.cursorColor = theme.primary }) const [store, setStore] = createStore<{ @@ -251,14 +252,14 @@ export function Prompt(props: PromptProps) { - - + + {store.mode === "normal" ? ">" : "!"} - + (input = r)} onMouseDown={(r) => r.target?.focus()} - focusedBackgroundColor={Theme().backgroundElement} - cursorColor={Theme().primary} - backgroundColor={Theme().backgroundElement} + focusedBackgroundColor={theme.backgroundElement} + cursorColor={theme.primary} + backgroundColor={theme.backgroundElement} /> - + - {local.model.parsed().provider}{" "} + {local.model.parsed().provider}{" "} {local.model.parsed().model} - compacting... + compacting... - esc interrupt + esc interrupt {props.hint!} - ctrl+p commands + ctrl+p commands diff --git a/packages/opencode/src/cli/cmd/tui/routes/home.tsx b/packages/opencode/src/cli/cmd/tui/routes/home.tsx index b1e1d95645..11244ad142 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/home.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/home.tsx @@ -1,6 +1,6 @@ import { Prompt } from "@tui/component/prompt" import { createMemo, Match, Show, Switch, type ParentProps } from "solid-js" -import { Theme } from "@tui/context/theme" +import { useTheme } from "@tui/context/theme" import { useKeybind } from "../context/keybind" import type { KeybindsConfig } from "@opencode-ai/sdk" import { Logo } from "../component/logo" @@ -9,6 +9,7 @@ import { useSync } from "../context/sync" export function Home() { const sync = useSync() + const { theme } = useTheme() const mcpError = createMemo(() => { return Object.values(sync.data.mcp).some((x) => x.status === "failed") }) @@ -19,11 +20,11 @@ export function Home() { - mcp errors{" "} - ctrl+x s + mcp errors{" "} + ctrl+x s - {" "} + {" "} {Locale.pluralize(Object.values(sync.data.mcp).length, "{} mcp server", "{} mcp servers")} @@ -50,10 +51,11 @@ export function Home() { function HelpRow(props: ParentProps<{ keybind: keyof KeybindsConfig }>) { const keybind = useKeybind() + const { theme } = useTheme() return ( {props.children} - {keybind.print(props.keybind)} + {keybind.print(props.keybind)} ) } diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx index ab0871b2f5..eeb868eb98 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx @@ -2,10 +2,11 @@ import { createMemo, Match, Show, Switch } from "solid-js" import { useRouteData } from "@tui/context/route" import { useSync } from "@tui/context/sync" import { pipe, sumBy } from "remeda" -import { Theme } from "@tui/context/theme" +import { useTheme } from "@tui/context/theme" import { SplitBorder } from "@tui/component/border" import type { AssistantMessage } from "@opencode-ai/sdk" + const { theme, syntaxTheme } = useTheme() export function Header() { const route = useRouteData("session") const sync = useSync() @@ -37,23 +38,23 @@ export function Header() { }) return ( - + - # {session().title} + # {session().title} - {session().share!.url} + {session().share!.url} - /share to create a shareable link + /share to create a shareable link - + {context()} ({cost()}) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 29779d315f..a649259004 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -15,7 +15,7 @@ import path from "path" import { useRouteData } from "@tui/context/route" import { useSync } from "@tui/context/sync" import { SplitBorder } from "@tui/component/border" -import { syntaxTheme, Theme } from "@tui/context/theme" +import { useTheme } from "@tui/context/theme" import { BoxRenderable, ScrollBoxRenderable } from "@opentui/core" import { Prompt, type PromptRef } from "@tui/component/prompt" import type { AssistantMessage, Part, ToolPart, UserMessage, TextPart, ReasoningPart } from "@opencode-ai/sdk" @@ -62,6 +62,7 @@ function use() { export function Session() { const route = useRouteData("session") const sync = useSync() + const { theme } = useTheme() const session = createMemo(() => sync.session.get(route.sessionID)!) const messages = createMemo(() => sync.data.message[route.sessionID] ?? []) const permissions = createMemo(() => sync.data.permission[route.sessionID] ?? []) @@ -420,17 +421,17 @@ export function Session() { flexShrink={0} border={["left"]} customBorderChars={SplitBorder.customBorderChars} - borderColor={Theme().backgroundPanel} + borderColor={theme.backgroundPanel} > - {revert()!.reverted.length} message reverted - - {keybind.print("messages_redo")} or /redo to + {revert()!.reverted.length} message reverted + + {keybind.print("messages_redo")} or /redo to restore @@ -440,10 +441,10 @@ export function Session() { {file.filename} 0}> - +{file.additions} + +{file.additions} 0}> - -{file.deletions} + -{file.deletions} )} @@ -513,6 +514,7 @@ function UserMessage(props: { message: UserMessage; parts: Part[]; onMouseUp: () const text = createMemo(() => props.parts.flatMap((x) => (x.type === "text" && !x.synthetic ? [x] : []))[0]) const files = createMemo(() => props.parts.flatMap((x) => (x.type === "file" ? [x] : []))) const sync = useSync() + const { theme } = useTheme() const [hover, setHover] = createSignal(false) return ( @@ -531,9 +533,9 @@ function UserMessage(props: { message: UserMessage; parts: Part[]; onMouseUp: () paddingBottom={1} paddingLeft={2} marginTop={props.index === 0 ? 0 : 1} - backgroundColor={hover() ? Theme().backgroundElement : Theme().backgroundPanel} + backgroundColor={hover() ? theme.backgroundElement : theme.backgroundPanel} customBorderChars={SplitBorder.customBorderChars} - borderColor={Theme().secondary} + borderColor={theme.secondary} flexShrink={0} > {text()?.text} @@ -542,14 +544,14 @@ function UserMessage(props: { message: UserMessage; parts: Part[]; onMouseUp: () {(file) => { const bg = createMemo(() => { - if (file.mime.startsWith("image/")) return Theme().accent - if (file.mime === "application/pdf") return Theme().primary - return Theme().secondary + if (file.mime.startsWith("image/")) return theme.accent + if (file.mime === "application/pdf") return theme.primary + return theme.secondary }) return ( - {MIME_BADGE[file.mime] ?? file.mime} - {file.filename} + {MIME_BADGE[file.mime] ?? file.mime} + {file.filename} ) }} @@ -558,7 +560,7 @@ function UserMessage(props: { message: UserMessage; parts: Part[]; onMouseUp: () {sync.data.config.username ?? "You"}{" "} - ({Locale.time(props.message.time.created)}) + ({Locale.time(props.message.time.created)}) @@ -567,6 +569,7 @@ function UserMessage(props: { message: UserMessage; parts: Part[]; onMouseUp: () function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; last: boolean }) { const local = useLocal() + const { theme } = useTheme() return ( <> @@ -586,11 +589,11 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las paddingBottom={1} paddingLeft={2} marginTop={1} - backgroundColor={Theme().backgroundPanel} + backgroundColor={theme.backgroundPanel} customBorderChars={SplitBorder.customBorderChars} - borderColor={Theme().error} + borderColor={theme.error} > - {props.message.error?.data.message} + {props.message.error?.data.message} @@ -601,17 +604,17 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las gap={1} border={["left"]} customBorderChars={SplitBorder.customBorderChars} - borderColor={Theme().backgroundElement} + borderColor={theme.backgroundElement} > {Locale.titlecase(props.message.mode)} - + {Locale.titlecase(props.message.mode)}{" "} - {props.message.modelID} + {props.message.modelID} @@ -626,6 +629,7 @@ const PART_MAPPING = { } function ReasoningPart(props: { part: ReasoningPart; message: AssistantMessage }) { + const { theme } = useTheme() return ( - + {props.part.text.trim()} @@ -657,6 +661,7 @@ function TextPart(props: { part: TextPart; message: AssistantMessage }) { // Pending messages moved to individual tool pending functions function ToolPart(props: { part: ToolPart; message: AssistantMessage }) { + const { theme } = useTheme() const sync = useSync() const [margin, setMargin] = createSignal(0) const component = createMemo(() => { @@ -678,9 +683,9 @@ function ToolPart(props: { part: ToolPart; message: AssistantMessage }) { paddingLeft: 2, marginTop: 1, gap: 1, - backgroundColor: Theme().backgroundPanel, + backgroundColor: theme.backgroundPanel, customBorderChars: SplitBorder.customBorderChars, - borderColor: permissionIndex === 0 ? Theme().warning : Theme().background, + borderColor: permissionIndex === 0 ? theme.warning : theme.background, } : { paddingLeft: 3, @@ -723,24 +728,24 @@ function ToolPart(props: { part: ToolPart; message: AssistantMessage }) { /> {props.part.state.status === "error" && ( - {props.part.state.error.replace("Error: ", "")} + {props.part.state.error.replace("Error: ", "")} )} {permission && ( - Permission required to run this tool: + Permission required to run this tool: enter - accept + accept a - accept always + accept always d - deny + deny @@ -789,8 +794,9 @@ const ToolRegistry = (() => { })() function ToolTitle(props: { fallback: string; when: any; icon: string; children: JSX.Element }) { + const { theme } = useTheme() return ( - + ~ {props.fallback}} when={props.when}> {props.icon} {props.children} @@ -803,17 +809,18 @@ ToolRegistry.register({ container: "block", render(props) { const output = createMemo(() => Bun.stripANSI(props.metadata.output?.trim() ?? "")) + const { theme } = useTheme() return ( <> {props.input.description || "Shell"} - $ {props.input.command} + $ {props.input.command} - {output()} + {output()} @@ -839,6 +846,7 @@ ToolRegistry.register({ name: "write", container: "block", render(props) { + const { theme, syntaxTheme } = useTheme() const lines = createMemo(() => { return props.input.content?.split("\n") ?? [] }) @@ -862,10 +870,10 @@ ToolRegistry.register({ - {(value) => {value}} + {(value) => {value}} - + @@ -925,6 +933,7 @@ ToolRegistry.register({ name: "task", container: "block", render(props) { + const { theme } = useTheme() return ( <> @@ -934,7 +943,7 @@ ToolRegistry.register({ {(task) => ( - + ∟ {task.tool} {task.state.status === "completed" ? task.state.title : ""} )} @@ -963,6 +972,7 @@ ToolRegistry.register({ container: "block", render(props) { const ctx = use() + const { syntaxTheme } = useTheme() const style = createMemo(() => (ctx.width > 120 ? "split" : "stacked")) @@ -1047,16 +1057,16 @@ ToolRegistry.register({ - + - + - + @@ -1088,11 +1098,12 @@ ToolRegistry.register({ name: "todowrite", container: "block", render(props) { + const { theme } = useTheme() return ( {(todo) => ( - + [{todo.status === "completed" ? "✓" : " "}] {todo.content} )} diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx index c7f341174d..34eb99adae 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx @@ -1,11 +1,12 @@ import { useSync } from "@tui/context/sync" import { createMemo, For, Show, Switch, Match } from "solid-js" -import { Theme } from "../../context/theme" +import { useTheme } from "../../context/theme" import { Locale } from "@/util/locale" import type { AssistantMessage } from "@opencode-ai/sdk" export function Sidebar(props: { sessionID: string }) { const sync = useSync() + const { theme } = useTheme() const session = createMemo(() => sync.session.get(props.sessionID)!) const todo = createMemo(() => sync.data.todo[props.sessionID] ?? []) const messages = createMemo(() => sync.data.message[props.sessionID] ?? []) @@ -52,16 +53,16 @@ export function Sidebar(props: { sessionID: string }) { {session().title} - {session().share!.url} + {session().share!.url} Context - {context()?.tokens ?? 0} tokens - {context()?.percentage ?? 0}% used - {cost()} spent + {context()?.tokens ?? 0} tokens + {context()?.percentage ?? 0}% used + {cost()} spent 0}> @@ -75,9 +76,9 @@ export function Sidebar(props: { sessionID: string }) { flexShrink={0} style={{ fg: { - connected: Theme().success, - failed: Theme().error, - disabled: Theme().textMuted, + connected: theme.success, + failed: theme.error, + disabled: theme.textMuted, }[item.status], }} > @@ -85,7 +86,7 @@ export function Sidebar(props: { sessionID: string }) { {key}{" "} - + Connected {(val) => {val().error}} @@ -110,14 +111,14 @@ export function Sidebar(props: { sessionID: string }) { flexShrink={0} style={{ fg: { - connected: Theme().success, - error: Theme().error, + connected: theme.success, + error: theme.error, }[item.status], }} > • - + {item.id} {item.root} @@ -130,7 +131,7 @@ export function Sidebar(props: { sessionID: string }) { Modified Files - {(file) => {Locale.truncateMiddle(file, 40)}} + {(file) => {Locale.truncateMiddle(file, 40)}} 0}> @@ -140,7 +141,7 @@ export function Sidebar(props: { sessionID: string }) { {(todo) => ( - + [{todo.status === "completed" ? "✓" : " "}] {todo.content} )} diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-alert.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-alert.tsx index d64b14855f..66c8334de9 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-alert.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-alert.tsx @@ -1,5 +1,5 @@ import { TextAttributes } from "@opentui/core" -import { Theme } from "../context/theme" +import { useTheme } from "../context/theme" import { useDialog, type DialogContext } from "./dialog" import { useKeyboard } from "@opentui/solid" @@ -11,6 +11,7 @@ export type DialogAlertProps = { export function DialogAlert(props: DialogAlertProps) { const dialog = useDialog() + const { theme } = useTheme() useKeyboard((evt) => { if (evt.name === "return") { @@ -22,22 +23,22 @@ export function DialogAlert(props: DialogAlertProps) { {props.title} - esc + esc - {props.message} + {props.message} { props.onConfirm?.() dialog.clear() }} > - ok + ok diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx index 4c48181621..9f78188f83 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx @@ -1,5 +1,5 @@ import { TextAttributes } from "@opentui/core" -import { Theme } from "../context/theme" +import { useTheme } from "../context/theme" import { useDialog, type DialogContext } from "./dialog" import { createStore } from "solid-js/store" import { For } from "solid-js" @@ -15,6 +15,7 @@ export type DialogConfirmProps = { export function DialogConfirm(props: DialogConfirmProps) { const dialog = useDialog() + const { theme } = useTheme() const [store, setStore] = createStore({ active: "confirm" as "confirm" | "cancel", }) @@ -34,10 +35,10 @@ export function DialogConfirm(props: DialogConfirmProps) { {props.title} - esc + esc - {props.message} + {props.message} @@ -45,14 +46,14 @@ export function DialogConfirm(props: DialogConfirmProps) { { if (key === "confirm") props.onConfirm?.() if (key === "cancel") props.onCancel?.() dialog.clear() }} > - {Locale.titlecase(key)} + {Locale.titlecase(key)} )} diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx index 1277bf60e1..ac05f8e08c 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx @@ -1,5 +1,5 @@ import { InputRenderable, RGBA, ScrollBoxRenderable, TextAttributes } from "@opentui/core" -import { Theme } from "@tui/context/theme" +import { useTheme } from "@tui/context/theme" import { entries, filter, flatMap, groupBy, pipe, take } from "remeda" import { batch, createEffect, createMemo, For, Show } from "solid-js" import { createStore } from "solid-js/store" @@ -45,6 +45,7 @@ export type DialogSelectRef = { export function DialogSelect(props: DialogSelectProps) { const dialog = useDialog() + const { theme } = useTheme() const [store, setStore] = createStore({ selected: 0, filter: "", @@ -155,7 +156,7 @@ export function DialogSelect(props: DialogSelectProps) { {props.title} - esc + esc (props: DialogSelectProps) { props.onFilter?.(e) }) }} - focusedBackgroundColor={Theme().backgroundPanel} - cursorColor={Theme().primary} - focusedTextColor={Theme().textMuted} + focusedBackgroundColor={theme.backgroundPanel} + cursorColor={theme.primary} + focusedTextColor={theme.textMuted} ref={(r) => { input = r input.focus() @@ -188,7 +189,7 @@ export function DialogSelect(props: DialogSelectProps) { <> 0 ? 1 : 0} paddingLeft={1}> - + {category} @@ -209,7 +210,7 @@ export function DialogSelect(props: DialogSelectProps) { if (index === -1) return moveTo(index) }} - backgroundColor={active() ? (option.bg ?? Theme().primary) : RGBA.fromInts(0, 0, 0, 0)} + backgroundColor={active() ? (option.bg ?? theme.primary) : RGBA.fromInts(0, 0, 0, 0)} paddingLeft={1} paddingRight={1} gap={1} @@ -233,10 +234,8 @@ export function DialogSelect(props: DialogSelectProps) { {(item) => ( - - {Keybind.toString(item.keybind)} - - {item.title} + {Keybind.toString(item.keybind)} + {item.title} )} @@ -253,21 +252,22 @@ function Option(props: { footer?: string onMouseOver?: () => void }) { + const { theme } = useTheme() return ( <> {Locale.truncate(props.title, 62)} - {props.description} + {props.description} - {props.footer} + {props.footer} diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx index a86d964f26..71e2702615 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx @@ -1,6 +1,6 @@ import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid" import { batch, createContext, Show, useContext, type JSX, type ParentProps } from "solid-js" -import { Theme } from "@tui/context/theme" +import { useTheme } from "@tui/context/theme" import { Renderable, RGBA } from "@opentui/core" import { createStore } from "solid-js/store" @@ -24,6 +24,7 @@ export function Dialog( }>, ) { const dimensions = useTerminalDimensions() + const { theme } = useTheme() return ( {props.children} From 34ff2510b6e0ffdc8e6dc7b07e3051c67a6e5867 Mon Sep 17 00:00:00 2001 From: Rohan Godha Date: Thu, 16 Oct 2025 23:35:29 -0400 Subject: [PATCH 4/7] fix reactivity --- packages/opencode/src/cli/cmd/tui/app.tsx | 24 +++--- .../cmd/tui/component/dialog-session-list.tsx | 2 +- .../cli/cmd/tui/component/dialog-status.tsx | 16 ++-- .../cmd/tui/component/dialog-theme-list.tsx | 36 ++++++--- .../src/cli/cmd/tui/component/logo.tsx | 6 +- .../cmd/tui/component/prompt/autocomplete.tsx | 11 ++- .../cli/cmd/tui/component/prompt/index.tsx | 28 +++---- .../src/cli/cmd/tui/context/local.tsx | 11 ++- .../src/cli/cmd/tui/context/theme.tsx | 12 +-- .../opencode/src/cli/cmd/tui/routes/home.tsx | 8 +- .../src/cli/cmd/tui/routes/session/header.tsx | 12 +-- .../src/cli/cmd/tui/routes/session/index.tsx | 80 +++++++++---------- .../cli/cmd/tui/routes/session/sidebar.tsx | 26 +++--- .../src/cli/cmd/tui/ui/dialog-alert.tsx | 8 +- .../src/cli/cmd/tui/ui/dialog-confirm.tsx | 8 +- .../src/cli/cmd/tui/ui/dialog-select.tsx | 24 +++--- .../opencode/src/cli/cmd/tui/ui/dialog.tsx | 4 +- 17 files changed, 168 insertions(+), 148 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index d182b4097b..3fedcd53a0 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -31,8 +31,8 @@ export async function tui(input: { url: string; onExit?: () => Promise }) - - + + @@ -42,8 +42,8 @@ export async function tui(input: { url: string; onExit?: () => Promise }) - - + + @@ -186,7 +186,7 @@ function App() { { const text = renderer.getSelection()?.getSelectedText() if (text && text.length > 0) { @@ -212,27 +212,27 @@ function App() { - - open + + open code - v{Installation.VERSION} + v{Installation.VERSION} - {process.cwd().replace(Global.Path.home, "~")} + {process.cwd().replace(Global.Path.home, "~")} - + tab {""} - + {local.agent.current().name.toUpperCase()} AGENT diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx index 605eb2bff7..5a6a5ffda8 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx @@ -30,7 +30,7 @@ export function DialogSessionList() { const isDeleting = toDelete() === x.id return { title: isDeleting ? "Press delete again to confirm" : x.title, - bg: isDeleting ? theme.error : undefined, + bg: isDeleting ? theme().error : undefined, value: x.id, category, footer: Locale.time(x.time.updated), diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx index 732aa45730..5fca1c60fe 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx @@ -13,7 +13,7 @@ export function DialogStatus() { Status - esc + esc 0}> @@ -25,9 +25,9 @@ export function DialogStatus() { flexShrink={0} style={{ fg: { - connected: theme.success, - failed: theme.error, - disabled: theme.textMuted, + connected: theme().success, + failed: theme().error, + disabled: theme().textMuted, }[item.status], }} > @@ -35,7 +35,7 @@ export function DialogStatus() { {key}{" "} - + Connected {(val) => val().error} @@ -58,15 +58,15 @@ export function DialogStatus() { flexShrink={0} style={{ fg: { - connected: theme.success, - error: theme.error, + connected: theme().success, + error: theme().error, }[item.status], }} > • - {item.id} {item.root} + {item.id} {item.root} )} diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-theme-list.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-theme-list.tsx index 0c49c73999..866b7aab9f 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-theme-list.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-theme-list.tsx @@ -1,21 +1,26 @@ import { DialogSelect, type DialogSelectRef } from "../ui/dialog-select" import { THEMES, useTheme } from "../context/theme" import { useDialog } from "../ui/dialog" -import { onCleanup } from "solid-js" +import { onCleanup, onMount } from "solid-js" export function DialogThemeList() { const { selectedTheme, setSelectedTheme } = useTheme() - const options = Object.keys(THEMES).map((theme) => ({ - title: theme, - value: theme as keyof typeof THEMES, + const options = Object.keys(THEMES).map((value) => ({ + title: value, + value: value as keyof typeof THEMES, })) - const initialTheme = selectedTheme() + const initial = selectedTheme() const dialog = useDialog() - let confimed = false + const state = { confirmed: false, ref: undefined as DialogSelectRef | undefined } + + onMount(() => { + // highlight the first theme in the list when we open it for UX + setSelectedTheme(Object.keys(THEMES)[0] as keyof typeof THEMES) + }) onCleanup(() => { - if (!confimed) setSelectedTheme(initialTheme) + // if we close the dialog without confirming, reset back to the initial theme + if (!state.confirmed) setSelectedTheme(initial) }) - let ref: DialogSelectRef return ( { setSelectedTheme(opt.value) - confimed = true + state.confirmed = true dialog.clear() }} - ref={(r) => (ref = r)} + ref={(ref) => { + state.ref = ref + }} onFilter={(query) => { - if (query.length === 0) setSelectedTheme(initialTheme) - else if (ref.filtered[0].value) setSelectedTheme(ref.filtered[0].value) + if (query.length === 0) { + setSelectedTheme(initial) + return + } + + const first = state.ref?.filtered[0] + if (first) setSelectedTheme(first.value) }} /> ) diff --git a/packages/opencode/src/cli/cmd/tui/component/logo.tsx b/packages/opencode/src/cli/cmd/tui/component/logo.tsx index 59db5fe7d1..5d255c2ac9 100644 --- a/packages/opencode/src/cli/cmd/tui/component/logo.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/logo.tsx @@ -14,15 +14,15 @@ export function Logo() { {(line, index) => ( - {line} - + {line} + {LOGO_RIGHT[index()]} )} - {Installation.VERSION} + {Installation.VERSION} ) diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx index bbc8a4fd00..9d893b014c 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx @@ -301,7 +301,7 @@ export function Autocomplete(props: { zIndex={100} {...SplitBorder} > - + - {option.display} + {option.display} - {option.description} + + {" "} + {option.description} + )} diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index cacba3fdc2..86358a184e 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -111,8 +111,8 @@ export function Prompt(props: PromptProps) { }) createEffect(() => { - if (props.disabled) input.cursorColor = theme.backgroundElement - if (!props.disabled) input.cursorColor = theme.primary + if (props.disabled) input.cursorColor = theme().backgroundElement + if (!props.disabled) input.cursorColor = theme().primary }) const [store, setStore] = createStore<{ @@ -252,14 +252,14 @@ export function Prompt(props: PromptProps) { - - + + {store.mode === "normal" ? ">" : "!"} - + (input = r)} onMouseDown={(r) => r.target?.focus()} - focusedBackgroundColor={theme.backgroundElement} - cursorColor={theme.primary} - backgroundColor={theme.backgroundElement} + focusedBackgroundColor={theme().backgroundElement} + cursorColor={theme().primary} + backgroundColor={theme().backgroundElement} /> - + - {local.model.parsed().provider}{" "} + {local.model.parsed().provider}{" "} {local.model.parsed().model} - compacting... + compacting... - esc interrupt + esc interrupt {props.hint!} - ctrl+p commands + ctrl+p commands diff --git a/packages/opencode/src/cli/cmd/tui/context/local.tsx b/packages/opencode/src/cli/cmd/tui/context/local.tsx index dfdb6aaa06..f87727155e 100644 --- a/packages/opencode/src/cli/cmd/tui/context/local.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/local.tsx @@ -21,6 +21,14 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ current: agents()[0].name, }) const { theme } = useTheme() + const colors = createMemo(() => [ + theme().secondary, + theme().accent, + theme().success, + theme().warning, + theme().primary, + theme().error, + ]) return { list() { return agents() @@ -45,8 +53,7 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ }, color(name: string) { const index = agents().findIndex((x) => x.name === name) - const colors = [theme.secondary, theme.accent, theme.success, theme.warning, theme.primary, theme.error] - return colors[index % colors.length] + return colors()[index % colors().length] }, } }) diff --git a/packages/opencode/src/cli/cmd/tui/context/theme.tsx b/packages/opencode/src/cli/cmd/tui/context/theme.tsx index 69e873e73a..1fdec5b005 100644 --- a/packages/opencode/src/cli/cmd/tui/context/theme.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/theme.tsx @@ -303,16 +303,12 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({ name: "Theme", init: () => { const [selectedTheme, setSelectedTheme] = createSignal("tokyonight") - const themeValue = createMemo(() => THEMES[selectedTheme()]) - const syntaxThemeValue = createMemo(() => SyntaxStyle.fromTheme(syntaxThemeDark)) + const theme = createMemo(() => THEMES[selectedTheme()]) + const syntaxTheme = createMemo(() => SyntaxStyle.fromTheme(syntaxThemeDark)) return { - get theme() { - return themeValue() - }, - get syntaxTheme() { - return syntaxThemeValue() - }, + theme, + syntaxTheme, selectedTheme, setSelectedTheme, } diff --git a/packages/opencode/src/cli/cmd/tui/routes/home.tsx b/packages/opencode/src/cli/cmd/tui/routes/home.tsx index 11244ad142..08dd925d28 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/home.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/home.tsx @@ -20,11 +20,11 @@ export function Home() { - mcp errors{" "} - ctrl+x s + mcp errors{" "} + ctrl+x s - {" "} + {" "} {Locale.pluralize(Object.values(sync.data.mcp).length, "{} mcp server", "{} mcp servers")} @@ -55,7 +55,7 @@ function HelpRow(props: ParentProps<{ keybind: keyof KeybindsConfig }>) { return ( {props.children} - {keybind.print(props.keybind)} + {keybind.print(props.keybind)} ) } diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx index eeb868eb98..89573c1465 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx @@ -6,10 +6,10 @@ import { useTheme } from "@tui/context/theme" import { SplitBorder } from "@tui/component/border" import type { AssistantMessage } from "@opencode-ai/sdk" - const { theme, syntaxTheme } = useTheme() export function Header() { const route = useRouteData("session") const sync = useSync() + const { theme } = useTheme() const session = createMemo(() => sync.session.get(route.sessionID)!) const messages = createMemo(() => sync.data.message[route.sessionID] ?? []) @@ -38,23 +38,23 @@ export function Header() { }) return ( - + - # {session().title} + # {session().title} - {session().share!.url} + {session().share!.url} - /share to create a shareable link + /share to create a shareable link - + {context()} ({cost()}) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index a649259004..f6f7299404 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -421,17 +421,17 @@ export function Session() { flexShrink={0} border={["left"]} customBorderChars={SplitBorder.customBorderChars} - borderColor={theme.backgroundPanel} + borderColor={theme().backgroundPanel} > - {revert()!.reverted.length} message reverted - - {keybind.print("messages_redo")} or /redo to + {revert()!.reverted.length} message reverted + + {keybind.print("messages_redo")} or /redo to restore @@ -441,10 +441,10 @@ export function Session() { {file.filename} 0}> - +{file.additions} + +{file.additions} 0}> - -{file.deletions} + -{file.deletions} )} @@ -533,9 +533,9 @@ function UserMessage(props: { message: UserMessage; parts: Part[]; onMouseUp: () paddingBottom={1} paddingLeft={2} marginTop={props.index === 0 ? 0 : 1} - backgroundColor={hover() ? theme.backgroundElement : theme.backgroundPanel} + backgroundColor={hover() ? theme().backgroundElement : theme().backgroundPanel} customBorderChars={SplitBorder.customBorderChars} - borderColor={theme.secondary} + borderColor={theme().secondary} flexShrink={0} > {text()?.text} @@ -544,14 +544,14 @@ function UserMessage(props: { message: UserMessage; parts: Part[]; onMouseUp: () {(file) => { const bg = createMemo(() => { - if (file.mime.startsWith("image/")) return theme.accent - if (file.mime === "application/pdf") return theme.primary - return theme.secondary + if (file.mime.startsWith("image/")) return theme().accent + if (file.mime === "application/pdf") return theme().primary + return theme().secondary }) return ( - {MIME_BADGE[file.mime] ?? file.mime} - {file.filename} + {MIME_BADGE[file.mime] ?? file.mime} + {file.filename} ) }} @@ -560,7 +560,7 @@ function UserMessage(props: { message: UserMessage; parts: Part[]; onMouseUp: () {sync.data.config.username ?? "You"}{" "} - ({Locale.time(props.message.time.created)}) + ({Locale.time(props.message.time.created)}) @@ -589,11 +589,11 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las paddingBottom={1} paddingLeft={2} marginTop={1} - backgroundColor={theme.backgroundPanel} + backgroundColor={theme().backgroundPanel} customBorderChars={SplitBorder.customBorderChars} - borderColor={theme.error} + borderColor={theme().error} > - {props.message.error?.data.message} + {props.message.error?.data.message} @@ -604,17 +604,17 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las gap={1} border={["left"]} customBorderChars={SplitBorder.customBorderChars} - borderColor={theme.backgroundElement} + borderColor={theme().backgroundElement} > {Locale.titlecase(props.message.mode)} - + {Locale.titlecase(props.message.mode)}{" "} - {props.message.modelID} + {props.message.modelID} @@ -638,9 +638,9 @@ function ReasoningPart(props: { part: ReasoningPart; message: AssistantMessage } flexShrink={0} border={["left"]} customBorderChars={SplitBorder.customBorderChars} - borderColor={theme.backgroundPanel} + borderColor={theme().backgroundPanel} > - + {props.part.text.trim()} @@ -683,9 +683,9 @@ function ToolPart(props: { part: ToolPart; message: AssistantMessage }) { paddingLeft: 2, marginTop: 1, gap: 1, - backgroundColor: theme.backgroundPanel, + backgroundColor: theme().backgroundPanel, customBorderChars: SplitBorder.customBorderChars, - borderColor: permissionIndex === 0 ? theme.warning : theme.background, + borderColor: permissionIndex === 0 ? theme().warning : theme().background, } : { paddingLeft: 3, @@ -728,24 +728,24 @@ function ToolPart(props: { part: ToolPart; message: AssistantMessage }) { /> {props.part.state.status === "error" && ( - {props.part.state.error.replace("Error: ", "")} + {props.part.state.error.replace("Error: ", "")} )} {permission && ( - Permission required to run this tool: + Permission required to run this tool: enter - accept + accept a - accept always + accept always d - deny + deny @@ -796,7 +796,7 @@ const ToolRegistry = (() => { function ToolTitle(props: { fallback: string; when: any; icon: string; children: JSX.Element }) { const { theme } = useTheme() return ( - + ~ {props.fallback}} when={props.when}> {props.icon} {props.children} @@ -816,11 +816,11 @@ ToolRegistry.register({ {props.input.description || "Shell"} - $ {props.input.command} + $ {props.input.command} - {output()} + {output()} @@ -870,10 +870,10 @@ ToolRegistry.register({ - {(value) => {value}} + {(value) => {value}} - + @@ -943,7 +943,7 @@ ToolRegistry.register({ {(task) => ( - + ∟ {task.tool} {task.state.status === "completed" ? task.state.title : ""} )} @@ -1057,16 +1057,16 @@ ToolRegistry.register({ - + - + - + @@ -1103,7 +1103,7 @@ ToolRegistry.register({ {(todo) => ( - + [{todo.status === "completed" ? "✓" : " "}] {todo.content} )} diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx index 34eb99adae..388137a13e 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx @@ -53,16 +53,16 @@ export function Sidebar(props: { sessionID: string }) { {session().title} - {session().share!.url} + {session().share!.url} Context - {context()?.tokens ?? 0} tokens - {context()?.percentage ?? 0}% used - {cost()} spent + {context()?.tokens ?? 0} tokens + {context()?.percentage ?? 0}% used + {cost()} spent 0}> @@ -76,9 +76,9 @@ export function Sidebar(props: { sessionID: string }) { flexShrink={0} style={{ fg: { - connected: theme.success, - failed: theme.error, - disabled: theme.textMuted, + connected: theme().success, + failed: theme().error, + disabled: theme().textMuted, }[item.status], }} > @@ -86,7 +86,7 @@ export function Sidebar(props: { sessionID: string }) { {key}{" "} - + Connected {(val) => {val().error}} @@ -111,14 +111,14 @@ export function Sidebar(props: { sessionID: string }) { flexShrink={0} style={{ fg: { - connected: theme.success, - error: theme.error, + connected: theme().success, + error: theme().error, }[item.status], }} > • - + {item.id} {item.root} @@ -131,7 +131,7 @@ export function Sidebar(props: { sessionID: string }) { Modified Files - {(file) => {Locale.truncateMiddle(file, 40)}} + {(file) => {Locale.truncateMiddle(file, 40)}} 0}> @@ -141,7 +141,7 @@ export function Sidebar(props: { sessionID: string }) { {(todo) => ( - + [{todo.status === "completed" ? "✓" : " "}] {todo.content} )} diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-alert.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-alert.tsx index 66c8334de9..ed77e5c237 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-alert.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-alert.tsx @@ -23,22 +23,22 @@ export function DialogAlert(props: DialogAlertProps) { {props.title} - esc + esc - {props.message} + {props.message} { props.onConfirm?.() dialog.clear() }} > - ok + ok diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx index 9f78188f83..f68db9328a 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx @@ -35,10 +35,10 @@ export function DialogConfirm(props: DialogConfirmProps) { {props.title} - esc + esc - {props.message} + {props.message} @@ -46,14 +46,14 @@ export function DialogConfirm(props: DialogConfirmProps) { { if (key === "confirm") props.onConfirm?.() if (key === "cancel") props.onCancel?.() dialog.clear() }} > - {Locale.titlecase(key)} + {Locale.titlecase(key)} )} diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx index ac05f8e08c..f6b9f45433 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx @@ -156,7 +156,7 @@ export function DialogSelect(props: DialogSelectProps) { {props.title} - esc + esc (props: DialogSelectProps) { props.onFilter?.(e) }) }} - focusedBackgroundColor={theme.backgroundPanel} - cursorColor={theme.primary} - focusedTextColor={theme.textMuted} + focusedBackgroundColor={theme().backgroundPanel} + cursorColor={theme().primary} + focusedTextColor={theme().textMuted} ref={(r) => { input = r input.focus() @@ -189,7 +189,7 @@ export function DialogSelect(props: DialogSelectProps) { <> 0 ? 1 : 0} paddingLeft={1}> - + {category} @@ -210,7 +210,7 @@ export function DialogSelect(props: DialogSelectProps) { if (index === -1) return moveTo(index) }} - backgroundColor={active() ? (option.bg ?? theme.primary) : RGBA.fromInts(0, 0, 0, 0)} + backgroundColor={active() ? (option.bg ?? theme().primary) : RGBA.fromInts(0, 0, 0, 0)} paddingLeft={1} paddingRight={1} gap={1} @@ -234,8 +234,10 @@ export function DialogSelect(props: DialogSelectProps) { {(item) => ( - {Keybind.toString(item.keybind)} - {item.title} + + {Keybind.toString(item.keybind)} + + {item.title} )} @@ -257,17 +259,17 @@ function Option(props: { <> {Locale.truncate(props.title, 62)} - {props.description} + {props.description} - {props.footer} + {props.footer} diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx index 71e2702615..491cd73c83 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx @@ -47,8 +47,8 @@ export function Dialog( customBorderChars={Border} width={props.size === "large" ? 80 : 60} maxWidth={dimensions().width - 2} - backgroundColor={theme.backgroundPanel} - borderColor={theme.border} + backgroundColor={theme().backgroundPanel} + borderColor={theme().border} paddingTop={1} > {props.children} From f4f508932a053ae77408c94696e103e4a01c423d Mon Sep 17 00:00:00 2001 From: Rohan Godha Date: Fri, 17 Oct 2025 00:39:29 -0400 Subject: [PATCH 5/7] default to theme in opencode config --- .../cmd/tui/component/dialog-theme-list.tsx | 13 +++++---- .../src/cli/cmd/tui/context/theme.tsx | 28 ++++++++++++++++--- 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-theme-list.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-theme-list.tsx index 866b7aab9f..9f7a9203dc 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-theme-list.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-theme-list.tsx @@ -11,7 +11,8 @@ export function DialogThemeList() { })) const initial = selectedTheme() const dialog = useDialog() - const state = { confirmed: false, ref: undefined as DialogSelectRef | undefined } + let confirmed = false + let ref: DialogSelectRef onMount(() => { // highlight the first theme in the list when we open it for UX @@ -19,7 +20,7 @@ export function DialogThemeList() { }) onCleanup(() => { // if we close the dialog without confirming, reset back to the initial theme - if (!state.confirmed) setSelectedTheme(initial) + if (!confirmed) setSelectedTheme(initial) }) return ( @@ -31,11 +32,11 @@ export function DialogThemeList() { }} onSelect={(opt) => { setSelectedTheme(opt.value) - state.confirmed = true + confirmed = true dialog.clear() }} - ref={(ref) => { - state.ref = ref + ref={(r) => { + ref = r }} onFilter={(query) => { if (query.length === 0) { @@ -43,7 +44,7 @@ export function DialogThemeList() { return } - const first = state.ref?.filtered[0] + const first = ref.filtered[0] if (first) setSelectedTheme(first.value) }} /> diff --git a/packages/opencode/src/cli/cmd/tui/context/theme.tsx b/packages/opencode/src/cli/cmd/tui/context/theme.tsx index 1fdec5b005..e8270ee47b 100644 --- a/packages/opencode/src/cli/cmd/tui/context/theme.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/theme.tsx @@ -1,5 +1,6 @@ import { SyntaxStyle, RGBA } from "@opentui/core" -import { createSignal, createMemo } from "solid-js" +import { createMemo, createSignal, createEffect } from "solid-js" +import { useSync } from "@tui/context/sync" import { createSimpleContext } from "./helper" import aura from "../../../../../../tui/internal/theme/themes/aura.json" with { type: "json" } import ayu from "../../../../../../tui/internal/theme/themes/ayu.json" with { type: "json" } @@ -23,6 +24,7 @@ import synthwave84 from "../../../../../../tui/internal/theme/themes/synthwave84 import tokyonight from "../../../../../../tui/internal/theme/themes/tokyonight.json" with { type: "json" } import vesper from "../../../../../../tui/internal/theme/themes/vesper.json" with { type: "json" } import zenburn from "../../../../../../tui/internal/theme/themes/zenburn.json" with { type: "json" } +import { iife } from "@/util/iife" type Theme = { primary: RGBA @@ -95,7 +97,7 @@ export const THEMES = { matrix: resolveTheme(matrix), monokai: resolveTheme(monokai), nord: resolveTheme(nord), - onedark: resolveTheme(onedark), + ["one-dark"]: resolveTheme(onedark), opencode: resolveTheme(opencode), palenight: resolveTheme(palenight), rosepine: resolveTheme(rosepine), @@ -110,7 +112,8 @@ function resolveTheme(theme: ThemeJson) { const defs = theme.defs ?? {} function resolveColor(c: ColorValue): RGBA { if (typeof c === "string") return c.startsWith("#") ? RGBA.fromHex(c) : resolveColor(defs[c]) - else return resolveColor(c.dark) // TODO: opentui doesn't have the equivalent of lipgloss adaptiveColor yet + // TODO: support light theme when opentui has the equivalent of lipgloss.AdaptiveColor + return resolveColor(c.dark) } return Object.fromEntries( Object.entries(theme.theme).map(([key, value]) => { @@ -302,7 +305,21 @@ const syntaxThemeDark = [ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({ name: "Theme", init: () => { - const [selectedTheme, setSelectedTheme] = createSignal("tokyonight") + const sync = useSync() + const [selectedTheme, setSelectedTheme] = createSignal("opencode") + + createEffect(() => { + if (!sync.ready) return + setSelectedTheme( + iife(() => { + if (typeof sync.data.config.theme === "string" && sync.data.config.theme in THEMES) { + return sync.data.config.theme as keyof typeof THEMES + } + return "opencode" + }), + ) + }) + const theme = createMemo(() => THEMES[selectedTheme()]) const syntaxTheme = createMemo(() => SyntaxStyle.fromTheme(syntaxThemeDark)) @@ -311,6 +328,9 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({ syntaxTheme, selectedTheme, setSelectedTheme, + get ready() { + return sync.ready + }, } }, }) From 23d41ccfce6e3b07fc3239f5a0fcd99768cdeca2 Mon Sep 17 00:00:00 2001 From: Rohan Godha Date: Fri, 17 Oct 2025 00:48:24 -0400 Subject: [PATCH 6/7] nit --- .../opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx index 9d893b014c..6dc5ddda3d 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx @@ -320,7 +320,6 @@ export function Autocomplete(props: { {option.display} - {" "} {option.description} From 47b69c09387a2139bf9aee14f29ad1f47aa2197b Mon Sep 17 00:00:00 2001 From: Rohan Godha Date: Thu, 23 Oct 2025 18:31:12 -0400 Subject: [PATCH 7/7] refactor: change theme to a store --- packages/opencode/src/cli/cmd/tui/app.tsx | 16 ++-- .../cmd/tui/component/dialog-session-list.tsx | 2 +- .../cli/cmd/tui/component/dialog-status.tsx | 16 ++-- .../src/cli/cmd/tui/component/logo.tsx | 6 +- .../cmd/tui/component/prompt/autocomplete.tsx | 10 +-- .../cli/cmd/tui/component/prompt/index.tsx | 28 +++---- .../src/cli/cmd/tui/context/local.tsx | 12 +-- .../src/cli/cmd/tui/context/theme.tsx | 9 ++- .../opencode/src/cli/cmd/tui/routes/home.tsx | 8 +- .../src/cli/cmd/tui/routes/session/header.tsx | 10 +-- .../src/cli/cmd/tui/routes/session/index.tsx | 74 +++++++++---------- .../cli/cmd/tui/routes/session/sidebar.tsx | 30 ++++---- .../src/cli/cmd/tui/ui/dialog-alert.tsx | 8 +- .../src/cli/cmd/tui/ui/dialog-confirm.tsx | 8 +- .../src/cli/cmd/tui/ui/dialog-select.tsx | 24 +++--- .../opencode/src/cli/cmd/tui/ui/dialog.tsx | 4 +- 16 files changed, 132 insertions(+), 133 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 194b342d71..12e8d628c7 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -188,7 +188,7 @@ function App() { { const text = renderer.getSelection()?.getSelectedText() if (text && text.length > 0) { @@ -214,27 +214,27 @@ function App() { - - open + + open code - v{Installation.VERSION} + v{Installation.VERSION} - {process.cwd().replace(Global.Path.home, "~")} + {process.cwd().replace(Global.Path.home, "~")} - + tab {""} - + {local.agent.current().name.toUpperCase()} AGENT diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx index 5a6a5ffda8..605eb2bff7 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-session-list.tsx @@ -30,7 +30,7 @@ export function DialogSessionList() { const isDeleting = toDelete() === x.id return { title: isDeleting ? "Press delete again to confirm" : x.title, - bg: isDeleting ? theme().error : undefined, + bg: isDeleting ? theme.error : undefined, value: x.id, category, footer: Locale.time(x.time.updated), diff --git a/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx b/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx index 5fca1c60fe..732aa45730 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx @@ -13,7 +13,7 @@ export function DialogStatus() { Status - esc + esc 0}> @@ -25,9 +25,9 @@ export function DialogStatus() { flexShrink={0} style={{ fg: { - connected: theme().success, - failed: theme().error, - disabled: theme().textMuted, + connected: theme.success, + failed: theme.error, + disabled: theme.textMuted, }[item.status], }} > @@ -35,7 +35,7 @@ export function DialogStatus() { {key}{" "} - + Connected {(val) => val().error} @@ -58,15 +58,15 @@ export function DialogStatus() { flexShrink={0} style={{ fg: { - connected: theme().success, - error: theme().error, + connected: theme.success, + error: theme.error, }[item.status], }} > • - {item.id} {item.root} + {item.id} {item.root} )} diff --git a/packages/opencode/src/cli/cmd/tui/component/logo.tsx b/packages/opencode/src/cli/cmd/tui/component/logo.tsx index 5d255c2ac9..59db5fe7d1 100644 --- a/packages/opencode/src/cli/cmd/tui/component/logo.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/logo.tsx @@ -14,15 +14,15 @@ export function Logo() { {(line, index) => ( - {line} - + {line} + {LOGO_RIGHT[index()]} )} - {Installation.VERSION} + {Installation.VERSION} ) diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx index 3587e033a8..3747e66db5 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx @@ -301,7 +301,7 @@ export function Autocomplete(props: { zIndex={100} {...SplitBorder} > - + - {option.display} + {option.display} - - {option.description} - + {option.description} )} diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx index 34693dfff2..1787a50a13 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -111,8 +111,8 @@ export function Prompt(props: PromptProps) { }) createEffect(() => { - if (props.disabled) input.cursorColor = theme().backgroundElement - if (!props.disabled) input.cursorColor = theme().primary + if (props.disabled) input.cursorColor = theme.backgroundElement + if (!props.disabled) input.cursorColor = theme.primary }) const [store, setStore] = createStore<{ @@ -248,14 +248,14 @@ export function Prompt(props: PromptProps) { - - + + {store.mode === "normal" ? ">" : "!"} - + (input = r)} onMouseDown={(r) => r.target?.focus()} - focusedBackgroundColor={theme().backgroundElement} - cursorColor={theme().primary} - backgroundColor={theme().backgroundElement} + focusedBackgroundColor={theme.backgroundElement} + cursorColor={theme.primary} + backgroundColor={theme.backgroundElement} /> - + - {local.model.parsed().provider}{" "} + {local.model.parsed().provider}{" "} {local.model.parsed().model} - compacting... + compacting... - esc interrupt + esc interrupt {props.hint!} - ctrl+p commands + ctrl+p commands diff --git a/packages/opencode/src/cli/cmd/tui/context/local.tsx b/packages/opencode/src/cli/cmd/tui/context/local.tsx index e33d493537..84018fdf6d 100644 --- a/packages/opencode/src/cli/cmd/tui/context/local.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/local.tsx @@ -22,12 +22,12 @@ export const { use: useLocal, provider: LocalProvider } = createSimpleContext({ }) const { theme } = useTheme() const colors = createMemo(() => [ - theme().secondary, - theme().accent, - theme().success, - theme().warning, - theme().primary, - theme().error, + theme.secondary, + theme.accent, + theme.success, + theme.warning, + theme.primary, + theme.error, ]) return { list() { diff --git a/packages/opencode/src/cli/cmd/tui/context/theme.tsx b/packages/opencode/src/cli/cmd/tui/context/theme.tsx index e8270ee47b..569042b097 100644 --- a/packages/opencode/src/cli/cmd/tui/context/theme.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/theme.tsx @@ -25,6 +25,7 @@ import tokyonight from "../../../../../../tui/internal/theme/themes/tokyonight.j import vesper from "../../../../../../tui/internal/theme/themes/vesper.json" with { type: "json" } import zenburn from "../../../../../../tui/internal/theme/themes/zenburn.json" with { type: "json" } import { iife } from "@/util/iife" +import { createStore, reconcile } from "solid-js/store" type Theme = { primary: RGBA @@ -307,6 +308,8 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({ init: () => { const sync = useSync() const [selectedTheme, setSelectedTheme] = createSignal("opencode") + const [theme, setTheme] = createStore({} as Theme) + const syntaxTheme = createMemo(() => SyntaxStyle.fromTheme(syntaxThemeDark)) createEffect(() => { if (!sync.ready) return @@ -319,9 +322,9 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({ }), ) }) - - const theme = createMemo(() => THEMES[selectedTheme()]) - const syntaxTheme = createMemo(() => SyntaxStyle.fromTheme(syntaxThemeDark)) + createEffect(() => { + setTheme(reconcile(THEMES[selectedTheme()])) + }) return { theme, diff --git a/packages/opencode/src/cli/cmd/tui/routes/home.tsx b/packages/opencode/src/cli/cmd/tui/routes/home.tsx index 08dd925d28..11244ad142 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/home.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/home.tsx @@ -20,11 +20,11 @@ export function Home() { - mcp errors{" "} - ctrl+x s + mcp errors{" "} + ctrl+x s - {" "} + {" "} {Locale.pluralize(Object.values(sync.data.mcp).length, "{} mcp server", "{} mcp servers")} @@ -55,7 +55,7 @@ function HelpRow(props: ParentProps<{ keybind: keyof KeybindsConfig }>) { return ( {props.children} - {keybind.print(props.keybind)} + {keybind.print(props.keybind)} ) } diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx index 1f332191b8..645fc1ba4b 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx @@ -38,23 +38,23 @@ export function Header() { }) return ( - + - # {session().title} + # {session().title} - {session().share!.url} + {session().share!.url} - /share to create a shareable link + /share to create a shareable link - + {context()} ({cost()}) diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx index 6eda88057c..d23a83b0a0 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx @@ -426,17 +426,17 @@ export function Session() { flexShrink={0} border={["left"]} customBorderChars={SplitBorder.customBorderChars} - borderColor={theme().backgroundPanel} + borderColor={theme.backgroundPanel} > - {revert()!.reverted.length} message reverted - - {keybind.print("messages_redo")} or /redo to + {revert()!.reverted.length} message reverted + + {keybind.print("messages_redo")} or /redo to restore @@ -446,10 +446,10 @@ export function Session() { {file.filename} 0}> - +{file.additions} + +{file.additions} 0}> - -{file.deletions} + -{file.deletions} )} @@ -529,7 +529,7 @@ function UserMessage(props: { const { theme } = useTheme() const [hover, setHover] = createSignal(false) const queued = createMemo(() => props.pending && props.message.id > props.pending) - const color = createMemo(() => (queued() ? theme().accent : theme().secondary)) + const color = createMemo(() => (queued() ? theme.accent : theme.secondary)) return ( @@ -547,7 +547,7 @@ function UserMessage(props: { paddingBottom={1} paddingLeft={2} marginTop={props.index === 0 ? 0 : 1} - backgroundColor={hover() ? theme().backgroundElement : theme().backgroundPanel} + backgroundColor={hover() ? theme.backgroundElement : theme.backgroundPanel} customBorderChars={SplitBorder.customBorderChars} borderColor={color()} flexShrink={0} @@ -558,14 +558,14 @@ function UserMessage(props: { {(file) => { const bg = createMemo(() => { - if (file.mime.startsWith("image/")) return theme().accent - if (file.mime === "application/pdf") return theme().primary - return theme().secondary + if (file.mime.startsWith("image/")) return theme.accent + if (file.mime === "application/pdf") return theme.primary + return theme.secondary }) return ( - {MIME_BADGE[file.mime] ?? file.mime} - {file.filename} + {MIME_BADGE[file.mime] ?? file.mime} + {file.filename} ) }} @@ -579,9 +579,9 @@ function UserMessage(props: { {sync.data.config.username ?? "You"}{" "} ({Locale.time(props.message.time.created)})} + fallback={({Locale.time(props.message.time.created)})} > - QUEUED + QUEUED @@ -611,11 +611,11 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las paddingBottom={1} paddingLeft={2} marginTop={1} - backgroundColor={theme().backgroundPanel} + backgroundColor={theme.backgroundPanel} customBorderChars={SplitBorder.customBorderChars} - borderColor={theme().error} + borderColor={theme.error} > - {props.message.error?.data.message} + {props.message.error?.data.message} @@ -626,17 +626,17 @@ function AssistantMessage(props: { message: AssistantMessage; parts: Part[]; las gap={1} border={["left"]} customBorderChars={SplitBorder.customBorderChars} - borderColor={theme().backgroundElement} + borderColor={theme.backgroundElement} > {Locale.titlecase(props.message.mode)} - + {Locale.titlecase(props.message.mode)}{" "} - {props.message.modelID} + {props.message.modelID} @@ -660,9 +660,9 @@ function ReasoningPart(props: { part: ReasoningPart; message: AssistantMessage } flexShrink={0} border={["left"]} customBorderChars={SplitBorder.customBorderChars} - borderColor={theme().backgroundPanel} + borderColor={theme.backgroundPanel} > - + {props.part.text.trim()} @@ -705,9 +705,9 @@ function ToolPart(props: { part: ToolPart; message: AssistantMessage }) { paddingLeft: 2, marginTop: 1, gap: 1, - backgroundColor: theme().backgroundPanel, + backgroundColor: theme.backgroundPanel, customBorderChars: SplitBorder.customBorderChars, - borderColor: permissionIndex === 0 ? theme().warning : theme().background, + borderColor: permissionIndex === 0 ? theme.warning : theme.background, } : { paddingLeft: 3, @@ -750,24 +750,24 @@ function ToolPart(props: { part: ToolPart; message: AssistantMessage }) { /> {props.part.state.status === "error" && ( - {props.part.state.error.replace("Error: ", "")} + {props.part.state.error.replace("Error: ", "")} )} {permission && ( - Permission required to run this tool: + Permission required to run this tool: enter - accept + accept a - accept always + accept always d - deny + deny @@ -818,7 +818,7 @@ const ToolRegistry = (() => { function ToolTitle(props: { fallback: string; when: any; icon: string; children: JSX.Element }) { const { theme } = useTheme() return ( - + ~ {props.fallback}} when={props.when}> {props.icon} {props.children} @@ -838,11 +838,11 @@ ToolRegistry.register({ {props.input.description || "Shell"} - $ {props.input.command} + $ {props.input.command} - {output()} + {output()} @@ -892,7 +892,7 @@ ToolRegistry.register({ - {(value) => {value}} + {(value) => {value}} @@ -965,7 +965,7 @@ ToolRegistry.register({ {(task) => ( - + ∟ {task.tool} {task.state.status === "completed" ? task.state.title : ""} )} @@ -1125,7 +1125,7 @@ ToolRegistry.register({ {(todo) => ( - + [{todo.status === "completed" ? "✓" : " "}] {todo.content} )} diff --git a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx index ed8e42d97e..816275d457 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx @@ -39,16 +39,16 @@ export function Sidebar(props: { sessionID: string }) { {session().title} - {session().share!.url} + {session().share!.url} Context - {context()?.tokens ?? 0} tokens - {context()?.percentage ?? 0}% used - {cost()} spent + {context()?.tokens ?? 0} tokens + {context()?.percentage ?? 0}% used + {cost()} spent 0}> @@ -62,9 +62,9 @@ export function Sidebar(props: { sessionID: string }) { flexShrink={0} style={{ fg: { - connected: theme().success, - failed: theme().error, - disabled: theme().textMuted, + connected: theme.success, + failed: theme.error, + disabled: theme.textMuted, }[item.status], }} > @@ -72,7 +72,7 @@ export function Sidebar(props: { sessionID: string }) { {key}{" "} - + Connected {(val) => {val().error}} @@ -97,14 +97,14 @@ export function Sidebar(props: { sessionID: string }) { flexShrink={0} style={{ fg: { - connected: theme().success, - error: theme().error, + connected: theme.success, + error: theme.error, }[item.status], }} > • - + {item.id} {item.root} @@ -120,10 +120,10 @@ export function Sidebar(props: { sessionID: string }) { {(item) => ( - {Locale.truncateMiddle(item.file, 40)} + {Locale.truncateMiddle(item.file, 40)} - +{item.additions} - -{item.deletions} + +{item.additions} + -{item.deletions} )} @@ -137,7 +137,7 @@ export function Sidebar(props: { sessionID: string }) { {(todo) => ( - + [{todo.status === "completed" ? "✓" : " "}] {todo.content} )} diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-alert.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-alert.tsx index ed77e5c237..66c8334de9 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-alert.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-alert.tsx @@ -23,22 +23,22 @@ export function DialogAlert(props: DialogAlertProps) { {props.title} - esc + esc - {props.message} + {props.message} { props.onConfirm?.() dialog.clear() }} > - ok + ok diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx index f68db9328a..9f78188f83 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx @@ -35,10 +35,10 @@ export function DialogConfirm(props: DialogConfirmProps) { {props.title} - esc + esc - {props.message} + {props.message} @@ -46,14 +46,14 @@ export function DialogConfirm(props: DialogConfirmProps) { { if (key === "confirm") props.onConfirm?.() if (key === "cancel") props.onCancel?.() dialog.clear() }} > - {Locale.titlecase(key)} + {Locale.titlecase(key)} )} diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx index dccade3a5a..668ffb8d37 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx @@ -156,7 +156,7 @@ export function DialogSelect(props: DialogSelectProps) { {props.title} - esc + esc (props: DialogSelectProps) { props.onFilter?.(e) }) }} - focusedBackgroundColor={theme().backgroundPanel} - cursorColor={theme().primary} - focusedTextColor={theme().textMuted} + focusedBackgroundColor={theme.backgroundPanel} + cursorColor={theme.primary} + focusedTextColor={theme.textMuted} ref={(r) => { input = r input.focus() @@ -189,7 +189,7 @@ export function DialogSelect(props: DialogSelectProps) { <> 0 ? 1 : 0} paddingLeft={1}> - + {category} @@ -210,7 +210,7 @@ export function DialogSelect(props: DialogSelectProps) { if (index === -1) return moveTo(index) }} - backgroundColor={active() ? (option.bg ?? theme().primary) : RGBA.fromInts(0, 0, 0, 0)} + backgroundColor={active() ? (option.bg ?? theme.primary) : RGBA.fromInts(0, 0, 0, 0)} paddingLeft={1} paddingRight={1} gap={1} @@ -234,10 +234,8 @@ export function DialogSelect(props: DialogSelectProps) { {(item) => ( - - {Keybind.toString(item.keybind)} - - {item.title} + {Keybind.toString(item.keybind)} + {item.title} )} @@ -259,17 +257,17 @@ function Option(props: { <> {Locale.truncate(props.title, 62)} - {props.description} + {props.description} - {props.footer} + {props.footer} diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx index 491cd73c83..71e2702615 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog.tsx @@ -47,8 +47,8 @@ export function Dialog( customBorderChars={Border} width={props.size === "large" ? 80 : 60} maxWidth={dimensions().width - 2} - backgroundColor={theme().backgroundPanel} - borderColor={theme().border} + backgroundColor={theme.backgroundPanel} + borderColor={theme.border} paddingTop={1} > {props.children}