Skip to content

fix(mobile): keep unmounted desktop browser tabs visible and closable - #31665

Merged
AmoabaKelvin merged 14 commits into
mainfrom
nwparker/mobile-b4-browser-tabs
Oct 28, 2026
Merged

AmoabaKelvin merged 14 commits into
mainfrom
nwparker/mobile-b4-browser-tabs

Conversation

@nwparker

@nwparker nwparker commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 18 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​828 $\color{#cf222e}{\Huge{\mathbf{−}}}$​28 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​800
Prod 12 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​222 $\color{#cf222e}{\Huge{\mathbf{−}}}$​34 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​188

ELI5

Your phone should remember a desktop browser tab even when that workspace is not on screen. Keep those tabs in the mobile list and send Close to the desktop that owns them, so closing one from your phone stays closed when you return to the workspace.

What Changed

  • Keep renderer-owned browser rows and group membership while their renderer view is unmounted. Continue requiring live pages for runtime-owned and client-hosted rows.
  • Use the accepted renderer publication's tab identities to distinguish ownership. Runtime merges and closes preserve the renderer publisher epoch; snapshot versions order content changes.
  • Route renderer-owned Close through the desktop session tab path. Close runtime-owned pages through the offscreen backend. On renderer detach, retain only live runtime pages and rebase the publication to headless ownership.
  • Keep compatibility with legacy merge-suffix epochs and unknown renderer epochs; no RPC schema change. The same runtime paths serve folder workspaces, git worktrees, and remote clients.

Carried onto current main by Kelvin (on top of Neil's commits):

  • Renderer browser rows stop counting as renderer-owned once the desktop window closes, so their webviews are no longer assumed alive.
  • In serve mode, the bridge also lists an attached desktop window's webviews. The runtime now lists only pages the offscreen backend created (BrowserBackend.ownsTab), so a desktop webview's page is listed once, under its renderer row.
  • Serve-mode browser refresh keeps the snapshot's publication epoch, as main does; only an offscreen close re-stamps a headless epoch. Minting a new epoch on every page change reset the phone's version floor and made session.tabs.closeLifecycle calls fail as stale-publication.
  • When the desktop window closes, the runtime republishes (with a version bump) every worktree snapshot that has browser rows, so subscribed phones and paired desktop/web clients drop renderer rows they can no longer open or close.
  • When the renderer stops publishing a worktree, the retained snapshot moves to a headless epoch only if it keeps a non-client browser row. Terminal-only snapshots keep the renderer epoch and the existing live/durable checks, so an exited runtime terminal is still pruned.
  • Serve-mode browser refresh keeps the existing row order, lets a newly built row replace a retained row with the same id (no duplicate rows), and only waits for an accepted renderer publication while a window is attached.
  • Test doubles updated for current main (BrowserBackend.ownsTab, notifier fields), plus a one-line delegate in prune-mobile-session-tab-group-layout.ts to stay under the 300-line lint cap.

Why

Create a browser tab in folder workspace A, switch desktop to workspace B, then reload the desktop renderer so A has no mounted webview. Before this change, the actual iOS app reports 0 tabs for A even though the saved desktop browser record and unified tab remain. Runtime projection and reconciliation incorrectly treated absence from the live page registry as proof that every browser tab had closed.

The fix sits at the host's tab projection, which owns the decision of which browser rows exist. Mobile code is unchanged, so old phones just see rows they used to lose.

Linked Issue

Fixes #14598. Adapts the community fix in #14792 by @bbingz (Bing.Z), with the original Bing.Z and Cursor co-author credits preserved.

Visual Proof

Before: the saved desktop browser disappears from the actual phone list after its desktop renderer is unmounted.

Before: native iOS workspace incorrectly shows zero tabs

After: the same workspace retains Example Domain with no mounted desktop webview.

After: native iOS retains the renderer-owned browser tab

The phone's real action menu can close it.

Native browser tab action menu

After Close, the desktop workspace remounts without restoring the browser.

Desktop workspace remounted after native Close

Testing

  • Reproduced on the original code: focused regression run 2 failed, 7 passed; actual native iOS screen showed 0 tabs while desktop persistence still held Example Domain and the desktop had zero webviews.
  • Fixed runtime and adjacent browser/session suites: 1,495 passed, 2 skipped across 9 files, including the 1,465-test runtime aggregate. macOS fixtures require canonical TMPDIR=/private/tmp; the initial run's two unrelated failures compared /var against /private/var.
  • Added absent-page coverage to the legacy missing-accepted-identity detach case; its final focused run passed.
  • Node typecheck, changed-file oxlint, formatting, and git diff --check passed.
  • Actual iOS Simulator app paired to an isolated desktop over real E2EE RPC. The fixed row survives desktop renderer reload with zero mounted webviews. Long-press → Close in the phone's real tab menu removes both browser and unified desktop records. Remounting desktop workspace A does not resurrect the browser.
  • Independent peer and root source reviews found no blocker.
  • Follow-up risk reduction: 56 passed across six lifecycle/backend/client-host files. Five new tests drive the real runtime through an offscreen close completing after a newer renderer epoch, stale close requests and stale renderer resends, detach/rebase/return of the same renderer publication, dead client pages with and without an offscreen backend, and a renderer close that rejects before selection tombstoning. Adjacent suites cover real offscreen backend asynchronous retirement/replacement and client-host reconnect/restored-page/ghost-row cleanup. No production defect was found in these probes.
  • Final head 49178f1e1b7 passes all applicable GitHub checks, including the aggregate gate, Node 24 unit shards, relay integration, static/type checks, Linux/Windows packaging, WSL distro checks and mode-switch layout oracle.

Native proof fixture: the available unsigned simulator binary lacks Keychain entitlement, so isolated pairing credential storage used a temporary AsyncStorage adapter. The actual app, built mobile web bundle, transport, runtime, and tab actions were exercised. The adapter and launch configuration are absent from this PR. Desktop renderer reload detaches the active browser screencast, so the after image also shows that streaming warning. That native run did not verify streaming recovery; subsequent transport and mounted-pane checks are described below. Android, Windows, Linux, and a separate remote host were not manually exercised.

Follow-up recovery and compatibility checks on unchanged head 49178f1e1b7:

  • A phone-scoped E2EE client with no advertised capabilities retained the browser row at zero webviews, closed it through session.tabs.close, and observed no browser row or webview after workspace remount. This exercises the existing capability/optional-field path, not a released old binary.
  • Six consecutive real E2EE renderer reload/re-entry cycles received a new binary frame in 503–612 ms after zero-webview reload. These probes stopped at the first frame, and the saved frames are blank, so the timing establishes stream delivery rather than visible page recovery. Earlier verification retained one Page.enable timeout; it did not reproduce in these six cycles or the preceding two successful exact-head repeats. No source change was made to mask that intermittent result.
  • The production MobileBrowserPane, streaming hook, and DirectRpcClient were mounted against the hidden desktop over real E2EE transport. Renderer reload produced the detach warning; the existing app foreground transition created exactly one replacement subscription, received fresh pixels, and cleared the warning. The same subscription and warning assertions pass against the baseline runtime. The saved head and baseline mounted-pane JPEGs contain identical page content (confirmed by SHA256 and decoded PNG inspection). Native component rendering and the AppState event were mocked in this check, so it is not native OS lifecycle proof.
  • Existing mobile app-resume, frameless stream, frame-layer, and stream-registry suites passed 53/53. The additional mounted-pane transport probe passed on both the PR head and baseline. The browser stream, mobile transport, and host screencast files have no source diff from baseline, and no reproducible branch-induced streaming defect justified adding automatic retries.

Further bounded checks on the unchanged head:

  • Three additional real E2EE reload/re-entry cycles now wait for decoded visible content instead of stopping at the first binary frame. All three saved 390×700 frames visibly show Example Domain after zero-webview reload, while the other desktop workspace remains selected. Content arrived in 2.28–2.47 seconds; this is probe timing, not a performance comparison.
  • The mounted production-pane probe now also waits for decoded visible content after mocked foreground. It passes on head and baseline, creates exactly one replacement subscription, and clears the detach warning; saved recovered frames are byte-identical. Native host components and AppState remain mocked.
  • Eight temporary production-runtime probes plus the four adjacent ownership/reconciliation suites pass 27/27. New probes cover a pending renderer close acknowledged after a newer same-ID publication, an offscreen close completing after reconnect with a reused row ID/different page, folder workspace variants, single-key accepted identities and legacy epoch suffix, and newer accepted renderer state retaining identity over stale persistence.
  • An exploratory metadata assertion found that the shared projection can overlay stale saved title/URL from serveBrowserTabs.listUnstarted onto a newer renderer row. The baseline reproduces this too (and also replaces the identity). The PR preserves the newer identity/group; stale saved metadata is an existing neighboring limitation, documented for follow-up without expanding this ownership fix.

Native OS re-entry remains unverified in this round: CoreSimulator create and boot calls stalled, including an isolated device set. Only owned pending calls were cancelled; existing visible simulators were left untouched. All desktop launches stayed hidden with ORCA_BACKGROUND_LAUNCH=1. Android, Windows/Linux app launches, and a separate physical remote host remain outside the manual coverage. Final-head CI still reports 27 successful checks, with 24 skipped.

Re-verified on rebased head afb8f0a28bf (Kelvin, 2026-10-28). Later commits only change serve-mode refresh and the window-close republish, neither of which this desktop flow reaches. Setup: hidden dev desktop built from this branch, with folder workspaces A and B and the released Android app (OTA shell) paired over E2EE.

  • Opened Example Domain in folder A, switched the desktop to B and reloaded the renderer, leaving zero webviews. The phone listed 2 tabs for A, including Example Domain.

  • Reloaded the desktop renderer again (0 webviews) and tapped Close in the phone's tab menu. The phone and the desktop's unified tabs for A both dropped to the terminal only.

  • Switched the desktop back to A. No browser tab came back, and the phone showed 1 tab.

  • Restarted the dev desktop on the same profile (on head e15b4e09b2a). Folder A came back with only its terminal, so the close from the phone persisted.

  • The iOS simulator could not be used this round: CoreSimulator is wedged on this machine, with every fleet simulator stuck in "Shutting Down".

  • Focused runtime/browser/session-tab and window-lifecycle suites: 1,831 passed, 3 skipped, 1 expected fail across 39 files. Four regression tests each fail on the previous head and pass now: serve epoch kept, window close pushes a newer-version drop, colliding ids listed once, and an omitted worktree's exited terminal pruned. Changed-file oxlint and oxfmt pass.

  • I manually tested these changes locally

  • Automated tests added/updated, or explained why not below

AI Disclosure

Review

review-loop (Codex + Claude), four rounds on the rebased branch:

  • Round 1: no P0/P1. Fixed the P2 on serve-mode epoch churn.
  • Round 2: no P0/P1. Fixed P2s on window close not reaching subscribed phones and on duplicate rows for colliding ids, plus the P3s on row order and the windowless guard.
  • Round 3: fixed a P1 (the window-close republish kept the version, so desktop/web mirrors dropped it) and a terminal liveness regression from the detach re-stamp.
  • Round 4 (final, cap reached): no P0/P1. The P2s and P3s left over are listed under Notes as follow-ups. Renamed a test file that was named after a review round.

Agent skill upstream boundary

  • Not applicable, or this change copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

Medium-low, reduced further by a reconnect fix. A delayed offscreen close across renderer detach/reconnect exposed a real group-projection bug: the surviving browser row remained in tabs but lost its group order entry when the renderer returned. The preserved-tab merge now reuses appendBrowserTabOrder and collectBrowserGroupAssignment for preserved browser rows. Incoming renderer rows and their ordering remain authoritative. The regression exercises production runtime close, detach, return, snapshot versions and group membership; no replacement page or newer renderer row is lost.

Validation: 1,482 runtime/lifecycle/ownership/reconciliation/epoch tests pass, two skipped, with TMPDIR=/private/tmp on macOS. The first broader run's two failures were /var versus /private/var assertions in unrelated repository setup tests; canonical TMPDIR resolves both. Node typecheck passes across 17,338 files; changed-code quality and diff checks pass. Final-head GitHub CI passes after this production follow-up and the integration cleanup below.

Earlier native iOS/macOS folder-workspace close/remount proof still applies to the unchanged close-routing behavior. The new group repair is covered by runtime integration tests, not a fresh native recording. Browser ownership spans projection, close, client placement and renderer generations, so broad platform/streaming-recovery coverage remains incomplete. No merge performed.

CI integration cleanup: current main pushed BrowserAddressBar and ClientHostedBrowserPagePane over the 400-line lint limit. Formatting-only JSX prop grouping clears both errors; the clean combined merge tree passes oxlint and formatting. No browser behavior changes in these follow-ups. Two retired Portuguese browser-zoom catalog entries were also removed to align with current main; full catalog validation passes. Final head 49178f1e1b7 passes all applicable CI checks, including the aggregate gate.

CI rerun provenance: the first full run had one failure in the unchanged structured-session option-retry test. Its focused file passed locally (19 checks), the exact case passed three repeated runs, and the full failed shard passed on rerun without source changes. The initial failure and successful rerun remain visible in Actions. No native-chat option code or tests were changed in this PR.

Follow-ups, not in this PR. Each is an edge case that main never reached because main hid every unmounted row:

  • Serve mode with a desktop window attached. While the host asks the renderer to resend, runtime browser rows with unknown ownership count as renderer-owned. The gap is short and fixes itself.
  • Serve mode with a desktop window attached. A detached snapshot that keeps both an offscreen page and a CLI terminal moves to a headless epoch, which loosens the terminal's liveness check until the renderer publishes the worktree again.
  • Serve mode with a desktop window attached. When a publication omission leaves only client-hosted browser rows, offscreen reconcile waits for a renderer publication, so retiring that client page doesn't advance the version for desktop/web mirrors.
  • After a renderer crash whose recovery gives up, the window still exists, so its browser rows stay listed until the window closes or reloads.
  • The browser-row visibility rule is written twice: in the projection and in collectPublicMobileSessionTabIds.

Ensure no issues in: Security, Cross-platform support (Linux, Windows, Mac), Remote SSH, Mobile, general backwards compatibility, performance

Checklist

  • This PR is small and focused
  • I explained what changed and why (ELI5, the user-facing before/after, the mechanism, and why over the alternatives)
  • 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, pnpm test, and pnpm build pass (or CI will cover; local preferred)

@coderabbitai

coderabbitai Bot commented Oct 21, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review.

Recent review info
Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: ffd46bd5-fca7-4fb1-b74a-3a9d25d61689
Commits

Reviewing files that changed from the base of the PR and between 7ee76a1 and ef129c8.

Files selected for processing: 5
  • src/main/runtime/orca-runtime-tests/terminal-handles-and-agent-status-part-03.spec.ts
  • src/main/runtime/renderer-browser-lifecycle.test.ts
  • src/main/runtime/renderer-browser-ownership.test.ts
  • src/main/runtime/rpc/methods/session-tabs-move-validation.test.ts
  • src/main/runtime/session-tabs/mobile-session-publication-preservation.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 2 remain after this review.

Walkthrough

The changes classify mobile browser tabs by renderer ownership, headless ownership, and publication epoch. Reconciliation preserves renderer-owned tabs and updates headless-owned tabs. Projection and public tab collection retain renderer-owned tabs without a live browser page, while headless snapshots continue to filter tabs without live pages. Tests cover reconciliation, closure, projection, publication epochs, and tab reordering.

Priority: Normal

Severity of issue fixed: Medium

Merge Risk: Moderate · previous review ef129

Legacy browser tabs can remain incorrectly owned, and preserved tabs can reappear in the wrong group or without one; these compatibility issues should be fixed before merging.

Pre-merge checks: Passed 4
Check name Status Explanation
Title check Passed The title "fix(mobile): keep unmounted desktop browser tabs visible and closable" matches the main change: renderer-owned desktop browser tabs stay listed in the mobile session when their workspace ha…
Linked Issues check Passed Requirement [#14598]: a renderer-published browser tab must stay in the phone tab list when its workspace has no mounted webview. The live browser bridge must not gate renderer-published rows. Headles…
Out of Scope Changes check Passed The changes support [#14598]. Each file in the change list traces to renderer-owned browser row handling:
- Projection, public ID collection, and ownership predicates (runtime-mobile-session-projectio…
Description check Passed The description is thorough and follows the template. It has an ELI5, a What Changed section, a Why section, a linked issue (Fixes #14598), visual proof with before and after images, extensive testing…
Full details: Title check

Explanation

The title "fix(mobile): keep unmounted desktop browser tabs visible and closable" matches the main change: renderer-owned desktop browser tabs stay listed in the mobile session when their workspace has no mounted webview, and Close routes through the desktop path. It is specific and follows the conventional-commit style. The title does not mention the epoch or ownership mechanics, but it does not need to cover every detail.

Full details: Linked Issues check

Explanation

Requirement [#14598]: a renderer-published browser tab must stay in the phone tab list when its workspace has no mounted webview. The live browser bridge must not gate renderer-published rows. Headless and runtime-owned rows still require a live page.

Evidence at the reviewed head:

  • runtime-mobile-session-projection.ts: a browser tab without a live page is kept when the host reports it as renderer-owned. A nonempty live title and URL override snapshot values. Other tabs without a live page are still skipped.
  • workspace-file-session-targets.ts: collectPublicMobileSessionTabIds applies the same renderer-owned rule. Tab-ID collection follows the same ownership distinction, as the issue requests.
  • mobile-session-publication-preservation.ts: isRendererOwnedMobileBrowserTab returns true when the tab is not client-placed, an authoritative window exists, and the tab is not headless-owned. isHeadlessOwnedMobileBrowserTab uses headless-built publication epochs or the accepted renderer identity set. Legacy ':headless-merge:' suffixes are normalized before identity comparison. Unknown renderer epochs are classified by identity, not dropped.
  • The changes do not activate or mount workspaces, which the issue forbids for enumeration.
  • The stale-browser-tab test in terminal-handles-and-agent-status-part-03.spec.ts is re-scoped to renderer snapshots, as the issue notes. New renderer-browser ownership, lifecycle, and reconciliation tests cover retention and headless-only drops.

Limits: the diff tool could not read changed blobs in this checkout. The verdict relies on the whole-PR change summary and two file reads at head (structured-session-tab-close.ts and mobile-session-publication-preservation.ts). Native iOS, E2EE, and test-count results in the PR description refer to head 49178f1. The reviewed head is ef129c8. Those results are not verified here. They do not change the code-based verdict.

Full details: Out of Scope Changes check

Explanation

The changes support [#14598]. Each file in the change list traces to renderer-owned browser row handling:

  • Projection, public ID collection, and ownership predicates (runtime-mobile-session-projection-contract.ts, runtime-mobile-session-projection.ts, workspace-file-session-targets.ts, prune-mobile-session-tab-group-layout.ts, reconcile-headless-mobile-session-browser-tabs.ts, mobile-session-publication-preservation.ts).
  • Preservation and group membership for preserved browser rows (mobile-session-preservation.ts). Without this, a surviving renderer row loses its group order after detach and return.
  • Close routing and publication-epoch handling (structured-session-tab-close.ts). A listed renderer row must be closable through the owning session path. Retirement now uses the delegated epoch method.
  • Tests for the same behavior (six test files).

The close routing, detach/rebase, and group repair go beyond the literal list display. They follow from the same ownership rule. A row that is listed as renderer-owned must also close and keep its group under that rule. No change is shown to be unrelated.

The PR description names BrowserAddressBar.tsx, ClientHostedBrowserPagePane, and two locale catalog entries as changed. The current change list does not include these files. The prior review found an empty BrowserAddressBar.tsx diff at the reviewed head. If any such formatting or catalog edit exists, it is non-behavioral cleanup. It would not change this verdict.

Limits: changed-file diffs were not readable in this checkout. This assessment relies on the change summary and head file reads.

Full details: Description check

Explanation

The description is thorough and follows the template. It has an ELI5, a What Changed section, a Why section, a linked issue (Fixes #14598), visual proof with before and after images, extensive testing notes, an AI-disclosure heading left empty (acceptable for team members, which the template allows), the agent skill upstream boundary checkbox, notes covering security/cross-platform/SSH/mobile/backwards compatibility, and a completed checklist. Minor gaps: the Testing section lists platforms tested, but the Linux/Windows/macOS coverage is only partly explicit, and the Review and Notes sections are long. These do not make it largely incomplete.

Finishing Touches: 💡 1
🛠️ Fix failing CI checks: 💡
  • Commit to this branch
  • Create a new PR
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

@nwparker
nwparker force-pushed the nwparker/mobile-b4-browser-tabs branch from 6dd0cd1 to 8955126 Compare October 21, 2026 21:11

@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.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: a6382bc3-f50c-4ea1-bdd6-51b195a1a2b9
📥 Commits

Reviewing files that changed from the base of the PR and between afcb385 and 2047972.

📒 Files selected for processing: 14
  • src/main/runtime/orca-runtime-tests/terminal-handles-and-agent-status-part-03.spec.ts
  • src/main/runtime/renderer-browser-accepted-epoch-review.test.ts
  • src/main/runtime/renderer-browser-lifecycle.test.ts
  • src/main/runtime/renderer-browser-ownership.test.ts
  • src/main/runtime/renderer-browser-session-reconciliation.test.ts
  • src/main/runtime/rpc/methods/session-tabs-move-validation.test.ts
  • src/main/runtime/runtime-mobile-session-projection-contract.ts
  • src/main/runtime/runtime-mobile-session-projection.ts
  • src/main/runtime/session-lifecycle/projection/prune-mobile-session-tab-group-layout.ts
  • src/main/runtime/session-lifecycle/projection/reconcile-headless-mobile-session-browser-tabs.ts
  • src/main/runtime/session-tabs/mobile-session-preservation.ts
  • src/main/runtime/session-tabs/mobile-session-publication-preservation.ts
  • src/main/runtime/session-tabs/structured-session-tab-close.ts
  • src/main/runtime/workspaces/workspace-file-session-targets.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 1 remain after this review.

Comment on lines +163 to +178
protected isHeadlessOwnedMobileBrowserTab(
snapshot: RuntimeMobileSessionTabsSnapshot,
tab: RuntimeMobileSessionBrowserTab
): boolean {
if (this.isHeadlessBuiltMobileSessionPublicationBase(snapshot.publicationEpoch)) {
return true
}
const rendererTabIdentityKeys =
this.getAcceptedRendererIdentityKeysForMobileSessionSnapshot(snapshot)
if (rendererTabIdentityKeys) {
return !getMobileSessionSnapshotTabIdentityKeys(tab).some((id) =>
rendererTabIdentityKeys.has(id)
)
}
return false
}

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.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🔎 Supported by static analysis

🏁 Script executed:

#!/bin/bash
rg -nP --type=ts -C3 'headless-merge' src/main src/renderer src/shared -g '!**/*.test.ts' -g '!**/*.spec.ts'
rg -nP --type=ts -C3 'acceptedRendererMobileSnapshotByWorktree\.(set|delete)' src/main

Repository: stablyai/orca

Length of output: 16610


🏁 Script executed:

set -e
printf '%s\n' '--- preservation file ---'
nl -ba src/main/runtime/session-tabs/mobile-session-publication-preservation.ts | sed -n '1,245p'
printf '%s\n' '--- close routing references ---'
rg -n -F --glob '*.ts' -e 'isOffscreenMobileSessionBrowserTab' -e 'getListedBrowserTabsByPageId' -e 'closeSessionTab' src/main
printf '%s\n' '--- close routing definition ---'
nl -ba src/main/runtime/session-lifecycle/projection/structured-session-tab-close.ts | sed -n '120,215p'
printf '%s\n' '--- reconciliation complete relevant blocks ---'
nl -ba src/main/runtime/session-tabs/mobile-session-reconciliation.ts | sed -n '110,255p'
printf '%s\n' '--- diff from supplied merge base ---'
git diff --unified=35 0b82f88e9f2b6904f8ecf296b11d0581de4cfbb9 204797298ddffcf825a1f20ca60d8b12a95dbf89 -- src/main/runtime/session-tabs/mobile-session-publication-preservation.ts src/main/runtime/session-tabs/mobile-session-reconciliation.ts src/main/runtime/session-lifecycle/projection/structured-session-tab-close.ts

Repository: stablyai/orca

Length of output: 40447


🏁 Script executed:

set -e
printf '%s\n' '--- close routing ---'
nl -ba src/main/runtime/session-tabs/structured-session-tab-close.ts | sed -n '135,205p'
printf '%s\n' '--- renderer ownership consumers ---'
rg -n -F --glob '*.ts' -e 'isRendererOwnedMobileBrowserTab' -e 'isHeadlessOwnedMobileBrowserTab' -e 'collectPublicMobileSessionTabIds' src/main
printf '%s\n' '--- consumer source ---'
for f in $(rg -l -F --glob '*.ts' -e 'isRendererOwnedMobileBrowserTab' -e 'collectPublicMobileSessionTabIds' src/main); do
  echo "--- $f"
  nl -ba "$f" | sed -n '1,260p'
done
printf '%s\n' '--- base merge implementation ---'
git show 0b82f88e9f2b6904f8ecf296b11d0581de4cfbb9:src/main/runtime/session-tabs/mobile-session-publication-preservation.ts | nl -ba | sed -n '35,145p'

Repository: stablyai/orca

Length of output: 42557


Treat listed pages under a legacy :headless-merge: epoch as headless-owned.

When no accepted renderer identity pair exists, isRuntimeOwnedBrowserOnRendererPublication treats a :headless-merge: snapshot as runtime-owned, but isHeadlessOwnedMobileBrowserTab returns false. A listed offscreen page can therefore be treated as renderer-owned, routed to closeSessionTab, and retained by renderer-owned projection filters.

Use the offscreen inventory as the ownership proof for this legacy fallback:

Suggested fix
     if (rendererTabIdentityKeys) {
       return !getMobileSessionSnapshotTabIdentityKeys(tab).some((id) =>
         rendererTabIdentityKeys.has(id)
       )
     }
-    return false
+    // Legacy mixed snapshot: a live offscreen page is the only owner proof left.
+    return (
+      snapshot.publicationEpoch.includes(':headless-merge:') &&
+      typeof tab.browserPageId === 'string' &&
+      this.getListedBrowserTabsByPageId(snapshot.worktree).has(tab.browserPageId)
+    )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
protected isHeadlessOwnedMobileBrowserTab(
snapshot: RuntimeMobileSessionTabsSnapshot,
tab: RuntimeMobileSessionBrowserTab
): boolean {
if (this.isHeadlessBuiltMobileSessionPublicationBase(snapshot.publicationEpoch)) {
return true
}
const rendererTabIdentityKeys =
this.getAcceptedRendererIdentityKeysForMobileSessionSnapshot(snapshot)
if (rendererTabIdentityKeys) {
return !getMobileSessionSnapshotTabIdentityKeys(tab).some((id) =>
rendererTabIdentityKeys.has(id)
)
}
return false
}
protected isHeadlessOwnedMobileBrowserTab(
snapshot: RuntimeMobileSessionTabsSnapshot,
tab: RuntimeMobileSessionBrowserTab
): boolean {
if (this.isHeadlessBuiltMobileSessionPublicationBase(snapshot.publicationEpoch)) {
return true
}
const rendererTabIdentityKeys =
this.getAcceptedRendererIdentityKeysForMobileSessionSnapshot(snapshot)
if (rendererTabIdentityKeys) {
return !getMobileSessionSnapshotTabIdentityKeys(tab).some((id) =>
rendererTabIdentityKeys.has(id)
)
}
// Legacy mixed snapshot: a live offscreen page is the only owner proof left.
return (
snapshot.publicationEpoch.includes(':headless-merge:') &&
typeof tab.browserPageId === 'string' &&
this.getListedBrowserTabsByPageId(snapshot.worktree).has(tab.browserPageId)
)
}

@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.

Actionable comments posted: 1


ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 741e3088-811f-473f-bc96-685beecff97c
📥 Commits

Reviewing files that changed from the base of the PR and between 2047972 and be97cd9.

📒 Files selected for processing: 2
  • src/main/runtime/renderer-browser-lifecycle.test.ts
  • src/main/runtime/session-tabs/mobile-session-preservation.ts

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 1 remain after this review.

@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.

Caution

Some comments are outside the diff and can’t be posted inline due to GitHub limitations.

⚠️ Outside diff range comments (1)

🟠 Major · Preserve renderer-owned browser rows during… · mobile-session-publication-preservation.ts:77-101

src/main/runtime/session-tabs/mobile-session-publication-preservation.ts:77-101
🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Preserve renderer-owned browser rows during snapshot merges.

When a non-client browser row has a non-legacy publication epoch with no accepted renderer identity set, isRendererOwnedMobileBrowserTab classifies it as renderer-owned. shouldPreserveHeadlessMobileSessionTab currently rejects it, so an incoming snapshot that omits the row can remove it from the merged snapshot.

Apply the renderer-ownership check before the offscreen-backend guard. This is separate from the legacy :headless-merge: fallback and browser-group restoration.

Suggested fix
       if (
         liveClientPage?.workspaceId === snapshot.worktree &&
         tab.placement?.kind === 'client' &&
         sameRuntimeBrowserPlacement(liveClientPage.placement, tab.placement)
       ) {
         return true
       }
+      if (this.isRendererOwnedMobileBrowserTab(snapshot, tab)) {
+        return true
+      }
       // Why: headless offscreen browser tabs exist only server-side, so a renderer-graph merge must keep them, not prune as "not in the graph".
       if (!this.offscreenBrowserBackend) {
         return false
       }

ℹ️ Review info
⚙️ Run configuration
  • Configuration used: Repository UI
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 60e321e7-f66c-4143-93a5-c7d0b809ba67
📥 Commits

Reviewing files that changed from the base of the PR and between be97cd9 and 3b12d36.

📒 Files selected for processing: 1
  • src/renderer/src/components/browser-pane/assemble-chrome/BrowserAddressBar.tsx

Included review availability: This review used your included allowance. Your plan provides up to 10 included reviews per hour; 0 remain after this review.

@AmoabaKelvin
AmoabaKelvin force-pushed the nwparker/mobile-b4-browser-tabs branch 2 times, most recently from 9d4174f to afb8f0a Compare October 28, 2026 22:03
@greptile-apps

greptile-apps Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

RetriggerView in GreptileConfidence Score: 5/5

[Medium impact] The PR appears safe to merge; no new blocking issue was found.

Summary

Keeps unmounted desktop browser tabs visible on phones and sends Close to the owner of each tab.

  • The phone keeps desktop browser tabs visible when their webviews are unmounted.
  • The phone closes each browser tab through the session that owns it.

No new actionable issues were found. The previous-review ancestry and merge-base checks did not pass, so the full PR diff was reviewed.

In the PR description, nwparker explicitly defers temporary unknown ownership while waiting for a renderer resend, looser terminal checks in mixed offscreen/terminal snapshots, missing mirror-version updates when an omitted snapshot retains only client pages, rows retained after renderer crash recovery gives up, and the duplicated visibility rule. These remain acknowledged follow-ups, not new findings. nwparker also identifies stale saved title/URL overlays as an existing baseline limitation.

Diagram

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Browser row] --> B{Client hosted?}
  B -->|Yes| C[Require a live client page]
  B -->|No| D{Renderer owns row and window exists?}
  D -->|Yes| E[Keep row without a mounted page]
  E --> F[Send Close to desktop session tab]
  D -->|No| G[Require a listed runtime page]
  G --> H[Close through offscreen backend]
Loading

Reviews (8) · Last reviewed commit: "test(mobile): name the offscreen epoch o..." · Reviewed by Greptile

@AmoabaKelvin
AmoabaKelvin force-pushed the nwparker/mobile-b4-browser-tabs branch from d9a2b14 to e15b4e0 Compare October 28, 2026 23:10
nwparker and others added 14 commits October 28, 2026 23:31
Adapt the community browser ownership fix from #14792 to the current runtime modules. Preserve renderer epochs and route mobile browser close through its owner.

Co-authored-by: Bing.Z <zzb@gxsmjx.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
In serve mode the offscreen reconcile no longer checked for a window, so a
closed window's browser rows stayed listed with no page behind them. Also
drop the legacy :headless-merge: ownership fallback, which nothing produces.
The bridge inventory also lists an attached window's webviews. Only the
offscreen reconcile filtered those out, so hydrate added a second row for
the same page, and closing it unregistered the desktop's guest. Filter in
the shared builder instead.
With a desktop window attached in serve mode the bridge inventory also
lists the window's background webviews, which the renderer does not
publish. Those became offscreen rows, and closing one unregistered the
desktop's guest. Ask the backend which pages it owns.
@AmoabaKelvin
AmoabaKelvin force-pushed the nwparker/mobile-b4-browser-tabs branch from e15b4e0 to 058fae6 Compare October 28, 2026 23:32
@AmoabaKelvin
AmoabaKelvin merged commit a532235 into main Oct 28, 2026
48 checks passed
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]: Browser tabs are missing from the phone's tab list until the desktop displays that workspace

2 participants