From 7556c51234d607fc79e7fe37c83cb31de8461730 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Thu, 30 Jul 2026 01:14:11 -0500 Subject: [PATCH] =?UTF-8?q?fix(desktop):=20focus=20the=20tab=20restored=20?= =?UTF-8?q?by=20undo-close=20(=E2=8C=98=E2=87=A7T)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adoption alone is silent, so reopenLastClosedTile only restored placement and left the tab behind the still-fronted workspace. Focus it after open. --- apps/desktop/src/store/session-states.test.ts | 93 ++++++++++++++++++- apps/desktop/src/store/session-states.ts | 7 +- 2 files changed, 96 insertions(+), 4 deletions(-) diff --git a/apps/desktop/src/store/session-states.test.ts b/apps/desktop/src/store/session-states.test.ts index 4133babb927..3bff4ba5208 100644 --- a/apps/desktop/src/store/session-states.test.ts +++ b/apps/desktop/src/store/session-states.test.ts @@ -1,7 +1,7 @@ -import { describe, expect, it } from 'vitest' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import type { ClientSessionState } from '@/app/types' -import { group, split } from '@/components/pane-shell/tree/model' +import { findGroupOfPane, group, split } from '@/components/pane-shell/tree/model' import { $layoutTree } from '@/components/pane-shell/tree/store' import { $selectedStoredSessionId } from '@/store/session' import type { SessionTile } from '@/store/session-states' @@ -138,3 +138,92 @@ describe('blankDraftTile', () => { expect(blankDraftTile([], {})).toBeNull() }) }) + +// ⌘⇧T used to only restore `$sessionTiles`. Adoption inserts silently +// (activate:false), so the tab came back behind the still-fronted workspace. +// Real path: register, adopt, focus — same as paneMirror + reopen. +describe('reopenLastClosedTile focuses the restored tab', () => { + beforeEach(() => { + window.localStorage.clear() + vi.resetModules() + }) + + afterEach(() => { + vi.resetModules() + }) + + async function setup() { + const tree = await import('@/components/pane-shell/tree/store') + const model = await import('@/components/pane-shell/tree/model') + const { registry } = await import('@/contrib/registry') + const session = await import('@/store/session') + const states = await import('@/store/session-states') + + registry.register({ + area: 'panes', + data: { placement: 'main', uncloseable: true }, + id: 'workspace', + render: () => null, + title: 'chat' + }) + + // panes ← $sessionTiles (paneMirror stub). Adoption is synchronous on + // register, so openSessionTile + focusOpenSession works the same tick. + const registered = new Map void>() + + const syncTiles = () => { + const wanted = new Set(states.$sessionTiles.get().map(t => t.storedSessionId)) + + for (const id of wanted) { + if (registered.has(id)) { + continue + } + + registered.set( + id, + registry.register({ + area: 'panes', + data: { dock: { pane: 'workspace', pos: 'center' }, placement: 'main' }, + id: tilePane(id), + render: () => null, + title: id + }) + ) + } + + for (const [id, dispose] of registered) { + if (!wanted.has(id)) { + dispose() + registered.delete(id) + tree.removeTreePane(tilePane(id)) + } + } + } + + states.$sessionTiles.listen(syncTiles) + tree.watchContributedPanes() + session.$selectedStoredSessionId.set('primary') + tree.declareDefaultTree(model.group(['workspace'], { active: 'workspace', id: 'grp-main' })) + + states.openSessionTile('closed', 'center', 'workspace') + states.focusOpenSession('closed') + tree.noteActiveTreeGroup('grp-main') + expect(findGroupOfPane(tree.$layoutTree.get()!, tilePane('closed'))?.active).toBe(tilePane('closed')) + + return { states, tree } + } + + it('fronts the restored tab after ⌘⇧T', async () => { + const { states, tree } = await setup() + + states.closeSessionTile('closed') + expect(states.$sessionTiles.get().some(t => t.storedSessionId === 'closed')).toBe(false) + expect(findGroupOfPane(tree.$layoutTree.get()!, 'workspace')?.active).toBe('workspace') + + states.reopenLastClosedTile() + + expect(states.$sessionTiles.get().some(t => t.storedSessionId === 'closed')).toBe(true) + expect(findGroupOfPane(tree.$layoutTree.get()!, tilePane('closed'))?.active).toBe(tilePane('closed')) + expect(tree.$activeTreeGroup.get()).toBe('grp-main') + }) +}) diff --git a/apps/desktop/src/store/session-states.ts b/apps/desktop/src/store/session-states.ts index 5d3f41c4fc6..ebf58f9819c 100644 --- a/apps/desktop/src/store/session-states.ts +++ b/apps/desktop/src/store/session-states.ts @@ -702,8 +702,10 @@ export function discardSessionTile(storedSessionId: string) { saveTiles($sessionTiles.get().filter(t => t.storedSessionId !== storedSessionId)) } -/** ⌘⇧T — reopen the most recently closed tab where it was. Skips ids that are - * live again (reopened, or now the primary). */ +/** ⌘⇧T — reopen the most recently closed tab where it was, then focus it. + * Adoption alone is silent (won't steal the active tab), so restore has to + * front the pane explicitly. Skips ids that are live again (reopened / now + * the primary). */ export function reopenLastClosedTile(): void { const stack = closedStack() @@ -716,6 +718,7 @@ export function reopenLastClosedTile(): void { if (!$sessionTiles.get().some(t => t.storedSessionId === storedSessionId)) { openSessionTile(storedSessionId, tile.dir, tile.anchor, tile.before) + focusOpenSession(storedSessionId) return }