@@ -65,8 +65,7 @@ export function MainHeader({
6565} : MainHeaderProps ) {
6666 const [ menuOpen , setMenuOpen ] = useState ( false ) ;
6767 const [ infoOpen , setInfoOpen ] = useState ( false ) ;
68- const [ isCreating , setIsCreating ] = useState ( false ) ;
69- const [ newBranch , setNewBranch ] = useState ( "" ) ;
68+ const [ branchQuery , setBranchQuery ] = useState ( "" ) ;
7069 const [ error , setError ] = useState < string | null > ( null ) ;
7170 const [ copyFeedback , setCopyFeedback ] = useState ( false ) ;
7271 const copyTimeoutRef = useRef < number | null > ( null ) ;
@@ -76,7 +75,50 @@ export function MainHeader({
7675 const renameConfirmRef = useRef < HTMLButtonElement | null > ( null ) ;
7776 const renameOnCancel = worktreeRename ?. onCancel ;
7877
79- const recentBranches = branches . slice ( 0 , 12 ) ;
78+ const recentBranches = branches ;
79+ const trimmedQuery = branchQuery . trim ( ) ;
80+ const lowercaseQuery = trimmedQuery . toLowerCase ( ) ;
81+ const filteredBranches =
82+ trimmedQuery . length > 0
83+ ? recentBranches . filter ( ( branch ) =>
84+ branch . name . toLowerCase ( ) . includes ( lowercaseQuery ) ,
85+ )
86+ : recentBranches . slice ( 0 , 12 ) ;
87+ const exactMatch = trimmedQuery
88+ ? recentBranches . find ( ( branch ) => branch . name === trimmedQuery ) ?? null
89+ : null ;
90+ const canCreate = trimmedQuery . length > 0 && ! exactMatch ;
91+ const branchValidationMessage = ( ( ) => {
92+ if ( trimmedQuery . length === 0 ) {
93+ return null ;
94+ }
95+ if ( trimmedQuery === "." || trimmedQuery === ".." ) {
96+ return "Branch name cannot be '.' or '..'." ;
97+ }
98+ if ( / \s / . test ( trimmedQuery ) ) {
99+ return "Branch name cannot contain spaces." ;
100+ }
101+ if ( trimmedQuery . startsWith ( "/" ) || trimmedQuery . endsWith ( "/" ) ) {
102+ return "Branch name cannot start or end with '/'." ;
103+ }
104+ if ( trimmedQuery . endsWith ( ".lock" ) ) {
105+ return "Branch name cannot end with '.lock'." ;
106+ }
107+ if ( trimmedQuery . includes ( ".." ) ) {
108+ return "Branch name cannot contain '..'." ;
109+ }
110+ if ( trimmedQuery . includes ( "@{" ) ) {
111+ return "Branch name cannot contain '@{'." ;
112+ }
113+ const invalidChars = [ "~" , "^" , ":" , "?" , "*" , "[" , "\\" ] ;
114+ if ( invalidChars . some ( ( char ) => trimmedQuery . includes ( char ) ) ) {
115+ return "Branch name contains invalid characters." ;
116+ }
117+ if ( trimmedQuery . endsWith ( "." ) ) {
118+ return "Branch name cannot end with '.'." ;
119+ }
120+ return null ;
121+ } ) ( ) ;
80122 const resolvedWorktreePath = worktreePath ?? workspace . path ;
81123 const relativeWorktreePath =
82124 parentPath && resolvedWorktreePath . startsWith ( `${ parentPath } /` )
@@ -95,8 +137,7 @@ export function MainHeader({
95137 if ( ! menuContains && ! infoContains ) {
96138 setMenuOpen ( false ) ;
97139 setInfoOpen ( false ) ;
98- setIsCreating ( false ) ;
99- setNewBranch ( "" ) ;
140+ setBranchQuery ( "" ) ;
100141 setError ( null ) ;
101142 }
102143 } ;
@@ -313,55 +354,93 @@ export function MainHeader({
313354 data-tauri-drag-region = "false"
314355 >
315356 < div className = "branch-actions" >
316- { ! isCreating ? (
317- < button
318- type = "button"
319- className = "branch-action"
320- onClick = { ( ) => setIsCreating ( true ) }
321- data-tauri-drag-region = "false"
322- >
323- < span className = "branch-action-icon" > +</ span >
324- Create branch
325- </ button >
326- ) : (
327- < div className = "branch-create" >
328- < input
329- value = { newBranch }
330- onChange = { ( event ) => setNewBranch ( event . target . value ) }
331- placeholder = "new-branch-name"
332- className = "branch-input"
333- autoFocus
334- data-tauri-drag-region = "false"
335- />
336- < button
337- type = "button"
338- className = "branch-create-button"
339- onClick = { async ( ) => {
340- const name = newBranch . trim ( ) ;
341- if ( ! name ) {
342- return ;
357+ < div className = "branch-search" >
358+ < input
359+ value = { branchQuery }
360+ onChange = { ( event ) => {
361+ setBranchQuery ( event . target . value ) ;
362+ setError ( null ) ;
363+ } }
364+ onKeyDown = { async ( event ) => {
365+ if ( event . key !== "Enter" ) {
366+ return ;
367+ }
368+ event . preventDefault ( ) ;
369+ if ( branchValidationMessage ) {
370+ setError ( branchValidationMessage ) ;
371+ return ;
372+ }
373+ if ( canCreate ) {
374+ try {
375+ await onCreateBranch ( trimmedQuery ) ;
376+ setMenuOpen ( false ) ;
377+ setBranchQuery ( "" ) ;
378+ setError ( null ) ;
379+ } catch ( err ) {
380+ setError (
381+ err instanceof Error ? err . message : String ( err ) ,
382+ ) ;
343383 }
384+ return ;
385+ }
386+ if ( exactMatch && exactMatch . name !== branchName ) {
344387 try {
345- await onCreateBranch ( name ) ;
388+ await onCheckoutBranch ( exactMatch . name ) ;
346389 setMenuOpen ( false ) ;
347- setIsCreating ( false ) ;
348- setNewBranch ( "" ) ;
390+ setBranchQuery ( "" ) ;
349391 setError ( null ) ;
350392 } catch ( err ) {
351393 setError (
352394 err instanceof Error ? err . message : String ( err ) ,
353395 ) ;
354396 }
355- } }
356- data-tauri-drag-region = "false"
357- >
358- Create + checkout
359- </ button >
397+ }
398+ } }
399+ placeholder = "Search or create branch"
400+ className = "branch-input"
401+ autoFocus
402+ data-tauri-drag-region = "false"
403+ aria-label = "Search branches"
404+ />
405+ < button
406+ type = "button"
407+ className = "branch-create-button"
408+ disabled = { ! canCreate || Boolean ( branchValidationMessage ) }
409+ onClick = { async ( ) => {
410+ if ( branchValidationMessage ) {
411+ setError ( branchValidationMessage ) ;
412+ return ;
413+ }
414+ if ( ! canCreate ) {
415+ return ;
416+ }
417+ try {
418+ await onCreateBranch ( trimmedQuery ) ;
419+ setMenuOpen ( false ) ;
420+ setBranchQuery ( "" ) ;
421+ setError ( null ) ;
422+ } catch ( err ) {
423+ setError (
424+ err instanceof Error ? err . message : String ( err ) ,
425+ ) ;
426+ }
427+ } }
428+ data-tauri-drag-region = "false"
429+ >
430+ Create
431+ </ button >
432+ </ div >
433+ { branchValidationMessage && (
434+ < div className = "branch-error" > { branchValidationMessage } </ div >
435+ ) }
436+ { canCreate && ! branchValidationMessage && (
437+ < div className = "branch-create-hint" >
438+ Create branch “{ trimmedQuery } ”
360439 </ div >
361440 ) }
362441 </ div >
363442 < div className = "branch-list" role = "none" >
364- { recentBranches . map ( ( branch ) => (
443+ { filteredBranches . map ( ( branch ) => (
365444 < button
366445 key = { branch . name }
367446 type = "button"
@@ -375,8 +454,7 @@ export function MainHeader({
375454 try {
376455 await onCheckoutBranch ( branch . name ) ;
377456 setMenuOpen ( false ) ;
378- setIsCreating ( false ) ;
379- setNewBranch ( "" ) ;
457+ setBranchQuery ( "" ) ;
380458 setError ( null ) ;
381459 } catch ( err ) {
382460 setError (
@@ -390,7 +468,7 @@ export function MainHeader({
390468 { branch . name }
391469 </ button >
392470 ) ) }
393- { recentBranches . length === 0 && (
471+ { filteredBranches . length === 0 && (
394472 < div className = "branch-empty" > No branches found</ div >
395473 ) }
396474 </ div >
0 commit comments