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 3029eafcce3..8371c395fbe 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 { useTheme } from "@tui/context/theme" +import { useTheme, selectedForeground } from "@tui/context/theme" import { SplitBorder } from "@tui/component/border" import { useCommandDialog } from "@tui/component/dialog-command" import { Locale } from "@/util/locale" @@ -455,7 +455,7 @@ export function Autocomplete(props: { {...SplitBorder} borderColor={theme.border} > - + - + {option.display} - + {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 b04cb7c6043..f2e97ff2324 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -791,10 +791,17 @@ export function Prompt(props: PromptProps) { height={1} border={["bottom"]} borderColor={theme.backgroundElement} - customBorderChars={{ - ...EmptyBorder, - horizontal: "▀", - }} + customBorderChars={ + theme.background.a != 0 + ? { + ...EmptyBorder, + horizontal: "▀", + } + : { + ...EmptyBorder, + horizontal: " ", + } + } /> diff --git a/packages/opencode/src/cli/cmd/tui/context/theme.tsx b/packages/opencode/src/cli/cmd/tui/context/theme.tsx index abca5ba2aac..4e3cc35315e 100644 --- a/packages/opencode/src/cli/cmd/tui/context/theme.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/theme.tsx @@ -33,7 +33,7 @@ import { createStore, produce } from "solid-js/store" import { Global } from "@/global" import { Filesystem } from "@/util/filesystem" -type Theme = { +type ThemeColors = { primary: RGBA secondary: RGBA accent: RGBA @@ -43,9 +43,11 @@ type Theme = { info: RGBA text: RGBA textMuted: RGBA + selectedListItemText: RGBA background: RGBA backgroundPanel: RGBA backgroundElement: RGBA + backgroundMenu: RGBA border: RGBA borderActive: RGBA borderSubtle: RGBA @@ -86,6 +88,27 @@ type Theme = { syntaxPunctuation: RGBA } +type Theme = ThemeColors & { + _hasSelectedListItemText: boolean +} + +export function selectedForeground(theme: Theme): RGBA { + // If theme explicitly defines selectedListItemText, use it + if (theme._hasSelectedListItemText) { + return theme.selectedListItemText + } + + // For transparent backgrounds, calculate contrast based on primary color + if (theme.background.a === 0) { + const { r, g, b } = theme.primary + const luminance = 0.299 * r + 0.587 * g + 0.114 * b + return luminance > 0.5 ? RGBA.fromInts(0, 0, 0) : RGBA.fromInts(255, 255, 255) + } + + // Fall back to background color + return theme.background +} + type HexColor = `#${string}` type RefName = string type Variant = { @@ -96,7 +119,10 @@ type ColorValue = HexColor | RefName | Variant | RGBA type ThemeJson = { $schema?: string defs?: Record - theme: Record + theme: Omit, "selectedListItemText" | "backgroundMenu"> & { + selectedListItemText?: ColorValue + backgroundMenu?: ColorValue + } } export const DEFAULT_THEMES: Record = { @@ -137,19 +163,44 @@ function resolveTheme(theme: ThemeJson, mode: "dark" | "light") { if (defs[c]) { return resolveColor(defs[c]) - } else if (theme.theme[c as keyof Theme]) { - return resolveColor(theme.theme[c as keyof Theme]) + } else if (theme.theme[c as keyof ThemeColors] !== undefined) { + return resolveColor(theme.theme[c as keyof ThemeColors]!) } else { throw new Error(`Color reference "${c}" not found in defs or theme`) } } return resolveColor(c[mode]) } - return Object.fromEntries( - Object.entries(theme.theme).map(([key, value]) => { - return [key, resolveColor(value)] - }), - ) as Theme + + const resolved = Object.fromEntries( + Object.entries(theme.theme) + .filter(([key]) => key !== "selectedListItemText" && key !== "backgroundMenu") + .map(([key, value]) => { + return [key, resolveColor(value)] + }), + ) as Partial + + // Handle selectedListItemText separately since it's optional + const hasSelectedListItemText = theme.theme.selectedListItemText !== undefined + if (hasSelectedListItemText) { + resolved.selectedListItemText = resolveColor(theme.theme.selectedListItemText!) + } else { + // Backward compatibility: if selectedListItemText is not defined, use background color + // This preserves the current behavior for all existing themes + resolved.selectedListItemText = resolved.background + } + + // Handle backgroundMenu - optional with fallback to backgroundElement + if (theme.theme.backgroundMenu !== undefined) { + resolved.backgroundMenu = resolveColor(theme.theme.backgroundMenu) + } else { + resolved.backgroundMenu = resolved.backgroundElement + } + + return { + ...resolved, + _hasSelectedListItemText: hasSelectedListItemText, + } as Theme } export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({ @@ -288,11 +339,13 @@ function generateSystem(colors: TerminalColors, mode: "dark" | "light"): ThemeJs // Text colors text: fg, textMuted, + selectedListItemText: bg, // Background colors background: bg, backgroundPanel: grays[2], backgroundElement: grays[3], + backgroundMenu: grays[3], // Border colors borderSubtle: grays[6], 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 6bb59d6c7bd..96ef982d7f2 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-alert.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-alert.tsx @@ -38,7 +38,7 @@ export function DialogAlert(props: DialogAlertProps) { 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 dd5b238b11b..9d0e7d2c74f 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-confirm.tsx @@ -53,7 +53,9 @@ export function DialogConfirm(props: DialogConfirmProps) { dialog.clear() }} > - {Locale.titlecase(key)} + + {Locale.titlecase(key)} + )} diff --git a/packages/opencode/src/cli/cmd/tui/ui/dialog-help.tsx b/packages/opencode/src/cli/cmd/tui/ui/dialog-help.tsx index f522fca9eeb..db9648f2c7d 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-help.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-help.tsx @@ -28,7 +28,7 @@ export function DialogHelp() { dialog.clear()}> - ok + ok 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 b33641ecdaa..987bbd0b941 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 { useTheme } from "@tui/context/theme" +import { useTheme, selectedForeground } from "@tui/context/theme" import { entries, filter, flatMap, groupBy, pipe, take } from "remeda" import { batch, createEffect, createMemo, For, Show, type JSX } from "solid-js" import { createStore } from "solid-js/store" @@ -262,32 +262,29 @@ function Option(props: { onMouseOver?: () => void }) { const { theme } = useTheme() + const fg = selectedForeground(theme) return ( <> - - ◆ + + ● {Locale.truncate(props.title, 62)} - {props.description} + {props.description} - {props.footer} + {props.footer} diff --git a/packages/web/public/theme.json b/packages/web/public/theme.json index b3e97f7ca89..7c80776344f 100644 --- a/packages/web/public/theme.json +++ b/packages/web/public/theme.json @@ -46,6 +46,7 @@ "info": { "$ref": "#/definitions/colorValue" }, "text": { "$ref": "#/definitions/colorValue" }, "textMuted": { "$ref": "#/definitions/colorValue" }, + "selectedListItemText": { "$ref": "#/definitions/colorValue" }, "background": { "$ref": "#/definitions/colorValue" }, "backgroundPanel": { "$ref": "#/definitions/colorValue" }, "backgroundElement": { "$ref": "#/definitions/colorValue" },