mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 16:47:34 +00:00
Main drops the layout of workspaces the catalog no longer validates (dark, layout-owner build only) (#30177)
* feat(layout): main drops the layout of workspaces the catalog no longer validates (dark, layout-owner build only) * rename the paired-host check so the switch-off bundle keeps its names * a repo id two hosts register skips the listing purge
This commit is contained in:
@@ -42,6 +42,7 @@
|
||||
4 src/main/persistence/migrating-orcad-catalog/orcad-source-workspace-session-subtraction.ts
|
||||
1 src/main/persistence/migrating-orcad-catalog/orcad-source-workspace-session.ts
|
||||
1 src/main/persistence/restoring-sessions/folder-workspace-operations.ts
|
||||
1 src/main/persistence/restoring-sessions/invalid-workspace-layout-purge.ts
|
||||
5 src/main/persistence/restoring-sessions/session-owner-fields.ts
|
||||
12 src/main/persistence/restoring-sessions/session-owner-removal.ts
|
||||
2 src/main/persistence/restoring-sessions/workspace-pane-normalization.ts
|
||||
|
||||
@@ -21,6 +21,8 @@ import {
|
||||
isCapturedRepoCurrent,
|
||||
resolveRepoOwnershipEvidence
|
||||
} from './worktree-host-ownership'
|
||||
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../../shared/layout-owner-switch'
|
||||
import { dropWorkspacesTheListingInvalidates } from '../../../persistence/restoring-sessions/invalid-workspace-layout-purge'
|
||||
|
||||
export async function listHostQualifiedDetectedWorktrees(
|
||||
store: Store,
|
||||
@@ -90,6 +92,9 @@ export async function listHostQualifiedDetectedWorktrees(
|
||||
return capturedAuthority !== null && isCurrentSshProviderAuthority(capturedAuthority)
|
||||
}
|
||||
const result = await listDetectedWorktreesForCapturedRepo(store, repo, isCurrent, providerAbort)
|
||||
if (ORCA_LAYOUT_OWNER_IS_MAIN) {
|
||||
dropWorkspacesTheListingInvalidates(store, result)
|
||||
}
|
||||
if (!result) {
|
||||
return rejected('stale')
|
||||
}
|
||||
|
||||
@@ -23,6 +23,8 @@ import {
|
||||
import { listHostQualifiedDetectedWorktrees } from './host-qualified-worktree-listing'
|
||||
import { findExactRepoOwner, isCapturedRepoCurrent } from './worktree-host-ownership'
|
||||
import type { WorktreeIpcContext } from '../worktree-ipc-context'
|
||||
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../../shared/layout-owner-switch'
|
||||
import { dropWorkspacesTheListingInvalidates } from '../../../persistence/restoring-sessions/invalid-workspace-layout-purge'
|
||||
|
||||
export const DETECTED_WORKTREE_PROVIDER_TIMEOUT_MS = 30_000
|
||||
|
||||
@@ -113,6 +115,9 @@ export function registerDetectedWorktreeHandlers(context: WorktreeIpcContext): v
|
||||
isCapturedRepoCurrent(store, repo) &&
|
||||
(!connectionId || (authority !== undefined && isCurrentSshProviderAuthority(authority)))
|
||||
)
|
||||
if (ORCA_LAYOUT_OWNER_IS_MAIN) {
|
||||
dropWorkspacesTheListingInvalidates(store, result)
|
||||
}
|
||||
return result && !('providerAbortStatus' in result)
|
||||
? result
|
||||
: {
|
||||
|
||||
@@ -0,0 +1,242 @@
|
||||
import {
|
||||
closeTestStores,
|
||||
testState,
|
||||
createStore,
|
||||
writeDataFile,
|
||||
makeRepo,
|
||||
makeTerminalTab
|
||||
} from './persistence-test-harness'
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
||||
import { rmSync, mkdtempSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { FLOATING_TERMINAL_WORKTREE_ID, getDefaultWorkspaceSession } from '../shared/constants'
|
||||
import { folderWorkspaceKey } from '../shared/workspace-scope'
|
||||
import type { WorkspaceSessionState } from '../shared/workspace-session-state-types'
|
||||
import type { DetectedWorktreeListResult } from '../shared/worktree/types'
|
||||
import type { WorkspaceLayoutEvent } from '../shared/workspace-layout/workspace-layout-stream-frames'
|
||||
import {
|
||||
bumpLocalWorktreeScanGeneration,
|
||||
getLocalWorktreeCatalogVersion
|
||||
} from './local-worktree-scan-generation'
|
||||
import { dropWorkspacesTheListingInvalidates } from './persistence/restoring-sessions/invalid-workspace-layout-purge'
|
||||
import { WorkspaceLayoutStream } from './runtime/workspace-layout-stream'
|
||||
|
||||
vi.mock('../shared/layout-owner-switch', () => ({ ORCA_LAYOUT_OWNER_IS_MAIN: true }))
|
||||
vi.mock('./ssh/ssh-config-parser', () => ({
|
||||
loadUserSshConfig: vi.fn(),
|
||||
sshConfigHostsToTargets: vi.fn()
|
||||
}))
|
||||
vi.mock('electron', () => ({
|
||||
app: { getPath: () => testState.dir },
|
||||
safeStorage: { isEncryptionAvailable: () => false }
|
||||
}))
|
||||
vi.mock('./telemetry/client', () => ({ track: vi.fn() }))
|
||||
vi.mock('./telemetry/cohort-classifier', () => ({ getCohortAtEmit: vi.fn().mockReturnValue({}) }))
|
||||
|
||||
const REPO = 'repo-a'
|
||||
const KEPT = `${REPO}::/work/kept`
|
||||
const DELETED = `${REPO}::/work/deleted`
|
||||
const SSH_REPO = 'ssh-repo'
|
||||
const SSH_WORKTREE = `${SSH_REPO}::/home/me/app`
|
||||
const SSH_HOST = 'ssh:target-1'
|
||||
const RUNTIME_HOST = 'runtime:env-a'
|
||||
const PAIRED_WORKTREE = 'paired-repo::/srv/app'
|
||||
|
||||
function sessionWith(...worktreeIds: string[]): WorkspaceSessionState {
|
||||
return {
|
||||
...getDefaultWorkspaceSession(),
|
||||
tabsByWorktree: Object.fromEntries(
|
||||
worktreeIds.map((worktreeId, index) => [
|
||||
worktreeId,
|
||||
[makeTerminalTab({ id: `tab-${index}`, ptyId: `pty-${index}`, worktreeId })]
|
||||
])
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
function listing(
|
||||
worktreeIds: string[],
|
||||
overrides: Partial<DetectedWorktreeListResult> = {}
|
||||
): DetectedWorktreeListResult {
|
||||
return {
|
||||
repoId: REPO,
|
||||
authoritative: true,
|
||||
source: 'git',
|
||||
catalogVersion: getLocalWorktreeCatalogVersion(REPO),
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: the purge reads only each row's id.
|
||||
worktrees: worktreeIds.map((id) => ({ id }) as DetectedWorktreeListResult['worktrees'][number]),
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
const storedKeys = (store: ReturnType<typeof createStore>, hostId?: string): string[] =>
|
||||
Object.keys(store.getWorkspaceSession(hostId).tabsByWorktree).sort()
|
||||
|
||||
describe('main drops the layout of workspaces the catalog no longer validates', () => {
|
||||
beforeEach(() => {
|
||||
testState.dir = mkdtempSync(join(tmpdir(), 'orca-invalid-layout-purge-'))
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await closeTestStores()
|
||||
rmSync(testState.dir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
describe('at load', () => {
|
||||
it('drops a stored workspace of a deregistered repo', () => {
|
||||
writeDataFile({
|
||||
schemaVersion: 1,
|
||||
repos: [makeRepo({ id: REPO, path: '/work/kept' })],
|
||||
worktreeMeta: {},
|
||||
workspaceSession: sessionWith(KEPT, 'gone-repo::/work/gone')
|
||||
})
|
||||
|
||||
expect(storedKeys(createStore())).toEqual([KEPT])
|
||||
})
|
||||
|
||||
it('drops a stored workspace whose key names no registered repo, as the window does', () => {
|
||||
writeDataFile({
|
||||
schemaVersion: 1,
|
||||
repos: [makeRepo({ id: REPO, path: '/work/kept' })],
|
||||
worktreeMeta: {},
|
||||
workspaceSession: sessionWith(KEPT, 'legacy-bare-key')
|
||||
})
|
||||
|
||||
expect(storedKeys(createStore())).toEqual([KEPT])
|
||||
})
|
||||
|
||||
it('keeps a degraded but registered repo, a disconnected SSH repo, folders and the floating workspace', () => {
|
||||
const folderKey = folderWorkspaceKey('folder-never-listed')
|
||||
writeDataFile({
|
||||
schemaVersion: 1,
|
||||
repos: [
|
||||
makeRepo({ id: REPO, path: '/work/kept' }),
|
||||
makeRepo({ id: SSH_REPO, path: '/home/me/app', connectionId: 'target-1' })
|
||||
],
|
||||
worktreeMeta: {},
|
||||
workspaceSession: sessionWith(KEPT, DELETED, folderKey, FLOATING_TERMINAL_WORKTREE_ID),
|
||||
workspaceSessionsByHostId: { [SSH_HOST]: sessionWith(SSH_WORKTREE) }
|
||||
})
|
||||
|
||||
const store = createStore()
|
||||
|
||||
expect(storedKeys(store)).toEqual(
|
||||
[KEPT, DELETED, folderKey, FLOATING_TERMINAL_WORKTREE_ID].sort()
|
||||
)
|
||||
expect(storedKeys(store, SSH_HOST)).toEqual([SSH_WORKTREE])
|
||||
})
|
||||
|
||||
it("keeps a paired host's workspaces, whose catalog lives on that host", () => {
|
||||
writeDataFile({
|
||||
schemaVersion: 1,
|
||||
repos: [],
|
||||
worktreeMeta: { [PAIRED_WORKTREE]: { hostId: RUNTIME_HOST, status: 'active' } },
|
||||
workspaceSession: sessionWith(PAIRED_WORKTREE),
|
||||
workspaceSessionsByHostId: { [RUNTIME_HOST]: sessionWith(PAIRED_WORKTREE, 'bare-paired') }
|
||||
})
|
||||
|
||||
const store = createStore()
|
||||
|
||||
expect(storedKeys(store)).toEqual([PAIRED_WORKTREE])
|
||||
expect(storedKeys(store, RUNTIME_HOST)).toEqual(['bare-paired', PAIRED_WORKTREE].sort())
|
||||
})
|
||||
})
|
||||
|
||||
describe('when a listing changes the catalog', () => {
|
||||
function seededStore(): ReturnType<typeof createStore> {
|
||||
writeDataFile({
|
||||
schemaVersion: 1,
|
||||
repos: [
|
||||
makeRepo({ id: REPO, path: '/work/kept' }),
|
||||
makeRepo({ id: 'other-repo', path: '/x' })
|
||||
],
|
||||
worktreeMeta: {},
|
||||
workspaceSession: sessionWith(KEPT, DELETED, 'other-repo::/x')
|
||||
})
|
||||
return createStore()
|
||||
}
|
||||
|
||||
it("drops the workspace an authoritative scan no longer finds, and the window's frames lose it", async () => {
|
||||
const store = seededStore()
|
||||
const stream = new WorkspaceLayoutStream({
|
||||
store: () => store,
|
||||
homeHostId: () => 'local',
|
||||
liveTitle: () => null
|
||||
})
|
||||
const events: WorkspaceLayoutEvent[] = []
|
||||
const { snapshot } = stream.subscribe((event) => events.push(event))
|
||||
expect(snapshot.map((entry) => entry.key)).toContain(DELETED)
|
||||
|
||||
dropWorkspacesTheListingInvalidates(store, listing([KEPT]))
|
||||
await Promise.resolve()
|
||||
|
||||
// Other repos have no listing in this catalog, so they are untouched.
|
||||
expect(storedKeys(store)).toEqual([KEPT, 'other-repo::/x'].sort())
|
||||
expect(events).toContainEqual(expect.objectContaining({ type: 'removed', key: DELETED }))
|
||||
})
|
||||
|
||||
it('keeps every workspace after a scan that could not prove deletion', () => {
|
||||
const store = seededStore()
|
||||
|
||||
dropWorkspacesTheListingInvalidates(
|
||||
store,
|
||||
listing([], { authoritative: false, source: 'metadata-fallback' })
|
||||
)
|
||||
dropWorkspacesTheListingInvalidates(
|
||||
store,
|
||||
listing([KEPT], { authoritative: false, source: 'metadata-fallback' })
|
||||
)
|
||||
dropWorkspacesTheListingInvalidates(store, null)
|
||||
dropWorkspacesTheListingInvalidates(store, { providerAbortStatus: 'timed-out' })
|
||||
|
||||
expect(storedKeys(store)).toEqual([KEPT, DELETED, 'other-repo::/x'].sort())
|
||||
})
|
||||
|
||||
it('keeps every workspace when a create or remove landed after the scan began', () => {
|
||||
const store = seededStore()
|
||||
const stale = listing([KEPT])
|
||||
bumpLocalWorktreeScanGeneration(REPO)
|
||||
|
||||
dropWorkspacesTheListingInvalidates(store, stale)
|
||||
dropWorkspacesTheListingInvalidates(store, listing([KEPT], { catalogVersion: undefined }))
|
||||
|
||||
expect(storedKeys(store)).toEqual([KEPT, DELETED, 'other-repo::/x'].sort())
|
||||
})
|
||||
|
||||
it('keeps every workspace of a repo id another host also registers', () => {
|
||||
writeDataFile({
|
||||
schemaVersion: 1,
|
||||
repos: [
|
||||
makeRepo({ id: REPO, path: '/work/kept' }),
|
||||
makeRepo({ id: REPO, path: '/home/me/kept', connectionId: 'target-1' })
|
||||
],
|
||||
worktreeMeta: {},
|
||||
workspaceSession: sessionWith(KEPT, DELETED)
|
||||
})
|
||||
const store = createStore()
|
||||
|
||||
dropWorkspacesTheListingInvalidates(store, listing([KEPT]))
|
||||
|
||||
expect(storedKeys(store)).toEqual([KEPT, DELETED].sort())
|
||||
})
|
||||
})
|
||||
|
||||
// Pending until Core-A: today a window save still carries structure, so it can restore the key.
|
||||
it.skip('a later window save that still names a dropped workspace does not bring it back', () => {
|
||||
const store = (() => {
|
||||
writeDataFile({
|
||||
schemaVersion: 1,
|
||||
repos: [makeRepo({ id: REPO, path: '/work/kept' })],
|
||||
worktreeMeta: {},
|
||||
workspaceSession: sessionWith(KEPT, DELETED)
|
||||
})
|
||||
return createStore()
|
||||
})()
|
||||
dropWorkspacesTheListingInvalidates(store, listing([KEPT]))
|
||||
|
||||
store.setWorkspaceSession(sessionWith(KEPT, DELETED), 'local')
|
||||
|
||||
expect(storedKeys(store)).toEqual([KEPT])
|
||||
})
|
||||
})
|
||||
@@ -17,6 +17,8 @@ import {
|
||||
type StoreDomainOperations
|
||||
} from './store-domain-composition'
|
||||
import type { PersistedState } from '../../../shared/persisted-state-types'
|
||||
import { ORCA_LAYOUT_OWNER_IS_MAIN } from '../../../shared/layout-owner-switch'
|
||||
import { dropInvalidWorkspaceLayoutsAtLoad } from '../restoring-sessions/invalid-workspace-layout-purge'
|
||||
import { scheduleSave } from './write-scheduling'
|
||||
import {
|
||||
migrateLegacyOpenCodeGoApiKey,
|
||||
@@ -124,6 +126,9 @@ export class Store {
|
||||
// Load is the only place an orphaned repo id can be swept: every removal path needs the repo to
|
||||
// still be registered, so rows outlive their owner without one (#17776).
|
||||
const sweptRepoIds = this.domains.repos.sweepDeregisteredRepoResidue()
|
||||
if (ORCA_LAYOUT_OWNER_IS_MAIN) {
|
||||
dropInvalidWorkspaceLayoutsAtLoad(this, this.state)
|
||||
}
|
||||
// Imported snapshots cannot own the live hook server or write their source file.
|
||||
if (!imported) {
|
||||
// A committed migration whose blob install failed or was cut short is finished here; shell
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
// Layout-owner build only: main drops the stored layout of workspaces the catalog no longer
|
||||
// validates, by the same rule the window applies when it hydrates, so neither keeps what the other dropped.
|
||||
|
||||
import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../shared/constants'
|
||||
import { parseExecutionHostId, type ExecutionHostId } from '../../../shared/execution-host'
|
||||
import type { PersistedState } from '../../../shared/persisted-state-types'
|
||||
import type { WorkspaceSessionState } from '../../../shared/workspace-session-state-types'
|
||||
import { parseWorkspaceKey } from '../../../shared/workspace-scope'
|
||||
import { isWorktreeCatalogVersionBefore } from '../../../shared/worktree/catalog-version'
|
||||
import {
|
||||
buildValidWorktreeIdsForSessionHydration,
|
||||
collectPersistedWorktreeIdsForSessionHydration
|
||||
} from '../../../shared/worktree/degraded-repo-worktree-validity'
|
||||
import { getRepoIdFromWorktreeId } from '../../../shared/worktree/id'
|
||||
import type { DetectedWorktreeListResult } from '../../../shared/worktree/types'
|
||||
import { getLocalWorktreeCatalogVersion } from '../../local-worktree-scan-generation'
|
||||
|
||||
type WorkspaceValidityCatalog = Parameters<typeof buildValidWorktreeIdsForSessionHydration>[0]
|
||||
|
||||
type PurgeStore = {
|
||||
getRepos(): readonly { id: string }[]
|
||||
getWorkspaceSession(hostId?: string | null): WorkspaceSessionState
|
||||
getWorkspaceSessionHostIds(): ExecutionHostId[]
|
||||
removeWorkspaceSessionStateForWorktree(worktreeId: string, hostId?: ExecutionHostId | null): void
|
||||
}
|
||||
|
||||
const ownedByPairedHost = (hostId: string | null | undefined): boolean =>
|
||||
parseExecutionHostId(hostId)?.kind === 'runtime'
|
||||
|
||||
/** The window's hydration rule: floating and every folder key always stay (the window adds them). */
|
||||
function invalidWorkspaceKeys(
|
||||
session: WorkspaceSessionState,
|
||||
catalog: WorkspaceValidityCatalog,
|
||||
inScope: (key: string) => boolean
|
||||
): string[] {
|
||||
const persisted = [...collectPersistedWorktreeIdsForSessionHydration(session)].filter(
|
||||
(key) =>
|
||||
inScope(key) &&
|
||||
key !== FLOATING_TERMINAL_WORKTREE_ID &&
|
||||
parseWorkspaceKey(key)?.type !== 'folder'
|
||||
)
|
||||
const valid = buildValidWorktreeIdsForSessionHydration(catalog, persisted)
|
||||
return persisted.filter((key) => !valid.has(key))
|
||||
}
|
||||
|
||||
function dropInvalidWorkspaces(
|
||||
store: PurgeStore,
|
||||
catalog: WorkspaceValidityCatalog,
|
||||
inScope: (key: string) => boolean = () => true
|
||||
): void {
|
||||
for (const hostId of store.getWorkspaceSessionHostIds()) {
|
||||
// A paired host's catalog lives on that host; not seeing it here proves nothing.
|
||||
if (ownedByPairedHost(hostId)) {
|
||||
continue
|
||||
}
|
||||
for (const key of invalidWorkspaceKeys(store.getWorkspaceSession(hostId), catalog, inScope)) {
|
||||
store.removeWorkspaceSessionStateForWorktree(key, hostId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* At load no listing has run, so only an unregistered repo is invalid. Repos a paired host still
|
||||
* names, and a staged migration's repos, count as registered, exactly as the load sweep keeps them.
|
||||
*/
|
||||
export function dropInvalidWorkspaceLayoutsAtLoad(store: PurgeStore, state: PersistedState): void {
|
||||
const repoIds = new Set(store.getRepos().map((repo) => repo.id))
|
||||
for (const staged of state.orcadMigrationStagedCatalogs ?? []) {
|
||||
for (const repo of staged.manifest.payload.repositories) {
|
||||
repoIds.add(repo.id)
|
||||
}
|
||||
}
|
||||
for (const [worktreeId, meta] of Object.entries(state.worktreeMeta)) {
|
||||
if (ownedByPairedHost(meta.hostId)) {
|
||||
repoIds.add(getRepoIdFromWorktreeId(worktreeId))
|
||||
}
|
||||
}
|
||||
for (const [hostId, session] of Object.entries(state.workspaceSessionsByHostId ?? {})) {
|
||||
if (session && ownedByPairedHost(hostId)) {
|
||||
for (const key of collectPersistedWorktreeIdsForSessionHydration(session)) {
|
||||
repoIds.add(getRepoIdFromWorktreeId(key))
|
||||
}
|
||||
}
|
||||
}
|
||||
dropInvalidWorkspaces(store, {
|
||||
repos: [...repoIds].map((id) => ({ id })),
|
||||
worktreesByRepo: {}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* After a listing main serves: drops that repo's workspaces an authoritative scan no longer finds.
|
||||
* Hidden rows still count as found: hiding a worktree must not close its tabs.
|
||||
*/
|
||||
export function dropWorkspacesTheListingInvalidates(
|
||||
store: PurgeStore,
|
||||
result: DetectedWorktreeListResult | { providerAbortStatus: unknown } | null
|
||||
): void {
|
||||
if (!result || 'providerAbortStatus' in result) {
|
||||
return
|
||||
}
|
||||
const { repoId, catalogVersion } = result
|
||||
// A create or remove since the scan began leaves its rows short of live worktrees.
|
||||
if (
|
||||
!catalogVersion ||
|
||||
isWorktreeCatalogVersionBefore(catalogVersion, getLocalWorktreeCatalogVersion(repoId))
|
||||
) {
|
||||
return
|
||||
}
|
||||
// A repo id registered on two hosts shares its keys; one host's scan cannot speak for the other.
|
||||
if (store.getRepos().filter((repo) => repo.id === repoId).length !== 1) {
|
||||
return
|
||||
}
|
||||
dropInvalidWorkspaces(
|
||||
store,
|
||||
{
|
||||
repos: store.getRepos(),
|
||||
worktreesByRepo: { [repoId]: result.worktrees },
|
||||
detectedWorktreesByRepo: { [repoId]: result }
|
||||
},
|
||||
(key) => getRepoIdFromWorktreeId(key) === repoId
|
||||
)
|
||||
}
|
||||
@@ -2,7 +2,7 @@ import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../../shared/constants'
|
||||
import { folderWorkspaceKey } from '../../../../../shared/workspace-scope'
|
||||
import type { BrowserSlice, BrowserSliceGet, BrowserSliceSet } from './browser-slice-contract'
|
||||
import { addAdditionalValidWorkspaceKeys } from '@/lib/workspace-session-hydration-keys'
|
||||
import { buildValidWorktreeIdsForSessionHydration } from '../degraded-repo-worktree-validity'
|
||||
import { buildValidWorktreeIdsForSessionHydration } from '../../../../../shared/worktree/degraded-repo-worktree-validity'
|
||||
import { destroyWorkspaceWebviews } from '../browser-webview-cleanup'
|
||||
import { projectHydratedBrowserSession } from './browser-hydration-state'
|
||||
import { ensureBrowserClientHostsForRestoredPages } from '@/runtime/restored-client-hosted-browser-host-attach'
|
||||
|
||||
@@ -3,7 +3,7 @@ import type { EditorSlice } from '../types/editor-slice'
|
||||
import { addAdditionalValidWorkspaceKeys } from '@/lib/workspace-session-hydration-keys'
|
||||
import { FLOATING_TERMINAL_WORKTREE_ID } from '../../../../../../shared/constants'
|
||||
import { folderWorkspaceKey } from '../../../../../../shared/workspace-scope'
|
||||
import { buildValidWorktreeIdsForSessionHydration } from '../../degraded-repo-worktree-validity'
|
||||
import { buildValidWorktreeIdsForSessionHydration } from '../../../../../../shared/worktree/degraded-repo-worktree-validity'
|
||||
import { migrateHydratedEditorTabsAndGroups } from '../file-ids/hydrated-editor-file-ids'
|
||||
import { buildHydratedEditorSessionState } from './hydrated-editor-session-state'
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { addAdditionalValidWorkspaceKeys } from '@/lib/workspace-session-hydrati
|
||||
import {
|
||||
buildValidWorktreeIdsForSessionHydration,
|
||||
collectPersistedWorktreeIdsForSessionHydration
|
||||
} from '../degraded-repo-worktree-validity'
|
||||
} from '../../../../../shared/worktree/degraded-repo-worktree-validity'
|
||||
import { buildHydratedTabState } from '../tabs-hydration'
|
||||
import { projectWorktreeTabModelReconciliation } from './tabs-reconciliation'
|
||||
import { createWorktreeTabModelReconciliationBatch } from './tabs-reconciliation-batch'
|
||||
|
||||
@@ -10,7 +10,7 @@ import { addAdditionalValidWorkspaceKeys } from '@/lib/workspace-session-hydrati
|
||||
import {
|
||||
buildValidWorktreeIdsForSessionHydration,
|
||||
collectPersistedWorktreeIdsForSessionHydration
|
||||
} from '../slices/degraded-repo-worktree-validity'
|
||||
} from '../../../../shared/worktree/degraded-repo-worktree-validity'
|
||||
import type { TerminalSlice, TerminalStoreGet, TerminalStoreSet } from './terminal-state'
|
||||
import { buildRuntimeSessionPlaceholders } from './workspace-terminal-placeholders'
|
||||
import {
|
||||
|
||||
+5
-5
@@ -1,8 +1,8 @@
|
||||
import type { Repo } from '../../../../shared/repo-types'
|
||||
import type { WorkspaceSessionState } from '../../../../shared/workspace-session-state-types'
|
||||
import type { DetectedWorktreeListResult, Worktree } from '../../../../shared/worktree/types'
|
||||
import { parseWorkspaceKey } from '../../../../shared/workspace-scope'
|
||||
import { getRepoIdFromWorktreeId } from '../../../../shared/worktree/id'
|
||||
import type { Repo } from '../repo-types'
|
||||
import type { WorkspaceSessionState } from '../workspace-session-state-types'
|
||||
import type { DetectedWorktreeListResult, Worktree } from './types'
|
||||
import { parseWorkspaceKey } from '../workspace-scope'
|
||||
import { getRepoIdFromWorktreeId } from './id'
|
||||
|
||||
type WorktreeValidityCatalog = {
|
||||
repos: readonly Pick<Repo, 'id'>[]
|
||||
Reference in New Issue
Block a user