Skip to content

fix(tui): resolve terminal theme mode from every signal instead of guessing dark - #1152

Open
sahrizvi wants to merge 4 commits into
mainfrom
fix/tui-terminal-readability
Open

fix(tui): resolve terminal theme mode from every signal instead of guessing dark#1152
sahrizvi wants to merge 4 commits into
mainfrom
fix/tui-terminal-readability

Conversation

@sahrizvi

@sahrizvi sahrizvi commented Aug 26, 2026

Copy link
Copy Markdown
Contributor

Issue for this PR

Closes #736
Refs #809

Closes for #736, Refs for #809 — the difference is deliberate and explained in the scope note. Everything else this description mentions is named to put it out of scope, not to claim it.

Type of change

  • Bug fix
  • New feature
  • Refactor / code improvement
  • Documentation

What does this PR do?

The bug. Code output renders in near-white on a light terminal — readable when the session is opened in VS Code, not in the terminal itself. This is the third report of it: #617#704#736.

Why the previous two fixes did not hold. Both adjusted colour values. The defect is not a palette; it is that the mode-resolution chain in app.tsx ended in a hardcoded guess:

const mode = envMode === "light" ? "light" : ((await renderer.waitForThemeMode(1000)) ?? "dark")

Apple Terminal — named in #736's metadata next to "macOS Appearance: Light" — sets no COLORFGBG and does not reliably answer the OSC 11 background query. With both signals absent the chain returns "dark", so a light-background user gets the dark palette no matter how well that palette is tuned. No colour change could close it.

Two layers are fixed.

Startup detection. resolveInitialMode() now encodes the whole chain as one pure function, ordered by how well each signal describes this terminal window: the OSC 11 reply, then COLORFGBG, then OS appearance, then dark as a genuine last resort. OSC outranks COLORFGBG because the env var is inherited and survives ssh, tmux, sudo and profile changes; COLORFGBG now only shortens the OSC wait to 250ms, which keeps the startup win from #704 without letting a stale value beat a live answer. detectSystemAppearance() supplies the signal that was missing — macOS leaves AppleInterfaceStyle unset in light mode, so a defaults exit saying the key does not exist is the light answer, while EACCES, ENOENT, a signal or a timeout mean unknown. It runs /usr/bin/defaults so a stray binary on PATH cannot answer, and skips the probe entirely over ssh (where the appearance belongs to the remote host) and in CI.

Direct-run renderer. resolveRunTheme returned a hardcoded dark fallback on both failure exits, so a light terminal whose palette query failed got dark panels. That fallback's text prefers the terminal's own default foreground, which on a light terminal resolves to black — black text over a hardcoded #0f172a panel is literally dark text in a dark box. The fallback is now built per mode and memoized; the dark instance is unchanged by identity, so callers comparing it with toBe are unaffected.

How did you verify your code works?

Mutation, not a green suite. Seven mutants were each confirmed to fail a test:

Mutation Caught by
Restore the hardcoded dark fallback the #736-shape test
COLORFGBG outranks OSC again precedence test
Drop the ssh guard ssh probe test
Drop the CI guard CI probe test
Use a relative defaults absolute-path test
Any defaults failure means light failure-classification tests
Direct-run fallback always dark / light panel still dark direct-run fallback tests

execFile is injected and the probe tests use a spy, so "does not spawn" is asserted rather than assumed.

Suites: 288 tui, 187 cli/run, 9 direct-run theme, typecheck clean. The one failing tui test (formats session continuation summary) fails identically on main.

Screenshots / recordings

Not a UI-layout change; the observable difference is text colour on a light terminal.

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Scope note. An audit of the whole colour-legibility area found the original grouping of these reports was wrong, so this claims only what the code supports:

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Improved startup theme detection using terminal settings, environment preferences, and macOS appearance.
    • Added light and dark fallback themes when terminal theme detection is unavailable.
    • Detection now prioritizes terminal signals, then system appearance, before defaulting to dark mode.
  • Bug Fixes

    • Light-mode terminals now receive a genuinely lighter fallback palette.
  • Tests

    • Added coverage for terminal, OS, timeout, SSH, CI, and fallback theme detection scenarios.

