Dax 3 ماه پیش
والد
کامیت
3ba7e243d0

+ 10 - 10
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/core@0.1.36", "", { "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/core@0.0.0-20251106-788e97e4", "", { "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/core-darwin-arm64@0.1.36", "", { "os": "darwin", "cpu": "arm64" }, "sha512-/fb0k1H0CeTroVt2UoEAcVrEx1cIYy4B2zfX0MrwUkIfXi36aoIBnisBeYvyCpsQfxFAkyLYCCA3NzaYEyC5hg=="],
+    "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.0.0-20251106-788e97e4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-EOO8SSIYJBIh+Sd9bgVTiQmt+TEJmfg65/oym54J4zfDtCYlAqSaLcRnDe4TzB+4hejV9of8etrG3ZZACBJT+A=="],
 
-    "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.36", "", { "os": "darwin", "cpu": "x64" }, "sha512-PZMydJbSDUoEWqZsyEV8+FSwMT+r7mWFL0ABgdALI3AOrSr7Z8dMcRnFWl8LhriuHS589THvETJEN28L4q/E2Q=="],
+    "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.0.0-20251106-788e97e4", "", { "os": "darwin", "cpu": "x64" }, "sha512-MUTt7CDbzL2afNGK8gJ4jUZd+AHiOUJEO0eJGDSfWU8DUs0zv8XoLZfaI5PPbkUPEL/7CEBMARAAiwfRtoG/4A=="],
 
-    "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.36", "", { "os": "linux", "cpu": "arm64" }, "sha512-ATR+vdtraZEC/gHR1mQa/NYPlqFNBpsnnJAGepQmcxm85VceLYM701QaaIgNAwyYXiP6RQN1ZCv06MD1Ph1m4w=="],
+    "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.0.0-20251106-788e97e4", "", { "os": "linux", "cpu": "arm64" }, "sha512-Zi1EzLCzooRfYoQnN/Dz8OxzrpRXByny8SJqhdO9ZP2mYX72yJ3AhUUW1Sl6YSzVi0H+QIKj7g+RX2KfsXIGFg=="],
 
-    "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.36", "", { "os": "linux", "cpu": "x64" }, "sha512-INsnPtcZVx68C+0Vd0L9+akDwNbWblUDqLmY9CftfmeLFubzvJXNRYTBvr7lX68fcst6Ho+0beUxyUoClKc0rg=="],
+    "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.0.0-20251106-788e97e4", "", { "os": "linux", "cpu": "x64" }, "sha512-/E0XEBVzO4JEEhJGzfURF2tPxDE2oTODxlgNYYB1QbAuOsLcV69uSrwAjo1TxuIn4P78tBR+ZOlmONjroPqfbQ=="],
 
-    "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.36", "", { "os": "win32", "cpu": "arm64" }, "sha512-x9lDZTL+xE8jsG1hP4pdsqCsZBu77JNR/ze5F7ZQkYQEC6Zl/XJtL1YT08nUlWOu4NMSws2xXV0lS/sJkbEgPA=="],
+    "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.0.0-20251106-788e97e4", "", { "os": "win32", "cpu": "arm64" }, "sha512-En/29cgpYVvzlrQ7fAoP+EUdzmczgMzBIGGM0RuLi2hmCmCqyMtOJ0EJUh9UXa5jYIXNGOP49sIP6bUBbvXt7g=="],
 
-    "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.36", "", { "os": "win32", "cpu": "x64" }, "sha512-WVU+qtAfJe8ikPWbw8Hfli15GuQTMKiceTkF5lql5AQYy7PKYtGTzWszxOZKeUU1/eEd2X4REi8Bn0TprEMxYw=="],
+    "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.0.0-20251106-788e97e4", "", { "os": "win32", "cpu": "x64" }, "sha512-2lu0bgEi+k/1c9VHQFg3wjVxMgQnuZhs/6sDDpxk9eNS3fuHEJfZi0PFJQk2J4IFQL61nzukOvJKgYDWQvKB1g=="],
 
-    "@opentui/solid": ["@opentui/solid@0.1.36", "", { "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/solid@0.0.0-20251106-788e97e4", "", { "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=="],
 

+ 2 - 2
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",

+ 5 - 7
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)
   })
 

+ 711 - 512
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,
+      },
+    },
+  ])
+}

+ 1 - 0
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,

+ 114 - 0
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"
+  }
+}