mirror of
https://github.com/stablyai/orca.git
synced 2026-10-24 16:03:45 +00:00
feat(workspace): set project location from the create-worktree host picker (#14868)
* feat(workspace): set project location from the create-worktree host picker Hosts that still say "Project location not set" now get an inline Set location action. It opens a nested dialog over Create worktree so the in-progress form stays put. * fix(workspace): replace unset-location status copy with a button Drop the redundant "Project location not set" caption and show a Set project location action with a hover tooltip instead. * refactor(workspace): tighten the set-project-location dialog - reuse CreateProjectParentBrowser instead of a second host-filesystem browse view - drop ProjectLocationBrowseTarget; parseExecutionHostId already models it - single setLocation path in RunTargetCombobox (row, button, Enter) - memoize the default clone URL instead of scanning on every store update - fix missing required props in the new composer-card test * fix(workspace): close the correctness gaps in set-project-location - Escape in the host browser now backs out to the form instead of dismissing the dialog and discarding the half-filled path/clone URL. Radix dismisses from a document-capture listener, so only preventDefault can stop it. - Drop the stopPropagation guards: window-capture (the composer's Escape handler) already ran by then, so they never protected it — the nestedDialogOpen gate does. They did silently kill RemoteFileBrowser's own key handling. - Hide Set project location for host-local repo:<id> projects (folder projects, git repos with no remote). Linking on another host matches by project identity, which those have none of, so the call could only ever toast an error. - Drop a standalone placeholder setup once a repo projection covers the same project+host, restoring the (projectId, hostId) uniqueness invariant. Setting a location on a host with a pending setup was leaving a ghost that sorts first and reads back as 'not set up'. - Existing-folder submit label matched a catalog string reading 'Importing...' * test(composer): follow the renamed local in the host-retarget source assertion
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import type { ProjectHostSetup } from '../../../shared/project-types'
|
||||
import type { Repo } from '../../../shared/repo-types'
|
||||
import { mergeProjectHostSetupCompatibilityState } from './project-host-compatibility'
|
||||
|
||||
const PROJECT_ID = 'github:acme/orca'
|
||||
|
||||
function repo(overrides: Partial<Repo> & Pick<Repo, 'id'>): Repo {
|
||||
return {
|
||||
path: `/src/${overrides.id}`,
|
||||
displayName: 'orca',
|
||||
addedAt: 1,
|
||||
upstream: { owner: 'acme', repo: 'orca' },
|
||||
...overrides
|
||||
} as Repo
|
||||
}
|
||||
|
||||
function pendingSetup(overrides: Partial<ProjectHostSetup> = {}): ProjectHostSetup {
|
||||
return {
|
||||
id: `${PROJECT_ID}::ssh:devbox`,
|
||||
projectId: PROJECT_ID,
|
||||
hostId: 'ssh:devbox',
|
||||
repoId: '',
|
||||
path: '',
|
||||
displayName: 'orca',
|
||||
setupState: 'not-set-up',
|
||||
setupMethod: 'pending',
|
||||
createdAt: 1,
|
||||
updatedAt: 1,
|
||||
...overrides
|
||||
} as ProjectHostSetup
|
||||
}
|
||||
|
||||
describe('mergeProjectHostSetupCompatibilityState', () => {
|
||||
it('keeps a placeholder for a host with no repo yet', () => {
|
||||
const merged = mergeProjectHostSetupCompatibilityState(
|
||||
{ projects: [], projectHostSetups: [pendingSetup()] },
|
||||
[repo({ id: 'local-repo' })]
|
||||
)
|
||||
|
||||
expect(merged.projectHostSetups.map((setup) => setup.hostId)).toEqual(['local', 'ssh:devbox'])
|
||||
})
|
||||
|
||||
// Setting a location on a host that already had a placeholder projects a ready setup for
|
||||
// the same project+host; the placeholder must not survive to shadow it.
|
||||
it('drops a placeholder once a repo covers the same project and host', () => {
|
||||
const merged = mergeProjectHostSetupCompatibilityState(
|
||||
{ projects: [], projectHostSetups: [pendingSetup()] },
|
||||
[repo({ id: 'local-repo' }), repo({ id: 'devbox-repo', executionHostId: 'ssh:devbox' })]
|
||||
)
|
||||
|
||||
expect(merged.projectHostSetups).toHaveLength(2)
|
||||
expect(merged.projectHostSetups.every((setup) => setup.setupState === 'ready')).toBe(true)
|
||||
expect(merged.projectHostSetups.map((setup) => setup.id)).toEqual(['local-repo', 'devbox-repo'])
|
||||
})
|
||||
|
||||
it('keeps the placeholder when the repo covers a different host', () => {
|
||||
const merged = mergeProjectHostSetupCompatibilityState(
|
||||
{ projects: [], projectHostSetups: [pendingSetup({ hostId: 'ssh:other' })] },
|
||||
[repo({ id: 'devbox-repo', executionHostId: 'ssh:devbox' })]
|
||||
)
|
||||
|
||||
expect(merged.projectHostSetups.map((setup) => setup.hostId)).toEqual([
|
||||
'ssh:devbox',
|
||||
'ssh:other'
|
||||
])
|
||||
})
|
||||
})
|
||||
@@ -23,6 +23,10 @@ export function isRepoBackedProjectHostSetup(
|
||||
return repoId.length > 0 && (currentRepoIds.has(repoId) || setup.id === repoId)
|
||||
}
|
||||
|
||||
function projectHostKey(setup: Pick<ProjectHostSetup, 'projectId' | 'hostId'>): string {
|
||||
return `${setup.projectId}\u0000${setup.hostId}`
|
||||
}
|
||||
|
||||
export function mergeProjectHostSetupCompatibilityState(
|
||||
state: Pick<PersistedState, 'projects' | 'projectHostSetups'>,
|
||||
repos: readonly Repo[]
|
||||
@@ -35,6 +39,7 @@ export function mergeProjectHostSetupCompatibilityState(
|
||||
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))
|
||||
const projectedHosts = new Set(projection.setups.map(projectHostKey))
|
||||
// Why: legacy/repo-backed setup rows reuse the repo id; keep only independent rows so repo deletion leaves no ghosts.
|
||||
const independentSetups = (state.projectHostSetups ?? [])
|
||||
.filter((setup) => {
|
||||
@@ -48,6 +53,10 @@ export function mergeProjectHostSetupCompatibilityState(
|
||||
const remappedProjectId = succession.remappedProjectIds.get(setup.projectId)
|
||||
return remappedProjectId ? { ...setup, projectId: remappedProjectId } : setup
|
||||
})
|
||||
// Why: a project resolves to one setup per host. Once a repo projection covers that
|
||||
// pair, a leftover placeholder is a ghost that shadows the ready row — it sorts first
|
||||
// and reads back as "not set up". Runs after the remap so renamed rows are caught too.
|
||||
.filter((setup) => !projectedHosts.has(projectHostKey(setup)))
|
||||
const independentProjectIds = new Set(independentSetups.map((setup) => setup.projectId))
|
||||
const independentProjects = (state.projects ?? [])
|
||||
.filter(
|
||||
|
||||
Reference in New Issue
Block a user