Skip to content

fix(runtime): a paired runtime's frame no longer drops another runtime's terminals in a shared workspace - #33414

Merged
nwparker merged 10 commits into
mainfrom
nwparker/fix-31983-cross-runtime-tab-mirror
Oct 25, 2026
Merged

nwparker merged 10 commits into
mainfrom
nwparker/fix-31983-cross-runtime-tab-mirror

Conversation

@nwparker

@nwparker nwparker commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor
Files Added Deleted Net
Test 3 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​325 $\color{#cf222e}{\Huge{\mathbf{−}}}$​10 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​315
Prod 9 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​138 $\color{#cf222e}{\Huge{\mathbf{−}}}$​10 $\color{#1a7f37}{\Huge{\mathbf{+}}}$​128

ELI5

Some setups show one workspace on a desktop using tabs from two different paired Orca runtimes. Example: the SSH host's server runs the terminals, and another desktop's Orca app hosts a browser tab that was opened there. Each time the second runtime sent an update, the desktop threw away every terminal tab in that workspace, then put them back seconds later. Now each runtime's update only touches the tabs that runtime owns.

What Changed

Before: desktop B is paired to an SSH host's managed server and also to desktop A's app. When A opened or reloaded a browser tab in the SSH workspace, or B closed A's browser tab, B dropped all of that workspace's terminal tabs. They came back a few seconds later as new rows (same PTYs, new createdAt). Sometimes they stayed gone until B was reloaded. The agents kept running the whole time; only B's view lost them.

After: B keeps the server's terminals and chats no matter what A's app publishes for that workspace.

Mechanism:

  • shouldReplaceTerminalTab replaced every mirrored terminal row on any runtime's frame. It now asks one ownership rule, web-session-tabs-sync/mirror-row-ownership.ts:
    • A mirrored terminal belongs to the runtime its pane bindings name, i.e. the runtime whose PTYs it streams.
    • A row still waiting for its first handle belongs to the runtimes whose last applied frames published it.
    • A frame doesn't retire a row that only another live runtime owns.
    • A frame that publishes the same id always rebuilds that row.
    • Runtimes that were removed, or are missing from a settled catalog, don't count. So rows they left behind can still be cleaned up.
  • Chat tabs follow the same rule, using their executionHostId. A frame that publishes or replaces (replacesSessionId) a chat takes that row over, so one chat never gets two rows.
  • When the active terminal is retired, focus falls back to a surviving terminal, which may be another runtime's.
  • Closing a terminal now goes to the runtime its own PTY names before the workspace catalog's route. A retained terminal from another runtime closes on that runtime.
  • Browser and editor rows were already scoped per runtime, so they're unchanged.

Why

Browser and editor rows already followed "a runtime's frame speaks only for its own rows". Terminals and chats didn't. Giving them the same per-source ownership fixes the whole class with one rule.

Alternative considered: ignore frames from any runtime that isn't the workspace's owner. Rejected, because it would also hide browser tabs another desktop hosts, which the mirror supports on purpose.

Two expectations in web-session-tabs-sync-visibility-collision.test.tsx encoded the old behavior, where the last publisher wiped the other runtime's terminals. They now show each host's rows staying until that host's own omission is applied. The visibility-resume repair logic itself is unchanged.

ref-oss check (this took more than 3 review rounds): per-source ownership, where each backend keeps and retires only its own terminals, is the standard pattern for clients that aggregate several remote backends. The design moved from separate guards to one ownership rule and dropped a coordinator change.

Linked Issue

Fixes #31983

Thanks to @amara-black for the detailed traces, the profile-state analysis and the follow-up tests that pinned this down.

Visual Proof

N/A. This is a renderer state reconciliation fix with no UI change; the visible effect (terminal tabs no longer disappearing) is covered by the unit reproductions below.

Testing

  • I manually tested these changes locally

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

  • New web-session-tabs-sync-cross-runtime-worktree.test.ts runs B's real snapshot-apply pipeline with two runtimes publishing the same workspace. It covers:

    • ready and pending terminals surviving the other runtime's open, close and retraction frames;
    • createdAt staying stable;
    • a terminal shared by both runtimes never staying bound to the runtime that dropped it;
    • chats surviving, with no duplicate rows;
    • rows of a removed runtime being retired;
    • a surviving terminal being refocused.
      The core cases fail on origin/main.
  • A close-routing case was added to terminal-tab-actions-pinned-server-close.test.ts.

  • Single-worker runs of every test file under src/renderer/src/runtime that imports the changed modules (212 files), the importers outside that folder (192 files) and the close-route importers all pass.

  • The full static job passes locally, except two steps that can't run on this machine: the Node 18 process-host smoke (no Node 18 installed) and the orcad terminal smoke (it needs a mobile-web build). This PR touches neither.

  • Not verified live: no two-desktop run. The headless e2e harness can't pair one client to two runtimes that publish the same workspace.

AI Disclosure

Review

Codex review is clean on the final head (base pinned at 407e9eb). An independent review found no P0 or P1 issues.

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

  • Remote and mixed versions: client-only. No wire or host changes, so old and new hosts behave the same.
  • Folder workspaces: they take the same path as git worktrees.
  • Mobile: not affected.
  • Known limit: a terminal still waiting for its first handle can briefly disappear if its runtime's subscription is reset at the moment another runtime publishes. The owner's next frame restores it.

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)

…e republishes a shared workspace

Desktop B can mirror one SSH workspace from two paired runtimes: the host's
server, which runs its terminals, and desktop A's app, which hosts a browser
tab A opened there. Any frame from A's app replaced every mirrored terminal
row in the workspace, so B dropped the server's terminals until the server's
next frame re-created them. A frame now replaces only the mirrored terminals
and chats its own runtime owns, matching how browser and editor rows were
already scoped.

Fixes #31983

Reported-by: amara-black
…ing host's replay

A tombstone now retires only its own host's rows, so the visibility-resume
repair no longer has to hold it back until every surviving host has a frame
to replay over the wiped workspace.
…me may retire

Bindings first, then the frames that last published the row, then the
workspace's own server; owners that were removed no longer count, so any
runtime's frame can retire their rows.
Drop the workspace-owner guess for rows with no evidence, let a frame that
publishes the same chat take its row over, and refocus a surviving runtime's
terminal when the active one is retired.
…time claims it

Union pane bindings and last-frame mappings into one claim set, so a row two
runtimes share survives one runtime's retraction. Drop the chat ownership
extension: chats are shared across runtimes by session id, and the reported
bug is terminals only.
A runtime that stopped publishing a terminal no longer claims it through a
cached pane binding; bindings only speak before any frame this session.
Keep the visibility-resume repair's survivor deferral as it was.
…me; chats follow the same rule

A shared terminal whose bound runtime drops it is retired and rebuilt by the
other runtime's next frame, so it never streams from a runtime that left.
Chats another live runtime owns survive this frame unless it publishes or
replaces them.
A workspace can now keep terminals another runtime publishes, so a close
follows the runtime the tab's own PTY names before the workspace catalog.
@coderabbitai

coderabbitai Bot commented Oct 25, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 24 seconds.

Check out review usage here.

View limit details

Limit details: You’ve used all 10 included reviews currently available.

Learn how review limits work.

Review configuration:

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: c2b12c46-0b4a-46de-8f53-6385bd0474e9
Commits

Reviewing files that changed from the base of the PR and between b1e18d8 and 9b15036.

Files selected for processing: 12
  • src/renderer/src/components/terminal/terminal-tab-actions-pinned-server-close.test.ts
  • src/renderer/src/lib/terminal-close-route.ts
  • src/renderer/src/runtime/web-session-tabs-sync-cross-runtime-worktree.test.ts
  • src/renderer/src/runtime/web-session-tabs-sync-visibility-collision.test.tsx
  • src/renderer/src/runtime/web-session-tabs-sync/apply-final-patch.ts
  • src/renderer/src/runtime/web-session-tabs-sync/apply-preparation-base.ts
  • src/renderer/src/runtime/web-session-tabs-sync/apply-preparation-browser.ts
  • src/renderer/src/runtime/web-session-tabs-sync/apply-preparation-unified.ts
  • src/renderer/src/runtime/web-session-tabs-sync/mirror-row-ownership.ts
  • src/renderer/src/runtime/web-session-tabs-sync/session-tabs-inventory-absence.ts
  • src/renderer/src/runtime/web-session-tabs-sync/terminal-surfaces.ts
  • src/renderer/src/runtime/web-session-tabs-sync/tracking-mappings.ts
  • 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.

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]: Second desktop loses all terminal tabs when the first desktop loads a page in a browser tab (SSH host, 1.4.237)

1 participant