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 }