fix(tui): suppress mouse-residue leaks during Python launcher startup (#31213)

* fix(tui): suppress mouse-residue leaks during Python launcher startup

`hermes --tui …` spends ~100–300ms inside the Python launcher (lazy
imports, arg parsing, session resolution) before exec'ing the Node TUI
binary. During that window stdin is still in cooked + echo mode. If a
prior session left DEC mouse tracking asserted (or the user spammed
mouse movement while the previous session was opening), the terminal
keeps emitting `\\x1b[<…M` SGR motion reports that get echoed straight
back into the user's shell scrollback as literal `^[[<…M` text and
sit there above the TUI banner until the next clear.

The Node side already calls `resetTerminalModes()` in `entry.tsx`, but
by then the race is already lost — the bytes echoed during the Python
warmup window were committed to the scrollback before Node started.

Fix: write the mouse-tracking disable sequence at the very top of
`hermes_cli.main`, before every heavy import. The terminal stops
emitting motion events as soon as the bytes hit the wire (one TTY
round-trip), shrinking the race window from hundreds of milliseconds
to a few. `HERMES_TUI_NO_EARLY_DISABLE=1` opts out for diagnostics.

* test(tui): drop dead _reload_main, hoist import out of patch context

Addresses Copilot review on PR #31213.

The tests used to import `hermes_cli.main` inside the `patch("os.write")`
context, which Copilot pointed out is order-dependent: if the module
is already loaded (e.g. imported by a prior test in the same process),
the import is a no-op and the patch only sees the explicit
`_suppress_mouse_residue_early()` call. Either way the assertion can
flake when run alongside other tests.

Move the import to module scope — every subprocess gets a fresh
`hermes_cli.main`, whose module-level invocation is a no-op under
pytest argv. Tests then exercise `_suppress_mouse_residue_early()`
directly inside their own patch context. Also drop the unused
`_reload_main` helper.

* fix(tui): skip early mouse-disable when stdout is not a TTY

Addresses Copilot review on PR #31213.

`hermes --tui … >log` or CI capture pipes fd 1 away from the terminal.
The disable bytes can't reach the terminal in that case but would
still get written into the log file as raw CSI sequences. Guard with
`os.isatty(1)` inside the existing `try/except OSError` block so the
'never break startup' contract holds.

* docs(tui): rephrase 'raw cooked mode' as 'cooked + echo mode'

Copilot review nit on PR #31213 — the original wording was self-
contradictory. Pre-TUI stdin state is cooked + echo (kernel TTY
discipline still owns the line buffer and echoes input back). The
TUI switches it to raw mode later when Ink mounts.
This commit is contained in:
brooklyn! 2026-05-27 22:03:45 -05:00 committed by GitHub
parent 0927fb5584
commit 912e6e2274
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 125 additions and 0 deletions

View file

@ -65,6 +65,39 @@ import os
import sys
# Mouse-tracking residue suppression — runs BEFORE every other import on the
# TUI hot path so the terminal stops emitting SGR/X10 mouse reports while the
# Python launcher is still doing imports (≈100300ms in cooked + echo mode,
# before the Node TUI takes stdin into raw mode). During that window any
# incoming bytes are echoed straight back to the user's shell scrollback as
# ``^[[<…M`` text. The TUI itself runs `resetTerminalModes()` again in
# `entry.tsx`; this is just the earlier cousin. ``HERMES_TUI_NO_EARLY_DISABLE``
# escapes the behaviour for diagnostics.
def _suppress_mouse_residue_early() -> None:
if os.environ.get("HERMES_TUI_NO_EARLY_DISABLE") == "1":
return
if not (os.environ.get("HERMES_TUI") == "1" or "--tui" in sys.argv[1:]):
return
try:
# Skip when stdout is redirected (`hermes --tui … >log`, CI capture):
# the bytes can't reach the terminal anyway and would just pollute
# the log with raw CSI.
if not os.isatty(1):
return
# Disable every mouse-tracking variant we know about. Idempotent and
# safe to send even when no tracking is currently asserted.
os.write(
1,
b"\x1b[?1003l\x1b[?1002l\x1b[?1001l\x1b[?1000l\x1b[?9l"
b"\x1b[?1006l\x1b[?1005l\x1b[?1015l\x1b[?1016l\x1b[?2029l",
)
except OSError:
pass
_suppress_mouse_residue_early()
def _is_termux_startup_environment_fast() -> bool:
"""Tiny Termux check for pre-import startup shortcuts."""
prefix = os.environ.get("PREFIX", "")