From f09f618f3b096892961bf7ec00bfc201a37a834d Mon Sep 17 00:00:00 2001 From: Jinwoo-H Date: Mon, 7 Sep 2026 15:08:45 -0400 Subject: [PATCH] refactor(mobile-web): put the transport connection state on the wire The shell collapsed six transport states into four, and every page route expanded them back. Both mappings are gone; the page reads the state the transport reported. Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb --- mobile/app/hybrid.tsx | 5 ++--- mobile/host-web-app/h/[hostId]/accounts.tsx | 8 +------- .../h/[hostId]/files/[worktreeId].tsx | 8 +------- .../h/[hostId]/files/preview/[worktreeId].tsx | 8 +------- .../h/[hostId]/review/[worktreeId].tsx | 8 +------- .../h/[hostId]/session/[worktreeId].tsx | 8 +------- .../h/[hostId]/source-control/[worktreeId].tsx | 8 +------- mobile/host-web-app/h/[hostId]/tasks.tsx | 8 +------- mobile/host-web-app/index.tsx | 8 +------- .../mobile-web-bridge-connection-state.ts | 16 ---------------- .../mobile-web/mobile-web-capability-broker.ts | 3 ++- .../mobile-web/mobile-web-shell-init-message.ts | 3 +-- src/mobile-web/src/native-shell-channel.ts | 3 ++- src/shared/mobile-web/bridge-contract.test.ts | 2 +- src/shared/mobile-web/bridge-contract.ts | 11 ++++++++++- 15 files changed, 26 insertions(+), 81 deletions(-) delete mode 100644 mobile/src/mobile-web/mobile-web-bridge-connection-state.ts diff --git a/mobile/app/hybrid.tsx b/mobile/app/hybrid.tsx index 27c6dcb2ff0..ac499326ab2 100644 --- a/mobile/app/hybrid.tsx +++ b/mobile/app/hybrid.tsx @@ -22,7 +22,6 @@ import { MobileWebHybridShellPresentation } from '../src/mobile-web/MobileWebHyb import { mobileWebShellLoadFailureWarning } from '../src/mobile-web/mobile-web-shell-load-failure-warning' import { useMobileWebNavigationIntentHandoff } from '../src/mobile-web/use-mobile-web-navigation-intent-handoff' import { useMobileWebColdResumeRoute } from '../src/mobile-web/use-mobile-web-cold-resume-route' -import { mobileWebBridgeConnectionState } from '../src/mobile-web/mobile-web-bridge-connection-state' import { MobileWebOneShotResponseDrop } from '../src/mobile-web/mobile-web-one-shot-response-drop' import { useMobileWebE2eHostSelection } from '../src/mobile-web/mobile-web-e2e-host-selection' import { useMobileWebAppForegroundAuthority } from '../src/mobile-web/use-mobile-web-app-foreground-authority' @@ -266,7 +265,7 @@ export default function HybridScreen() { }, [postInit]) useEffect(() => { - brokerRef.current?.updateConnectionState(mobileWebBridgeConnectionState(state)) + brokerRef.current?.updateConnectionState(state) const current = session if (!current || pageDocument.initializedSessionRef.current !== current.sessionId) { return @@ -276,7 +275,7 @@ export default function HybridScreen() { type: 'connection', shellSessionId: current.sessionId, buildId: current.buildId, - state: mobileWebBridgeConnectionState(state), + state, reconnectAttempts: reconnects, lastConnectedAt: lastConnected }) diff --git a/mobile/host-web-app/h/[hostId]/accounts.tsx b/mobile/host-web-app/h/[hostId]/accounts.tsx index e539c86395b..201db4a8058 100644 --- a/mobile/host-web-app/h/[hostId]/accounts.tsx +++ b/mobile/host-web-app/h/[hostId]/accounts.tsx @@ -14,18 +14,12 @@ export default function HostMobileWebAccountsRoute() { : undefined, [shell.client, shell.hostDisplayName] ) - const connectionState = - shell.connection === 'offline' - ? 'disconnected' - : shell.connection === 'recovering' - ? 'reconnecting' - : shell.connection return ( ) diff --git a/mobile/host-web-app/h/[hostId]/files/[worktreeId].tsx b/mobile/host-web-app/h/[hostId]/files/[worktreeId].tsx index df3fd039e15..b75f361e723 100644 --- a/mobile/host-web-app/h/[hostId]/files/[worktreeId].tsx +++ b/mobile/host-web-app/h/[hostId]/files/[worktreeId].tsx @@ -9,17 +9,11 @@ export default function HostMobileWebFileExplorerRoute() { () => (shell.client ? webHostFileExplorerOperations(shell.client) : undefined), [shell.client] ) - const connectionState = - shell.connection === 'offline' - ? 'disconnected' - : shell.connection === 'recovering' - ? 'reconnecting' - : shell.connection return ( ) diff --git a/mobile/host-web-app/h/[hostId]/files/preview/[worktreeId].tsx b/mobile/host-web-app/h/[hostId]/files/preview/[worktreeId].tsx index b52bbde909e..438a53d9782 100644 --- a/mobile/host-web-app/h/[hostId]/files/preview/[worktreeId].tsx +++ b/mobile/host-web-app/h/[hostId]/files/preview/[worktreeId].tsx @@ -9,17 +9,11 @@ export default function HostMobileWebFilePreviewRoute() { () => (shell.client ? webHostFilePreviewOperations(shell.client) : undefined), [shell.client] ) - const connectionState = - shell.connection === 'offline' - ? 'disconnected' - : shell.connection === 'recovering' - ? 'reconnecting' - : shell.connection return ( ) diff --git a/mobile/host-web-app/h/[hostId]/review/[worktreeId].tsx b/mobile/host-web-app/h/[hostId]/review/[worktreeId].tsx index d1e57a20559..ba4fa52058e 100644 --- a/mobile/host-web-app/h/[hostId]/review/[worktreeId].tsx +++ b/mobile/host-web-app/h/[hostId]/review/[worktreeId].tsx @@ -16,19 +16,13 @@ export default function HostMobileWebDiffReviewRoute() { () => (shell.client && workspaceId ? webHostDiffReviewClient(shell.client, workspaceId) : null), [shell.client, workspaceId] ) - const connectionState = - shell.connection === 'offline' - ? 'disconnected' - : shell.connection === 'recovering' - ? 'reconnecting' - : shell.connection return ( { await shell.client?.navigationReconnect() }, diff --git a/mobile/host-web-app/h/[hostId]/session/[worktreeId].tsx b/mobile/host-web-app/h/[hostId]/session/[worktreeId].tsx index 2df9052100d..ac460129e36 100644 --- a/mobile/host-web-app/h/[hostId]/session/[worktreeId].tsx +++ b/mobile/host-web-app/h/[hostId]/session/[worktreeId].tsx @@ -83,12 +83,6 @@ export default function HostMobileWebSessionRoute() { () => (shell.client ? webHostSessionChatPendingDeliveryOperations(shell.client) : undefined), [shell.client] ) - const connectionState = - shell.connection === 'offline' - ? 'disconnected' - : shell.connection === 'recovering' - ? 'reconnecting' - : shell.connection return ( shell.client diff --git a/mobile/host-web-app/h/[hostId]/source-control/[worktreeId].tsx b/mobile/host-web-app/h/[hostId]/source-control/[worktreeId].tsx index eeecd745a73..a92ec8416c8 100644 --- a/mobile/host-web-app/h/[hostId]/source-control/[worktreeId].tsx +++ b/mobile/host-web-app/h/[hostId]/source-control/[worktreeId].tsx @@ -17,19 +17,13 @@ export default function HostMobileWebSourceControlRoute() { shell.client && workspaceId ? webHostSourceControlClient(shell.client, workspaceId) : null, [shell.client, workspaceId] ) - const connectionState = - shell.connection === 'offline' - ? 'disconnected' - : shell.connection === 'recovering' - ? 'reconnecting' - : shell.connection return ( { await shell.client?.navigationReconnect() }, diff --git a/mobile/host-web-app/h/[hostId]/tasks.tsx b/mobile/host-web-app/h/[hostId]/tasks.tsx index 58974df04c2..2c069af31fe 100644 --- a/mobile/host-web-app/h/[hostId]/tasks.tsx +++ b/mobile/host-web-app/h/[hostId]/tasks.tsx @@ -41,12 +41,6 @@ export default function HostMobileWebTasksRoute() { workspaceCreation: webHostWorkspaceCreationOperations(shell.client) } }, [shell.client]) - const connectionState = - shell.connection === 'offline' - ? 'disconnected' - : shell.connection === 'recovering' - ? 'reconnecting' - : shell.connection return ( { type: 'connection', shellSessionId: SHELL_SESSION_ID, buildId: BUILD_ID, - state: 'recovering' + state: 'reconnecting' } expect( diff --git a/src/shared/mobile-web/bridge-contract.ts b/src/shared/mobile-web/bridge-contract.ts index 76133e1b5ff..c53dc43d305 100644 --- a/src/shared/mobile-web/bridge-contract.ts +++ b/src/shared/mobile-web/bridge-contract.ts @@ -165,7 +165,16 @@ const ShellEnvelopeSchema = z.object({ buildId: BuildIdSchema }) -const ConnectionStateSchema = z.enum(['connecting', 'connected', 'offline', 'recovering']) +// The shell publishes the transport's own state; the page decides what to show for each. +const ConnectionStateSchema = z.enum([ + 'connecting', + 'handshaking', + 'connected', + 'disconnected', + 'reconnecting', + 'auth-failed' +]) +export type MobileWebConnectionState = z.infer const ConnectionMetricsShape = { reconnectAttempts: z.number().int().nonnegative().max(1_000_000).optional(), lastConnectedAt: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER).nullable().optional()