fix(tui): drain Kitty key release events before exit to prevent SSH leak

Adds Terminal.prepareForExit() to disable Kitty protocol and wait for
in-flight release events before fully stopping. This prevents escape
sequences from leaking to the parent shell over slow SSH connections.

Fixes #1204
This commit is contained in:
Mario Zechner 2026-02-03 00:01:39 +01:00
parent c9a20a3aa4
commit 9a4d043b28
5 changed files with 43 additions and 1 deletions

View file

@ -2,6 +2,14 @@
## [Unreleased]
### Added
- Added `Terminal.prepareForExit()` method to drain Kitty key release events before exit
### Fixed
- Fixed Kitty key release events leaking to parent shell over slow SSH connections ([#1204](https://github.com/badlogic/pi-mono/issues/1204))
## [0.51.1] - 2026-02-02
### Added

View file

@ -12,6 +12,14 @@ export interface Terminal {
// Stop the terminal and restore state
stop(): void;
/**
* Prepare for process exit by disabling Kitty protocol and draining stdin.
* Call this before stop() when exiting to prevent Kitty key release events
* from leaking to the parent shell over slow SSH connections.
* @param drainMs - How long to drain stdin (default: 50ms)
*/
prepareForExit(drainMs?: number): Promise<void>;
// Write output to terminal
write(data: string): void;
@ -150,11 +158,25 @@ export class ProcessTerminal implements Terminal {
process.stdout.write("\x1b[?u");
}
async prepareForExit(drainMs = 50): Promise<void> {
if (!this._kittyProtocolActive) return;
// Disable Kitty keyboard protocol first
process.stdout.write("\x1b[<u");
this._kittyProtocolActive = false;
setKittyProtocolActive(false);
// Wait briefly to let any in-flight key release events arrive and be
// consumed by our still-active stdin handler. This prevents Kitty
// escape sequences from leaking to the parent shell over slow SSH.
await new Promise((resolve) => setTimeout(resolve, drainMs));
}
stop(): void {
// Disable bracketed paste mode
process.stdout.write("\x1b[?2004l");
// Disable Kitty keyboard protocol (pop the flags we pushed) - only if we enabled it
// Disable Kitty keyboard protocol if not already done by prepareForExit()
if (this._kittyProtocolActive) {
process.stdout.write("\x1b[<u");
this._kittyProtocolActive = false;

View file

@ -36,6 +36,10 @@ export class VirtualTerminal implements Terminal {
this.xterm.write("\x1b[?2004h");
}
async prepareForExit(_drainMs?: number): Promise<void> {
// No-op for virtual terminal - no Kitty protocol to drain
}
stop(): void {
// Disable bracketed paste mode
this.xterm.write("\x1b[?2004l");