From 2aed2cf64a5ea961235e6cf2e5be146428b2d8fd Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Mon, 28 Sep 2026 16:34:36 -0700 Subject: [PATCH] fix(terminal): reveal splits while the source pane binds (#23692) * test(e2e): observe passive terminal restoration before activation * fix(terminal): reveal persisted splits during source binding publication * test(terminal): handle nullable persisted layout roots --- ...me-resolve-terminal-split-source-authority.ts | 3 ++- ...orca-runtime-terminal-split-authority.test.ts | 16 +++++++++++++++- ...erminal-tab-close-restart-persistence.spec.ts | 15 +++++++++++++-- 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/src/main/runtime/orca-runtime-resolve-terminal-split-source-authority.ts b/src/main/runtime/orca-runtime-resolve-terminal-split-source-authority.ts index 6acf8ba325f..167aee3e21b 100644 --- a/src/main/runtime/orca-runtime-resolve-terminal-split-source-authority.ts +++ b/src/main/runtime/orca-runtime-resolve-terminal-split-source-authority.ts @@ -54,12 +54,13 @@ export class OrcaRuntimeWithResolveTerminalSplitSourceAuthority extends OrcaRunt ) const rendererTab = this.tabs.get(tabId) const rendererLeaf = this.leaves.get(this.getLeafKey(tabId, leafId)) + // A mounted pane can publish before its PTY binds; persisted identity fences that gap. const rendererMounted = Boolean( rendererTab && rendererLeaf && runtimeWorktreeIdsEqual(rendererTab.worktreeId, worktreeId) && runtimeWorktreeIdsEqual(rendererLeaf.worktreeId, worktreeId) && - rendererLeaf.ptyId === ptyId + (rendererLeaf.ptyId === ptyId || (persisted && rendererLeaf.ptyId === null)) ) if (persisted && persistedLayout) { return { diff --git a/src/main/runtime/orca-runtime-terminal-split-authority.test.ts b/src/main/runtime/orca-runtime-terminal-split-authority.test.ts index 9da61e59072..8369a219331 100644 --- a/src/main/runtime/orca-runtime-terminal-split-authority.test.ts +++ b/src/main/runtime/orca-runtime-terminal-split-authority.test.ts @@ -78,6 +78,7 @@ function createHarness( deferSpawn?: boolean includePairedSnapshot?: boolean rendererMounted?: boolean + rendererPtyId?: string | null graphOnlySource?: boolean sourceIncarnationId?: string stopAndWaitResult?: boolean @@ -169,7 +170,7 @@ function createHarness( worktreeId: WORKTREE_ID, leafId: SOURCE_LEAF_ID, paneRuntimeId: 1, - ptyId: SOURCE_PTY_ID + ptyId: options.rendererPtyId === undefined ? SOURCE_PTY_ID : options.rendererPtyId } ] : [], @@ -283,6 +284,19 @@ describe('remote runtime terminal split authority', () => { }) }) + it('reveals a persisted split while its mounted source is still publishing its PTY binding', async () => { + const harness = createHarness(true, { rendererMounted: true, rendererPtyId: null }) + await harness.runtime.splitTerminal(harness.handle, { direction: 'vertical' }) + expect(harness.revealTerminalSession).toHaveBeenCalledOnce() + expect(harness.getSession().terminalLayoutsByTabId[TAB_ID]?.root?.type).toBe('split') + }) + + it('does not reveal a persisted split into a renderer bound to a different PTY', async () => { + const harness = createHarness(true, { rendererMounted: true, rendererPtyId: 'replacement' }) + await harness.runtime.splitTerminal(harness.handle, { direction: 'vertical' }) + expect(harness.revealTerminalSession).not.toHaveBeenCalled() + }) + it('splits a persisted tab without consulting an unmounted host renderer', async () => { const harness = createHarness() diff --git a/tests/e2e/terminal-tab-close-restart-persistence.spec.ts b/tests/e2e/terminal-tab-close-restart-persistence.spec.ts index 9df296d287b..20421dd1730 100644 --- a/tests/e2e/terminal-tab-close-restart-persistence.spec.ts +++ b/tests/e2e/terminal-tab-close-restart-persistence.spec.ts @@ -137,8 +137,19 @@ test('durable whole-tab close removes a split tab across restart', async (// oxl const secondLaunch = await session.launch() secondApp = secondLaunch.app await waitForSessionReady(secondLaunch.page) - const restoredWorktreeId = await attachRepoAndOpenTerminal(secondLaunch.page, repoPath) - expect(restoredWorktreeId).toBe(worktreeId) + // Activation deliberately creates a fresh terminal; observe passive hydration first. + await expect + .poll(() => + secondLaunch.page.evaluate( + (id) => + window.__store + ?.getState() + .allWorktrees() + .some((worktree) => worktree.id === id), + worktreeId + ) + ) + .toBe(true) // Why: wait past initial worktree effects so this checks resurrection, not // only the first hydrated frame before default-tab logic has run.