@@ -105,6 +105,8 @@ pub struct App {
105105 pub input_buffer : String ,
106106 pub session_filter : SessionFilter ,
107107 pub directory_filter : String ,
108+ directory_filter_history : Vec < String > ,
109+ directory_suggestion_cursor : Option < usize > ,
108110 pub detail_scroll : usize ,
109111 pub help_scroll : usize ,
110112 pub should_quit : bool ,
@@ -147,6 +149,8 @@ impl App {
147149 input_buffer : String :: new ( ) ,
148150 session_filter : SessionFilter :: All ,
149151 directory_filter : String :: new ( ) ,
152+ directory_filter_history : Vec :: new ( ) ,
153+ directory_suggestion_cursor : None ,
150154 detail_scroll : 0 ,
151155 help_scroll : 0 ,
152156 should_quit : false ,
@@ -351,11 +355,15 @@ impl App {
351355 pub fn begin_directory_filter ( & mut self ) {
352356 self . mode = Mode :: DirectoryFilter ;
353357 self . input_buffer = self . directory_filter . clone ( ) ;
358+ self . sync_directory_suggestion_cursor ( ) ;
354359 }
355360
356361 pub fn confirm_directory_filter ( & mut self ) {
357- self . directory_filter = self . input_buffer . trim ( ) . to_string ( ) ;
362+ let directory_filter = self . input_buffer . trim ( ) . to_string ( ) ;
363+ self . remember_directory_filter ( & directory_filter) ;
364+ self . directory_filter = directory_filter;
358365 self . input_buffer . clear ( ) ;
366+ self . directory_suggestion_cursor = None ;
359367 self . mode = Mode :: Normal ;
360368 self . apply_session_filters ( ) ;
361369 }
@@ -374,7 +382,8 @@ impl App {
374382 /// Open the prompt to launch a new copilot session.
375383 pub fn begin_new_session ( & mut self ) {
376384 self . mode = Mode :: NewSessionDir ;
377- self . input_buffer = self . default_new_session_dir ( ) . to_string_lossy ( ) . to_string ( ) ;
385+ self . input_buffer = self . default_new_session_dir ( ) ;
386+ self . sync_directory_suggestion_cursor ( ) ;
378387 }
379388
380389 /// Confirm the new session directory prompt and queue a launch.
@@ -392,13 +401,58 @@ impl App {
392401 PathBuf :: from ( & raw )
393402 } ;
394403 self . input_buffer . clear ( ) ;
404+ self . directory_suggestion_cursor = None ;
395405 self . mode = Mode :: Normal ;
396406 self . pending_action = PendingAction :: LaunchNew { dir } ;
397407 }
398408
399409 pub fn cancel_input ( & mut self ) {
400410 self . mode = Mode :: Normal ;
401411 self . input_buffer . clear ( ) ;
412+ self . directory_suggestion_cursor = None ;
413+ }
414+
415+ pub fn select_next_directory_suggestion ( & mut self ) {
416+ self . select_directory_suggestion ( false ) ;
417+ }
418+
419+ pub fn select_previous_directory_suggestion ( & mut self ) {
420+ self . select_directory_suggestion ( true ) ;
421+ }
422+
423+ pub fn sync_directory_suggestion_cursor ( & mut self ) {
424+ let current = self . input_buffer . trim ( ) ;
425+ self . directory_suggestion_cursor = self
426+ . directory_suggestions ( )
427+ . iter ( )
428+ . position ( |suggestion| suggestion == current) ;
429+ }
430+
431+ fn select_directory_suggestion ( & mut self , reverse : bool ) {
432+ let suggestions = self . directory_suggestions ( ) ;
433+ if suggestions. is_empty ( ) {
434+ return ;
435+ }
436+
437+ let current_index = self
438+ . directory_suggestion_cursor
439+ . filter ( |index| * index < suggestions. len ( ) )
440+ . or_else ( || {
441+ let current = self . input_buffer . trim ( ) ;
442+ suggestions
443+ . iter ( )
444+ . position ( |suggestion| suggestion == current)
445+ } ) ;
446+ let next_index = match current_index {
447+ Some ( 0 ) if reverse => suggestions. len ( ) - 1 ,
448+ Some ( index) if reverse => index - 1 ,
449+ Some ( index) => ( index + 1 ) % suggestions. len ( ) ,
450+ None if reverse => suggestions. len ( ) - 1 ,
451+ None => 0 ,
452+ } ;
453+
454+ self . input_buffer = suggestions[ next_index] . clone ( ) ;
455+ self . directory_suggestion_cursor = Some ( next_index) ;
402456 }
403457
404458 /// Queue opening an embedded terminal for the session under the cursor.
@@ -474,11 +528,55 @@ impl App {
474528 self . flat_list . get ( self . cursor ) . copied ( )
475529 }
476530
477- fn default_new_session_dir ( & self ) -> PathBuf {
531+ fn default_new_session_dir ( & self ) -> String {
532+ let directory_filter = self . directory_filter . trim ( ) ;
533+ if !directory_filter. is_empty ( ) {
534+ return directory_filter. to_string ( ) ;
535+ }
536+
478537 self . session_at_cursor ( )
479538 . or ( self . selected_session )
480- . map ( |idx| self . sessions [ idx] . cwd . clone ( ) )
481- . unwrap_or_else ( || self . launch_dir . clone ( ) )
539+ . map ( |idx| self . sessions [ idx] . cwd . to_string_lossy ( ) . to_string ( ) )
540+ . unwrap_or_else ( || self . launch_dir . to_string_lossy ( ) . to_string ( ) )
541+ }
542+
543+ fn remember_directory_filter ( & mut self , directory_filter : & str ) {
544+ let directory_filter = directory_filter. trim ( ) ;
545+ if directory_filter. is_empty ( ) {
546+ return ;
547+ }
548+
549+ self . directory_filter_history
550+ . retain ( |entry| entry != directory_filter) ;
551+ self . directory_filter_history
552+ . insert ( 0 , directory_filter. to_string ( ) ) ;
553+ self . directory_filter_history . truncate ( 20 ) ;
554+ }
555+
556+ pub fn directory_suggestions ( & self ) -> Vec < String > {
557+ let mut suggestions = Vec :: new ( ) ;
558+ push_unique_directory_suggestion ( & mut suggestions, self . directory_filter . trim ( ) ) ;
559+
560+ for entry in & self . directory_filter_history {
561+ push_unique_directory_suggestion ( & mut suggestions, entry) ;
562+ }
563+
564+ let mut local_sessions: Vec < _ > = self
565+ . sessions
566+ . iter ( )
567+ . filter ( |session| session. source == SessionSource :: Local )
568+ . collect ( ) ;
569+ local_sessions. sort_by ( |a, b| b. updated_at . cmp ( & a. updated_at ) ) ;
570+ for session in local_sessions {
571+ push_unique_directory_suggestion ( & mut suggestions, & session. cwd . to_string_lossy ( ) ) ;
572+ }
573+
574+ suggestions
575+ }
576+
577+ pub fn directory_suggestion_cursor ( & self ) -> Option < usize > {
578+ self . directory_suggestion_cursor
579+ . filter ( |index| * index < self . directory_suggestions ( ) . len ( ) )
482580 }
483581
484582 fn take_waiting_notification ( & mut self ) -> bool {
@@ -657,6 +755,13 @@ fn is_remote_session(session: &CopilotSession) -> bool {
657755 session. source == SessionSource :: Remote
658756}
659757
758+ fn push_unique_directory_suggestion ( suggestions : & mut Vec < String > , value : & str ) {
759+ let value = value. trim ( ) ;
760+ if !value. is_empty ( ) && !suggestions. iter ( ) . any ( |suggestion| suggestion == value) {
761+ suggestions. push ( value. to_string ( ) ) ;
762+ }
763+ }
764+
660765#[ cfg( test) ]
661766mod tests {
662767 use super :: * ;
@@ -678,6 +783,8 @@ mod tests {
678783 input_buffer : String :: new ( ) ,
679784 session_filter : SessionFilter :: All ,
680785 directory_filter : String :: new ( ) ,
786+ directory_filter_history : Vec :: new ( ) ,
787+ directory_suggestion_cursor : None ,
681788 detail_scroll : 0 ,
682789 help_scroll : 0 ,
683790 should_quit : false ,
@@ -866,6 +973,93 @@ mod tests {
866973 assert ! ( build_flat_list( & sessions, SessionFilter :: Remote , "/tmp/react/src" ) . is_empty( ) ) ;
867974 }
868975
976+ #[ test]
977+ fn new_session_defaults_to_active_directory_filter ( ) {
978+ let mut app = app_with_sessions ( vec ! [ session_with_details(
979+ "local" ,
980+ SessionSource :: Local ,
981+ "/work/selected" ,
982+ SessionStatus :: Idle ,
983+ None ,
984+ None ,
985+ ) ] ) ;
986+ app. directory_filter = "/work/filtered" . to_string ( ) ;
987+
988+ app. begin_new_session ( ) ;
989+
990+ assert_eq ! ( app. input_buffer, "/work/filtered" ) ;
991+ }
992+
993+ #[ test]
994+ fn directory_suggestions_are_sorted_by_mru ( ) {
995+ let mut app = app_with_sessions ( vec ! [
996+ session_with_details(
997+ "older-local" ,
998+ SessionSource :: Local ,
999+ "/work/older-session" ,
1000+ SessionStatus :: Idle ,
1001+ None ,
1002+ None ,
1003+ ) ,
1004+ session_with_details(
1005+ "recent-local" ,
1006+ SessionSource :: Local ,
1007+ "/work/recent-session" ,
1008+ SessionStatus :: Idle ,
1009+ None ,
1010+ None ,
1011+ ) ,
1012+ session_with_details(
1013+ "remote" ,
1014+ SessionSource :: Remote ,
1015+ "owner/repo" ,
1016+ SessionStatus :: Idle ,
1017+ None ,
1018+ Some ( "owner/repo" ) ,
1019+ ) ,
1020+ ] ) ;
1021+ app. sessions [ 0 ] . updated_at = Utc :: now ( ) - chrono:: Duration :: minutes ( 10 ) ;
1022+ app. sessions [ 1 ] . updated_at = Utc :: now ( ) ;
1023+
1024+ app. input_buffer = "/work/older-filter" . to_string ( ) ;
1025+ app. confirm_directory_filter ( ) ;
1026+ app. input_buffer = "/work/recent-filter" . to_string ( ) ;
1027+ app. confirm_directory_filter ( ) ;
1028+
1029+ assert_eq ! (
1030+ app. directory_suggestions( ) ,
1031+ vec![
1032+ "/work/recent-filter" ,
1033+ "/work/older-filter" ,
1034+ "/work/recent-session" ,
1035+ "/work/older-session"
1036+ ]
1037+ ) ;
1038+ }
1039+
1040+ #[ test]
1041+ fn directory_suggestions_can_be_selected ( ) {
1042+ let mut app = app_with_sessions ( vec ! [ session_with_details(
1043+ "local" ,
1044+ SessionSource :: Local ,
1045+ "/work/session" ,
1046+ SessionStatus :: Idle ,
1047+ None ,
1048+ None ,
1049+ ) ] ) ;
1050+ app. input_buffer = "/work/filtered" . to_string ( ) ;
1051+ app. confirm_directory_filter ( ) ;
1052+ app. input_buffer . clear ( ) ;
1053+
1054+ app. select_next_directory_suggestion ( ) ;
1055+ assert_eq ! ( app. input_buffer, "/work/filtered" ) ;
1056+ assert_eq ! ( app. directory_suggestion_cursor( ) , Some ( 0 ) ) ;
1057+
1058+ app. select_next_directory_suggestion ( ) ;
1059+ assert_eq ! ( app. input_buffer, "/work/session" ) ;
1060+ assert_eq ! ( app. directory_suggestion_cursor( ) , Some ( 1 ) ) ;
1061+ }
1062+
8691063 #[ test]
8701064 fn moving_cursor_to_remote_task_does_not_load_log ( ) {
8711065 let mut app = app_with_sessions ( vec ! [
0 commit comments