mirror of
https://github.com/stablyai/orca.git
synced 2026-10-16 16:03:26 +00:00
Preserve floating workspace state after restart (#5047)
* Preserve floating workspace state during restart purge * rm doc
This commit is contained in:
@@ -88,6 +88,7 @@ import {
|
||||
registerPersistentWebview,
|
||||
unregisterPersistentWebview
|
||||
} from '../../components/browser-pane/webview-registry'
|
||||
import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../shared/constants'
|
||||
|
||||
function resetRemoteRuntimeMocks() {
|
||||
clearRuntimeCompatibilityCacheForTests()
|
||||
@@ -3243,6 +3244,117 @@ describe('fetchAllWorktrees hydration-time purge (design §4.4)', () => {
|
||||
})
|
||||
expect(store.getState().hasHydratedWorktreePurge).toBe(true)
|
||||
})
|
||||
|
||||
it('preserves floating workspace state while purging a real stale worktree', async () => {
|
||||
const store = createTestStore()
|
||||
const wtA = makeWorktree({ id: 'repoA::/a/wt1', repoId: 'repoA', path: '/a/wt1' })
|
||||
const wtB = makeWorktree({ id: 'repoB::/b/wt1', repoId: 'repoB', path: '/b/wt1' })
|
||||
const staleId = 'repoA::/a/zombie'
|
||||
const floatingFile = {
|
||||
id: 'floating-file',
|
||||
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
|
||||
filePath: '/floating/note.md',
|
||||
relativePath: 'note.md',
|
||||
language: 'markdown',
|
||||
isDirty: false,
|
||||
isPreview: false,
|
||||
mode: 'edit' as const
|
||||
}
|
||||
|
||||
mockApi.worktrees.list.mockImplementation(async ({ repoId }: { repoId: string }) =>
|
||||
repoId === 'repoA' ? [wtA] : [wtB]
|
||||
)
|
||||
|
||||
store.setState({
|
||||
repos: [repoA, repoB],
|
||||
activeWorktreeId: FLOATING_TERMINAL_WORKTREE_ID,
|
||||
activeFileId: 'floating-file',
|
||||
activeTabId: 'floating-terminal-tab',
|
||||
activeTabType: 'editor' as const,
|
||||
tabsByWorktree: {
|
||||
[wtA.id]: [{ id: 'tab-A', worktreeId: wtA.id }],
|
||||
[staleId]: [{ id: 'tab-zombie', worktreeId: staleId }],
|
||||
[FLOATING_TERMINAL_WORKTREE_ID]: [
|
||||
{ id: 'floating-terminal-tab', worktreeId: FLOATING_TERMINAL_WORKTREE_ID }
|
||||
]
|
||||
},
|
||||
browserTabsByWorktree: {
|
||||
[FLOATING_TERMINAL_WORKTREE_ID]: [{ id: 'floating-browser', url: 'https://orca.test' }]
|
||||
},
|
||||
activeBrowserTabIdByWorktree: {
|
||||
[FLOATING_TERMINAL_WORKTREE_ID]: 'floating-browser'
|
||||
},
|
||||
openFiles: [
|
||||
floatingFile,
|
||||
{
|
||||
id: 'stale-file',
|
||||
worktreeId: staleId,
|
||||
filePath: '/a/zombie/stale.ts',
|
||||
relativePath: 'stale.ts',
|
||||
language: 'typescript',
|
||||
isDirty: false,
|
||||
isPreview: false,
|
||||
mode: 'edit' as const
|
||||
}
|
||||
],
|
||||
activeFileIdByWorktree: {
|
||||
[FLOATING_TERMINAL_WORKTREE_ID]: 'floating-file',
|
||||
[staleId]: 'stale-file'
|
||||
},
|
||||
unifiedTabsByWorktree: {
|
||||
[FLOATING_TERMINAL_WORKTREE_ID]: [
|
||||
{
|
||||
id: 'floating-unified-tab',
|
||||
type: 'editor',
|
||||
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
|
||||
fileId: 'floating-file'
|
||||
}
|
||||
],
|
||||
[staleId]: [{ id: 'stale-unified-tab', worktreeId: staleId }]
|
||||
},
|
||||
groupsByWorktree: {
|
||||
[FLOATING_TERMINAL_WORKTREE_ID]: [
|
||||
{
|
||||
id: 'floating-group',
|
||||
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
|
||||
activeTabId: 'floating-unified-tab'
|
||||
}
|
||||
],
|
||||
[staleId]: [{ id: 'stale-group', worktreeId: staleId, activeTabId: 'stale-unified-tab' }]
|
||||
},
|
||||
layoutByWorktree: {
|
||||
[FLOATING_TERMINAL_WORKTREE_ID]: { type: 'leaf', groupId: 'floating-group' },
|
||||
[staleId]: { type: 'leaf', groupId: 'stale-group' }
|
||||
}
|
||||
} as unknown as Partial<AppState>)
|
||||
|
||||
await store.getState().fetchAllWorktrees()
|
||||
|
||||
expect(store.getState().hasHydratedWorktreePurge).toBe(true)
|
||||
expect(store.getState().tabsByWorktree).toEqual({
|
||||
[wtA.id]: [{ id: 'tab-A', worktreeId: wtA.id }],
|
||||
[FLOATING_TERMINAL_WORKTREE_ID]: [
|
||||
{ id: 'floating-terminal-tab', worktreeId: FLOATING_TERMINAL_WORKTREE_ID }
|
||||
]
|
||||
})
|
||||
expect(store.getState().browserTabsByWorktree[FLOATING_TERMINAL_WORKTREE_ID]).toEqual([
|
||||
{ id: 'floating-browser', url: 'https://orca.test' }
|
||||
])
|
||||
expect(store.getState().openFiles).toEqual([floatingFile])
|
||||
expect(store.getState().activeFileIdByWorktree[FLOATING_TERMINAL_WORKTREE_ID]).toBe(
|
||||
'floating-file'
|
||||
)
|
||||
expect(store.getState().unifiedTabsByWorktree[FLOATING_TERMINAL_WORKTREE_ID]).toHaveLength(1)
|
||||
expect(store.getState().groupsByWorktree[FLOATING_TERMINAL_WORKTREE_ID]).toHaveLength(1)
|
||||
expect(store.getState().layoutByWorktree[FLOATING_TERMINAL_WORKTREE_ID]).toEqual({
|
||||
type: 'leaf',
|
||||
groupId: 'floating-group'
|
||||
})
|
||||
expect(store.getState().activeWorktreeId).toBe(FLOATING_TERMINAL_WORKTREE_ID)
|
||||
expect(store.getState().activeFileId).toBe('floating-file')
|
||||
expect(store.getState().activeTabId).toBe('floating-terminal-tab')
|
||||
expect(store.getState().activeTabType).toBe('editor')
|
||||
})
|
||||
})
|
||||
|
||||
// Why: design §4.4 — purgeWorktreeTerminalState wipes every worktree-scoped
|
||||
@@ -3342,6 +3454,157 @@ describe('purgeWorktreeTerminalState direct (design §4.4)', () => {
|
||||
|
||||
expect(store.getState().tabsByWorktree).toBe(before)
|
||||
})
|
||||
|
||||
it('ignores the floating workspace sentinel while purging mixed real ids', () => {
|
||||
const store = createTestStore()
|
||||
const staleId = 'repoA::/a/wt1'
|
||||
const floatingFile = {
|
||||
id: 'floating-file',
|
||||
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
|
||||
filePath: '/floating/note.md',
|
||||
relativePath: 'note.md',
|
||||
language: 'markdown',
|
||||
isDirty: false,
|
||||
isPreview: false,
|
||||
mode: 'edit' as const
|
||||
}
|
||||
|
||||
store.setState({
|
||||
tabsByWorktree: {
|
||||
[staleId]: [{ id: 'tab-1', worktreeId: staleId }],
|
||||
[FLOATING_TERMINAL_WORKTREE_ID]: [
|
||||
{ id: 'floating-terminal-tab', worktreeId: FLOATING_TERMINAL_WORKTREE_ID }
|
||||
]
|
||||
},
|
||||
browserTabsByWorktree: {
|
||||
[FLOATING_TERMINAL_WORKTREE_ID]: [{ id: 'floating-browser', url: 'https://orca.test' }]
|
||||
},
|
||||
openFiles: [
|
||||
floatingFile,
|
||||
{
|
||||
id: 'stale-file',
|
||||
worktreeId: staleId,
|
||||
filePath: '/a/wt1/stale.ts',
|
||||
relativePath: 'stale.ts',
|
||||
language: 'typescript',
|
||||
isDirty: false,
|
||||
isPreview: false,
|
||||
mode: 'edit' as const
|
||||
}
|
||||
],
|
||||
activeFileIdByWorktree: {
|
||||
[FLOATING_TERMINAL_WORKTREE_ID]: 'floating-file',
|
||||
[staleId]: 'stale-file'
|
||||
},
|
||||
unifiedTabsByWorktree: {
|
||||
[FLOATING_TERMINAL_WORKTREE_ID]: [
|
||||
{
|
||||
id: 'floating-unified-tab',
|
||||
type: 'editor',
|
||||
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
|
||||
fileId: 'floating-file'
|
||||
}
|
||||
],
|
||||
[staleId]: [{ id: 'stale-unified-tab', worktreeId: staleId }]
|
||||
},
|
||||
groupsByWorktree: {
|
||||
[FLOATING_TERMINAL_WORKTREE_ID]: [
|
||||
{
|
||||
id: 'floating-group',
|
||||
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
|
||||
activeTabId: 'floating-unified-tab'
|
||||
}
|
||||
],
|
||||
[staleId]: [{ id: 'stale-group', worktreeId: staleId, activeTabId: 'stale-unified-tab' }]
|
||||
},
|
||||
layoutByWorktree: {
|
||||
[FLOATING_TERMINAL_WORKTREE_ID]: { type: 'leaf', groupId: 'floating-group' },
|
||||
[staleId]: { type: 'leaf', groupId: 'stale-group' }
|
||||
}
|
||||
} as unknown as Partial<AppState>)
|
||||
|
||||
store.getState().purgeWorktreeTerminalState([staleId, FLOATING_TERMINAL_WORKTREE_ID])
|
||||
|
||||
expect(store.getState().tabsByWorktree).toEqual({
|
||||
[FLOATING_TERMINAL_WORKTREE_ID]: [
|
||||
{ id: 'floating-terminal-tab', worktreeId: FLOATING_TERMINAL_WORKTREE_ID }
|
||||
]
|
||||
})
|
||||
expect(store.getState().browserTabsByWorktree[FLOATING_TERMINAL_WORKTREE_ID]).toEqual([
|
||||
{ id: 'floating-browser', url: 'https://orca.test' }
|
||||
])
|
||||
expect(store.getState().openFiles).toEqual([floatingFile])
|
||||
expect(store.getState().activeFileIdByWorktree).toEqual({
|
||||
[FLOATING_TERMINAL_WORKTREE_ID]: 'floating-file'
|
||||
})
|
||||
expect(store.getState().unifiedTabsByWorktree[FLOATING_TERMINAL_WORKTREE_ID]).toHaveLength(1)
|
||||
expect(store.getState().groupsByWorktree[FLOATING_TERMINAL_WORKTREE_ID]).toHaveLength(1)
|
||||
expect(store.getState().layoutByWorktree[FLOATING_TERMINAL_WORKTREE_ID]).toEqual({
|
||||
type: 'leaf',
|
||||
groupId: 'floating-group'
|
||||
})
|
||||
})
|
||||
|
||||
it('is a no-op when only the floating workspace sentinel is passed', () => {
|
||||
const store = createTestStore()
|
||||
const tabsByWorktree = {}
|
||||
const browserTabsByWorktree = {
|
||||
[FLOATING_TERMINAL_WORKTREE_ID]: [{ id: 'floating-browser', url: 'https://orca.test' }]
|
||||
}
|
||||
const openFiles = [
|
||||
{
|
||||
id: 'floating-file',
|
||||
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
|
||||
filePath: '/floating/note.md',
|
||||
relativePath: 'note.md',
|
||||
language: 'markdown',
|
||||
isDirty: false,
|
||||
isPreview: false,
|
||||
mode: 'edit' as const
|
||||
}
|
||||
]
|
||||
const unifiedTabsByWorktree = {
|
||||
[FLOATING_TERMINAL_WORKTREE_ID]: [
|
||||
{
|
||||
id: 'floating-unified-tab',
|
||||
type: 'editor',
|
||||
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
|
||||
fileId: 'floating-file'
|
||||
}
|
||||
]
|
||||
}
|
||||
const groupsByWorktree = {
|
||||
[FLOATING_TERMINAL_WORKTREE_ID]: [
|
||||
{
|
||||
id: 'floating-group',
|
||||
worktreeId: FLOATING_TERMINAL_WORKTREE_ID,
|
||||
activeTabId: 'floating-unified-tab'
|
||||
}
|
||||
]
|
||||
}
|
||||
const layoutByWorktree = {
|
||||
[FLOATING_TERMINAL_WORKTREE_ID]: { type: 'leaf', groupId: 'floating-group' }
|
||||
}
|
||||
|
||||
store.setState({
|
||||
tabsByWorktree,
|
||||
browserTabsByWorktree,
|
||||
openFiles,
|
||||
activeFileIdByWorktree: { [FLOATING_TERMINAL_WORKTREE_ID]: 'floating-file' },
|
||||
unifiedTabsByWorktree,
|
||||
groupsByWorktree,
|
||||
layoutByWorktree
|
||||
} as unknown as Partial<AppState>)
|
||||
|
||||
store.getState().purgeWorktreeTerminalState([FLOATING_TERMINAL_WORKTREE_ID])
|
||||
|
||||
expect(store.getState().tabsByWorktree).toBe(tabsByWorktree)
|
||||
expect(store.getState().browserTabsByWorktree).toBe(browserTabsByWorktree)
|
||||
expect(store.getState().openFiles).toBe(openFiles)
|
||||
expect(store.getState().unifiedTabsByWorktree).toBe(unifiedTabsByWorktree)
|
||||
expect(store.getState().groupsByWorktree).toBe(groupsByWorktree)
|
||||
expect(store.getState().layoutByWorktree).toBe(layoutByWorktree)
|
||||
})
|
||||
})
|
||||
|
||||
describe('markWorktreeVisited', () => {
|
||||
|
||||
@@ -45,6 +45,7 @@ import {
|
||||
parseExecutionHostId,
|
||||
type ExecutionHostId
|
||||
} from '../../../../shared/execution-host'
|
||||
import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../shared/constants'
|
||||
export type { WorktreeSlice, WorktreeDeleteState } from './worktree-helpers'
|
||||
|
||||
// Why: old runtime servers only have `worktree.list`; preserve the large-list
|
||||
@@ -940,6 +941,9 @@ export const createWorktreeSlice: StateCreator<AppState, [], [], WorktreeSlice>
|
||||
return
|
||||
}
|
||||
const validIds = new Set<string>()
|
||||
// Why: floating is persisted renderer state, but not a repo worktree that
|
||||
// authoritative runtime scans can return.
|
||||
validIds.add(FLOATING_TERMINAL_WORKTREE_ID)
|
||||
for (const result of Object.values(get().detectedWorktreesByRepo)) {
|
||||
if (!result.authoritative) {
|
||||
continue
|
||||
@@ -2556,9 +2560,10 @@ export const createWorktreeSlice: StateCreator<AppState, [], [], WorktreeSlice>
|
||||
getKnownWorktreeById: (worktreeId) => findKnownWorktreeById(get(), worktreeId),
|
||||
|
||||
purgeWorktreeTerminalState: (worktreeIds: string[]) => {
|
||||
if (worktreeIds.length === 0) {
|
||||
const purgeableWorktreeIds = worktreeIds.filter((id) => id !== FLOATING_TERMINAL_WORKTREE_ID)
|
||||
if (purgeableWorktreeIds.length === 0) {
|
||||
return
|
||||
}
|
||||
set((s) => buildWorktreePurgeState(s, worktreeIds))
|
||||
set((s) => buildWorktreePurgeState(s, purgeableWorktreeIds))
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user