mirror of
https://github.com/stablyai/orca.git
synced 2026-10-15 16:02:49 +00:00
Show unavailable hosts in workspace run target
Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
@@ -314,7 +314,10 @@ Needs:
|
||||
- compatibility resolver from `{ projectId, hostId }` to current repo/setup
|
||||
internals while old APIs remain
|
||||
|
||||
Current branch status: partially implemented.
|
||||
Current branch status: partially implemented. Ready project-host setups can be
|
||||
selected from the composer `Run on` menu, and known hosts that still need setup
|
||||
now appear as disabled rows instead of being invisible. Inline setup actions for
|
||||
those rows are still missing.
|
||||
|
||||
### 5. Project Setup Flow
|
||||
|
||||
@@ -471,10 +474,13 @@ Already partially landed:
|
||||
- CLI commands for listing projects/setups and creating worktrees by
|
||||
project/setup
|
||||
- composer `Run on` selector when a project has multiple ready setups
|
||||
- disabled `Run on` rows for known hosts where the selected project is not set
|
||||
up yet
|
||||
|
||||
Not complete yet:
|
||||
|
||||
- clone/provision flows
|
||||
- inline setup actions from the composer `Run on` menu
|
||||
- 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
|
||||
|
||||
@@ -354,7 +354,10 @@ Needs:
|
||||
- compatibility resolver from `{ projectId, hostId }` to current repo/setup
|
||||
internals while old APIs remain
|
||||
|
||||
Current branch status: partially implemented.
|
||||
Current branch status: partially implemented. Ready project-host setups can be
|
||||
selected from the composer `Run on` menu, and known hosts that still need setup
|
||||
now appear as disabled rows instead of being invisible. Inline setup actions for
|
||||
those rows are still missing.
|
||||
|
||||
### 5. Project Setup Flow
|
||||
|
||||
@@ -510,10 +513,13 @@ Already partially landed:
|
||||
- CLI commands for listing projects/setups and creating worktrees by
|
||||
project/setup
|
||||
- composer `Run on` selector when a project has multiple ready setups
|
||||
- disabled `Run on` rows for known hosts where the selected project is not set
|
||||
up yet
|
||||
|
||||
Not complete yet:
|
||||
|
||||
- clone/provision flows
|
||||
- inline setup actions from the composer `Run on` menu
|
||||
- bulk setup flows and setup for hosts that are not already known to the client
|
||||
- full project settings split into global and host-specific ownership
|
||||
- host settings/capability UI aligned with project setup
|
||||
|
||||
@@ -743,6 +743,10 @@ Landed so far:
|
||||
panes, and imports an existing folder on another known local, SSH, or active
|
||||
runtime host. Setup routing now follows the selected setup host instead of the
|
||||
currently focused runtime.
|
||||
- Updated the new workspace composer `Run on` menu so known hosts can appear as
|
||||
disabled "project not set up on this host" rows, not only as invisible missing
|
||||
options. Ready setups remain selectable and still resolve through the
|
||||
compatibility repo path.
|
||||
- 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, and runtime RPC
|
||||
@@ -764,8 +768,8 @@ Important limitation:
|
||||
|
||||
Remaining end-to-end work:
|
||||
|
||||
- teach workspace creation to resolve `{ projectId, hostId }` through a ready
|
||||
project-host setup
|
||||
- add inline clone/import actions for `Run on` hosts where the project is not
|
||||
set up yet
|
||||
- broaden setup-on-host flows beyond known local, SSH, and active runtime hosts
|
||||
- add clone/provision setup-on-host flows
|
||||
- split settings into explicit client, host, project, and project-host setup
|
||||
|
||||
@@ -19,14 +19,18 @@ export default function ProjectHostSetupCombobox({
|
||||
onValueChange
|
||||
}: ProjectHostSetupComboboxProps): React.JSX.Element {
|
||||
const [open, setOpen] = React.useState(false)
|
||||
const selected = options.find((option) => option.id === value) ?? options[0] ?? null
|
||||
const readyOptions = options.filter((option) => option.kind === 'ready')
|
||||
const selected = readyOptions.find((option) => option.id === value) ?? readyOptions[0] ?? null
|
||||
|
||||
const handleSelect = React.useCallback(
|
||||
(setupId: string): void => {
|
||||
if (!readyOptions.some((option) => option.id === setupId)) {
|
||||
return
|
||||
}
|
||||
onValueChange(setupId)
|
||||
setOpen(false)
|
||||
},
|
||||
[onValueChange]
|
||||
[onValueChange, readyOptions]
|
||||
)
|
||||
|
||||
return (
|
||||
@@ -72,6 +76,7 @@ export default function ProjectHostSetupCombobox({
|
||||
key={option.id}
|
||||
value={option.id}
|
||||
onSelect={() => handleSelect(option.id)}
|
||||
disabled={option.kind !== 'ready'}
|
||||
className="items-center gap-2 px-3 py-2"
|
||||
>
|
||||
<Check
|
||||
@@ -84,7 +89,7 @@ export default function ProjectHostSetupCombobox({
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="truncate text-sm">{option.label}</div>
|
||||
<div className="mt-0.5 truncate text-[11px] text-muted-foreground">
|
||||
{option.path}
|
||||
{option.kind === 'ready' ? option.path : option.detail}
|
||||
</div>
|
||||
</div>
|
||||
</CommandItem>
|
||||
|
||||
@@ -88,6 +88,8 @@ import {
|
||||
buildProjectHostSetupOptions,
|
||||
type ProjectHostSetupOption
|
||||
} from '@/lib/project-host-setup-options'
|
||||
import { buildExecutionHostRegistry } from '../../../shared/execution-host-registry'
|
||||
import { getHostDisplayLabelOverrides } from '../../../shared/host-setting-overrides'
|
||||
import { queueNewWorkspaceTerminalFocus } from '@/lib/new-workspace-terminal-focus'
|
||||
import { getSettingsForRepoRuntimeOwner } from '@/lib/repo-runtime-owner'
|
||||
import { getSuggestedCreatureName } from '@/components/sidebar/worktree-name-suggestions'
|
||||
@@ -331,7 +333,9 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
|
||||
const sparsePresetsByRepo = useAppStore((s) => s.sparsePresetsByRepo)
|
||||
const workspaceStatuses = useAppStore((s) => s.workspaceStatuses)
|
||||
const sshConnectionStates = useAppStore((s) => s.sshConnectionStates)
|
||||
const sshTargetLabels = useAppStore((s) => s.sshTargetLabels)
|
||||
const sshConnectedGeneration = useAppStore((s) => s.sshConnectedGeneration)
|
||||
const runtimeStatusByEnvironmentId = useAppStore((s) => s.runtimeStatusByEnvironmentId)
|
||||
const workspaceHostScope = useAppStore((s) => s.workspaceHostScope)
|
||||
const eligibleRepos = useMemo(() => getComposerEligibleRepos(repos), [repos])
|
||||
const draftRepoId = persistDraft ? (newWorkspaceDraft?.repoId ?? null) : null
|
||||
@@ -382,14 +386,27 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
|
||||
selectedWorkspaceTarget.status === 'ready'
|
||||
? selectedWorkspaceTarget.target.projectHostSetupId
|
||||
: null
|
||||
const hostOptions = useMemo(
|
||||
() =>
|
||||
buildExecutionHostRegistry({
|
||||
repos,
|
||||
settings,
|
||||
sshTargetLabels,
|
||||
sshConnectionStates,
|
||||
runtimeStatusByEnvironmentId,
|
||||
hostLabelOverrides: getHostDisplayLabelOverrides(settings)
|
||||
}),
|
||||
[repos, settings, sshConnectionStates, sshTargetLabels, runtimeStatusByEnvironmentId]
|
||||
)
|
||||
const projectHostSetupOptions = useMemo(
|
||||
() =>
|
||||
buildProjectHostSetupOptions({
|
||||
projectId: selectedProjectId,
|
||||
projectHostSetups,
|
||||
eligibleRepos
|
||||
eligibleRepos,
|
||||
hosts: hostOptions
|
||||
}),
|
||||
[eligibleRepos, projectHostSetups, selectedProjectId]
|
||||
[eligibleRepos, hostOptions, projectHostSetups, selectedProjectId]
|
||||
)
|
||||
const selectedRepoSettings = useMemo(() => {
|
||||
if (!settings) {
|
||||
@@ -1679,7 +1696,7 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS
|
||||
const handleProjectHostSetupChange = useCallback(
|
||||
(setupId: string): void => {
|
||||
const option = projectHostSetupOptions.find((candidate) => candidate.id === setupId)
|
||||
if (!option) {
|
||||
if (!option || option.kind !== 'ready') {
|
||||
return
|
||||
}
|
||||
handleRepoChange(option.repoId)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { ExecutionHostId } from '../../../shared/execution-host'
|
||||
import type { ExecutionHostRegistryEntry } from '../../../shared/execution-host-registry'
|
||||
import type { ProjectHostSetup, Repo } from '../../../shared/types'
|
||||
import { buildProjectHostSetupOptions } from './project-host-setup-options'
|
||||
|
||||
@@ -35,6 +36,20 @@ function setup(
|
||||
}
|
||||
}
|
||||
|
||||
function host(
|
||||
id: ExecutionHostId,
|
||||
overrides: Partial<ExecutionHostRegistryEntry> = {}
|
||||
): ExecutionHostRegistryEntry {
|
||||
return {
|
||||
id,
|
||||
kind: id === 'local' ? 'local' : id.startsWith('ssh:') ? 'ssh' : 'runtime',
|
||||
label: id === 'local' ? 'Local Mac' : id.replace(/^ssh:|^runtime:/, ''),
|
||||
detail: id === 'local' ? 'This computer' : 'Host',
|
||||
health: id === 'local' ? 'local' : 'available',
|
||||
...overrides
|
||||
}
|
||||
}
|
||||
|
||||
describe('buildProjectHostSetupOptions', () => {
|
||||
it('returns ready setup choices for one project sorted with local first', () => {
|
||||
const options = buildProjectHostSetupOptions({
|
||||
@@ -66,4 +81,23 @@ describe('buildProjectHostSetupOptions', () => {
|
||||
|
||||
expect(options.map((option) => option.id)).toEqual(['ready'])
|
||||
})
|
||||
|
||||
it('includes known hosts that still need project setup', () => {
|
||||
const options = buildProjectHostSetupOptions({
|
||||
projectId: 'project-1',
|
||||
eligibleRepos: [repo('local-repo')],
|
||||
hosts: [host('local'), host('ssh:builder', { label: 'Builder' })],
|
||||
projectHostSetups: [setup('local', 'project-1', 'local', 'local-repo')]
|
||||
})
|
||||
|
||||
expect(options).toEqual([
|
||||
expect.objectContaining({ id: 'local', kind: 'ready', label: 'Local Mac' }),
|
||||
expect.objectContaining({
|
||||
id: 'needs-setup:ssh:builder',
|
||||
kind: 'needs-setup',
|
||||
label: 'Builder',
|
||||
detail: 'Project not set up on this host'
|
||||
})
|
||||
])
|
||||
})
|
||||
})
|
||||
|
||||
@@ -3,32 +3,78 @@ import {
|
||||
LOCAL_EXECUTION_HOST_ID,
|
||||
type ExecutionHostId
|
||||
} from '../../../shared/execution-host'
|
||||
import type { ExecutionHostRegistryEntry } from '../../../shared/execution-host-registry'
|
||||
import type { ProjectHostSetup, Repo } from '../../../shared/types'
|
||||
|
||||
export type ProjectHostSetupOption = {
|
||||
id: string
|
||||
export type ProjectHostSetupOption =
|
||||
| {
|
||||
id: string
|
||||
kind: 'ready'
|
||||
projectId: string
|
||||
hostId: ExecutionHostId
|
||||
repoId: string
|
||||
label: string
|
||||
detail: string
|
||||
path: string
|
||||
}
|
||||
| {
|
||||
id: string
|
||||
kind: 'needs-setup'
|
||||
projectId: string
|
||||
hostId: ExecutionHostId
|
||||
label: string
|
||||
detail: string
|
||||
}
|
||||
|
||||
export type ReadyProjectHostSetupOption = Extract<ProjectHostSetupOption, { kind: 'ready' }>
|
||||
|
||||
type NeedsSetupProjectHostOption = Extract<ProjectHostSetupOption, { kind: 'needs-setup' }>
|
||||
|
||||
type BuildReadySetupOptionsInput = {
|
||||
projectId: string
|
||||
hostId: ExecutionHostId
|
||||
repoId: string
|
||||
label: string
|
||||
detail: string
|
||||
path: string
|
||||
projectHostSetups: readonly ProjectHostSetup[]
|
||||
eligibleRepos: readonly Repo[]
|
||||
}
|
||||
|
||||
type BuildNeedsSetupOptionsInput = {
|
||||
projectId: string
|
||||
hosts: readonly ExecutionHostRegistryEntry[]
|
||||
readySetupByHost: ReadonlyMap<ExecutionHostId, ReadyProjectHostSetupOption>
|
||||
}
|
||||
|
||||
type BuildProjectHostSetupOptionsInput = {
|
||||
projectId: string | null
|
||||
projectHostSetups: readonly ProjectHostSetup[]
|
||||
eligibleRepos: readonly Repo[]
|
||||
hosts?: readonly ExecutionHostRegistryEntry[]
|
||||
}
|
||||
|
||||
export function buildProjectHostSetupOptions({
|
||||
projectId,
|
||||
projectHostSetups,
|
||||
eligibleRepos
|
||||
eligibleRepos,
|
||||
hosts = []
|
||||
}: BuildProjectHostSetupOptionsInput): ProjectHostSetupOption[] {
|
||||
if (!projectId) {
|
||||
return []
|
||||
}
|
||||
const readyOptions = buildReadySetupOptions({ projectId, projectHostSetups, eligibleRepos })
|
||||
const readySetupByHost = new Map(readyOptions.map((option) => [option.hostId, option]))
|
||||
return [
|
||||
...readyOptions,
|
||||
...buildNeedsSetupOptions({
|
||||
projectId,
|
||||
hosts,
|
||||
readySetupByHost
|
||||
})
|
||||
].sort((a, b) => compareProjectHostSetupOptions(a, b))
|
||||
}
|
||||
|
||||
function buildReadySetupOptions({
|
||||
projectId,
|
||||
projectHostSetups,
|
||||
eligibleRepos
|
||||
}: BuildReadySetupOptionsInput): ReadyProjectHostSetupOption[] {
|
||||
const eligibleRepoIds = new Set(eligibleRepos.map((repo) => repo.id))
|
||||
return projectHostSetups
|
||||
.filter(
|
||||
@@ -39,6 +85,7 @@ export function buildProjectHostSetupOptions({
|
||||
)
|
||||
.map((setup) => ({
|
||||
id: setup.id,
|
||||
kind: 'ready' as const,
|
||||
projectId: setup.projectId,
|
||||
hostId: setup.hostId,
|
||||
repoId: setup.repoId,
|
||||
@@ -46,7 +93,23 @@ export function buildProjectHostSetupOptions({
|
||||
detail: setup.displayName,
|
||||
path: setup.path
|
||||
}))
|
||||
.sort((a, b) => compareProjectHostSetupOptions(a, b))
|
||||
}
|
||||
|
||||
function buildNeedsSetupOptions({
|
||||
projectId,
|
||||
hosts,
|
||||
readySetupByHost
|
||||
}: 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'
|
||||
}))
|
||||
}
|
||||
|
||||
function compareProjectHostSetupOptions(
|
||||
@@ -59,5 +122,7 @@ function compareProjectHostSetupOptions(
|
||||
if (b.hostId === LOCAL_EXECUTION_HOST_ID && a.hostId !== LOCAL_EXECUTION_HOST_ID) {
|
||||
return 1
|
||||
}
|
||||
return a.label.localeCompare(b.label) || a.path.localeCompare(b.path)
|
||||
const aDetail = a.kind === 'ready' ? a.path : a.detail
|
||||
const bDetail = b.kind === 'ready' ? b.path : b.detail
|
||||
return a.label.localeCompare(b.label) || aDetail.localeCompare(bDetail)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user