sahrizvi and others added 3 commits August 21, 2026 04:32
…ssing dark

Third attempt at the same defect (#617#704#736): code rendered in
near-white on a light terminal, readable in VS Code but not in the terminal
itself. The first two fixes adjusted colour values, which is why neither held.

The actual defect is in `app.tsx`, where the mode-resolution chain ended in a
hardcoded fallback:

    const envMode = detectModeFromCOLORFGBG(process.env.COLORFGBG)
    const mode = envMode === "light" ? "light" : ((await renderer.waitForThemeMode(1000)) ?? "dark")

Apple Terminal — the client named in #736's metadata, alongside "macOS
Appearance: Light" — sets no `COLORFGBG` and does not reliably answer the OSC 11
background query. Both signals are therefore absent, the chain returns "dark",
and a light-background user gets the dark palette no matter how its colours are
tuned. That is not a palette bug, so palette fixes could not close it.

Changes:

- `resolveInitialMode()` encodes the whole chain as one pure function, ordered
  by how well each signal describes *this terminal window*: COLORFGBG, then the
  OSC 11 reply, then OS appearance, then dark as a genuine last resort. A
  dark-profile terminal under a light system theme stays dark.
- `detectSystemAppearance()` adds the signal that was missing. macOS sets
  `AppleInterfaceStyle` to "Dark" in dark mode and leaves it *unset* in light
  mode, so `defaults` exiting non-zero is the light answer rather than a
  failure; only ENOENT or a timeout is treated as "unknown". Every report of
  this bug came from darwin.
- The call site now honours a dark `COLORFGBG` too. It previously kept only
  "light", so a terminal that had already reported a dark background still paid
  the full one-second OSC timeout before agreeing with it.

`detectModeFromCOLORFGBG` carried a comment saying it was "extracted from
app.tsx for direct test coverage (#704)" but had no tests at all. It does now.

Verified by mutation rather than by the suite going green: restoring the old
hardcoded fallback fails the test named for #736, discarding a dark COLORFGBG
fails the precedence test, and letting OS appearance outrank the terminal's own
background fails two more.

Scope note: this closes the colour-mode family. #404 (garbled ASCII logo), #609
(malformed layout) and #737 (unexpected CJK glyphs) were grouped with it during
triage, but they are glyph-width and encoding problems rather than colour, and
need separate work. #809 (dark text on a dark box) is plausibly the same
misdetection, but the report carries no terminal details, so it is referenced
rather than closed.

Tests: 13 new, 280 tui pass (1 pre-existing failure unrelated, identical on main).

Closes #736
Refs #809

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… fallback assuming dark

Reworked after a codex audit of the whole colour-legibility class, which found
the first attempt was aimed at the less important of two layers and that several
of its tests proved less than their names claimed.

## Direct-run renderer (the layer the audit ranked higher)

`resolveRunTheme` returned a hardcoded dark `RUN_THEME_FALLBACK` on both failure
exits, so a light terminal whose palette query failed got dark panels. Worse,
that fallback's `text` prefers the terminal's *own* default foreground, which on
a light terminal resolves to black — a black foreground over a hardcoded
`#0f172a` panel is literally dark text in a dark box, the symptom reported in
#809.

The fallback is now built per mode and memoized, and both exits resolve a mode
first. The dark instance is still the same object, so callers comparing it by
identity are unaffected.

## Startup detection

- Precedence corrected. OSC 11 describes *this* window right now; `COLORFGBG` is
  inherited and survives ssh, tmux, sudo and profile changes. The previous
  ordering let a stale env var override a live answer. COLORFGBG now only
  shortens the OSC wait (250ms instead of 1s), which keeps #704's startup win
  without trading away correctness.
- The appearance probe no longer reports "light" for every failure. macOS leaves
  `AppleInterfaceStyle` unset in light mode and `defaults` says so explicitly;
  that diagnostic is the light answer, while EACCES, EMFILE, ENOENT, a signal or
  a timeout mean unknown. Guessing light on those produces the inverse of the
  bug being fixed.
- It invokes `/usr/bin/defaults`, so a different `defaults` earlier on PATH
  cannot answer a question about macOS appearance.
- It does not run over ssh, where the appearance belongs to the remote host
  rather than the terminal the user is looking at, nor in CI.

## Tests

The audit named six tests that overclaimed. `execFile` is now injectable and the
probe tests use a spy, so "does not spawn" is asserted rather than assumed, and
every failure branch is driven directly. The regression test is renamed for the
shape it actually covers instead of implying end-to-end coverage it does not
have.

Seven mutants were confirmed to fail: old precedence, missing ssh guard, missing
CI guard, relative `defaults`, any-failure-means-light, always-dark direct-run
fallback, and a light fallback whose panel is still dark.

## Scope

Claims only what the code supports. The audit found #617 was missing Markdown
`fg` and code-block background (fixed separately in 5ae5b79), #704 bundled
three changes with no way to attribute the fix, and #404/#609/#737/#116 are
glyph-width, encoding or layout problems rather than colour. #736 is the
best-supported colour-mode case but remains conditional, so it is referenced,
not closed.

Tests: 17 detection, 9 direct-run theme, 284 tui, 187 cli/run. Typecheck clean.

Refs #736
Refs #809

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review for a one-time review, or @claude review always to subscribe this PR to a review on every future push.

Tip: disable this comment in your organization's Code Review settings.

@github-actions

Copy link
Copy Markdown

Thanks for your contribution!

This PR doesn't have a linked issue. All PRs must reference an existing issue.

Please:

  1. Open an issue describing the bug/feature (if one doesn't exist)
  2. Add Fixes #<number> or Closes #<number> to this PR description

See CONTRIBUTING.md for details.

@coderabbitai

coderabbitai Bot commented Aug 26, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: bc6fba14-28cf-4e91-98c0-14ab71cafb5a

📥 Commits

Reviewing files that changed from the base of the PR and between 0306bf0 and e452112.

📒 Files selected for processing (1)
  • packages/opencode/src/cli/cmd/run/theme.ts

Included review availability: Your plan provides up to 4 included reviews per hour; 3 remain after this review.


📝 Walkthrough

Walkthrough

The TUI now detects terminal mode from OSC responses, COLORFGBG, and macOS appearance. Startup and direct-run fallback themes use the resolved mode. Fallback themes are generated lazily and cached per mode.

Changes

Terminal theme detection

Layer / File(s) Summary
Mode detection and resolution
packages/tui/src/terminal-detection.ts, packages/tui/test/terminal-detection.test.ts
Added COLORFGBG parsing, signal precedence, macOS appearance detection, and tests for success, failure, timeout, SSH, and CI cases.
Startup detection integration
packages/tui/src/app.tsx, packages/tui/package.json
Startup mode resolution combines OSC, COLORFGBG, and system appearance. Export mappings are reordered without changing their targets.
Mode-specific fallback themes
packages/opencode/src/cli/cmd/run/theme.ts, packages/opencode/test/cli/run/theme.test.ts
Fallback themes use mode-specific seeds, cache each mode lazily, and select the detected mode when theme resolution fails or lacks a background. Tests cover light and dark fallbacks.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Merge Risk: 🟡 Moderate · up to e4521

Direct-run rendering can still select a dark fallback on light macOS terminals when terminal signals are unavailable, leaving text difficult to read and preserving the bug this PR is intended to fix. The fallback path needs correction or explicit owner acceptance before merge.

Sequence Diagram(s)

sequenceDiagram
  participant TUI
  participant Terminal
  participant SystemAppearance
  participant ThemeResolver
  TUI->>Terminal: Query OSC 11 and read COLORFGBG
  TUI->>SystemAppearance: Query appearance if terminal signals are absent
  SystemAppearance-->>TUI: Return light, dark, or unknown
  TUI->>ThemeResolver: Resolve initial mode
  ThemeResolver-->>TUI: Return mode-specific fallback theme
Loading

Poem

A rabbit reads the terminal glow
Light seeds bloom and dark seeds show
OSC whispers, colors guide
Cached themes wait on either side
The TUI starts with mode in sight

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The theme-detection and direct-run fallback changes are within the scope of [#736]. However, reordering export mappings in packages/tui/package.json is unrelated because the exported subpaths and targ… Remove the packages/tui/package.json export-order-only changes, or document a specific functional requirement that makes them necessary for this fix.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes address the linked issue [#736] by resolving light terminal mode through OSC 11, COLORFGBG, macOS appearance, and a final dark fallback. Mode-specific direct-run fallbacks also prevent unr…
Docstring Coverage ✅ Passed Docstring coverage is 80.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 10 functions across 5 files.
Title check ✅ Passed The title clearly summarizes the primary change: terminal theme mode detection now uses available signals instead of defaulting to dark.
Description check ✅ Passed The description follows the repository template. It identifies the issue, marks the change as a bug fix, explains the implementation, documents verification results, addresses screenshots, and complet…
Full details: Linked Issues check

Explanation

The changes address the linked issue [#736] by resolving light terminal mode through OSC 11, COLORFGBG, macOS appearance, and a final dark fallback. Mode-specific direct-run fallbacks also prevent unreadable dark-terminal styling on light backgrounds.

Full details: Out of Scope Changes check

Explanation

The theme-detection and direct-run fallback changes are within the scope of [#736]. However, reordering export mappings in packages/tui/package.json is unrelated because the exported subpaths and target files remain unchanged.

Full details: Description check

Explanation

The description follows the repository template. It identifies the issue, marks the change as a bug fix, explains the implementation, documents verification results, addresses screenshots, and completes the checklist.

  • Fix all pre-merge checks with AI
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/tui-terminal-readability

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

* This is the signal that was missing: every report of this bug came from
* darwin, on a terminal that answers neither of the cheaper probes.
*/
/** Minimal shape of `child_process.execFile`, injectable so tests can drive every branch. */

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: The detectSystemAppearance doc comment (lines 61-68) is now orphaned — inserting ExecFileLike here detaches it from the function it documents.

The macOS-specific explanation (AppleInterfaceStyle set to "Dark" in dark mode and absent in light mode, non-zero exit = light) is meant to describe detectSystemAppearance, but it now sits directly above the ExecFileLike type. Move that doc comment down to immediately precede detectSystemAppearance (or fold it into that function's JSDoc), so the two descriptions stop pointing at the wrong declarations.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

// silently reported as light.
const err = error as NodeJS.ErrnoException & { killed?: boolean; status?: number | null; stderr?: string }
const notFound = /does not exist/i.test(String(err.stderr ?? err.message ?? ""))
const clean = err.code === undefined && err.killed !== true

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: The clean clause is dead code that contradicts the contract stated in the comment above it.

For a real execFile error, code is always populated: the errno string on spawn failure (ENOENT/EACCES/EMFILE/...), the numeric exit code on a non-zero exit, or null when killed is true. So err.code === undefined never holds, and this clause never fires. If it ever did fire (e.g. a future error shape without code), it would silently report "light" for an unknown failure — exactly what the comment says must not happen. If the intent is to treat a clean non-zero exit (missing key) as light, match the exit status (typeof err.code === "number" / err.status === 1) instead; otherwise remove the clause.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

* the direct-run and scrollback renderer.
*/
function fallbackMode(renderer: CliRenderer): "dark" | "light" {
return resolveInitialMode({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SUGGESTION: fallbackMode omits the OS-appearance signal, so the direct-run renderer does not actually "agree" with the TUI startup path it shares resolveInitialMode with.

The startup path feeds appearance from detectSystemAppearance() into resolveInitialMode, but this fallback only passes COLORFGBG and themeMode. On a light Apple Terminal (no COLORFGBG, no OSC 11 reply) a failed palette query still resolves to "dark" and reproduces the dark-on-dark symptom (#809) this PR targets. Consider threading appearance through here too (which would require making this path async), or note the limitation explicitly.


Reply with @kilocode-bot fix it to have Kilo Code address this issue.

@kilo-code-bot

kilo-code-bot Bot commented Aug 26, 2026

Copy link
Copy Markdown

Code Review Summary

Status: 3 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 3
Issue Details (click to expand)

SUGGESTION

File Line Issue
packages/tui/src/terminal-detection.ts 69 Orphaned detectSystemAppearance doc comment detached by inserted ExecFileLike type
packages/tui/src/terminal-detection.ts 112 clean clause is dead code contradicting the documented failure-classification contract
packages/opencode/src/cli/cmd/run/theme.ts 703 fallbackMode omits OS appearance, so direct-run still falls back to dark on light Apple Terminal
Files Reviewed (6 files)
  • packages/opencode/src/cli/cmd/run/theme.ts - 1 issue
  • packages/opencode/test/cli/run/theme.test.ts - 0 issues
  • packages/tui/package.json - 0 issues
  • packages/tui/src/app.tsx - 0 issues
  • packages/tui/src/terminal-detection.ts - 2 issues
  • packages/tui/test/terminal-detection.test.ts - 0 issues

Fix these issues in Kilo Cloud

Previous Review Summary (commit 0306bf0)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 0306bf0)

Status: 3 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 0
SUGGESTION 3
Issue Details (click to expand)

SUGGESTION

File Line Issue
packages/tui/src/terminal-detection.ts 69 Orphaned detectSystemAppearance doc comment detached by inserted ExecFileLike type
packages/tui/src/terminal-detection.ts 112 clean clause is dead code contradicting the documented failure-classification contract
packages/opencode/src/cli/cmd/run/theme.ts 696 fallbackMode omits OS appearance, so direct-run still falls back to dark on light Apple Terminal
Files Reviewed (6 files)
  • packages/opencode/src/cli/cmd/run/theme.ts - 1 issue
  • packages/opencode/test/cli/run/theme.test.ts - 0 issues
  • packages/tui/package.json - 0 issues
  • packages/tui/src/app.tsx - 0 issues
  • packages/tui/src/terminal-detection.ts - 2 issues
  • packages/tui/test/terminal-detection.test.ts - 0 issues

Fix these issues in Kilo Cloud


Reviewed by deepseek-v4-pro · Input: 51.5K · Output: 12.7K · Cached: 439K

Review guidance: REVIEW.md from base branch main

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

5 issues found across 6 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/tui/src/app.tsx">

<violation number="1" location="packages/tui/src/app.tsx:278">
P2: When a valid but stale `COLORFGBG` is present and OSC 11 takes longer than 250 ms, the live terminal answer is discarded and the stale value determines the theme. Keep the OSC probe alive through the full response deadline before falling back to `COLORFGBG`.</violation>
</file>

<file name="packages/opencode/src/cli/cmd/run/theme.ts">

<violation number="1" location="packages/opencode/src/cli/cmd/run/theme.ts:696">
P1: When direct-run palette detection fails on a light macOS terminal with no OSC or `COLORFGBG`, `fallbackMode` still returns dark because it omits the macOS appearance probe. Run `detectSystemAppearance()` when the terminal signals are unavailable before resolving the fallback mode.</violation>

<violation number="2" location="packages/opencode/src/cli/cmd/run/theme.ts:696">
P2: fallbackMode never supplies the `appearance` signal to resolveInitialMode, so on the exact reported scenario — macOS Apple Terminal with no COLORFGBG and no OSC 11 reply (renderer.themeMode stays null) — the direct-run/scrollback fallback still resolves to `"dark"` and repaints a light panel dark, which is the #809 symptom this PR sets out to fix. resolveInitialMode explicitly supports `appearance` (and detectSystemAppearance exists for it); only the dark last-resort stays. Note detectSystemAppearance is async (Promise), so wiring it in requires awaiting it in the fallback path rather than calling fallbackMode synchronously.</violation>

<violation number="3" location="packages/opencode/src/cli/cmd/run/theme.ts:709">
P2: When a runtime palette refresh fails in light mode, this returns a distinct light fallback that `footer.ts` does not recognize as a fallback. The footer then replaces the last known-good theme; preserve the existing theme for either per-mode fallback.</violation>
</file>

<file name="packages/tui/src/terminal-detection.ts">

<violation number="1" location="packages/tui/src/terminal-detection.ts:112">
P3: The `clean` branch resolves any error without a `code` and without `killed` to `"light"`, which contradicts the comment directly above it ("must not be silently reported as light"). Only the missing-AppleInterfaceStyle diagnostic should be treated as light; dropping the `clean` fallback keeps unknown errors as `null` and avoids ever guessing light on an unknown dark terminal (the inverse of the bug this PR fixes).</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

* the direct-run and scrollback renderer.
*/
function fallbackMode(renderer: CliRenderer): "dark" | "light" {
return resolveInitialMode({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: When direct-run palette detection fails on a light macOS terminal with no OSC or COLORFGBG, fallbackMode still returns dark because it omits the macOS appearance probe. Run detectSystemAppearance() when the terminal signals are unavailable before resolving the fallback mode.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/opencode/src/cli/cmd/run/theme.ts, line 696:

<comment>When direct-run palette detection fails on a light macOS terminal with no OSC or `COLORFGBG`, `fallbackMode` still returns dark because it omits the macOS appearance probe. Run `detectSystemAppearance()` when the terminal signals are unavailable before resolving the fallback mode.</comment>

<file context>
@@ -651,6 +676,27 @@ export const RUN_THEME_FALLBACK: RunTheme = {
+ * the direct-run and scrollback renderer.
+ */
+function fallbackMode(renderer: CliRenderer): "dark" | "light" {
+  return resolveInitialMode({
+    colorfgbg: process.env["COLORFGBG"],
+    osc: renderer.themeMode ?? null,
</file context>

Comment thread packages/tui/src/app.tsx
// now. COLORFGBG only buys a shorter wait: with a usable hint in hand we
// can stop waiting sooner, which keeps #704's startup win without
// letting a stale env var override a live answer.
const oscMode = (await renderer.waitForThemeMode(envMode ? 250 : 1000)) ?? null

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When a valid but stale COLORFGBG is present and OSC 11 takes longer than 250 ms, the live terminal answer is discarded and the stale value determines the theme. Keep the OSC probe alive through the full response deadline before falling back to COLORFGBG.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/tui/src/app.tsx, line 278:

<comment>When a valid but stale `COLORFGBG` is present and OSC 11 takes longer than 250 ms, the live terminal answer is discarded and the stale value determines the theme. Keep the OSC probe alive through the full response deadline before falling back to `COLORFGBG`.</comment>

<file context>
@@ -265,9 +265,19 @@ export const run = Effect.fn("Tui.run")(function* (input: TuiInput) {
+        // now. COLORFGBG only buys a shorter wait: with a usable hint in hand we
+        // can stop waiting sooner, which keeps #704's startup win without
+        // letting a stale env var override a live answer.
+        const oscMode = (await renderer.waitForThemeMode(envMode ? 250 : 1000)) ?? null
+        const appearance = oscMode || envMode ? null : await detectSystemAppearance()
+        const mode = resolveInitialMode({ colorfgbg: process.env.COLORFGBG, osc: oscMode, appearance })
</file context>
Suggested change
const oscMode = (await renderer.waitForThemeMode(envMode ? 250 : 1000)) ?? null
const oscMode = (await renderer.waitForThemeMode(1000)) ?? null

const bg = colors.defaultBackground ?? colors.palette[0]
if (!bg) {
return RUN_THEME_FALLBACK
return runThemeFallback(fallbackMode(renderer))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: When a runtime palette refresh fails in light mode, this returns a distinct light fallback that footer.ts does not recognize as a fallback. The footer then replaces the last known-good theme; preserve the existing theme for either per-mode fallback.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/opencode/src/cli/cmd/run/theme.ts, line 709:

<comment>When a runtime palette refresh fails in light mode, this returns a distinct light fallback that `footer.ts` does not recognize as a fallback. The footer then replaces the last known-good theme; preserve the existing theme for either per-mode fallback.</comment>

<file context>
@@ -660,7 +706,7 @@ export async function resolveRunTheme(renderer: CliRenderer): Promise<RunTheme>
     const bg = colors.defaultBackground ?? colors.palette[0]
     if (!bg) {
-      return RUN_THEME_FALLBACK
+      return runThemeFallback(fallbackMode(renderer))
     }
 
</file context>

* the direct-run and scrollback renderer.
*/
function fallbackMode(renderer: CliRenderer): "dark" | "light" {
return resolveInitialMode({

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: fallbackMode never supplies the appearance signal to resolveInitialMode, so on the exact reported scenario — macOS Apple Terminal with no COLORFGBG and no OSC 11 reply (renderer.themeMode stays null) — the direct-run/scrollback fallback still resolves to "dark" and repaints a light panel dark, which is the #809 symptom this PR sets out to fix. resolveInitialMode explicitly supports appearance (and detectSystemAppearance exists for it); only the dark last-resort stays. Note detectSystemAppearance is async (Promise), so wiring it in requires awaiting it in the fallback path rather than calling fallbackMode synchronously.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/opencode/src/cli/cmd/run/theme.ts, line 696:

<comment>fallbackMode never supplies the `appearance` signal to resolveInitialMode, so on the exact reported scenario — macOS Apple Terminal with no COLORFGBG and no OSC 11 reply (renderer.themeMode stays null) — the direct-run/scrollback fallback still resolves to `"dark"` and repaints a light panel dark, which is the #809 symptom this PR sets out to fix. resolveInitialMode explicitly supports `appearance` (and detectSystemAppearance exists for it); only the dark last-resort stays. Note detectSystemAppearance is async (Promise), so wiring it in requires awaiting it in the fallback path rather than calling fallbackMode synchronously.</comment>

<file context>
@@ -651,6 +676,27 @@ export const RUN_THEME_FALLBACK: RunTheme = {
+ * the direct-run and scrollback renderer.
+ */
+function fallbackMode(renderer: CliRenderer): "dark" | "light" {
+  return resolveInitialMode({
+    colorfgbg: process.env["COLORFGBG"],
+    osc: renderer.themeMode ?? null,
</file context>

Comment on lines +112 to +114
const clean = err.code === undefined && err.killed !== true
resolve(notFound || clean ? "light" : null)
},

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P3: The clean branch resolves any error without a code and without killed to "light", which contradicts the comment directly above it ("must not be silently reported as light"). Only the missing-AppleInterfaceStyle diagnostic should be treated as light; dropping the clean fallback keeps unknown errors as null and avoids ever guessing light on an unknown dark terminal (the inverse of the bug this PR fixes).

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/tui/src/terminal-detection.ts, line 112:

<comment>The `clean` branch resolves any error without a `code` and without `killed` to `"light"`, which contradicts the comment directly above it ("must not be silently reported as light"). Only the missing-AppleInterfaceStyle diagnostic should be treated as light; dropping the `clean` fallback keeps unknown errors as `null` and avoids ever guessing light on an unknown dark terminal (the inverse of the bug this PR fixes).</comment>

<file context>
@@ -20,4 +22,99 @@ export function detectModeFromCOLORFGBG(value: string | undefined): "dark" | "li
+          // silently reported as light.
+          const err = error as NodeJS.ErrnoException & { killed?: boolean; status?: number | null; stderr?: string }
+          const notFound = /does not exist/i.test(String(err.stderr ?? err.message ?? ""))
+          const clean = err.code === undefined && err.killed !== true
+          resolve(notFound || clean ? "light" : null)
+        },
</file context>
Suggested change
const clean = err.code === undefined && err.killed !== true
resolve(notFound || clean ? "light" : null)
},
resolve(notFound ? "light" : null)

Marker Guard failed on #1152: theme.ts is an upstream-shared file, so custom
code there must be fenced to survive an upstream merge overwriting it. The
mode-aware fallback added in this branch was unmarked.

Six regions are now fenced: the shared-resolver import, the per-mode seed, the
memoized per-mode fallback theme, the mode probe, and both failure exits in
resolveRunTheme.

Verified with the same command CI runs:
  bun run script/upstream/analyze.ts --markers --base origin/main --strict

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

2 similar comments
@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

@github-actions

Copy link
Copy Markdown

👋 This PR was automatically closed by our quality checks.

Common reasons:

  • New GitHub account with limited contribution history
  • PR description doesn't meet our guidelines
  • Contribution appears to be AI-generated without meaningful review

If you believe this was a mistake, please open an issue explaining your intended contribution and a maintainer will help you.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Code output still renders as white text on light terminal backgrounds (regression of #704)

1 participant