Skip to content

fix(tui): SGR mouse sequence leakage into composer under heavy render in tmux #18658

Description

@Nicolas-Formenton

Describe the bug

Under heavy render load in hermes --tui (especially inside tmux with mouse tracking enabled), SGR mouse escape sequences leak into the chat composer as visible garbage text. The composer fills with patterns like:

5;34M;34M35;16;35M35;18;35M36M35;23;36M25;36M6M;29;37M5;32;37M5;...

This is NOT model output — it's terminal mouse-report leakage.

Steps to reproduce

  1. Start Hermes TUI inside tmux: hermes --tui
  2. Ensure mouse tracking is on (default: display.tui_mouse: true)
  3. Have a large session (200k+ tokens) so rendering is heavy
  4. Move the mouse across the terminal rapidly for several seconds
  5. Observe that the composer () fills with numeric garbage matching digit;digit;digitM patterns

Root cause

The mechanism

  1. Hermes activates DECSET 1000+1002+1003+1006 (SGR mouse tracking) via ENABLE_MOUSE_TRACKING in packages/hermes-ink/src/ink/termio/dec.ts
  2. The terminal responds with SGR mouse reports: \x1b[<btn;col;rowM (press) and \x1b[<btn;col;rowm (release)
  3. Under heavy render (React reconciler busy), stdin is not read promptly
  4. Mouse events accumulate in the OS tty buffer
  5. The 50ms incomplete-escape flush timer in App.tsx (line ~138, NORMAL_TIMEOUT = 50) fires
  6. The \x1b prefix is flushed as a lone ESC key event (via input-event.ts)
  7. The remaining [<btn;col;rowM arrives as text on the next chunk
  8. parseTextWithSgrMouseFragments() in parse-keypress.ts attempts to recover these fragments via SGR_MOUSE_FRAGMENT_RE

Why the current fix is incomplete

The existing fix (commits 71b685aee and ded011c5a) added SGR_MOUSE_FRAGMENT_RE and parseTextWithSgrMouseFragments() to catch orphaned SGR fragments. This works for simple cases but fails when:

  1. Extreme concatenation: Under sustained render backpressure, dozens of mouse events pile up. Fragments concatenate without separators, and the regex can't always match overlapping/adjacent fragments correctly.

  2. Corrupted fragments: When multiple writes to the composer buffer overlap during re-entry to the event loop, partial fragments get interleaved, producing patterns that don't match any regex (e.g., 37M37M6; — two M final bytes in a row).

Code path

The critical path is in parse-keypress.ts:

stdin chunk → createTokenizer({x10Mouse: true}) → tokens
  → for each 'text' token:
      parseTextWithSgrMouseFragments(token.value)  // tries to recover
        if recovery fails: parseKeypress(token.value)  // falls through
          → default key with name="" → leaks into input

// App.tsx flush timer:
incompleteEscapeTimer → tokenizer.flush()
  → splits ESC from continuation
  → ESC → 'escape' key event
  → '[<...M' → tries parseTextWithSgrMouseFragments

Expected behavior

SGR mouse events should never appear as visible text in the composer, even under extreme render load and tmux.

Suggested improvements

  1. Buffer-escaping: If parseTextWithSgrMouseFragments can't recover, strip the pattern rather than letting it leak as visible text (whitelist approach: if text looks like a corrupted SGR mouse fragment, drop it silently)

  2. Staggered flush: Instead of always flushing at exactly 50ms, push the timer back up to ~200ms when CSI state is active and stdin data is arriving rapidly (indicates mouse tracking is active)

  3. Rate-limit mouse tracking re-assertion: The onStdinResume handler re-enables mouse tracking. Consider adding a cooldown or detecting the leak and temporarily disabling mouse tracking when fragments are detected

  4. Disambiguate flush vs keypress ESC: When the flush timer fires with a buffered ESC where the next byte would start a CSI sequence, emit a synthetic "mouse leak detected" event instead of an ESC keypress

Environment

  • OS: Linux (WSL2 Ubuntu)
  • Terminal: tmux with mouse on
  • Hermes version: f98b5d00a (main)
  • Session size at time of leak: ~271k tokens

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Medium — degraded but workaround existscomp/tuiTerminal UI (ui-tui/ + tui_gateway/)sweeper:implemented-on-mainSweeper: behavior already present on current maintype/bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions