From b60df2e3d64b2cc61ec385cae7bb8f4edaccd416 Mon Sep 17 00:00:00 2001 From: Jinjing <6427696+AmethystLiang@users.noreply.github.com> Date: Sun, 16 Aug 2026 10:41:39 -0700 Subject: [PATCH] =?UTF-8?q?Revert=20"feat(workspace):=20set=20project=20lo?= =?UTF-8?q?cation=20from=20the=20create-worktree=20host=20p=E2=80=A6"=20(#?= =?UTF-8?q?14912)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit e4e54a17d0551fa992bafa945aed706f8d155378. --- .../project-host-compatibility.test.ts | 68 ----- .../project-host-compatibility.ts | 9 - ...orkspaceComposerCard.set-location.test.tsx | 238 --------------- .../NewWorkspaceComposerCard.test.tsx | 16 +- .../components/NewWorkspaceComposerCard.tsx | 47 +-- .../components/NewWorkspaceComposerModal.tsx | 6 +- .../new-workspace/RunTargetCombobox.tsx | 42 +-- .../new-workspace/RunTargetComboboxRow.tsx | 50 ---- .../SetProjectLocationDialog.test.tsx | 177 ----------- .../SetProjectLocationDialog.tsx | 278 ------------------ .../new-workspace/SetProjectLocationForms.tsx | 224 -------------- .../pick-local-project-folder.ts | 14 - .../useComposerState-host-retarget.test.ts | 2 +- src/renderer/src/hooks/useComposerState.ts | 15 +- src/renderer/src/i18n/locales/en.json | 12 - .../lib/project-host-setup-options.test.ts | 36 +-- .../src/lib/project-host-setup-options.ts | 22 -- src/shared/project-host-setup-projection.ts | 13 +- 18 files changed, 26 insertions(+), 1243 deletions(-) delete mode 100644 src/main/persistence/tracking-repos/project-host-compatibility.test.ts delete mode 100644 src/renderer/src/components/NewWorkspaceComposerCard.set-location.test.tsx delete mode 100644 src/renderer/src/components/new-workspace/SetProjectLocationDialog.test.tsx delete mode 100644 src/renderer/src/components/new-workspace/SetProjectLocationDialog.tsx delete mode 100644 src/renderer/src/components/new-workspace/SetProjectLocationForms.tsx delete mode 100644 src/renderer/src/components/new-workspace/pick-local-project-folder.ts diff --git a/src/main/persistence/tracking-repos/project-host-compatibility.test.ts b/src/main/persistence/tracking-repos/project-host-compatibility.test.ts deleted file mode 100644 index 540dc6955f7..00000000000 --- a/src/main/persistence/tracking-repos/project-host-compatibility.test.ts +++ /dev/null @@ -1,68 +0,0 @@ -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 & Pick): Repo { - return { - path: `/src/${overrides.id}`, - displayName: 'orca', - addedAt: 1, - upstream: { owner: 'acme', repo: 'orca' }, - ...overrides - } as Repo -} - -function pendingSetup(overrides: Partial = {}): 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' - ]) - }) -}) diff --git a/src/main/persistence/tracking-repos/project-host-compatibility.ts b/src/main/persistence/tracking-repos/project-host-compatibility.ts index 0b51c758652..88e80ecdb93 100644 --- a/src/main/persistence/tracking-repos/project-host-compatibility.ts +++ b/src/main/persistence/tracking-repos/project-host-compatibility.ts @@ -23,10 +23,6 @@ export function isRepoBackedProjectHostSetup( return repoId.length > 0 && (currentRepoIds.has(repoId) || setup.id === repoId) } -function projectHostKey(setup: Pick): string { - return `${setup.projectId}\u0000${setup.hostId}` -} - export function mergeProjectHostSetupCompatibilityState( state: Pick, repos: readonly Repo[] @@ -39,7 +35,6 @@ 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) => { @@ -53,10 +48,6 @@ 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( diff --git a/src/renderer/src/components/NewWorkspaceComposerCard.set-location.test.tsx b/src/renderer/src/components/NewWorkspaceComposerCard.set-location.test.tsx deleted file mode 100644 index 92c7e889075..00000000000 --- a/src/renderer/src/components/NewWorkspaceComposerCard.set-location.test.tsx +++ /dev/null @@ -1,238 +0,0 @@ -// @vitest-environment happy-dom - -import React, { act } from 'react' -import { createRoot } from 'react-dom/client' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import NewWorkspaceComposerCard from './NewWorkspaceComposerCard' -import type { NewWorkspaceProjectOption } from '@/lib/new-workspace-project-options' -import type { ProjectHostSetupOption } from '@/lib/project-host-setup-options' - -const storeMocks = vi.hoisted(() => ({ - closeModal: vi.fn(), - openModal: vi.fn(), - openSettingsPage: vi.fn(), - openSettingsTarget: vi.fn() -})) - -vi.mock('@/store', () => ({ - useAppStore: Object.assign( - (selector: (state: unknown) => unknown) => - selector({ - closeModal: storeMocks.closeModal, - openModal: storeMocks.openModal, - openSettingsPage: storeMocks.openSettingsPage, - openSettingsTarget: storeMocks.openSettingsTarget, - setRuntimeEnvironmentStatus: vi.fn(), - setupProjectExistingFolder: vi.fn(), - setupProjectClone: vi.fn(), - activeModal: 'new-workspace-composer', - settings: { defaultTuiAgent: null, disabledTuiAgents: [] }, - updateSettings: vi.fn(), - projects: [], - repos: [] - }), - { getState: () => ({}) } - ) -})) - -vi.mock('@/components/contextual-tours/use-contextual-tour', () => ({ - useContextualTour: vi.fn() -})) - -vi.mock('@/components/ui/tooltip', () => ({ - Tooltip: ({ children }: { children: React.ReactNode }) => <>{children}, - TooltipContent: ({ children }: { children: React.ReactNode }) => <>{children}, - TooltipTrigger: ({ children }: { children: React.ReactNode }) => <>{children} -})) - -vi.mock('@/components/ui/tooltip', () => ({ - Tooltip: ({ children }: { children: React.ReactNode }) => <>{children}, - TooltipContent: ({ children }: { children: React.ReactNode }) => <>{children}, - TooltipTrigger: ({ children }: { children: React.ReactNode }) => <>{children} -})) - -vi.mock('@/components/agent/AgentCombobox', () => ({ - default: () => -})) - -vi.mock('@/components/sidebar/AddRemoteHostDialog', () => ({ - AddRemoteHostDialog: () => null -})) - -vi.mock('@/components/sparse/SparseCheckoutPresetSelect', () => ({ - default: () => null -})) - -vi.mock('@/components/new-workspace/SmartWorkspaceNameField', () => ({ - default: () => -})) - -vi.mock('@/components/new-workspace/ProjectCombobox', () => ({ - default: () =>
-})) - -vi.mock('@/components/new-workspace/SetProjectLocationDialog', () => ({ - SetProjectLocationDialog: ({ - option, - projectName - }: { - option: { label: string } | null - projectName: string - }) => - option ? ( -
- ) : null -})) - -const projectOptions: NewWorkspaceProjectOption[] = [ - { - kind: 'project-group', - id: 'project-group:platform', - projectGroupId: 'platform', - displayName: 'Platform', - badgeColor: 'var(--muted-foreground)', - detail: '/workspace/platform', - parentPath: '/workspace/platform', - connectionId: null - } -] - -const hostOptions: ProjectHostSetupOption[] = [ - { - kind: 'ready', - id: 'setup-local', - projectId: 'project-group:platform', - hostId: 'local', - repoId: 'repo-a', - label: 'Local Mac', - detail: 'Orca', - path: '/Users/alice/orca' - }, - { - kind: 'needs-setup', - id: 'needs-setup:ssh:devbox', - projectId: 'project-group:platform', - hostId: 'ssh:devbox', - label: 'Devbox', - detail: 'Project location not set', - isAvailable: true, - attention: false, - canSetLocation: true - } -] - -function renderCard( - overrides: Partial> = {} -): HTMLDivElement { - const container = document.createElement('div') - document.body.appendChild(container) - const root = createRoot(container) - act(() => { - root.render( - {}} - eligibleRepos={[]} - repoId="repo-a" - projectOptions={projectOptions} - selectedProjectId="project-group:platform" - selectedRepoIsGit - onRepoChange={() => {}} - onProjectChange={() => {}} - primaryActionLabel="Create workspace" - name="" - onNameValueChange={() => {}} - onSmartGitHubItemSelect={() => {}} - onSmartGitLabItemSelect={() => {}} - onSmartBranchSelect={() => {}} - onSmartLinearIssueSelect={() => {}} - smartNameSelection={null} - onClearSmartNameSelection={() => {}} - canReuseSelectedBranch={false} - reuseSelectedBranch={false} - onReuseSelectedBranchChange={() => {}} - forkPushWarning={null} - detectedAgentIds={null} - onOpenAgentSettings={() => {}} - advancedOpen={false} - onToggleAdvanced={() => {}} - createDisabled={false} - projectError={null} - creating={false} - onCreate={() => {}} - note="" - onNoteChange={() => {}} - setupConfig={null} - requiresExplicitSetupChoice={false} - setupDecision={null} - onSetupDecisionChange={() => {}} - setupAgentStartupPolicy="start-immediately" - onSetupAgentStartupPolicyChange={() => {}} - shouldWaitForSetupCheck={false} - resolvedSetupDecision={null} - createError={null} - selectedRepoConnectionId={null} - selectedRepoSshStatus={null} - selectedRepoRequiresConnection={false} - selectedRepoConnectInProgress={false} - onConnectSelectedRepo={async () => {}} - canUseSparseCheckout={false} - sparsePresets={[]} - sparseSelectedPresetId={null} - onSparseSelectPreset={() => {}} - branchNameOverride={undefined} - onBranchNameOverrideChange={() => {}} - branchesEnabled={false} - setupControlsEnabled={false} - sparseControlsEnabled={false} - projectHostSetupOptions={hostOptions} - selectedProjectHostSetupId="setup-local" - {...overrides} - /> - ) - }) - return container -} - -describe('NewWorkspaceComposerCard set location', () => { - let container: HTMLDivElement | null = null - - beforeEach(() => { - storeMocks.closeModal.mockReset() - storeMocks.openModal.mockReset() - storeMocks.openSettingsPage.mockReset() - }) - - afterEach(() => { - container?.remove() - container = null - }) - - it('opens set-location over the composer without leaving the create dialog', () => { - const nestedOpenChanges: boolean[] = [] - container = renderCard({ - onNestedDialogOpenChange: (open) => nestedOpenChanges.push(open) - }) - - act(() => { - container?.querySelector('div[data-run-target-combobox-root="true"]')?.click() - }) - const setLocation = [...document.body.querySelectorAll('button')].find( - (button) => button.textContent?.includes('Set project location') - ) - expect(setLocation).toBeTruthy() - act(() => setLocation?.click()) - - const dialog = document.body.querySelector('[data-testid="set-project-location-dialog"]') - expect(dialog?.getAttribute('data-host')).toBe('Devbox') - expect(dialog?.getAttribute('data-project')).toBe('Platform') - expect(nestedOpenChanges).toEqual([true]) - expect(storeMocks.closeModal).not.toHaveBeenCalled() - expect(storeMocks.openModal).not.toHaveBeenCalled() - expect(storeMocks.openSettingsPage).not.toHaveBeenCalled() - }) -}) diff --git a/src/renderer/src/components/NewWorkspaceComposerCard.test.tsx b/src/renderer/src/components/NewWorkspaceComposerCard.test.tsx index 9775cd06dae..33fc740980b 100644 --- a/src/renderer/src/components/NewWorkspaceComposerCard.test.tsx +++ b/src/renderer/src/components/NewWorkspaceComposerCard.test.tsx @@ -31,9 +31,7 @@ vi.mock('@/store', () => ({ setRuntimeEnvironmentStatus: storeMocks.setRuntimeEnvironmentStatus, activeModal: 'none', settings: { defaultTuiAgent: null, disabledTuiAgents: [] }, - updateSettings: vi.fn(), - projects: [], - repos: [] + updateSettings: vi.fn() }), { getState: () => ({ @@ -64,10 +62,6 @@ vi.mock('@/components/sidebar/AddRemoteHostDialog', () => ({ mode ?
: null })) -vi.mock('@/components/new-workspace/SetProjectLocationDialog', () => ({ - SetProjectLocationDialog: () => null -})) - vi.mock('@/components/sparse/SparseCheckoutPresetSelect', () => ({ default: () =>
})) @@ -161,8 +155,7 @@ const devboxNeedsSetupHostOption: ProjectHostSetupOption = { label: 'Devbox', detail: 'Project location not set', isAvailable: true, - attention: false, - canSetLocation: true + attention: false } const disconnectedDevboxNeedsSetupHostOption: ProjectHostSetupOption = { @@ -174,7 +167,6 @@ const disconnectedDevboxNeedsSetupHostOption: ProjectHostSetupOption = { detail: 'Connect this host to set up projects', isAvailable: false, attention: false, - canSetLocation: false, connectAction: { kind: 'ssh', targetId: 'devbox' } } @@ -187,7 +179,6 @@ const disconnectedBastionNeedsSetupHostOption: ProjectHostSetupOption = { detail: 'Connect this host to set up projects', isAvailable: false, attention: false, - canSetLocation: false, connectAction: { kind: 'ssh', targetId: 'bastion' } } @@ -610,8 +601,7 @@ describe('NewWorkspaceComposerCard folder task source mode', () => { openRunTargetPicker(current.container) const devboxItem = findRunTargetItem('Devbox') - expect(devboxItem?.textContent).not.toContain('Project location not set') - expect(devboxItem?.textContent).toContain('Set project location') + expect(devboxItem?.textContent).toContain('Project location not set') // Not-connected rows stay highlightable (never `disabled`) so they hover like // the other rows; they're quieted visually instead. expect(devboxItem?.hasAttribute('data-disabled')).toBe(false) diff --git a/src/renderer/src/components/NewWorkspaceComposerCard.tsx b/src/renderer/src/components/NewWorkspaceComposerCard.tsx index 29b7760d055..4606a625c07 100644 --- a/src/renderer/src/components/NewWorkspaceComposerCard.tsx +++ b/src/renderer/src/components/NewWorkspaceComposerCard.tsx @@ -48,7 +48,6 @@ import SmartWorkspaceNameField, { import type { SmartNameMode } from '@/components/new-workspace/smart-workspace-source-results' import ProjectCombobox from '@/components/new-workspace/ProjectCombobox' import RunTargetCombobox from '@/components/new-workspace/RunTargetCombobox' -import { SetProjectLocationDialog } from '@/components/new-workspace/SetProjectLocationDialog' import { AddRemoteHostDialog, type AddRemoteHostMode @@ -163,8 +162,6 @@ type NewWorkspaceComposerCardProps = { sparseControlsEnabled?: boolean /** When set, "Add project" opens a host-provided flow instead of swapping the store's active modal. */ onAddProjectOverride?: () => void - /** Fires as the nested Set-project-location dialog opens and closes, so the host can stand down its Escape/submit handling. */ - onNestedDialogOpenChange?: (open: boolean) => void } const SSH_STATUS_LABELS: Partial> = { @@ -374,8 +371,7 @@ export default function NewWorkspaceComposerCard({ sparseSelectedPresetId, onSparseSelectPreset, sparseControlsEnabled = true, - onAddProjectOverride, - onNestedDialogOpenChange + onAddProjectOverride }: NewWorkspaceComposerCardProps): React.JSX.Element { // Why: subscribe (form uses translate() directly) so an open create dialog repaints when the UI language changes. useTranslation() @@ -503,38 +499,6 @@ export default function NewWorkspaceComposerCard({ const handleAddRemoteServer = React.useCallback((): void => { setAddRemoteHostMode('server') }, []) - const [setLocationOption, setSetLocationOption] = - React.useState(null) - const handleSetLocation = React.useCallback( - (option: NeedsSetupProjectHostOption): void => { - setSetLocationOption(option) - onNestedDialogOpenChange?.(true) - }, - [onNestedDialogOpenChange] - ) - const handleSetLocationClose = React.useCallback((): void => { - setSetLocationOption(null) - onNestedDialogOpenChange?.(false) - }, [onNestedDialogOpenChange]) - const handleSetLocationReady = React.useCallback( - (setupId: string): void => { - handleSetLocationClose() - onProjectHostSetupChange?.(setupId) - }, - [handleSetLocationClose, onProjectHostSetupChange] - ) - const projects = useAppStore((state) => state.projects) - const repos = useAppStore((state) => state.repos) - // Prefill "Clone from URL" with the project's own remote so the common case is one click. - const defaultCloneUrl = React.useMemo(() => { - const sourceRepoIds = - projects.find((candidate) => candidate.id === selectedProjectId)?.sourceRepoIds ?? [] - return ( - sourceRepoIds - .map((sourceId) => repos.find((repo) => repo.id === sourceId)?.gitRemoteIdentity?.remoteUrl) - .find((url): url is string => Boolean(url)) ?? '' - ) - }, [projects, repos, selectedProjectId]) const handleConnectRunTargetHost = React.useCallback( async (option: NeedsSetupProjectHostOption): Promise => { const action = option.connectAction @@ -740,7 +704,6 @@ export default function NewWorkspaceComposerCard({ onAddSshHost={handleAddSshHost} onAddRemoteServer={handleAddRemoteServer} onConnectHost={handleConnectRunTargetHost} - onSetLocation={handleSetLocation} /> {ephemeralVmRecipeError ? (

@@ -1268,14 +1231,6 @@ export default function NewWorkspaceComposerCard({ the in-progress workspace form is preserved; on success the new host flows back into the run-target picker via the store. */} -

) } diff --git a/src/renderer/src/components/NewWorkspaceComposerModal.tsx b/src/renderer/src/components/NewWorkspaceComposerModal.tsx index 2309ef54b50..77b9cf64e52 100644 --- a/src/renderer/src/components/NewWorkspaceComposerModal.tsx +++ b/src/renderer/src/components/NewWorkspaceComposerModal.tsx @@ -197,7 +197,6 @@ function QuickTabBody({ // outcomes still navigate away and tear the whole modal down.) const [addProjectOpen, setAddProjectOpen] = useState(false) const [addProjectMounted, setAddProjectMounted] = useState(false) - const [setLocationOpen, setSetLocationOpen] = useState(false) const handleOpenAddProject = useCallback((): void => { setAddProjectMounted(true) setAddProjectOpen(true) @@ -239,10 +238,10 @@ function QuickTabBody({ : translate('auto.components.NewWorkspaceComposerModal.createWorkspace', 'Create workspace') // Cmd/Ctrl+Enter submits, Esc first blurs the focused input (like the full page). - const nestedDialogOpen = agentSettingsOpen || addProjectOpen || setLocationOpen + const nestedDialogOpen = agentSettingsOpen || addProjectOpen useEffect(() => { if (!active || nestedDialogOpen) { - // Why: while a nested dialog (Add Project / Agents / Set location) is layered on top, + // Why: while a nested dialog (Add Project / Agents) is layered on top, // this capture-phase handler must not steal its Escape (which should // close only the nested dialog) or fire composer submit underneath it. return @@ -325,7 +324,6 @@ function QuickTabBody({ onOpenAgentSettings={() => setAgentSettingsOpen(true)} onCreate={() => void handleCreate()} onAddProjectOverride={handleOpenAddProject} - onNestedDialogOpenChange={setSetLocationOpen} /> {addProjectMounted ? ( diff --git a/src/renderer/src/components/new-workspace/RunTargetCombobox.tsx b/src/renderer/src/components/new-workspace/RunTargetCombobox.tsx index 4ca978baa07..4b3d0a07ea9 100644 --- a/src/renderer/src/components/new-workspace/RunTargetCombobox.tsx +++ b/src/renderer/src/components/new-workspace/RunTargetCombobox.tsx @@ -12,8 +12,7 @@ import { ConnectHostButton, HostRowIcon, NeedsSetupHostIcon, - RunTargetRow, - SetLocationButton + RunTargetRow } from './RunTargetComboboxRow' import { buildRunTargetRows, @@ -35,7 +34,6 @@ type RunTargetComboboxProps = { onAddRemoteServer?: () => void onAddSshHost?: () => void onConnectHost?: (option: NeedsSetupProjectHostOption) => Promise | void - onSetLocation?: (option: NeedsSetupProjectHostOption) => void } const ROOT_ATTRIBUTE = 'data-run-target-combobox-root' @@ -58,8 +56,7 @@ export default function RunTargetCombobox({ onRecipeChange, onAddRemoteServer, onAddSshHost, - onConnectHost, - onSetLocation + onConnectHost }: RunTargetComboboxProps): React.JSX.Element { const [submenu, setSubmenu] = useState<'recipes' | 'add-host' | null>(null) // Track in-flight connects per host so one stalling connect never blocks the others. @@ -137,19 +134,6 @@ export default function RunTargetCombobox({ [connectingHostIds, onConnectHost] ) - // Why: reached from the row body, its inline button, and Enter — keep one path - // so all three close the picker before handing off to the nested dialog. - const setLocation = useCallback( - (option: NeedsSetupProjectHostOption): void => { - if (!option.canSetLocation || !onSetLocation) { - return - } - close() - onSetLocation(option) - }, - [close, onSetLocation] - ) - /** Commits a row, or opens its submenu when the row is a submenu row. */ const activate = useCallback( (key: string | null): void => { @@ -162,13 +146,12 @@ export default function RunTargetCombobox({ return } if (row.kind === 'needs-setup') { - // Not ready: setting the location is the only way forward from the row itself. - setLocation(row.option) + // Not ready: selecting is a no-op, the Connect action is the way forward. return } setSubmenu(row.kind === 'recipes' ? 'recipes' : 'add-host') }, - [rows, selectHost, setLocation] + [rows, selectHost] ) const handleKeyDown = useCallback( @@ -308,7 +291,6 @@ export default function RunTargetCombobox({ if (row.kind === 'needs-setup') { const connecting = connectingHostIds.has(row.option.hostId) const hasConnect = Boolean(row.option.connectAction && onConnectHost) - const hasSetLocation = Boolean(row.option.canSetLocation && onSetLocation) return ( } label={row.option.label} - // Why: Connect / Set project location already say the next - // step, so the detail line would only repeat that. - detail={hasConnect || hasSetLocation ? '' : row.option.detail} + // Why: a Connect button on the row already says the host + // isn't connected, so its detail line only repeats that. + // Rows without the action still need theirs to explain why. + detail={hasConnect ? '' : row.option.detail} armed={isArmed} current={false} dimmed @@ -331,18 +314,13 @@ export default function RunTargetCombobox({ arm(row.key) setSubmenu(null) }} - onCommit={() => setLocation(row.option)} + onCommit={() => {}} trailing={ - hasConnect ? ( + row.option.connectAction && onConnectHost ? ( void connectHost(row.option)} /> - ) : hasSetLocation ? ( - setLocation(row.option)} - /> ) : undefined } /> diff --git a/src/renderer/src/components/new-workspace/RunTargetComboboxRow.tsx b/src/renderer/src/components/new-workspace/RunTargetComboboxRow.tsx index 4d7cb3df50f..fb5fa811f07 100644 --- a/src/renderer/src/components/new-workspace/RunTargetComboboxRow.tsx +++ b/src/renderer/src/components/new-workspace/RunTargetComboboxRow.tsx @@ -1,7 +1,6 @@ import React from 'react' import { AlertTriangle, ChevronRight, LoaderCircle, Monitor, Server } from 'lucide-react' import { Button } from '@/components/ui/button' -import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip' import { cn } from '@/lib/utils' import { LOCAL_EXECUTION_HOST_ID, type ExecutionHostId } from '../../../../shared/execution-host' import { ProjectOptionDetail } from './ProjectComboboxRow' @@ -174,52 +173,3 @@ export function ConnectHostButton({ ) } - -/** Inline Set project location action on a host that still needs a project path. */ -export function SetLocationButton({ - hostLabel, - onSetLocation -}: { - hostLabel: string - onSetLocation: () => void -}): React.JSX.Element { - const label = translate( - 'auto.components.NewWorkspaceComposerCard.setLocation', - 'Set project location' - ) - return ( - - - - - - {translate( - 'auto.components.NewWorkspaceComposerCard.setLocationTooltip', - 'Choose a folder or clone this project onto {{host}}.', - { host: hostLabel } - )} - - - ) -} diff --git a/src/renderer/src/components/new-workspace/SetProjectLocationDialog.test.tsx b/src/renderer/src/components/new-workspace/SetProjectLocationDialog.test.tsx deleted file mode 100644 index 086d6599869..00000000000 --- a/src/renderer/src/components/new-workspace/SetProjectLocationDialog.test.tsx +++ /dev/null @@ -1,177 +0,0 @@ -// @vitest-environment happy-dom - -import { act, cleanup, render, screen } from '@testing-library/react' -import userEvent from '@testing-library/user-event' -import type { ComponentProps } from 'react' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' -import type { NeedsSetupProjectHostOption } from '@/lib/project-host-setup-options' - -const storeMocks = vi.hoisted(() => ({ - setupProjectExistingFolder: vi.fn(), - setupProjectClone: vi.fn() -})) - -vi.mock('@/store', () => ({ - useAppStore: (selector: (state: unknown) => unknown) => selector(storeMocks) -})) - -vi.mock('@/components/sidebar/RemoteFileBrowser', () => ({ - RemoteFileBrowser: ({ - targetId, - onSelect, - onCancel - }: { - targetId?: string - onSelect: (path: string) => void - onCancel: () => void - }) => ( -
- - -
- ) -})) - -import { SetProjectLocationDialog } from './SetProjectLocationDialog' - -const option: NeedsSetupProjectHostOption = { - kind: 'needs-setup', - id: 'needs-setup:ssh:openclaw', - projectId: 'project-orca', - hostId: 'ssh:openclaw', - label: 'openclaw', - detail: 'Project location not set', - isAvailable: true, - attention: false, - canSetLocation: true -} - -function renderDialog( - overrides: Partial> = {} -): ReturnType { - const user = userEvent.setup() - render( - - ) - return user -} - -beforeEach(() => { - storeMocks.setupProjectExistingFolder.mockReset() - storeMocks.setupProjectClone.mockReset() - storeMocks.setupProjectExistingFolder.mockResolvedValue({ - setup: { id: 'setup-openclaw' } - }) - storeMocks.setupProjectClone.mockResolvedValue({ - setup: { id: 'setup-openclaw-clone' } - }) -}) - -afterEach(() => { - cleanup() -}) - -describe('SetProjectLocationDialog', () => { - it('keeps the parent caller in charge of close and saves an existing folder', async () => { - const onClose = vi.fn() - const onReady = vi.fn() - const user = renderDialog({ onClose, onReady }) - - expect(screen.getByTestId('set-project-location-dialog')).toBeTruthy() - expect(screen.getByText('Set project location')).toBeTruthy() - expect(screen.getByText('Choose where orca lives on openclaw.')).toBeTruthy() - - await user.click(screen.getByRole('button', { name: /Browse folder/ })) - await user.click(screen.getByRole('button', { name: 'Browse host filesystem' })) - await user.click(screen.getByRole('button', { name: 'Select remote folder' })) - await user.click(screen.getByRole('button', { name: 'Set location' })) - - expect(storeMocks.setupProjectExistingFolder).toHaveBeenCalledWith({ - projectId: 'project-orca', - hostId: 'ssh:openclaw', - path: '/remote/orca', - kind: 'git', - displayName: 'orca' - }) - expect(onReady).toHaveBeenCalledWith('setup-openclaw') - expect(onClose).not.toHaveBeenCalled() - }) - - it('clones onto the selected host', async () => { - const onReady = vi.fn() - const user = renderDialog({ onReady }) - - await user.click(screen.getByRole('button', { name: /Clone from URL/ })) - await user.click(screen.getByRole('button', { name: 'Browse host filesystem' })) - await user.click(screen.getByRole('button', { name: 'Select remote folder' })) - await user.click(screen.getByRole('button', { name: 'Clone' })) - - expect(storeMocks.setupProjectClone).toHaveBeenCalledWith({ - projectId: 'project-orca', - hostId: 'ssh:openclaw', - url: 'git@github.com:stablyai/orca.git', - destination: '/remote/orca', - displayName: 'orca' - }) - expect(onReady).toHaveBeenCalledWith('setup-openclaw-clone') - }) - - it('hides clone for folder projects', () => { - renderDialog({ projectKind: 'folder' }) - expect(screen.queryByRole('button', { name: /Clone from URL/ })).toBeNull() - }) - - it('does not render when no host is selected', () => { - render( - - ) - expect(screen.queryByTestId('set-project-location-dialog')).toBeNull() - }) - - it('backs out of the host browser on Escape instead of discarding the form', async () => { - const onClose = vi.fn() - const user = renderDialog({ onClose }) - - await user.click(screen.getByRole('button', { name: /Browse folder/ })) - await user.click(screen.getByRole('button', { name: 'Browse host filesystem' })) - expect(screen.getByTestId('remote-file-browser')).toBeTruthy() - - await user.keyboard('{Escape}') - - expect(onClose).not.toHaveBeenCalled() - expect(screen.queryByTestId('remote-file-browser')).toBeNull() - expect(screen.getByRole('button', { name: 'Set location' })).toBeTruthy() - - // A second Escape, now back on the form, dismisses the dialog as usual. - await user.keyboard('{Escape}') - expect(onClose).toHaveBeenCalled() - }) - - it('notifies the parent when dismissed', async () => { - const onClose = vi.fn() - renderDialog({ onClose }) - await act(async () => { - document.querySelector('[data-slot="dialog-close"]')?.click() - }) - expect(onClose).toHaveBeenCalled() - }) -}) diff --git a/src/renderer/src/components/new-workspace/SetProjectLocationDialog.tsx b/src/renderer/src/components/new-workspace/SetProjectLocationDialog.tsx deleted file mode 100644 index 2362c0ba43b..00000000000 --- a/src/renderer/src/components/new-workspace/SetProjectLocationDialog.tsx +++ /dev/null @@ -1,278 +0,0 @@ -import { useEffect, useRef, useState, type RefObject } from 'react' -import { Download, FolderOpen } from 'lucide-react' -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle -} from '@/components/ui/dialog' -import { translate } from '@/i18n/i18n' -import { useAppStore } from '@/store' -import { CreateProjectParentBrowser } from '@/components/sidebar/CreateProjectLocationField' -import type { NeedsSetupProjectHostOption } from '@/lib/project-host-setup-options' -import { parseExecutionHostId } from '../../../../shared/execution-host' -import type { RepoKind } from '../../../../shared/repo-types' -import { pickLocalProjectLocationFolder } from './pick-local-project-folder' -import { CloneForm, ExistingFolderForm, LocationActionButton } from './SetProjectLocationForms' - -type DialogView = 'choose' | 'existing' | 'clone' | 'browse' -type BrowseField = 'existing' | 'clone' - -type SetProjectLocationDialogProps = { - option: NeedsSetupProjectHostOption | null - projectName: string - projectKind: RepoKind - defaultCloneUrl: string - onClose: () => void - onReady: (setupId: string) => void -} - -export function SetProjectLocationDialog({ - option, - projectName, - projectKind, - defaultCloneUrl, - onClose, - onReady -}: SetProjectLocationDialogProps): React.JSX.Element { - const open = option !== null - // Why: keep the last option rendered through the close animation, so the body - // doesn't blank out as the dialog slides away. - const [renderOption, setRenderOption] = useState(option) - if (option !== null && option !== renderOption) { - setRenderOption(option) - } - const activeOption = option ?? renderOption - // Why: Radix dismisses on Escape from a document-capture listener, so the host - // browser can never intercept it itself. The body parks a back-out here so - // Escape steps out of the browser instead of discarding the half-filled form. - const exitHostBrowser = useRef<(() => boolean) | null>(null) - - return ( - { - if (!next) { - onClose() - } - }} - > - { - if (exitHostBrowser.current?.()) { - // Radix only skips onDismiss when the escape was defaultPrevented. - event.preventDefault() - } - }} - > - {activeOption ? ( - - ) : null} - - - ) -} - -function SetProjectLocationDialogBody({ - option, - projectName, - projectKind, - defaultCloneUrl, - exitHostBrowser, - onReady -}: { - option: NeedsSetupProjectHostOption - projectName: string - projectKind: RepoKind - defaultCloneUrl: string - exitHostBrowser: RefObject<(() => boolean) | null> - onReady: (setupId: string) => void -}): React.JSX.Element { - const setupProjectExistingFolder = useAppStore((state) => state.setupProjectExistingFolder) - const setupProjectClone = useAppStore((state) => state.setupProjectClone) - const [view, setView] = useState('choose') - const [browseField, setBrowseField] = useState('existing') - const [setupPath, setSetupPath] = useState('') - const [setupKind, setSetupKind] = useState(projectKind) - const [cloneUrl, setCloneUrl] = useState(defaultCloneUrl) - const [cloneDestination, setCloneDestination] = useState('') - const [isSubmitting, setIsSubmitting] = useState(false) - const parsedHost = parseExecutionHostId(option.hostId) - // Remote hosts browse in-dialog; the local host gets the native folder picker. - const remoteHost = - parsedHost?.kind === 'ssh' || parsedHost?.kind === 'runtime' ? parsedHost : null - const canClone = projectKind === 'git' - // Both views browse for a path; this is the field each one writes back to. - const pathFields: Record void }> = { - existing: { value: setupPath, set: setSetupPath }, - clone: { value: cloneDestination, set: setCloneDestination } - } - - const browsing = view === 'browse' && remoteHost !== null - useEffect(() => { - exitHostBrowser.current = browsing - ? () => { - setView(browseField) - return true - } - : null - return () => { - exitHostBrowser.current = null - } - }, [browseField, browsing, exitHostBrowser]) - - const openHostBrowser = (field: BrowseField): void => { - if (!remoteHost) { - void pickLocalProjectLocationFolder(pathFields[field].set) - return - } - setBrowseField(field) - setView('browse') - } - - const handleExistingSubmit = async (): Promise => { - if (!setupPath.trim()) { - return - } - setIsSubmitting(true) - try { - const result = await setupProjectExistingFolder({ - projectId: option.projectId, - hostId: option.hostId, - path: setupPath.trim(), - kind: setupKind, - displayName: projectName - }) - if (result) { - onReady(result.setup.id) - } - } finally { - setIsSubmitting(false) - } - } - - const handleCloneSubmit = async (): Promise => { - if (!cloneUrl.trim() || !cloneDestination.trim()) { - return - } - setIsSubmitting(true) - try { - const result = await setupProjectClone({ - projectId: option.projectId, - hostId: option.hostId, - url: cloneUrl.trim(), - destination: cloneDestination.trim(), - displayName: projectName - }) - if (result) { - onReady(result.setup.id) - } - } finally { - setIsSubmitting(false) - } - } - - if (browsing && remoteHost) { - return ( - setView(browseField)} - /> - ) - } - - return ( - <> - - - {translate( - 'auto.components.new.workspace.SetProjectLocationDialog.title', - 'Set project location' - )} - - - {translate( - 'auto.components.new.workspace.SetProjectLocationDialog.description', - 'Choose where {{project}} lives on {{host}}.', - { project: projectName, host: option.label } - )} - - - {view === 'choose' ? ( -
- { - setView('existing') - // Local hosts get the native picker straight away — one click instead of two. - if (!remoteHost && !setupPath) { - void pickLocalProjectLocationFolder(setSetupPath) - } - }} - /> - {canClone ? ( - setView('clone')} - /> - ) : null} -
- ) : null} - {view === 'existing' ? ( - setView('choose')} - onPathChange={setSetupPath} - onKindChange={setSetupKind} - onBrowse={() => openHostBrowser('existing')} - onSubmit={() => void handleExistingSubmit()} - /> - ) : null} - {view === 'clone' ? ( - setView('choose')} - onCloneUrlChange={setCloneUrl} - onCloneDestinationChange={setCloneDestination} - onBrowse={() => openHostBrowser('clone')} - onSubmit={() => void handleCloneSubmit()} - /> - ) : null} - - ) -} diff --git a/src/renderer/src/components/new-workspace/SetProjectLocationForms.tsx b/src/renderer/src/components/new-workspace/SetProjectLocationForms.tsx deleted file mode 100644 index 1793e65a9cf..00000000000 --- a/src/renderer/src/components/new-workspace/SetProjectLocationForms.tsx +++ /dev/null @@ -1,224 +0,0 @@ -import { ArrowLeft, FolderOpen, LoaderCircle } from 'lucide-react' -import { Button } from '@/components/ui/button' -import { Input } from '@/components/ui/input' -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue -} from '@/components/ui/select' -import { translate } from '@/i18n/i18n' -import { cn } from '@/lib/utils' -import type { RepoKind } from '../../../../shared/repo-types' - -export function ExistingFolderForm({ - setupPath, - setupKind, - isSubmitting, - onBack, - onPathChange, - onKindChange, - onBrowse, - onSubmit -}: { - setupPath: string - setupKind: RepoKind - isSubmitting: boolean - onBack: () => void - onPathChange: (value: string) => void - onKindChange: (value: RepoKind) => void - onBrowse: () => void - onSubmit: () => void -}): React.JSX.Element { - return ( -
- -
- onPathChange(event.target.value)} - placeholder={translate( - 'auto.components.settings.RepositoryPane.setupExistingFolderPathPlaceholder', - '/path/to/project/on/host' - )} - className="h-9 min-w-0 flex-1 font-mono text-sm" - spellCheck={false} - /> - -
- -
- -
-
- ) -} - -export function CloneForm({ - cloneUrl, - cloneDestination, - isSubmitting, - onBack, - onCloneUrlChange, - onCloneDestinationChange, - onBrowse, - onSubmit -}: { - cloneUrl: string - cloneDestination: string - isSubmitting: boolean - onBack: () => void - onCloneUrlChange: (value: string) => void - onCloneDestinationChange: (value: string) => void - onBrowse: () => void - onSubmit: () => void -}): React.JSX.Element { - return ( -
- - onCloneUrlChange(event.target.value)} - placeholder={translate( - 'auto.components.settings.RepositoryPane.cloneUrlPlaceholder', - 'Repository URL' - )} - className="h-9 min-w-0" - spellCheck={false} - /> -
- onCloneDestinationChange(event.target.value)} - placeholder={translate( - 'auto.components.settings.RepositoryPane.cloneDestinationPlaceholder', - '/destination/on/host' - )} - className="h-9 min-w-0 flex-1 font-mono text-sm" - spellCheck={false} - /> - -
-
- -
-
- ) -} - -export function LocationActionButton({ - icon: Icon, - title, - description, - onClick -}: { - icon: typeof FolderOpen - title: string - description: string - onClick: () => void -}): React.JSX.Element { - return ( - - ) -} - -function StepBackButton({ - onBack, - label -}: { - onBack: () => void - label: string -}): React.JSX.Element { - return ( - - ) -} diff --git a/src/renderer/src/components/new-workspace/pick-local-project-folder.ts b/src/renderer/src/components/new-workspace/pick-local-project-folder.ts deleted file mode 100644 index c49f24b6b33..00000000000 --- a/src/renderer/src/components/new-workspace/pick-local-project-folder.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { toast } from 'sonner' - -export async function pickLocalProjectLocationFolder( - onPicked: (path: string) => void -): Promise { - try { - const path = await window.api.repos.pickFolder() - if (path) { - onPicked(path) - } - } catch (error) { - toast.error(error instanceof Error ? error.message : String(error)) - } -} diff --git a/src/renderer/src/hooks/useComposerState-host-retarget.test.ts b/src/renderer/src/hooks/useComposerState-host-retarget.test.ts index e057f401a4f..7e2c35cb75d 100644 --- a/src/renderer/src/hooks/useComposerState-host-retarget.test.ts +++ b/src/renderer/src/hooks/useComposerState-host-retarget.test.ts @@ -55,7 +55,7 @@ describe('useComposerState host retarget', () => { 'const handleProjectHostSetupChange', 'const handleProjectChange' ) - expect(switchSection).toContain('setSelectedProjectHostSetupOverrideId(target.id)') + expect(switchSection).toContain('setSelectedProjectHostSetupOverrideId(option.id)') expect(switchSection).toContain('preserveStartFrom: true') expect(switchSection).toContain('forceResetStartFrom: true') }) diff --git a/src/renderer/src/hooks/useComposerState.ts b/src/renderer/src/hooks/useComposerState.ts index b74133490eb..ec91d1f969a 100644 --- a/src/renderer/src/hooks/useComposerState.ts +++ b/src/renderer/src/hooks/useComposerState.ts @@ -2796,21 +2796,12 @@ export function useComposerState(options: UseComposerStateOptions): UseComposerS const handleProjectHostSetupChange = useCallback( (setupId: string): void => { const option = projectHostSetupOptions.find((candidate) => candidate.id === setupId) - const target = - option?.kind === 'ready' - ? option - : // Why: a just-created setup lands in the store before the memoized picker options refresh. - useAppStore - .getState() - .projectHostSetups.find( - (candidate) => candidate.id === setupId && candidate.setupState === 'ready' - ) - if (!target) { + if (!option || option.kind !== 'ready') { return } // Why: switching run host for the same project must not erase the task/PR source the user is starting from. - setSelectedProjectHostSetupOverrideId(target.id) - handleRepoChange(target.repoId, { + setSelectedProjectHostSetupOverrideId(option.id) + handleRepoChange(option.repoId, { preserveStartFrom: true, forceResetStartFrom: true }) diff --git a/src/renderer/src/i18n/locales/en.json b/src/renderer/src/i18n/locales/en.json index d5600ceb63f..046bbb9df45 100644 --- a/src/renderer/src/i18n/locales/en.json +++ b/src/renderer/src/i18n/locales/en.json @@ -1489,9 +1489,6 @@ "connectTimedOut": "Connection timed out. It may still be connecting in the background.", "connectingHost": "Connecting…", "connectHost": "Connect", - "setLocation": "Set project location", - "setLocationOnHost": "Set project location on {{host}}", - "setLocationTooltip": "Choose a folder or clone this project onto {{host}}.", "addHost": "Add host", "addHostHint": "Register another machine or Orca server", "addSshHost": "Add SSH host", @@ -12465,15 +12462,6 @@ }, "new": { "workspace": { - "SetProjectLocationDialog": { - "title": "Set project location", - "description": "Choose where {{project}} lives on {{host}}.", - "browseFolder": "Browse folder", - "browseFolderHelp": "Use an existing checkout or folder on this host.", - "cloneFromUrl": "Clone from URL", - "cloneFromUrlHelp": "Clone this repository onto {{host}}.", - "saveLocation": "Set location" - }, "SmartWorkspaceNameField": { "2a0d535f69": "Create new branch", "a44229ce4d": "as workspace name", 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 898fbd7b876..9eee19f9725 100644 --- a/src/renderer/src/lib/project-host-setup-options.test.ts +++ b/src/renderer/src/lib/project-host-setup-options.test.ts @@ -270,30 +270,11 @@ describe('buildProjectHostSetupOptions', () => { kind: 'needs-setup', label: 'Builder', detail: 'Project location not set', - isAvailable: true, - canSetLocation: true + isAvailable: true }) ]) }) - // A `repo:` project has no cross-host identity, so linking it on another host - // always fails in main — the row keeps its status line instead of a dead button. - it('cannot set a location for a host-local project', () => { - const options = buildProjectHostSetupOptions({ - projectId: 'repo:local-repo', - eligibleRepos: [repo('local-repo')], - hosts: [host('local'), host('ssh:builder', { label: 'Builder' })], - projectHostSetups: [setup('local', 'repo:local-repo', 'local', 'local-repo')] - }) - - expect(options.at(-1)).toMatchObject({ - kind: 'needs-setup', - detail: 'Project location not set', - isAvailable: true, - canSetLocation: false - }) - }) - it.each([ ['connecting' as const, 'Connecting to host'], ['disconnected' as const, 'Connect this host to set up projects'], @@ -426,8 +407,7 @@ describe('buildProjectHostSetupOptions', () => { kind: 'needs-setup', label: 'GPU VM', detail: 'Project setup is in progress', - isAvailable: true, - canSetLocation: false + isAvailable: true }) ]) }) @@ -457,10 +437,7 @@ describe('buildProjectHostSetupOptions', () => { }) ] }).at(-1) - ).toMatchObject({ - detail: 'Project tracked on this host but not set up', - canSetLocation: true - }) + ).toMatchObject({ detail: 'Project tracked on this host but not set up' }) expect( buildProjectHostSetupOptions({ @@ -480,7 +457,7 @@ describe('buildProjectHostSetupOptions', () => { }) ] }).at(-1) - ).toMatchObject({ detail: 'Project setup needs attention', canSetLocation: true }) + ).toMatchObject({ detail: 'Project setup needs attention' }) expect( buildProjectHostSetupOptions({ @@ -500,10 +477,7 @@ describe('buildProjectHostSetupOptions', () => { }) ] }).at(-1) - ).toMatchObject({ - detail: 'Project is unsupported on this host', - canSetLocation: false - }) + ).toMatchObject({ detail: 'Project is unsupported on this host' }) }) it('marks incompatible runtime hosts as visible but unavailable', () => { diff --git a/src/renderer/src/lib/project-host-setup-options.ts b/src/renderer/src/lib/project-host-setup-options.ts index b4e975b0569..eca4dc397c6 100644 --- a/src/renderer/src/lib/project-host-setup-options.ts +++ b/src/renderer/src/lib/project-host-setup-options.ts @@ -6,7 +6,6 @@ import { type ExecutionHostId } from '../../../shared/execution-host' import type { ExecutionHostRegistryEntry } from '../../../shared/execution-host-registry' -import { isHostLocalProjectId } from '../../../shared/project-host-setup-projection' import { isEphemeralVmRuntimeEnvironment } from '../../../shared/runtime-environments' import { PROJECT_HOST_SETUP_RUNTIME_CAPABILITY, @@ -37,9 +36,6 @@ export type ProjectHostSetupOption = // Why: only a genuine connection error warrants an alarm glyph; a dormant // disconnected host is merely not-yet-connected, not broken. attention: boolean - // Why: available hosts without a path can be set up in place; connecting or - // in-progress/unsupported hosts need a different next step. - canSetLocation: boolean connectAction?: { kind: 'ssh'; targetId: string } | { kind: 'runtime'; environmentId: string } } @@ -191,7 +187,6 @@ function buildNeedsSetupOptions({ : availability.detail, isAvailable: availability.isAvailable, attention: host.health === 'error', - canSetLocation: canSetProjectLocation(projectId, availability.isAvailable, pendingSetup), ...(connectAction ? { connectAction } : {}) } }) @@ -268,23 +263,6 @@ function getHostHealthUnavailableDetail( } } -function canSetProjectLocation( - projectId: string, - isAvailable: boolean, - pendingSetup: ProjectHostSetup | undefined -): boolean { - // Why: setting up on another host links by project identity, and a host-local - // `repo:` project has none to match against — the call always fails, so offer - // the plain status line rather than a button that only ever toasts an error. - if (!isAvailable || isHostLocalProjectId(projectId)) { - return false - } - if (!pendingSetup) { - return true - } - return pendingSetup.setupState === 'not-set-up' || pendingSetup.setupState === 'error' -} - function getHostConnectAction( host: ExecutionHostRegistryEntry ): NeedsSetupProjectHostOption['connectAction'] | undefined { diff --git a/src/shared/project-host-setup-projection.ts b/src/shared/project-host-setup-projection.ts index 8c3e5d15643..09f799e5527 100644 --- a/src/shared/project-host-setup-projection.ts +++ b/src/shared/project-host-setup-projection.ts @@ -93,8 +93,6 @@ export function isProjectRemoteIdentityPending( return repo.gitRemoteIdentity === undefined && !hasProjectRemoteIdentity(repo) } -const HOST_LOCAL_PROJECT_ID_PREFIX = 'repo:' - export function getProjectIdentityKey( repo: Pick ): string { @@ -106,16 +104,7 @@ export function getProjectIdentityKey( if (gitRemoteIdentity) { return `git:${gitRemoteIdentity.canonicalKey}` } - return `${HOST_LOCAL_PROJECT_ID_PREFIX}${repo.id}` -} - -/** - * True for the `repo:` 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) + return `repo:${repo.id}` } export function getProjectIdForProviderIdentity(identity: ProjectProviderIdentity): string {