Skip to content

Light themes for dark-painted TUIs, and color-scheme reporting - #65

Merged
jamesyong-42 merged 4 commits into
mainfrom
feat/light-mode-adaptation
Oct 3, 2026
Merged

jamesyong-42 merged 4 commits into
mainfrom
feat/light-mode-adaptation

Conversation

@jamesyong-42

@jamesyong-42 jamesyong-42 commented Oct 3, 2026 •

Copy link
Copy Markdown
Member

Why

With a light ghosttea theme, several agent TUIs stay dark or half-broken:

  • Grok paints #141414 on every cell, so the terminal background never shows. Its auto theme follows the macOS appearance, not the terminal.
  • Claude Code's "Auto (match terminal)" theme reads our OSC 11 reply correctly, but never learns about a theme change, because we don't send mode-2031 reports.
  • Codex and OpenCode already adapt from OSC 11.

A probe showed CSI ? 996 n going unanswered and DECRQM claiming mode 2031 support while no CSI ? 997 n was ever sent.

What changes

feat(terminal): report the color scheme

  • The VT shim derives light/dark from the default background, answers CSI ? 996 n, and queues CSI ? 997 ; 1|2 n when a theme change flips the scheme and the program enabled mode 2031.
  • New sessions get COLORFGBG (0;15 / 15;0) unless the caller set one.

feat(react): adapt dark-painted panes to light themes

  • New renderers/light-adaptation.ts, applied per style in resolveStyle before blending. Glyph coverage, color glyphs and the shader stack are untouched.
  • It only engages when the theme is light and at least 60% of a pane's cells carry an explicit dark background, and releases below 35%. Apps that follow the theme are never touched.
  • Surfaces mirror their toe-corrected OKLab lightness around the theme background. Neutral ink keeps its distance from its cell background, and accents land in an APCA mid-tone band with their hue kept. Block elements use the surface mapping.
  • TerminalTheme.lightAdaptation defaults to "auto" from configuration; embedders can pass "off" through the workspace theme override.

The rules come from capturing each app's dark and own-light screens through a private ghosttead and scoring candidate remaps against them. Mean ink ΔE against each app's own light design (lower is better):

Remap Grok Codex OpenCode
Full-screen invert 5.0 10.1 5.5
This remap 3.1 3.3 3.7

A full-screen invert also flips hue: Grok's amber accent turns blue and Claude's red diff rows turn cyan.

Verification

  • New tests: ghosttea-vt (996 reply, 2031 reports only on a scheme flip), ghosttea-core (theme change emits the report as WriteToTransport), ghosttea (COLORFGBG hint yields to the caller), light-adaptation.test.ts (parity with the measured colors, engagement thresholds, hysteresis, opt-out).
  • ghosttea-react: tsc, eslint, prettier, and 275 vitest tests pass. The ghosttea-vt, ghosttea-core and ghosttea suites pass.
  • Live daemon: with Claude Code's Auto theme highlighted, reloading the theme mid-session switches its preview to light. The previous binary stays dark.
  • Real renderer: Grok frames replayed through WebGpuTerminalRenderer render light with the card border, gray hierarchy and amber accent intact.

service::protocol_tests::closing_a_session_records_why_it_ended is flaky here. It failed 1 of 4 runs on unmodified main too.

CI notes

  • chore(deps) commit: Rust 1.99.0 (2026-09-28) added clippy::double_must_use, which fires on code generated by async-trait 0.1.89 in mesh.rs. That broke the required verify and windows gates for every branch. async-trait 0.1.92 fixes it; it and syn 3 need Rust 1.71, inside our 1.88 minimum.
  • audit-published: expected to fail. This PR changes native sources, so the pinned Apple native artifact predates them until the next release rebuilds and pins it.
  • dependency-audit: new npm advisories (vitest, electron, undici, brace-expansion). This is unrelated and fails on main too.

Not in this PR

  • A Ghostty config key and settings toggle for lightAdaptation.
  • Palette provenance in frame style records, which would let palette colors map exactly to the light theme's palette.
  • A hue-preserving minimum-contrast.
  • The remap in the Apple Metal renderer. iOS does get the signaling fix through the shared shim.
  • A manual pass in the desktop app with Grok.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

jamesyong-42 and others added 4 commits October 2, 2026 18:44
Programs with an automatic theme (Claude Code's Auto, Neovim) can detect
light or dark through the color-scheme query and mode 2031, but the shim
never installed the color-scheme callback: CSI ? 996 n went unanswered,
and DECRQM reported mode 2031 as supported while no change report was
ever sent. A theme switch left those programs on the old scheme.

The shim now derives the scheme from the default background (light when
dark text out-contrasts light text), answers CSI ? 996 n with it, and
queues CSI ? 997 ; 1|2 n on the PTY response path when a theme change
flips the scheme and the program enabled mode 2031. New sessions also get
COLORFGBG unless the caller set one, for programs that read it at startup.

Verified against a live daemon: Claude Code's Auto preview now follows a
mid-session theme reload; with the previous binary it stayed dark.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Applications that paint every cell with their own background ignore the
terminal theme; Grok's default theme fills the screen with #141414, so a
light theme never shows. Under a light theme, the WebGPU renderer now
remaps a pane when at least 60% of its cells carry an explicit dark
background (released below 35% to avoid flicker), so programs that follow
the theme are never touched.

The remap runs per style in resolveStyle, before blending, leaving glyph
coverage, color glyphs, and the shader stack alone. Surfaces mirror their
toe-corrected OKLab lightness around the theme background, neutral ink
keeps its distance from its cell background, and accents land in an APCA
mid-tone band with their hue kept. Block elements use the surface mapping
so art seams line up. Against each app's own light design this measured
mean ink ΔE 3.1 (Grok), 3.3 (Codex), 3.7 (OpenCode), versus 5.0, 10.1,
and 5.5 for a full-screen invert.

Configuration defaults TerminalTheme.lightAdaptation to "auto"; embedders
can pass "off" through the workspace theme override.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rust 1.99.0 (2026-09-28) added clippy::double_must_use, which fires on
the code async-trait 0.1.89 generates for the mesh traits, so the
required verify and windows gates fail under `-D warnings` on any branch.
0.1.92 no longer emits the redundant attribute. It and its new syn 3
dependency need Rust 1.71, inside the workspace's 1.88 minimum.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…entory

async-trait is not in the aarch64-apple-ios graph, so the reviewed
components are unchanged; only the Cargo.lock digest and the BOM and
resource hashes derived from it move.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@jamesyong-42
jamesyong-42 merged commit 63e5d72 into main Oct 3, 2026
8 of 10 checks passed
@jamesyong-42
jamesyong-42 deleted the feat/light-mode-adaptation branch October 3, 2026 02:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant