You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
TUI stays dark on a light terminal after a dark palette was cached #52457
On a light terminal the TUI starts in dark mode with theme.mode: "system" if ~/.local/state/opencode/<channel>/tui/system-theme.json was written while the terminal was dark. At startup the cached palette's mode overrides the mode the terminal just reported (OSC 10/11). With a non-system theme that cache is never refreshed, so it stays dark. v1 was fine; this came with the palette cache added in #48570.
Plugins
No response
OpenCode version
2.0.21
Steps to reproduce
In a dark terminal, use theme system once so the palette is cached.
Switch to another theme (e.g. github), keep mode: "system".
Switch the terminal to a light background and start OpenCode: it renders dark.
Not a duplicate of #42635: that one is about re-detecting after the terminal switches at runtime (1.18). This is v2 at startup: the terminal already answered light, but the cached palette overrides it. Fix in #52459.
The cached file held defaultBackground: "#1e1e2e" (written while the terminal was dark). With the system on light, WezTerm reports light, yet the TUI started dark every time. Deleting ~/.local/state/opencode/latest/tui/system-theme.json fixed it.
In packages/tui/src/context/theme.tsx at v2.1.4, terminalMode(cachedPalette) still overrides renderer.themeMode at init, while refreshPalette() returns early unless store.active === "system", so the cache can never be corrected for non-system themes. Applying the cached palette's mode only when the active theme is system (or preferring renderer.themeMode when available) should fix it.
Thanks for the clear report! I can confirm this on 2.0.21, and it still happens on the latest dev (891a020).
What I did, in a pty that answers the terminal colour queries (OSC 10/11/4) and with mode: "system" in every run:
Light terminal, theme github, no cache: renders light.
Dark terminal, theme system: renders dark and writes system-theme.json with the dark palette.
Light terminal, theme github: renders dark (github's dark variant).
After deleting system-theme.json, the same light terminal with github renders light again.
The recording below shows all four runs, and the screenshots show step 3 (dark on a light terminal) and step 4 (light after deleting the cache). The script below reproduces it.
The cause looks like what you describe: at startup the mode from the cached palette overrides the mode the terminal just reported, and the cache is only refreshed while the system theme is active. Until this is fixed, deleting system-theme.json works around it, as you found.
recording.mp4
Reproduction script (repro.py)
#!/usr/bin/env python3"""Reproduce #52457: cached dark system palette forces dark mode on a light terminal.Runs `opencode` (or OC_CMD) in a pty that answers OSC 10/11/4 color queries likea real terminal, with an isolated HOME. Afterwards it reports which backgroundcolour the TUI painted most (dark vs light). python3 repro.py # uses `opencode` from PATH OC_CMD="bun ... index.ts" python3 repro.py"""importbase64importos, pty, re, select, shlex, subprocess, sys, time, json, shutil, collectionsCMD=shlex.split(os.environ.get("OC_CMD", "opencode"))
ROOT="/tmp/opencode/repro-52457"DARK=dict(fg=(0xdd, 0xdd, 0xdd), bg=(0x10, 0x10, 0x10))
LIGHT=dict(fg=(0x20, 0x20, 0x20), bg=(0xfa, 0xfa, 0xfa))
BASE16= [(0,0,0),(205,49,49),(13,188,121),(229,229,16),(36,114,200),(188,63,188),(17,168,205),(229,229,229),
(102,102,102),(241,76,76),(35,209,139),(245,245,67),(59,142,234),(214,112,214),(41,184,219),(255,255,255)]
TL=None# optional terminal timeline (opencode-drive recording format) for video exportT0=time.time()
deftl(data):
ifTL: TL.write(json.dumps({"type": "output", "at_ms": int((time.time() -T0) *1000), "data": base64.b64encode(data).decode()}) +"\n")
defbanner(text):
tl(b"\x1b[0m\x1b[2J\x1b[H\r\n\r\n "+text.encode() +b"\r\n")
time.sleep(2.5)
tl(b"\x1b[0m")
defrgb(c):
return"rgb:%04x/%04x/%04x"%tuple(v*257forvinc)
defrun(label, scheme, theme, seconds=int(os.environ.get("OC_WAIT", "8"))):
cfg=os.path.join(ROOT, "home/.config/opencode")
os.makedirs(cfg, exist_ok=True)
withopen(os.path.join(cfg, "cli.json"), "w") asf:
json.dump({"theme": {"name": theme, "mode": "system"}}, f)
env=dict(os.environ, HOME=os.path.join(ROOT, "home"), TERM="xterm-256color", COLORTERM="truecolor")
forkin ("XDG_CONFIG_HOME", "XDG_STATE_HOME", "XDG_DATA_HOME", "XDG_CACHE_HOME"):
env.pop(k, None)
banner(f"[{label}] terminal background: {'DARK'ifschemeisDARKelse'LIGHT'} theme: {theme} mode: system cache files: {len(cache_files())}")
pid, fd=pty.fork()
ifpid==0:
os.chdir(os.path.join(ROOT, "project"))
os.execvpe(CMD[0], CMD, env)
importfcntl, termios, structfcntl.ioctl(fd, termios.TIOCSWINSZ, struct.pack("HHHH", 40, 120, 0, 0))
out=b""end=time.time() +secondshandled=0whiletime.time() <end:
r, _, _=select.select([fd], [], [], 0.1)
ifnotr:
continuetry:
data=os.read(fd, 65536)
exceptOSError:
breakout+=datatl(data)
tail=out[handled:]
replies=b""forminre.finditer(rb"\x1b\](10|11|4;(\d+));\?(\x07|\x1b\\)", tail):
what, idx, term=m.group(1), m.group(2), m.group(3)
ifwhat==b"10": c, code=scheme["fg"], b"10"elifwhat==b"11": c, code=scheme["bg"], b"11"else:
i=int(idx); c=BASE16[i] ifi<16else (128, 128, 128); code=b"4;"+idxreplies+=b"\x1b]"+code+b";"+rgb(c).encode() +termforminre.finditer(rb"\x1b\[\?996n", tail):
replies+=b"\x1b[?997;%dn"% (1ifschemeisDARKelse2)
forminre.finditer(rb"\x1b\[6n", tail):
replies+=b"\x1b[1;1R"forminre.finditer(rb"\x1b\[c", tail):
replies+=b"\x1b[?62;22c"ifreplies:
os.write(fd, replies)
handled=max(handled, len(out) -64)
os.kill(pid, 9)
os.waitpid(pid, 0)
bgs=collections.Counter(re.findall(rb"\x1b\[48;2;(\d+);(\d+);(\d+)m", out))
top=bgs.most_common(1)
iftop:
r, g, b=map(int, top[0][0])
mode="LIGHT"if (r+g+b) /3>128else"DARK"print(f"[{label}] terminal={'dark'ifschemeisDARKelse'light'} theme={theme}: most painted bg rgb({r},{g},{b}) -> TUI rendered {mode}")
else:
print(f"[{label}] no truecolor background output captured ({len(out)} bytes)")
returnoutdefcache_files():
st=os.path.join(ROOT, "home/.local/state")
return [os.path.join(d, f) ford, _, fsinos.walk(st) forfinfsiff=="system-theme.json"]
if__name__=="__main__":
shutil.rmtree(ROOT, ignore_errors=True)
ifos.environ.get("OC_TIMELINE"):
TL=open(os.environ["OC_TIMELINE"], "w")
TL.write(json.dumps({"type": "header", "version": 1, "cols": 120, "rows": 40, "encoding": "base64"}) +"\n")
os.makedirs(os.path.join(ROOT, "project"))
subprocess.run(["git", "init", "-q"], cwd=os.path.join(ROOT, "project"))
ifos.environ.get("OC_PRELOAD"): # running from source: background server needs the solid preload toowithopen(os.path.join(ROOT, "project/bunfig.toml"), "w") asf:
f.write('preload = ["%s"]\n'%os.environ["OC_PRELOAD"])
# Control: light terminal, named theme, no cacherun("control", LIGHT, "github")
print("cache after control:", cache_files())
# Step 1: dark terminal with theme system -> caches a dark paletterun("step1", DARK, "system")
files=cache_files()
print("cache after step1:", files)
forfinfiles:
print(" ", open(f).read()[:200])
# Step 2+3: light terminal, theme github, mode systemrun("step3", LIGHT, "github")
# Workaround: delete cache, run againforfinfiles: os.remove(f)
run("after-delete", LIGHT, "github")
ifTL:
banner("done")
TL.close()
Run it with opencode-drive: opencode-drive run ./repro.py. It drives opencode from your PATH.
Activity
github-actions commentedon Oct 1, 2026
This issue might be a duplicate of existing issues. Please check:
systemtheme stays dark after the terminal switches to a light appearance because the palette is not refreshed.x0c commentedon Oct 1, 2026
Not a duplicate of #42635: that one is about re-detecting after the terminal switches at runtime (1.18). This is v2 at startup: the terminal already answered light, but the cached palette overrides it. Fix in #52459.
DEAN-Cherry commentedon Oct 24, 2026
Still reproduces on v2.1.4, and not macOS-specific.
"theme": { "name": "opencode", "mode": "system" }The cached file held
defaultBackground: "#1e1e2e"(written while the terminal was dark). With the system on light, WezTerm reports light, yet the TUI started dark every time. Deleting~/.local/state/opencode/latest/tui/system-theme.jsonfixed it.In
packages/tui/src/context/theme.tsxat v2.1.4,terminalMode(cachedPalette)still overridesrenderer.themeModeat init, whilerefreshPalette()returns early unlessstore.active === "system", so the cache can never be corrected for non-systemthemes. Applying the cached palette's mode only when the active theme issystem(or preferringrenderer.themeModewhen available) should fix it.opencodejr commentedon Oct 28, 2026
Thanks for the clear report! I can confirm this on 2.0.21, and it still happens on the latest
dev(891a020).What I did, in a pty that answers the terminal colour queries (OSC 10/11/4) and with
mode: "system"in every run:github, no cache: renders light.system: renders dark and writessystem-theme.jsonwith the dark palette.github: renders dark (github's dark variant).system-theme.json, the same light terminal withgithubrenders light again.The recording below shows all four runs, and the screenshots show step 3 (dark on a light terminal) and step 4 (light after deleting the cache). The script below reproduces it.
The cause looks like what you describe: at startup the mode from the cached palette overrides the mode the terminal just reported, and the cache is only refreshed while the
systemtheme is active. Until this is fixed, deletingsystem-theme.jsonworks around it, as you found.recording.mp4
Reproduction script (repro.py)
Run it with opencode-drive:
opencode-drive run ./repro.py. It drivesopencodefrom yourPATH.