diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index fee65414ea..9974879413 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -29,6 +29,63 @@ import { Session as SessionApi } from "@/session" import { TuiEvent } from "./event" import { KVProvider, useKV } from "./context/kv" +async function getTerminalBackgroundColor(): Promise<"dark" | "light"> { + return new Promise((resolve) => { + let timeout: NodeJS.Timeout + + const cleanup = () => { + process.stdin.setRawMode(false) + process.stdin.removeListener("data", handler) + clearTimeout(timeout) + } + + const handler = (data: Buffer) => { + const str = data.toString() + const match = str.match(/\x1b]11;([^\x07\x1b]+)/) + if (match) { + cleanup() + const color = match[1] + // Parse RGB values from color string + // Formats: rgb:RR/GG/BB or #RRGGBB or rgb(R,G,B) + let r = 0, + g = 0, + b = 0 + + if (color.startsWith("rgb:")) { + const parts = color.substring(4).split("/") + r = parseInt(parts[0], 16) >> 8 // Convert 16-bit to 8-bit + g = parseInt(parts[1], 16) >> 8 // Convert 16-bit to 8-bit + b = parseInt(parts[2], 16) >> 8 // Convert 16-bit to 8-bit + } else if (color.startsWith("#")) { + r = parseInt(color.substring(1, 3), 16) + g = parseInt(color.substring(3, 5), 16) + b = parseInt(color.substring(5, 7), 16) + } else if (color.startsWith("rgb(")) { + const parts = color.substring(4, color.length - 1).split(",") + r = parseInt(parts[0]) + g = parseInt(parts[1]) + b = parseInt(parts[2]) + } + + // Calculate luminance using relative luminance formula + const luminance = (0.299 * r + 0.587 * g + 0.114 * b) / 255 + + // Determine if dark or light based on luminance threshold + resolve(luminance > 0.5 ? "light" : "dark") + } + } + + process.stdin.setRawMode(true) + process.stdin.on("data", handler) + process.stdout.write("\x1b]11;?\x07") + + timeout = setTimeout(() => { + cleanup() + resolve("dark") + }, 1000) + }) +} + export function tui(input: { url: string sessionID?: string @@ -38,7 +95,9 @@ export function tui(input: { onExit?: () => Promise }) { // promise to prevent immediate exit - return new Promise((resolve) => { + return new Promise(async (resolve) => { + const mode = await getTerminalBackgroundColor() + const routeData: Route | undefined = input.sessionID ? { type: "session", @@ -65,8 +124,12 @@ export function tui(input: { - - + + @@ -109,7 +172,7 @@ function App() { const sync = useSync() const toast = useToast() const [sessionExists, setSessionExists] = createSignal(false) - const { theme } = useTheme() + const { theme, mode, setMode } = useTheme() const exit = useExit() useKeyboard(async (evt) => { @@ -238,6 +301,14 @@ function App() { }, category: "System", }, + { + title: `Switch to ${mode() === "dark" ? "light" : "dark"} mode`, + value: "theme.switch_mode", + onSelect: () => { + setMode(mode() === "dark" ? "light" : "dark") + }, + category: "System", + }, { title: "Help", value: "help.show", @@ -251,7 +322,7 @@ function App() { value: "app.exit", onSelect: exit, category: "System", - } + }, ]) createEffect(() => { @@ -335,7 +406,9 @@ function App() { paddingRight={1} > open - code + + code{" "} + v{Installation.VERSION} 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 e958580e0c..d1ef5ca56c 100644 --- a/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/dialog-status.tsx @@ -14,12 +14,14 @@ export function DialogStatus() { return ( - Status + + Status + esc 0} fallback={No MCP Servers}> - {Object.keys(sync.data.mcp).length} MCP Servers + {Object.keys(sync.data.mcp).length} MCP Servers {([key, item]) => ( @@ -35,7 +37,7 @@ export function DialogStatus() { > • - + {key}{" "} @@ -52,7 +54,7 @@ export function DialogStatus() { {sync.data.lsp.length > 0 && ( - {sync.data.lsp.length} LSP Servers + {sync.data.lsp.length} LSP Servers {(item) => ( @@ -67,7 +69,7 @@ export function DialogStatus() { > • - + {item.id} {item.root} @@ -75,9 +77,12 @@ export function DialogStatus() { )} - 0} fallback={No Formatters}> + 0} + fallback={No Formatters} + > - {enabledFormatters().length} Formatters + {enabledFormatters().length} Formatters {(item) => ( @@ -89,7 +94,7 @@ export function DialogStatus() { > • - + {item.name} 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 25e394a619..e45fa677f2 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, batch } from "solid-js" import { useLocal } from "@tui/context/local" -import { SyntaxTheme, useTheme } 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" @@ -60,7 +60,7 @@ export function Prompt(props: PromptProps) { const history = usePromptHistory() const command = useCommandDialog() const renderer = useRenderer() - const { theme } = useTheme() + const { theme, syntax } = useTheme() const textareaKeybindings = createMemo(() => { const newlineBindings = keybind.all.input_newline || [] @@ -86,9 +86,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 = syntax().getStyleId("extmark.file")! + const agentStyleId = syntax().getStyleId("extmark.agent")! + const pasteStyleId = syntax().getStyleId("extmark.paste")! let promptPartTypeId: number command.register(() => { @@ -315,9 +315,9 @@ export function Prompt(props: PromptProps) { const sessionID = props.sessionID ? props.sessionID : await (async () => { - const sessionID = await sdk.client.session.create({}).then((x) => x.data!.id) - return sessionID - })() + const sessionID = await sdk.client.session.create({}).then((x) => x.data!.id) + return sessionID + })() const messageID = Identifier.ascending("message") let inputText = store.prompt.input @@ -680,7 +680,7 @@ export function Prompt(props: PromptProps) { onMouseDown={(r: MouseEvent) => r.target?.focus()} focusedBackgroundColor={theme.backgroundElement} cursorColor={theme.primary} - syntaxStyle={SyntaxTheme} + syntaxStyle={syntax()} /> - + {local.model.parsed().provider}{" "} {local.model.parsed().model} @@ -701,14 +701,14 @@ export function Prompt(props: PromptProps) { - + esc interrupt {props.hint!} - + ctrl+p commands diff --git a/packages/opencode/src/cli/cmd/tui/context/theme.tsx b/packages/opencode/src/cli/cmd/tui/context/theme.tsx index e1bb474b56..f402b8ffc3 100644 --- a/packages/opencode/src/cli/cmd/tui/context/theme.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/theme.tsx @@ -68,6 +68,15 @@ type Theme = { markdownImage: RGBA markdownImageText: RGBA markdownCodeBlock: RGBA + syntaxComment: RGBA + syntaxKeyword: RGBA + syntaxFunction: RGBA + syntaxVariable: RGBA + syntaxString: RGBA + syntaxNumber: RGBA + syntaxType: RGBA + syntaxOperator: RGBA + syntaxPunctuation: RGBA } type HexColor = `#${string}` @@ -83,37 +92,36 @@ type ThemeJson = { theme: Record } -export const THEMES: Record = { - 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), - ["one-dark"]: 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), +export const THEMES: Record = { + aura, + ayu, + catppuccin, + cobalt2, + dracula, + everforest, + github, + gruvbox, + kanagawa, + material, + matrix, + monokai, + nord, + ["one-dark"]: onedark, + opencode, + palenight, + rosepine, + solarized, + synthwave84, + tokyonight, + vesper, + zenburn, } -function resolveTheme(theme: ThemeJson) { +function resolveTheme(theme: ThemeJson, mode: "dark" | "light") { const defs = theme.defs ?? {} function resolveColor(c: ColorValue): RGBA { if (typeof c === "string") return c.startsWith("#") ? RGBA.fromHex(c) : resolveColor(defs[c]) - // TODO: support light theme when opentui has the equivalent of lipgloss.AdaptiveColor - return resolveColor(c.dark) + return resolveColor(c[mode]) } return Object.fromEntries( Object.entries(theme.theme).map(([key, value]) => { @@ -122,517 +130,518 @@ function resolveTheme(theme: ThemeJson) { ) as Theme } -const syntaxThemeDark = [ - { - scope: ["prompt"], - style: { - foreground: "#7dcfff", - }, - }, - { - scope: ["extmark.file"], - style: { - foreground: "#ff9e64", - bold: true, - }, - }, - { - scope: ["extmark.agent"], - style: { - foreground: "#bb9af7", - bold: true, - }, - }, - { - scope: ["extmark.paste"], - style: { - foreground: "#1a1b26", - background: "#ff9e64", - bold: true, - }, - }, - { - scope: ["comment"], - style: { - foreground: "#565f89", - italic: true, - }, - }, - { - scope: ["comment.documentation"], - style: { - foreground: "#565f89", - italic: true, - }, - }, - { - scope: ["string", "symbol"], - style: { - foreground: "#9ece6a", - }, - }, - { - scope: ["number", "boolean"], - style: { - foreground: "#ff9e64", - }, - }, - { - scope: ["character.special"], - style: { - foreground: "#9ece6a", - }, - }, - { - scope: ["keyword.return", "keyword.conditional", "keyword.repeat", "keyword.coroutine"], - style: { - foreground: "#bb9af7", - italic: true, - }, - }, - { - scope: ["keyword.type"], - style: { - foreground: "#2ac3de", - bold: true, - italic: true, - }, - }, - { - scope: ["keyword.function", "function.method"], - style: { - foreground: "#bb9af7", - }, - }, - { - scope: ["keyword"], - style: { - foreground: "#bb9af7", - italic: true, - }, - }, - { - scope: ["keyword.import"], - style: { - foreground: "#bb9af7", - }, - }, - { - scope: ["operator", "keyword.operator", "punctuation.delimiter"], - style: { - foreground: "#89ddff", - }, - }, - { - scope: ["keyword.conditional.ternary"], - style: { - foreground: "#89ddff", - }, - }, - { - scope: ["variable", "variable.parameter", "function.method.call", "function.call"], - style: { - foreground: "#7dcfff", - }, - }, - { - scope: ["variable.member", "function", "constructor"], - style: { - foreground: "#7aa2f7", - }, - }, - { - scope: ["type", "module"], - style: { - foreground: "#2ac3de", - }, - }, - { - scope: ["constant"], - style: { - foreground: "#ff9e64", - }, - }, - { - scope: ["property"], - style: { - foreground: "#73daca", - }, - }, - { - scope: ["class"], - style: { - foreground: "#2ac3de", - }, - }, - { - scope: ["parameter"], - style: { - foreground: "#e0af68", - }, - }, - { - scope: ["punctuation", "punctuation.bracket"], - style: { - foreground: "#89ddff", - }, - }, - { - scope: [ - "variable.builtin", - "type.builtin", - "function.builtin", - "module.builtin", - "constant.builtin", - ], - style: { - foreground: "#f7768e", - }, - }, - { - scope: ["variable.super"], - style: { - foreground: "#f7768e", - }, - }, - { - scope: ["string.escape", "string.regexp"], - style: { - foreground: "#bb9af7", - }, - }, - { - scope: ["keyword.directive"], - style: { - foreground: "#bb9af7", - italic: true, - }, - }, - { - scope: ["punctuation.special"], - style: { - foreground: "#89ddff", - }, - }, - { - scope: ["keyword.modifier"], - style: { - foreground: "#bb9af7", - italic: true, - }, - }, - { - scope: ["keyword.exception"], - style: { - foreground: "#bb9af7", - italic: true, - }, - }, - // Markdown specific styles - { - scope: ["markup.heading"], - style: { - foreground: "#7aa2f7", - bold: true, - }, - }, - { - scope: ["markup.heading.1"], - style: { - foreground: "#bb9af7", - bold: true, - }, - }, - { - scope: ["markup.heading.2"], - style: { - foreground: "#7aa2f7", - bold: true, - }, - }, - { - scope: ["markup.heading.3"], - style: { - foreground: "#7dcfff", - bold: true, - }, - }, - { - scope: ["markup.heading.4"], - style: { - foreground: "#73daca", - bold: true, - }, - }, - { - scope: ["markup.heading.5"], - style: { - foreground: "#9ece6a", - bold: true, - }, - }, - { - scope: ["markup.heading.6"], - style: { - foreground: "#565f89", - bold: true, - }, - }, - { - scope: ["markup.bold", "markup.strong"], - style: { - foreground: "#e6edf3", - bold: true, - }, - }, - { - scope: ["markup.italic"], - style: { - foreground: "#e6edf3", - italic: true, - }, - }, - { - scope: ["markup.list"], - style: { - foreground: "#ff9e64", - }, - }, - { - scope: ["markup.quote"], - style: { - foreground: "#565f89", - italic: true, - }, - }, - { - scope: ["markup.raw", "markup.raw.block"], - style: { - foreground: "#9ece6a", - }, - }, - { - scope: ["markup.raw.inline"], - style: { - foreground: "#9ece6a", - background: "#1a1b26", - }, - }, - { - scope: ["markup.link"], - style: { - foreground: "#7aa2f7", - underline: true, - }, - }, - { - scope: ["markup.link.label"], - style: { - foreground: "#7dcfff", - underline: true, - }, - }, - { - scope: ["markup.link.url"], - style: { - foreground: "#7aa2f7", - underline: true, - }, - }, - { - scope: ["label"], - style: { - foreground: "#73daca", - }, - }, - { - scope: ["spell", "nospell"], - style: { - foreground: "#e6edf3", - }, - }, - { - scope: ["conceal"], - style: { - foreground: "#565f89", - }, - }, - // Additional common highlight groups - { - scope: ["string.special", "string.special.url"], - style: { - foreground: "#73daca", - underline: true, - }, - }, - { - scope: ["character"], - style: { - foreground: "#9ece6a", - }, - }, - { - scope: ["float"], - style: { - foreground: "#ff9e64", - }, - }, - { - scope: ["comment.error"], - style: { - foreground: "#f7768e", - italic: true, - bold: true, - }, - }, - { - scope: ["comment.warning"], - style: { - foreground: "#e0af68", - italic: true, - bold: true, - }, - }, - { - scope: ["comment.todo", "comment.note"], - style: { - foreground: "#7aa2f7", - italic: true, - bold: true, - }, - }, - { - scope: ["namespace"], - style: { - foreground: "#2ac3de", - }, - }, - { - scope: ["field"], - style: { - foreground: "#73daca", - }, - }, - { - scope: ["type.definition"], - style: { - foreground: "#2ac3de", - bold: true, - }, - }, - { - scope: ["keyword.export"], - style: { - foreground: "#bb9af7", - }, - }, - { - scope: ["attribute", "annotation"], - style: { - foreground: "#e0af68", - }, - }, - { - scope: ["tag"], - style: { - foreground: "#f7768e", - }, - }, - { - scope: ["tag.attribute"], - style: { - foreground: "#bb9af7", - }, - }, - { - scope: ["tag.delimiter"], - style: { - foreground: "#89ddff", - }, - }, - { - scope: ["markup.strikethrough"], - style: { - foreground: "#565f89", - }, - }, - { - scope: ["markup.underline"], - style: { - foreground: "#e6edf3", - underline: true, - }, - }, - { - scope: ["markup.list.checked"], - style: { - foreground: "#9ece6a", - }, - }, - { - scope: ["markup.list.unchecked"], - style: { - foreground: "#565f89", - }, - }, - { - scope: ["diff.plus"], - style: { - foreground: "#9ece6a", - }, - }, - { - scope: ["diff.minus"], - style: { - foreground: "#f7768e", - }, - }, - { - scope: ["diff.delta"], - style: { - foreground: "#7dcfff", - }, - }, - { - scope: ["error"], - style: { - foreground: "#f7768e", - bold: true, - }, - }, - { - scope: ["warning"], - style: { - foreground: "#e0af68", - bold: true, - }, - }, - { - scope: ["info"], - style: { - foreground: "#7dcfff", - }, - }, - { - scope: ["debug"], - style: { - foreground: "#565f89", - }, - }, -] - -export const SyntaxTheme = SyntaxStyle.fromTheme(syntaxThemeDark) - export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({ name: "Theme", - init: () => { + init: (props: { mode: "dark" | "light" }) => { const sync = useSync() const kv = useKV() const [theme, setTheme] = createSignal(sync.data.config.theme ?? kv.get("theme", "opencode")) + const [mode, setMode] = createSignal(props.mode) const values = createMemo(() => { - return THEMES[theme()] ?? THEMES.opencode + return resolveTheme(THEMES[theme()] ?? THEMES.opencode, mode()) + }) + + const syntax = createMemo(() => { + return SyntaxStyle.fromTheme([ + { + scope: ["prompt"], + style: { + foreground: values().accent, + }, + }, + { + scope: ["extmark.file"], + style: { + foreground: values().warning, + bold: true, + }, + }, + { + scope: ["extmark.agent"], + style: { + foreground: values().secondary, + bold: true, + }, + }, + { + scope: ["extmark.paste"], + style: { + foreground: values().background, + background: values().warning, + bold: true, + }, + }, + { + scope: ["comment"], + style: { + foreground: values().syntaxComment, + italic: true, + }, + }, + { + scope: ["comment.documentation"], + style: { + foreground: values().syntaxComment, + italic: true, + }, + }, + { + scope: ["string", "symbol"], + style: { + foreground: values().syntaxString, + }, + }, + { + scope: ["number", "boolean"], + style: { + foreground: values().syntaxNumber, + }, + }, + { + scope: ["character.special"], + style: { + foreground: values().syntaxString, + }, + }, + { + scope: ["keyword.return", "keyword.conditional", "keyword.repeat", "keyword.coroutine"], + style: { + foreground: values().syntaxKeyword, + italic: true, + }, + }, + { + scope: ["keyword.type"], + style: { + foreground: values().syntaxType, + bold: true, + italic: true, + }, + }, + { + scope: ["keyword.function", "function.method"], + style: { + foreground: values().syntaxFunction, + }, + }, + { + scope: ["keyword"], + style: { + foreground: values().syntaxKeyword, + italic: true, + }, + }, + { + scope: ["keyword.import"], + style: { + foreground: values().syntaxKeyword, + }, + }, + { + scope: ["operator", "keyword.operator", "punctuation.delimiter"], + style: { + foreground: values().syntaxOperator, + }, + }, + { + scope: ["keyword.conditional.ternary"], + style: { + foreground: values().syntaxOperator, + }, + }, + { + scope: ["variable", "variable.parameter", "function.method.call", "function.call"], + style: { + foreground: values().syntaxVariable, + }, + }, + { + scope: ["variable.member", "function", "constructor"], + style: { + foreground: values().syntaxFunction, + }, + }, + { + scope: ["type", "module"], + style: { + foreground: values().syntaxType, + }, + }, + { + scope: ["constant"], + style: { + foreground: values().syntaxNumber, + }, + }, + { + scope: ["property"], + style: { + foreground: values().syntaxVariable, + }, + }, + { + scope: ["class"], + style: { + foreground: values().syntaxType, + }, + }, + { + scope: ["parameter"], + style: { + foreground: values().syntaxVariable, + }, + }, + { + scope: ["punctuation", "punctuation.bracket"], + style: { + foreground: values().syntaxPunctuation, + }, + }, + { + scope: [ + "variable.builtin", + "type.builtin", + "function.builtin", + "module.builtin", + "constant.builtin", + ], + style: { + foreground: values().error, + }, + }, + { + scope: ["variable.super"], + style: { + foreground: values().error, + }, + }, + { + scope: ["string.escape", "string.regexp"], + style: { + foreground: values().syntaxKeyword, + }, + }, + { + scope: ["keyword.directive"], + style: { + foreground: values().syntaxKeyword, + italic: true, + }, + }, + { + scope: ["punctuation.special"], + style: { + foreground: values().syntaxOperator, + }, + }, + { + scope: ["keyword.modifier"], + style: { + foreground: values().syntaxKeyword, + italic: true, + }, + }, + { + scope: ["keyword.exception"], + style: { + foreground: values().syntaxKeyword, + italic: true, + }, + }, + // Markdown specific styles + { + scope: ["markup.heading"], + style: { + foreground: values().markdownHeading, + bold: true, + }, + }, + { + scope: ["markup.heading.1"], + style: { + foreground: values().markdownHeading, + bold: true, + }, + }, + { + scope: ["markup.heading.2"], + style: { + foreground: values().markdownHeading, + bold: true, + }, + }, + { + scope: ["markup.heading.3"], + style: { + foreground: values().markdownHeading, + bold: true, + }, + }, + { + scope: ["markup.heading.4"], + style: { + foreground: values().markdownHeading, + bold: true, + }, + }, + { + scope: ["markup.heading.5"], + style: { + foreground: values().markdownHeading, + bold: true, + }, + }, + { + scope: ["markup.heading.6"], + style: { + foreground: values().markdownHeading, + bold: true, + }, + }, + { + scope: ["markup.bold", "markup.strong"], + style: { + foreground: values().markdownStrong, + bold: true, + }, + }, + { + scope: ["markup.italic"], + style: { + foreground: values().markdownEmph, + italic: true, + }, + }, + { + scope: ["markup.list"], + style: { + foreground: values().markdownListItem, + }, + }, + { + scope: ["markup.quote"], + style: { + foreground: values().markdownBlockQuote, + italic: true, + }, + }, + { + scope: ["markup.raw", "markup.raw.block"], + style: { + foreground: values().markdownCode, + }, + }, + { + scope: ["markup.raw.inline"], + style: { + foreground: values().markdownCode, + background: values().background, + }, + }, + { + scope: ["markup.link"], + style: { + foreground: values().markdownLink, + underline: true, + }, + }, + { + scope: ["markup.link.label"], + style: { + foreground: values().markdownLinkText, + underline: true, + }, + }, + { + scope: ["markup.link.url"], + style: { + foreground: values().markdownLink, + underline: true, + }, + }, + { + scope: ["label"], + style: { + foreground: values().markdownLinkText, + }, + }, + { + scope: ["spell", "nospell"], + style: { + foreground: values().text, + }, + }, + { + scope: ["conceal"], + style: { + foreground: values().textMuted, + }, + }, + // Additional common highlight groups + { + scope: ["string.special", "string.special.url"], + style: { + foreground: values().markdownLink, + underline: true, + }, + }, + { + scope: ["character"], + style: { + foreground: values().syntaxString, + }, + }, + { + scope: ["float"], + style: { + foreground: values().syntaxNumber, + }, + }, + { + scope: ["comment.error"], + style: { + foreground: values().error, + italic: true, + bold: true, + }, + }, + { + scope: ["comment.warning"], + style: { + foreground: values().warning, + italic: true, + bold: true, + }, + }, + { + scope: ["comment.todo", "comment.note"], + style: { + foreground: values().info, + italic: true, + bold: true, + }, + }, + { + scope: ["namespace"], + style: { + foreground: values().syntaxType, + }, + }, + { + scope: ["field"], + style: { + foreground: values().syntaxVariable, + }, + }, + { + scope: ["type.definition"], + style: { + foreground: values().syntaxType, + bold: true, + }, + }, + { + scope: ["keyword.export"], + style: { + foreground: values().syntaxKeyword, + }, + }, + { + scope: ["attribute", "annotation"], + style: { + foreground: values().warning, + }, + }, + { + scope: ["tag"], + style: { + foreground: values().error, + }, + }, + { + scope: ["tag.attribute"], + style: { + foreground: values().syntaxKeyword, + }, + }, + { + scope: ["tag.delimiter"], + style: { + foreground: values().syntaxOperator, + }, + }, + { + scope: ["markup.strikethrough"], + style: { + foreground: values().textMuted, + }, + }, + { + scope: ["markup.underline"], + style: { + foreground: values().text, + underline: true, + }, + }, + { + scope: ["markup.list.checked"], + style: { + foreground: values().success, + }, + }, + { + scope: ["markup.list.unchecked"], + style: { + foreground: values().textMuted, + }, + }, + { + scope: ["diff.plus"], + style: { + foreground: values().diffAdded, + }, + }, + { + scope: ["diff.minus"], + style: { + foreground: values().diffRemoved, + }, + }, + { + scope: ["diff.delta"], + style: { + foreground: values().diffContext, + }, + }, + { + scope: ["error"], + style: { + foreground: values().error, + bold: true, + }, + }, + { + scope: ["warning"], + style: { + foreground: values().warning, + bold: true, + }, + }, + { + scope: ["info"], + style: { + foreground: values().info, + }, + }, + { + scope: ["debug"], + style: { + foreground: values().textMuted, + }, + }, + ]) }) return { @@ -645,6 +654,11 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({ get selected() { return theme() }, + syntax, + mode, + setMode(mode: "dark" | "light") { + setMode(mode) + }, set(theme: string) { if (!THEMES[theme]) return setTheme(theme) diff --git a/packages/opencode/src/cli/cmd/tui/routes/home.tsx b/packages/opencode/src/cli/cmd/tui/routes/home.tsx index f249151130..7f8d00b87e 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/home.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/home.tsx @@ -27,7 +27,7 @@ export function Home() { const Hint = ( 0}> - + mcp errors{" "} @@ -76,7 +76,7 @@ function HelpRow(props: ParentProps<{ keybind: keyof KeybindsConfig }>) { const { theme } = useTheme() return ( - {props.children} + {props.children} {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 31d25baa37..4427d5ea75 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/header.tsx @@ -51,7 +51,7 @@ export function Header() { borderColor={theme.backgroundElement} flexShrink={0} > - + #{" "} {session().title} @@ -64,7 +64,7 @@ export function Header() { - + /share to create a shareable link 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 7eb05cd4ee..9868d3affe 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, useTheme } from "@tui/context/theme" +import { useTheme } from "@tui/context/theme" import { BoxRenderable, ScrollBoxRenderable, addDefaultParsers } from "@opentui/core" import { Prompt, type PromptRef } from "@tui/component/prompt" import type { @@ -641,7 +641,7 @@ function UserMessage(props: { borderColor={color()} flexShrink={0} > - {text()?.text} + {text()?.text} @@ -652,7 +652,7 @@ function UserMessage(props: { return theme.secondary }) return ( - + {" "} {MIME_BADGE[file.mime] ?? file.mime}{" "} @@ -667,7 +667,7 @@ function UserMessage(props: { - + {sync.data.config.username ?? "You"}{" "} - {props.part.text.trim()} + {props.part.text.trim()} @@ -791,13 +791,14 @@ function ReasoningPart(props: { part: ReasoningPart; message: AssistantMessage } function TextPart(props: { part: TextPart; message: AssistantMessage }) { const ctx = use() + const { syntax } = useTheme() return ( @@ -997,7 +998,7 @@ ToolRegistry.register({ name: "write", container: "block", render(props) { - const { theme } = useTheme() + const { theme, syntax } = useTheme() const lines = createMemo(() => { return props.input.content?.split("\n") ?? [] }) @@ -1028,7 +1029,7 @@ ToolRegistry.register({ @@ -1131,6 +1132,7 @@ ToolRegistry.register({ container: "block", render(props) { const ctx = use() + const { theme, syntax } = useTheme() const style = createMemo(() => (ctx.width > 120 ? "split" : "stacked")) @@ -1210,21 +1212,21 @@ ToolRegistry.register({ - {props.permission["diff"]?.trim()} + {props.permission["diff"]?.trim()} - + - + - + @@ -1237,6 +1239,7 @@ ToolRegistry.register({ name: "patch", container: "block", render(props) { + const { theme } = useTheme() return ( <> @@ -1244,7 +1247,7 @@ ToolRegistry.register({ - {props.output?.trim()} + {props.output?.trim()} 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 380d82964f..c63297db2f 100644 --- a/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx +++ b/packages/opencode/src/cli/cmd/tui/routes/session/sidebar.tsx @@ -42,7 +42,7 @@ export function Sidebar(props: { sessionID: string }) { - + {session().title} @@ -50,7 +50,7 @@ export function Sidebar(props: { sessionID: string }) { - + Context {context()?.tokens ?? 0} tokens @@ -59,7 +59,7 @@ export function Sidebar(props: { sessionID: string }) { 0}> - + MCP @@ -77,7 +77,7 @@ export function Sidebar(props: { sessionID: string }) { > • - + {key}{" "} @@ -96,7 +96,7 @@ export function Sidebar(props: { sessionID: string }) { 0}> - + LSP @@ -123,7 +123,7 @@ export function Sidebar(props: { sessionID: string }) { - + Modified Files @@ -155,7 +155,7 @@ export function Sidebar(props: { sessionID: string }) { 0}> - + Todo 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 18b53498af..373c4995c8 100644 --- a/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx +++ b/packages/opencode/src/cli/cmd/tui/ui/dialog-select.tsx @@ -161,7 +161,9 @@ export function DialogSelect(props: DialogSelectProps) { - {props.title} + + {props.title} + esc