Startup theme auto-detection still writes raw OSC 11 query bytes to the terminal
Nobody has claimed this yet.
- Dominant language
- Rust
- Stars
- 19.9k
- Forks
- 2.3k
- Avg merge
- 2d 7h
- Merged PRs (30d)
- 30
Description
Summary
jcode still writes an OSC 11 background-color query to the terminal during interactive startup on current upstream master.
On terminals, wrappers, or host environments that do not safely consume that query, the raw control bytes become visible to the user as startup garbage.
This is not the older dual-reply/composer-corruption bug fixed by e67d74e47. That fix reduced the probe from OSC 10 + OSC 11 to OSC 11 only, but the remaining startup-side write is still observable and still user-visible on some terminals.
Current upstream status
Verified against upstream default branch master at:
- commit:
70dcb085d(origin/masteras fetched on 2026-08-18 23:42 UTC)
Relevant code on current upstream:
crates/jcode-tui/src/tui/theme_detect.rs:158-214- specifically:
:163-171capability gateterminal_background_query_supported(...):191-199call toterminal_colorsaurus::background_color(options)
The latest upstream still performs the startup-time background query.
Repro steps
Interactive TTY repro, using a clean JCODE_HOME and telemetry disabled so the startup bytes are easy to inspect:
JCODE_HOME=/tmp/jcode-issue-home \
JCODE_NO_TELEMETRY=1 \
TERM=xterm-256color \
TERM_PROGRAM=Orca \
python3 - <<'PY'
import os, pty, select, subprocess, time
master, slave = pty.openpty()
env = os.environ.copy()
p = subprocess.Popen(['jcode'], stdin=slave, stdout=slave, stderr=slave, env=env, close_fds=True)
os.close(slave)
end = time.time() + 0.9
buf = bytearray()
sent = False
while time.time() < end:
r, _, _ = select.select([master], [], [], 0.05)
if master in r:
try:
data = os.read(master, 65536)
except OSError:
break
if not data:
break
buf.extend(data)
if not sent and time.time() > end - 0.2:
try:
os.write(master, b'\x03')
sent = True
except OSError:
pass
print(bytes(buf)[:120].hex())
PY
On current upstream master, the capture begins with:
1b5d31313b3f071b5b63
Which decodes to:
ESC ] 11 ; ? BEL ESC [ c
Meaning:
OSC 11background-color query- immediately followed by
DA1/CSI c
Observed bytes
Raw injected bytes at startup:
1b 5d 31 31 3b 3f 07 1b 5b 63
Observed startup prefix from the current upstream build:
1b5d31313b3f071b5b63436f6e6e656374696e6720746f207365727665722e2e2e0d0a1b5b3f31303439681b5b3e37751b5b3f32303034681b5b3f31303034681b5b3f31303030681b5b3f31303032681b5b3f31303033681b5b3f31303135681b5b3f31303036681b5d303b6a636f646507...
So the query is emitted before normal TUI setup such as alternate-screen entry and title-setting.
Expected behavior
Startup should not emit raw terminal query bytes that can become visible to the user.
More concretely:
- if jcode cannot prove that the host terminal safely supports this probe, it should not send the query
display.theme=autoshould degrade to a safe default (dark) instead of writing potentially visible OSC bytes- startup should remain visually clean even on wrappers, multiplexers, remoting layers, or nonstandard terminal hosts
Actual behavior
On affected hosts, users can see visible startup garbage originating from the theme auto-detection query.
Current upstream no longer leaks the old dual-query OSC 10/OSC 11 behavior into the composer, but it still writes a single OSC 11 probe during startup, and that is enough to be user-visible in some environments.
Affected terminals / hosts
Confirmed affected environment during repro:
TERM=xterm-256colorTERM_PROGRAM=Orca
The core issue is broader than Orca specifically: the current gate treats any non-empty TERM_PROGRAM or LC_TERMINAL as a strong positive signal for OSC-query safety, but that does not guarantee the host will consume the query invisibly.
Root cause
terminal_background_query_supported(...) is too permissive:
if term_program.is_some_and(|value| !value.trim().is_empty())
|| lc_terminal.is_some_and(|value| !value.trim().is_empty())
{
return true;
}
That logic answers “this looks like a terminal emulator” rather than the stronger property jcode actually needs here:
“sending an OSC 11 query during startup will not surface raw bytes to the user”
So display.theme=auto still enters the probe path in environments that are not safe for that startup write.
Suggested fix
Any of the following would fix the user-visible bug:
-
Tighten the allowlist
- Only send the startup OSC 11 query for terminals explicitly known to handle it safely.
- Do not treat arbitrary
TERM_PROGRAM/LC_TERMINALvalues as sufficient proof.
-
Default to safe fallback
- If safety cannot be established, skip probing and default to
ThemeMode::Dark.
- If safety cannot be established, skip probing and default to
-
Keep the manual override as the escape hatch
JCODE_THEME=dark|lightalready avoids the probe path and can remain the explicit override.
Extra context
The earlier bug was introduced by 9e28c2bf5 (feat(tui): detect light terminal themes and adapt colors), which used terminal_colorsaurus::theme_mode(options) and therefore sent both OSC 10 and OSC 11.
That older behavior was partially fixed by e67d74e47 (fix(tui): prevent theme query replies entering composer), which switched the code to background_color(options) and removed the second query/reply leak.
However, the remaining single-query startup write is still observable and still looks like an upstream bug, because it violates the user-facing expectation that startup should not print raw terminal control bytes.
Drafted by gpt-5.4.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in crates/jcode-tui/src/tui/theme_detect.rs, especially terminal_background_query_supported(...) and the background_color(options) call around lines 163-199. Reproduce the startup bytes with the provided PTY script, then trace the auto-detection path and its existing tests or callers. Done means unsafe hosts skip the OSC 11 query, fall back to dark, and startup remains free of raw query bytes.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- cli
- Issue type
- Bug
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Activity status
- Quiet
- Clarity
- Mostly clear
- Newbie friendliness
- 68/100