@@ -22,6 +22,24 @@ const PROVIDERS: { id: SpawnProvider; label: string }[] = [
2222 { id : 'omp' , label : 'Oh My Pi' } ,
2323] ;
2424
25+ // Working directories of successful spawns, most recent first. Typing an
26+ // absolute path once is enough — later sessions pick it from the dropdown.
27+ const RECENT_CWDS_KEY = 'chatmux-recent-spawn-cwds' ;
28+ const RECENT_CWDS_MAX = 5 ;
29+
30+ function readRecentCwds ( ) : string [ ] {
31+ try {
32+ const raw = localStorage . getItem ( RECENT_CWDS_KEY ) ;
33+ const parsed : unknown = raw ? JSON . parse ( raw ) : [ ] ;
34+ return Array . isArray ( parsed )
35+ ? parsed . filter ( ( entry ) : entry is string => typeof entry === 'string' && entry . length > 0 ) . slice ( 0 , RECENT_CWDS_MAX )
36+ : [ ] ;
37+ } catch {
38+ // localStorage unavailable (SSR/tests) or corrupted payload.
39+ return [ ] ;
40+ }
41+ }
42+
2543/**
2644 * Unified new-session form. GJC boots through the control tower; every other
2745 * provider boots its native CLI in tmux through /sessions/external/spawn.
@@ -37,12 +55,27 @@ export default function SidebarNewSession({
3755 const [ open , setOpen ] = useState ( initiallyOpen ) ;
3856 const [ provider , setProvider ] = useState < SpawnProvider > ( 'gjc' ) ;
3957 const [ name , setName ] = useState ( '' ) ;
40- const [ cwd , setCwd ] = useState ( '' ) ;
58+ const [ recentCwds , setRecentCwds ] = useState < string [ ] > ( readRecentCwds ) ;
59+ // The most recent working directory is the best default: repeated spawns
60+ // in the same repo need no path input at all.
61+ const [ cwd , setCwd ] = useState ( ( ) => readRecentCwds ( ) [ 0 ] ?? '' ) ;
4162 const [ status , setStatus ] = useState < SpawnStatus > ( { kind : 'idle' } ) ;
4263
64+ const rememberCwd = ( path : string ) => {
65+ const next = [ path , ...recentCwds . filter ( ( entry ) => entry !== path ) ] . slice ( 0 , RECENT_CWDS_MAX ) ;
66+ setRecentCwds ( next ) ;
67+ try {
68+ localStorage . setItem ( RECENT_CWDS_KEY , JSON . stringify ( next ) ) ;
69+ } catch {
70+ // best-effort persistence
71+ }
72+ } ;
73+
4374 const reset = ( ) => {
4475 setName ( '' ) ;
45- setCwd ( '' ) ;
76+ // Keep the path of least resistance: the next spawn most likely targets
77+ // the same repo, so the field reopens prefilled with the latest cwd.
78+ setCwd ( readRecentCwds ( ) [ 0 ] ?? '' ) ;
4679 setStatus ( { kind : 'idle' } ) ;
4780 } ;
4881
@@ -64,6 +97,7 @@ export default function SidebarNewSession({
6497 detail ?: string ;
6598 } ;
6699 if ( response . ok && data . ok ) {
100+ rememberCwd ( trimmedCwd ) ;
67101 setOpen ( false ) ;
68102 reset ( ) ;
69103 return ;
@@ -82,6 +116,7 @@ export default function SidebarNewSession({
82116 const response = await api . externalCliSessionSpawn ( provider , trimmedName , trimmedCwd ) ;
83117 const body = await response . json ( ) . catch ( ( ) => null ) ;
84118 if ( response . ok && body ?. data ?. ok ) {
119+ rememberCwd ( trimmedCwd ) ;
85120 setOpen ( false ) ;
86121 reset ( ) ;
87122 onCreated ( ) ;
@@ -140,6 +175,8 @@ export default function SidebarNewSession({
140175 onChange = { setCwd }
141176 onSubmit = { ( ) => void spawn ( ) }
142177 placeholder = { t ( 'newSessionForm.workingDirectoryPlaceholder' ) }
178+ quickPicks = { recentCwds }
179+ quickPicksLabel = { t ( 'newSessionForm.recentPaths' ) }
143180 />
144181 { status . kind === 'error' && < p className = "text-[11px] text-red-500" > { status . text } </ p > }
145182 < div className = "flex items-center justify-end gap-2" >
0 commit comments