Skip to content

fix(remote): focus the pane a host-delegated split creates - #16513

Closed
ASDFGHoney wants to merge 8 commits into
stablyai:mainfrom
ASDFGHoney:fix/remote-split-pane-focus
Closed

ASDFGHoney wants to merge 8 commits into
stablyai:mainfrom
ASDFGHoney:fix/remote-split-pane-focus

Conversation

@ASDFGHoney

@ASDFGHoney ASDFGHoney commented Aug 26, 2026 •

Copy link
Copy Markdown

ELI5

Split a terminal with Cmd+D on your own machine and you can start typing in the new
pane straight away. Do it on a workspace hosted by a remote Orca runtime and the pane
shows up but your typing still goes to the pane you split from — every time, until you
click the new one.

The reason is that the two cases take completely different routes. Locally, Orca creates
the pane itself and focuses it in the same breath. Remotely, Orca asks the host to do the
split and the pane comes back later as part of the host's session snapshot. When that
snapshot lands, the pane Orca focuses is whichever one the layout says is active — and
the layout deliberately prefers the leaf this client already had active over whatever the
host claims, so an agent's "I'm thinking" status update can't yank your cursor away
(#5435). Nobody ever told it that this particular arrival was the pane you just asked for,
so the old pane won.

This PR gives the split the same thing a remote "new terminal" already has: the client says
"I asked for this one" before the snapshot arrives, and the reconcile hands it focus.

What Changed

  • RuntimeTerminalSplit carries the created pane's leafId. Both host split paths
    populate it — the PTY-backed one already had the id in hand, and the graph-backed one now
    resolves the leaf alongside its handle.
  • splitWebRuntimeTerminal records a focus intent naming that leaf, then replays the
    current snapshot behind it — the same recordWebSessionFocusIntent +
    acceptCurrentSnapshot pairing createWebRuntimeSessionTerminal uses at
    web-runtime-session.ts:465.
  • A host that does not send leafId is left alone: no intent, today's behaviour. The same holds for
    any claim the mirror never matches — the layout keeps its existing activeLeafId, so an unmatched
    claim lands unfocused, never misfocused.
  • The split's failure toast now says a re-pair happened in words, instead of forwarding the pairing
    fence's log text.

Why

terminal-pane-split-with-inherited-cwd.ts:19 returns before the local split helper runs
whenever the pane is remote:

const ptyId = args.paneTransports.get(args.pane.id)?.getPtyId() ?? null
if (splitWebRuntimeTerminal(ptyId, args.direction, args.source)) {
  return
}

so splitManagedPane's setActivePaneId(newPane.id) (pane-split-close.ts:67) and
newPane.terminal.focus() (:145) never run for a remote split. The pane is materialised
later by the mirror, and TerminalPane.tsx:1478-1486 focuses whatever
restoredLayout.activeLeafId names.

That value comes from chooseRemoteTerminalLayout (web-session-tabs-sync.ts:975-990),
whose precedence is requestedActiveLeafId → the client's existing activeLeafId →
the host's surface.isActive. The existing leaf is the pane that was split, so it always
won. requestedActiveLeafId is fed by a client-recorded focus intent, which the split
never wrote.

Two alternatives were rejected:

  • Promote the host's isActive above the client's existing leaf. That is the exact
    ordering Fix remote terminal focus snapping during agent runs #5435 installed to stop status-echo snapshots stealing focus. It would trade
    this bug for that one.
  • Match the intent on the handle the split already returns. No wire change needed, but
    the graph-backed path returns a leaf handle (issueHandle) while session surfaces
    publish PTY handles (issuePtyHandle), and those two namespaces are not guaranteed to
    converge. leafId is authoritative on both paths.

No expectedCurrentLocalTabId guard is attached: the intent names the tab the user just
acted on, and the local path focuses unconditionally, so adding one would break parity for
a split in a non-focused group column.

Remote wire compatibility

Rule 1 of docs/reference/remote-wire-compatibility.md: a new optional JSON field on an
existing frame, and the reader falls back rather than requiring it.

new host old host
new client leafId present → new pane focused leafId absent → intent skipped, focus unchanged (today)
old client field ignored unchanged

No opcode, no protocol-version bump, no change to what the host publishes over the session
snapshot.

Linked Issue

Fixes #16510

Visual Proof

N/A — focus is not something a screenshot shows, and the topology needs two paired app
instances. The before/after is captured as an executable artifact instead:
tests/e2e/paired-remote-split-pane-focus.spec.ts, run against a real headed host paired
to a real Electron client.

active pane's PTY after Cmd+D
before (fix stubbed out, app rebuilt) remote:e71aab66-…@@term_95147121-… — the pane that was split
after the newly created pane

That failing run is the bug, reproduced end-to-end rather than described.

Testing

  • I manually tested these changes locally
  • Automated tests added/updated

Six tests, each pinning a different part of the mechanism:

  • web-session-tabs-sync-focus-intent.test.ts — "activates a host-split leaf only when the
    splitting client claimed it"
    . One snapshot, applied twice: unclaimed it keeps the source
    leaf active even though the host marks the new surface isActive (this is the Fix remote terminal focus snapping during agent runs #5435 guard,
    now pinned so the fix cannot be "solved" by weakening it); claimed it activates the new leaf.
  • web-runtime-session-terminal-pane-delegation.test.ts — the split records the intent for
    the host-returned leafId and replays the snapshot behind it, and records nothing when
    the host omits leafId (the old-host path).
  • orca-runtime.test.ts — the PTY-backed split result names the leaf its spawn actually got,
    compared against ORCA_PANE_KEY.
  • tests/e2e/paired-remote-split-pane-focus.spec.ts (new) — the whole journey on a real
    headed host paired to a real Electron client: create a host terminal, activate its mirrored
    tab in the client, split it through the same helper Cmd+D reaches (not manager.splitPane,
    which would bypass splitWebRuntimeTerminal entirely), then assert the active pane's PTY is
    not the one that was split.

Revert oracles, both run — a test that passes either way proves nothing:

stubbed out result
focusSplitWebRuntimeTerminalPane (unit) "claims the created leaf so the mirrored split arrives focused" fails; the other 7 in that file stay green
same, app rebuilt (E2E) the paired spec fails with the source PTY still active — the reported bug

Ran green on macOS 26.5.2:

  • pnpm tc
  • src/renderer/src/runtime + orca-runtime.test.ts — 144 files, 2308 passed, 1 skipped
  • src/cli, runtime-client-export-parity, claude-agent-teams-service,
    mobile-subscribe-integration — green in the same sweep
  • Electron E2E electron-headless: paired-remote-split-pane-focus (17.2s) and
    paired-remote-terminal-materialization-reconnect (18.9s). The second one matters because its
    helpers moved into the shared module — it is the check that the extraction changed no behaviour.
    Its third case stays skipped: that is a pre-existing test.fixme on main, untouched.
  • typecheck:e2e clean for the new spec and the shared helper
  • oxlint (incl. React Doctor) and oxfmt, via lint-staged on commit

Not run here: Windows and Linux. Nothing in this change is platform-dependent — no paths, no
shells, no key handling — so the risk is CI-shaped rather than behavioural.

AI Disclosure

Written with Claude Code (Claude Opus 5). Diagnosis, patch and tests were reviewed against
the source by the author before submitting.

Review

Root cause was traced through the whole chain rather than guessed at: the local/remote fork
in terminal-pane-split-with-inherited-cwd.ts, the host's two split implementations, the
mirror's layout precedence, and the effect in TerminalPane.tsx that turns activeLeafId
into an actual setActivePane. The sync-layer test was written first against unpatched code
to confirm the precedence really is what strands focus, and the E2E spec was then run with
the fix stubbed out to confirm the same conclusion holds in a real paired topology and not
only in the reasoning.

Agent skill upstream boundary

  • Not applicable, or this change follows docs/reference/agent-skill-sharing-upstream-boundary.md and copies or mechanically translates no upstream skill-installer source, tests, fixtures, registry entries, path tables, comments, or documentation.

Notes

  • Security: none — no new surface, no user input, no credentials.
  • Cross-platform: nothing platform-dependent; no paths, shells, or key handling touched.
  • Remote SSH: an SSH-hosted worktree whose PTYs are local ids never enters
    splitWebRuntimeTerminal and is untouched. Only paired-runtime panes (remote: ids) change.
  • Mobile: no mobile code touched.
  • Backwards compatibility: covered in the matrix above.
  • Performance: one snapshot refresh per split, on the path that already awaits an RPC.
  • Folder workspaces: the split path is workspace-kind agnostic; nothing assumes a git worktree.

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, pnpm test, and pnpm build pass (targeted suites + typecheck + lint ran locally; full suite and build left to CI)

Cmd+D on a paired remote pane short-circuits into splitWebRuntimeTerminal
before splitManagedPane's setActivePaneId/focus, so the new pane is only
materialised later from the mirrored session-tabs snapshot. There the active
pane is whatever layout.activeLeafId says, and chooseRemoteTerminalLayout
prefers the client's existing activeLeafId over the host's isActive — a
deliberate guard against status-echo focus theft (stablyai#5435). With no client-side
claim the source pane always won, so remote splits landed unfocused.

Carry the created pane's leafId back on RuntimeTerminalSplit (optional, so
older hosts stay compatible) and have the split initiator record the same
focus intent createWebRuntimeSessionTerminal records for a created tab. Hosts
that omit leafId keep today's behaviour rather than claiming the wrong leaf.

Fixes stablyai#16510

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

coderabbitai Bot commented Aug 26, 2026 •

Copy link
Copy Markdown
Contributor

Review 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

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7ba208de-3469-4322-a0b5-2e73909c32b1

📥 Commits

Reviewing files that changed from the base of the PR and between 719b411 and 0b677f9.

📒 Files selected for processing (7)
  • src/main/runtime/orca-runtime.ts
  • src/renderer/src/i18n/locales/en.json
  • src/renderer/src/runtime/web-runtime-session-terminal-pane-delegation.test.ts
  • src/renderer/src/runtime/web-runtime-session.ts
  • tests/e2e/helpers/paired-client-host-session.ts
  • tests/e2e/paired-remote-split-pane-focus.spec.ts
  • tests/e2e/paired-remote-terminal-materialization-reconnect.spec.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/runtime/orca-runtime.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 3 remain after this review.


📝 Walkthrough

Walkthrough

Terminal split results now include the created pane’s optional leafId. The main runtime propagates this identifier with terminal metadata. The web runtime records focus for the new pane when the host returns a valid tab ID, leaf ID, and pairing revision. Mirrored session snapshots then apply the claimed focus. Hosts without leafId preserve existing focus behavior. Tests cover leaf propagation, focus recording, snapshot replay, unclaimed host snapshots, and paired remote pane focus.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 61.54% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 13 functions across 7 files. (2 skipped: … Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes satisfy issue [#16510] by returning the created leaf ID, recording a focus intent before snapshot replay, preserving existing focus-precedence protections, and covering remote split behavi…
Out of Scope Changes check ✅ Passed The changes remain within scope. The shared E2E helper extraction, pairing-revision handling, localization, compatibility support, and related tests directly support the remote split-focus fix.
Title check ✅ Passed The title clearly and concisely describes the primary change: focusing the pane created by a host-delegated remote split.
Description check ✅ Passed The description follows the repository template and provides a clear explanation, scope, rationale, linked issue, visual-proof rationale, testing evidence, AI disclosure, review notes, compatibility c…
Full details: Linked Issues check

Explanation

The changes satisfy issue [#16510] by returning the created leaf ID, recording a focus intent before snapshot replay, preserving existing focus-precedence protections, and covering remote split behavior with unit and end-to-end tests.

Full details: Docstring Coverage

Explanation

Docstring coverage is 61.54% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 13 functions across 7 files. (2 skipped: 1 unsupported, 1 too large.)

Full details: Description check

Explanation

The description follows the repository template and provides a clear explanation, scope, rationale, linked issue, visual-proof rationale, testing evidence, AI disclosure, review notes, compatibility considerations, and checklist status.


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.

Caution

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

⚠️ Outside diff range comments (1)
src/renderer/src/runtime/web-runtime-session.ts (1)

1484-1500: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Fence terminal.split with the captured pairing revision.

If the pairing changes before dispatch, the missing revision lets terminal.split run on the replacement host, while focusSplitWebRuntimeTerminalPane drops the handoff because the captured revision is stale. Route this call through captureRuntimeEnvironmentCall(environmentId, intentOwner.pairingRevision) and add a regression test.


ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: f71d99b1-003b-4673-8ded-38cddfc9b57a

📥 Commits

Reviewing files that changed from the base of the PR and between cac5388 and 61186c4.

📒 Files selected for processing (6)
  • src/main/runtime/orca-runtime.test.ts
  • src/main/runtime/orca-runtime.ts
  • src/renderer/src/runtime/web-runtime-session-terminal-pane-delegation.test.ts
  • src/renderer/src/runtime/web-runtime-session.ts
  • src/renderer/src/runtime/web-session-tabs-sync-focus-intent.test.ts
  • src/shared/runtime-terminal-contracts.ts

Included review availability: Your plan provides up to 10 included reviews per hour; 9 remain after this review.

ASDFGHoney and others added 2 commits August 26, 2026 10:02
Drives the split through the same helper Cmd+D reaches, against a headed host
paired to a real Electron client, and asserts the active pane's PTY is not the
one that was split. Without the focus claim the assertion sees the source PTY
back, which is the reported bug.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The split RPC went out unfenced while the focus claim behind it captured a
revision, so a same-id re-pair mid-dispatch would split the replacement host
and then silently drop the claim — a stray unfocused pane on a host the caller
never addressed. Route it through captureRuntimeEnvironmentCall like every
other intent-bearing call in this file, so the pairing gate rejects it instead.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ASDFGHoney

Copy link
Copy Markdown
Author

Fence terminal.split with the captured pairing revision.

Good catch — fixed in 632b87f.

The call really was the odd one out. Every other intent-bearing call in this file already
goes through captureRuntimeEnvironmentCall(environmentId, intentOwner.pairingRevision)
(lines 291, 572, 1165, 1241, 1353); the split was reaching
window.api.runtimeEnvironments.call raw. That was harmless while the split was
fire-and-forget, but this PR gives it a revision-sensitive follow-up, so the asymmetry
started to matter: unfenced, a same-id re-pair mid-dispatch splits the replacement host,
and then matchesWebSessionIntentOwner drops the claim against the stale revision — a
stray unfocused pane on a host the caller never addressed. Fenced, runtime-environments.ts:124
rejects it outright, which is the behaviour the rest of the file already gets.

Regression test added: "fences the host split on the revision the focus claim captured".
It pins both halves — the RPC carries expectedEnvironmentPairingRevision, and a re-pair
while the split is in flight leaves no focus intent behind. Confirmed it fails for its
stated reason by reverting the call to the raw form: that test fails, the other 8 in the
file stay green.

Re-verified after the change: pnpm tc, src/renderer/src/runtime (143 files, 1100
passed), and the paired Electron E2E paired-remote-split-pane-focus.spec.ts (17.6s).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ASDFGHoney

Copy link
Copy Markdown
Author

On the one failing pre-merge check — Docstring Coverage 28.57% vs an 80% threshold:

Added docstrings in 719b411 to the three functions this PR actually introduces or reshapes
in web-runtime-session.ts — splitWebRuntimeTerminal, focusSplitWebRuntimeTerminalPane
(its // rationale is now a real docstring), and
resolveWebRuntimeSessionWorktreeIdForHostTab. Those had something non-obvious to say, so
they were worth writing.

I have deliberately not padded the rest to reach 80%, and want to be explicit about why
rather than leave it looking overlooked. The threshold is a long way from this repo's actual
convention — measured on the files this diff touches:

file JSDoc blocks / function-ish declarations
web-runtime-session.ts 5 / ~34 (~15%)
web-session-tabs-sync.ts 26 / ~150 (~17%)
orca-runtime.ts 97 / ~1275 (~8%)

And AGENTS.md asks for the opposite of blanket coverage:

Concise/Brief Non-obvious Comments ONLY — DO NOT: be verbose, explain the obvious,
walk through the code ("WHY not HOW"). BE CONCISE. 1 LINE if possible.

The remaining uncovered functions in the count are pre-existing host methods
(splitTerminal, splitPtyBackedTerminal, waitForNewLeafInTab) that this PR only edits
mechanically. Documenting them to satisfy a threshold would be scope creep in a file that is
~8% documented by convention, and would work against the "Out of Scope Changes" check that
currently passes.

Happy to write them anyway if a maintainer wants the check green — just say so and it is a
two-minute change.


Minor: the Merge Risk blurb still lists "follow-up on fencing the split request to the
pairing revision" while reporting up to 632b8 — but 632b87f is that fence. The recent-review
section for a45fa5d..632b87f agrees ("No actionable comments"), so the rollup text looks stale
rather than outstanding.

@ASDFGHoney

Copy link
Copy Markdown
Author

Two-axis review of main...HEAD (merge-base cac5388545, 4 commits): Standards (does it follow AGENTS.md + docs/reference/?) and Spec (does it do what #16510 asked?). Findings verified against the source, not just the diff.

Overall: the mechanism is right and the wire-compat reasoning holds up. One reuse violation, and three behavioural questions the PR body doesn't address.


Standards

Hard — AGENTS.md → Style → "Reuse Before Reimplementing"

tests/e2e/paired-remote-split-pane-focus.spec.ts:16-22, 24-38, 44-58 — three parallel copies of code already in tests/e2e/paired-remote-terminal-materialization-reconnect.spec.ts:

new spec existing
showClient :16-22 :80-87 byte-for-byte identical
waitForClientWorktree :24-38 :89-105 a narrowed copy — the existing one is already the more general form (optional expectedId, returns the id)
inline call/unwrap :44-58 callRuntime :65-78 same wrapper, re-done inline

The standard says "extend or generalize it instead of building a parallel version". Suggest promoting all three into tests/e2e/helpers/paired-electron-client.ts — already the home of createRuntimeDesktopPairingOffer/launchPairedElectronClient — and importing from both specs.

(Not reusing splitActiveTerminalPane (tests/e2e/helpers/terminal.ts:451) is correct and the spec says why at :91-92. No issue there.)

Judgement calls

  • src/renderer/src/runtime/web-runtime-session.ts:1536-1538 — the conditional spread is inert. refreshWebRuntimeSessionTabsSnapshot already falls back with options.expectedEnvironmentPairingRevision ?? getRuntimeEnvironmentRevision(environmentId) (:1031-1032), and all five other call sites (:468, :796, :1181, :1437, :1452) pass it unconditionally. Collapsing to one line matches the file's idiom.
  • web-runtime-session.ts:1548 — tab.id === localTabId || tab.id === hostTabId: toWebTerminalSurfaceTabId always prefixes (src/shared/terminal-surface-id.ts:4-9), so the raw-id arm can only match a tab that never came from the mirror, and nothing exercises it. Drop it, or add a line saying when a raw host id lands in tabsByWorktree.
  • web-runtime-session.ts:1545 — resolveWebRuntimeSessionWorktreeIdForHostTab reads tabsByWorktree (terminal tabs only) while placement code in the same file reads unifiedTabsByWorktree (:517-519). The name promises the general answer; consider putting the scope in it.
  • web-runtime-session.ts:1516-1518 — the // Why: hosts predating RuntimeTerminalSplit.leafId… comment sits on const leafId = split?.leafId?.trim() but explains the guard on the next line's if. It also restates src/shared/runtime-terminal-contracts.ts:259-261. Per "Concise/Brief Non-obvious Comments ONLY", a 14-line function carrying a 3-line JSDoc plus two inline Whys could lose one.
  • src/main/runtime/orca-runtime.ts:30694, 30695, 30709 — { handle: string; leafId: string } spelled inline three times in waitForNewLeafInTab; one named type reads lighter.

Clean

docs/reference/remote-wire-compatibility.md — compliant on all three rules, and the PR body's analysis is accurate. leafId is optional (Rule 1) and the reader genuinely falls back rather than requiring it (web-runtime-session.ts:1527 returns without claiming); both host paths populate it (orca-runtime.ts:30359-30364, :30554-30559), so absent unambiguously means old host; no new opcode (Rule 2); nothing changes in what the host publishes (Rule 3). ssh-execution-boundary.md is N/A (the split stays delegated; no liveness verdict introduced). No max-lines disable added, no .d.ts, no vague module name.


Spec (#16510)

Entry points — all three covered. The issue's expected behaviour names "shortcut, context menu, or pane header". Shortcut → terminal-keyboard-action-dispatch.ts:207, context menu → use-terminal-pane-split-actions.ts:57, pane header → TerminalPane.tsx:2869 (all under src/renderer/src/components/terminal-pane/); all three call splitTerminalPaneWithInheritedCwd, which forks into splitWebRuntimeTerminal at terminal-pane-split-with-inherited-cwd.ts:19. ✅

#5435 guard intact. chooseRemoteTerminalLayout (web-session-tabs-sync.ts:986-996) still orders requested → existing → parentLayout → isActive, and the file has no source change in the diff. The issue's "the fix belongs on the split initiator, not in the snapshot precedence" is honoured. ✅

Data flow checks out. intent → matcher (web-session-tabs-sync.ts:2686-2698) → focusTarget (:2762-2774) → requestedActiveLeafId (:1075) → activeLeafId (:986) → setActivePane(..., { focus: isActive }) (TerminalPane.tsx:1484). The PTY path publishes the same leafId variable it returns (orca-runtime.ts:30506); the graph path returns leaf.leafId from this.leaves (:30698), which is what the surface records carry. ✅

1. The intent channel carries two effects a local split does not

This is the one I'd most like a second opinion on. The PR argues no expectedCurrentLocalTabId guard is attached because "the local path focuses unconditionally, so adding one would break parity for a split in a non-focused group column". But the intent channel does more than move pane focus, and those extras have no local counterpart:

  • With expectedCurrentLocalTabId === undefined, callerFocusIntentTab always survives (web-session-tabs-sync.ts:2700-2704), so honorSnapshotActiveFocus is true (:2713) and intentUnifiedTabId overrides currentVisibleUnifiedTabId (:3081-3087). A remote split in a non-visible tab therefore pulls the unified tab bar to that tab. splitManagedPane never does.
  • expandedLeafId (:999-1005): with a requestedActiveLeafId and any existing expanded pane, the new pane inherits zoom. A local split does not zoom.

Both are arguably improvements, but they're parity changes the PR body doesn't claim, and the second one is reachable from any split of a tab that already has a zoomed pane.

2. "Both host split paths populate it" is broader than the code

orca-runtime.ts:30552-30557 returns leafId unconditionally — but publishPtyBackedMobileSessionTerminal sits inside if (createdPty) (:30500-30512). With a falsy createdPty the client records an intent naming a leaf no snapshot will ever carry. It degrades to today's unfocused behaviour rather than misfocusing, so not a regression — just worth not claiming more than holds.

3. waitForNewLeafInTab now decides focus, and it resolves any new leaf

orca-runtime.ts:30694-30700 returns the first new leaf in the tab. That was fine when its only job was minting handle; now it also names the leaf the client will claim, so two splits racing in one tab can claim each other's pane. Narrow in practice — worth a note either way.

4. Minor scope: the pairing fence changes split-failure UX

632b87ffea is a sound fix on its own terms, but routing the split RPC through captureRuntimeEnvironmentCall (web-runtime-session.ts:1489) means a same-id re-pair mid-dispatch now lands in the catch at :1505-1511, which surfaces toast.error(message) with the raw internal message. Previously the split silently went to the replacement host. Better behaviour, but it's a user-visible change outside what #16510 asked for, and the toast text isn't written for users.


Nothing here blocks the core fix — the diagnosis, the wire-compat handling, and the revert-oracle testing are all solid. The e2e helper duplication is the one I'd fix before merge; items 1 and 3 are worth a decision on the record.

Automated two-axis review (Claude Opus 5), findings verified against the source by the author.

ASDFGHoney and others added 3 commits August 26, 2026 10:43
Collapse the conditional pairing-revision spread the snapshot refresh already
falls back for, scope the resolver's name to the terminal tabs it reads, and
name the leaf a split creates instead of respelling its shape three times.

Records why the claim rides the create-tab intent channel and why the raw host
id arm is live, and states the bound waitForNewLeafInTab now decides focus on.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ransport did

Fencing the split RPC on the captured pairing revision made a same-id re-pair
mid-dispatch reject, and the split's catch put that rejection's raw text --
"Runtime environment pairing changed; refresh and try again" -- straight into a
toast. That string is written for logs and names our internals; the browser
stream path already classifies it for the same reason.

Classify only the fence code, so every other split failure still speaks for
itself in the toast, and keep the raw text on the console warning.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…es of

The split-focus spec re-declared showClient byte-for-byte, narrowed the
materialization spec's waitForClientWorktree, and inlined its callRuntime.

Both now reveal through the existing revealPairedClientWindow -- which also
waits for the renderer's own visibilitychange, the thing the copies were really
after -- and share one host-session helper for the RPC and the workspace wait.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ASDFGHoney

Copy link
Copy Markdown
Author

Worked through all ten findings. 8 fixed, 1 rejected, 1 needed no change. Three commits:
d2ce66e, 2607add, 0b677f9.

Fixed

  • A (reuse) — 0b677f9. callRuntime → callPairedRuntime and waitForClientWorktree →
    waitForPairedClientWorktree now live in tests/e2e/helpers/paired-client-host-session.ts, and
    both specs import them; the shared copy keeps the more general form (optional expectedId)
    rather than my narrowed one. showClient turned out to be a third copy of something that already
    exists — revealPairedClientWindow — so both specs now use that instead. Net −31 lines.
    It went in a new file rather than paired-electron-client.ts because that file sits 12 lines
    under the max-lines cap, and AGENTS.md forbids raising it.
  • B1 — the conditional spread was indeed inert; collapsed to the unconditional form the other
    five call sites use.
  • B3 — resolveWebRuntimeSessionWorktreeIdForHostTab → …ForHostTerminalTab, so the name
    carries the tabsByWorktree scope.
  • B4 — comment moved off the const and folded into the JSDoc; the contract restatement is gone.
  • B5 — one named CreatedSplitLeaf replaces the three inline spellings.
  • C4 — 2607add. You were right that the raw text leaks: runtime-environment-revision-guard.ts:19
    answers a mid-dispatch re-pair with runtime_environment_changed and a message written for logs.
    The toast now says "The remote host reconnected before the split ran. Split the pane again."
    Two tests: the classified path gets the written message, an unclassified failure still speaks for
    itself. Revert oracle run on both.

Rejected — B2

The tab.id === hostTabId arm is live, not dead. A client-minted agent-session tab keeps its own
id until the mirror replaces it, and the host adopts that id verbatim:

// orca-runtime.ts:28361
const operationTabId = request.placement?.tabId ?? deterministicAgentSessionUuid(...)

web-session-tabs-sync.ts:2731 handles exactly that case (nextHostTerminalTabIds.has(tab.id) for a
non-mirrored tab id). Kept the arm, added a one-line note saying when a raw host id lands in
tabsByWorktree.

No change needed — C2

The concern does not reach: getOrCreatePtyWorktreeRecord reads ptysById on the line after
registerPty wrote it. And the failure mode is benign either way — an intent naming a leaf no
snapshot carries never matches, so the layout keeps its existing activeLeafId and the split lands
unfocused, never misfocused. I have stopped claiming more than that in the PR body.

Decisions recorded in code

  • C1 — both extras accepted, with the reasoning now at the claim site rather than only in this
    thread: making the split's tab visible and moving an existing zoom onto the new pane both keep the
    pane the user just asked for on screen. Focusing a pane hidden behind a zoomed sibling would be
    the worse outcome, and expectedCurrentLocalTabId would trade this parity gap for a bigger one
    (a split in a non-focused group column would stop focusing at all).
  • C3 — bound documented on waitForNewLeafInTab: it resolves the first leaf the tab gained, not
    provably this split's, so two splits racing in one tab can swap leaves. Narrowing it needs the
    renderer to echo which split request minted which leaf — a bigger change than this fix, and the
    pre-existing handle-minting had the same bound.

Verification

pnpm tc · 144 files / 2308 unit tests passed · oxlint clean on all 6 changed files ·
typecheck:e2e clean for the new helper and both specs · Electron E2E
paired-remote-split-pane-focus (17.2s) and paired-remote-terminal-materialization-reconnect
(18.9s) both pass — the second is the spec whose helpers moved, so it is the check that the
extraction changed no behaviour. Its third case stays skipped; that is the pre-existing test.fixme
on main, untouched.

@nwparker

Copy link
Copy Markdown
Contributor

Closing as superseded: issue #16510 is fixed by merged PR #16886 (commit 16886), which restores focus for host-delegated split panes.

@nwparker nwparker closed this Aug 31, 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]: Cmd+D split in a paired remote session leaves focus on the source pane

3 participants