diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 4db4a4aa2c..4ac4b3cf93 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -2,7 +2,15 @@ import { render, useKeyboard, useRenderer, useTerminalDimensions } from "@opentu import { Clipboard } from "@tui/util/clipboard" import { TextAttributes } from "@opentui/core" import { RouteProvider, useRoute, type Route } from "@tui/context/route" -import { Switch, Match, createEffect, untrack, ErrorBoundary, createMemo, createSignal } from "solid-js" +import { + Switch, + Match, + createEffect, + untrack, + ErrorBoundary, + createMemo, + createSignal, +} from "solid-js" import { Installation } from "@/installation" import { Global } from "@/global" import { DialogProvider, useDialog } from "@tui/ui/dialog" @@ -11,11 +19,12 @@ import { SyncProvider, useSync } from "@tui/context/sync" import { LocalProvider, useLocal } from "@tui/context/local" import { DialogModel } from "@tui/component/dialog-model" import { DialogStatus } from "@tui/component/dialog-status" +import { DialogThemeList } from "@tui/component/dialog-theme-list" import { CommandProvider, useCommandDialog } from "@tui/component/dialog-command" import { DialogAgent } from "@tui/component/dialog-agent" import { DialogSessionList } from "@tui/component/dialog-session-list" import { KeybindProvider } 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" @@ -24,12 +33,18 @@ import { ToastProvider, useToast } from "./ui/toast" import { ExitProvider } from "./context/exit" import type { SessionRoute } from "./context/route" -export async function tui(input: { url: string; sessionID?: string; model?: string; agent?: string; onExit?: () => Promise }) { +export async function tui(input: { + url: string + sessionID?: string + model?: string + agent?: string + onExit?: () => Promise +}) { const routeData: Route | undefined = input.sessionID ? { - type: "session", - sessionID: input.sessionID, - } + type: "session", + sessionID: input.sessionID, + } : undefined await render( () => { @@ -40,17 +55,19 @@ export async function tui(input: { url: string; sessionID?: string; model?: stri - - - - - - - - - - - + + + + + + + + + + + + + @@ -63,7 +80,6 @@ export async function tui(input: { url: string; sessionID?: string; model?: stri targetFps: 60, gatherStats: false, exitOnCtrlC: false, - useKittyKeyboard: true, }, ) } @@ -80,6 +96,7 @@ function App() { const sync = useSync() const toast = useToast() const [sessionExists, setSessionExists] = createSignal(false) + const { theme } = useTheme() useKeyboard(async (evt) => { if (evt.meta && evt.name === "t") { @@ -97,14 +114,13 @@ function App() { createEffect(async () => { if (route.data.type === "session") { const data = route.data as SessionRoute - await sync.session.sync(data.sessionID) - .catch(() => { - toast.show({ - message: `Session not found: ${data.sessionID}`, - type: "error", - }) - return route.navigate({ type: "home" }) + await sync.session.sync(data.sessionID).catch(() => { + toast.show({ + message: `Session not found: ${data.sessionID}`, + type: "error", }) + return route.navigate({ type: "home" }) + }) setSessionExists(true) } }) @@ -182,6 +198,14 @@ function App() { }, category: "System", }, + { + title: "Switch theme", + value: "theme.switch", + onSelect: () => { + dialog.replace(() => ) + }, + category: "System", + }, ]) createEffect(() => { @@ -205,7 +229,7 @@ function App() { { const text = renderer.getSelection()?.getSelectedText() if (text && text.length > 0) { @@ -231,27 +255,36 @@ 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..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-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..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 8833886f2d..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 new file mode 100644 index 0000000000..9f7a9203dc --- /dev/null +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-theme-list.tsx @@ -0,0 +1,52 @@ +import { DialogSelect, type DialogSelectRef } from "../ui/dialog-select" +import { THEMES, useTheme } from "../context/theme" +import { useDialog } from "../ui/dialog" +import { onCleanup, onMount } from "solid-js" + +export function DialogThemeList() { + const { selectedTheme, setSelectedTheme } = useTheme() + const options = Object.keys(THEMES).map((value) => ({ + title: value, + value: value as keyof typeof THEMES, + })) + const initial = selectedTheme() + const dialog = useDialog() + let confirmed = false + let ref: DialogSelectRef + + 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 we close the dialog without confirming, reset back to the initial theme + if (!confirmed) setSelectedTheme(initial) + }) + + return ( + { + setSelectedTheme(opt.value) + }} + onSelect={(opt) => { + setSelectedTheme(opt.value) + confirmed = true + dialog.clear() + }} + ref={(r) => { + ref = r + }} + onFilter={(query) => { + if (query.length === 0) { + setSelectedTheme(initial) + return + } + + const first = 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 02ec9df6bc..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/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx index ddd460ef60..b7d110bfe2 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" @@ -38,6 +38,7 @@ export function Autocomplete(props: { const sdk = useSDK() const sync = useSync() const command = useCommandDialog() + const { theme } = useTheme() const [store, setStore] = createStore({ index: 0, @@ -361,7 +362,7 @@ export function Autocomplete(props: { zIndex={100} {...SplitBorder} > - + - + {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 839563205c..bc1de162fd 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/index.tsx @@ -11,7 +11,7 @@ import { } from "@opentui/core" import { createEffect, createMemo, Match, Switch, type JSX, onMount } from "solid-js" import { useLocal } from "@tui/context/local" -import { Theme, syntaxTheme } 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" @@ -59,6 +59,7 @@ export function Prompt(props: PromptProps) { const history = usePromptHistory() const command = useCommandDialog() const renderer = useRenderer() + const { theme, syntaxTheme } = useTheme() const textareaKeybindings = createMemo(() => { const newlineBindings = keybind.all.input_newline || [] @@ -84,9 +85,9 @@ export function Prompt(props: PromptProps) { ] }) - const fileStyleId = syntaxTheme.getStyleId("extmark.file")! - const agentStyleId = syntaxTheme.getStyleId("extmark.agent")! - const pasteStyleId = syntaxTheme.getStyleId("extmark.paste")! + const fileStyleId = syntaxTheme().getStyleId("extmark.file")! + const agentStyleId = syntaxTheme().getStyleId("extmark.agent")! + const pasteStyleId = syntaxTheme().getStyleId("extmark.paste")! let promptPartTypeId: number command.register(() => { @@ -171,8 +172,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<{ @@ -475,35 +476,35 @@ export function Prompt(props: PromptProps) { flexDirection="row" {...SplitBorder} borderColor={ - keybind.leader ? Theme.accent : store.mode === "shell" ? Theme.secondary : undefined + keybind.leader ? theme.accent : store.mode === "shell" ? theme.secondary : theme.border } justifyContent="space-evenly" > - + {store.mode === "normal" ? ">" : "!"}