summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--bun.lock20
-rw-r--r--packages/opencode/package.json4
-rw-r--r--packages/opencode/src/cli/cmd/tui/component/dialog-theme-list.tsx12
-rw-r--r--packages/opencode/src/cli/cmd/tui/context/theme.tsx1223
-rw-r--r--packages/opencode/src/cli/cmd/tui/routes/session/index.tsx1
-rw-r--r--packages/opencode/src/cli/cmd/tui/util/terminal.ts114
6 files changed, 843 insertions, 531 deletions
diff --git a/bun.lock b/bun.lock
index cf47b5efe..226cf3624 100644
--- a/bun.lock
+++ b/bun.lock
@@ -185,8 +185,8 @@
"@opencode-ai/plugin": "workspace:*",
"@opencode-ai/script": "workspace:*",
"@opencode-ai/sdk": "workspace:*",
- "@opentui/core": "0.1.36",
- "@opentui/solid": "0.1.36",
+ "@opentui/core": "0.0.0-20251106-788e97e4",
+ "@opentui/solid": "0.0.0-20251106-788e97e4",
"@parcel/watcher": "2.5.1",
"@pierre/precision-diffs": "catalog:",
"@solid-primitives/event-bus": "1.1.2",
@@ -962,21 +962,21 @@
"@opentelemetry/api": ["@opentelemetry/[email protected]", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
- "@opentui/core": ["@opentui/[email protected]", "", { "dependencies": { "bun-ffi-structs": "^0.1.0", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.36", "@opentui/core-darwin-x64": "0.1.36", "@opentui/core-linux-arm64": "0.1.36", "@opentui/core-linux-x64": "0.1.36", "@opentui/core-win32-arm64": "0.1.36", "@opentui/core-win32-x64": "0.1.36", "bun-webgpu": "0.1.3", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-urDrj33udJ0dJGkZv+T5U0mCFBOOvUt9Tvqkrj8aRvi6kN0Bc5d2COuWcpAKo0TO9/PvjSwHC+CMnw2Sr46/ug=="],
+ "@opentui/core": ["@opentui/[email protected]", "", { "dependencies": { "bun-ffi-structs": "^0.1.0", "jimp": "1.6.0", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.0.0-20251106-788e97e4", "@opentui/core-darwin-x64": "0.0.0-20251106-788e97e4", "@opentui/core-linux-arm64": "0.0.0-20251106-788e97e4", "@opentui/core-linux-x64": "0.0.0-20251106-788e97e4", "@opentui/core-win32-arm64": "0.0.0-20251106-788e97e4", "@opentui/core-win32-x64": "0.0.0-20251106-788e97e4", "bun-webgpu": "0.1.3", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-Es2Oe7/J/yb58e0jjq/04pV9Mekx6hM4go4C5uTiZksX3asfIGWk553cuf5WlWj0PDlVnC+s7Nnayi/NbLJ5jQ=="],
- "@opentui/core-darwin-arm64": ["@opentui/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-/fb0k1H0CeTroVt2UoEAcVrEx1cIYy4B2zfX0MrwUkIfXi36aoIBnisBeYvyCpsQfxFAkyLYCCA3NzaYEyC5hg=="],
+ "@opentui/core-darwin-arm64": ["@opentui/[email protected]", "", { "os": "darwin", "cpu": "arm64" }, "sha512-EOO8SSIYJBIh+Sd9bgVTiQmt+TEJmfg65/oym54J4zfDtCYlAqSaLcRnDe4TzB+4hejV9of8etrG3ZZACBJT+A=="],
- "@opentui/core-darwin-x64": ["@opentui/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-PZMydJbSDUoEWqZsyEV8+FSwMT+r7mWFL0ABgdALI3AOrSr7Z8dMcRnFWl8LhriuHS589THvETJEN28L4q/E2Q=="],
+ "@opentui/core-darwin-x64": ["@opentui/[email protected]", "", { "os": "darwin", "cpu": "x64" }, "sha512-MUTt7CDbzL2afNGK8gJ4jUZd+AHiOUJEO0eJGDSfWU8DUs0zv8XoLZfaI5PPbkUPEL/7CEBMARAAiwfRtoG/4A=="],
- "@opentui/core-linux-arm64": ["@opentui/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-ATR+vdtraZEC/gHR1mQa/NYPlqFNBpsnnJAGepQmcxm85VceLYM701QaaIgNAwyYXiP6RQN1ZCv06MD1Ph1m4w=="],
+ "@opentui/core-linux-arm64": ["@opentui/[email protected]", "", { "os": "linux", "cpu": "arm64" }, "sha512-Zi1EzLCzooRfYoQnN/Dz8OxzrpRXByny8SJqhdO9ZP2mYX72yJ3AhUUW1Sl6YSzVi0H+QIKj7g+RX2KfsXIGFg=="],
- "@opentui/core-linux-x64": ["@opentui/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-INsnPtcZVx68C+0Vd0L9+akDwNbWblUDqLmY9CftfmeLFubzvJXNRYTBvr7lX68fcst6Ho+0beUxyUoClKc0rg=="],
+ "@opentui/core-linux-x64": ["@opentui/[email protected]", "", { "os": "linux", "cpu": "x64" }, "sha512-/E0XEBVzO4JEEhJGzfURF2tPxDE2oTODxlgNYYB1QbAuOsLcV69uSrwAjo1TxuIn4P78tBR+ZOlmONjroPqfbQ=="],
- "@opentui/core-win32-arm64": ["@opentui/[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-x9lDZTL+xE8jsG1hP4pdsqCsZBu77JNR/ze5F7ZQkYQEC6Zl/XJtL1YT08nUlWOu4NMSws2xXV0lS/sJkbEgPA=="],
+ "@opentui/core-win32-arm64": ["@opentui/[email protected]", "", { "os": "win32", "cpu": "arm64" }, "sha512-En/29cgpYVvzlrQ7fAoP+EUdzmczgMzBIGGM0RuLi2hmCmCqyMtOJ0EJUh9UXa5jYIXNGOP49sIP6bUBbvXt7g=="],
- "@opentui/core-win32-x64": ["@opentui/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-WVU+qtAfJe8ikPWbw8Hfli15GuQTMKiceTkF5lql5AQYy7PKYtGTzWszxOZKeUU1/eEd2X4REi8Bn0TprEMxYw=="],
+ "@opentui/core-win32-x64": ["@opentui/[email protected]", "", { "os": "win32", "cpu": "x64" }, "sha512-2lu0bgEi+k/1c9VHQFg3wjVxMgQnuZhs/6sDDpxk9eNS3fuHEJfZi0PFJQk2J4IFQL61nzukOvJKgYDWQvKB1g=="],
- "@opentui/solid": ["@opentui/[email protected]", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.36", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-oHI01kZgyNecvXRFyQKJEDC5TCcsvfTPxHCa/XjbcZzH2qE2rfYMUF0mpwlLqoY9b3pm3w7Tpa8upzi1euBGJg=="],
+ "@opentui/solid": ["@opentui/[email protected]", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.0.0-20251106-788e97e4", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.9", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.9" } }, "sha512-82rFS6BB60rJZU5Ad8Wf58V6HaMSkpnjciizkv3vsjJc9hvIAwLRNYqPypQB+etypuELhYMzzaVqt+wUsPHSqQ=="],
"@oslojs/asn1": ["@oslojs/[email protected]", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
diff --git a/packages/opencode/package.json b/packages/opencode/package.json
index 34c9081c5..b9e2c307b 100644
--- a/packages/opencode/package.json
+++ b/packages/opencode/package.json
@@ -54,8 +54,8 @@
"@opencode-ai/plugin": "workspace:*",
"@opencode-ai/script": "workspace:*",
"@opencode-ai/sdk": "workspace:*",
- "@opentui/core": "0.1.36",
- "@opentui/solid": "0.1.36",
+ "@opentui/core": "0.0.0-20251106-788e97e4",
+ "@opentui/solid": "0.0.0-20251106-788e97e4",
"@parcel/watcher": "2.5.1",
"@pierre/precision-diffs": "catalog:",
"@solid-primitives/event-bus": "1.1.2",
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 60411e562..5240603f8 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,26 +1,24 @@
import { DialogSelect, type DialogSelectRef } from "../ui/dialog-select"
-import { THEMES, useTheme } from "../context/theme"
+import { useTheme } from "../context/theme"
import { useDialog } from "../ui/dialog"
import { onCleanup, onMount } from "solid-js"
export function DialogThemeList() {
const theme = useTheme()
- const options = Object.keys(THEMES).map((value) => ({
+ const options = Object.keys(theme.all()).map((value) => ({
title: value,
- value: value as keyof typeof THEMES,
+ value: value,
}))
const dialog = useDialog()
let confirmed = false
- let ref: DialogSelectRef<keyof typeof THEMES>
+ let ref: DialogSelectRef<string>
const initial = theme.selected
onMount(() => {
- // highlight the first theme in the list when we open it for UX
- theme.set(Object.keys(THEMES)[0] as keyof typeof THEMES)
+ theme.set(Object.keys(theme.all())[0])
})
onCleanup(() => {
- // if we close the dialog without confirming, reset back to the initial theme
if (!confirmed) theme.set(initial)
})
diff --git a/packages/opencode/src/cli/cmd/tui/context/theme.tsx b/packages/opencode/src/cli/cmd/tui/context/theme.tsx
index 93eae6c21..a609bcc94 100644
--- a/packages/opencode/src/cli/cmd/tui/context/theme.tsx
+++ b/packages/opencode/src/cli/cmd/tui/context/theme.tsx
@@ -1,5 +1,5 @@
-import { SyntaxStyle, RGBA } from "@opentui/core"
-import { createMemo, createSignal } from "solid-js"
+import { SyntaxStyle, RGBA, type TerminalColors } from "@opentui/core"
+import { createMemo } from "solid-js"
import { useSync } from "@tui/context/sync"
import { createSimpleContext } from "./helper"
import aura from "./theme/aura.json" with { type: "json" }
@@ -26,6 +26,8 @@ import tokyonight from "./theme/tokyonight.json" with { type: "json" }
import vesper from "./theme/vesper.json" with { type: "json" }
import zenburn from "./theme/zenburn.json" with { type: "json" }
import { useKV } from "./kv"
+import { useRenderer } from "@opentui/solid"
+import { createStore } from "solid-js/store"
type Theme = {
primary: RGBA
@@ -86,14 +88,14 @@ type Variant = {
dark: HexColor | RefName
light: HexColor | RefName
}
-type ColorValue = HexColor | RefName | Variant
+type ColorValue = HexColor | RefName | Variant | RGBA
type ThemeJson = {
$schema?: string
defs?: Record<string, HexColor | RefName>
theme: Record<keyof Theme, ColorValue>
}
-export const THEMES: Record<string, ThemeJson> = {
+export const DEFAULT_THEMES: Record<string, ThemeJson> = {
aura,
ayu,
catppuccin,
@@ -122,6 +124,7 @@ export const THEMES: Record<string, ThemeJson> = {
function resolveTheme(theme: ThemeJson, mode: "dark" | "light") {
const defs = theme.defs ?? {}
function resolveColor(c: ColorValue): RGBA {
+ if (c instanceof RGBA) return c
if (typeof c === "string") return c.startsWith("#") ? RGBA.fromHex(c) : resolveColor(defs[c])
return resolveColor(c[mode])
}
@@ -137,514 +140,27 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
init: (props: { mode: "dark" | "light" }) => {
const sync = useSync()
const kv = useKV()
+ const [store, setStore] = createStore({
+ themes: DEFAULT_THEMES,
+ mode: props.mode,
+ active: (sync.data.config.theme ?? kv.get("theme", "opencode")) as string,
+ })
- const [theme, setTheme] = createSignal(sync.data.config.theme ?? kv.get("theme", "opencode"))
- const [mode, setMode] = createSignal(props.mode)
+ const renderer = useRenderer()
+ renderer
+ .getPalette({
+ size: 16,
+ })
+ .then((colors) => {
+ if (!colors.palette[0]) return
+ setStore("themes", "system", generateSystem(colors, store.mode))
+ })
const values = createMemo(() => {
- return resolveTheme(THEMES[theme()] ?? THEMES.opencode, mode())
+ return resolveTheme(store.themes[store.active] ?? store.themes.opencode, store.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,
- },
- },
- ])
- })
+ const syntax = createMemo(() => generateSyntax(values()))
return {
theme: new Proxy(values(), {
@@ -654,16 +170,20 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
},
}),
get selected() {
- return theme()
+ return store.active
+ },
+ all() {
+ return store.themes
},
syntax,
- mode,
+ mode() {
+ return store.mode
+ },
setMode(mode: "dark" | "light") {
- setMode(mode)
+ setStore("mode", mode)
},
set(theme: string) {
- if (!THEMES[theme]) return
- setTheme(theme)
+ setStore("active", theme)
kv.set("theme", theme)
},
get ready() {
@@ -672,3 +192,682 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
}
},
})
+
+function generateSystem(colors: TerminalColors, mode: "dark" | "light"): ThemeJson {
+ const bg = RGBA.fromHex(colors.defaultBackground ?? colors.palette[0]!)
+ const fg = RGBA.fromHex(colors.defaultForeground ?? colors.palette[7]!)
+ const palette = colors.palette.map((x) => RGBA.fromHex(x!))
+ const isDark = mode == "dark"
+
+ // Generate gray scale based on terminal background
+ const grays = generateGrayScale(bg, isDark)
+ const textMuted = generateMutedTextColor(bg, isDark)
+
+ // ANSI color references
+ const ansiColors = {
+ black: palette[0],
+ red: palette[1],
+ green: palette[2],
+ yellow: palette[3],
+ blue: palette[4],
+ magenta: palette[5],
+ cyan: palette[6],
+ white: palette[7],
+ }
+
+ return {
+ theme: {
+ // Primary colors using ANSI
+ primary: ansiColors.cyan,
+ secondary: ansiColors.magenta,
+ accent: ansiColors.cyan,
+
+ // Status colors using ANSI
+ error: ansiColors.red,
+ warning: ansiColors.yellow,
+ success: ansiColors.green,
+ info: ansiColors.cyan,
+
+ // Text colors
+ text: fg,
+ textMuted,
+
+ // Background colors
+ background: bg,
+ backgroundPanel: grays[2],
+ backgroundElement: grays[3],
+
+ // Border colors
+ borderSubtle: grays[6],
+ border: grays[7],
+ borderActive: grays[8],
+
+ // Diff colors
+ diffAdded: ansiColors.green,
+ diffRemoved: ansiColors.red,
+ diffContext: grays[7],
+ diffHunkHeader: grays[7],
+ diffHighlightAdded: ansiColors.green,
+ diffHighlightRemoved: ansiColors.red,
+ diffAddedBg: grays[2],
+ diffRemovedBg: grays[2],
+ diffContextBg: grays[1],
+ diffLineNumber: grays[6],
+ diffAddedLineNumberBg: grays[3],
+ diffRemovedLineNumberBg: grays[3],
+
+ // Markdown colors
+ markdownText: fg,
+ markdownHeading: fg,
+ markdownLink: ansiColors.blue,
+ markdownLinkText: ansiColors.cyan,
+ markdownCode: ansiColors.green,
+ markdownBlockQuote: ansiColors.yellow,
+ markdownEmph: ansiColors.yellow,
+ markdownStrong: fg,
+ markdownHorizontalRule: grays[7],
+ markdownListItem: ansiColors.blue,
+ markdownListEnumeration: ansiColors.cyan,
+ markdownImage: ansiColors.blue,
+ markdownImageText: ansiColors.cyan,
+ markdownCodeBlock: fg,
+
+ // Syntax colors
+ syntaxComment: textMuted,
+ syntaxKeyword: ansiColors.magenta,
+ syntaxFunction: ansiColors.blue,
+ syntaxVariable: fg,
+ syntaxString: ansiColors.green,
+ syntaxNumber: ansiColors.yellow,
+ syntaxType: ansiColors.cyan,
+ syntaxOperator: ansiColors.cyan,
+ syntaxPunctuation: fg,
+ },
+ }
+}
+
+function generateGrayScale(bg: RGBA, isDark: boolean): Record<number, RGBA> {
+ const grays: Record<number, RGBA> = {}
+
+ // RGBA stores floats in range 0-1, convert to 0-255
+ const bgR = bg.r * 255
+ const bgG = bg.g * 255
+ const bgB = bg.b * 255
+
+ const luminance = 0.299 * bgR + 0.587 * bgG + 0.114 * bgB
+
+ for (let i = 1; i <= 12; i++) {
+ const factor = i / 12.0
+
+ let grayValue: number
+ let newR: number
+ let newG: number
+ let newB: number
+
+ if (isDark) {
+ if (luminance < 10) {
+ grayValue = Math.floor(factor * 0.4 * 255)
+ newR = grayValue
+ newG = grayValue
+ newB = grayValue
+ } else {
+ const newLum = luminance + (255 - luminance) * factor * 0.4
+
+ const ratio = newLum / luminance
+ newR = Math.min(bgR * ratio, 255)
+ newG = Math.min(bgG * ratio, 255)
+ newB = Math.min(bgB * ratio, 255)
+ }
+ } else {
+ if (luminance > 245) {
+ grayValue = Math.floor(255 - factor * 0.4 * 255)
+ newR = grayValue
+ newG = grayValue
+ newB = grayValue
+ } else {
+ const newLum = luminance * (1 - factor * 0.4)
+
+ const ratio = newLum / luminance
+ newR = Math.max(bgR * ratio, 0)
+ newG = Math.max(bgG * ratio, 0)
+ newB = Math.max(bgB * ratio, 0)
+ }
+ }
+
+ grays[i] = RGBA.fromInts(Math.floor(newR), Math.floor(newG), Math.floor(newB))
+ }
+
+ return grays
+}
+
+function generateMutedTextColor(bg: RGBA, isDark: boolean): RGBA {
+ // RGBA stores floats in range 0-1, convert to 0-255
+ const bgR = bg.r * 255
+ const bgG = bg.g * 255
+ const bgB = bg.b * 255
+
+ const bgLum = 0.299 * bgR + 0.587 * bgG + 0.114 * bgB
+
+ let grayValue: number
+
+ if (isDark) {
+ if (bgLum < 10) {
+ // Very dark/black background
+ grayValue = 180 // #b4b4b4
+ } else {
+ // Scale up for lighter dark backgrounds
+ grayValue = Math.min(Math.floor(160 + bgLum * 0.3), 200)
+ }
+ } else {
+ if (bgLum > 245) {
+ // Very light/white background
+ grayValue = 75 // #4b4b4b
+ } else {
+ // Scale down for darker light backgrounds
+ grayValue = Math.max(Math.floor(100 - (255 - bgLum) * 0.2), 60)
+ }
+ }
+
+ return RGBA.fromInts(grayValue, grayValue, grayValue)
+}
+
+function generateSyntax(theme: Theme) {
+ return SyntaxStyle.fromTheme([
+ {
+ scope: ["prompt"],
+ style: {
+ foreground: theme.accent,
+ },
+ },
+ {
+ scope: ["extmark.file"],
+ style: {
+ foreground: theme.warning,
+ bold: true,
+ },
+ },
+ {
+ scope: ["extmark.agent"],
+ style: {
+ foreground: theme.secondary,
+ bold: true,
+ },
+ },
+ {
+ scope: ["extmark.paste"],
+ style: {
+ foreground: theme.background,
+ background: theme.warning,
+ bold: true,
+ },
+ },
+ {
+ scope: ["comment"],
+ style: {
+ foreground: theme.syntaxComment,
+ italic: true,
+ },
+ },
+ {
+ scope: ["comment.documentation"],
+ style: {
+ foreground: theme.syntaxComment,
+ italic: true,
+ },
+ },
+ {
+ scope: ["string", "symbol"],
+ style: {
+ foreground: theme.syntaxString,
+ },
+ },
+ {
+ scope: ["number", "boolean"],
+ style: {
+ foreground: theme.syntaxNumber,
+ },
+ },
+ {
+ scope: ["character.special"],
+ style: {
+ foreground: theme.syntaxString,
+ },
+ },
+ {
+ scope: ["keyword.return", "keyword.conditional", "keyword.repeat", "keyword.coroutine"],
+ style: {
+ foreground: theme.syntaxKeyword,
+ italic: true,
+ },
+ },
+ {
+ scope: ["keyword.type"],
+ style: {
+ foreground: theme.syntaxType,
+ bold: true,
+ italic: true,
+ },
+ },
+ {
+ scope: ["keyword.function", "function.method"],
+ style: {
+ foreground: theme.syntaxFunction,
+ },
+ },
+ {
+ scope: ["keyword"],
+ style: {
+ foreground: theme.syntaxKeyword,
+ italic: true,
+ },
+ },
+ {
+ scope: ["keyword.import"],
+ style: {
+ foreground: theme.syntaxKeyword,
+ },
+ },
+ {
+ scope: ["operator", "keyword.operator", "punctuation.delimiter"],
+ style: {
+ foreground: theme.syntaxOperator,
+ },
+ },
+ {
+ scope: ["keyword.conditional.ternary"],
+ style: {
+ foreground: theme.syntaxOperator,
+ },
+ },
+ {
+ scope: ["variable", "variable.parameter", "function.method.call", "function.call"],
+ style: {
+ foreground: theme.syntaxVariable,
+ },
+ },
+ {
+ scope: ["variable.member", "function", "constructor"],
+ style: {
+ foreground: theme.syntaxFunction,
+ },
+ },
+ {
+ scope: ["type", "module"],
+ style: {
+ foreground: theme.syntaxType,
+ },
+ },
+ {
+ scope: ["constant"],
+ style: {
+ foreground: theme.syntaxNumber,
+ },
+ },
+ {
+ scope: ["property"],
+ style: {
+ foreground: theme.syntaxVariable,
+ },
+ },
+ {
+ scope: ["class"],
+ style: {
+ foreground: theme.syntaxType,
+ },
+ },
+ {
+ scope: ["parameter"],
+ style: {
+ foreground: theme.syntaxVariable,
+ },
+ },
+ {
+ scope: ["punctuation", "punctuation.bracket"],
+ style: {
+ foreground: theme.syntaxPunctuation,
+ },
+ },
+ {
+ scope: [
+ "variable.builtin",
+ "type.builtin",
+ "function.builtin",
+ "module.builtin",
+ "constant.builtin",
+ ],
+ style: {
+ foreground: theme.error,
+ },
+ },
+ {
+ scope: ["variable.super"],
+ style: {
+ foreground: theme.error,
+ },
+ },
+ {
+ scope: ["string.escape", "string.regexp"],
+ style: {
+ foreground: theme.syntaxKeyword,
+ },
+ },
+ {
+ scope: ["keyword.directive"],
+ style: {
+ foreground: theme.syntaxKeyword,
+ italic: true,
+ },
+ },
+ {
+ scope: ["punctuation.special"],
+ style: {
+ foreground: theme.syntaxOperator,
+ },
+ },
+ {
+ scope: ["keyword.modifier"],
+ style: {
+ foreground: theme.syntaxKeyword,
+ italic: true,
+ },
+ },
+ {
+ scope: ["keyword.exception"],
+ style: {
+ foreground: theme.syntaxKeyword,
+ italic: true,
+ },
+ },
+ // Markdown specific styles
+ {
+ scope: ["markup.heading"],
+ style: {
+ foreground: theme.markdownHeading,
+ bold: true,
+ },
+ },
+ {
+ scope: ["markup.heading.1"],
+ style: {
+ foreground: theme.markdownHeading,
+ bold: true,
+ },
+ },
+ {
+ scope: ["markup.heading.2"],
+ style: {
+ foreground: theme.markdownHeading,
+ bold: true,
+ },
+ },
+ {
+ scope: ["markup.heading.3"],
+ style: {
+ foreground: theme.markdownHeading,
+ bold: true,
+ },
+ },
+ {
+ scope: ["markup.heading.4"],
+ style: {
+ foreground: theme.markdownHeading,
+ bold: true,
+ },
+ },
+ {
+ scope: ["markup.heading.5"],
+ style: {
+ foreground: theme.markdownHeading,
+ bold: true,
+ },
+ },
+ {
+ scope: ["markup.heading.6"],
+ style: {
+ foreground: theme.markdownHeading,
+ bold: true,
+ },
+ },
+ {
+ scope: ["markup.bold", "markup.strong"],
+ style: {
+ foreground: theme.markdownStrong,
+ bold: true,
+ },
+ },
+ {
+ scope: ["markup.italic"],
+ style: {
+ foreground: theme.markdownEmph,
+ italic: true,
+ },
+ },
+ {
+ scope: ["markup.list"],
+ style: {
+ foreground: theme.markdownListItem,
+ },
+ },
+ {
+ scope: ["markup.quote"],
+ style: {
+ foreground: theme.markdownBlockQuote,
+ italic: true,
+ },
+ },
+ {
+ scope: ["markup.raw", "markup.raw.block"],
+ style: {
+ foreground: theme.markdownCode,
+ },
+ },
+ {
+ scope: ["markup.raw.inline"],
+ style: {
+ foreground: theme.markdownCode,
+ background: theme.background,
+ },
+ },
+ {
+ scope: ["markup.link"],
+ style: {
+ foreground: theme.markdownLink,
+ underline: true,
+ },
+ },
+ {
+ scope: ["markup.link.label"],
+ style: {
+ foreground: theme.markdownLinkText,
+ underline: true,
+ },
+ },
+ {
+ scope: ["markup.link.url"],
+ style: {
+ foreground: theme.markdownLink,
+ underline: true,
+ },
+ },
+ {
+ scope: ["label"],
+ style: {
+ foreground: theme.markdownLinkText,
+ },
+ },
+ {
+ scope: ["spell", "nospell"],
+ style: {
+ foreground: theme.text,
+ },
+ },
+ {
+ scope: ["conceal"],
+ style: {
+ foreground: theme.textMuted,
+ },
+ },
+ // Additional common highlight groups
+ {
+ scope: ["string.special", "string.special.url"],
+ style: {
+ foreground: theme.markdownLink,
+ underline: true,
+ },
+ },
+ {
+ scope: ["character"],
+ style: {
+ foreground: theme.syntaxString,
+ },
+ },
+ {
+ scope: ["float"],
+ style: {
+ foreground: theme.syntaxNumber,
+ },
+ },
+ {
+ scope: ["comment.error"],
+ style: {
+ foreground: theme.error,
+ italic: true,
+ bold: true,
+ },
+ },
+ {
+ scope: ["comment.warning"],
+ style: {
+ foreground: theme.warning,
+ italic: true,
+ bold: true,
+ },
+ },
+ {
+ scope: ["comment.todo", "comment.note"],
+ style: {
+ foreground: theme.info,
+ italic: true,
+ bold: true,
+ },
+ },
+ {
+ scope: ["namespace"],
+ style: {
+ foreground: theme.syntaxType,
+ },
+ },
+ {
+ scope: ["field"],
+ style: {
+ foreground: theme.syntaxVariable,
+ },
+ },
+ {
+ scope: ["type.definition"],
+ style: {
+ foreground: theme.syntaxType,
+ bold: true,
+ },
+ },
+ {
+ scope: ["keyword.export"],
+ style: {
+ foreground: theme.syntaxKeyword,
+ },
+ },
+ {
+ scope: ["attribute", "annotation"],
+ style: {
+ foreground: theme.warning,
+ },
+ },
+ {
+ scope: ["tag"],
+ style: {
+ foreground: theme.error,
+ },
+ },
+ {
+ scope: ["tag.attribute"],
+ style: {
+ foreground: theme.syntaxKeyword,
+ },
+ },
+ {
+ scope: ["tag.delimiter"],
+ style: {
+ foreground: theme.syntaxOperator,
+ },
+ },
+ {
+ scope: ["markup.strikethrough"],
+ style: {
+ foreground: theme.textMuted,
+ },
+ },
+ {
+ scope: ["markup.underline"],
+ style: {
+ foreground: theme.text,
+ underline: true,
+ },
+ },
+ {
+ scope: ["markup.list.checked"],
+ style: {
+ foreground: theme.success,
+ },
+ },
+ {
+ scope: ["markup.list.unchecked"],
+ style: {
+ foreground: theme.textMuted,
+ },
+ },
+ {
+ scope: ["diff.plus"],
+ style: {
+ foreground: theme.diffAdded,
+ },
+ },
+ {
+ scope: ["diff.minus"],
+ style: {
+ foreground: theme.diffRemoved,
+ },
+ },
+ {
+ scope: ["diff.delta"],
+ style: {
+ foreground: theme.diffContext,
+ },
+ },
+ {
+ scope: ["error"],
+ style: {
+ foreground: theme.error,
+ bold: true,
+ },
+ },
+ {
+ scope: ["warning"],
+ style: {
+ foreground: theme.warning,
+ bold: true,
+ },
+ },
+ {
+ scope: ["info"],
+ style: {
+ foreground: theme.info,
+ },
+ },
+ {
+ scope: ["debug"],
+ style: {
+ foreground: theme.textMuted,
+ },
+ },
+ ])
+}
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 b0d98b088..7e0ccdaec 100644
--- a/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
+++ b/packages/opencode/src/cli/cmd/tui/routes/session/index.tsx
@@ -683,6 +683,7 @@ export function Session() {
<scrollbox
ref={(r) => (scroll = r)}
scrollbarOptions={{
+ paddingLeft: 2,
trackOptions: {
backgroundColor: theme.backgroundElement,
foregroundColor: theme.border,
diff --git a/packages/opencode/src/cli/cmd/tui/util/terminal.ts b/packages/opencode/src/cli/cmd/tui/util/terminal.ts
new file mode 100644
index 000000000..2b81068b3
--- /dev/null
+++ b/packages/opencode/src/cli/cmd/tui/util/terminal.ts
@@ -0,0 +1,114 @@
+import { RGBA } from "@opentui/core"
+
+export namespace Terminal {
+ export type Colors = Awaited<ReturnType<typeof colors>>
+ /**
+ * Query terminal colors including background, foreground, and palette (0-15).
+ * Uses OSC escape sequences to retrieve actual terminal color values.
+ *
+ * Note: OSC 4 (palette) queries may not work through tmux as responses are filtered.
+ * OSC 10/11 (foreground/background) typically work in most environments.
+ *
+ * Returns an object with background, foreground, and colors array.
+ * Any query that fails will be null/empty.
+ */
+ export async function colors(): Promise<{
+ background: RGBA | null
+ foreground: RGBA | null
+ colors: RGBA[]
+ }> {
+ if (!process.stdin.isTTY) return { background: null, foreground: null, colors: [] }
+
+ return new Promise((resolve) => {
+ let background: RGBA | null = null
+ let foreground: RGBA | null = null
+ const paletteColors: RGBA[] = []
+ let timeout: NodeJS.Timeout
+
+ const cleanup = () => {
+ process.stdin.setRawMode(false)
+ process.stdin.removeListener("data", handler)
+ clearTimeout(timeout)
+ }
+
+ const parseColor = (colorStr: string): RGBA | null => {
+ if (colorStr.startsWith("rgb:")) {
+ const parts = colorStr.substring(4).split("/")
+ return RGBA.fromInts(
+ parseInt(parts[0], 16) >> 8, // Convert 16-bit to 8-bit
+ parseInt(parts[1], 16) >> 8,
+ parseInt(parts[2], 16) >> 8,
+ 255,
+ )
+ }
+ if (colorStr.startsWith("#")) {
+ return RGBA.fromHex(colorStr)
+ }
+ if (colorStr.startsWith("rgb(")) {
+ const parts = colorStr.substring(4, colorStr.length - 1).split(",")
+ return RGBA.fromInts(parseInt(parts[0]), parseInt(parts[1]), parseInt(parts[2]), 255)
+ }
+ return null
+ }
+
+ const handler = (data: Buffer) => {
+ const str = data.toString()
+
+ // Match OSC 11 (background color)
+ const bgMatch = str.match(/\x1b]11;([^\x07\x1b]+)/)
+ if (bgMatch) {
+ background = parseColor(bgMatch[1])
+ }
+
+ // Match OSC 10 (foreground color)
+ const fgMatch = str.match(/\x1b]10;([^\x07\x1b]+)/)
+ if (fgMatch) {
+ foreground = parseColor(fgMatch[1])
+ }
+
+ // Match OSC 4 (palette colors)
+ const paletteMatches = str.matchAll(/\x1b]4;(\d+);([^\x07\x1b]+)/g)
+ for (const match of paletteMatches) {
+ const index = parseInt(match[1])
+ const color = parseColor(match[2])
+ if (color) paletteColors[index] = color
+ }
+
+ // Return immediately if we have all 16 palette colors
+ if (paletteColors.filter((c) => c !== undefined).length === 16) {
+ cleanup()
+ resolve({ background, foreground, colors: paletteColors })
+ }
+ }
+
+ process.stdin.setRawMode(true)
+ process.stdin.on("data", handler)
+
+ // Query background (OSC 11)
+ process.stdout.write("\x1b]11;?\x07")
+ // Query foreground (OSC 10)
+ process.stdout.write("\x1b]10;?\x07")
+ // Query palette colors 0-15 (OSC 4)
+ for (let i = 0; i < 16; i++) {
+ process.stdout.write(`\x1b]4;${i};?\x07`)
+ }
+
+ timeout = setTimeout(() => {
+ cleanup()
+ resolve({ background, foreground, colors: paletteColors })
+ }, 1000)
+ })
+ }
+
+ export async function getTerminalBackgroundColor(): Promise<"dark" | "light"> {
+ const result = await colors()
+ if (!result.background) return "dark"
+
+ const { r, g, b } = result.background
+ // 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
+ return luminance > 0.5 ? "light" : "dark"
+ }
+}