mirror of
https://github.com/stablyai/orca.git
synced 2026-10-18 16:03:17 +00:00
fix(session): retain terminal-free degraded workspaces
This commit is contained in:
@@ -117,9 +117,25 @@ function makeDegradedRepoSession(): WorkspaceSessionState {
|
||||
}
|
||||
}
|
||||
|
||||
function hydrateWithFailedScan(
|
||||
function makeTerminalFreeDegradedRepoSession(): WorkspaceSessionState {
|
||||
const session = makeDegradedRepoSession()
|
||||
session.activeTabId = null
|
||||
session.tabsByWorktree = {}
|
||||
session.unifiedTabs![WORKTREE_ID] = session.unifiedTabs![WORKTREE_ID].filter(
|
||||
(tab) => tab.contentType !== 'terminal'
|
||||
)
|
||||
session.tabGroups![WORKTREE_ID] = session.tabGroups![WORKTREE_ID].map((group) => ({
|
||||
...group,
|
||||
tabOrder: [EDITOR_FILE_ID, BROWSER_ID],
|
||||
recentTabIds: [EDITOR_FILE_ID, BROWSER_ID]
|
||||
}))
|
||||
return session
|
||||
}
|
||||
|
||||
function hydrateWithRepoScan(
|
||||
store: ReturnType<typeof createTestStore>,
|
||||
session: WorkspaceSessionState
|
||||
session: WorkspaceSessionState,
|
||||
authoritative = false
|
||||
): void {
|
||||
store.setState({
|
||||
repos: [{ id: 'repo1', path: '/repo1', displayName: 'Repo 1', badgeColor: '#000', addedAt: 0 }],
|
||||
@@ -127,8 +143,8 @@ function hydrateWithFailedScan(
|
||||
detectedWorktreesByRepo: {
|
||||
repo1: {
|
||||
repoId: 'repo1',
|
||||
authoritative: false,
|
||||
source: 'metadata-fallback',
|
||||
authoritative,
|
||||
source: authoritative ? 'git' : 'metadata-fallback',
|
||||
worktrees: []
|
||||
}
|
||||
}
|
||||
@@ -141,14 +157,14 @@ function hydrateWithFailedScan(
|
||||
|
||||
it('keeps tab, editor, and browser chrome through degraded hydration and persistence', () => {
|
||||
const firstStore = createTestStore()
|
||||
hydrateWithFailedScan(firstStore, makeDegradedRepoSession())
|
||||
hydrateWithRepoScan(firstStore, makeDegradedRepoSession())
|
||||
firstStore.setState({ workspaceSessionReady: true })
|
||||
firstStore.getState().setHydrationSucceeded(true)
|
||||
expect(shouldPersistWorkspaceSession(firstStore.getState())).toBe(true)
|
||||
|
||||
const persisted = buildWorkspaceSessionPayload(firstStore.getState())
|
||||
const restoredStore = createTestStore()
|
||||
hydrateWithFailedScan(restoredStore, persisted)
|
||||
hydrateWithRepoScan(restoredStore, persisted)
|
||||
|
||||
const restored = restoredStore.getState()
|
||||
expect(restored.unifiedTabsByWorktree[WORKTREE_ID]?.map((tab) => tab.id)).toEqual([
|
||||
@@ -159,3 +175,37 @@ it('keeps tab, editor, and browser chrome through degraded hydration and persist
|
||||
expect(restored.openFiles.map((file) => file.id)).toEqual([EDITOR_FILE_ID])
|
||||
expect(restored.browserTabsByWorktree[WORKTREE_ID]?.map((tab) => tab.id)).toEqual([BROWSER_ID])
|
||||
})
|
||||
|
||||
it('keeps a terminal-free degraded workspace selected through hydration and persistence', () => {
|
||||
const firstStore = createTestStore()
|
||||
hydrateWithRepoScan(firstStore, makeTerminalFreeDegradedRepoSession())
|
||||
|
||||
const first = firstStore.getState()
|
||||
expect(first.activeWorktreeId).toBe(WORKTREE_ID)
|
||||
expect(first.activeTabType).toBe('browser')
|
||||
|
||||
firstStore.setState({ workspaceSessionReady: true })
|
||||
firstStore.getState().setHydrationSucceeded(true)
|
||||
const persisted = buildWorkspaceSessionPayload(firstStore.getState())
|
||||
const restoredStore = createTestStore()
|
||||
hydrateWithRepoScan(restoredStore, persisted)
|
||||
|
||||
const restored = restoredStore.getState()
|
||||
expect(restored.activeWorktreeId).toBe(WORKTREE_ID)
|
||||
expect(restored.unifiedTabsByWorktree[WORKTREE_ID]?.map((tab) => tab.id)).toEqual([
|
||||
EDITOR_FILE_ID,
|
||||
BROWSER_ID
|
||||
])
|
||||
expect(restored.activeTabType).toBe('browser')
|
||||
})
|
||||
|
||||
it('drops terminal-free chrome when an authoritative scan proves deletion', () => {
|
||||
const store = createTestStore()
|
||||
hydrateWithRepoScan(store, makeTerminalFreeDegradedRepoSession(), true)
|
||||
|
||||
const state = store.getState()
|
||||
expect(state.activeWorktreeId).toBeNull()
|
||||
expect(state.unifiedTabsByWorktree[WORKTREE_ID]).toBeUndefined()
|
||||
expect(state.openFiles).toEqual([])
|
||||
expect(state.browserTabsByWorktree[WORKTREE_ID]).toBeUndefined()
|
||||
})
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
import type { DetectedWorktreeListResult, Repo, Worktree } from '../../../../shared/types'
|
||||
import type {
|
||||
DetectedWorktreeListResult,
|
||||
Repo,
|
||||
WorkspaceSessionState,
|
||||
Worktree
|
||||
} from '../../../../shared/types'
|
||||
import { parseWorkspaceKey } from '../../../../shared/workspace-scope'
|
||||
import { getRepoIdFromWorktreeId } from '../../../../shared/worktree-id'
|
||||
|
||||
@@ -10,6 +15,25 @@ type WorktreeValidityCatalog = {
|
||||
>
|
||||
}
|
||||
|
||||
export function collectPersistedWorktreeIdsForSessionHydration(
|
||||
session: WorkspaceSessionState
|
||||
): Set<string> {
|
||||
const persistedWorktreeIds = new Set<string>()
|
||||
for (const worktreeId of Object.keys(session.tabsByWorktree)) {
|
||||
persistedWorktreeIds.add(worktreeId)
|
||||
}
|
||||
for (const worktreeId of Object.keys(session.unifiedTabs ?? {})) {
|
||||
persistedWorktreeIds.add(worktreeId)
|
||||
}
|
||||
for (const worktreeId of Object.keys(session.openFilesByWorktree ?? {})) {
|
||||
persistedWorktreeIds.add(worktreeId)
|
||||
}
|
||||
for (const worktreeId of Object.keys(session.browserTabsByWorktree ?? {})) {
|
||||
persistedWorktreeIds.add(worktreeId)
|
||||
}
|
||||
return persistedWorktreeIds
|
||||
}
|
||||
|
||||
export function buildValidWorktreeIdsForSessionHydration(
|
||||
catalog: WorktreeValidityCatalog,
|
||||
persistedWorktreeIds: Iterable<string>
|
||||
|
||||
@@ -36,7 +36,10 @@ import {
|
||||
addAdditionalValidWorkspaceKeys,
|
||||
type WorkspaceSessionHydrationOptions
|
||||
} from '@/lib/workspace-session-hydration-keys'
|
||||
import { buildValidWorktreeIdsForSessionHydration } from './degraded-repo-worktree-validity'
|
||||
import {
|
||||
buildValidWorktreeIdsForSessionHydration,
|
||||
collectPersistedWorktreeIdsForSessionHydration
|
||||
} from './degraded-repo-worktree-validity'
|
||||
|
||||
export type TabSplitDirection = 'left' | 'right' | 'up' | 'down'
|
||||
|
||||
@@ -1967,11 +1970,7 @@ export const createTabsSlice: StateCreator<AppState, [], [], TabsSlice> = (set,
|
||||
|
||||
hydrateTabsSession: (session, options) => {
|
||||
const state = get()
|
||||
const persistedWorktreeIds = new Set([
|
||||
...Object.keys(session.tabsByWorktree),
|
||||
...Object.keys(session.unifiedTabs ?? {}),
|
||||
...Object.keys(session.openFilesByWorktree ?? {})
|
||||
])
|
||||
const persistedWorktreeIds = collectPersistedWorktreeIdsForSessionHydration(session)
|
||||
const validWorktreeIds = buildValidWorktreeIdsForSessionHydration(state, persistedWorktreeIds)
|
||||
validWorktreeIds.add(FLOATING_TERMINAL_WORKTREE_ID)
|
||||
for (const workspace of state.folderWorkspaces) {
|
||||
|
||||
@@ -99,7 +99,10 @@ import {
|
||||
addAdditionalValidWorkspaceKeys,
|
||||
type WorkspaceSessionHydrationOptions
|
||||
} from '@/lib/workspace-session-hydration-keys'
|
||||
import { buildValidWorktreeIdsForSessionHydration } from './degraded-repo-worktree-validity'
|
||||
import {
|
||||
buildValidWorktreeIdsForSessionHydration,
|
||||
collectPersistedWorktreeIdsForSessionHydration
|
||||
} from './degraded-repo-worktree-validity'
|
||||
import {
|
||||
collectHibernatedCompletionEvidenceForWorktree,
|
||||
collectSleepingAgentSessionRecordsForWorktree,
|
||||
@@ -3044,7 +3047,7 @@ export const createTerminalSlice: StateCreator<AppState, [], [], TerminalSlice>
|
||||
worktreesByRepo: runtimeSessionPlaceholders.worktreesByRepo,
|
||||
detectedWorktreesByRepo: s.detectedWorktreesByRepo
|
||||
},
|
||||
Object.keys(session.tabsByWorktree)
|
||||
collectPersistedWorktreeIdsForSessionHydration(session)
|
||||
)
|
||||
const knownRepoIds = new Set(runtimeSessionPlaceholders.repos.map((r) => r.id))
|
||||
// Why: the Floating Workspace isn't a repo worktree, but its tabs use the normal session pipeline so daemon PTYs survive app restart.
|
||||
|
||||
Reference in New Issue
Block a user