mirror of
https://github.com/stablyai/orca.git
synced 2026-10-16 00:03:09 +00:00
Preserve independent project host setups
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -493,12 +493,15 @@ Already partially landed:
|
||||
compatibility projection and persistence sync
|
||||
- project-host setup runtime capability advertisement, read fallback, and
|
||||
remote mutation gating for older runtime servers
|
||||
- persistence merge that preserves independently persisted project/setup rows
|
||||
across load, repo updates, and repo reorders while refreshing repo-backed
|
||||
compatibility rows from repos
|
||||
|
||||
Not complete yet:
|
||||
|
||||
- SSH clone streamed-progress parity and provisioning flows
|
||||
- independent project-host setup persistence beyond the repo-backed
|
||||
compatibility records
|
||||
- first-class independent project-host setup creation/update/delete APIs beyond
|
||||
the current repo-backed import/clone compatibility paths
|
||||
- bulk setup flows and setup for hosts that are not already known to the client
|
||||
- project settings split into global and host-specific ownership
|
||||
- host settings/capability UI aligned with project setup
|
||||
|
||||
@@ -497,11 +497,14 @@ Already partially landed:
|
||||
compatibility projection and persistence sync
|
||||
- project-host setup runtime capability advertisement, read fallback, and
|
||||
remote mutation gating for older runtime servers
|
||||
- persistence merge that preserves independently persisted project/setup rows
|
||||
across load, repo updates, and repo reorders while refreshing repo-backed
|
||||
compatibility rows from repos
|
||||
|
||||
Not complete yet:
|
||||
|
||||
- independent project-host setup persistence beyond repo-backed compatibility
|
||||
records
|
||||
- first-class independent project-host setup creation/update/delete APIs beyond
|
||||
the current repo-backed import/clone compatibility paths
|
||||
- bulk setup flows and setup for hosts that are not already known to the client
|
||||
- SSH streamed clone progress parity
|
||||
- full project settings split into global and host-specific ownership
|
||||
@@ -531,7 +534,7 @@ tests, and version-skew handling.
|
||||
|
||||
The largest remaining areas are:
|
||||
|
||||
1. independent `ProjectHostSetup` persistence
|
||||
1. independent `ProjectHostSetup` creation/update/delete APIs
|
||||
2. project-first creation/setup UI completion
|
||||
3. project and host settings ownership split
|
||||
4. cache/request ownership audit
|
||||
|
||||
@@ -532,14 +532,17 @@ Already partially landed:
|
||||
compatibility projection and persistence sync
|
||||
- project-host setup runtime capability advertisement, read fallback, and
|
||||
remote mutation gating for older runtime servers
|
||||
- persistence merge that preserves independently persisted project/setup rows
|
||||
across load, repo updates, and repo reorders while refreshing repo-backed
|
||||
compatibility rows from repos
|
||||
|
||||
Not complete yet:
|
||||
|
||||
- SSH clone streamed-progress parity
|
||||
- provisioning flows and inline provisioning actions from the composer
|
||||
- bulk setup flows and setup for hosts that are not already known to the client
|
||||
- independent project-host setup persistence beyond repo-backed compatibility
|
||||
records
|
||||
- first-class independent project-host setup creation/update/delete APIs beyond
|
||||
the current repo-backed import/clone compatibility paths
|
||||
- full project settings split into global and host-specific ownership
|
||||
- host settings/capability UI aligned with project setup
|
||||
- complete cache/request ownership audit
|
||||
|
||||
@@ -415,12 +415,15 @@ Already partially landed:
|
||||
- repo-backed setup method metadata for imported/cloned project-host setups
|
||||
- project-host setup runtime capability advertisement, read fallback, and
|
||||
remote mutation gating for older runtime servers
|
||||
- persistence merge that preserves independently persisted project/setup rows
|
||||
across load, repo updates, and repo reorders while refreshing repo-backed
|
||||
compatibility rows from repos
|
||||
|
||||
Still needed:
|
||||
|
||||
- finish SSH clone streamed-progress parity and provisioning flows
|
||||
- independent project-host setup persistence beyond the repo-backed
|
||||
compatibility records
|
||||
- first-class independent project-host setup creation/update/delete APIs beyond
|
||||
the current repo-backed import/clone compatibility paths
|
||||
- bulk setup flows and setup for newly added hosts
|
||||
- project settings split into global and host-specific ownership
|
||||
- host settings/capability UI aligned with project setup
|
||||
|
||||
@@ -766,16 +766,20 @@ Landed so far:
|
||||
advertise `project-host-setup.v1`; new clients fall back to repo-derived
|
||||
project/setup projection for reads when that capability is missing and block
|
||||
remote setup/clone mutations before contacting an older server.
|
||||
- Added a persistence merge for project-host setup state. Repo-backed
|
||||
compatibility rows are still refreshed from repos, but independently
|
||||
persisted projects/setups now survive load, repo update, and repo reorder
|
||||
paths instead of being overwritten by the repo projection.
|
||||
- Added tests for local repos, SSH repos, same-provider multi-host grouping,
|
||||
no-identity same-name non-grouping, selector cache behavior, persistence
|
||||
backfill, repo mutation synchronization, renderer hydration, runtime RPC
|
||||
routing, project-host setup capability gating, setup method persistence,
|
||||
local/runtime/SSH clone setup composition, remote clone IPC, and GitHub clone
|
||||
URL inference. Sidebar row-builder tests now cover project-first multi-host
|
||||
grouping and same-name repo separation without project identity. Workspace
|
||||
target tests cover local-only fallback, focused-host setup selection, explicit
|
||||
project-plus-host resolution, same-name non-merging, and unavailable setup
|
||||
reasons.
|
||||
routing, project-host setup capability gating, independent setup preservation,
|
||||
setup method persistence, local/runtime/SSH clone setup composition, remote
|
||||
clone IPC, and GitHub clone URL inference. Sidebar row-builder tests now cover
|
||||
project-first multi-host grouping and same-name repo separation without
|
||||
project identity. Workspace target tests cover local-only fallback,
|
||||
focused-host setup selection, explicit project-plus-host resolution, same-name
|
||||
non-merging, and unavailable setup reasons.
|
||||
|
||||
Important limitation:
|
||||
|
||||
@@ -791,15 +795,19 @@ Important limitation:
|
||||
not yet have local-clone parity for progress events. Abort now propagates to
|
||||
the relay `git.exec` request, and failed/aborted SSH clones clean up only
|
||||
when Orca can prove the target did not already exist before the clone.
|
||||
- Project-host setup records are still regenerated from repo compatibility
|
||||
records. The setup method now persists through that projection, but the final
|
||||
independent setup table is still future work.
|
||||
- Repo-backed project-host setup records are still regenerated from repo
|
||||
compatibility records. Independently persisted setup records are now preserved
|
||||
across load and repo mutations, but the UI/API still mostly creates setups
|
||||
through repo-backed import/clone paths. A fully independent setup creation and
|
||||
mutation table is still future work.
|
||||
|
||||
Remaining end-to-end work:
|
||||
|
||||
- broaden setup-on-host flows beyond known local, SSH, and active runtime hosts
|
||||
- finish SSH clone streamed-progress parity, provisioning, and bulk setup-on-host
|
||||
flows
|
||||
- add first-class independent project-host setup creation/update/delete APIs
|
||||
instead of routing most setup changes through repo import/update compatibility
|
||||
- split settings into explicit client, host, project, and project-host setup
|
||||
scopes
|
||||
- validate the default project-first sidebar view in Electron and continue
|
||||
|
||||
@@ -16,7 +16,9 @@ import { join } from 'path'
|
||||
import { tmpdir } from 'os'
|
||||
import type {
|
||||
PersistedState,
|
||||
Project,
|
||||
ProjectGroup,
|
||||
ProjectHostSetup,
|
||||
Repo,
|
||||
TerminalPaneLayoutNode,
|
||||
TerminalTab,
|
||||
@@ -27,6 +29,7 @@ import { isTerminalLeafId, makePaneKey } from '../shared/stable-pane-id'
|
||||
import { TERMINAL_SCROLLBACK_REPLAY_BYTE_LIMIT } from '../shared/terminal-scrollback-limits'
|
||||
import { MAX_BROWSER_HISTORY_ENTRIES } from '../shared/workspace-session-browser-history'
|
||||
import {
|
||||
getDefaultPersistedState,
|
||||
getDefaultWorkspaceSession,
|
||||
ONBOARDING_FINAL_STEP,
|
||||
ONBOARDING_FLOW_VERSION
|
||||
@@ -161,6 +164,30 @@ const makeRepo = (overrides: Partial<Repo> = {}): Repo => ({
|
||||
...overrides
|
||||
})
|
||||
|
||||
const makeProject = (overrides: Partial<Project> = {}): Project => ({
|
||||
id: 'project-1',
|
||||
displayName: 'Project',
|
||||
badgeColor: '#737373',
|
||||
sourceRepoIds: [],
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
...overrides
|
||||
})
|
||||
|
||||
const makeProjectHostSetup = (overrides: Partial<ProjectHostSetup> = {}): ProjectHostSetup => ({
|
||||
id: 'setup-1',
|
||||
projectId: 'project-1',
|
||||
hostId: 'local',
|
||||
repoId: '',
|
||||
path: '/repo',
|
||||
displayName: 'Project',
|
||||
setupState: 'ready',
|
||||
setupMethod: 'imported-existing-folder',
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
...overrides
|
||||
})
|
||||
|
||||
const makeTerminalTab = (overrides: Partial<TerminalTab> = {}): TerminalTab => ({
|
||||
id: 'tab1',
|
||||
ptyId: 'pty1',
|
||||
@@ -319,6 +346,38 @@ describe('Store', () => {
|
||||
expect(persisted.projectHostSetups).toEqual(store.getProjectHostSetups())
|
||||
})
|
||||
|
||||
it('preserves independent project host setup records on load', async () => {
|
||||
const independentProject = makeProject({
|
||||
id: 'cloud-project',
|
||||
displayName: 'Cloud Project'
|
||||
})
|
||||
const independentSetup = makeProjectHostSetup({
|
||||
id: 'cloud-project::gpu-vm',
|
||||
projectId: independentProject.id,
|
||||
hostId: 'runtime:gpu-vm',
|
||||
repoId: '',
|
||||
path: '/srv/cloud-project',
|
||||
displayName: 'GPU VM'
|
||||
})
|
||||
writeDataFile({
|
||||
...getDefaultPersistedState(testState.dir),
|
||||
repos: [makeRepo({ id: 'r1', path: '/repo', displayName: 'Repo' })],
|
||||
projects: [independentProject],
|
||||
projectHostSetups: [independentSetup]
|
||||
})
|
||||
|
||||
const store = await createStore()
|
||||
|
||||
expect(store.getProjects().map((project) => project.id)).toEqual(['repo:r1', 'cloud-project'])
|
||||
expect(store.getProjectHostSetups().map((setup) => setup.id)).toEqual([
|
||||
'r1',
|
||||
'cloud-project::gpu-vm'
|
||||
])
|
||||
store.flush()
|
||||
const persisted = readDataFile() as PersistedState
|
||||
expect(persisted.projectHostSetups).toContainEqual(independentSetup)
|
||||
})
|
||||
|
||||
it('returns default settings when no data file exists', async () => {
|
||||
const store = await createStore()
|
||||
const settings = store.getSettings()
|
||||
@@ -2329,6 +2388,37 @@ describe('Store', () => {
|
||||
])
|
||||
})
|
||||
|
||||
it('repo mutations preserve independent project host setup records', async () => {
|
||||
const independentProject = makeProject({
|
||||
id: 'cloud-project',
|
||||
displayName: 'Cloud Project'
|
||||
})
|
||||
const independentSetup = makeProjectHostSetup({
|
||||
id: 'cloud-project::gpu-vm',
|
||||
projectId: independentProject.id,
|
||||
hostId: 'runtime:gpu-vm',
|
||||
repoId: '',
|
||||
path: '/srv/cloud-project',
|
||||
displayName: 'GPU VM'
|
||||
})
|
||||
writeDataFile({
|
||||
...getDefaultPersistedState(testState.dir),
|
||||
repos: [makeRepo({ id: 'r1' })],
|
||||
projects: [independentProject],
|
||||
projectHostSetups: [independentSetup]
|
||||
})
|
||||
const store = await createStore()
|
||||
|
||||
store.updateRepo('r1', { displayName: 'renamed' })
|
||||
store.reorderRepos(['r1'])
|
||||
|
||||
expect(store.getProjects().map((project) => project.id)).toEqual(['repo:r1', 'cloud-project'])
|
||||
expect(store.getProjectHostSetups()).toEqual([
|
||||
expect.objectContaining({ id: 'r1', displayName: 'renamed' }),
|
||||
independentSetup
|
||||
])
|
||||
})
|
||||
|
||||
it('updateRepo preserves repo-backed project host setup method', async () => {
|
||||
const store = await createStore()
|
||||
store.addRepo(makeRepo())
|
||||
|
||||
+37
-8
@@ -1590,21 +1590,50 @@ function migrationUnsupportedEntriesEqual(
|
||||
|
||||
function projectHostSetupCompatibilityStateEqual(
|
||||
state: Pick<PersistedState, 'projects' | 'projectHostSetups'>,
|
||||
projection: Pick<PersistedState, 'projects' | 'projectHostSetups'>
|
||||
nextState: Pick<PersistedState, 'projects' | 'projectHostSetups'>
|
||||
): boolean {
|
||||
return (
|
||||
JSON.stringify(state.projects ?? []) === JSON.stringify(projection.projects) &&
|
||||
JSON.stringify(state.projectHostSetups ?? []) === JSON.stringify(projection.projectHostSetups)
|
||||
JSON.stringify(state.projects ?? []) === JSON.stringify(nextState.projects) &&
|
||||
JSON.stringify(state.projectHostSetups ?? []) === JSON.stringify(nextState.projectHostSetups)
|
||||
)
|
||||
}
|
||||
|
||||
function buildProjectHostSetupCompatibilityState(
|
||||
function isRepoBackedProjectHostSetup(
|
||||
setup: ProjectHostSetup,
|
||||
currentRepoIds: ReadonlySet<string>
|
||||
): boolean {
|
||||
const repoId = typeof setup.repoId === 'string' ? setup.repoId : ''
|
||||
return repoId.length > 0 && (currentRepoIds.has(repoId) || setup.id === repoId)
|
||||
}
|
||||
|
||||
function mergeProjectHostSetupCompatibilityState(
|
||||
state: Pick<PersistedState, 'projects' | 'projectHostSetups'>,
|
||||
repos: readonly Repo[]
|
||||
): Pick<PersistedState, 'projects' | 'projectHostSetups'> {
|
||||
const projection = projectHostSetupProjectionFromRepos(repos)
|
||||
const currentRepoIds = new Set(repos.map((repo) => repo.id))
|
||||
const projectedProjectIds = new Set(projection.projects.map((project) => project.id))
|
||||
const projectedSetupIds = new Set(projection.setups.map((setup) => setup.id))
|
||||
// Why: legacy/repo-backed setup rows use the repo id as the setup id. Keep
|
||||
// only independent setup rows here so repo deletion does not leave ghosts.
|
||||
const independentSetups = (state.projectHostSetups ?? []).filter((setup) => {
|
||||
if (projectedSetupIds.has(setup.id)) {
|
||||
return false
|
||||
}
|
||||
return !isRepoBackedProjectHostSetup(setup, currentRepoIds)
|
||||
})
|
||||
const independentProjectIds = new Set(independentSetups.map((setup) => setup.projectId))
|
||||
const independentProjects = (state.projects ?? [])
|
||||
.filter(
|
||||
(project) => independentProjectIds.has(project.id) && !projectedProjectIds.has(project.id)
|
||||
)
|
||||
.map((project) => ({
|
||||
...project,
|
||||
sourceRepoIds: project.sourceRepoIds.filter((repoId) => currentRepoIds.has(repoId))
|
||||
}))
|
||||
return {
|
||||
projects: projection.projects,
|
||||
projectHostSetups: projection.setups
|
||||
projects: [...projection.projects, ...independentProjects],
|
||||
projectHostSetups: [...projection.setups, ...independentSetups]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2300,7 +2329,7 @@ export class Store {
|
||||
}
|
||||
|
||||
const repos = clearMissingProjectGroupMemberships(result.repos, result.projectGroups ?? [])
|
||||
const projectHostSetupCompatibility = buildProjectHostSetupCompatibilityState(repos)
|
||||
const projectHostSetupCompatibility = mergeProjectHostSetupCompatibilityState(result, repos)
|
||||
if (!projectHostSetupCompatibilityStateEqual(result, projectHostSetupCompatibility)) {
|
||||
this.loadNeedsSave = true
|
||||
}
|
||||
@@ -2791,7 +2820,7 @@ export class Store {
|
||||
}
|
||||
|
||||
private syncProjectHostSetupCompatibilityState(): void {
|
||||
const compatibilityState = buildProjectHostSetupCompatibilityState(this.state.repos)
|
||||
const compatibilityState = mergeProjectHostSetupCompatibilityState(this.state, this.state.repos)
|
||||
this.state.projects = compatibilityState.projects
|
||||
this.state.projectHostSetups = compatibilityState.projectHostSetups
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user