From acd501486dd1b18cd551b1bcd6449bb16c4c9769 Mon Sep 17 00:00:00 2001 From: Brennan Benson <79079362+brennanb2025@users.noreply.github.com> Date: Wed, 9 Sep 2026 00:17:27 -0700 Subject: [PATCH] Unify tab surface selection across workspace activation (#19635) * Unify tab surface selection across workspace activation * Cover the folder activation entry point and name its selection contract Rewrite the folder-workspace selection tests to drive setActiveFolderWorkspace, the entry point this PR rewrote; they previously went through setActiveWorktree and exercised the git-worktree projection instead, so none of them failed against pre-PR code. Add the layout-only ownership case. Hoist the remembered-file condition out of a three-deep nested ternary and pin the remembered agent-session/simulator cases that make it load-bearing, and replace the Parameters indirection with a named ActiveSurfaceSourceState. * Pin the folder-path openFiles fallback The folder path now reaches the shared openFiles fallback: with no groups, no layout and the remembered browser tab gone, an open file selects the editor surface instead of falling through to terminal. That is parity with the long-shipped git path, and nothing covered it. --------- Co-authored-by: Merge Sim --- ...-workspace-activation-and-activity.test.ts | 117 +++++++++ .../tabs/tab-selection-contract.test.ts | 228 ++++++++++++++++++ .../src/store/slices/tabs/tabs-surface.ts | 71 +++--- .../session/active-worktree-surface.ts | 111 +-------- .../session/set-active-folder-workspace.ts | 72 +----- 5 files changed, 399 insertions(+), 200 deletions(-) create mode 100644 src/renderer/src/store/slices/tabs/tab-selection-contract.test.ts diff --git a/src/renderer/src/store/slices/folder-workspace-activation-and-activity.test.ts b/src/renderer/src/store/slices/folder-workspace-activation-and-activity.test.ts index 35e9397c1a5..405007067d7 100644 --- a/src/renderer/src/store/slices/folder-workspace-activation-and-activity.test.ts +++ b/src/renderer/src/store/slices/folder-workspace-activation-and-activity.test.ts @@ -47,6 +47,29 @@ function makeFolderWorkspace(overrides: Partial = {}): FolderWo } } +function rememberedBrowserSurface(workspaceKey: string): Partial { + return { + browserTabsByWorktree: { + [workspaceKey]: [ + { + id: 'remembered', + worktreeId: workspaceKey, + url: 'about:blank', + title: 'Browser', + loading: false, + faviconUrl: null, + canGoBack: false, + canGoForward: false, + loadError: null, + createdAt: 1 + } + ] + }, + activeBrowserTabIdByWorktree: { [workspaceKey]: 'remembered' }, + activeTabTypeByWorktree: { [workspaceKey]: 'browser' } + } +} + type FolderWorkspaceUpdateArgs = { folderWorkspaceId: string updates: Partial @@ -121,6 +144,100 @@ describe('folder workspace generic activation and activity', () => { }) }) + it.each([ + ['simulator', 'local'], + ['agent-session', 'local'], + ['simulator', 'ssh:test-host'], + ['agent-session', 'ssh:test-host'] + ] as const)( + 'restores a folder %s tab on %s using its concrete visible type', + (contentType, executionHostId) => { + const folder = makeFolderWorkspace({ executionHostId }) + const workspaceKey = folderWorkspaceKey(folder.id) + const store = seedLocalFolderStore(folder) + store.getState().createUnifiedTab(workspaceKey, contentType, { id: 'selected' }) + store.setState({ activeTabTypeByWorktree: { [workspaceKey]: 'editor' } }) + + store.getState().setActiveFolderWorkspace(folder.id, executionHostId) + + expect(store.getState().activeWorkspaceExecutionHostId).toBe(executionHostId) + expect(store.getState().activeTabType).toBe(contentType) + expect(store.getState().activeTabTypeByWorktree[workspaceKey]).toBe(contentType) + expect(store.getState().getActiveTab(workspaceKey)?.id).toBe('selected') + } + ) + + it('does not let remembered browser state select content in an empty folder group', () => { + const folder = makeFolderWorkspace() + const workspaceKey = folderWorkspaceKey(folder.id) + const store = seedLocalFolderStore(folder) + store.setState({ + ...rememberedBrowserSurface(workspaceKey), + groupsByWorktree: { + [workspaceKey]: [ + { + id: 'empty', + worktreeId: workspaceKey, + activeTabId: null, + tabOrder: [] + } + ] + }, + activeGroupIdByWorktree: { [workspaceKey]: 'empty' } + } as Partial) + + store.getState().setActiveFolderWorkspace(folder.id) + + expect(store.getState().activeTabType).toBe('terminal') + expect(store.getState().activeBrowserTabId).toBe('remembered') + }) + + it('keeps layout-only folder ownership above remembered browser state', () => { + const folder = makeFolderWorkspace() + const workspaceKey = folderWorkspaceKey(folder.id) + const store = seedLocalFolderStore(folder) + store.setState({ + ...rememberedBrowserSurface(workspaceKey), + groupsByWorktree: {}, + layoutByWorktree: { [workspaceKey]: { type: 'leaf', groupId: 'pending' } } + } as Partial) + + store.getState().setActiveFolderWorkspace(folder.id) + + expect(store.getState().activeTabType).toBe('terminal') + expect(store.getState().activeBrowserTabId).toBe('remembered') + }) + + it('falls back to an open file when nothing else owns the folder surface', () => { + const folder = makeFolderWorkspace() + const workspaceKey = folderWorkspaceKey(folder.id) + const store = seedLocalFolderStore(folder) + store.setState({ + groupsByWorktree: {}, + layoutByWorktree: {}, + // Why: the remembered browser tab is gone, so only the open file is left to show. + activeBrowserTabIdByWorktree: { [workspaceKey]: 'closed' }, + browserTabsByWorktree: { [workspaceKey]: [] }, + activeTabTypeByWorktree: { [workspaceKey]: 'browser' }, + openFiles: [ + { + id: 'fallback-file', + worktreeId: workspaceKey, + filePath: '/workspace/folder/file', + relativePath: 'file', + language: 'plaintext', + isDirty: false, + mode: 'edit' + } + ] + } as Partial) + + store.getState().setActiveFolderWorkspace(folder.id) + + expect(store.getState().activeTabType).toBe('editor') + expect(store.getState().activeFileId).toBe('fallback-file') + }) + it('coalesces repeated activity persistence while keeping local activity current', async () => { vi.useFakeTimers() vi.setSystemTime(1_000) diff --git a/src/renderer/src/store/slices/tabs/tab-selection-contract.test.ts b/src/renderer/src/store/slices/tabs/tab-selection-contract.test.ts new file mode 100644 index 00000000000..3100301b50a --- /dev/null +++ b/src/renderer/src/store/slices/tabs/tab-selection-contract.test.ts @@ -0,0 +1,228 @@ +import { describe, expect, it } from 'vitest' +import type { Tab, TabContentType } from '../../../../../shared/tab-types' +import { buildHydratedTabState } from '../tabs-hydration' +import { resolveActivatedWorktreeSurface } from '../worktrees/session/active-worktree-surface' +import { deriveActiveSurfaceForWorktree } from './tabs-surface' + +type SelectionState = Parameters[0] + +const workspace = 'repo::/workspace' +function selectedTab(contentType: TabContentType): Tab { + return { + id: 'selected', + entityId: 'selected-entity', + groupId: 'group', + worktreeId: workspace, + contentType, + label: 'Selected', + customLabel: null, + color: null, + sortOrder: 0, + createdAt: 1 + } +} + +function selectionState(tab: Tab | null): SelectionState { + return { + rightSidebarExplorerViewByWorktree: {}, + activeGroupIdByWorktree: { [workspace]: 'group' }, + groupsByWorktree: { + [workspace]: [ + { + id: 'group', + worktreeId: workspace, + activeTabId: tab?.id ?? null, + tabOrder: tab ? [tab.id] : [] + } + ] + }, + unifiedTabsByWorktree: { [workspace]: tab ? [tab] : [] }, + layoutByWorktree: {}, + activeTabIdByWorktree: { [workspace]: 'remembered-terminal' }, + activeFileIdByWorktree: { [workspace]: 'remembered-file' }, + activeBrowserTabIdByWorktree: { [workspace]: 'remembered-browser' }, + activeTabTypeByWorktree: { [workspace]: 'browser' }, + tabsByWorktree: { + [workspace]: [ + { + id: 'remembered-terminal', + worktreeId: workspace, + ptyId: null, + title: 'Terminal', + customTitle: null, + color: null, + sortOrder: 0, + createdAt: 1 + } + ] + }, + browserTabsByWorktree: { + [workspace]: [ + { + id: 'remembered-browser', + worktreeId: workspace, + url: 'about:blank', + title: 'Browser', + loading: false, + faviconUrl: null, + canGoBack: false, + canGoForward: false, + loadError: null, + createdAt: 1 + } + ] + }, + openFiles: [ + { + id: 'remembered-file', + worktreeId: workspace, + filePath: '/workspace/file', + relativePath: 'file', + language: 'plaintext', + isDirty: false, + mode: 'edit' + } + ] + } +} + +function activate(state: SelectionState) { + const { restoredRightSidebarExplorerView: _view, ...surface } = resolveActivatedWorktreeSurface( + state, + workspace, + undefined, + null + ) + return surface +} + +describe('tab selection and hydration ownership', () => { + it.each([ + ['terminal', 'terminal'], + ['editor', 'editor'], + ['diff', 'editor'], + ['conflict-review', 'editor'], + ['check-details', 'editor'], + ['browser', 'browser'], + ['simulator', 'simulator'], + ['agent-session', 'agent-session'] + ] as const)( + 'projects %s selection while retaining other remembered surfaces', + (kind, visible) => { + const state = selectionState(selectedTab(kind)) + const expected = { + activeTabType: visible, + activeTabId: kind === 'terminal' ? 'selected-entity' : 'remembered-terminal', + activeFileId: visible === 'editor' ? 'selected-entity' : 'remembered-file', + activeBrowserTabId: kind === 'browser' ? 'selected-entity' : 'remembered-browser' + } + expect(deriveActiveSurfaceForWorktree(state, workspace)).toEqual(expected) + expect(activate(state)).toEqual(expected) + } + ) + + it('keeps empty groups authoritative over remembered browser/editor surfaces', () => { + const state = selectionState(null) + expect(activate(state).activeTabType).toBe('terminal') + expect(deriveActiveSurfaceForWorktree(state, workspace).activeTabType).toBe('terminal') + }) + + it('keeps layout-only ownership authoritative during staged hydration', () => { + const state = selectionState(null) + state.groupsByWorktree = {} + state.layoutByWorktree = { [workspace]: { type: 'leaf', groupId: 'pending' } } + expect(activate(state).activeTabType).toBe('terminal') + expect(deriveActiveSurfaceForWorktree(state, workspace).activeTabType).toBe('terminal') + }) + + it('distinguishes workspace restoration from group-focus legacy fallback', () => { + const state = selectionState(null) + state.groupsByWorktree = {} + state.activeTabTypeByWorktree[workspace] = 'terminal' + expect(activate(state).activeTabType).toBe('terminal') + expect(deriveActiveSurfaceForWorktree(state, workspace).activeTabType).toBe('browser') + }) + + it('does not select a preferred tab owned by another group', () => { + const state = selectionState(selectedTab('browser')) + state.unifiedTabsByWorktree[workspace].push({ + ...selectedTab('editor'), + id: 'foreign', + groupId: 'other' + }) + expect(resolveActivatedWorktreeSurface(state, workspace, 'foreign', null).activeTabType).toBe( + 'terminal' + ) + }) + + it('resolves stale active-group IDs to the first group consistently', () => { + const state = selectionState(selectedTab('simulator')) + state.activeGroupIdByWorktree[workspace] = 'removed' + expect(activate(state).activeTabType).toBe('simulator') + expect(deriveActiveSurfaceForWorktree(state, workspace).activeTabType).toBe('simulator') + }) + + it('requires group ownership even for an explicit preferred tab during hydration', () => { + const state = selectionState(selectedTab('simulator')) + state.groupsByWorktree = {} + state.activeTabTypeByWorktree[workspace] = 'terminal' + expect(resolveActivatedWorktreeSurface(state, workspace, 'selected', null).activeTabType).toBe( + 'terminal' + ) + }) + + it('honors a preferred tab in the selected group without mutating selection', () => { + const state = selectionState(selectedTab('browser')) + const preferred = { ...selectedTab('simulator'), id: 'preferred' } + state.unifiedTabsByWorktree[workspace].push(preferred) + state.groupsByWorktree[workspace][0].tabOrder.push(preferred.id) + expect( + resolveActivatedWorktreeSurface(state, workspace, preferred.id, null).activeTabType + ).toBe('simulator') + expect(state.groupsByWorktree[workspace][0].activeTabId).toBe('selected') + }) + + it.each([ + ['terminal', 'terminal', 'remembered-file'], + ['editor', 'editor', 'remembered-file'], + ['browser', 'browser', 'remembered-file'], + // Why: nothing renders a remembered agent-session/simulator once its tab is gone, so the browser + // surface takes over and must not leave the remembered file selected underneath it. + ['agent-session', 'browser', null], + ['simulator', 'browser', null] + ] as const)( + 'projects legacy %s memory as %s when unified groups are absent', + (activeTabType, visible, activeFileId) => { + const state = selectionState(null) + state.groupsByWorktree = {} + state.activeTabTypeByWorktree[workspace] = activeTabType + expect(activate(state)).toEqual({ + activeTabType: visible, + activeTabId: 'remembered-terminal', + activeFileId, + activeBrowserTabId: 'remembered-browser' + }) + } + ) + + it('hydrates unified selection without allowing conflicting legacy memories to choose it', () => { + const tab = selectedTab('simulator') + const state = selectionState(tab) + const session = { + activeRepoId: null, + activeWorktreeId: workspace, + activeTabId: 'remembered-terminal', + tabsByWorktree: {}, + terminalLayoutsByTabId: {}, + unifiedTabs: state.unifiedTabsByWorktree, + tabGroups: state.groupsByWorktree, + activeGroupIdByWorktree: state.activeGroupIdByWorktree, + activeTabTypeByWorktree: { [workspace]: 'browser' as const }, + activeTabIdByWorktree: state.activeTabIdByWorktree + } + const before = structuredClone(session) + const hydrated = buildHydratedTabState(session, new Set([workspace])) + expect(activate({ ...state, ...hydrated }).activeTabType).toBe('simulator') + expect(session).toEqual(before) + }) +}) diff --git a/src/renderer/src/store/slices/tabs/tabs-surface.ts b/src/renderer/src/store/slices/tabs/tabs-surface.ts index b870ca7bb32..793d0437fca 100644 --- a/src/renderer/src/store/slices/tabs/tabs-surface.ts +++ b/src/renderer/src/store/slices/tabs/tabs-surface.ts @@ -2,22 +2,26 @@ import type { AppState } from '../../types' import { toVisibleTabType } from '../../../../../shared/tab-types' import type { WorkspaceVisibleTabType } from '../../../../../shared/tab-types' +export type ActiveSurfaceSourceState = Pick< + AppState, + | 'activeBrowserTabIdByWorktree' + | 'activeFileIdByWorktree' + | 'activeGroupIdByWorktree' + | 'activeTabIdByWorktree' + | 'activeTabTypeByWorktree' + | 'browserTabsByWorktree' + | 'groupsByWorktree' + | 'layoutByWorktree' + | 'openFiles' + | 'tabsByWorktree' + | 'unifiedTabsByWorktree' +> + export function deriveActiveSurfaceForWorktree( - state: Pick< - AppState, - | 'activeBrowserTabIdByWorktree' - | 'activeFileIdByWorktree' - | 'activeGroupIdByWorktree' - | 'activeTabIdByWorktree' - | 'browserTabsByWorktree' - | 'groupsByWorktree' - | 'layoutByWorktree' - | 'openFiles' - | 'tabsByWorktree' - | 'unifiedTabsByWorktree' - >, + state: ActiveSurfaceSourceState, worktreeId: string, - preferredGroupId?: string | null + preferredGroupId?: string | null, + options?: { preferredTabId?: string; legacySelection?: 'remembered-type' } ): { activeBrowserTabId: string | null activeFileId: string | null @@ -28,10 +32,12 @@ export function deriveActiveSurfaceForWorktree( const activeGroupId = preferredGroupId ?? state.activeGroupIdByWorktree[worktreeId] ?? null const activeGroup = (activeGroupId ? groups.find((group) => group.id === activeGroupId) : null) ?? groups[0] ?? null + const activeUnifiedTabId = options?.preferredTabId ?? activeGroup?.activeTabId const activeUnifiedTab = - activeGroup?.activeTabId != null + activeUnifiedTabId != null ? ((state.unifiedTabsByWorktree[worktreeId] ?? []).find( - (tab) => tab.id === activeGroup.activeTabId && tab.groupId === activeGroup.id + (tab) => + tab.id === activeUnifiedTabId && activeGroup != null && tab.groupId === activeGroup.id ) ?? null) : null const restoredFileId = state.activeFileIdByWorktree[worktreeId] ?? null @@ -50,6 +56,14 @@ export function deriveActiveSurfaceForWorktree( : false const hasGroupOwnedSurface = groups.length > 0 || Boolean(state.layoutByWorktree[worktreeId]) + const restoreLegacyType = options?.legacySelection === 'remembered-type' + const restoredTabType = restoreLegacyType + ? (state.activeTabTypeByWorktree[worktreeId] ?? 'terminal') + : null + // Why: only a remembered browser type — or group focus, which remembers no type at all — may keep + // the remembered file selected under the browser surface; a stale agent-session/simulator clears it. + const keepRememberedFileUnderBrowser = restoredTabType === null || restoredTabType === 'browser' + let activeFileId: string | null let activeBrowserTabId: string | null let activeTabType: WorkspaceVisibleTabType @@ -76,8 +90,16 @@ export function deriveActiveSurfaceForWorktree( activeBrowserTabId = browserTabStillOpen ? restoredBrowserTabId : (browserTabs[0]?.id ?? null) // Why: focusing an empty split should target its default terminal area, not the previously active browser/editor in another group. activeTabType = 'terminal' - } else if (browserTabStillOpen) { + } else if (restoredTabType === 'terminal') { activeFileId = fileStillOpen ? restoredFileId : null + activeBrowserTabId = browserTabStillOpen ? restoredBrowserTabId : (browserTabs[0]?.id ?? null) + activeTabType = 'terminal' + } else if (restoredTabType === 'editor' && fileStillOpen) { + activeFileId = restoredFileId + activeBrowserTabId = browserTabStillOpen ? restoredBrowserTabId : (browserTabs[0]?.id ?? null) + activeTabType = 'editor' + } else if (browserTabStillOpen) { + activeFileId = keepRememberedFileUnderBrowser && fileStillOpen ? restoredFileId : null activeBrowserTabId = restoredBrowserTabId activeTabType = 'browser' } else if (fileStillOpen) { @@ -106,20 +128,7 @@ export function deriveActiveSurfaceForWorktree( } export function buildActiveSurfacePatch( - state: Pick< - AppState, - | 'activeBrowserTabIdByWorktree' - | 'activeFileIdByWorktree' - | 'activeGroupIdByWorktree' - | 'activeTabIdByWorktree' - | 'activeTabTypeByWorktree' - | 'browserTabsByWorktree' - | 'groupsByWorktree' - | 'layoutByWorktree' - | 'openFiles' - | 'tabsByWorktree' - | 'unifiedTabsByWorktree' - >, + state: ActiveSurfaceSourceState, worktreeId: string, preferredGroupId?: string | null ): Pick< diff --git a/src/renderer/src/store/slices/worktrees/session/active-worktree-surface.ts b/src/renderer/src/store/slices/worktrees/session/active-worktree-surface.ts index 6dcfc62d155..3e185435551 100644 --- a/src/renderer/src/store/slices/worktrees/session/active-worktree-surface.ts +++ b/src/renderer/src/store/slices/worktrees/session/active-worktree-surface.ts @@ -1,9 +1,10 @@ import type { AppState } from '../../../types' import type { WorkspaceVisibleTabType } from '../../../../../../shared/tab-types' -import { toVisibleTabType } from '../../../../../../shared/tab-types' +import type { ActiveSurfaceSourceState } from '../../tabs/tabs-surface' +import { deriveActiveSurfaceForWorktree } from '../../tabs/tabs-surface' export function resolveActivatedWorktreeSurface( - s: AppState, + s: ActiveSurfaceSourceState & Pick, worktreeId: string, preferredActiveUnifiedTabId: string | undefined, reconciledActiveTabId: string | null @@ -16,107 +17,11 @@ export function resolveActivatedWorktreeSurface( activeTabType: WorkspaceVisibleTabType activeTabId: string | null } { - // Why: Search lives under Explorer, so the files/search sub-route must switch with the worktree, not leak the prior one. - const restoredRightSidebarExplorerView = - s.rightSidebarExplorerViewByWorktree?.[worktreeId] ?? 'files' - const restoredFileId = s.activeFileIdByWorktree[worktreeId] ?? null - const restoredBrowserTabId = s.activeBrowserTabIdByWorktree[worktreeId] ?? null - const restoredTabType = s.activeTabTypeByWorktree[worktreeId] ?? 'terminal' - const activeGroupId = - s.activeGroupIdByWorktree[worktreeId] ?? s.groupsByWorktree[worktreeId]?.[0]?.id ?? null - const activeGroup = activeGroupId - ? ((s.groupsByWorktree[worktreeId] ?? []).find((group) => group.id === activeGroupId) ?? null) - : null - const activeUnifiedTabId = - preferredActiveUnifiedTabId ?? reconciledActiveTabId ?? activeGroup?.activeTabId ?? null - const activeUnifiedTab = - activeUnifiedTabId != null - ? ((s.unifiedTabsByWorktree[worktreeId] ?? []).find( - (tab) => tab.id === activeUnifiedTabId && (!activeGroup || tab.groupId === activeGroup.id) - ) ?? null) - : null - // Verify the restored file still exists in openFiles - const fileStillOpen = restoredFileId - ? s.openFiles.some((f) => f.id === restoredFileId && f.worktreeId === worktreeId) - : false - const browserTabs = s.browserTabsByWorktree[worktreeId] ?? [] - const browserTabStillOpen = restoredBrowserTabId - ? browserTabs.some((tab) => tab.id === restoredBrowserTabId) - : false - const hasGroupOwnedSurface = - (s.groupsByWorktree[worktreeId]?.length ?? 0) > 0 || Boolean(s.layoutByWorktree[worktreeId]) - - // Why: restore from the reconciled tab-group model first; preferring legacy fallbacks can show a blank worktree. - let activeFileId: string | null - let activeBrowserTabId: string | null - let activeTabType: WorkspaceVisibleTabType - if (activeUnifiedTab) { - activeFileId = - activeUnifiedTab.contentType === 'editor' || - activeUnifiedTab.contentType === 'diff' || - activeUnifiedTab.contentType === 'conflict-review' || - activeUnifiedTab.contentType === 'check-details' - ? activeUnifiedTab.entityId - : fileStillOpen - ? restoredFileId - : null - activeBrowserTabId = - activeUnifiedTab.contentType === 'browser' - ? activeUnifiedTab.entityId - : browserTabStillOpen - ? restoredBrowserTabId - : (browserTabs[0]?.id ?? null) - activeTabType = toVisibleTabType(activeUnifiedTab.contentType) - } else if (hasGroupOwnedSurface) { - activeFileId = fileStillOpen ? restoredFileId : null - activeBrowserTabId = browserTabStillOpen ? restoredBrowserTabId : (browserTabs[0]?.id ?? null) - activeTabType = 'terminal' - } else if (restoredTabType === 'terminal') { - activeFileId = fileStillOpen ? restoredFileId : null - activeBrowserTabId = browserTabStillOpen ? restoredBrowserTabId : (browserTabs[0]?.id ?? null) - activeTabType = 'terminal' - } else if (restoredTabType === 'browser' && browserTabStillOpen) { - activeFileId = fileStillOpen ? restoredFileId : null - activeBrowserTabId = restoredBrowserTabId - activeTabType = 'browser' - } else if (restoredTabType === 'editor' && fileStillOpen) { - activeFileId = restoredFileId - activeBrowserTabId = browserTabStillOpen ? restoredBrowserTabId : (browserTabs[0]?.id ?? null) - activeTabType = 'editor' - } else if (browserTabStillOpen) { - activeFileId = null - activeBrowserTabId = restoredBrowserTabId - activeTabType = 'browser' - } else if (fileStillOpen) { - activeFileId = restoredFileId - activeBrowserTabId = browserTabs[0]?.id ?? null - activeTabType = 'editor' - } else { - const fallbackFile = s.openFiles.find((f) => f.worktreeId === worktreeId) - const fallbackBrowserTab = browserTabs[0] ?? null - activeFileId = fallbackFile?.id ?? null - activeBrowserTabId = browserTabStillOpen - ? restoredBrowserTabId - : (fallbackBrowserTab?.id ?? null) - activeTabType = fallbackFile ? 'editor' : fallbackBrowserTab ? 'browser' : 'terminal' - } - - // Why: restore the last-active terminal tab so the user returns to where they left, not tab 0. - const restoredTabId = s.activeTabIdByWorktree[worktreeId] ?? null - const worktreeTabs = s.tabsByWorktree[worktreeId] ?? [] - const tabStillExists = restoredTabId ? worktreeTabs.some((t) => t.id === restoredTabId) : false - const activeTabId = - activeUnifiedTab?.contentType === 'terminal' - ? activeUnifiedTab.entityId - : tabStillExists - ? restoredTabId - : (worktreeTabs[0]?.id ?? null) - return { - restoredRightSidebarExplorerView, - activeFileId, - activeBrowserTabId, - activeTabType, - activeTabId + restoredRightSidebarExplorerView: s.rightSidebarExplorerViewByWorktree?.[worktreeId] ?? 'files', + ...deriveActiveSurfaceForWorktree(s, worktreeId, undefined, { + legacySelection: 'remembered-type', + preferredTabId: preferredActiveUnifiedTabId ?? reconciledActiveTabId ?? undefined + }) } } diff --git a/src/renderer/src/store/slices/worktrees/session/set-active-folder-workspace.ts b/src/renderer/src/store/slices/worktrees/session/set-active-folder-workspace.ts index 1ca9f11ee5a..d2b5af79114 100644 --- a/src/renderer/src/store/slices/worktrees/session/set-active-folder-workspace.ts +++ b/src/renderer/src/store/slices/worktrees/session/set-active-folder-workspace.ts @@ -8,6 +8,7 @@ import { folderWorkspaceMatchesHost } from '../listing/detected-worktree-meta' import { shouldDeferActivationTerminalPrep } from './activation-terminal-prep' +import { deriveActiveSurfaceForWorktree } from '../../tabs/tabs-surface' export function createSetActiveFolderWorkspace( set: WorktreeSliceSet, @@ -28,72 +29,11 @@ export function createSetActiveFolderWorkspace( const reconciledActiveTabId = get().reconcileWorktreeTabModel(workspaceKey).activeRenderableTabId set((s) => { - const restoredFileId = s.activeFileIdByWorktree[workspaceKey] ?? null - const restoredBrowserTabId = s.activeBrowserTabIdByWorktree[workspaceKey] ?? null - const restoredTabType = s.activeTabTypeByWorktree[workspaceKey] ?? 'terminal' - const activeGroupId = - s.activeGroupIdByWorktree[workspaceKey] ?? s.groupsByWorktree[workspaceKey]?.[0]?.id ?? null - const activeGroup = activeGroupId - ? ((s.groupsByWorktree[workspaceKey] ?? []).find((group) => group.id === activeGroupId) ?? - null) - : null - const activeUnifiedTabId = reconciledActiveTabId ?? activeGroup?.activeTabId ?? null - const activeUnifiedTab = - activeUnifiedTabId != null - ? ((s.unifiedTabsByWorktree[workspaceKey] ?? []).find( - (tab) => - tab.id === activeUnifiedTabId && (!activeGroup || tab.groupId === activeGroup.id) - ) ?? null) - : null - const fileStillOpen = restoredFileId - ? s.openFiles.some((file) => file.id === restoredFileId && file.worktreeId === workspaceKey) - : false - const browserTabs = s.browserTabsByWorktree[workspaceKey] ?? [] - const browserTabStillOpen = restoredBrowserTabId - ? browserTabs.some((tab) => tab.id === restoredBrowserTabId) - : false - const worktreeTabs = s.tabsByWorktree[workspaceKey] ?? [] - const restoredTabId = s.activeTabIdByWorktree[workspaceKey] ?? null - const tabStillExists = restoredTabId - ? worktreeTabs.some((tab) => tab.id === restoredTabId) - : false - const activeFileId = - activeUnifiedTab?.contentType === 'editor' || - activeUnifiedTab?.contentType === 'diff' || - activeUnifiedTab?.contentType === 'conflict-review' || - activeUnifiedTab?.contentType === 'check-details' - ? activeUnifiedTab.entityId - : fileStillOpen - ? restoredFileId - : null - const activeBrowserTabId = - activeUnifiedTab?.contentType === 'browser' - ? activeUnifiedTab.entityId - : browserTabStillOpen - ? restoredBrowserTabId - : (browserTabs[0]?.id ?? null) - const activeTabType = - activeUnifiedTab?.contentType === 'terminal' - ? 'terminal' - : activeUnifiedTab?.contentType === 'browser' - ? 'browser' - : activeUnifiedTab - ? 'editor' - : restoredTabType === 'browser' && browserTabStillOpen - ? 'browser' - : restoredTabType === 'editor' && fileStillOpen - ? 'editor' - : fileStillOpen - ? 'editor' - : browserTabs.length > 0 - ? 'browser' - : 'terminal' - const activeTabId = - activeUnifiedTab?.contentType === 'terminal' - ? activeUnifiedTab.entityId - : tabStillExists - ? restoredTabId - : (worktreeTabs[0]?.id ?? null) + const { activeFileId, activeBrowserTabId, activeTabType, activeTabId } = + deriveActiveSurfaceForWorktree(s, workspaceKey, undefined, { + legacySelection: 'remembered-type', + preferredTabId: reconciledActiveTabId ?? undefined + }) const nextEverActivated = s.everActivatedWorktreeIds.has(workspaceKey) ? s.everActivatedWorktreeIds : new Set([...s.everActivatedWorktreeIds, workspaceKey])