diff --git a/web/src/components/structured/useStructuredSession.ts b/web/src/components/structured/useStructuredSession.ts index 232de22..bba2ced 100644 --- a/web/src/components/structured/useStructuredSession.ts +++ b/web/src/components/structured/useStructuredSession.ts @@ -196,12 +196,24 @@ export function useStructuredSession( connect(); }; + // Mobile browsers suspend timers and silently kill WebSockets in the + // background. Kick a fresh connect when the page becomes visible again. + const onVisibility = () => { + if (document.visibilityState !== "visible" || disposed) return; + if (socket && (socket.readyState === WebSocket.OPEN || socket.readyState === WebSocket.CONNECTING)) return; + cancelTimer(); + setStatus("reconnecting"); + connect(); + }; + document.addEventListener("visibilitychange", onVisibility); + setStatus("connecting"); connect(); return () => { disposed = true; reconnectRef.current = () => {}; + document.removeEventListener("visibilitychange", onVisibility); cancelTimer(); closeSocket(); }; diff --git a/web/src/components/terminal/TerminalPane.tsx b/web/src/components/terminal/TerminalPane.tsx index c6bfc5f..da03014 100644 --- a/web/src/components/terminal/TerminalPane.tsx +++ b/web/src/components/terminal/TerminalPane.tsx @@ -302,6 +302,24 @@ export const TerminalPane = forwardRef( ta?.addEventListener("focus", onTaFocus); ta?.addEventListener("blur", onTaBlur); + // Mobile browsers (Android especially) suspend timers and silently kill + // WebSockets when the app goes to background. The backoff timer set by + // onclose is frozen too, so recovery stalls until visibility returns. Kick + // a fresh connect immediately when the page becomes visible again. + const onVisibility = () => { + if (document.visibilityState !== "visible" || disposed) return; + if (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) return; + // Cancel any pending backoff — we're reconnecting right now. + if (reconnectTimer !== null) { + window.clearTimeout(reconnectTimer); + reconnectTimer = null; + } + reconnectMs = INITIAL_RECONNECT_MS; + onStatusChange?.("reconnecting"); + connect(); + }; + document.addEventListener("visibilitychange", onVisibility); + window.addEventListener("resize", onResize); const visual = window.visualViewport ?? null; visual?.addEventListener("resize", onResize); @@ -326,6 +344,7 @@ export const TerminalPane = forwardRef( reconnectTimer = null; } ro.disconnect(); + document.removeEventListener("visibilitychange", onVisibility); window.removeEventListener("resize", onResize); visual?.removeEventListener("resize", onResize); ta?.removeEventListener("focus", onTaFocus);