Show pending project host setup status in composer

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinwoo-H
2026-06-11 12:33:59 -07:00
co-authored by Orca
parent a0682f082f
commit f6ac9f7ab2
6 changed files with 138 additions and 10 deletions
@@ -506,6 +506,8 @@ Already partially landed:
delete, routed by setup-owning host
- project settings action for creating pending setup/provisioning metadata on a
known host before a checkout path exists
- workspace creation host options distinguish pending/error/unsupported setup
metadata from hosts that have no setup
Not complete yet:
@@ -510,6 +510,8 @@ Already partially landed:
delete, routed by setup-owning host
- project settings action for creating pending setup/provisioning metadata on a
known host before a checkout path exists
- workspace creation host options distinguish pending/error/unsupported setup
metadata from hosts that have no setup
Not complete yet:
@@ -428,6 +428,8 @@ Already partially landed:
delete, routed by setup-owning host
- project settings action for creating pending setup/provisioning metadata on a
known host before a checkout path exists
- workspace creation host options distinguish pending/error/unsupported setup
metadata from hosts that have no setup
Still needed:
+3
View File
@@ -788,6 +788,9 @@ Landed so far:
- Added a project settings action to create pending project-host setup metadata
for a known host without requiring an existing checkout path. This lets the UI
represent setup/provisioning intent before import/clone/provisioning finishes.
- Updated workspace creation host options so non-ready setup metadata appears
with its real pending/error/unsupported state instead of being collapsed into
a generic "project not set up" row.
- 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
@@ -100,4 +100,83 @@ describe('buildProjectHostSetupOptions', () => {
})
])
})
it('shows pending setup status for known hosts with non-ready setup metadata', () => {
const options = buildProjectHostSetupOptions({
projectId: 'project-1',
eligibleRepos: [repo('local-repo')],
hosts: [host('local'), host('runtime:gpu', { label: 'GPU VM' })],
projectHostSetups: [
setup('local', 'project-1', 'local', 'local-repo'),
setup('gpu-pending', 'project-1', 'runtime:gpu', '', {
path: '',
setupState: 'setting-up',
setupMethod: 'provisioned'
})
]
})
expect(options).toEqual([
expect.objectContaining({ id: 'local', kind: 'ready', label: 'Local Mac' }),
expect.objectContaining({
id: 'needs-setup:runtime:gpu',
kind: 'needs-setup',
label: 'GPU VM',
detail: 'Project setup is in progress'
})
])
})
it('uses specific pending details for not-set-up, error, and unsupported setup metadata', () => {
const base = {
projectId: 'project-1',
eligibleRepos: [repo('local-repo')],
projectHostSetups: [setup('local', 'project-1', 'local', 'local-repo')]
}
expect(
buildProjectHostSetupOptions({
...base,
hosts: [host('runtime:gpu', { label: 'GPU VM' })],
projectHostSetups: [
...base.projectHostSetups,
setup('gpu-pending', 'project-1', 'runtime:gpu', '', {
path: '',
setupState: 'not-set-up',
setupMethod: 'provisioned'
})
]
}).at(-1)
).toMatchObject({ detail: 'Project tracked on this host but not set up' })
expect(
buildProjectHostSetupOptions({
...base,
hosts: [host('runtime:gpu', { label: 'GPU VM' })],
projectHostSetups: [
...base.projectHostSetups,
setup('gpu-pending', 'project-1', 'runtime:gpu', '', {
path: '',
setupState: 'error',
setupMethod: 'provisioned'
})
]
}).at(-1)
).toMatchObject({ detail: 'Project setup needs attention' })
expect(
buildProjectHostSetupOptions({
...base,
hosts: [host('runtime:gpu', { label: 'GPU VM' })],
projectHostSetups: [
...base.projectHostSetups,
setup('gpu-pending', 'project-1', 'runtime:gpu', '', {
path: '',
setupState: 'unsupported',
setupMethod: 'provisioned'
})
]
}).at(-1)
).toMatchObject({ detail: 'Project is unsupported on this host' })
})
})
@@ -40,6 +40,7 @@ type BuildNeedsSetupOptionsInput = {
projectId: string
hosts: readonly ExecutionHostRegistryEntry[]
readySetupByHost: ReadonlyMap<ExecutionHostId, ReadyProjectHostSetupOption>
pendingSetupByHost: ReadonlyMap<ExecutionHostId, ProjectHostSetup>
}
type BuildProjectHostSetupOptionsInput = {
@@ -60,16 +61,34 @@ export function buildProjectHostSetupOptions({
}
const readyOptions = buildReadySetupOptions({ projectId, projectHostSetups, eligibleRepos })
const readySetupByHost = new Map(readyOptions.map((option) => [option.hostId, option]))
const pendingSetupByHost = getPendingSetupByHost(projectId, projectHostSetups)
return [
...readyOptions,
...buildNeedsSetupOptions({
projectId,
hosts,
readySetupByHost
readySetupByHost,
pendingSetupByHost
})
].sort((a, b) => compareProjectHostSetupOptions(a, b))
}
function getPendingSetupByHost(
projectId: string,
projectHostSetups: readonly ProjectHostSetup[]
): Map<ExecutionHostId, ProjectHostSetup> {
const setups = new Map<ExecutionHostId, ProjectHostSetup>()
for (const setup of projectHostSetups) {
if (setup.projectId !== projectId || setup.setupState === 'ready') {
continue
}
if (!setups.has(setup.hostId)) {
setups.set(setup.hostId, setup)
}
}
return setups
}
function buildReadySetupOptions({
projectId,
projectHostSetups,
@@ -98,18 +117,39 @@ function buildReadySetupOptions({
function buildNeedsSetupOptions({
projectId,
hosts,
readySetupByHost
readySetupByHost,
pendingSetupByHost
}: BuildNeedsSetupOptionsInput): NeedsSetupProjectHostOption[] {
return hosts
.filter((host) => !readySetupByHost.has(host.id))
.map((host) => ({
id: `needs-setup:${host.id}`,
kind: 'needs-setup' as const,
projectId,
hostId: host.id,
label: host.label || getExecutionHostLabel(host.id),
detail: 'Project not set up on this host'
}))
.map((host) => {
const pendingSetup = pendingSetupByHost.get(host.id)
return {
id: `needs-setup:${host.id}`,
kind: 'needs-setup' as const,
projectId,
hostId: host.id,
label: host.label || getExecutionHostLabel(host.id),
detail: pendingSetup
? getPendingSetupDetail(pendingSetup)
: 'Project not set up on this host'
}
})
}
function getPendingSetupDetail(setup: ProjectHostSetup): string {
switch (setup.setupState) {
case 'not-set-up':
return 'Project tracked on this host but not set up'
case 'setting-up':
return 'Project setup is in progress'
case 'error':
return 'Project setup needs attention'
case 'unsupported':
return 'Project is unsupported on this host'
case 'ready':
return setup.path
}
}
function compareProjectHostSetupOptions(