Keep new projects as Git repositories (#5612)

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Brennan Benson
2026-06-17 12:06:41 -07:00
committed by GitHub
co-authored by Orca
parent 12d78b55cd
commit 2916f68dc9
14 changed files with 62 additions and 232 deletions
@@ -3,17 +3,15 @@ import { describe, expect, it, vi } from 'vitest'
import { Dialog } from '@/components/ui/dialog'
import { TooltipProvider } from '@/components/ui/tooltip'
import { CreateStep } from './AddRepoCreateStep'
import type { GitAvailability, RepoKind } from './create-project-defaults'
import type { GitAvailability } from './create-project-defaults'
function renderCreateStep({
createName = '',
createKind = 'git',
gitAvailability = 'available',
createParent = '/Users/alice/orca/projects',
parentDefaultPending = false
}: {
createName?: string
createKind?: RepoKind
gitAvailability?: GitAvailability
createParent?: string
parentDefaultPending?: boolean
@@ -24,7 +22,6 @@ function renderCreateStep({
<CreateStep
createName={createName}
createParent={createParent}
createKind={createKind}
createError={null}
isCreating={false}
defaultParent="/Users/alice/orca/projects"
@@ -33,7 +30,6 @@ function renderCreateStep({
parentDefaultPending={parentDefaultPending}
onNameChange={vi.fn()}
onParentChange={vi.fn()}
onKindChange={vi.fn()}
onPickParent={vi.fn()}
onCreate={vi.fn()}
/>
@@ -56,11 +52,15 @@ describe('CreateStep', () => {
expect(html).not.toContain('aria-label="Browse host filesystem"')
})
it('shows the Git fallback explanation in the collapsed summary', () => {
const html = renderCreateStep({ createKind: 'folder', gitAvailability: 'unavailable' })
it('shows the Git-required explanation in the collapsed summary', () => {
const html = renderCreateStep({
createName: 'demo-project',
gitAvailability: 'unavailable'
})
expect(html).toContain('Folder in ~/orca/projects')
expect(html).toContain('Git isn&#x27;t installed, so a plain folder is the default.')
expect(html).toContain('Git repository in ~/orca/projects')
expect(html).toContain('Git is required to create a project.')
expect(html).toContain('disabled=""')
})
it('disables create while an auto-filled parent belongs to a previous target', () => {
@@ -1,6 +1,6 @@
// Step for AddRepoDialog (orca#763), split out so create-project state stays scoped.
import React, { useCallback, useMemo, useRef, useState } from 'react'
import { ChevronDown, Folder, GitBranch, Loader2 } from 'lucide-react'
import React, { useMemo, useState } from 'react'
import { ChevronDown, GitBranch, Loader2 } from 'lucide-react'
import { DialogHeader, DialogTitle, DialogDescription } from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
@@ -13,8 +13,7 @@ import { translate } from '@/i18n/i18n'
import {
formatCreateProjectParentSummary,
joinCreateProjectPath,
type GitAvailability,
type RepoKind
type GitAvailability
} from './create-project-defaults'
// ── UI helpers ───────────────────────────────────────────────────────
@@ -24,7 +23,6 @@ const CREATE_PROJECT_NAME_PLACEHOLDER = 'project-name'
type CreateStepProps = {
createName: string
createParent: string
createKind: RepoKind
createError: string | null
isCreating: boolean
defaultParent?: string
@@ -36,7 +34,6 @@ type CreateStepProps = {
sshTargetId?: string | null
onNameChange: (value: string) => void
onParentChange: (value: string) => void
onKindChange: (kind: RepoKind) => void
onPickParent: () => void
onCreate: () => void
}
@@ -44,7 +41,6 @@ type CreateStepProps = {
export function CreateStep({
createName,
createParent,
createKind,
createError,
isCreating,
defaultParent = '',
@@ -56,54 +52,21 @@ export function CreateStep({
sshTargetId,
onNameChange,
onParentChange,
onKindChange,
onPickParent,
onCreate
}: CreateStepProps): React.JSX.Element {
const radioGroupRef = useRef<HTMLDivElement>(null)
const radioFocusFrameRef = useRef<number | null>(null)
const [browsingParent, setBrowsingParent] = useState(false)
// Why: SSH hosts need a typed remote path; hiding that field behind the
// collapsed defaults makes the create flow look impossible.
const [advancedOpen, setAdvancedOpen] = useState(manualParentEntry)
const cancelRadioFocusFrame = useCallback((): void => {
if (radioFocusFrameRef.current === null) {
return
}
cancelAnimationFrame(radioFocusFrameRef.current)
radioFocusFrameRef.current = null
}, [])
const setRadioGroupNode = useCallback(
(node: HTMLDivElement | null): void => {
// Why: the queued arrow-key focus is only valid while this radiogroup is mounted.
if (!node) {
cancelRadioFocusFrame()
}
radioGroupRef.current = node
},
[cancelRadioFocusFrame]
)
// Arrow keys cycle selection within the radiogroup (WAI-ARIA radio pattern).
const cycleKind = useCallback(() => {
const next = createKind === 'git' ? 'folder' : 'git'
onKindChange(next)
cancelRadioFocusFrame()
radioFocusFrameRef.current = requestAnimationFrame(() => {
radioFocusFrameRef.current = null
const nextEl = radioGroupRef.current?.querySelector<HTMLButtonElement>(
`[data-kind="${next}"]`
)
nextEl?.focus()
})
}, [cancelRadioFocusFrame, createKind, onKindChange])
// Why: SSH hosts report "unknown"; only a confirmed Git miss should block
// Git-only creation.
const canSubmit =
createName.trim().length > 0 &&
createParent.trim().length > 0 &&
gitAvailability !== 'checking' &&
gitAvailability !== 'unavailable' &&
!parentDefaultPending &&
!isCreating
const missingLocationLabel = translate(
@@ -139,10 +102,10 @@ export function CreateStep({
const name = createName.trim() || CREATE_PROJECT_NAME_PLACEHOLDER
return createParent.trim() ? joinCreateProjectPath(createParent, name) : ''
}, [createName, createParent])
const kindLabel =
createKind === 'git'
? translate('auto.components.sidebar.AddRepoCreateStep.11fd2a7db8', 'Git repository')
: translate('auto.components.sidebar.AddRepoCreateStep.038729c107', 'Folder')
const kindLabel = translate(
'auto.components.sidebar.AddRepoCreateStep.11fd2a7db8',
'Git repository'
)
const showGitFallback = gitAvailability === 'unavailable'
const showGitChecking = gitAvailability === 'checking'
const showRuntimeMissingParent =
@@ -216,11 +179,7 @@ export function CreateStep({
className="flex w-full min-w-0 items-start gap-2.5 rounded-md px-3 py-2.5 text-left transition-colors cursor-pointer hover:bg-accent/50 focus-visible:outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50"
>
<span className="mt-0.5 inline-flex size-6 shrink-0 items-center justify-center rounded-md border border-border bg-background/60 text-muted-foreground">
{createKind === 'git' ? (
<GitBranch className="size-3.5" />
) : (
<Folder className="size-3.5" />
)}
<GitBranch className="size-3.5" />
</span>
<div className="min-w-0 flex-1">
<p className="truncate text-sm font-medium">
@@ -242,10 +201,10 @@ export function CreateStep({
)}
</p>
) : showGitFallback ? (
<p className="mt-0.5 text-[11px] text-muted-foreground">
<p className="mt-0.5 text-[11px] text-destructive">
{translate(
'auto.components.sidebar.AddRepoCreateStep.fe1e616c5b',
"Git isn't installed, so a plain folder is the default."
'Git is required to create a project.'
)}
</p>
) : showRuntimeMissingParent ? (
@@ -274,87 +233,6 @@ export function CreateStep({
{advancedOpen && (
<div className="space-y-3 border-t border-border px-3 py-3">
{/* Real radiogroup so screen readers announce the segmented choice. */}
<div className="space-y-1.5">
<span className="text-[11px] font-medium text-muted-foreground block">
{translate(
'auto.components.sidebar.AddRepoCreateStep.180e9b5e48',
'Project kind'
)}
</span>
<div
ref={setRadioGroupNode}
role="radiogroup"
aria-label={translate(
'auto.components.sidebar.AddRepoCreateStep.180e9b5e48',
'Project kind'
)}
className="grid grid-cols-2 rounded-md border border-border bg-muted/30 p-0.5"
>
{(['git', 'folder'] as const).map((kind) => {
const selected = createKind === kind
const label =
kind === 'git'
? translate(
'auto.components.sidebar.AddRepoCreateStep.11fd2a7db8',
'Git repository'
)
: translate(
'auto.components.sidebar.AddRepoCreateStep.038729c107',
'Folder'
)
const Icon = kind === 'git' ? GitBranch : Folder
return (
<button
key={kind}
type="button"
role="radio"
aria-checked={selected}
tabIndex={selected ? 0 : -1}
onClick={() => onKindChange(kind)}
onKeyDown={(e) => {
// Why: keep keyboard radio navigation intact inside the compact segmented control.
if (
e.key === 'ArrowLeft' ||
e.key === 'ArrowRight' ||
e.key === 'ArrowUp' ||
e.key === 'ArrowDown'
) {
e.preventDefault()
cycleKind()
} else if (e.key === ' ' || e.key === 'Enter') {
e.preventDefault()
onKindChange(kind)
}
}}
disabled={isCreating}
data-kind={kind}
className={cn(
'inline-flex min-w-0 items-center justify-center gap-1.5 rounded-sm border px-2.5 py-2 text-xs font-medium outline-none transition-colors',
// Why: the segment sits on a muted card, so bg-background alone
// is too subtle; the border makes the selected state legible.
selected
? 'border-border bg-background text-foreground shadow-xs'
: 'border-transparent text-muted-foreground hover:text-foreground',
'focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-60'
)}
>
<Icon className="size-3.5 shrink-0" />
<span className="truncate">{label}</span>
</button>
)
})}
</div>
{showGitFallback && (
<p className="text-[11px] text-muted-foreground">
{translate(
'auto.components.sidebar.AddRepoCreateStep.fe1e616c5b',
"Git isn't installed, so a plain folder is the default."
)}
</p>
)}
</div>
{/* The local picker returns client paths; runtime servers browse host paths via RPC. */}
<CreateProjectLocationField
createParent={createParent}
@@ -100,12 +100,10 @@ const AddRepoDialog = React.memo(function AddRepoDialog() {
const {
createName,
createParent,
createKind,
createError,
isCreating,
setCreateName,
setCreateParent,
setCreateKind,
setCreateError,
resetCreateState,
handlePickParent,
@@ -127,15 +125,13 @@ const AddRepoDialog = React.memo(function AddRepoDialog() {
createRuntimeParentStatus,
createParentDefaultPending,
resetCreateDefaultState,
markCreateParentTouched,
markCreateKindTouched
markCreateParentTouched
} = useCreateProjectDefaults({
step,
activeRuntimeEnvironmentId: selectedRuntimeEnvironmentId,
sshTargetId: hostSelection.selectedSshTargetId,
createParent,
setCreateParent,
setCreateKind
setCreateParent
})
const {
@@ -325,7 +321,6 @@ const AddRepoDialog = React.memo(function AddRepoDialog() {
nestedGroupName={nestedGroupName}
createName={createName}
createParent={createParent}
createKind={createKind}
createError={createError}
isCreating={isCreating}
hostSelector={<AddRepoHostSelectorSlot hostSelection={hostSelection} />}
@@ -395,11 +390,6 @@ const AddRepoDialog = React.memo(function AddRepoDialog() {
setCreateParent(value)
setCreateError(null)
}}
onCreateKindChange={(kind) => {
markCreateKindTouched()
setCreateKind(kind)
setCreateError(null)
}}
onPickCreateParent={() => {
void handlePickParent().then((dir) => {
if (dir) {
@@ -53,7 +53,6 @@ function renderStepContent(overrides: Partial<StepContentProps>): string {
nestedGroupName: 'platform',
createName: '',
createParent: '',
createKind: 'git',
createError: null,
isCreating: false,
createDefaultParent: '',
@@ -82,7 +81,6 @@ function renderStepContent(overrides: Partial<StepContentProps>): string {
onImportNestedRepos: vi.fn(),
onCreateNameChange: vi.fn(),
onCreateParentChange: vi.fn(),
onCreateKindChange: vi.fn(),
onPickCreateParent: vi.fn(),
onCreate: vi.fn(),
...overrides
@@ -41,7 +41,6 @@ type AddRepoDialogStepContentProps = {
nestedGroupName: string
createName: string
createParent: string
createKind: 'git' | 'folder'
createError: string | null
isCreating: boolean
hostSelector?: ReactNode
@@ -75,7 +74,6 @@ type AddRepoDialogStepContentProps = {
onImportNestedRepos: (mode: 'group' | 'separate') => void
onCreateNameChange: (name: string) => void
onCreateParentChange: (parent: string) => void
onCreateKindChange: (kind: 'git' | 'folder') => void
onPickCreateParent: () => void
onCreate: () => void
}
@@ -111,7 +109,6 @@ export function AddRepoDialogStepContent({
nestedGroupName,
createName,
createParent,
createKind,
createError,
isCreating,
hostSelector,
@@ -145,7 +142,6 @@ export function AddRepoDialogStepContent({
onImportNestedRepos,
onCreateNameChange,
onCreateParentChange,
onCreateKindChange,
onPickCreateParent,
onCreate
}: AddRepoDialogStepContentProps): React.JSX.Element | null {
@@ -251,7 +247,6 @@ export function AddRepoDialogStepContent({
<CreateStep
createName={createName}
createParent={createParent}
createKind={createKind}
createError={createError}
isCreating={isCreating}
defaultParent={createDefaultParent}
@@ -263,7 +258,6 @@ export function AddRepoDialogStepContent({
sshTargetId={selectedSshTargetId}
onNameChange={onCreateNameChange}
onParentChange={onCreateParentChange}
onKindChange={onCreateKindChange}
onPickParent={onPickCreateParent}
onCreate={onCreate}
/>
@@ -63,16 +63,14 @@ function useHarness(overrides: Partial<Parameters<typeof useCreateProjectDefault
mocks.stateIndex = 0
mocks.refIndex = 0
const setCreateParent = vi.fn()
const setCreateKind = vi.fn()
const result = useCreateProjectDefaults({
step: 'create',
activeRuntimeEnvironmentId: null,
createParent: '',
setCreateParent,
setCreateKind,
...overrides
})
return { result, setCreateParent, setCreateKind }
return { result, setCreateParent }
}
describe('useCreateProjectDefaults', () => {
@@ -93,16 +91,15 @@ describe('useCreateProjectDefaults', () => {
mocks.getDefaultCreateProjectParent.mockResolvedValue('/Users/alice/orca/projects')
})
it('auto-fills the local default parent and defaults to git when available', async () => {
it('auto-fills the local default parent and reports Git availability', async () => {
mocks.isGitAvailable.mockResolvedValue(true)
const { setCreateParent, setCreateKind } = useHarness()
const { setCreateParent } = useHarness()
await flushAsync()
expect(setCreateParent).toHaveBeenCalledWith('/Users/alice/orca/projects')
expect(mocks.stateValues[DEFAULT_PARENT_STATE]).toBe('/Users/alice/orca/projects')
expect(mocks.stateValues[GIT_AVAILABILITY_STATE]).toBe('available')
expect(setCreateKind).toHaveBeenCalledWith('git')
expect(mocks.getDefaultCreateProjectParent).toHaveBeenCalled()
expect(mocks.callRuntimeRpc).not.toHaveBeenCalled()
})
@@ -128,24 +125,22 @@ describe('useCreateProjectDefaults', () => {
expect(mocks.stateValues[DEFAULT_PARENT_STATE]).toBe('/Users/alice/orca/projects')
})
it('defaults to folder with a visible fallback when Git is unavailable', async () => {
it('reports unavailable Git without changing the fixed project kind', async () => {
mocks.isGitAvailable.mockResolvedValue(false)
const { setCreateKind } = useHarness()
useHarness()
await flushAsync()
expect(mocks.stateValues[GIT_AVAILABILITY_STATE]).toBe('unavailable')
expect(setCreateKind).toHaveBeenCalledWith('folder')
})
it('reports unknown availability and keeps the kind when the Git probe fails', async () => {
it('reports unknown availability when the Git probe fails', async () => {
mocks.isGitAvailable.mockRejectedValue(new Error('probe failed'))
const { setCreateKind } = useHarness()
useHarness()
await flushAsync()
expect(mocks.stateValues[GIT_AVAILABILITY_STATE]).toBe('unknown')
expect(setCreateKind).not.toHaveBeenCalled()
})
it('does not overwrite a parent the user already chose', async () => {
@@ -161,7 +156,7 @@ describe('useCreateProjectDefaults', () => {
mocks.browseRuntimeServerDirectory.mockResolvedValue({ resolvedPath: '/home/alice' })
mocks.callRuntimeRpc.mockResolvedValue({ available: true })
const { setCreateParent, setCreateKind } = useHarness({ activeRuntimeEnvironmentId: 'env-1' })
const { setCreateParent } = useHarness({ activeRuntimeEnvironmentId: 'env-1' })
await flushAsync()
expect(mocks.browseRuntimeServerDirectory).toHaveBeenCalledWith('env-1', '~')
@@ -176,7 +171,6 @@ describe('useCreateProjectDefaults', () => {
{ timeoutMs: 3000 }
)
expect(mocks.isGitAvailable).not.toHaveBeenCalled()
expect(setCreateKind).toHaveBeenCalledWith('git')
})
it('replaces an untouched local default when switching to a runtime target', async () => {
@@ -251,11 +245,10 @@ describe('useCreateProjectDefaults', () => {
it('does not use client defaults or Git probing for SSH targets', async () => {
mocks.isGitAvailable.mockResolvedValue(true)
const { setCreateParent, setCreateKind } = useHarness({ sshTargetId: 'ssh-1' })
const { setCreateParent } = useHarness({ sshTargetId: 'ssh-1' })
await flushAsync()
expect(setCreateParent).not.toHaveBeenCalled()
expect(setCreateKind).not.toHaveBeenCalled()
expect(mocks.getDefaultCreateProjectParent).not.toHaveBeenCalled()
expect(mocks.isGitAvailable).not.toHaveBeenCalled()
expect(mocks.callRuntimeRpc).not.toHaveBeenCalled()
@@ -263,11 +256,10 @@ describe('useCreateProjectDefaults', () => {
})
it('does nothing outside the create step', async () => {
const { setCreateParent, setCreateKind } = useHarness({ step: 'add' })
const { setCreateParent } = useHarness({ step: 'add' })
await flushAsync()
expect(setCreateParent).not.toHaveBeenCalled()
expect(setCreateKind).not.toHaveBeenCalled()
expect(mocks.isGitAvailable).not.toHaveBeenCalled()
expect(mocks.browseRuntimeServerDirectory).not.toHaveBeenCalled()
})
@@ -5,11 +5,7 @@ import { useCallback, useEffect, useRef, useState } from 'react'
import { browseRuntimeServerDirectory } from '@/runtime/runtime-server-directory-browser'
import { callRuntimeRpc } from '@/runtime/runtime-rpc-client'
import type { AddRepoDialogStep } from './add-repo-dialog-types'
import {
getDefaultCreateProjectParent,
type GitAvailability,
type RepoKind
} from './create-project-defaults'
import { getDefaultCreateProjectParent, type GitAvailability } from './create-project-defaults'
const LOCAL_GIT_AVAILABILITY_TIMEOUT_MS = 1500
const RUNTIME_GIT_AVAILABILITY_TIMEOUT_MS = 3000
@@ -52,15 +48,13 @@ export function useCreateProjectDefaults({
activeRuntimeEnvironmentId,
sshTargetId,
createParent,
setCreateParent,
setCreateKind
setCreateParent
}: {
step: AddRepoDialogStep
activeRuntimeEnvironmentId: string | null | undefined
sshTargetId?: string | null | undefined
createParent: string
setCreateParent: (value: string) => void
setCreateKind: (kind: RepoKind) => void
}): {
createDefaultParent: string
createGitAvailability: GitAvailability
@@ -68,7 +62,6 @@ export function useCreateProjectDefaults({
createParentDefaultPending: boolean
resetCreateDefaultState: () => void
markCreateParentTouched: (value?: string) => void
markCreateKindTouched: () => void
} {
const [createDefaultParent, setCreateDefaultParent] = useState('')
const [createGitAvailability, setCreateGitAvailability] = useState<GitAvailability>('unknown')
@@ -78,7 +71,6 @@ export function useCreateProjectDefaults({
const autoFilledCreateParentRef = useRef<AutoFilledCreateParent | null>(null)
const createParentProvenanceRef = useRef<CreateParentProvenance | null>(null)
const createParentTouchedRef = useRef(false)
const createKindTouchedRef = useRef(false)
const createParentDefaultGenRef = useRef(0)
const createGitProbeGenRef = useRef(0)
const activeCreateParentRuntimeEnvironmentId = activeRuntimeEnvironmentId?.trim() || null
@@ -104,13 +96,12 @@ export function useCreateProjectDefaults({
autoFilledCreateParentRef.current = null
createParentProvenanceRef.current = null
createParentTouchedRef.current = false
createKindTouchedRef.current = false
setCreateDefaultParent('')
setCreateGitAvailability('unknown')
setCreateRuntimeParentStatus('idle')
}, [])
// Why: a default must never clobber a parent or kind the user picked themselves.
// Why: a default must never clobber a parent the user picked themselves.
const markCreateParentTouched = useCallback(
(value?: string) => {
autoFilledCreateParentRef.current = null
@@ -122,9 +113,6 @@ export function useCreateProjectDefaults({
},
[activeCreateParentTargetKey, createParent]
)
const markCreateKindTouched = useCallback(() => {
createKindTouchedRef.current = true
}, [])
const createParentDefaultPending =
step === 'create' &&
@@ -296,10 +284,6 @@ export function useCreateProjectDefaults({
return
}
setCreateGitAvailability(available ? 'available' : 'unavailable')
if (createKindTouchedRef.current) {
return
}
setCreateKind(available ? 'git' : 'folder')
})
.catch(() => {
if (gen !== createGitProbeGenRef.current) {
@@ -307,7 +291,7 @@ export function useCreateProjectDefaults({
}
setCreateGitAvailability('unknown')
})
}, [activeRuntimeEnvironmentId, activeCreateParentSshTargetId, setCreateKind, step])
}, [activeRuntimeEnvironmentId, activeCreateParentSshTargetId, step])
return {
createDefaultParent,
@@ -315,7 +299,6 @@ export function useCreateProjectDefaults({
createRuntimeParentStatus,
createParentDefaultPending: createParentPending,
resetCreateDefaultState,
markCreateParentTouched,
markCreateKindTouched
markCreateParentTouched
}
}
@@ -81,6 +81,11 @@ vi.mock('@/runtime/runtime-rpc-client', () => ({
callRuntimeRpc: mocks.callRuntimeRpc
}))
const STATE_NAME = 0
const STATE_PARENT_PATH = 1
const STATE_ERROR_MESSAGE = 2
const STATE_IS_CREATING = 3
function makeRepo(overrides: Partial<Repo> = {}): Repo {
return {
id: 'repo-created',
@@ -98,7 +103,11 @@ describe('useCreateRepo default-checkout handoff', () => {
vi.clearAllMocks()
mocks.stateIndex = 0
mocks.stateSetters = []
mocks.stateValues = ['created', '/projects', 'git', null, false]
mocks.stateValues = []
mocks.stateValues[STATE_NAME] = 'created'
mocks.stateValues[STATE_PARENT_PATH] = '/projects'
mocks.stateValues[STATE_ERROR_MESSAGE] = null
mocks.stateValues[STATE_IS_CREATING] = false
mocks.storeState.repos = []
mocks.storeState.projects = []
mocks.storeState.projectHostSetups = []
@@ -151,7 +160,7 @@ describe('useCreateRepo default-checkout handoff', () => {
const result = useCreateRepo(mocks.fetchWorktrees, vi.fn(), mocks.onGitRepoReady)
await expect(result.handlePickParent()).resolves.toBe(pickedDir)
expect(mocks.stateSetters[1]).toHaveBeenCalledWith(pickedDir)
expect(mocks.stateSetters[STATE_PARENT_PATH]).toHaveBeenCalledWith(pickedDir)
})
it('does not return a parent path when the runtime target blocks the local picker', async () => {
@@ -163,7 +172,7 @@ describe('useCreateRepo default-checkout handoff', () => {
await expect(result.handlePickParent()).resolves.toBeNull()
expect(window.api.repos.pickDirectory).not.toHaveBeenCalled()
expect(mocks.stateSetters[1]).not.toHaveBeenCalled()
expect(mocks.stateSetters[STATE_PARENT_PATH]).not.toHaveBeenCalled()
})
it('continues to completion when refresh is not authoritative after create', async () => {
@@ -179,16 +188,15 @@ describe('useCreateRepo default-checkout handoff', () => {
requireAuthoritative: true
})
expect(mocks.onGitRepoReady).toHaveBeenCalledWith(repo.id)
expect(mocks.stateSetters[3]).not.toHaveBeenCalledWith(
expect(mocks.stateSetters[STATE_ERROR_MESSAGE]).not.toHaveBeenCalledWith(
'Could not refresh project worktrees. Try again.'
)
})
it('marks onboarding folder progress when a created folder project opens', async () => {
it('opens an existing folder project returned by create dedupe', async () => {
const repo = makeRepo({ kind: 'folder' })
const worktree = { id: `${repo.id}::/projects/created` }
const closeModal = vi.fn()
mocks.stateValues = ['created', '/projects', 'folder', null, false]
mocks.createRepo.mockResolvedValue({ repo })
mocks.fetchWorktrees.mockImplementation(async (repoId: string) => {
mocks.storeState.worktreesByRepo = { [repoId]: [worktree] }
@@ -202,7 +210,7 @@ describe('useCreateRepo default-checkout handoff', () => {
expect(mocks.createRepo).toHaveBeenCalledWith({
parentPath: '/projects',
name: 'created',
kind: 'folder'
kind: 'git'
})
expect(mocks.fetchWorktrees).toHaveBeenCalledWith(repo.id)
expect(mocks.activateAndRevealWorktree).toHaveBeenCalledWith(worktree.id, {
@@ -10,7 +10,6 @@ import { callRuntimeRpc, getActiveRuntimeTarget } from '@/runtime/runtime-rpc-cl
import { isGitRepoKind } from '../../../../shared/repo-kind'
import type { Repo } from '../../../../shared/types'
import { translate } from '@/i18n/i18n'
import type { RepoKind } from './create-project-defaults'
import { extractIpcErrorMessage } from '@/lib/ipc-error'
import { upsertAddedRepoWithProjectHostSetup } from './add-repo-store-upsert'
@@ -29,7 +28,6 @@ export function useCreateRepo(
) {
const [createName, setCreateName] = useState('')
const [createParent, setCreateParent] = useState('')
const [createKind, setCreateKind] = useState<RepoKind>('git')
const [createError, setCreateError] = useState<string | null>(null)
const [isCreating, setIsCreating] = useState(false)
const mountedRef = useMountedRef()
@@ -46,7 +44,6 @@ export function useCreateRepo(
createGenRef.current++
setCreateName('')
setCreateParent('')
setCreateKind('git')
setCreateError(null)
setIsCreating(false)
}, [])
@@ -101,6 +98,9 @@ export function useCreateRepo(
...useAppStore.getState().settings,
activeRuntimeEnvironmentId: null
})
// Why: Create Project is intentionally Git-only; non-Git folders use the
// existing add-folder flows instead of this path.
const createKind = 'git' as const
const result = options.sshTargetId
? await window.api.repos.createRemote({
connectionId: options.sshTargetId,
@@ -218,7 +218,6 @@ export function useCreateRepo(
}, [
createName,
createParent,
createKind,
fetchWorktrees,
mountedRef,
closeModal,
@@ -230,12 +229,10 @@ export function useCreateRepo(
return {
createName,
createParent,
createKind,
createError,
isCreating,
setCreateName,
setCreateParent,
setCreateKind,
setCreateError,
resetCreateState,
handlePickParent,
+1 -3
View File
@@ -3213,14 +3213,12 @@
"AddRepoCreateStep": {
"0ae45b8238": "my-project",
"a8149a3a5a": "Name",
"038729c107": "Folder",
"11fd2a7db8": "Git repository",
"180e9b5e48": "Project kind",
"c7b9f94456": "Create a new project",
"b100311784": "Name it and Orca will create a real project with sensible defaults.",
"685b5eefe1": "{{kind}} in {{parent}}",
"2a762f3b19": "Checking Git on this host...",
"fe1e616c5b": "Git isn't installed, so a plain folder is the default.",
"fe1e616c5b": "Git is required to create a project.",
"c234df77f7": "Choose or enter a host parent folder before creating.",
"3a13f6e88b": "location not selected",
"6ed14c0281": "host folder not selected",
+1 -3
View File
@@ -3213,9 +3213,7 @@
"AddRepoCreateStep": {
"0ae45b8238": "mi-proyecto",
"a8149a3a5a": "Nombre",
"038729c107": "Carpeta",
"11fd2a7db8": "Repositorio Git",
"180e9b5e48": "Tipo de proyecto",
"5e97f0c4b9": "Proyecto creado",
"2c12db1511": "Proyecto ya agregado",
"875dda0995": "Introduzca una ruta principal del servidor.",
@@ -3225,7 +3223,7 @@
"b100311784": "Ponle un nombre y Orca creará un proyecto real con valores predeterminados sensatos.",
"685b5eefe1": "{{kind}} en {{parent}}",
"2a762f3b19": "Comprobando Git en este host...",
"fe1e616c5b": "Git no está instalado, así que una carpeta simple será el valor predeterminado.",
"fe1e616c5b": "Git es necesario para crear un proyecto.",
"c234df77f7": "Elige o introduce una carpeta principal del servidor antes de crear.",
"3a13f6e88b": "ubicación no seleccionada",
"6ed14c0281": "carpeta del servidor no seleccionada",
+1 -3
View File
@@ -3194,9 +3194,7 @@
"AddRepoCreateStep": {
"0ae45b8238": "私のプロジェクト",
"a8149a3a5a": "名前",
"038729c107": "フォルダ",
"11fd2a7db8": "Gitリポジトリ",
"180e9b5e48": "プロジェクトの種類",
"5e97f0c4b9": "プロジェクトが作成されました",
"2c12db1511": "プロジェクトはすでに追加されています",
"875dda0995": "サーバーの親パスを入力します。",
@@ -3206,7 +3204,7 @@
"b100311784": "名前を付けると、Orca が適切な既定値で実際のプロジェクトを作成します。",
"685b5eefe1": "{{parent}} の {{kind}}",
"2a762f3b19": "このホストで Git を確認中...",
"fe1e616c5b": "Git がインストールされていないため、通常のフォルダが既定になります。",
"fe1e616c5b": "プロジェクトを作成するには Git が必要です。",
"c234df77f7": "作成する前に、サーバーの親フォルダを選択または入力してください。",
"3a13f6e88b": "場所が選択されていません",
"6ed14c0281": "サーバーフォルダが選択されていません",
+1 -3
View File
@@ -3194,9 +3194,7 @@
"AddRepoCreateStep": {
"0ae45b8238": "내 프로젝트",
"a8149a3a5a": "이름",
"038729c107": "폴더",
"11fd2a7db8": "Git 저장소",
"180e9b5e48": "프로젝트 종류",
"5e97f0c4b9": "프로젝트가 생성되었습니다.",
"2c12db1511": "프로젝트가 이미 추가되었습니다.",
"875dda0995": "서버 상위 경로를 입력하세요.",
@@ -3206,7 +3204,7 @@
"b100311784": "이름을 지정하면 Orca가 적절한 기본값으로 실제 프로젝트를 만듭니다.",
"685b5eefe1": "{{parent}}의 {{kind}}",
"2a762f3b19": "이 호스트에서 Git 확인 중...",
"fe1e616c5b": "Git이 설치되어 있지 않아 일반 폴더가 기본값입니다.",
"fe1e616c5b": "프로젝트를 만들려면 Git이 필요합니다.",
"c234df77f7": "만들기 전에 서버 상위 폴더를 선택하거나 입력하세요.",
"3a13f6e88b": "위치가 선택되지 않음",
"6ed14c0281": "서버 폴더가 선택되지 않음",
+1 -3
View File
@@ -3194,9 +3194,7 @@
"AddRepoCreateStep": {
"0ae45b8238": "我的项目",
"a8149a3a5a": "名称",
"038729c107": "文件夹",
"11fd2a7db8": "Git 存储库",
"180e9b5e48": "项目类型",
"5e97f0c4b9": "项目已创建",
"2c12db1511": "项目已添加",
"875dda0995": "输入服务器父路径。",
@@ -3206,7 +3204,7 @@
"b100311784": "为它命名,Orca 会使用合理的默认设置创建一个真实项目。",
"685b5eefe1": "{{parent}} 中的 {{kind}}",
"2a762f3b19": "正在检查此主机上的 Git...",
"fe1e616c5b": "未安装 Git,因此默认创建普通文件夹。",
"fe1e616c5b": "创建项目需要 Git。",
"c234df77f7": "创建前请选择或输入服务器父文件夹。",
"3a13f6e88b": "未选择位置",
"6ed14c0281": "未选择服务器文件夹",