Skip to content

[Bug]: TUI can be suspended by tty input and leave terminal modes dirty #3655

@lizhengwu

Description

@lizhengwu

Version line

v2 — Go rewrite (1.x), main-v2 (active development)

Exact version

reasonix desktop-v1.3.0-44-gba094b9f

What happened?

Problem

Reasonix TUI was stopped by Unix/macOS job control with:

[1] + 16300 suspended (tty input) reasonix

After returning to the shell, the terminal showed many mouse/terminal escape fragments, for example:

70m0;22;70M32;21;70M32;20;70M...

This looks like the TUI was stopped while alt-screen / raw mode / mouse tracking were still active, leaving the shell terminal in a dirty state.

Screenshot:

Evidence

Process state showed reasonix was stopped, not crashed:

PID    PPID  PGID  TPGID STAT TTY      COMMAND
14708 14707 14708  14708 S+   ttys005  -/bin/zsh
16300 14708 16300  14708 T    ttys005  reasonix
16305 16300 16300  14708 T    ttys005  ...codegraph.js serve --mcp

Key points:

- reasonix was in state T, meaning stopped/suspended.
- reasonix PGID was 16300.
- terminal foreground PGID was 14708, the shell.
- This matches a background process group reading from TTY and receiving SIGTTIN.

Code context

The TUI enables alt-screen and mouse tracking in the non-Termux path:

v.AltScreen = true
v.MouseMode = tea.MouseModeCellMotion

The controller cleanup currently runs only after tea.Program.Run() returns. If the OS stops the process inside Run(), final cleanup does not run.

A targeted search found no explicit handling for:

ctrl+z
tea.Suspend
SIGTTIN
SIGTTOU
SIGTSTP
SIGCONT

Current hypothesis

Likely flow:

1. Reasonix TUI enables alt-screen / raw mode / mouse tracking.
2. The reasonix process group is no longer the terminal foreground process group.
3. Reasonix still tries to read from the TTY.
4. Unix/macOS job control sends SIGTTIN.
5. The process is stopped inside the TUI runtime.
6. Normal cleanup does not run.
7. The shell becomes foreground again while terminal modes are still dirty.
8. Mouse/terminal escape sequences become visible in the shell.

### Steps to reproduce

Proposed fix plan

I plan to split this into three small PRs.

PR 1: Ctrl+Z suspend mitigation

Map Ctrl+Z to Bubble Tea's suspend path.

Goal:

- explicit user suspend releases terminal state before stopping;
- fg can resume the TUI cleanly.

This is a mitigation only. It does not fully cover background tty-read / SIGTTIN.

PR 2: Terminal reset failsafe + idempotent cleanup

Add a conservative terminal reset fallback on final exit/error paths, and make Controller.Close() idempotent.

Goal:

- normal exit leaves the shell terminal usable;
- error exit leaves the shell terminal usable;
- final controller/plugin/job cleanup runs once;
- suspend/resume does not call Controller.Close().

This supports the full fix but does not by itself handle a process already stopped by job control.

PR 3: Unix job-control guard

Add a Unix-only job-control guard for:

SIGTSTP
SIGTTIN
SIGTTOU
SIGCONT

Goal:

- release terminal before job-control stop where possible;
- preserve shell job-control behavior;
- after SIGCONT, restore terminal only after the process group is foreground again;
- cover background tty-read / SIGTTIN cases.

This PR will be gated by a local PTY/job-control POC before merge.

Success criteria

- Ctrl+Z releases terminal before suspend.
- fg resumes TUI cleanly.
- Background tty-read / SIGTTIN does not leave shell-visible terminal escape residue.
- Normal exit and error exit leave terminal usable.
- Resume then exit performs final cleanup once.
- Controller/plugin/job cleanup does not run during suspend/resume.

### OS / platform

macos 15.7

### Relevant logs or output

```shell

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingmacosmacOS-specifictuiTerminal UI / CLI (internal/cli, internal/control)v2Go rewrite (1.x) — main-v2 branch, active development

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions