1- import { useState } from 'react'
2- import { useMutation , useQueryClient } from '@tanstack/react-query'
3- import { createRepo , discoverRepos } from '@/api/repos'
1+ import { useState , useRef , useCallback , useMemo } from 'react'
2+ import { useMutation , useQueryClient , useQuery } from '@tanstack/react-query'
3+ import { listRepos , createRepo , discoverRepos } from '@/api/repos'
44import { Button } from '@/components/ui/button'
55import { Input } from '@/components/ui/input'
66import { Dialog , DialogContent , DialogHeader , DialogTitle } from '@/components/ui/dialog'
77import { Tabs , TabsList , TabsTrigger } from '@/components/ui/tabs'
88import { Loader2 } from 'lucide-react'
99import { showToast } from '@/lib/toast'
10+ import { getRepoBaseDirectoryName , getRepoDirectoryNameError , getRepoNameFromUrl , normalizeRepoUrlForCompare , sanitizeRepoDirectoryName } from '@opencode-manager/shared/utils'
1011import type { DiscoverReposResponse } from '@opencode-manager/shared/types'
1112import type { Repo } from '@/api/types'
1213
@@ -20,15 +21,40 @@ export function AddRepoDialog({ open, onOpenChange }: AddRepoDialogProps) {
2021 const [ repoUrl , setRepoUrl ] = useState ( '' )
2122 const [ localPath , setLocalPath ] = useState ( '' )
2223 const [ folderPath , setFolderPath ] = useState ( '' )
24+ const [ directoryName , setDirectoryName ] = useState ( '' )
2325 const [ branch , setBranch ] = useState ( '' )
2426 const [ skipSSHVerification , setSkipSSHVerification ] = useState ( false )
27+ const directoryTouched = useRef ( false )
2528 const queryClient = useQueryClient ( )
2629
2730 const isSSHUrl = ( url : string ) : boolean => {
2831 return url . startsWith ( 'git@' ) || url . startsWith ( 'ssh://' )
2932 }
3033
3134 const showSkipSSHCheckbox = repoType === 'remote' && isSSHUrl ( repoUrl )
35+ const showDirectoryName = repoType === 'remote'
36+
37+ const { data : existingRepos } = useQuery ( {
38+ queryKey : [ 'repos' ] ,
39+ queryFn : listRepos ,
40+ staleTime : 30_000 ,
41+ } )
42+
43+ const directoryNameError = useMemo ( ( ) => {
44+ if ( ! showDirectoryName || ! directoryName ) return null
45+ return getRepoDirectoryNameError ( directoryName )
46+ } , [ showDirectoryName , directoryName ] )
47+
48+ const directoryCollision = useMemo ( ( ) => {
49+ if ( ! showDirectoryName || ! directoryName || directoryNameError || ! existingRepos ) return null
50+ const normalizedNewUrl = normalizeRepoUrlForCompare ( repoUrl )
51+ const colliding = existingRepos . find ( ( r ) => {
52+ if ( r . localPath !== directoryName && getRepoBaseDirectoryName ( r ) !== directoryName ) return false
53+ if ( r . repoUrl && normalizeRepoUrlForCompare ( r . repoUrl ) === normalizedNewUrl ) return false
54+ return true
55+ } )
56+ return colliding ?? null
57+ } , [ showDirectoryName , directoryName , directoryNameError , existingRepos , repoUrl ] )
3258
3359 type AddRepoResult =
3460 | { mode : 'single' ; repo : Repo }
@@ -46,7 +72,13 @@ export function AddRepoDialog({ open, onOpenChange }: AddRepoDialogProps) {
4672 return { mode : 'discover' , ...result }
4773 }
4874
49- const repo = await createRepo ( { repoUrl, branch : branch || undefined , useWorktree : false , skipSSHVerification } )
75+ const repo = await createRepo ( {
76+ repoUrl,
77+ directoryName : directoryName || undefined ,
78+ branch : branch || undefined ,
79+ useWorktree : false ,
80+ skipSSHVerification,
81+ } )
5082 return { mode : 'single' , repo }
5183 } ,
5284 onSuccess : ( result ) => {
@@ -55,9 +87,11 @@ export function AddRepoDialog({ open, onOpenChange }: AddRepoDialogProps) {
5587 setRepoUrl ( '' )
5688 setLocalPath ( '' )
5789 setFolderPath ( '' )
90+ setDirectoryName ( '' )
5891 setBranch ( '' )
5992 setRepoType ( 'remote' )
6093 setSkipSSHVerification ( false )
94+ directoryTouched . current = false
6195
6296 if ( result . mode === 'discover' ) {
6397 const summary = [
@@ -91,12 +125,21 @@ export function AddRepoDialog({ open, onOpenChange }: AddRepoDialogProps) {
91125 }
92126 }
93127
94- const handleRepoUrlChange = ( value : string ) => {
128+ const handleRepoUrlChange = useCallback ( ( value : string ) => {
95129 setRepoUrl ( value )
96130 if ( ! isSSHUrl ( value ) ) {
97131 setSkipSSHVerification ( false )
98132 }
99- }
133+ if ( ! directoryTouched . current ) {
134+ const extracted = sanitizeRepoDirectoryName ( getRepoNameFromUrl ( value ) )
135+ setDirectoryName ( extracted )
136+ }
137+ } , [ ] )
138+
139+ const handleDirectoryNameChange = useCallback ( ( value : string ) => {
140+ directoryTouched . current = true
141+ setDirectoryName ( value )
142+ } , [ ] )
100143
101144 return (
102145 < Dialog open = { open } onOpenChange = { onOpenChange } >
@@ -161,6 +204,37 @@ export function AddRepoDialog({ open, onOpenChange }: AddRepoDialogProps) {
161204 </ p >
162205 </ div >
163206 ) }
207+
208+ { showDirectoryName && (
209+ < div className = "space-y-2" >
210+ < label className = "text-sm text-zinc-400" > Directory Name</ label >
211+ < Input
212+ placeholder = "Auto-detected from URL"
213+ value = { directoryName }
214+ onChange = { ( e ) => handleDirectoryNameChange ( e . target . value ) }
215+ disabled = { mutation . isPending }
216+ className = "bg-[#1a1a1a] border-[#2a2a2a] text-white placeholder:text-zinc-500 min-h-[44px] text-base"
217+ />
218+ { directoryNameError ? (
219+ < p className = "text-xs text-amber-400" >
220+ { directoryNameError } .
221+ </ p >
222+ ) : directoryCollision ? (
223+ < p className = "text-xs text-amber-400" >
224+ A repository named '{ directoryName } ' already exists.
225+ { directoryCollision . repoUrl && directoryCollision . repoUrl !== repoUrl
226+ ? ` (${ directoryCollision . repoUrl } )`
227+ : ''
228+ }
229+ { ' ' } Choose a different directory name to clone this fork.
230+ </ p >
231+ ) : (
232+ < p className = "text-xs text-zinc-500" >
233+ Custom directory name for the cloned repository
234+ </ p >
235+ ) }
236+ </ div >
237+ ) }
164238
165239 < div className = "space-y-2" >
166240 < label className = "text-sm text-zinc-400" > Branch (optional)</ label >
@@ -204,7 +278,7 @@ export function AddRepoDialog({ open, onOpenChange }: AddRepoDialogProps) {
204278
205279 < Button
206280 type = "submit"
207- disabled = { ( ! repoUrl && repoType === 'remote' ) || ( ! localPath && repoType === 'local' ) || ( ! folderPath && repoType === 'folder' ) || mutation . isPending }
281+ disabled = { ( ! repoUrl && repoType === 'remote' ) || ( ! localPath && repoType === 'local' ) || ( ! folderPath && repoType === 'folder' ) || mutation . isPending || ( showDirectoryName && ( ! ! directoryNameError || ! ! directoryCollision ) ) }
208282 className = "w-full min-h-[48px] bg-blue-600 hover:bg-blue-700 text-white text-base font-medium"
209283 >
210284 { mutation . isPending ? (
0 commit comments