fix(desktop): recover remote terminal after laptop sleep/wake
Fixes #5130.
Problem
A desktop workspace terminal on a remote host freezes after the laptop sleeps and wakes — no output, keystrokes do nothing, and the only fix is to kill the pane and open a new one. The PTY itself keeps running in pty-daemon on the host; the break is client-side.
On suspend the terminal WebSocket goes half-open: the TCP connection is dead but readyState stays OPEN and no close event ever fires. terminal-ws-transport.ts only reconnects from its close handler (scheduleReconnect() is reached from a WS construction failure and the close handler, nowhere else), so it sat wedged on the dead pipe reporting connectionState === "open".
The web workspace terminal (apps/web/.../WebTerminal/TerminalConnection.ts) already recovers on resume via visibilitychange/resume/pageshow/online + a backoff reset in handleResume. The desktop transport never got the equivalent — and DOM resume events alone aren't enough on desktop, since app-suspend doesn't reliably fire visibilitychange/focus when the window was focused both before and after sleep. The dependable signal is a wall-clock gap.
Fix
Add a liveness/wake path to the transport, set up in connect() and torn down in disconnect() / disposeTransport():
- Wall-clock-gap watchdog — a 5s
setIntervalcomparesDate.now()between ticks; a gap > 20s means the process was suspended, so it force-recycles the (presumed half-open) socket. This catches the dead-but-OPENcase. online/focus/visibilitychangelisteners — reset backoff and reconnect a dead socket immediately (a healthy/connecting one is left alone), mirroringTerminalConnection.handleResumeon web.
reconnectNow() drops the dead socket without waiting for a close and re-dials. Re-attach is cheap: the host keeps the PTY alive, so reconnecting just re-subscribes and the protocol replays anything missed. The existing transport.socket !== socket guard means a late OS-delivered close on the old socket is harmless.
Testing
- Folded the issue's sleep/wake repro into
terminal-ws-transport.test.ts. It drives the real watchdogsetInterval+Date.now()-gap path: fails onmain(Expected: 2, Received: 1— only one socket ever created) and passes with the fix. bun test(3 pass),bun run lint(clean),bun run typecheck(pass).
Manual verification (real half-open socket) is a lid-close test: open a remote-host terminal running top, close the lid ≥1 min, reopen → recovers within ~5s.
Summary by cubic
Recover remote desktop terminals after laptop sleep/wake by detecting suspension and force-reconnecting the WebSocket. Fixes frozen panes caused by half-open sockets where output and keystrokes stopped.
- Bug Fixes
- Added a wall-clock watchdog (checks every 5s); a >20s gap triggers a socket recycle.
- Added resume listeners (
online,focus,visibilitychange) to reset backoff and reconnect only if the socket is dead. - Introduced
reconnectNow()and wired liveness setup/teardown inconnect()anddisconnect()/disposeTransport(); added a test that simulates half-open recovery.
Summary by CodeRabbit
Release Notes
-
Bug Fixes
- Improved terminal websocket resilience when machines wake from sleep or experience network changes. The transport now detects stale connections and automatically establishes a fresh websocket to restore terminal connectivity.
-
Tests
- Added test cases validating proper websocket recovery following machine sleep and network resume events.