mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 08:02:28 +00:00
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<typeof ...> 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 <sim@local>
This commit is contained in:
co-authored by
Merge Sim
parent
5868fdc9e3
commit
acd501486d
@@ -47,6 +47,29 @@ function makeFolderWorkspace(overrides: Partial<FolderWorkspace> = {}): FolderWo
|
||||
}
|
||||
}
|
||||
|
||||
function rememberedBrowserSurface(workspaceKey: string): Partial<AppState> {
|
||||
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<FolderWorkspace>
|
||||
@@ -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<AppState>)
|
||||
|
||||
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<AppState>)
|
||||
|
||||
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<AppState>)
|
||||
|
||||
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)
|
||||
|
||||
@@ -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<typeof resolveActivatedWorktreeSurface>[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)
|
||||
})
|
||||
})
|
||||
@@ -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<
|
||||
|
||||
@@ -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<AppState, 'rightSidebarExplorerViewByWorktree'>,
|
||||
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
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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])
|
||||
|
||||
Reference in New Issue
Block a user