reland(workspace): set project location from the create-worktree host picker (#14965)

* feat(workspace): reland set project location from the create-worktree host picker

Relands #14868 (reverted by #14912) with a fix for the regression that caused the
revert: setting a project location could change the path before Orca used it.

The retarget-after-setup path read the raw store record to find a just-created
setup, because the memoized picker options had not refreshed yet:

    useAppStore.getState().projectHostSetups.find(
      (candidate) => candidate.id === setupId && candidate.setupState === 'ready'
    )

That hand-rolls a second selection path that skips every rule the option builder
applies — repo eligibility, ephemeral-VM and runtime-owned SSH host exclusion,
and the one-setup-per-host dedupe whose own comment notes that
resolveWorkspaceCreationTarget takes the first project+host match and ignores the
rest. So the composer could be retargeted at a setup other than the canonical one
for that host, pointing creation at a different location than the one chosen.

Resolves through buildProjectHostSetupOptions against fresh store state instead,
so the fallback and the steady-state picker agree by construction.

STA-4547

* fix(workspace): sanitize the clone prefill and drop an abandoned set-location

Review follow-ups on this PR.

The "Clone from URL" prefill seeded the field with the verbatim `git remote` URL,
which can embed a PAT (`https://x-access-token:ghp_...@github.com/...`). The
clone then runs on the *target* host, writing that token into its .git/config —
a credential the user never typed into this flow, now readable by anyone on a
shared host. Strip it with the same sanitizer `getProvisionedRootRecipeRepoUrl`
already applies to the ephemeral-VM recipe URL. Extracted to
resolveProjectCloneUrlPrefill so the rule is directly testable.

The dialog also stays dismissable while a submit is in flight, and an SSH clone
is unbounded. A clone the user backed out of minutes earlier still called
onReady, silently moving the run target and resetting start-from under a form
they had since pointed at another host. Drop the result if the dialog went away.

* fix(workspace): re-arm the abandoned guard on mount

StrictMode runs mount/cleanup/mount, so latching `abandoned` on the first
cleanup left it true for the rest of the session and permanently suppressed
onReady — the app wraps its root in StrictMode. Reset it on mount.
This commit is contained in:
Neil
2026-08-16 16:50:58 -07:00
committed by GitHub
parent 3f58d5cf9a
commit 85565a9302
20 changed files with 1438 additions and 28 deletions
+12 -1
View File
@@ -93,6 +93,8 @@ export function isProjectRemoteIdentityPending(
return repo.gitRemoteIdentity === undefined && !hasProjectRemoteIdentity(repo)
}
const HOST_LOCAL_PROJECT_ID_PREFIX = 'repo:'
export function getProjectIdentityKey(
repo: Pick<Repo, 'id' | 'upstream' | 'repoIcon' | 'gitRemoteIdentity'>
): string {
@@ -104,7 +106,16 @@ export function getProjectIdentityKey(
if (gitRemoteIdentity) {
return `git:${gitRemoteIdentity.canonicalKey}`
}
return `repo:${repo.id}`
return `${HOST_LOCAL_PROJECT_ID_PREFIX}${repo.id}`
}
/**
* True for the `repo:<id>` fallback above — a folder project, or a git repo with no
* remote. The id is a per-host repo id, so the same project on another host derives a
* different one and can never be matched there.
*/
export function isHostLocalProjectId(projectId: string): boolean {
return projectId.startsWith(HOST_LOCAL_PROJECT_ID_PREFIX)
}
export function getProjectIdForProviderIdentity(identity: ProjectProviderIdentity): string {