diff --git a/notes/project-first-host-model-change-inventory.md b/notes/project-first-host-model-change-inventory.md index 92b09298ca5..cb78423b00e 100644 --- a/notes/project-first-host-model-change-inventory.md +++ b/notes/project-first-host-model-change-inventory.md @@ -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: diff --git a/notes/project-first-host-model-conversation.md b/notes/project-first-host-model-conversation.md index 067bb75658f..fce74ed7189 100644 --- a/notes/project-first-host-model-conversation.md +++ b/notes/project-first-host-model-conversation.md @@ -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: diff --git a/notes/project-first-host-model-handoff.md b/notes/project-first-host-model-handoff.md index 39088c38ad8..711278b850e 100644 --- a/notes/project-first-host-model-handoff.md +++ b/notes/project-first-host-model-handoff.md @@ -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: diff --git a/notes/project-first-host-model.md b/notes/project-first-host-model.md index 3c4015d8b30..a9c5099afc1 100644 --- a/notes/project-first-host-model.md +++ b/notes/project-first-host-model.md @@ -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 diff --git a/src/renderer/src/lib/project-host-setup-options.test.ts b/src/renderer/src/lib/project-host-setup-options.test.ts index fa4c7d747a5..656f66e3882 100644 --- a/src/renderer/src/lib/project-host-setup-options.test.ts +++ b/src/renderer/src/lib/project-host-setup-options.test.ts @@ -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' }) + }) }) diff --git a/src/renderer/src/lib/project-host-setup-options.ts b/src/renderer/src/lib/project-host-setup-options.ts index 755d34eca43..7e21cff9cf9 100644 --- a/src/renderer/src/lib/project-host-setup-options.ts +++ b/src/renderer/src/lib/project-host-setup-options.ts @@ -40,6 +40,7 @@ type BuildNeedsSetupOptionsInput = { projectId: string hosts: readonly ExecutionHostRegistryEntry[] readySetupByHost: ReadonlyMap + pendingSetupByHost: ReadonlyMap } 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 { + const setups = new Map() + 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(