Skip to content

fix(mobile): resume browser screencast after transport reconnect - #14115

Closed
blade035 wants to merge 2 commits into
stablyai:mainfrom
blade035:fix/mobile-browser-screencast-freeze-on-reconnect
Closed

blade035 wants to merge 2 commits into
stablyai:mainfrom
blade035:fix/mobile-browser-screencast-freeze-on-reconnect

Conversation

@blade035

@blade035 blade035 commented Aug 12, 2026 •

Copy link
Copy Markdown
Contributor

ELI5

On mobile, the in-app browser shows the host's browser as a live picture stream. If the connection blips (network drop or relay switch) and reconnects, the picture used to freeze on the last frame — taps still worked, but the image never updated until the tab was reopened. The pane never restarted the picture stream after reconnecting. This PR makes the pane notice the reconnect and restart the stream, so the browser picture comes back to life.

What Changed

  • Adds useBrowserScreencastReconnectSignal — a hook returning a number that increments only on a reconnect (connected → away → connected), derived from the logical RPC client's existing onStateChange. It ignores the very first connect.
  • MobileBrowserPane's browser.screencast subscribe effect now depends on this signal, so on every reconnect the effect tears down and recreates the stream and resets its double-buffer render state.

One new module + one effect-dependency line. No wire/RPC/stream-opcode changes and no new dependencies.

Why

MobileBrowserPane's screencast subscribe effect depended only on the stable logical client, so it never re-ran on a relay migrateTo / direct-socket reconnect. The host tears down Page.startScreencast on reconnect and only restarts it on a fresh browser.screencast subscribe — which never came. A single missed offscreen <Image> onLoad across the blip then left pendingFrameLayerRef stuck, so every later frame painted only the hidden layer: the display froze on the last decoded frame while input RPCs kept landing. Driving the effect off a reconnect-derived signal is the minimal fix — it reuses the client's own state events and changes no transport behavior.

Linked Issue

Fixes #14274

Visual Proof

No visual change. The change recovers a pane that was already frozen; there is no new UI to screenshot. See Testing for how the behavior is verified.

Testing

  • Automated tests added/updated

  • I manually tested these changes locally

  • use-browser-screencast-reconnect-signal.test.tsx — behavioral test (react-test-renderer + mock RPC client): the signal stays 0 on the first connect and on duplicate connected states, and increments once per reconnect — including across a disconnected → connected → disconnected → connected sequence from a fresh start and across repeated reconnect cycles.

  • mobile-browser-pane-source.test.ts — source invariant that MobileBrowserPane consumes the reconnect signal in its screencast subscribe-effect dependency array.

Behavioral verification of the freeze→resume path needs a paired host and a live transport reconnect (relay migration or direct-socket drop/recover) on a real device/emulator; the unit tests pin the underlying invariant that drives it. Platforms in scope: mobile (iOS/Android). The hook is RN-agnostic and keys off the shared logical client.

Locally: pnpm typecheck and pnpm lint pass, and the screencast test files pass in isolation. The full mobile/ suite has a pre-existing, unrelated failure in src/transport/host-store.test.ts (a zod schema-resolution error in a different subsystem; this PR does not touch it). CI is the source of truth for the full run.

A bounded pending-frame watchdog (force-swap the visible layer if the offscreen onLoad is missed outside a reconnect) is intentionally deferred to a follow-up to keep this change small.

AI Disclosure

Developed with assistance from Claude (Anthropic). The root-cause analysis, approach, and tests were reviewed by the contributor.

Review

  • Cross-platform: uses only the logical client's onStateChange / getState (RN-agnostic); works for both direct-socket recovery and relay migrateTo. No platform-specific code.
  • Wire compatibility: no RPC params, stream frames, or opcodes changed; host-side emission is unchanged, so paired clients/hosts of mixed versions are unaffected.
  • Performance: no per-frame work added — the signal changes only on a reconnect.
  • UI quality: no visual change; recovers an already-frozen pane.
  • Security: no auth/transport surface changed.
  • SSH / remote / folder workspaces: the fix keys off the logical client's state transitions, which are identical across local, relay, and SSH transports.

