Skip to content

Adaptive theme always falls back to dark mode due to incorrect luminance calculation in terminal.ts #23810

@Klassikcat

Description

@Klassikcat

Summary

OpenCode TUI's Adaptive Theme always starts in dark mode regardless of the terminal's actual background color. The terminal background query (OSC 11) works correctly and returns a valid response, but the luminance calculation in mode() incorrectly assumes the RGBA channels are in the 0..255 range when they are in fact normalized to 0..1. As a result, even a pure white background produces a luminance of ~0.0039, which never crosses the 0.5 threshold, so the theme is always resolved as "dark".

Environment

  • OpenCode version: 1.14.20 (also reproduces on the latest dev branch)
  • First reported in: issues related to v1.2.27
  • OS: Ubuntu 24.04 (OS-independent; reproduces on any environment where the terminal supports OSC 11 responses)
  • Terminals: Windows Terminal, kitty, Ghostty, and other terminals that respond to OSC 11
  • Runtime: Bun-based TUI

Details

Expected Behavior

  • When the terminal emulator's background is light → OpenCode TUI should start with the light theme.
  • When the terminal emulator's background is dark → OpenCode TUI should start with the dark theme.

Actual Behavior

  • TUI always starts in dark mode, even when the terminal background is clearly light.
  • Manually toggling via /theme switches between light and dark correctly, but the initial adaptive detection is always biased toward dark.
  • Even after the user unlocks the theme, runtime adaptive events (THEME_MODE) are not reflected correctly because the underlying mode detection is stuck on dark due to the luminance miscalculation.

Root Cause

Location

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

Offending code

function mode(background: RGBA | null): "dark" | "light" {
  if (!background) return "dark"
  const luminance = (0.299 * background.r + 0.587 * background.g + 0.114 * background.b) / 255
  return luminance > 0.5 ? "light" : "dark"
}

Why this is wrong

In this codebase, the .r, .g, and .b channels of an RGBA object are normalized floats in the range 0..1, not 0..255. Other theme-related code in the same repository (context/theme.tsx) treats them this way explicitly:

// 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

Here the channels are explicitly multiplied by 255 before computing luminance.

In contrast, terminal.ts's mode() function takes the already-normalized 0..1 values and divides them by 255 again. Consequences:

  • A white background (1, 1, 1) yields a luminance of ~0.0039 instead of ~1.0.
  • The 0.5 threshold is essentially unreachable, so the result is always "dark" for any non-black background.

Numerical proof

Background r, g, b (normalized) Current luminance Expected luminance Result
White 1.0, 1.0, 1.0 0.0039 1.0 dark (wrong)
Light gray 0.8, 0.8, 0.8 0.0031 0.8 dark (wrong)
Mid gray 0.5, 0.5, 0.5 0.0020 0.5 dark (wrong)
Black 0.0, 0.0, 0.0 0.0 0.0 dark (correct, but coincidental)

Real-world OSC 11 response

Response observed from kitty when querying OSC 11:

b'\x1b]11;rgb:f4f4/f4f4/f4f4\x1b\\'

This is a very light background (~0.957 normalized), but the current code computes luminance ≈ 0.00375"dark".

History

The bug has existed for a while and was preserved across refactors:

Both commits contain the same erroneous / 255 expression.

Proposed Fix

Before

function mode(background: RGBA | null): "dark" | "light" {
  if (!background) return "dark"
  const luminance = (0.299 * background.r + 0.587 * background.g + 0.114 * background.b) / 255
  return luminance > 0.5 ? "light" : "dark"
}

After

function mode(background: RGBA | null): "dark" | "light" {
  if (!background) return "dark"
  const luminance = 0.299 * background.r + 0.587 * background.g + 0.114 * background.b
  return luminance > 0.5 ? "light" : "dark"
}

Impact

  • Fix effect: getTerminalBackgroundColor() now returns the correct "dark" / "light" based on the actual terminal background.
  • TUI propagation: The correct initial mode is propagated along app.tsxThemeProviderresolveTheme(), so the adaptive theme works as intended at startup.
  • Side effects: None — this is purely a fix to a single arithmetic expression; no behavioral change elsewhere.
  • non-TTY / timeout fallback: Still returns "dark" as before (intended behavior preserved).

Related Issues

Verification

  • Static analysis: RGBA usage across the codebase is consistent with 0..1 normalized floats.
  • Numerical reproduction: verified in Node.js that the current expression produces the values shown in the table above.
  • OSC 11 response parsing: confirmed the real terminal response format is parsed correctly.
  • bun typecheck passes in packages/opencode.
  • Build succeeds: bun run script/build.ts --single.
  • Smoke test: built binary runs and responds to --version.

Plugins

No response

OpenCode version

1.14.20

Steps to reproduce

  1. Use a terminal emulator configured with a light background color.
  2. Run the TUI: opencode
  3. Observe that the TUI starts in dark mode.

Screenshot and/or share link

Theme

Terminal Theme: Light

Before Fix

Image

After Fix

Image

Theme

Before Fix

Image

After Fix

Image

Operating System

Ubuntu 24.04

Terminal

Kitty, Warp

Metadata

Metadata

Assignees

Labels

bugSomething isn't workingopentuiThis relates to changes in v1.0, now that opencode uses opentui

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions