Skip to content

bug: missing SIGTSTP handler causes mouse garbling when process is externally suspended #906

Open

Description

@agutmanstein-scale

Description

When a process using @opentui/core receives SIGTSTP from an external source (e.g., code-server terminal management, job control, kill -TSTP), the process is suspended by the kernel without any terminal cleanup. Mouse tracking remains enabled, causing mouse events to appear as garbled escape sequences in the shell.

Root Cause

opentui's exitSignals list includes SIGINT, SIGTERM, SIGQUIT, SIGABRT, SIGHUP, SIGBREAK, SIGPIPE, SIGBUS, SIGFPE — but no SIGTSTP.

There is no process.on("SIGTSTP") handler anywhere in the renderer. When SIGTSTP arrives:

  • Mouse tracking (\x1b[?1000h, \x1b[?1003h, \x1b[?1006h) stays enabled
  • Kitty keyboard protocol stays enabled
  • Raw mode stays on (until the shell overrides)
  • The shell receives mouse events as raw escape sequences → garbled text

The Correct Pattern Already Exists

The manual suspend() method does this correctly:

suspend() {
  this._suspendedMouseEnabled = this._useMouse
  this.disableMouse()              // 1. disable mouse
  this.removeExitListeners()
  this.stdinParser?.reset()
  this.stdin.removeListener("data", this.stdinListener)
  this.lib.suspendRenderer(...)    // 2. native suspend
  this.stdin.setRawMode(false)     // 3. raw mode off
  this.stdin.pause()
}

But this is only called from consumer code (e.g., a keybind handler). An external SIGTSTP bypasses it entirely.

Proposed Fix

Register SIGTSTP/SIGCONT handlers in the renderer that call suspend()/resume():

private sigtstpHandler = () => {
  this.suspend()
  // Remove handler to allow default behavior (suspend)
  process.removeListener("SIGTSTP", this.sigtstpHandler)
  // Re-raise to actually suspend the process
  process.kill(process.pid, "SIGTSTP")
}

private sigcontHandler = () => {
  // Re-register SIGTSTP handler
  process.on("SIGTSTP", this.sigtstpHandler)
  this.resume()
}

Register in setupTerminal(), remove in cleanupBeforeDestroy().

Note: In Node/Bun, registering a SIGTSTP handler overrides the default behavior (immediate suspend). The handler must remove itself and re-raise SIGTSTP to actually suspend. The SIGCONT handler re-registers it.

Reproduction

  1. Run any opentui TUI application with mouse tracking enabled
  2. In another terminal: kill -TSTP $(pgrep -f <app>)
  3. Move the mouse in the shell
  4. Garbled escape sequences appear: 35;89;19M35;84;20M35...

Observed in opencode running in code-server (VS Code web terminal) where the terminal management layer can send SIGTSTP during inactivity.

Related

Activity

  1. added a commit that references this issue on Apr 1, 2026
    a23aad1
  2. added
    bugSomething isn't working
    coreThis relates to the core package
    on Apr 30, 2026
  3. djbclark commented on Sep 27, 2026

    @djbclark

    Additional OpenCode data point: anomalyco/opencode#51600

    On macOS 27 / Bash 5.3, OpenCode 1.18.32 spontaneously becomes job-control stopped after rendering, without Ctrl-Z. jobs -l captured both:

    Stopped (tty output): 22   command opencode --auto
    Stopped (tty input): 21    command opencode attach http://127.0.0.1:4096
    

    After the stop, Bash owns the TTY foreground PGID and OpenCode remains in a distinct stopped PGID; mouse movement then leaks SGR sequences. It reproduces with --pure, in both Ghostty and a Herdr PTY, and with 1.18.30 and 1.18.32. Temporarily disabling Bash monitor mode around the TUI (set +m; opencode; set -m) keeps both processes in one foreground PGID and prevents the stop.

    This suggests the cleanup gap applies to SIGTTIN/SIGTTOU stops as well as external SIGTSTP, although the foreground-PGID loss itself likely needs fixing in OpenCode/Bun rather than only cleanup here.

  4. simonklee commented on Sep 27, 2026

    @simonklee
    Member

    Thanks, I’ll look into this

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingcoreThis relates to the core package

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions