TUI: one failed color requery permanently disables OSC 10/11 re-detection (poisoned cache in requery_default_colors)
What version of Codex is running?
codex-cli 0.146.0 (macOS, arm64)
What issue are you seeing?
A single failed terminal-color requery permanently disables OSC 10/11 color re-detection for the rest of the process lifetime, leaving the composer/input colors stale forever (e.g. after the OS switches between light and dark appearance). Only restarting Codex recovers it.
The TUI re-detects terminal default colors on Event::FocusGained via terminal_palette::requery_default_colors(). In codex-rs/tui/src/terminal_palette.rs (tag rust-v0.146.0):
pub(super) fn requery_default_colors() {
if let Ok(mut cache) = default_colors_cache().lock() {
// Don't try to refresh if the cache is already attempted and failed.
if cache.attempted && cache.value.is_none() {
return;
}
let fg = query_foreground_color().ok().flatten().and_then(color_to_tuple);
let bg = query_background_color().ok().flatten().and_then(color_to_tuple);
cache.value = fg.zip(bg).map(|(fg, bg)| DefaultColors { fg, bg });
cache.attempted = true;
}
}
Two problems compound here:
- If either crossterm query fails or times out once (for example a focus event delivered while the TUI is busy and the reply misses the read deadline),
cache.value = fg.zip(bg)overwrites a previously good cached value withNone. - The guard at the top then treats that state as "attempted and failed" and short-circuits every future requery permanently. No later
FocusGainedcan ever repair it.
So one unlucky requery poisons the cache for the life of the process, even in a terminal that answers every OSC 10/11 query correctly.
Observed behavior
Host terminal: xterm.js-based terminal whose PTY layer answers OSC 10/11 queries in well under 1 ms (instrumented). One long-running Codex process:
- Answered color queries continuously after startup (the host counted 1178 queries, 1178 answers).
- After one requery at ~5 s of process age, it never emitted another OSC 10/11 query.
- Subsequent focus reports (
CSI I), including synthetic ones injected directly into the PTY hours later, produce no requery at all — consistent with the short-circuit above. - Meanwhile sibling Codex processes of the same version in the same terminal (not yet poisoned) requery and repaint correctly on the same
CSI Isignal.
Net effect for users: after a macOS auto light/dark switch, the composer input background stays in the old theme's color and nothing short of restarting Codex fixes it.
Reproduction sketch
- Run Codex in a terminal that supports OSC 10/11 and focus reporting (mode 1004).
- Cause exactly one requery to miss its deadline (e.g. deliver a focus event while the TUI is saturated, or briefly delay the OSC reply past the crossterm timeout).
- Change the terminal background color and focus the terminal again.
- Codex never re-queries; colors remain stale until restart.
Suggested fix
In the failure path of requery_default_colors():
- Keep the previous
cache.valuewhen the fresh query returnsNone(stale colors are strictly better than none, and the nextFocusGainedcan retry), and/or - Don't let a failed requery latch the
attempted && value.is_none()short-circuit — reserve that short-circuit for the bounded startup probe, so focus-driven requeries remain retryable.
Either is a small change and makes color re-detection self-healing.
Related: #5107 (unsolicited OSC 11 replies are mis-parsed into the composer), which rules out the host-terminal workaround of proactively pushing color updates.