Explorar o código

Apply PR #23188: stabilize TUI theme persistence and KV writes

opencode-agent[bot] hai 1 día
pai
achega
ca1fca429c

+ 14 - 14
bun.lock

@@ -442,8 +442,8 @@
         "@opentelemetry/exporter-trace-otlp-http": "0.214.0",
         "@opentelemetry/sdk-trace-base": "2.6.1",
         "@opentelemetry/sdk-trace-node": "2.6.1",
-        "@opentui/core": "catalog:",
-        "@opentui/solid": "catalog:",
+        "@opentui/core": "0.0.0-20260417-7aaea62b",
+        "@opentui/solid": "0.0.0-20260417-7aaea62b",
         "@parcel/watcher": "2.5.1",
         "@pierre/diffs": "catalog:",
         "@solid-primitives/event-bus": "1.1.2",
@@ -543,16 +543,16 @@
         "zod": "catalog:",
       },
       "devDependencies": {
-        "@opentui/core": "catalog:",
-        "@opentui/solid": "catalog:",
+        "@opentui/core": "0.0.0-20260417-7aaea62b",
+        "@opentui/solid": "0.0.0-20260417-7aaea62b",
         "@tsconfig/node22": "catalog:",
         "@types/node": "catalog:",
         "@typescript/native-preview": "catalog:",
         "typescript": "catalog:",
       },
       "peerDependencies": {
-        "@opentui/core": ">=0.1.100",
-        "@opentui/solid": ">=0.1.100",
+        "@opentui/core": ">=0.0.0-20260417-7aaea62b",
+        "@opentui/solid": ">=0.0.0-20260417-7aaea62b",
       },
       "optionalPeers": [
         "@opentui/core",
@@ -1912,21 +1912,21 @@
 
     "@opentelemetry/semantic-conventions": ["@opentelemetry/[email protected]", "", {}, "sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw=="],
 
-    "@opentui/core": ["@opentui/core@0.1.99", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.99", "@opentui/core-darwin-x64": "0.1.99", "@opentui/core-linux-arm64": "0.1.99", "@opentui/core-linux-x64": "0.1.99", "@opentui/core-win32-arm64": "0.1.99", "@opentui/core-win32-x64": "0.1.99", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-I3+AEgGzqNWIpWX9g2WOscSPwtQDNOm4KlBjxBWCZjLxkF07u77heWXF7OiAdhKLtNUW6TFiyt6yznqAZPdG3A=="],
+    "@opentui/core": ["@opentui/core@0.0.0-20260417-7aaea62b", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.0.0-20260417-7aaea62b", "@opentui/core-darwin-x64": "0.0.0-20260417-7aaea62b", "@opentui/core-linux-arm64": "0.0.0-20260417-7aaea62b", "@opentui/core-linux-x64": "0.0.0-20260417-7aaea62b", "@opentui/core-win32-arm64": "0.0.0-20260417-7aaea62b", "@opentui/core-win32-x64": "0.0.0-20260417-7aaea62b", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-LF7yVr71aTawpLkSjT8MvSQRF6CJXPX3AllpWGXC5G8mLvaPxeROrDNyBofkcCebjusVhY4DYppC5b49xYbxVA=="],
 
-    "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.99", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bzVrqeX2vb5iWrc/ftOUOqeUY8XO+qSgoTwj5TXHuwagavgwD3Hpeyjx8+icnTTeM4pao0som1WR9xfye6/X5Q=="],
+    "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.0.0-20260417-7aaea62b", "", { "os": "darwin", "cpu": "arm64" }, "sha512-rM73d1OHfonbrsBV/UxgP6LSsnIifB5KzwEF+VuMtGCc1p7KDpPKGauPbcosNpz7be8T0ZvYAhUlH1UVYkIXOg=="],
 
-    "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.99", "", { "os": "darwin", "cpu": "x64" }, "sha512-VE4FrXBYpkxnvkqcCV1a8aN9jyyMJMihVW+V2NLCtp+4yQsj0AapG5TiUSN76XnmSZRptxDy5rBmEempeoIZbg=="],
+    "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.0.0-20260417-7aaea62b", "", { "os": "darwin", "cpu": "x64" }, "sha512-w1lOoqMXnkXDBg9N5Hn7s3PI+Z//ihxKeKdv5jpQKrnahp1vIqkeC5a6rqQb28OhOW8p32uQDdUYnx//nx0nPg=="],
 
