Fix terminals staying black after mobile PWA resumes from background - #53
Merged
Conversation
Contributor
There was a problem hiding this comment.
🟢 Approval recommended
The changes directly address the described root cause, include targeted regression tests for the generation/teardown edge case, and keep recovery behavior rate-limited and explicit.
Pull request overview
Fixes a mobile PWA resume failure mode where the bridge WebSocket can die silently while the page is frozen, leading to generation desync and terminals staying black until a manual refresh. The PR hardens the bridge connection lifecycle and adds a terminal-specific recovery path (including a rate-limited reload as a last resort) so terminals recover automatically after foregrounding.
Changes:
- Harden bridge reconnection logic: make disconnect handling idempotent and add an explicit foreground probe to detect silently-dead sockets immediately.
- Add terminal recovery utilities + tests to decide when a post-resume reload is allowed and rate-limited.
- Add TerminalView resume handling: repaint xterm, re-arm stuck attaches on foreground, and surface an explicit attach error before falling back to a rate-limited reload.
File summaries
| File | Description |
|---|---|
| web/src/terminalRecovery.ts | Adds reusable helpers/constants for arming post-resume recovery and rate-limiting reloads via storage. |
| web/src/terminalRecovery.test.ts | Unit tests for the new terminal recovery decision logic and storage handling. |
| web/src/store.ts | Probes the bridge immediately on foreground to kick off reconnect/reattach without waiting for the next heartbeat tick. |
| web/src/components/TerminalView.tsx | Adds foreground/visibility recovery: repaint xterm, re-arm attach when stuck, and perform rate-limited reload on repeated no-frame attaches. |
| web/src/api.ts | Makes heartbeat probe behavior safer and adds probeConnectionNow; makes handleDisconnect idempotent after full teardown. |
| web/src/api.test.ts | Regression tests for probe/heartbeat/teardown generation behavior and silent-close detection. |
| CHANGELOG.md | Documents the mobile PWA terminal resume fix under Unreleased. |
Review details
- Files reviewed: 7/7 changed files
- Comments generated: 0
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Fixes terminals staying black after the mobile PWA returns from the lock screen or switches back from another app, requiring a manual page refresh to recover.
Root cause: while the OS freezes the page, it can kill the bridge WebSocket without delivering a close event. On resume, the overdue heartbeat fires on the zombie socket, the delayed close event tears the connection down (advancing the client generation, which status listeners capture), and the rejected heartbeat ping then triggers a second teardown that advances the generation again without a status transition. Scoped clients keep the stale generation forever: reconnect and global RPCs succeed, but terminal attach, metadata refresh, and terminal input are silently discarded as stale.
Changes:
handleDisconnectidempotent once the socket is fully torn down, and only let the heartbeat probe force-reconnect a live connection, so the client generation cannot advance twice without a status transition.Verification
bun run format:check && bun run lint && bun run test— 716 tests pass, including new regressions covering the frozen-resume generation desync.cd web && bun run typecheck && bun run build;cd server && bun run typecheck.