mirror of
https://github.com/stablyai/orca.git
synced 2026-10-16 00:03:09 +00:00
@@ -809,6 +809,12 @@ Landed so far:
|
||||
narrow relay `git.clone` RPC that emits `git.cloneProgress` notifications,
|
||||
and the main process forwards those through the existing
|
||||
`repos:clone-progress` renderer event used by local clone.
|
||||
- Started the host-aware Add Project migration. The Add Project start screen now
|
||||
shows a host selector when more than Local Mac is known, uses the shared
|
||||
sidebar host registry for labels/status, routes Browse to local picker,
|
||||
selected runtime server, or preselected SSH target, and lets Clone from URL
|
||||
clone on local, runtime, or SSH hosts. The old separate "Remote project" start
|
||||
row is hidden in the host-aware flow.
|
||||
- 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
|
||||
@@ -842,12 +848,20 @@ Important limitation:
|
||||
first-class setup create/update/delete now exist for independent metadata and
|
||||
are exposed through the renderer store/settings, but actual provisioning
|
||||
execution and full UI flows are still future work.
|
||||
- Add Project is now host-aware for single-host Browse and Clone flows, but
|
||||
multi-host setup rows are not yet implemented in the Add Project dialog. SSH
|
||||
"Create new project" is visibly unavailable because the app still lacks a
|
||||
first-class SSH create-project IPC/API equivalent; runtime create is already
|
||||
handled through the existing runtime RPC path.
|
||||
|
||||
Remaining end-to-end work:
|
||||
|
||||
- continue broadening setup-on-host flows beyond the shared known-host registry
|
||||
toward bulk setup and newly added host onboarding
|
||||
- finish provisioning and bulk setup-on-host flows
|
||||
- finish provisioning, bulk setup-on-host flows, and Add Project multi-host
|
||||
setup rows with per-host paths
|
||||
- add a first-class SSH create-project path so Create can target SSH hosts, not
|
||||
just local and runtime hosts
|
||||
- add actual provisioning execution APIs instead of only recording independent
|
||||
setup/provisioning metadata and intent
|
||||
- split settings into explicit client, host, project, and project-host setup
|
||||
|
||||
@@ -52,6 +52,7 @@ import type {
|
||||
PersistedUIState,
|
||||
Project,
|
||||
ProjectHostSetup,
|
||||
ProjectHostSetupCloneArgs,
|
||||
ProjectHostSetupCreateArgs,
|
||||
ProjectHostSetupCreateResult,
|
||||
ProjectHostSetupDeleteArgs,
|
||||
|
||||
@@ -19,6 +19,8 @@ import {
|
||||
shouldTrackAddRepoExistingWorkspacesDetected
|
||||
} from './add-repo-existing-workspaces-telemetry'
|
||||
import { finishProjectAddWithDefaultCheckout } from './project-added-default-checkout'
|
||||
import { AddRepoHostSelector } from './AddRepoHostSelector'
|
||||
import { useAddRepoHostSelection } from './use-add-repo-host-selection'
|
||||
|
||||
const AddRepoDialog = React.memo(function AddRepoDialog() {
|
||||
const activeModal = useAppStore((s) => s.activeModal)
|
||||
@@ -34,8 +36,6 @@ const AddRepoDialog = React.memo(function AddRepoDialog() {
|
||||
const openSettingsTarget = useAppStore((s) => s.openSettingsTarget)
|
||||
const setHideDefaultBranchWorkspace = useAppStore((s) => s.setHideDefaultBranchWorkspace)
|
||||
const settings = useAppStore((s) => s.settings)
|
||||
const sshConnectionStates = useAppStore((s) => s.sshConnectionStates)
|
||||
const sshTargetLabels = useAppStore((s) => s.sshTargetLabels)
|
||||
|
||||
const [step, setStep] = useState<AddRepoDialogStep>('add')
|
||||
const [isAdding, setIsAdding] = useState(false)
|
||||
@@ -142,6 +142,16 @@ const AddRepoDialog = React.memo(function AddRepoDialog() {
|
||||
}
|
||||
)
|
||||
|
||||
const {
|
||||
hostOptions,
|
||||
selectedHostId,
|
||||
selectedParsedHost,
|
||||
selectedSshTargetId,
|
||||
hostSelectorOpen,
|
||||
setHostSelectorOpen,
|
||||
handleSelectAddProjectHost
|
||||
} = useAddRepoHostSelection({ isOpen: activeModal === 'add-repo', setStep })
|
||||
|
||||
const {
|
||||
createName,
|
||||
createParent,
|
||||
@@ -174,6 +184,7 @@ const AddRepoDialog = React.memo(function AddRepoDialog() {
|
||||
} = useAddRepoCloneFlow({
|
||||
step,
|
||||
activeRuntimeEnvironmentId: settings?.activeRuntimeEnvironmentId,
|
||||
sshTargetId: selectedSshTargetId,
|
||||
workspaceDir: settings?.workspaceDir,
|
||||
fetchWorktrees,
|
||||
onGitRepoReady: completeGitRepoAdd
|
||||
@@ -183,13 +194,6 @@ const AddRepoDialog = React.memo(function AddRepoDialog() {
|
||||
const droppedLocalPath =
|
||||
typeof modalData.droppedLocalPath === 'string' ? modalData.droppedLocalPath : ''
|
||||
const isRuntimeEnvironmentActive = Boolean(settings?.activeRuntimeEnvironmentId?.trim())
|
||||
// Why: repo_added telemetry cannot reliably separate SSH from local folder adds,
|
||||
// so promote remote projects from durable local SSH state instead.
|
||||
const isSshLikely =
|
||||
repos.some((repo) => Boolean(repo.connectionId)) ||
|
||||
sshTargetLabels.size > 0 ||
|
||||
Array.from(sshConnectionStates.values()).some((state) => state.status === 'connected')
|
||||
|
||||
const { handleBrowse, resetLocalFolderFlow } = useAddRepoLocalFolderFlow({
|
||||
isOpen,
|
||||
droppedLocalPath,
|
||||
@@ -279,6 +283,16 @@ const AddRepoDialog = React.memo(function AddRepoDialog() {
|
||||
resetState()
|
||||
}, [resetState, step, trackNestedBackAction])
|
||||
|
||||
const hostSelector = (
|
||||
<AddRepoHostSelector
|
||||
hosts={hostOptions}
|
||||
selectedHostId={selectedHostId}
|
||||
open={hostSelectorOpen}
|
||||
onOpenChange={setHostSelectorOpen}
|
||||
onSelectHost={(hostId) => void handleSelectAddProjectHost(hostId)}
|
||||
/>
|
||||
)
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open={isOpen}
|
||||
@@ -302,7 +316,7 @@ const AddRepoDialog = React.memo(function AddRepoDialog() {
|
||||
step={step}
|
||||
isRuntimeEnvironmentActive={isRuntimeEnvironmentActive}
|
||||
activeRuntimeEnvironmentId={settings?.activeRuntimeEnvironmentId}
|
||||
isSshLikely={isSshLikely}
|
||||
isSshLikely={false}
|
||||
repoCount={repos.length}
|
||||
isAdding={isAdding}
|
||||
addProjectBusyLabel={addProjectBusyLabel}
|
||||
@@ -329,12 +343,22 @@ const AddRepoDialog = React.memo(function AddRepoDialog() {
|
||||
createKind={createKind}
|
||||
createError={createError}
|
||||
isCreating={isCreating}
|
||||
onBrowse={handleBrowse}
|
||||
hostSelector={hostSelector}
|
||||
showRemoteAction={false}
|
||||
canCreateProject={selectedParsedHost?.kind !== 'ssh'}
|
||||
onBrowse={
|
||||
selectedParsedHost?.kind === 'ssh'
|
||||
? () => void handleOpenRemoteStep(selectedSshTargetId)
|
||||
: handleBrowse
|
||||
}
|
||||
onOpenCloneStep={() => {
|
||||
setCloneError(null)
|
||||
setStep('clone')
|
||||
}}
|
||||
onOpenCreateStep={() => {
|
||||
if (selectedParsedHost?.kind === 'ssh') {
|
||||
return
|
||||
}
|
||||
setCreateError(null)
|
||||
setStep('create')
|
||||
}}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Dispatch, SetStateAction } from 'react'
|
||||
import type { Dispatch, ReactNode, SetStateAction } from 'react'
|
||||
import { CloneStep } from './AddRepoSteps'
|
||||
import { RemoteStep } from './AddRepoRemoteStep'
|
||||
import { CreateStep } from './AddRepoCreateStep'
|
||||
@@ -40,10 +40,13 @@ type AddRepoDialogStepContentProps = {
|
||||
createKind: 'git' | 'folder'
|
||||
createError: string | null
|
||||
isCreating: boolean
|
||||
hostSelector?: ReactNode
|
||||
showRemoteAction?: boolean
|
||||
canCreateProject?: boolean
|
||||
onBrowse: () => void
|
||||
onOpenCloneStep: () => void
|
||||
onOpenCreateStep: () => void
|
||||
onOpenRemoteStep: () => void
|
||||
onOpenRemoteStep: (targetId?: string | null) => void
|
||||
onStopNestedScan: () => void
|
||||
onServerPathChange: (path: string) => void
|
||||
onAddServerPath: (kind: 'git' | 'folder') => void
|
||||
@@ -98,6 +101,9 @@ export function AddRepoDialogStepContent({
|
||||
createKind,
|
||||
createError,
|
||||
isCreating,
|
||||
hostSelector,
|
||||
showRemoteAction = true,
|
||||
canCreateProject = true,
|
||||
onBrowse,
|
||||
onOpenCloneStep,
|
||||
onOpenCreateStep,
|
||||
@@ -131,6 +137,7 @@ export function AddRepoDialogStepContent({
|
||||
runtimeEnvironmentId={activeRuntimeEnvironmentId}
|
||||
isAddingServerPath={isAddingServerPath}
|
||||
addProjectBusyLabel={addProjectBusyLabel}
|
||||
hostSelector={hostSelector}
|
||||
onServerPathChange={onServerPathChange}
|
||||
onAddServerPath={onAddServerPath}
|
||||
onOpenCloneStep={onOpenCloneStep}
|
||||
@@ -148,6 +155,9 @@ export function AddRepoDialogStepContent({
|
||||
addProjectBusyLabel={addProjectBusyLabel}
|
||||
nestedScanInProgress={nestedScanInProgress}
|
||||
nestedScanId={nestedScanId}
|
||||
hostSelector={hostSelector}
|
||||
showRemoteAction={showRemoteAction}
|
||||
canCreateProject={canCreateProject}
|
||||
onBrowse={onBrowse}
|
||||
onOpenCloneStep={onOpenCloneStep}
|
||||
onOpenRemoteStep={onOpenRemoteStep}
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
import { Check, ChevronsUpDown } from 'lucide-react'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Command, CommandItem, CommandList } from '@/components/ui/command'
|
||||
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover'
|
||||
import { cn } from '@/lib/utils'
|
||||
import type { SidebarHostOption } from './sidebar-host-options'
|
||||
import { getSidebarHostHealthLabel, shouldShowHostScopeControls } from './sidebar-host-options'
|
||||
import type { ExecutionHostId } from '../../../../shared/execution-host'
|
||||
import { translate } from '@/i18n/i18n'
|
||||
|
||||
type AddRepoHostSelectorProps = {
|
||||
hosts: SidebarHostOption[]
|
||||
selectedHostId: ExecutionHostId
|
||||
open: boolean
|
||||
onOpenChange: (open: boolean) => void
|
||||
onSelectHost: (hostId: ExecutionHostId) => void
|
||||
}
|
||||
|
||||
function getHostKindLabel(host: SidebarHostOption): string {
|
||||
switch (host.kind) {
|
||||
case 'local':
|
||||
return translate('auto.components.sidebar.AddRepoHostSelector.local', 'Local')
|
||||
case 'ssh':
|
||||
return translate('auto.components.sidebar.AddRepoHostSelector.ssh', 'SSH')
|
||||
case 'runtime':
|
||||
return translate('auto.components.sidebar.AddRepoHostSelector.runtime', 'Server')
|
||||
}
|
||||
}
|
||||
|
||||
function isHostDisabled(host: SidebarHostOption): boolean {
|
||||
return host.health === 'blocked'
|
||||
}
|
||||
|
||||
export function AddRepoHostSelector({
|
||||
hosts,
|
||||
selectedHostId,
|
||||
open,
|
||||
onOpenChange,
|
||||
onSelectHost
|
||||
}: AddRepoHostSelectorProps): React.JSX.Element | null {
|
||||
if (!shouldShowHostScopeControls(hosts)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const selectedHost = hosts.find((host) => host.id === selectedHostId) ?? hosts[0]
|
||||
if (!selectedHost) {
|
||||
return null
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="text-[11px] font-medium text-muted-foreground">
|
||||
{translate('auto.components.sidebar.AddRepoHostSelector.host', 'Host')}
|
||||
</span>
|
||||
<Popover open={open} onOpenChange={onOpenChange}>
|
||||
<PopoverTrigger asChild>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
aria-expanded={open}
|
||||
className="h-8 min-w-56 justify-between px-3 text-xs font-normal"
|
||||
>
|
||||
<span className="min-w-0 truncate">{selectedHost.label}</span>
|
||||
<ChevronsUpDown className="size-3.5 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
align="end"
|
||||
className="w-[min(340px,calc(100vw-1rem))] min-w-[var(--radix-popover-trigger-width)] p-0"
|
||||
>
|
||||
<Command>
|
||||
<CommandList>
|
||||
{hosts.map((host) => {
|
||||
const selected = host.id === selectedHostId
|
||||
const disabled = isHostDisabled(host)
|
||||
return (
|
||||
<CommandItem
|
||||
key={host.id}
|
||||
value={`${host.label} ${host.detail}`}
|
||||
disabled={disabled}
|
||||
onSelect={() => {
|
||||
if (disabled) {
|
||||
return
|
||||
}
|
||||
onSelectHost(host.id)
|
||||
onOpenChange(false)
|
||||
}}
|
||||
className="items-start gap-2 px-3 py-2 text-xs"
|
||||
>
|
||||
<Check
|
||||
className={cn(
|
||||
'mt-0.5 size-3 text-muted-foreground',
|
||||
selected ? 'opacity-70' : 'opacity-0'
|
||||
)}
|
||||
/>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="flex min-w-0 items-center gap-2">
|
||||
<span className="truncate font-medium">{host.label}</span>
|
||||
<span className="shrink-0 rounded-sm border border-border px-1.5 py-0.5 text-[10px] uppercase tracking-wide text-muted-foreground">
|
||||
{getHostKindLabel(host)}
|
||||
</span>
|
||||
</span>
|
||||
<span className="mt-0.5 block truncate text-[11px] text-muted-foreground">
|
||||
{getSidebarHostHealthLabel(host.health)}
|
||||
{host.detail ? ` - ${host.detail}` : ''}
|
||||
</span>
|
||||
</span>
|
||||
</CommandItem>
|
||||
)
|
||||
})}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, type ComponentType } from 'react'
|
||||
import { useState, type ComponentType, type ReactNode } from 'react'
|
||||
import { FolderOpen, Globe, Lightbulb, Loader2, Server } from 'lucide-react'
|
||||
import { DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
@@ -12,6 +12,7 @@ type AddRepoServerPathStartStepProps = {
|
||||
runtimeEnvironmentId: string | null | undefined
|
||||
isAddingServerPath: boolean
|
||||
addProjectBusyLabel: string | null
|
||||
hostSelector?: ReactNode
|
||||
onServerPathChange: (path: string) => void
|
||||
onAddServerPath: (kind: 'git' | 'folder') => void
|
||||
onOpenCloneStep: () => void
|
||||
@@ -23,6 +24,7 @@ export function AddRepoServerPathStartStep({
|
||||
runtimeEnvironmentId,
|
||||
isAddingServerPath,
|
||||
addProjectBusyLabel,
|
||||
hostSelector,
|
||||
onServerPathChange,
|
||||
onAddServerPath,
|
||||
onOpenCloneStep,
|
||||
@@ -35,9 +37,18 @@ export function AddRepoServerPathStartStep({
|
||||
return (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{translate("auto.components.sidebar.AddRepoServerStartStep.ac66a3ed2d", "Browse server filesystem")}</DialogTitle>
|
||||
<DialogTitle>
|
||||
{translate(
|
||||
'auto.components.sidebar.AddRepoServerStartStep.ac66a3ed2d',
|
||||
'Browse server filesystem'
|
||||
)}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{translate("auto.components.sidebar.AddRepoServerStartStep.0f8aba944c", "Navigate to a directory and click Select to choose it.")}</DialogDescription>
|
||||
{translate(
|
||||
'auto.components.sidebar.AddRepoServerStartStep.0f8aba944c',
|
||||
'Navigate to a directory and click Select to choose it.'
|
||||
)}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<RemoteFileBrowser
|
||||
runtimeEnvironmentId={runtimeEnvironmentId}
|
||||
@@ -59,31 +70,59 @@ export function AddRepoServerPathStartStep({
|
||||
return (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{translate("auto.components.sidebar.AddRepoServerStartStep.39bd249b3a", "Add a project")}</DialogTitle>
|
||||
<DialogTitle>
|
||||
{translate(
|
||||
'auto.components.sidebar.AddRepoServerStartStep.39bd249b3a',
|
||||
'Add a project'
|
||||
)}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{translate("auto.components.sidebar.AddRepoServerStartStep.8efa930eb5", "Add another project from the selected runtime server.")}</DialogDescription>
|
||||
{translate(
|
||||
'auto.components.sidebar.AddRepoServerStartStep.8efa930eb5',
|
||||
'Add another project from the selected runtime server.'
|
||||
)}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-3 pt-2">
|
||||
{hostSelector}
|
||||
<div className="grid grid-cols-3 gap-2">
|
||||
<AddRepoServerStartAction
|
||||
icon={FolderOpen}
|
||||
title={translate("auto.components.sidebar.AddRepoServerStartStep.0adf083af7", "Browse server")}
|
||||
description={translate("auto.components.sidebar.AddRepoServerStartStep.516187414c", "Existing project or folder")}
|
||||
title={translate(
|
||||
'auto.components.sidebar.AddRepoServerStartStep.0adf083af7',
|
||||
'Browse server'
|
||||
)}
|
||||
description={translate(
|
||||
'auto.components.sidebar.AddRepoServerStartStep.516187414c',
|
||||
'Existing project or folder'
|
||||
)}
|
||||
disabled={disabled}
|
||||
onClick={() => setBrowsing(true)}
|
||||
/>
|
||||
<AddRepoServerStartAction
|
||||
icon={Globe}
|
||||
title={translate("auto.components.sidebar.AddRepoServerStartStep.47759c9491", "Clone from URL")}
|
||||
description={translate("auto.components.sidebar.AddRepoServerStartStep.a2ea37d549", "Remote Git repository")}
|
||||
title={translate(
|
||||
'auto.components.sidebar.AddRepoServerStartStep.47759c9491',
|
||||
'Clone from URL'
|
||||
)}
|
||||
description={translate(
|
||||
'auto.components.sidebar.AddRepoServerStartStep.a2ea37d549',
|
||||
'Remote Git repository'
|
||||
)}
|
||||
disabled={disabled}
|
||||
onClick={onOpenCloneStep}
|
||||
/>
|
||||
<AddRepoServerStartAction
|
||||
icon={Server}
|
||||
title={translate("auto.components.sidebar.AddRepoServerStartStep.a81ffa0a99", "Create on server")}
|
||||
description={translate("auto.components.sidebar.AddRepoServerStartStep.d40d751517", "New repo or folder")}
|
||||
title={translate(
|
||||
'auto.components.sidebar.AddRepoServerStartStep.a81ffa0a99',
|
||||
'Create on server'
|
||||
)}
|
||||
description={translate(
|
||||
'auto.components.sidebar.AddRepoServerStartStep.d40d751517',
|
||||
'New repo or folder'
|
||||
)}
|
||||
disabled={disabled}
|
||||
onClick={onOpenCreateStep}
|
||||
/>
|
||||
@@ -94,7 +133,11 @@ export function AddRepoServerPathStartStep({
|
||||
<Lightbulb className="size-3.5" />
|
||||
</span>
|
||||
<span className="min-w-0">
|
||||
{translate("auto.components.sidebar.AddRepoServerStartStep.6b9958492a", "Want to import many repos at once? Browse to the parent folder.")}</span>
|
||||
{translate(
|
||||
'auto.components.sidebar.AddRepoServerStartStep.6b9958492a',
|
||||
'Want to import many repos at once? Browse to the parent folder.'
|
||||
)}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<button
|
||||
@@ -103,7 +146,11 @@ export function AddRepoServerPathStartStep({
|
||||
disabled={disabled}
|
||||
className="mx-auto block rounded px-2 py-1 text-xs text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-default disabled:opacity-40"
|
||||
>
|
||||
{translate("auto.components.sidebar.AddRepoServerStartStep.438493f214", "Or enter a server path manually")}</button>
|
||||
{translate(
|
||||
'auto.components.sidebar.AddRepoServerStartStep.438493f214',
|
||||
'Or enter a server path manually'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
@@ -112,24 +159,38 @@ export function AddRepoServerPathStartStep({
|
||||
return (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{translate("auto.components.sidebar.AddRepoServerStartStep.3d0c035483", "Open server project")}</DialogTitle>
|
||||
<DialogTitle>
|
||||
{translate(
|
||||
'auto.components.sidebar.AddRepoServerStartStep.3d0c035483',
|
||||
'Open server project'
|
||||
)}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{translate("auto.components.sidebar.AddRepoServerStartStep.423b5d3d31", "Add a Git repository or folder that already exists on the selected runtime server.")}</DialogDescription>
|
||||
{translate(
|
||||
'auto.components.sidebar.AddRepoServerStartStep.423b5d3d31',
|
||||
'Add a Git repository or folder that already exists on the selected runtime server.'
|
||||
)}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-3 pt-2">
|
||||
{hostSelector}
|
||||
<div className="space-y-1">
|
||||
<label
|
||||
htmlFor="server-project-path"
|
||||
className="block text-[11px] font-medium text-muted-foreground"
|
||||
>
|
||||
{translate("auto.components.sidebar.AddRepoServerStartStep.867692f505", "Server path")}</label>
|
||||
{translate('auto.components.sidebar.AddRepoServerStartStep.867692f505', 'Server path')}
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
id="server-project-path"
|
||||
value={serverPath}
|
||||
onChange={(event) => onServerPathChange(event.target.value)}
|
||||
placeholder={translate("auto.components.sidebar.AddRepoServerStartStep.92d25420a0", "/home/user/project")}
|
||||
placeholder={translate(
|
||||
'auto.components.sidebar.AddRepoServerStartStep.92d25420a0',
|
||||
'/home/user/project'
|
||||
)}
|
||||
className="h-11 min-w-0 flex-1 font-mono text-sm"
|
||||
disabled={isAddingServerPath}
|
||||
autoFocus
|
||||
@@ -144,13 +205,20 @@ export function AddRepoServerPathStartStep({
|
||||
className="h-11 w-11 shrink-0"
|
||||
onClick={() => setBrowsing(true)}
|
||||
disabled={isAddingServerPath || !runtimeEnvironmentId}
|
||||
aria-label={translate("auto.components.sidebar.AddRepoServerStartStep.ac66a3ed2d", "Browse server filesystem")}
|
||||
aria-label={translate(
|
||||
'auto.components.sidebar.AddRepoServerStartStep.ac66a3ed2d',
|
||||
'Browse server filesystem'
|
||||
)}
|
||||
>
|
||||
<FolderOpen className="size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="top" sideOffset={4}>
|
||||
{translate("auto.components.sidebar.AddRepoServerStartStep.ac66a3ed2d", "Browse server filesystem")}</TooltipContent>
|
||||
{translate(
|
||||
'auto.components.sidebar.AddRepoServerStartStep.ac66a3ed2d',
|
||||
'Browse server filesystem'
|
||||
)}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
@@ -160,14 +228,22 @@ export function AddRepoServerPathStartStep({
|
||||
disabled={!serverPath.trim() || isAddingServerPath}
|
||||
className="h-10"
|
||||
>
|
||||
{translate("auto.components.sidebar.AddRepoServerStartStep.8da4d1a5be", "Add Git Project")}</Button>
|
||||
{translate(
|
||||
'auto.components.sidebar.AddRepoServerStartStep.8da4d1a5be',
|
||||
'Add Git Project'
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => onAddServerPath('folder')}
|
||||
disabled={!serverPath.trim() || isAddingServerPath}
|
||||
variant="outline"
|
||||
className="h-10"
|
||||
>
|
||||
{translate("auto.components.sidebar.AddRepoServerStartStep.e1710bf831", "Open as Folder")}</Button>
|
||||
{translate(
|
||||
'auto.components.sidebar.AddRepoServerStartStep.e1710bf831',
|
||||
'Open as Folder'
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
{isAddingServerPath && addProjectBusyLabel ? (
|
||||
<div className="flex items-center gap-2 rounded-md border border-border bg-muted px-3 py-2 text-xs text-muted-foreground">
|
||||
@@ -181,7 +257,11 @@ export function AddRepoServerPathStartStep({
|
||||
disabled={isAddingServerPath}
|
||||
className="mx-auto block rounded px-2 py-1 text-xs text-muted-foreground transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-default disabled:opacity-40"
|
||||
>
|
||||
{translate("auto.components.sidebar.AddRepoServerStartStep.ae990c86a0", "Back to add options")}</button>
|
||||
{translate(
|
||||
'auto.components.sidebar.AddRepoServerStartStep.ae990c86a0',
|
||||
'Back to add options'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -120,6 +120,27 @@ function getActionTitles(isSshLikely: boolean): {
|
||||
}
|
||||
}
|
||||
|
||||
function getHostAwareActionModel(): {
|
||||
secondary: string[]
|
||||
createDisabled: boolean | undefined
|
||||
} {
|
||||
const { secondaryActions } = getAddRepoLocalStartActions({
|
||||
isSshLikely: true,
|
||||
showRemoteAction: false,
|
||||
canCreateProject: false,
|
||||
onBrowse: vi.fn(),
|
||||
onOpenCloneStep: vi.fn(),
|
||||
onOpenRemoteStep: vi.fn(),
|
||||
onOpenCreateStep: vi.fn()
|
||||
})
|
||||
const createAction = secondaryActions.find((action) => action.kind === 'create')
|
||||
|
||||
return {
|
||||
secondary: secondaryActions.map((action) => action.title),
|
||||
createDisabled: createAction?.disabled
|
||||
}
|
||||
}
|
||||
|
||||
describe('AddRepoLocalStartStep', () => {
|
||||
afterEach(() => {
|
||||
document.body.innerHTML = ''
|
||||
@@ -159,6 +180,13 @@ describe('AddRepoLocalStartStep', () => {
|
||||
expect(titles.secondary).toEqual(['Remote project', 'Clone from URL', 'Create new project'])
|
||||
})
|
||||
|
||||
it('lets host-aware Add Project replace the separate remote row', () => {
|
||||
const model = getHostAwareActionModel()
|
||||
|
||||
expect(model.secondary).toEqual(['Clone from URL', 'Create new project'])
|
||||
expect(model.createDisabled).toBe(true)
|
||||
})
|
||||
|
||||
it('focuses Browse folder when the default Add Project step opens', async () => {
|
||||
const { container, root } = await renderLocalStartStepDom(false)
|
||||
const browseButton = findButton(container, 'Browse folder')
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useEffect, useRef, useState, type ComponentType, type Ref } from 'react'
|
||||
import { useEffect, useRef, useState, type ComponentType, type ReactNode, type Ref } from 'react'
|
||||
import { CircleStop, Loader2 } from 'lucide-react'
|
||||
import { DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'
|
||||
import { Button } from '@/components/ui/button'
|
||||
@@ -66,6 +66,9 @@ type AddRepoLocalStartStepProps = {
|
||||
addProjectBusyLabel: string | null
|
||||
nestedScanInProgress: boolean
|
||||
nestedScanId: string | null
|
||||
hostSelector?: ReactNode
|
||||
showRemoteAction?: boolean
|
||||
canCreateProject?: boolean
|
||||
onBrowse: () => void
|
||||
onOpenCloneStep: () => void
|
||||
onOpenRemoteStep: () => void
|
||||
@@ -80,6 +83,9 @@ export function AddRepoLocalStartStep({
|
||||
addProjectBusyLabel,
|
||||
nestedScanInProgress,
|
||||
nestedScanId,
|
||||
hostSelector,
|
||||
showRemoteAction = true,
|
||||
canCreateProject = true,
|
||||
onBrowse,
|
||||
onOpenCloneStep,
|
||||
onOpenRemoteStep,
|
||||
@@ -93,7 +99,9 @@ export function AddRepoLocalStartStep({
|
||||
onBrowse,
|
||||
onOpenCloneStep,
|
||||
onOpenRemoteStep,
|
||||
onOpenCreateStep
|
||||
onOpenCreateStep,
|
||||
showRemoteAction,
|
||||
canCreateProject
|
||||
})
|
||||
|
||||
// The white fill + ⏎ chip is a roving selection indicator, not a fixed "primary" badge:
|
||||
@@ -161,6 +169,7 @@ export function AddRepoLocalStartStep({
|
||||
onBlur={handleActionsBlur}
|
||||
onKeyDown={handleArrowNavigation}
|
||||
>
|
||||
{hostSelector}
|
||||
<AddRepoPrimaryStartAction
|
||||
icon={primaryAction.icon}
|
||||
title={primaryAction.title}
|
||||
@@ -187,7 +196,7 @@ export function AddRepoLocalStartStep({
|
||||
icon={action.icon}
|
||||
title={action.title}
|
||||
description={action.description}
|
||||
disabled={isAdding}
|
||||
disabled={isAdding || Boolean(action.disabled)}
|
||||
selected={selectedKind === action.kind}
|
||||
onClick={action.onClick}
|
||||
onFocus={() => setSelectedKind(action.kind)}
|
||||
|
||||
@@ -67,37 +67,47 @@ export function useRemoteRepo(
|
||||
void cancelNestedRepoScan(remoteNestedScanId)
|
||||
}, [cancelNestedRepoScan, remoteNestedScanId])
|
||||
|
||||
const handleOpenRemoteStep = useCallback(async () => {
|
||||
const gen = ++remoteGenRef.current
|
||||
setStep('remote')
|
||||
try {
|
||||
const targets = (await window.api.ssh.listTargets()) as SshTarget[]
|
||||
if (gen !== remoteGenRef.current) {
|
||||
return
|
||||
const handleOpenRemoteStep = useCallback(
|
||||
async (preferredTargetId?: string | null) => {
|
||||
const gen = ++remoteGenRef.current
|
||||
setStep('remote')
|
||||
try {
|
||||
const targets = (await window.api.ssh.listTargets()) as SshTarget[]
|
||||
if (gen !== remoteGenRef.current) {
|
||||
return
|
||||
}
|
||||
const withState = await Promise.all(
|
||||
targets.map(async (t) => {
|
||||
const state = (await window.api.ssh.getState({
|
||||
targetId: t.id
|
||||
})) as SshConnectionState | null
|
||||
return { ...t, state: state ?? undefined }
|
||||
})
|
||||
)
|
||||
if (gen !== remoteGenRef.current) {
|
||||
return
|
||||
}
|
||||
setSshTargets(withState)
|
||||
const preferred = preferredTargetId
|
||||
? withState.find((t) => t.id === preferredTargetId)
|
||||
: undefined
|
||||
const connected = withState.find((t) => t.state?.status === 'connected')
|
||||
if (preferred) {
|
||||
setSelectedTargetId(preferred.id)
|
||||
return
|
||||
}
|
||||
if (connected) {
|
||||
setSelectedTargetId(connected.id)
|
||||
}
|
||||
} catch {
|
||||
if (gen !== remoteGenRef.current) {
|
||||
return
|
||||
}
|
||||
setSshTargets([])
|
||||
}
|
||||
const withState = await Promise.all(
|
||||
targets.map(async (t) => {
|
||||
const state = (await window.api.ssh.getState({
|
||||
targetId: t.id
|
||||
})) as SshConnectionState | null
|
||||
return { ...t, state: state ?? undefined }
|
||||
})
|
||||
)
|
||||
if (gen !== remoteGenRef.current) {
|
||||
return
|
||||
}
|
||||
setSshTargets(withState)
|
||||
const connected = withState.find((t) => t.state?.status === 'connected')
|
||||
if (connected) {
|
||||
setSelectedTargetId(connected.id)
|
||||
}
|
||||
} catch {
|
||||
if (gen !== remoteGenRef.current) {
|
||||
return
|
||||
}
|
||||
setSshTargets([])
|
||||
}
|
||||
}, [setStep])
|
||||
},
|
||||
[setStep]
|
||||
)
|
||||
|
||||
// Why: keep the target list's connection state in sync while the dialog is
|
||||
// open, so clicking the inline Connect button below updates the dot/label
|
||||
@@ -116,7 +126,11 @@ export function useRemoteRepo(
|
||||
try {
|
||||
await window.api.ssh.connect({ targetId })
|
||||
} catch (err) {
|
||||
toast.error(err instanceof Error ? err.message : translate("auto.components.sidebar.AddRepoSteps.3e64e8a70d", "Connection failed"))
|
||||
toast.error(
|
||||
err instanceof Error
|
||||
? err.message
|
||||
: translate('auto.components.sidebar.AddRepoSteps.3e64e8a70d', 'Connection failed')
|
||||
)
|
||||
}
|
||||
}, [])
|
||||
|
||||
@@ -189,7 +203,10 @@ export function useRemoteRepo(
|
||||
if (!mountedRef.current || gen !== remoteGenRef.current) {
|
||||
return
|
||||
}
|
||||
toast.success(translate("auto.components.sidebar.AddRepoSteps.df8b0e6c22", "Remote project added"), { description: repo.displayName })
|
||||
toast.success(
|
||||
translate('auto.components.sidebar.AddRepoSteps.df8b0e6c22', 'Remote project added'),
|
||||
{ description: repo.displayName }
|
||||
)
|
||||
// Why: the repo is already persisted here; if SSH refresh is temporarily
|
||||
// non-authoritative, finish onto the project row instead of stranding the dialog.
|
||||
await fetchWorktrees(repo.id, { requireAuthoritative: true })
|
||||
@@ -293,9 +310,18 @@ export function CloneStep({
|
||||
return (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{translate("auto.components.sidebar.AddRepoSteps.a93ef169b5", "Browse server filesystem")}</DialogTitle>
|
||||
<DialogTitle>
|
||||
{translate(
|
||||
'auto.components.sidebar.AddRepoSteps.a93ef169b5',
|
||||
'Browse server filesystem'
|
||||
)}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{translate("auto.components.sidebar.AddRepoSteps.fe8e629fe3", "Navigate to a directory and click Select to choose it.")}</DialogDescription>
|
||||
{translate(
|
||||
'auto.components.sidebar.AddRepoSteps.fe8e629fe3',
|
||||
'Navigate to a directory and click Select to choose it.'
|
||||
)}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<RemoteFileBrowser
|
||||
runtimeEnvironmentId={runtimeEnvironmentId}
|
||||
@@ -313,18 +339,30 @@ export function CloneStep({
|
||||
return (
|
||||
<>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{translate("auto.components.sidebar.AddRepoSteps.c05f88a31f", "Clone from URL")}</DialogTitle>
|
||||
<DialogDescription>{translate("auto.components.sidebar.AddRepoSteps.5b2ea674b1", "Enter the Git URL and choose where to clone it.")}</DialogDescription>
|
||||
<DialogTitle>
|
||||
{translate('auto.components.sidebar.AddRepoSteps.c05f88a31f', 'Clone from URL')}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
{translate(
|
||||
'auto.components.sidebar.AddRepoSteps.5b2ea674b1',
|
||||
'Enter the Git URL and choose where to clone it.'
|
||||
)}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-3 pt-1">
|
||||
<div className="space-y-1">
|
||||
<label className="text-[11px] font-medium text-muted-foreground">{translate("auto.components.sidebar.AddRepoSteps.3d4acbe693", "Git URL")}</label>
|
||||
<label className="text-[11px] font-medium text-muted-foreground">
|
||||
{translate('auto.components.sidebar.AddRepoSteps.3d4acbe693', 'Git URL')}
|
||||
</label>
|
||||
<Input
|
||||
value={cloneUrl}
|
||||
onChange={(e) => onUrlChange(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={translate("auto.components.sidebar.AddRepoSteps.b698a4a29d", "https://github.com/user/repo.git")}
|
||||
placeholder={translate(
|
||||
'auto.components.sidebar.AddRepoSteps.b698a4a29d',
|
||||
'https://github.com/user/repo.git'
|
||||
)}
|
||||
className="h-8 text-xs"
|
||||
disabled={isCloning}
|
||||
autoFocus
|
||||
@@ -332,13 +370,18 @@ export function CloneStep({
|
||||
</div>
|
||||
|
||||
<div className="space-y-1">
|
||||
<label className="text-[11px] font-medium text-muted-foreground">{translate("auto.components.sidebar.AddRepoSteps.04a4c4e84a", "Clone location")}</label>
|
||||
<label className="text-[11px] font-medium text-muted-foreground">
|
||||
{translate('auto.components.sidebar.AddRepoSteps.04a4c4e84a', 'Clone location')}
|
||||
</label>
|
||||
<div className="flex gap-2">
|
||||
<Input
|
||||
value={cloneDestination}
|
||||
onChange={(e) => onDestChange(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={translate("auto.components.sidebar.AddRepoSteps.2ce3f6edf8", "/path/to/destination")}
|
||||
placeholder={translate(
|
||||
'auto.components.sidebar.AddRepoSteps.2ce3f6edf8',
|
||||
'/path/to/destination'
|
||||
)}
|
||||
className="h-8 text-xs flex-1"
|
||||
disabled={isCloning}
|
||||
/>
|
||||
@@ -354,8 +397,22 @@ export function CloneStep({
|
||||
onPickDestination()
|
||||
}}
|
||||
disabled={isCloning || (disableDestinationPicker && !runtimeEnvironmentId)}
|
||||
title={runtimeEnvironmentId ? translate("auto.components.sidebar.AddRepoSteps.a93ef169b5", "Browse server filesystem") : translate("auto.components.sidebar.AddRepoSteps.569326d9cc", "Choose folder")}
|
||||
aria-label={runtimeEnvironmentId ? translate("auto.components.sidebar.AddRepoSteps.a93ef169b5", "Browse server filesystem") : translate("auto.components.sidebar.AddRepoSteps.569326d9cc", "Choose folder")}
|
||||
title={
|
||||
runtimeEnvironmentId
|
||||
? translate(
|
||||
'auto.components.sidebar.AddRepoSteps.a93ef169b5',
|
||||
'Browse server filesystem'
|
||||
)
|
||||
: translate('auto.components.sidebar.AddRepoSteps.569326d9cc', 'Choose folder')
|
||||
}
|
||||
aria-label={
|
||||
runtimeEnvironmentId
|
||||
? translate(
|
||||
'auto.components.sidebar.AddRepoSteps.a93ef169b5',
|
||||
'Browse server filesystem'
|
||||
)
|
||||
: translate('auto.components.sidebar.AddRepoSteps.569326d9cc', 'Choose folder')
|
||||
}
|
||||
>
|
||||
<Folder className="size-3.5" />
|
||||
</Button>
|
||||
@@ -369,7 +426,9 @@ export function CloneStep({
|
||||
disabled={!cloneUrl.trim() || !cloneDestination.trim() || isCloning}
|
||||
className="w-full"
|
||||
>
|
||||
{isCloning ? translate("auto.components.sidebar.AddRepoSteps.69f5b5380d", "Cloning...") : translate("auto.components.sidebar.AddRepoSteps.32a7256d85", "Clone")}
|
||||
{isCloning
|
||||
? translate('auto.components.sidebar.AddRepoSteps.69f5b5380d', 'Cloning...')
|
||||
: translate('auto.components.sidebar.AddRepoSteps.32a7256d85', 'Clone')}
|
||||
</Button>
|
||||
|
||||
{/* Why: progress bar lives below the button so it doesn't push the
|
||||
|
||||
@@ -7,6 +7,8 @@ export type AddRepoLocalStartActionHandlers = {
|
||||
onOpenCloneStep: () => void
|
||||
onOpenRemoteStep: () => void
|
||||
onOpenCreateStep: () => void
|
||||
showRemoteAction?: boolean
|
||||
canCreateProject?: boolean
|
||||
}
|
||||
|
||||
export type AddRepoLocalStartAction = {
|
||||
@@ -14,6 +16,7 @@ export type AddRepoLocalStartAction = {
|
||||
icon: ComponentType<{ className?: string }>
|
||||
title: string
|
||||
description: string
|
||||
disabled?: boolean
|
||||
onClick: () => void
|
||||
}
|
||||
|
||||
@@ -22,7 +25,9 @@ export function getAddRepoLocalStartActions({
|
||||
onBrowse,
|
||||
onOpenCloneStep,
|
||||
onOpenRemoteStep,
|
||||
onOpenCreateStep
|
||||
onOpenCreateStep,
|
||||
showRemoteAction = true,
|
||||
canCreateProject = true
|
||||
}: { isSshLikely: boolean } & AddRepoLocalStartActionHandlers): {
|
||||
primaryAction: AddRepoLocalStartAction
|
||||
secondaryActions: AddRepoLocalStartAction[]
|
||||
@@ -30,35 +35,68 @@ export function getAddRepoLocalStartActions({
|
||||
const primaryAction = {
|
||||
kind: 'browse' as const,
|
||||
icon: FolderOpen,
|
||||
title: translate("auto.components.sidebar.add.repo.local.start.actions.2281fdc8c7", "Browse folder"),
|
||||
description: translate("auto.components.sidebar.add.repo.local.start.actions.fb4fc5380e", "Local project, Git repo, or folder with many repos"),
|
||||
title: translate(
|
||||
'auto.components.sidebar.add.repo.local.start.actions.2281fdc8c7',
|
||||
'Browse folder'
|
||||
),
|
||||
description: translate(
|
||||
'auto.components.sidebar.add.repo.local.start.actions.fb4fc5380e',
|
||||
'Local project, Git repo, or folder with many repos'
|
||||
),
|
||||
onClick: onBrowse
|
||||
}
|
||||
|
||||
const remote = {
|
||||
kind: 'remote' as const,
|
||||
icon: Monitor,
|
||||
title: translate("auto.components.sidebar.add.repo.local.start.actions.3d162cc76f", "Remote project"),
|
||||
description: translate("auto.components.sidebar.add.repo.local.start.actions.a6c20dca96", "Open a project from an SSH target"),
|
||||
title: translate(
|
||||
'auto.components.sidebar.add.repo.local.start.actions.3d162cc76f',
|
||||
'Remote project'
|
||||
),
|
||||
description: translate(
|
||||
'auto.components.sidebar.add.repo.local.start.actions.a6c20dca96',
|
||||
'Open a project from an SSH target'
|
||||
),
|
||||
onClick: onOpenRemoteStep
|
||||
}
|
||||
const clone = {
|
||||
kind: 'clone' as const,
|
||||
icon: Globe,
|
||||
title: translate("auto.components.sidebar.add.repo.local.start.actions.7edb8ebe24", "Clone from URL"),
|
||||
description: translate("auto.components.sidebar.add.repo.local.start.actions.5f9ffac036", "Clone a remote Git repository"),
|
||||
title: translate(
|
||||
'auto.components.sidebar.add.repo.local.start.actions.7edb8ebe24',
|
||||
'Clone from URL'
|
||||
),
|
||||
description: translate(
|
||||
'auto.components.sidebar.add.repo.local.start.actions.5f9ffac036',
|
||||
'Clone a remote Git repository'
|
||||
),
|
||||
onClick: onOpenCloneStep
|
||||
}
|
||||
const create = {
|
||||
kind: 'create' as const,
|
||||
icon: Plus,
|
||||
title: translate("auto.components.sidebar.add.repo.local.start.actions.c709860596", "Create new project"),
|
||||
description: translate("auto.components.sidebar.add.repo.local.start.actions.d72789705e", "Start from an empty folder"),
|
||||
title: translate(
|
||||
'auto.components.sidebar.add.repo.local.start.actions.c709860596',
|
||||
'Create new project'
|
||||
),
|
||||
description: canCreateProject
|
||||
? translate(
|
||||
'auto.components.sidebar.add.repo.local.start.actions.d72789705e',
|
||||
'Start from an empty folder'
|
||||
)
|
||||
: translate(
|
||||
'auto.components.sidebar.add.repo.local.start.actions.sshCreateUnavailable',
|
||||
'Not available for SSH hosts yet'
|
||||
),
|
||||
disabled: !canCreateProject,
|
||||
onClick: onOpenCreateStep
|
||||
}
|
||||
|
||||
// SSH-likely users reach for remote targets first, so surface that row ahead of clone.
|
||||
const secondaryActions = isSshLikely ? [remote, clone, create] : [clone, remote, create]
|
||||
const secondaryActions = showRemoteAction
|
||||
? isSshLikely
|
||||
? [remote, clone, create]
|
||||
: [clone, remote, create]
|
||||
: [clone, create]
|
||||
|
||||
return { primaryAction, secondaryActions }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,87 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react'
|
||||
import { useAppStore } from '@/store'
|
||||
import {
|
||||
getSettingsFocusedExecutionHostId,
|
||||
LOCAL_EXECUTION_HOST_ID,
|
||||
parseExecutionHostId,
|
||||
type ExecutionHostId
|
||||
} from '../../../../shared/execution-host'
|
||||
import type { AddRepoDialogStep } from './add-repo-dialog-types'
|
||||
import { useSidebarHostScopeOptions } from './use-sidebar-host-scope-options'
|
||||
|
||||
export function useAddRepoHostSelection({
|
||||
isOpen,
|
||||
setStep
|
||||
}: {
|
||||
isOpen: boolean
|
||||
setStep: (step: AddRepoDialogStep) => void
|
||||
}): {
|
||||
hostOptions: ReturnType<typeof useSidebarHostScopeOptions>['hostOptions']
|
||||
selectedHostId: ExecutionHostId
|
||||
selectedParsedHost: ReturnType<typeof parseExecutionHostId>
|
||||
selectedSshTargetId: string | null
|
||||
hostSelectorOpen: boolean
|
||||
setHostSelectorOpen: (open: boolean) => void
|
||||
handleSelectAddProjectHost: (hostId: ExecutionHostId) => Promise<void>
|
||||
} {
|
||||
const settings = useAppStore((s) => s.settings)
|
||||
const switchRuntimeEnvironment = useAppStore((s) => s.switchRuntimeEnvironment)
|
||||
const { hostOptions } = useSidebarHostScopeOptions()
|
||||
const [selectedAddProjectHostId, setSelectedAddProjectHostId] =
|
||||
useState<ExecutionHostId>(LOCAL_EXECUTION_HOST_ID)
|
||||
const [hostSelectorOpen, setHostSelectorOpen] = useState(false)
|
||||
const previousOpenRef = useRef(false)
|
||||
|
||||
const selectedHost =
|
||||
hostOptions.find((host) => host.id === selectedAddProjectHostId) ??
|
||||
hostOptions.find((host) => host.id === LOCAL_EXECUTION_HOST_ID) ??
|
||||
hostOptions[0]
|
||||
const selectedHostId = selectedHost?.id ?? LOCAL_EXECUTION_HOST_ID
|
||||
const selectedParsedHost = parseExecutionHostId(selectedHostId)
|
||||
const selectedSshTargetId =
|
||||
selectedParsedHost?.kind === 'ssh' ? selectedParsedHost.targetId : null
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && !previousOpenRef.current) {
|
||||
const focusedHostId = getSettingsFocusedExecutionHostId(settings)
|
||||
const nextHostId = hostOptions.some((host) => host.id === focusedHostId)
|
||||
? focusedHostId
|
||||
: LOCAL_EXECUTION_HOST_ID
|
||||
setSelectedAddProjectHostId(nextHostId)
|
||||
}
|
||||
if (!isOpen) {
|
||||
setHostSelectorOpen(false)
|
||||
}
|
||||
previousOpenRef.current = isOpen
|
||||
}, [hostOptions, isOpen, settings])
|
||||
|
||||
const handleSelectAddProjectHost = useCallback(
|
||||
async (hostId: ExecutionHostId): Promise<void> => {
|
||||
const parsed = parseExecutionHostId(hostId)
|
||||
if (parsed?.kind === 'runtime') {
|
||||
const switched = await switchRuntimeEnvironment(parsed.environmentId)
|
||||
if (!switched) {
|
||||
return
|
||||
}
|
||||
} else if (settings?.activeRuntimeEnvironmentId?.trim()) {
|
||||
const switched = await switchRuntimeEnvironment(null)
|
||||
if (!switched) {
|
||||
return
|
||||
}
|
||||
}
|
||||
setSelectedAddProjectHostId(hostId)
|
||||
setStep('add')
|
||||
},
|
||||
[settings?.activeRuntimeEnvironmentId, setStep, switchRuntimeEnvironment]
|
||||
)
|
||||
|
||||
return {
|
||||
hostOptions,
|
||||
selectedHostId,
|
||||
selectedParsedHost,
|
||||
selectedSshTargetId,
|
||||
hostSelectorOpen,
|
||||
setHostSelectorOpen,
|
||||
handleSelectAddProjectHost
|
||||
}
|
||||
}
|
||||
@@ -11,12 +11,14 @@ import { translate } from '@/i18n/i18n'
|
||||
export function useAddRepoCloneFlow({
|
||||
step,
|
||||
activeRuntimeEnvironmentId,
|
||||
sshTargetId,
|
||||
workspaceDir,
|
||||
fetchWorktrees,
|
||||
onGitRepoReady
|
||||
}: {
|
||||
step: AddRepoDialogStep
|
||||
activeRuntimeEnvironmentId: string | null | undefined
|
||||
sshTargetId?: string | null
|
||||
workspaceDir: string | null | undefined
|
||||
fetchWorktrees: (repoId: string, options?: { requireAuthoritative?: boolean }) => Promise<unknown>
|
||||
onGitRepoReady: (repoId: string, source: AddRepoExistingWorkspaceSource) => Promise<void>
|
||||
@@ -79,10 +81,15 @@ export function useAddRepoCloneFlow({
|
||||
}, [])
|
||||
|
||||
const handlePickDestination = useCallback(async (): Promise<void> => {
|
||||
if (activeRuntimeEnvironmentId?.trim()) {
|
||||
if (activeRuntimeEnvironmentId?.trim() || sshTargetId?.trim()) {
|
||||
// Why: the native folder picker returns a client-local path. Runtime
|
||||
// clone destinations must be typed as server paths.
|
||||
toast.error(translate("auto.components.sidebar.useAddRepoCloneFlow.0dc4d1b657", "Enter a server path for the clone destination."))
|
||||
// and SSH clone destinations must be typed as paths on that host.
|
||||
toast.error(
|
||||
translate(
|
||||
'auto.components.sidebar.useAddRepoCloneFlow.0dc4d1b657',
|
||||
'Enter a server path for the clone destination.'
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
const gen = cloneGenRef.current
|
||||
@@ -91,7 +98,7 @@ export function useAddRepoCloneFlow({
|
||||
setCloneDestination(dir)
|
||||
setCloneError(null)
|
||||
}
|
||||
}, [activeRuntimeEnvironmentId])
|
||||
}, [activeRuntimeEnvironmentId, sshTargetId])
|
||||
|
||||
const handleClone = useCallback(async (): Promise<void> => {
|
||||
const trimmedUrl = cloneUrl.trim()
|
||||
@@ -104,8 +111,13 @@ export function useAddRepoCloneFlow({
|
||||
setCloneProgress(null)
|
||||
try {
|
||||
const target = getActiveRuntimeTarget(useAppStore.getState().settings)
|
||||
const repo =
|
||||
target.kind === 'environment'
|
||||
const repo = sshTargetId?.trim()
|
||||
? await window.api.repos.cloneRemote({
|
||||
connectionId: sshTargetId.trim(),
|
||||
url: trimmedUrl,
|
||||
destination: cloneDestination.trim()
|
||||
})
|
||||
: target.kind === 'environment'
|
||||
? (
|
||||
await callRuntimeRpc<{ repo: Repo }>(
|
||||
target,
|
||||
@@ -124,7 +136,10 @@ export function useAddRepoCloneFlow({
|
||||
if (gen !== cloneGenRef.current) {
|
||||
return
|
||||
}
|
||||
toast.success(translate("auto.components.sidebar.useAddRepoCloneFlow.4d0013cc93", "Repository cloned"), { description: repo.displayName })
|
||||
toast.success(
|
||||
translate('auto.components.sidebar.useAddRepoCloneFlow.4d0013cc93', 'Repository cloned'),
|
||||
{ description: repo.displayName }
|
||||
)
|
||||
// Why: eagerly upsert so step 2 finds the repo before the IPC event.
|
||||
const state = useAppStore.getState()
|
||||
const existingIdx = state.repos.findIndex((r) => r.id === repo.id)
|
||||
@@ -153,7 +168,7 @@ export function useAddRepoCloneFlow({
|
||||
setIsCloning(false)
|
||||
}
|
||||
}
|
||||
}, [cloneUrl, cloneDestination, fetchWorktrees, onGitRepoReady])
|
||||
}, [cloneUrl, cloneDestination, fetchWorktrees, onGitRepoReady, sshTargetId])
|
||||
|
||||
return {
|
||||
cloneUrl,
|
||||
|
||||
@@ -3043,6 +3043,12 @@
|
||||
"45b7c26034": "Create project",
|
||||
"85085d74d2": "Creating…"
|
||||
},
|
||||
"AddRepoHostSelector": {
|
||||
"host": "Host",
|
||||
"local": "Local",
|
||||
"runtime": "Server",
|
||||
"ssh": "SSH"
|
||||
},
|
||||
"AddRepoNestedImportStep": {
|
||||
"496f68cf8c": "Scanning repositories. Click to stop.",
|
||||
"a32bef9516": "Stop scanning",
|
||||
@@ -3625,6 +3631,7 @@
|
||||
"5f9ffac036": "Clone a remote Git repository",
|
||||
"7edb8ebe24": "Clone from URL",
|
||||
"a6c20dca96": "Open a project from an SSH target",
|
||||
"sshCreateUnavailable": "Not available for SSH hosts yet",
|
||||
"3d162cc76f": "Remote project",
|
||||
"fb4fc5380e": "Local project, Git repo, or folder with many repos",
|
||||
"2281fdc8c7": "Browse folder"
|
||||
|
||||
@@ -3026,6 +3026,12 @@
|
||||
"45b7c26034": "プロジェクトの作成",
|
||||
"85085d74d2": "作成…"
|
||||
},
|
||||
"AddRepoHostSelector": {
|
||||
"host": "Host",
|
||||
"local": "Local",
|
||||
"runtime": "Server",
|
||||
"ssh": "SSH"
|
||||
},
|
||||
"AddRepoNestedImportStep": {
|
||||
"496f68cf8c": "リポジトリをスキャンしています。クリックして停止します。",
|
||||
"a32bef9516": "スキャンを停止する",
|
||||
@@ -3602,6 +3608,7 @@
|
||||
"c709860596": "新規プロジェクトを作成する",
|
||||
"5f9ffac036": "リモート Git リポジトリのクローンを作成する",
|
||||
"7edb8ebe24": "URLからクローンを作成する",
|
||||
"sshCreateUnavailable": "Not available for SSH hosts yet",
|
||||
"a6c20dca96": "SSH ターゲットからプロジェクトを開く",
|
||||
"3d162cc76f": "リモートプロジェクト",
|
||||
"fb4fc5380e": "ローカル プロジェクト、Git リポジトリ、または多数のリポジトリを含むフォルダー",
|
||||
|
||||
@@ -3026,6 +3026,12 @@
|
||||
"45b7c26034": "프로젝트 생성",
|
||||
"85085d74d2": "만드는 중…"
|
||||
},
|
||||
"AddRepoHostSelector": {
|
||||
"host": "Host",
|
||||
"local": "Local",
|
||||
"runtime": "Server",
|
||||
"ssh": "SSH"
|
||||
},
|
||||
"AddRepoNestedImportStep": {
|
||||
"496f68cf8c": "저장소를 스캔하는 중입니다. 중지하려면 클릭하세요.",
|
||||
"a32bef9516": "스캔 중지",
|
||||
@@ -3602,6 +3608,7 @@
|
||||
"c709860596": "새 프로젝트 만들기",
|
||||
"5f9ffac036": "원격 Git 저장소 복제",
|
||||
"7edb8ebe24": "URL에서 복제",
|
||||
"sshCreateUnavailable": "Not available for SSH hosts yet",
|
||||
"a6c20dca96": "SSH 대상에서 프로젝트 열기",
|
||||
"3d162cc76f": "원격 프로젝트",
|
||||
"fb4fc5380e": "로컬 프로젝트, Git 저장소 또는 저장소가 많은 폴더",
|
||||
|
||||
@@ -3026,6 +3026,12 @@
|
||||
"45b7c26034": "创建项目",
|
||||
"85085d74d2": "创建中…"
|
||||
},
|
||||
"AddRepoHostSelector": {
|
||||
"host": "Host",
|
||||
"local": "Local",
|
||||
"runtime": "Server",
|
||||
"ssh": "SSH"
|
||||
},
|
||||
"AddRepoNestedImportStep": {
|
||||
"496f68cf8c": "扫描存储库。单击停止。",
|
||||
"a32bef9516": "停止扫描",
|
||||
@@ -3602,6 +3608,7 @@
|
||||
"c709860596": "创建新项目",
|
||||
"5f9ffac036": "克隆远程 Git 存储库",
|
||||
"7edb8ebe24": "从 URL 克隆",
|
||||
"sshCreateUnavailable": "Not available for SSH hosts yet",
|
||||
"a6c20dca96": "从 SSH 目标打开项目",
|
||||
"3d162cc76f": "远程项目",
|
||||
"fb4fc5380e": "本地项目、Git 存储库或包含多个存储库的文件夹",
|
||||
|
||||
Reference in New Issue
Block a user