-    "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.99", "", { "os": "linux", "cpu": "arm64" }, "sha512-viXQsbpS7yHjYkl7+am32JdvG96QU9lvHh1UiZtpOxcNUUqiYmA2ZwZFPD2Bi54jNyj5l2hjH6YkD3DzE2FEWA=="],
+    "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.0.0-20260417-7aaea62b", "", { "os": "linux", "cpu": "arm64" }, "sha512-RKcC7dSO441BXvXkyUFiSj/5hGLtPTvtvTDMEuTZIYjpLi9jtuRhBtKPrPKBrNvisqpKqZMf1cy1ocb09nu71w=="],
 
-    "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.99", "", { "os": "linux", "cpu": "x64" }, "sha512-WLoEFINOSp0tZSR9y4LUuGc7n4Y7H1wcpjUPzQ9vChkYDXrfZltEanzoDWbDcQ4kZQW5tHVC7LrZHpAsRLwFZg=="],
+    "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.0.0-20260417-7aaea62b", "", { "os": "linux", "cpu": "x64" }, "sha512-KJzv/R5vh/DKbC5LNFqKD6zNpyxWzk8Vio3m4GZikTg+aD0lZfhT+0pozLkMxWXqTDE6q/Iu1ea8tgXVU4H0xQ=="],
 
-    "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.99", "", { "os": "win32", "cpu": "arm64" }, "sha512-yWMOLWCEO8HdrctU1dMkgZC8qGkiO4Dwr4/e11tTvVpRmYhDsP/IR89ZjEEtOwnKwFOFuB/MxvflqaEWVQ2g5Q=="],
+    "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.0.0-20260417-7aaea62b", "", { "os": "win32", "cpu": "arm64" }, "sha512-S6V8/kDlSDgkhij2gOpLOOt9QmYGWYg0OaOrU3xTjDCztlKIylZZkf0vP2tYIRLZ7rTv8xrqT/hxXbw3S5g+cQ=="],
 
-    "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.99", "", { "os": "win32", "cpu": "x64" }, "sha512-aYRlsL2w8YRL6vPd7/hrqlNVkXU3QowWb01TOvAcHS8UAsXaGFUr47kSDyjxDi1wg1MzmVduCfsC7T3NoThV1w=="],
+    "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.0.0-20260417-7aaea62b", "", { "os": "win32", "cpu": "x64" }, "sha512-n+TxVJjj/7dtSwH7lgTWjmZLN5bC9IiDZlrthnV5pGQ955rfjFeV20NPT31/1jpjl6+ivMLJn7ebMCRe26ZPHQ=="],
 
