Repository navigation
Description
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
- Run any opentui TUI application with mouse tracking enabled
- In another terminal:
kill -TSTP $(pgrep -f <app>) - Move the mouse in the shell
- 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
- bug: mouse escape sequences garbled after renderer destroy — cleanupBeforeDestroy() disables raw mode before mouse tracking #904 / fix(renderer): disable mouse tracking before raw mode in destroy path #905 — cleanupBeforeDestroy ordering fix (different bug, same symptom)
Activity
fix(renderer): handle SIGTSTP to prevent mouse garbling on external s…
djbclark commentedon Sep 27, 2026
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 -lcaptured both: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.
simonklee commentedon Sep 27, 2026
Thanks, I’ll look into this