Skip to content

fix(tty): drain pending feature-detection replies on exit#391

Merged
esengine merged 1 commit intomainfrom
fix/365-drain-tty-on-exit
May 7, 2026
Merged

fix(tty): drain pending feature-detection replies on exit#391
esengine merged 1 commit intomainfrom
fix/365-drain-tty-on-exit

Conversation

@esengine
Copy link
Copy Markdown
Owner

@esengine esengine commented May 7, 2026

Closes #365.

Why

Reporter saw ^[]11;rgb:...^[\^[[33;1R^[[?62;1;4c printed by fish / bash right after exit. Those bytes are responses, not queries:

  • \x1b]11;rgb:...\x1b\ — OSC 11 reply (terminal background color)
  • \x1b[33;1R — CPR reply (cursor position)
  • \x1b[?62;1;4c — DA1 reply (primary device attributes)

So something sent the matching queries (\x1b]11;?\x1b\, \x1b[6n, \x1b[c) into the tty during the session. I scanned the v0.30.2 dist bundle for the raw query bytes — zero hits. The source is the runtime (bun does some tty feature detection on startup) or a transitive dep doing color-scheme detection. Either way, the responses sat in stdin's queue until reasonix exited; the parent shell then read them and rendered the bytes as if the user typed them.

What changes

  • src/cli/ui/drain-tty.ts — new drainTtyResponses(timeoutMs = 50) helper. Sets raw mode, reads-and-discards anything pending for timeoutMs, restores cooked. No-op when stdin isn't a TTY or raw mode isn't available.

  • src/cli/commands/chat.tsxawait drainTtyResponses() in the exit finally block, after runtime.closeAll().

Two-layer mitigation:

  • Upstream (already in 0.30.3): alt-screen default wraps the session in \x1b[?1049h / \x1b[?1049l, which flushes the input queue on most terminals.
  • Program-side (this PR): drain regardless of which terminal the user is in, so we don't depend on any specific terminal's DECSET 1049 behavior.

The drain runs even with --no-alt-screen, so legacy in-shell scrollback users get the fix too.

Tests

tests/drain-tty.test.ts — 3 cases: returns immediately on non-TTY stdin; respects the timeout cap when raw mode is available; doesn't blow up when a terminal-response burst arrives mid-window.

Test plan

  • npm run verify — 135 files / 2155 passed / 1 skipped (was 2152, +3 new)
  • Manual on Arch + fish (the original reproducer environment): bunx reasonix@<this PR> code → exit → no escape junk in fish prompt
  • Manual: --no-alt-screen mode → still gets the drain → no junk
  • Manual: starting reasonix from a piped / non-TTY stdin (CI smoke) — drain no-ops cleanly

Reporter saw `^[]11;rgb:...^[\^[[33;1R^[[?62;1;4c` printed by fish /
bash right after exiting reasonix. Those bytes are RESPONSES — OSC 11
bg-color, CPR cursor-position, DA1 device-attributes — sent by the
terminal in reply to queries the runtime / a dep emitted during
session startup. The dist bundle has none of those query bytes
itself, so the source is bun's tty feature-detection path or a
transitive dep doing color-scheme detection.

Whoever sent them, the responses sat in stdin's queue until reasonix
exited. The parent shell then read them and rendered the bytes as
input.

Add `drainTtyResponses(50ms)` and call it in chat.tsx's exit finally,
after `runtime.closeAll()`. Best-effort: sets raw mode, reads-and-
discards anything still queued for 50ms, restores cooked. No-op when
stdin isn't a TTY or raw mode isn't available.

0.30.3's alt-screen default is the upstream mitigation (DECSET 1049
flushes the input queue on most terminals); this is the program-side
mitigation that's independent of which terminal the user is in.
@esengine esengine merged commit 0e4cba3 into main May 7, 2026
3 checks passed
@esengine esengine deleted the fix/365-drain-tty-on-exit branch May 7, 2026 15:53
@esengine esengine mentioned this pull request May 7, 2026
6 tasks
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.

Exiting reasonix leaves unknown text in fish shell

1 participant