From 2d511f55aa66f6f48fdf66b5d539bcc119e1318d Mon Sep 17 00:00:00 2001 From: Teknium <127238744+teknium1@users.noreply.github.com> Date: Tue, 18 Aug 2026 01:17:42 -0700 Subject: [PATCH] fix(desktop): closing main in a side-by-side layout promotes the neighboring session instead of spawning a fresh draft (#88924) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit nextSessionTileForWorkspace() only walked tabs stacked WITH the workspace tab. In a side-by-side layout (session tiles in their own zones — db's three-pane report), the walk found nothing, closeWorkspaceTab() skipped promotion, and requestFreshSession() dropped main to a new draft: closing a pane read as 'it gave me a new session'. A tile in any zone now promotes (its zone collapses via the tile close), so Close is also the path from N panes back to 1. Ghost panes whose tile is gone still never promote. --- apps/desktop/src/store/session-states.test.ts | 42 +++++++++++++++++++ apps/desktop/src/store/session-states.ts | 11 +++++ 2 files changed, 53 insertions(+) diff --git a/apps/desktop/src/store/session-states.test.ts b/apps/desktop/src/store/session-states.test.ts index 9620f8404a..b3173aacd0 100644 --- a/apps/desktop/src/store/session-states.test.ts +++ b/apps/desktop/src/store/session-states.test.ts @@ -11,6 +11,7 @@ import { blankDraftTile, focusedSessionNeedsRoute, markSelectionRestore, + nextSessionTileForWorkspace, orderTilesByTree, releaseSessionTranscript, resetTileRuntimeBindings, @@ -111,6 +112,47 @@ describe('selectionHomesToWorkspace', () => { }) }) +describe('nextSessionTileForWorkspace (⌘W promotion source)', () => { + afterEach(() => { + $layoutTree.set(null) + $sessionTiles.set([]) + }) + + it('prefers a tile stacked WITH the workspace tab (nearest-out)', () => { + $layoutTree.set(group(['workspace', tilePane('a'), tilePane('b')], { active: 'workspace', id: 'main' })) + $sessionTiles.set([tile('a'), tile('b')]) + + expect(nextSessionTileForWorkspace()).toBe('a') + }) + + it('side-by-side layout: a tile in ANOTHER zone still promotes instead of dropping main to a fresh draft (#88924)', () => { + // main zone holds only the workspace; the session tile lives in its own + // zone beside it — db's three-pane report shape. + $layoutTree.set( + split('row', [ + group(['workspace'], { active: 'workspace', id: 'main' }), + group([tilePane('side')], { active: tilePane('side'), id: 'right' }) + ]) + ) + $sessionTiles.set([tile('side')]) + + expect(nextSessionTileForWorkspace()).toBe('side') + }) + + it('returns null when no live tile exists anywhere in the tree', () => { + $layoutTree.set( + split('row', [ + group(['workspace'], { active: 'workspace', id: 'main' }), + group([tilePane('stale')], { active: tilePane('stale'), id: 'right' }) + ]) + ) + // Pane persisted in the tree but its tile is gone — must not promote a ghost. + $sessionTiles.set([]) + + expect(nextSessionTileForWorkspace()).toBe(null) + }) +}) + describe('boot-restore selection homing (⌘R tab persistence)', () => { const mainGroup = () => group(['workspace', tilePane('t')], { active: tilePane('t'), id: 'main' }) diff --git a/apps/desktop/src/store/session-states.ts b/apps/desktop/src/store/session-states.ts index 0474e208a3..9d98bced1b 100644 --- a/apps/desktop/src/store/session-states.ts +++ b/apps/desktop/src/store/session-states.ts @@ -818,6 +818,17 @@ export function nextSessionTileForWorkspace(): null | string { } } + // Nothing stacked WITH main — but a session tile in another zone can still + // shift in. Without this, closing main in a side-by-side layout skipped + // promotion entirely and dropped to a fresh "New session" draft, which read + // as "closing a pane gave me a new session" (#88924). Promoting the tile + // also collapses its zone, so Close is how a multi-pane layout shrinks. + for (const tile of tiles) { + if (tree && findGroupOfPane(tree, `${TILE_PANE_PREFIX}${tile.storedSessionId}`)) { + return tile.storedSessionId + } + } + return null }