Skip to content

Commit a824064

Browse files
authored
stabilize TUI theme persistence and KV writes (#23188)
1 parent 33b2795 commit a824064

7 files changed

Lines changed: 57 additions & 74 deletions

File tree

‎bun.lock‎

Lines changed: 14 additions & 14 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎packages/opencode/package.json‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -122,8 +122,8 @@
122122
"@opentelemetry/exporter-trace-otlp-http": "0.214.0",
123123
"@opentelemetry/sdk-trace-base": "2.6.1",
124124
"@opentelemetry/sdk-trace-node": "2.6.1",
125-
"@opentui/core": "catalog:",
126-
"@opentui/solid": "catalog:",
125+
"@opentui/core": "0.1.101",
126+
"@opentui/solid": "0.1.101",
127127
"@parcel/watcher": "2.5.1",
128128
"@pierre/diffs": "catalog:",
129129
"@solid-primitives/event-bus": "1.1.2",

‎packages/opencode/src/cli/cmd/tui/app.tsx‎

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import{render,TimeToFirstDraw,useKeyboard,useRenderer,useTerminalDimensions}from"@opentui/solid"
22
import*asClipboardfrom"@tui/util/clipboard"
33
import*asSelectionfrom"@tui/util/selection"
4-
import*asTerminalfrom"@tui/util/terminal"
54
import{createCliRenderer,MouseButton,typeCliRendererConfig}from"@opentui/core"
65
import{RouteProvider,useRoute}from"@tui/context/route"
76
import{
@@ -120,12 +119,6 @@ export function tui(input: {
120119
constunguard=win32InstallCtrlCGuard()
121120
win32DisableProcessedInput()
122121

123-
constmode=awaitTerminal.getTerminalBackgroundColor()
124-
125-
// Re-clear after getTerminalBackgroundColor() — setRawMode(false) restores
126-
// the original console mode which re-enables ENABLE_PROCESSED_INPUT.
127-
win32DisableProcessedInput()
128-
129122
constonExit=async()=>{
130123
unguard?.()
131124
resolve()
@@ -136,6 +129,7 @@ export function tui(input: {
136129
}
137130

138131
constrenderer=awaitcreateCliRenderer(rendererConfig(input.config))
132+
constmode=(awaitrenderer.waitForThemeMode(1000))??"dark"
139133

140134
awaitrender(()=>{
141135
return(

‎packages/opencode/src/cli/cmd/tui/context/kv.tsx‎

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
import{Global}from"@/global"
22
import{Filesystem}from"@/util"
3+
import{Flock}from"@opencode-ai/shared/util/flock"
4+
import{rename,rm}from"fs/promises"
35
import{createSignal,typeSetter}from"solid-js"
4-
import{createStore}from"solid-js/store"
6+
import{createStore,unwrap}from"solid-js/store"
57
import{createSimpleContext}from"./helper"
68
importpathfrom"path"
79

@@ -11,12 +13,29 @@ export const { use: useKV, provider: KVProvider } = createSimpleContext({
1113
const[ready,setReady]=createSignal(false)
1214
const[store,setStore]=createStore<Record<string,any>>()
1315
constfilePath=path.join(Global.Path.state,"kv.json")
16+
constlock=`tui-kv:${filePath}`
17+
// Queue same-process writes so rapid updates persist in order.
18+
letwrite=Promise.resolve()
1419

15-
Filesystem.readJson<Record<string,any>>(filePath)
20+
// Write to a temp file first so kv.json is only replaced once the JSON is complete, avoiding partial writes if shutdown interrupts persistence.
21+
functionwriteSnapshot(snapshot: Record<string,any>){
22+
consttempPath=`${filePath}.${process.pid}.${Date.now()}.tmp`
23+
returnFilesystem.writeJson(tempPath,snapshot)
24+
.then(()=>rename(tempPath,filePath))
25+
.catch(async(error)=>{
26+
awaitrm(tempPath,{force: true}).catch(()=>undefined)
27+
throwerror
28+
})
29+
}
30+
31+
// Read under the same lock used for writes because kv.json is shared across processes.
32+
Flock.withLock(lock,()=>Filesystem.readJson<Record<string,any>>(filePath))
1633
.then((x)=>{
1734
setStore(x)
1835
})
19-
.catch(()=>{})
36+
.catch((error)=>{
37+
console.error("Failed to read KV state",{ filePath, error })
38+
})
2039
.finally(()=>{
2140
setReady(true)
2241
})
@@ -44,7 +63,12 @@ export const { use: useKV, provider: KVProvider } = createSimpleContext({
4463
},
4564
set(key: string,value: any){
4665
setStore(key,value)
47-
voidFilesystem.writeJson(filePath,store)
66+
constsnapshot=structuredClone(unwrap(store))
67+
write=write
68+
.then(()=>Flock.withLock(lock,()=>writeSnapshot(snapshot)))
69+
.catch((error)=>{
70+
console.error("Failed to write KV state",{ filePath, error })
71+
})
4872
},
4973
}
5074
returnresult

‎packages/opencode/src/cli/cmd/tui/context/theme.tsx‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -314,8 +314,11 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
314314
setStore(
315315
produce((draft)=>{
316316
constlock=pick(kv.get("theme_mode_lock"))
317-
constmode=pick(kv.get("theme_mode",props.mode))
318-
draft.mode=lock??mode??props.mode
317+
constmode=lock??props.mode
318+
if(!lock&&pick(kv.get("theme_mode"))!==undefined){
319+
kv.set("theme_mode",undefined)
320+
}
321+
draft.mode=mode
319322
draft.lock=lock
320323
constactive=config.theme??kv.get("theme","opencode")
321324
draft.active=typeofactive==="string" ? active : "opencode"
@@ -373,7 +376,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
373376
}
374377

375378
functionapply(mode: "dark"|"light"){
376-
kv.set("theme_mode",mode)
379+
if(store.lock!==undefined)kv.set("theme_mode",mode)
377380
if(store.mode===mode)return
378381
setStore("mode",mode)
379382
renderer.clearPaletteCache()
@@ -389,6 +392,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
389392
functionfree(){
390393
setStore("lock",undefined)
391394
kv.set("theme_mode_lock",undefined)
395+
kv.set("theme_mode",undefined)
392396
constmode=renderer.themeMode
393397
if(mode)apply(mode)
394398
}
@@ -397,7 +401,7 @@ export const { use: useTheme, provider: ThemeProvider } = createSimpleContext({
397401
if(store.lock)return
398402
apply(mode)
399403
}
400-
// renderer.on(CliRenderEvents.THEME_MODE, handle)
404+
renderer.on(CliRenderEvents.THEME_MODE,handle)
401405

402406
constrefresh=()=>{
403407
renderer.clearPaletteCache()

‎packages/opencode/src/cli/cmd/tui/util/terminal.ts‎

Lines changed: 0 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,6 @@ function parse(color: string): RGBA | null {
1717
returnnull
1818
}
1919

20-
functionmode(bg: RGBA|null): "dark"|"light"{
21-
if(!bg)return"dark"
22-
constluminance=(0.299*bg.r+0.587*bg.g+0.114*bg.b)/255
23-
returnluminance>0.5 ? "light" : "dark"
24-
}
25-
2620
/**
2721
* Query terminal colors including background, foreground, and palette (0-15).
2822
* Uses OSC escape sequences to retrieve actual terminal color values.
@@ -100,36 +94,3 @@ export async function colors(): Promise<{
10094
},1000)
10195
})
10296
}
103-
104-
// Keep startup mode detection separate from `colors()`: the TUI boot path only
105-
// needs OSC 11 and should resolve on the first background response instead of
106-
// waiting on the full palette query used by system theme generation.
107-
exportasyncfunctiongetTerminalBackgroundColor(): Promise<"dark"|"light">{
108-
if(!process.stdin.isTTY)return"dark"
109-
110-
returnnewPromise((resolve)=>{
111-
lettimeout: NodeJS.Timeout
112-
113-
constcleanup=()=>{
114-
process.stdin.setRawMode(false)
115-
process.stdin.removeListener("data",handler)
116-
clearTimeout(timeout)
117-
}
118-
119-
consthandler=(data: Buffer)=>{
120-
constmatch=data.toString().match(/\x1b]11;([^\x07\x1b]+)/)
121-
if(!match)return
122-
cleanup()
123-
resolve(mode(parse(match[1])))
124-
}
125-
126-
process.stdin.setRawMode(true)
127-
process.stdin.on("data",handler)
128-
process.stdout.write("\x1b]11;?\x07")
129-
130-
timeout=setTimeout(()=>{
131-
cleanup()
132-
resolve("dark")
133-
},1000)
134-
})
135-
}

‎packages/plugin/package.json‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@
2222
"zod": "catalog:"
2323
},
2424
"peerDependencies": {
25-
"@opentui/core": ">=0.1.100",
26-
"@opentui/solid": ">=0.1.100"
25+
"@opentui/core": ">=0.1.101",
26+
"@opentui/solid": ">=0.1.101"
2727
},
2828
"peerDependenciesMeta": {
2929
"@opentui/core": {
@@ -34,8 +34,8 @@
3434
}
3535
},
3636
"devDependencies": {
37-
"@opentui/core": "catalog:",
38-
"@opentui/solid": "catalog:",
37+
"@opentui/core": "0.1.101",
38+
"@opentui/solid": "0.1.101",
3939
"@tsconfig/node22": "catalog:",
4040
"@types/node": "catalog:",
4141
"typescript": "catalog:",

0 commit comments

Comments
 (0)