Checklist

  • This PR is small and focused
  • I explained what changed and why (including ELI5)
  • Before/after screenshots or videos attached for UI changes, or N/A with reason
  • Self-reviewed for correctness, security, and performance
  • Cross-platform, SSH/remote, and path/shortcut impact considered (or N/A)
  • pnpm lint, pnpm typecheck, and the in-scope pnpm test pass (CI covers the full suite and pnpm build)

Author

  • X / Twitter: (optional — add your handle to be shouted out on merge)

MobileBrowserPane's browser.screencast subscribe effect never re-ran on a
relay migrateTo / direct-socket reconnect — its deps hold the stable logical
client — so the double-buffer render state was never reset. After a missed
offscreen <Image> onLoad across the blip the pane froze on the last decoded
frame while input RPCs kept landing.

Drive the subscribe effect off a reconnect signal derived from the logical
client's onStateChange so the stream is torn down, recreated, and its render
state reset on every reconnect. Adds a behavioral hook test plus a source
invariant; no wire/RPC changes.

Co-Authored-By: Claude <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 12, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 10a3e1bc-efbf-4d01-8c7b-fcaccca54fe0

📥 Commits

Reviewing files that changed from the base of the PR and between 23e2cbc and f7d47c3.

📒 Files selected for processing (1)
  • mobile/src/browser/use-browser-screencast-reconnect-signal.test.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
  • mobile/src/browser/use-browser-screencast-reconnect-signal.test.tsx

📝 Walkthrough

Walkthrough

Adds useBrowserScreencastReconnectSignal to track RPC client reconnect cycles. The hook ignores the initial connection, increments a signal on subsequent connected states, and cleans up subscriptions. MobileBrowserPane uses the signal in its screencast subscription effect dependencies. Tests cover reconnect handling, duplicate states, missing clients, cleanup, and source integration.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Title check ✅ Passed The title clearly and concisely describes resuming the mobile browser screencast after a transport reconnect.
Description check ✅ Passed The description covers the required sections, explains the fix and rationale, links issue #14274, and documents testing and compatibility considerations.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
mobile/src/browser/use-browser-screencast-reconnect-signal.test.tsx (1)

50-58: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Cover a reconnect after the first connection.

Line 50 verifies only the initial connection. Add disconnected -> connected -> disconnected -> connected and expect one increment. This verifies that the hook records the first connection before the later reconnect.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: d900f1bb-4938-4f9b-ba54-90c60377a6e9

📥 Commits

Reviewing files that changed from the base of the PR and between e4c278e and 23e2cbc.

📒 Files selected for processing (4)
  • mobile/src/browser/MobileBrowserPane.tsx
  • mobile/src/browser/mobile-browser-pane-source.test.ts
  • mobile/src/browser/use-browser-screencast-reconnect-signal.test.tsx
  • mobile/src/browser/use-browser-screencast-reconnect-signal.ts

Add the disconnected -> connected (first connect, no bump) -> disconnected -> connected (reconnect) -> repeat sequence, proving the signal records the initial connection before bumping on later reconnects and increments once per cycle. Addresses the reconnect-coverage gap flagged in review.

Co-Authored-By: Claude <noreply@anthropic.com>
@blade035

Copy link
Copy Markdown
Contributor Author

Closing this one — superseded by #16122, which reaches the actual root cause (the stuck decoder slot after reconnect) rather than the symptom-level subscription restart this PR took.

Thanks for the co-author credit — glad the report helped pin down the mechanism. Happy to test on mobile once #16122 lands.

@blade035 blade035 closed this Aug 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Mobile browser pane freezes on the last frame after a relay/direct-socket transport reconnect

4 participants