feat(desktop): surface a host unreachable screen for v2 workspaces
Summary
A v2 workspace whose host stops answering used to degrade into failing panes or a blank frame — the durable-object/relay connection dropping was never surfaced. This adds a takeover screen wired to the real connection.
packages/workspace-client— the shared per-host event-bus socket now tracksconnecting | open | reconnecting | closedplus the last_whoownsprobe, exposed asgetConnectionStatus/subscribeConnectionStatus/reconnect().useHostReachability— reads that status, retains the bus so it keeps dialling, and reports unreachable only after 10s of sustained loss. Keyed off the real data path rather than the cloudisOnlineflag, whose drift got this gate reverted twice before (#4430, #4727).WorkspaceHostUnreachableState— revives the orphanedWorkspaceHostOfflineStateleft on disk by #4727, with a live Reconnecting/Disconnected chip, a reason line derived from the relay probe (503 host not connected, 401/403 unauthorized, 502/504, routing, relay unreachable), and a Retry button.WorkspaceHostGate— overlays the screen instead of replacing the workspace, so panes stay mounted and terminal scrollback, unsaved editor documents and in-flight agent sessions survive a drop.WorkspaceLocalHostPendingState— replaces the blank<div>the provider rendered when the local host service has no port, with copy perhostServiceStatusand a button that restarts the service through the same coordinator call the tray menu uses.
Two fixes came out of driving the running app over CDP:
- Blank frame on a host-service restart. Losing the port set
activeHostUrlto null, which unmounted the whole workspace and left ~5s of empty pane before the service returned. The provider now holds the last known local host URL through a port gap. Measured: 45s across a kill and respawn, zero blank frames. - Recovery lagged 20.7s behind the host coming back, riding partysocket's backoff toward its 30s ceiling. The hook now re-dials every 5s while the screen is visible → 5.7s.
Test plan
-
bun run typecheck(desktop) andbiome checkclean;packages/workspace-clienttests pass (13/13) - Normal opens and route changes never flash the screen — 189/181/286/308 samples at 100ms, zero visible frames, no console errors
- Sustained local outage: screen appears 14.1s in (≈10s after the socket closed), correct copy, one visibility transition, no flicker
- Recovery: clears 5.7s after the host is listenable again
- Host-service crash: workspace stays mounted, no blank frame
- Remote/relay path unverified — the dev stack runs no relay and every other device has zero workspaces, so the six probe-status variants are only verified as rendered components, not end to end
Screens
Captured from the running app over CDP — real host name, real host ID, not mocked.
The takeover. Amber Reconnecting chip while the socket keeps dialling, Retry and Host settings. Note the right sidebar stays reachable outside the overlay, which is why there is no "browse workspaces" escape link.
Before — the blank frame this PR also fixes. A host-service restart nulled the host URL, which unmounted the whole workspace and left ~5s of empty pane with no explanation. Measured 7.5s–12.7s on a plain crash.
After the host returns. The screen lifts on its own — no reload, no navigation — 5.7s after the host is listenable again. (This workspace had no panes open, so it lands on the empty state.)
Summary by cubic
Surfaces a Host unreachable overlay for v2 workspaces to prevent blank frames and failing panes. Previously we unmounted or left panes failing; now after 10s of sustained loss we cover the workspace, keep panes mounted, and redial every 5s to cut recovery lag.
packages/workspace-client: per-host event bus now tracksconnecting | open | reconnecting | closedplus last relay_whoownsprobe; addsgetConnectionStatus,subscribeConnectionStatus, andreconnect(); exportsHostConnectionStateandHostConnectionStatus. No migration required.useHostReachability: retains the bus, declares unreachable after 10s, runs a 5s redial loop only while the overlay is visible, and maps probe status to user-facing detail.WorkspaceHostGate: overlays instead of unmounting so terminals, unsaved files, and agent sessions survive; marks covered panesinertto stop focus behind the overlay; resolves host names via the cloud list; wrapper stays mounted to avoid remount on reconnect.WorkspaceHostUnreachableState: shows a Reconnecting/Disconnected chip, probe-derived reason, a Retry button wired toreconnect(), and a link to host settings.WorkspaceLocalHostPendingState: status-specific copy before the local host service exposes a port; can restart the service via the coordinator; never prompts restart while starting/running; shows a toast if restart is clicked without an active org.WorkspaceProvider: caches the last local host URL across host-service restarts to avoid blank frames; shows the pending state only before any port has ever been seen; keysWorkspaceTrpcProvideron the workspace (not the host URL) so a port change swaps transport without remounting panes.
Review focus
- Event-bus status transitions, probe wiring, listener lifecycles, and
retain/subscribeConnectionStatuscleanup. - Redial loop: runs only while the overlay is visible and doesn’t fight partysocket backoff; manual
reconnect()sets state correctly. - Overlay behavior: no flicker, children never remount, and covered panes are truly inert.
- Local-host pending path: restart behavior, no restart prompt while starting/running, and toast on missing org.
Summary by CodeRabbit
- New Features
- Added clearer workspace host connection states, including connecting, reconnecting, unreachable, and disconnected.
- Added automatic reconnection attempts and a manual retry option.
- Added connection-specific messaging for relay and local host failures.
- Preserved workspace content while displaying host connectivity status.
- Added pending-state feedback when a local host is starting or restarting.
- Improved handling of temporary local host service restarts.
- Kept workspace content mounted and protected it from interaction during host outages.