diff --git a/notes/project-first-host-model-change-inventory.md b/notes/project-first-host-model-change-inventory.md index 5b30853ff74..ad3a8d26231 100644 --- a/notes/project-first-host-model-change-inventory.md +++ b/notes/project-first-host-model-change-inventory.md @@ -510,6 +510,8 @@ Already partially landed: metadata from hosts that have no setup - workspace creation target resolution reports `setup-not-ready` when a selected host has non-ready setup metadata +- project settings setup-host choices now use the shared execution host + registry, including hydrated runtime hosts that are not currently focused Not complete yet: diff --git a/notes/project-first-host-model-conversation.md b/notes/project-first-host-model-conversation.md index 9b9df69f841..fecb2d059bf 100644 --- a/notes/project-first-host-model-conversation.md +++ b/notes/project-first-host-model-conversation.md @@ -514,6 +514,8 @@ Already partially landed: metadata from hosts that have no setup - workspace creation target resolution reports `setup-not-ready` when a selected host has non-ready setup metadata +- project settings setup-host choices now use the shared execution host + registry, including hydrated runtime hosts that are not currently focused Not complete yet: diff --git a/notes/project-first-host-model-handoff.md b/notes/project-first-host-model-handoff.md index 12e4a3c0d98..c0643655308 100644 --- a/notes/project-first-host-model-handoff.md +++ b/notes/project-first-host-model-handoff.md @@ -432,6 +432,8 @@ Already partially landed: metadata from hosts that have no setup - workspace creation target resolution reports `setup-not-ready` when a selected host has non-ready setup metadata +- project settings setup-host choices now use the shared execution host + registry, including hydrated runtime hosts that are not currently focused Still needed: diff --git a/notes/project-first-host-model.md b/notes/project-first-host-model.md index 01a3385461c..8a65ddab116 100644 --- a/notes/project-first-host-model.md +++ b/notes/project-first-host-model.md @@ -794,6 +794,10 @@ Landed so far: - Updated workspace creation target resolution so selecting a host with non-ready setup metadata reports `setup-not-ready` instead of `project-not-set-up-on-host`. +- Broadened project settings setup-host choices to use the shared execution + host registry. The setup flow can now offer hydrated runtime hosts even when + they are not the currently focused Orca server, and host labels stay aligned + with sidebar rename overrides. - 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 @@ -829,7 +833,8 @@ Important limitation: Remaining end-to-end work: -- broaden setup-on-host flows beyond known local, SSH, and active runtime hosts +- continue broadening setup-on-host flows beyond the shared known-host registry + toward bulk setup and newly added host onboarding - finish SSH clone streamed-progress parity, provisioning, and bulk setup-on-host flows - add actual provisioning execution APIs instead of only recording independent diff --git a/src/renderer/src/components/settings/RepositoryHostSetupsSection.test.tsx b/src/renderer/src/components/settings/RepositoryHostSetupsSection.test.tsx index 405cd675ca8..e859f1db11f 100644 --- a/src/renderer/src/components/settings/RepositoryHostSetupsSection.test.tsx +++ b/src/renderer/src/components/settings/RepositoryHostSetupsSection.test.tsx @@ -320,4 +320,78 @@ describe('RepositoryHostSetupsSection', () => { setupMethod: 'provisioned' }) }) + + it('offers inactive runtime hosts discovered from hydrated runtime status', async () => { + const createProjectHostSetup = vi.fn().mockResolvedValue({ + project: makeProject({ id: 'github:stablyai/orca' }), + setup: makeSetup({ + id: 'gpu-setup', + projectId: 'github:stablyai/orca', + repoId: '', + hostId: 'runtime:gpu', + path: '', + setupState: 'not-set-up', + setupMethod: 'provisioned' + }) + }) + const localRepo = makeRepo({ + id: 'local-repo', + displayName: 'Orca', + path: '/Users/alice/orca' + }) + useAppStore.setState({ + repos: [localRepo], + projects: [makeProject({ id: 'github:stablyai/orca' })], + projectHostSetups: [ + makeSetup({ + id: 'local-repo', + projectId: 'github:stablyai/orca', + repoId: 'local-repo', + hostId: 'local', + path: '/Users/alice/orca' + }) + ], + settings: { activeRuntimeEnvironmentId: null } as never, + runtimeStatusByEnvironmentId: new Map([ + [ + 'gpu', + { + checkedAt: 1, + appVersion: '1.8.0', + status: { + runtimeId: 'runtime-gpu', + rendererGraphEpoch: 1, + graphStatus: 'ready', + authoritativeWindowId: 1, + liveTabCount: 0, + liveLeafCount: 0, + runtimeProtocolVersion: 1, + minCompatibleRuntimeClientVersion: 1, + capabilities: ['project-host-setup.v1'] + } + } + ] + ]), + createProjectHostSetup + }) + + renderSection(localRepo) + + expect(container.textContent).toContain('gpu') + const trackButton = Array.from(container.querySelectorAll('button')).find( + (button) => button.textContent === 'Track setup' + ) + + await act(async () => { + trackButton?.dispatchEvent(new MouseEvent('click', { bubbles: true })) + }) + + expect(createProjectHostSetup).toHaveBeenCalledWith({ + projectId: 'github:stablyai/orca', + hostId: 'runtime:gpu', + displayName: 'Orca', + setupState: 'not-set-up', + setupMethod: 'provisioned' + }) + }) }) diff --git a/src/renderer/src/components/settings/RepositoryHostSetupsSection.tsx b/src/renderer/src/components/settings/RepositoryHostSetupsSection.tsx index 58405fc1ce9..8cc3b5be562 100644 --- a/src/renderer/src/components/settings/RepositoryHostSetupsSection.tsx +++ b/src/renderer/src/components/settings/RepositoryHostSetupsSection.tsx @@ -1,11 +1,11 @@ -import { useState } from 'react' +import { useMemo, useState } from 'react' import { getExecutionHostLabel } from '../../../../shared/execution-host' +import { type ExecutionHostId } from '../../../../shared/execution-host' import { - LOCAL_EXECUTION_HOST_ID, - toRuntimeExecutionHostId, - toSshExecutionHostId, - type ExecutionHostId -} from '../../../../shared/execution-host' + buildExecutionHostRegistry, + type ExecutionHostRegistryEntry +} from '../../../../shared/execution-host-registry' +import { getHostDisplayLabelOverrides } from '../../../../shared/host-setting-overrides' import type { ProjectHostSetup, ProjectHostSetupState, Repo } from '../../../../shared/types' import { useAppStore } from '../../store' import { getProjectHostSetupProjectionFromState } from '../../store/selectors' @@ -58,35 +58,18 @@ function getSetupStateLabel(setupState: ProjectHostSetupState): string { function buildSetupHostOptions({ projectHostSetups, - sshTargetLabels, - activeRuntimeEnvironmentId + hostOptions }: { projectHostSetups: ProjectHostSetup[] - sshTargetLabels: Map - activeRuntimeEnvironmentId: string | null | undefined + hostOptions: readonly ExecutionHostRegistryEntry[] }): SetupHostOption[] { const setupHostIds = new Set(projectHostSetups.map((setup) => setup.hostId)) - const options: SetupHostOption[] = [] - if (!setupHostIds.has(LOCAL_EXECUTION_HOST_ID)) { - options.push({ - id: LOCAL_EXECUTION_HOST_ID, - label: getExecutionHostLabel(LOCAL_EXECUTION_HOST_ID) - }) - } - for (const [targetId, label] of sshTargetLabels) { - const id = toSshExecutionHostId(targetId) - if (!setupHostIds.has(id)) { - options.push({ id, label }) - } - } - const runtimeEnvironmentId = activeRuntimeEnvironmentId?.trim() - if (runtimeEnvironmentId) { - const id = toRuntimeExecutionHostId(runtimeEnvironmentId) - if (!setupHostIds.has(id)) { - options.push({ id, label: runtimeEnvironmentId }) - } - } - return options + return hostOptions + .filter((host) => !setupHostIds.has(host.id)) + .map((host) => ({ + id: host.id, + label: host.label || getExecutionHostLabel(host.id) + })) } export function RepositoryHostSetupsSection({ @@ -100,9 +83,30 @@ export function RepositoryHostSetupsSection({ const setupProjectExistingFolder = useAppStore((state) => state.setupProjectExistingFolder) const createProjectHostSetup = useAppStore((state) => state.createProjectHostSetup) const deleteProjectHostSetup = useAppStore((state) => state.deleteProjectHostSetup) + const repos = useAppStore((state) => state.repos) const sshTargetLabels = useAppStore((state) => state.sshTargetLabels) - const activeRuntimeEnvironmentId = useAppStore( - (state) => state.settings?.activeRuntimeEnvironmentId + const sshConnectionStates = useAppStore((state) => state.sshConnectionStates) + const settings = useAppStore((state) => state.settings) + const runtimeStatusByEnvironmentId = useAppStore((state) => state.runtimeStatusByEnvironmentId) + const hostLabelOverrides = useMemo(() => getHostDisplayLabelOverrides(settings), [settings]) + const hostOptions = useMemo( + () => + buildExecutionHostRegistry({ + repos, + settings, + sshTargetLabels, + sshConnectionStates, + runtimeStatusByEnvironmentId, + hostLabelOverrides + }), + [ + repos, + settings, + sshTargetLabels, + sshConnectionStates, + runtimeStatusByEnvironmentId, + hostLabelOverrides + ] ) const projectHostSetupProjection = useAppStore((state) => getProjectHostSetupProjectionFromState(state) @@ -117,8 +121,7 @@ export function RepositoryHostSetupsSection({ : [] const setupHostOptions = buildSetupHostOptions({ projectHostSetups, - sshTargetLabels, - activeRuntimeEnvironmentId + hostOptions }) const [selectedSetupHostId, setSelectedSetupHostId] = useState(null) const [setupPath, setSetupPath] = useState('') diff --git a/src/shared/execution-host-registry.test.ts b/src/shared/execution-host-registry.test.ts index 02561148e0e..10c3aed194c 100644 --- a/src/shared/execution-host-registry.test.ts +++ b/src/shared/execution-host-registry.test.ts @@ -162,4 +162,43 @@ describe('execution host registry', () => { { id: 'runtime:env-2', kind: 'runtime', label: 'env-2', health: 'available' } ]) }) + + it('includes runtime hosts from hydrated status even when they are not focused', () => { + const hosts = buildExecutionHostRegistry({ + repos: [], + settings: { activeRuntimeEnvironmentId: null }, + runtimeStatusByEnvironmentId: new Map([ + [ + 'gpu', + { + appVersion: '1.8.0', + status: { + runtimeId: 'runtime-gpu', + rendererGraphEpoch: 1, + graphStatus: 'ready', + authoritativeWindowId: 1, + liveTabCount: 0, + liveLeafCount: 0, + runtimeProtocolVersion: RUNTIME_PROTOCOL_VERSION, + minCompatibleRuntimeClientVersion: 1, + capabilities: ['project-host-setup.v1'], + hostPlatform: 'linux' + } + } + ] + ]) + }) + + expect(hosts).toMatchObject([ + { id: 'local', health: 'local' }, + { + id: 'runtime:gpu', + kind: 'runtime', + label: 'gpu', + health: 'available', + capabilities: ['project-host-setup.v1'], + platform: 'linux' + } + ]) + }) }) diff --git a/src/shared/execution-host-registry.ts b/src/shared/execution-host-registry.ts index 4addbaf4ce4..bfe12e0dbf7 100644 --- a/src/shared/execution-host-registry.ts +++ b/src/shared/execution-host-registry.ts @@ -161,6 +161,9 @@ export function buildExecutionHostRegistry(args: { args.runtimeStatusByEnvironmentId ) } + for (const environmentId of args.runtimeStatusByEnvironmentId?.keys() ?? []) { + addRuntimeHost(hosts, environmentId, environmentId, args.runtimeStatusByEnvironmentId) + } const focusedHost = getSettingsFocusedExecutionHostId(args.settings) const parsedFocusedHost = parseExecutionHostId(focusedHost)