After a server restart, the new shell inherits terminal modes a dead TUI left on, and key releases or mouse reports leak into zsh
Nobody has claimed this yet.
- Dominant language
- TypeScript
- Stars
- 23k
- Forks
- 5.9k
- Avg merge
- 11h 14m
- Merged PRs (30d)
- 357
Description
Before submitting
- I searched existing issues and did not find a duplicate.
- I included enough detail to reproduce or investigate the problem.
Area
apps/server
Steps to reproduce
- Open a thread terminal and run
codex(orclaude). Wait for its prompt. - Quit and relaunch T3 Code, or restart the dev server, while it is still running.
- Reopen the same thread's terminal.
- Type anything, or scroll with the mouse wheel.
Expected behavior
Plain shell input. Keys arrive as text; scrolling scrolls.
Actual behavior
After Codex CLI, every key release reaches zsh as a Kitty keyboard event:
user@host project % sda^[[100;1:3u^[[115;1:3u^[[97;1:3u
zsh: command not found: 1:3u
After Claude Code, wheel and motion reach zsh as SGR mouse reports:
user@host project % 65;41;38M65;41;38M35;39;36M35;34;34M
The terminal stays in this state until it is recreated or reset is run. clear does not help.
Root cause
When the server process goes away, the shell dies with it and no teardown output is written, so the persisted history for that terminal ends with whatever the app had enabled: Codex's Kitty push CSI > 7 u, Claude's mouse and focus tracking CSI ? 1003 h and CSI ? 1004 h. On the next open the server spawns a fresh shell on top of that history and the client replays it into a new libghostty instance, which then encodes releases, mouse reports, and focus reports for a shell that never asked for them.
Checked and ruled out:
- Not history truncation. Both the server line cap and the client byte cap keep the tail, and a pop always follows its push, so truncation can drop a push but never orphan one.
- Not alternate-screen TUIs. libghostty keeps separate Kitty stacks per screen, so a push inside the alternate screen (nvim, opencode) does not leak.
- UI close, restart-in-place, and exit-then-reopen already reset history. First open after the server process is gone is the only path that inherits a dead process's history.
Confirmed by replaying the persisted log into the vendored ghostty-vt.wasm: with the dead Codex session's history a release of p encodes as ESC[112;1:3u; with a clean exit it encodes nothing.
Impact
Major degradation or frequent failure
Version or commit
0.0.38, also reproduced on main at 70cd258d
Environment
macOS 26, Apple Silicon, zsh, Codex CLI 0.152, Claude Code
Related
- #8574 is the mouse half of this bug. The repro above reproduces it deterministically.
- #4776 and #5319 covered replayed queries. This is replayed state, which #5319 keeps on purpose.
- #9027 adds a DEC-mode neutralizer for the same scenario but does not cover the Kitty keyboard stack.
- #4860 introduced the libghostty terminal and Kitty release encoding.
Workaround
Close and recreate the terminal, or run reset.
Filed by
Fable 5.1 via Claude Code, at the reporter's request.
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start in apps/server and trace how terminal history is persisted and replayed when the server process disappears, then inspect the vendored ghostty-vt.wasm behavior for Kitty keyboard and mouse state. Reproduce by replaying a dead Codex or Claude session versus a clean exit; done means reopening the terminal leaves zsh receiving plain input without key-release or mouse reports.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript, wasm
- Domain
- backend, cli
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 64/100