-    "@opentui/solid": ["@opentui/solid@0.1.99", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.99", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.10", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.11" } }, "sha512-DrqqO4h2V88FmeIP2cErYkMU0ZK5MrUsZw3w6IzZpoXyyiL4/9qpWzUq+CXx+r16VP2iGxDJwGKUmtFAzUch2Q=="],
+    "@opentui/solid": ["@opentui/solid@0.0.0-20260417-7aaea62b", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.0.0-20260417-7aaea62b", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.10", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.11" } }, "sha512-nxYIphzPWjVVm6ohTEIU+NL867yg03tjFxf/HZqrBuACWqWTd8Ka2soim6kZdjY+WAXMtO7BbsnXLTgzlhc9YQ=="],
 
     "@oslojs/asn1": ["@oslojs/[email protected]", "", { "dependencies": { "@oslojs/binary": "1.0.0" } }, "sha512-zw/wn0sj0j0QKbIXfIlnEcTviaCzYOY3V5rAyjR6YtOByFtJiT574+8p9Wlach0lZH9fddD4yb9laEAIl4vXQA=="],
 

+ 2 - 2
packages/opencode/package.json

@@ -123,8 +123,8 @@
     "@opentelemetry/exporter-trace-otlp-http": "0.214.0",
     "@opentelemetry/sdk-trace-base": "2.6.1",
     "@opentelemetry/sdk-trace-node": "2.6.1",
-    "@opentui/core": "catalog:",
-    "@opentui/solid": "catalog:",
+    "@opentui/core": "0.0.0-20260417-7aaea62b",
+    "@opentui/solid": "0.0.0-20260417-7aaea62b",
     "@parcel/watcher": "2.5.1",
     "@pierre/diffs": "catalog:",
     "@solid-primitives/event-bus": "1.1.2",

+ 1 - 7
packages/opencode/src/cli/cmd/tui/app.tsx

@@ -1,7 +1,6 @@
 import { render, TimeToFirstDraw, useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/solid"
 import * as Clipboard from "@tui/util/clipboard"
 import * as Selection from "@tui/util/selection"
-import * as Terminal from "@tui/util/terminal"
 import { createCliRenderer, MouseButton, type CliRendererConfig } from "@opentui/core"
 import { RouteProvider, useRoute } from "@tui/context/route"
 import {
@@ -121,12 +120,6 @@ export function tui(input: {
     const unguard = win32InstallCtrlCGuard()
     win32DisableProcessedInput()
 
-    const mode = await Terminal.getTerminalBackgroundColor()
-
-    // Re-clear after getTerminalBackgroundColor() — setRawMode(false) restores
-    // the original console mode which re-enables ENABLE_PROCESSED_INPUT.
-    win32DisableProcessedInput()
-
     const onExit = async () => {
       unguard?.()
       resolve()
@@ -137,6 +130,7 @@ export function tui(input: {
     }
 
     const renderer = await createCliRenderer(rendererConfig(input.config))
+    const mode = (await renderer.waitForThemeMode(1000)) ?? "dark"
 
     await render(() => {
       return (

+ 28 - 4
packages/opencode/src/cli/cmd/tui/context/kv.tsx

@@ -1,7 +1,9 @@
 import { Global } from "@/global"
 import { Filesystem } from "@/util"
+import { Flock } from "@opencode-ai/shared/util/flock"
+import { rename, rm } from "fs/promises"
 import { createSignal, type Setter } from "solid-js"
-import { createStore } from "solid-js/store"
+import { createStore, unwrap } from "solid-js/store"
 import { createSimpleContext } from "./helper"
 import path from "path"
 
@@ -11,12 +13,29 @@ export const { use: useKV, provider: KVProvider } = createSimpleContext({
     const [ready, setReady] = createSignal(false)
     const [store, setStore] = createStore<Record<string, any>>()
     const filePath = path.join(Global.Path.state, "kv.json")
+    const lock = `tui-kv:${filePath}`
+    // Queue same-process writes so rapid updates persist in order.
+    let write = Promise.resolve()
 
-    Filesystem.readJson<Record<string, any>>(filePath)
+    // Write to a temp file first so kv.json is only replaced once the JSON is complete, avoiding partial writes if shutdown interrupts persistence.
+    function writeSnapshot(snapshot: Record<string, any>) {
+      const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`
+      return Filesystem.writeJson(tempPath, snapshot)
+        .then(() => rename(tempPath, filePath))
+        .catch(async (error) => {
+          await rm(tempPath, { force: true }).catch(() => undefined)
+          throw error
+        })
+    }
+
+    // Read under the same lock used for writes because kv.json is shared across processes.
+    Flock.withLock(lock, () => Filesystem.readJson<Record<string, any>>(filePath))
       .then((x) => {
         setStore(x)
       })
-      .catch(() => {})
+      .catch((error) => {
+        console.error("Failed to read KV state", { filePath, error })
+      })
       .finally(() => {
         setReady(true)
       })
@@ -44,7 +63,12 @@ export const { use: useKV, provider: KVProvider } = createSimpleContext({
       },
       set(key: string, value: any) {
         setStore(key, value)
-        void Filesystem.writeJson(filePath, store)
+        const snapshot = structuredClone(unwrap(store))
+        write = write
+          .then(() => Flock.withLock(lock, () => writeSnapshot(snapshot)))
+          .catch((error) => {
+            console.error("Failed to write KV state", { filePath, error })
+          })
       },
     }
     return result

+ 8 - 4
packages/opencode/src/cli/cmd/tui/context/theme.tsx

@@ -314,8 +314,11 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
     setStore(
       produce((draft) => {
         const lock = pick(kv.get("theme_mode_lock"))
-        const mode = pick(kv.get("theme_mode", props.mode))
-        draft.mode = lock ?? mode ?? props.mode
+        const mode = lock ?? props.mode
+        if (!lock && pick(kv.get("theme_mode")) !== undefined) {
+          kv.set("theme_mode", undefined)
+        }
+        draft.mode = mode
         draft.lock = lock
         const active = config.theme ?? kv.get("theme", "opencode")
         draft.active = typeof active === "string" ? active : "opencode"
@@ -373,7 +376,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
     }
 
     function apply(mode: "dark" | "light") {
-      kv.set("theme_mode", mode)
+      if (store.lock !== undefined) kv.set("theme_mode", mode)
       if (store.mode === mode) return
       setStore("mode", mode)
       renderer.clearPaletteCache()
@@ -389,6 +392,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
     function free() {
       setStore("lock", undefined)
       kv.set("theme_mode_lock", undefined)
+      kv.set("theme_mode", undefined)
       const mode = renderer.themeMode
       if (mode) apply(mode)
     }
@@ -397,7 +401,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
       if (store.lock) return
       apply(mode)
     }
-    // renderer.on(CliRenderEvents.THEME_MODE, handle)
+    renderer.on(CliRenderEvents.THEME_MODE, handle)
 
     const refresh = () => {
       renderer.clearPaletteCache()

+ 0 - 39
packages/opencode/src/cli/cmd/tui/util/terminal.ts

@@ -17,12 +17,6 @@ function parse(color: string): RGBA | null {
   return null
 }
 
-function mode(bg: RGBA | null): "dark" | "light" {
-  if (!bg) return "dark"
-  const luminance = (0.299 * bg.r + 0.587 * bg.g + 0.114 * bg.b) / 255
-  return luminance > 0.5 ? "light" : "dark"
-}
-
 /**
  * Query terminal colors including background, foreground, and palette (0-15).
  * Uses OSC escape sequences to retrieve actual terminal color values.
@@ -100,36 +94,3 @@ export async function colors(): Promise<{
     }, 1000)
   })
 }
-
-// Keep startup mode detection separate from `colors()`: the TUI boot path only
-// needs OSC 11 and should resolve on the first background response instead of
-// waiting on the full palette query used by system theme generation.
-export async function getTerminalBackgroundColor(): Promise<"dark" | "light"> {
-  if (!process.stdin.isTTY) return "dark"
-
-  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 match = data.toString().match(/\x1b]11;([^\x07\x1b]+)/)
-      if (!match) return
-      cleanup()
-      resolve(mode(parse(match[1])))
-    }
-
-    process.stdin.setRawMode(true)
-    process.stdin.on("data", handler)
-    process.stdout.write("\x1b]11;?\x07")
-
-    timeout = setTimeout(() => {
-      cleanup()
-      resolve("dark")
-    }, 1000)
-  })
-}

+ 4 - 4
packages/plugin/package.json

@@ -22,8 +22,8 @@
     "zod": "catalog:"
   },
   "peerDependencies": {
-    "@opentui/core": ">=0.1.100",
-    "@opentui/solid": ">=0.1.100"
+    "@opentui/core": ">=0.0.0-20260417-7aaea62b",
+    "@opentui/solid": ">=0.0.0-20260417-7aaea62b"
   },
   "peerDependenciesMeta": {
     "@opentui/core": {
@@ -34,8 +34,8 @@
     }
   },
   "devDependencies": {
-    "@opentui/core": "catalog:",
-    "@opentui/solid": "catalog:",
+    "@opentui/core": "0.0.0-20260417-7aaea62b",
+    "@opentui/solid": "0.0.0-20260417-7aaea62b",
     "@tsconfig/node22": "catalog:",
     "@types/node": "catalog:",
     "typescript": "catalog:",