Repository navigation
fix(terminal): a pane another client moves to a new tab leaves this window's source tab - #33032
Merged
nwparker merged 96 commits intoOct 26, 2026
Conversation
…indow's source tab When a phone, another desktop or the host moved a pane out of a split, this window kept drawing the same live terminal in both tabs. Orphan recovery read the source tab's binding as an orphan and kept it, and the pane reconciler only closes panes whose PTY ended. A binding whose terminal the host publishes in another tab is now stale rather than orphaned, and a live pane whose PTY another tab now binds is detached with its PTY left running.
This comment has been minimized.
This comment has been minimized.
…snapshots; retire the moved pane's agent records A host snapshot taken before the host processed this window's own move still lists the terminal in the source tab, so the stale-binding rule would drop the new tab's pane. A pending host move now exempts its target pane until it settles. An externally moved pane's agent records are retired with the source pane, and only leaves in a tab's tree count as binding a PTY.
…get this window already moved it to If a pre-move snapshot redraws the source pane after this window's own move, the source key already aliases the live target; retiring it would delete the target's agent status and launch records.
# Conflicts: # src/renderer/src/components/terminal-pane/terminal-live-layout-reconciliation.test.ts # src/renderer/src/components/terminal-pane/terminal-live-layout-reconciliation.ts # src/renderer/src/components/terminal-pane/use-terminal-pane-reconciliation.ts
#33706) A repo saved as \\wsl.localhost\<distro>\mnt\c\... runs Git in WSL, which lists its worktrees as /mnt/c/.... The listing mapped those to C:\..., so the primary worktree got the id <repo>::C:\... instead of the saved <repo>::\\wsl.localhost\...\mnt\c\.... Its tabs, agents and terminal history are keyed by the saved id: the renderer dropped them as belonging to no worktree, and the C:\ row showed up as a hidden discovered worktree. Map drvfs output to the distro's UNC view when the repo itself is saved under it. Repos saved as drive paths (Git routed to WSL) and in-distro repos keep today's drive mapping.
Moves the push gateway's database layer into a shared cloud/packages/cloud-database package (service name and schema passed in). Push's behaviour, log names, SQLite file and tests are unchanged.
* refactor(renderer): group agent-status records and reducers * refactor(renderer): consolidate concrete type owner imports
…iry, on connector tokens (#33734)
…ot Orca's launch flag (#33460) * fix(codex): resume a thread with the permissions it was last given, not Orca's launch flag Codex restores a resumed thread's saved approval/sandbox settings unless the launch passes a permission flag. Orca replayed its launch-time recipe (--dangerously-bypass-approvals-and-sandbox, --approve-for-me, or the Ask flags) on every resume, so a session switched to "Approve for me" came back as Full Access after a restart, and that override was saved to the thread. Fixes #10886 Co-authored-by: mys <4546173+wowpotato@users.noreply.github.com> * test(mobile): Codex AI Vault resume no longer replays Orca's permission flag The mobile AI Vault resume builds its command through the shared resume plan, so it now omits --dangerously-bypass-approvals-and-sandbox like desktop does. Co-authored-by: mys <4546173+wowpotato@users.noreply.github.com> --------- Co-authored-by: mys <4546173+wowpotato@users.noreply.github.com>
…resumed (#33776) * fix(agent-resume): record a background agent launch's recipe so resume keeps its flags * test: narrow the reveal's notifier so the test needs no cast * refactor(agent-resume): one recipe registration path; skip adopted panes * test: add the launch-recipe listener to the remaining window mocks
…once its shell is up A desktop-seeded first tab in an empty paired workspace is still settling its first spawn when a split lands right after it opens, which can leave the split pane unbound; the spec now has the server make the source tab, as #33027's does. Its timeout bump says why.
releaseDrainWaiters runs on every stream-socket read, but waiters only exist while a disconnect drains. Return early when the list is empty so each read no longer allocates a throwaway filter array and callback closure. Co-authored-by: OrcaWin <alpha-eng@stably.ai>
…#33794) oxlint 1.89 adds eslint(no-setter-return), which flagged one test setter. Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
…33796) * test: remove low-value inventories, passthroughs and redundant cases * style: format test suites after pruning redundant cases * test: remove obsolete workflow and flaky-test references * test: prune deleted builder test from Windows shim allowlist --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
… failing (#24839) * fix(agent-awake): back off and warn once when the Linux lid sleep assertion keeps failing On a headless Linux host, systemd-inhibit starts and then exits with code 1. start() reset the failure streak right after the spawn, so every 30 s retry logged a fresh warning and the retry never backed off. Keep the streak until an intentional stop or until an inhibitor has held for a full retry interval, and double the retry delay on consecutive failures (30 s up to 30 min). Fixes #20739 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(agent-awake): keep the lid assertion failure streak across idle/active cycles stop() reset the warn-once dedupe and backoff, so a host where logind denies the inhibitor still warned once per agent turn. Reset only after an inhibitor was actually held for a retry interval. * fix(agent-awake): keep the 30s lid assertion retry instead of backing off Backoff delayed re-acquiring the lid inhibitor on laptops after transient failures; warning once per failure streak already fixes the log flood. --------- Co-authored-by: brynnclaw <261708852+brynnclaw@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
…10586) set_value looked up node.is_editable_text before attempt() ran. GI bindings from at-spi2-core < 2.53 (e.g. Ubuntu 24.04) do not expose that function, so the AttributeError escaped and every set-value failed. Probe with get_editable_text_iface, which has been bound since 2.9.2 and checks the same interface. Closes #10569 Co-authored-by: bbingz <zzb@gxsmjx.com>
* fix(updater): probe latest-linux-arm64.yml on Linux arm64 Preflight HEAD of the Linux updater manifest used latest-linux.yml on every arch, so arm64 could pin a tag whose arm64 yml was still missing (or skip a tag that was arm64-ready). Match electron-updater and probe latest-linux-arm64.yml when process.arch is not x64. * test(updater): accept arch-suffixed Linux manifests in updater feed mocks On a Linux arm64 runner the feed now requests latest-linux-arm64.yml, which the [a-z]+ suffix in these mocks rejected (12 failures with process.arch=arm64). --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
Each custom oxlint JS rule now checks the file text first and returns no visitors when it cannot report, so oxlint skips the AST transfer to JS. app-store-performance only resolves scopes for tracked hook names. anti-slop rules are wrapped with per-rule text prefilters. react-doctor rules are off for folders without React (the old cloud/** ignore never matched: config/ globs need a leading **/). Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
#22494) * fix(computer-use): send correct named keys and hotkey letters on Linux press_key passed the keysym for named keys (Return, Tab, arrows, Escape) to AT-SPI as PRESSRELEASE, which expects a hardware keycode, so a different key was pressed. Synthesize with SYM instead. hotkey forwarded upper-case letters such as CmdOrCtrl+A to xdotool, which reads them as shifted keysyms and sends Ctrl+Shift+A. Lower-case single letters in key combinations. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(computer-use): drop the unreachable lone-key hotkey case --------- Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
* Unify terminal starts under one host operation * Pin winning geometry across a late terminal join * Use terminal creation options type in launch race test * Check closed panes after acquiring the terminal spawn lease * Preserve terminal start resource and join boundaries * Keep terminal attach regression within project boundaries * Capture terminal spawn authority and preserve image coverage * Use checked fixtures for terminal image replay regression * test: expose persisted pane to main layout in attach race * test: contract terminal spawn scaling and retention * test: correct spawn scaling fixture types and CI selection * test(terminal): cover exited prompt claims and adopted team cleanup * test(pty): consolidate mock registry imports * fix(pty): retain untyped IPC resume inputs in common contract
…, not the first copy (#33818) A worktree id or project host setup id repeats across hosts. Lookups that named no host picked the first match, so a delete or setup mutation could land on the other host's copy. They now go through one sole-copy rule and refuse when the matches span more than one host; callers that know the host (settings Remove, ephemeral VM rollback) now name it. Co-authored-by: fettpl <38704082+fettpl@users.noreply.github.com>
… again Back to the reviewed behaviour: the source tab's agent records are retired and the host republishes the agent under its new tab. Keeps the independent fixes: a committed move stays fenced until a host snapshot shows it (confirmed on the raw frame, and not re-held by a reply that arrives after its snapshot), the draw-before-reply release on a throw, and the reconcile effect's tab ids.
…-10 unit shards (#33887) - Plan unit selection starts after setup (overlaps lint) instead of last. - Localization extraction starts with the typecheck; test-file-runners is background. - run-ratchets.mjs runs ratchets in a pool (19s serial -> 8s locally). - pnpm lint uses CI's single-parse check-import-boundaries.mjs. - ORCA_UNIT_FULL_SHARD_COUNT accepts 2-10 (default stays 5). Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
Issue create (including the empty-body fallback and follow-up PATCH), issue body edits, and issue comments passed the body as `--raw-field body=...` on argv. Windows caps the command line at 32,767 chars, so bodies with pasted inline images failed with `spawn ENAMETOOLONG` before reaching GitHub. These calls now use `gh api -X <METHOD> <endpoint> --input -` with a JSON payload on stdin. ghExecFileWithScopeAsync also now forwards `options.stdin` to the child process; it was previously dropped. Fixes #15832 Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
… Linux (#31675) Open in started editors with Orca's full environment, so an AppImage launch leaked ARGV0/APPDIR/APPIMAGE/OWD/APPIMAGE_LIBRARY_PATH and the mount-path PATH and LD_LIBRARY_PATH entries into the editor's integrated shells (zsh then runs host commands like cargo under the AppImage name). Add envWithoutAppImageRuntime() next to removeAppImageRuntimeEnv and pass its result to every editor spawn path on Linux; macOS and Windows keep inheriting the env unchanged. The clipboard publisher and both AppImage daemon command builders reuse the same helper instead of their own copy-and-scrub loops. Fixes #31455 Co-authored-by: Neil <neil@stably.ai>
…don't skip dates (#19086) The RRULE day scan stepped local midnight by a fixed 24h. Walking back over a 23-hour spring-forward day landed at 23:00 two dates earlier, so the latest-occurrence lookup used for catch-up runs skipped a date. It returned a run a day too old for daily schedules, and a week too old for weekly ones. Step with setDate via a shared addLocalDays helper, in both the RRULE scan and the cron feasibility scan. Add a regression test for America/New_York and Australia/Lord_Howe. The test is skipped on Windows, where unstubbing TZ doesn't restore the OS zone. Co-authored-by: liutian <liutian.6000@bytedance.com> Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
…ge fills (#32894) Returning early from the for-await over the forward-page ReadStream destroyed the stream, which closed the shared FileHandle before the boundary checkpoint read, so any forward page that hit its limit failed with transcript_parse_failed. Scan with positional handle.read calls instead so the handle stays open and read errors reject inside the caller's try/finally. Fixes #32892 Co-authored-by: Kim Kaznowski <kim@doubledashstudios.com> Co-authored-by: Neil <neil@stably.ai>
…6030) `orca fill --value ""` and `orca select --value ""` were rejected client-side with "Missing required --value" even though the browser.fill/browser.select RPC schemas already accept an empty string. Read --value with the existing getRequiredStringFlagAllowingEmpty helper so an empty value clears a field or picks the blank option, while an absent --value is still rejected. Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
…k_install_failed (#25287) agent_hook_install_failed sent up to 200 chars of the raw installer error, which embeds home-directory paths (and so the OS user name) and can echo config contents. Send only the error's errno code or 'unknown', and enforce that in the event schema. The anchored, capped errno pattern now lives in src/shared/errno-code.ts and is shared with the renderer launch probe and the file-write precondition instead of their own copies. Fixes #21492 Co-authored-by: brynnclaw <261708852+brynnclaw@users.noreply.github.com> Co-authored-by: Neil <neil@stably.ai>
…otification option (#26321) On macOS the System sound option passed sound: "default" to Electron, which the UNUserNotificationCenter backend resolves via soundNamed:"default" (a file lookup) instead of UNNotificationSound.defaultSound, so banners were silent. Leave sound unset so Electron plays the OS default, and drop the now-dead sound field from the notification options type. Fixes #26043 Co-authored-by: Neil <neil@stably.ai>
…` is not on PATH (#14010) GUI-launched Orca often inherits a minimal PATH, so bare `code` fails with spawn ENOENT for Open in VS Code, including Remote-SSH worktrees. When PATH and install-dir lookup leave `code`/`code-insiders` unresolved, fall back to the CLI bundled in the standard macOS app bundle (/Applications or ~/Applications) or the Windows user/system install. Linux packages already link the CLI onto PATH, so no Linux candidates are added. Fixes #14008 Co-authored-by: Neil <neil@stably.ai>
#33899) The main bootstrap banner was prepended to index.js in generateBundle, after Rolldown had already emitted index.js.map, so every main-process crash trace decoded ~200 lines off. Emit it via Rolldown's output.postBanner instead, which shifts the source map natively. Co-authored-by: outof-place <contact@outofplace.space>
…ens fail (#33831) * fix(drops): keep files dropped onto a remote workspace out of git status Uploads into `.orca/drops` now write `.orca/drops/.gitignore` (`*`) the first time it is missing, so dropped files no longer block a plain workspace delete or get committed by `git add -A`. Callers share one constant for the folder. Fixes #23856 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * Re-check the import session after the drop staging ignore step The ignore step swallows its own errors, including a stale session. When nothing was staged the loop never asserted again, so the caller got skipped/failed results instead of the retry error. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(drops): say why a remote drop or file open failed Co-authored-by: Jae Heo <83424878+jae-heo@users.noreply.github.com> Co-authored-by: Bing.Z <zzb@gxsmjx.com> * fix(drops): keep paths out of failure toasts and reuse the too-large classifier * fix(drops): shorten whole quoted paths in failure toasts * fix(drops): describe upload failures by category instead of host text * fix(drops): match failure categories on unquoted text and name manual disconnects --------- Co-authored-by: Younghwan Chae <chyohw97@gmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: Jae Heo <83424878+jae-heo@users.noreply.github.com> Co-authored-by: Bing.Z <zzb@gxsmjx.com>
…test ratchet (#33903) lint:ast-grep and check-flaky-test-pattern-ratchet each parsed the whole tree. ast-grep-gates.mjs scans once; the lint gate re-runs ast-grep only on files with warning-or-error findings (ast-grep's own report and exit code), and the flaky ratchet counts test-* matches from the shared scan. Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
Collapsing the section the View all diff viewport was inside kept the scroll anchor's within-section offset, which pointed into the removed body, so the structural restore landed on later sections. Pin the anchor to the section header before the collapse is published. Fixes #20665
…rse under any locale (#33898) macOS ps formats lstart with the user's locale (%c) and vis-encodes non-ASCII argv when no UTF-8 locale is set. Under de_DE/en_NZ/ja_JP the process-table parsers dropped tty/startTime and folded them into command, breaking agent recognition, pane fingerprints, agent-session start times and daemon identity. Run those ps readers with LC_ALL=en_US.UTF-8 on macOS; other platforms keep the inherited env. Fixes #27004 Co-authored-by: outof-place <contact@outofplace.space>
#14812) On drive and `\\` UNC roots, an open tab whose filePath used a different separator or drive-letter spelling than the watcher event (for example `c:\Repo\File.ts` vs `C:/Repo/File.ts`) never matched, so external edits did not reload the tab or raise the changed-on-disk banner. Both direct-match sites (the indexed batch lookup and the non-indexed fallback in getOpenFilesForExternalFileChange) now compare through externalChangePathKeyFor(root), which reuses editorBufferPathKey to fold separators, the drive letter, and the UNC server only. Path-tail case stays exact, and WSL, POSIX, SSH, and `//` roots keep byte-exact keys. Fixes #14801 Co-authored-by: VXNCXNX <VXNCXNX@users.noreply.github.com> Co-authored-by: Neil <neil@stably.ai>
…on macOS (#33901) The resource panel summed RSS. On macOS that counts the Electron framework's shared pages once per helper process, so main and renderer read 2-8x too high, while the GPU process read 3-5x too low because RSS excludes graphics memory. The detached terminal daemon was also missing from the app total. On macOS, sample per-process phys_footprint from top (the figure Activity Monitor shows) at most every 10 s while the panel is open, and fall back to RSS when top is unavailable. Count the local terminal daemon in the app's 'other' bucket; its PTY children stay attributed to their worktrees. Linux and Windows keep RSS/working set. Co-authored-by: m4air <m4air@Mac.localdomain>
…sn't re-paired (#33109) A WSL host's bridge listened on a fresh port each launch, which changed the saved pairing endpoint. The renderer treats a changed WSL pairing as a different machine and purges the host's workspaces, tabs and selection, then rebuilds them once the host relists its projects.
…cleanup (#33907) Prove the relay reaches wsl.exe through --exec unchanged, carries every byte value both ways, holds the desktop back under a stalled reader on either side, falls back to Node for no bash, old bash, no /dev/tcp and refused dials with unchanged sentinels, and leaves no bash or cat behind when either side goes away.
…Cmd+Shift+G (#18844) The Cmd+G / Cmd+Shift+G path called findNext/findPrevious with only { caseSensitive, regex }. @xterm/addon-search only draws the active-match decoration, fires onDidChangeResults, and keeps refreshing highlights on new output when the call carries decorations, so the shortcut froze the active highlight and the match counter. Share one buildTerminalSearchOptions() between the find bar and the shortcut path so both send the same decorated options. Co-authored-by: Anton Kuligin <anton.kuligin@notraffic.tech> Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
… it (#33906) Agent Session History now routes Delete for a session on a paired or SSH-managed Orca server to that server through a new aiVault.deleteSession RPC. The host revalidates the path against its own roots, removes the transcript (into Trash on a desktop host; permanently on a plain-Node host, which has none), and invalidates its caches. Hosts advertise aiVault.delete-session.v1; an older host keeps Delete disabled with "Update <host> to delete its sessions from here." Co-authored-by: brynnclaw <261708852+brynnclaw@users.noreply.github.com>
…ogin, not OAuth (#26609) Orca has no OAuth app; it reads tokens from the gh CLI on the machine running Orca. Document that, the per-host login for Remote Orca Servers, and the repo/read:org/project scopes auth-diagnose expects. Refs #26604 Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
# Conflicts: # src/renderer/src/components/terminal-pane/terminal-pane-tab-detach-runtime-host.test.ts # src/renderer/src/components/terminal-pane/terminal-pane-tab-detach.ts # src/renderer/src/runtime/web-runtime-leaf-move.ts
nwparker
merged commit Oct 26, 2026
3804670
into
nwparker/fix-32939-move-pane-new-tab
105 checks passed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
ELI5
You have a terminal split in two on a paired Orca server. From your phone, another desktop, or the server itself, someone moves one half of the split into its own tab. Before this change, your desktop showed that terminal twice: once in the new tab, and again in the old tab next to its former neighbour. Now the old tab drops the moved pane, and the terminal keeps running in its new tab.
What Changed
Two client-side paths each kept the stale pane, so both are fixed (1 and 2 below). Items 3 and 4 keep that fix from breaking this window's own optimistic move.
Orphan recovery (
web-session-terminal-orphan-recovery-surface.ts). When a host snapshot stops naming a pane this window has, recovery treats the pane as a possible orphan. It asks the host whether that terminal is still alive, and if so it puts the pane back into its old tab. A moved terminal is alive, so recovery put it back. The fix: if the same snapshot shows that terminal handle ready in a different tab, the old binding is stale, not an orphan. Recovery skips it, makes noterminal.listcall, and the source tab's layout comes from the host alone.Pane reconcile (
use-terminal-pane-reconciliation.ts,selectRetiredPaneIds). When the host drops a leaf, the mounted pane manager used to close only panes whose PTY had already ended, so a live moved pane stayed on screen. Now a retired pane whose PTY another tab of the same workspace binds is detached withdetachPaneForExternalMove. That is the same no-kill path a local drag-out uses. The PTY keeps running for its new tab, and the last pane of a tab is still never removed.This window's own move (
pending-host-pane-moves.ts). A pane this window is moving through fix(terminal): a pane moved to a new tab stays moved on paired servers #33027's host move is marked until the move settles. Until then, a host snapshot taken before the host had the move can't read the new tab's binding as stale. Without the mark, that snapshot would drop the new tab.Agent records. A pane detached this way also gives up its agent status records under the source tab, so the source tab doesn't keep the moved agent's badge. The one exception is a source key this window already aliased to the new tab, which is left alone so the live target keeps its records.
This window's own move, after the reply. The host can reply before its snapshot of the move arrives. A move the host committed now stays marked until a host frame shows the pane in its new tab. Otherwise an older snapshot arriving after the reply could still drop the new tab. Only the raw host frame counts as confirmation, not rows that recovery keeps for this window. If that frame arrives before the reply, the reply does not mark the move again. If drawing the move throws, the mark is released.
Both checks rely only on what the host publishes, so they cover a move from a phone, from another desktop, or from the host's own window.
Why
The host already decides which tab holds each pane, and its snapshot after the move is correct (
session.tabs.listshows each leaf alone in its own tab). The client was overriding it in two places. Fixing it at those two places makes every source of the move behave the same, without a new message or capability.Alternatives considered:
Mixed versions: client-only and wire-neutral. Older hosts that can't move panes never publish one terminal in two tabs, so nothing changes for them. An older client keeps today's behaviour.
Mobile: checked. The phone rebuilds its terminal list from each snapshot's rows and has no orphan retention, so it never showed the duplicate. The web client uses the same renderer code as the desktop, so this fix covers it.
Folder workspaces / SSH: the change doesn't depend on git or the transport. It keys off the tab, leaf and PTY ids in the host snapshot.
Found while fixing #32939 (the window-side move in #33027). There's no separate issue. This PR is stacked on #33027 because its e2e calls
session.tabs.movePaneToNewTab, which #33027 adds.Known limit: a committed-move mark clears only when a host frame names the pane in its new tab. If the host never publishes that tab, for example because it closes before its first frame arrives, the mark stays for that pane key. The cost is that a later move of that same pane by another client is not detected as stale. Tab ids are unique per tab, so no other pane is affected.
Follow-up (not in this PR): when another client moves a pane, this window retires the source pane's agent records, and the host republishes the agent's status under the new tab. Local-only records (unread marks, launch config, sleeping state) do not follow the pane yet. An attempt to transfer them was dropped from this PR because it raced with snapshot freshness.
Linked Issue
No issue. Found while fixing #32939; it builds on #33027.
Visual Proof
N/A. This removes a duplicate pane that should never have been drawn. The paired e2e checks the outcome: the pane count, the layout leaves, and the bound PTY of both tabs.
Testing
I manually tested these changes locally
Automated tests added/updated, or explained why not below
E2e
tests/e2e/paired-remote-terminal-pane-moved-by-another-client.spec.ts: a headless paired host and a paired Electron desktop, run withORCA_BACKGROUND_LAUNCH=1and hidden windows. The test splits a pane, echoes a marker into the second pane, and moves that pane from the host client withsession.tabs.movePaneToNewTab. It then checks:It fails without this fix: the source tab kept 2 leaves and 2 panes. With only the reconcile change it still fails, keeping 2 panes. Both changes are needed.
fix(terminal): a pane moved to a new tab stays moved on paired servers #33027's own e2e (
paired-remote-terminal-pane-move-to-new-tab.spec.ts, 2 tests) still passes on top of this change.Unit tests:
web-session-split-pane-retirement-fences.test.ts: a real snapshot apply in which the second leaf moves tohost-tab-2. It expects no orphan RPC, a source layout holding only the first leaf, and the reconcile planner detaching (not closing) the moved pane. It fails without the orphan-recovery fix.terminal-live-layout-reconciliation.test.ts: the planner detaches a pane whose PTY another tab draws, and never removes the last pane.web-session-split-pane-retirement-fences.test.ts, second case: this window's own optimistic move, followed by a snapshot from before the host move. The new tab's binding stays an orphan candidate and is not dropped. It fails without the pending-move mark.terminal-pane-external-move-detach.test.ts: the source pane's agent records are retired, unless the source key is already aliased to the target.E2e change: the spec now has the server create the source tab and splits once its shell is up. A desktop-seeded first tab in an empty paired workspace can leave a pane split right after it opens unbound, which is a separate bug.
Fence tests (
web-session-split-pane-retirement-fences.test.ts,terminal-pane-tab-detach-runtime-host.test.ts): a committed move stays fenced past the reply until a host snapshot shows it; a retained row does not confirm it; a reply that arrives after its snapshot does not re-hold it. Each fails without its fix.Local checks: every unit test related to the changed modules by import graph (1,918 files) passes. One timing-sensitive test failed under machine load and passed when re-run on its own. The full static-analysis job, Codex review (3 rounds; the round 2 alias finding is fixed) and an independent review (no P0-P2) are clean on the head.
Platforms: macOS, locally. The change has no platform-specific code.