1- import { useState , useCallback , useEffect } from 'react' ;
1+ import { useState , useCallback , useEffect , useMemo } from 'react' ;
22import { useTranslation } from 'react-i18next' ;
33import { GitBranch , Loader2 , FolderGit , ListTodo } from 'lucide-react' ;
44import {
@@ -20,6 +20,7 @@ import {
2020 SelectTrigger ,
2121 SelectValue ,
2222} from '../ui/select' ;
23+ import { Combobox , type ComboboxOption } from '../ui/combobox' ;
2324import type { Task , TerminalWorktreeConfig } from '../../../shared/types' ;
2425import { useProjectStore } from '../../stores/project-store' ;
2526
@@ -99,12 +100,15 @@ export function CreateWorktreeDialog({
99100 } , [ open , projectPath , project ?. settings ?. mainBranch ] ) ;
100101
101102 const handleNameChange = useCallback ( ( e : React . ChangeEvent < HTMLInputElement > ) => {
102- // Auto-sanitize: lowercase, replace spaces and invalid chars
103+ // Auto-sanitize: lowercase, allow letters, numbers, dashes, underscores, and dots
104+ // Replace spaces with dashes, remove other invalid characters
103105 const sanitized = e . target . value
104106 . toLowerCase ( )
105- . replace ( / [ ^ a - z 0 - 9 - _ ] / g, '-' )
106- . replace ( / - + / g, '-' )
107- . replace ( / ^ - | - $ / g, '' ) ;
107+ . replace ( / \s + / g, '-' )
108+ . replace ( / [ ^ a - z 0 - 9 . _ - ] / g, '' )
109+ . replace ( / \. { 2 , } / g, '.' )
110+ . replace ( / - { 2 , } / g, '-' )
111+ . replace ( / ^ [ . - ] + / , '' ) ;
108112 setName ( sanitized ) ;
109113 setError ( null ) ;
110114 } , [ ] ) ;
@@ -119,12 +123,13 @@ export function CreateWorktreeDialog({
119123 if ( ! name ) {
120124 const task = backlogTasks . find ( t => t . id === taskId ) ;
121125 if ( task ) {
122- // Convert task title to valid name
126+ // Convert task title to valid name - allow dots and underscores
123127 const autoName = task . title
124128 . toLowerCase ( )
125- . replace ( / [ ^ a - z 0 - 9 ] + / g, '-' )
126- . replace ( / ^ - | - $ / g, '' )
127- . slice ( 0 , 30 ) ;
129+ . replace ( / \s + / g, '-' )
130+ . replace ( / [ ^ a - z 0 - 9 . _ - ] / g, '' )
131+ . replace ( / ^ [ . - ] + / , '' )
132+ . slice ( 0 , 40 ) ;
128133 setName ( autoName ) ;
129134 }
130135 }
@@ -136,8 +141,9 @@ export function CreateWorktreeDialog({
136141 return ;
137142 }
138143
139- // Validate name format
140- if ( ! / ^ [ a - z 0 - 9 ] [ a - z 0 - 9 _ - ] * [ a - z 0 - 9 ] $ | ^ [ a - z 0 - 9 ] $ / . test ( name ) ) {
144+ // Validate name format - allow letters, numbers, dashes, underscores, and dots
145+ // Must start with letter or number, must not end with dot
146+ if ( ! / ^ [ a - z 0 - 9 ] [ a - z 0 - 9 . _ - ] * $ / . test ( name ) || name . endsWith ( '.' ) ) {
141147 setError ( t ( 'terminal:worktree.nameInvalid' ) ) ;
142148 return ;
143149 }
@@ -185,6 +191,30 @@ export function CreateWorktreeDialog({
185191 onOpenChange ( newOpen ) ;
186192 } ;
187193
194+ // Memoized branch options for the Combobox
195+ const branchOptions : ComboboxOption [ ] = useMemo ( ( ) => {
196+ const options : ComboboxOption [ ] = [
197+ {
198+ value : PROJECT_DEFAULT_BRANCH ,
199+ label : t ( 'terminal:worktree.useProjectDefault' , { branch : projectDefaultBranch || 'main' } ) ,
200+ } ,
201+ ] ;
202+
203+ // Add all branches except the project default (which is already shown as "Use project default")
204+ branches
205+ . filter ( ( b ) => b !== projectDefaultBranch )
206+ . forEach ( ( branch ) => {
207+ options . push ( { value : branch , label : branch } ) ;
208+ } ) ;
209+
210+ // If the project default branch is not in the list, add it
211+ if ( projectDefaultBranch && ! branches . includes ( projectDefaultBranch ) ) {
212+ options . push ( { value : projectDefaultBranch , label : projectDefaultBranch } ) ;
213+ }
214+
215+ return options ;
216+ } , [ branches , projectDefaultBranch , t ] ) ;
217+
188218 return (
189219 < Dialog open = { open } onOpenChange = { handleOpenChange } >
190220 < DialogContent className = "sm:max-w-[425px]" >
@@ -256,39 +286,22 @@ export function CreateWorktreeDialog({
256286 />
257287 </ div >
258288
259- { /* Base Branch Selection */ }
289+ { /* Base Branch Selection - Searchable */ }
260290 < div className = "space-y-2" >
261291 < Label htmlFor = "base-branch" className = "flex items-center gap-2" >
262292 < GitBranch className = "h-4 w-4" />
263293 { t ( 'terminal:worktree.baseBranch' ) }
264294 </ Label >
265- < Select
295+ < Combobox
296+ id = "base-branch"
266297 value = { baseBranch }
267298 onValueChange = { setBaseBranch }
299+ options = { branchOptions }
300+ placeholder = { t ( 'terminal:worktree.selectBaseBranch' ) }
301+ searchPlaceholder = { t ( 'terminal:worktree.searchBranch' ) }
302+ emptyMessage = { t ( 'terminal:worktree.noBranchFound' ) }
268303 disabled = { isCreating || isLoadingBranches }
269- >
270- < SelectTrigger id = "base-branch" >
271- < SelectValue placeholder = { t ( 'terminal:worktree.selectBaseBranch' ) } />
272- </ SelectTrigger >
273- < SelectContent >
274- < SelectItem value = { PROJECT_DEFAULT_BRANCH } >
275- { t ( 'terminal:worktree.useProjectDefault' , { branch : projectDefaultBranch || 'main' } ) }
276- </ SelectItem >
277- { branches
278- . filter ( b => b !== projectDefaultBranch )
279- . slice ( 0 , 15 )
280- . map ( ( branch ) => (
281- < SelectItem key = { branch } value = { branch } >
282- { branch }
283- </ SelectItem >
284- ) ) }
285- { projectDefaultBranch && ! branches . includes ( projectDefaultBranch ) && (
286- < SelectItem value = { projectDefaultBranch } >
287- { projectDefaultBranch }
288- </ SelectItem >
289- ) }
290- </ SelectContent >
291- </ Select >
304+ />
292305 < p className = "text-xs text-muted-foreground" >
293306 { t ( 'terminal:worktree.baseBranchHelp' ) }
294307 </ p >
0 commit comments