@@ -60,10 +60,10 @@ enum Phase {
6060 } ,
6161}
6262
63- /// Whether the event loop exited normally or because `/new ` was requested.
63+ /// Whether the event loop exited normally or because `/pair ` was requested.
6464enum EventLoopExit {
6565 Quit ,
66- NewSession ,
66+ NewSession { name : Option < String > } ,
6767}
6868
6969/// Bitwarden CLI login item structure
@@ -219,6 +219,13 @@ fn check_bw_status() -> BwStatus {
219219
220220type SessionInfo = ( IdentityFingerprint , Option < String > , u64 , u64 ) ;
221221
222+ /// Reload the session list from disk (the client may have updated it).
223+ fn reload_sessions ( ) -> Vec < SessionInfo > {
224+ FileSessionCache :: load_or_create ( "user_client" )
225+ . map ( |cache| cache. list_sessions ( ) )
226+ . unwrap_or_default ( )
227+ }
228+
222229/// Build session info messages for display in the TUI.
223230fn session_info_messages ( sessions : & [ SessionInfo ] , pending_label : Option < & str > ) -> Vec < Message > {
224231 let mut sorted = sessions. to_vec ( ) ;
@@ -233,29 +240,38 @@ fn session_info_messages(sessions: &[SessionInfo], pending_label: Option<&str>)
233240 . add_modifier( Modifier :: BOLD ) ,
234241 ) ] ,
235242 ) ] ;
236- for ( fingerprint, _ , cached_at, last_connected_at) in & sorted {
243+ for ( fingerprint, name , cached_at, last_connected_at) in & sorted {
237244 let short_hex = hex:: encode ( fingerprint. 0 )
238245 . chars ( )
239246 . take ( 12 )
240247 . collect :: < String > ( ) ;
241248 let created = format_relative_time ( * cached_at) ;
242249 let last_used = format_relative_time ( * last_connected_at) ;
243- msgs. push ( Message :: rich (
244- MessageKind :: Info ,
245- vec ! [
246- Span :: raw( " " ) ,
247- Span :: styled(
248- short_hex,
249- Style :: default ( )
250- . fg( Color :: Cyan )
251- . add_modifier( Modifier :: BOLD ) ,
252- ) ,
253- Span :: styled(
254- format!( " created {created}, last used {last_used}" ) ,
255- Style :: default ( ) . fg( Color :: DarkGray ) ,
256- ) ,
257- ] ,
250+ let mut spans = vec ! [ Span :: raw( " " ) ] ;
251+ if let Some ( name) = name {
252+ spans. push ( Span :: styled (
253+ name. clone ( ) ,
254+ Style :: default ( )
255+ . fg ( Color :: Yellow )
256+ . add_modifier ( Modifier :: BOLD ) ,
257+ ) ) ;
258+ spans. push ( Span :: styled (
259+ format ! ( " ({short_hex})" ) ,
260+ Style :: default ( ) . fg ( Color :: DarkGray ) ,
261+ ) ) ;
262+ } else {
263+ spans. push ( Span :: styled (
264+ short_hex,
265+ Style :: default ( )
266+ . fg ( Color :: Cyan )
267+ . add_modifier ( Modifier :: BOLD ) ,
268+ ) ) ;
269+ }
270+ spans. push ( Span :: styled (
271+ format ! ( " created {created}, last used {last_used}" ) ,
272+ Style :: default ( ) . fg ( Color :: DarkGray ) ,
258273 ) ) ;
274+ msgs. push ( Message :: rich ( MessageKind :: Info , spans) ) ;
259275 }
260276 if let Some ( label) = pending_label {
261277 msgs. push ( Message :: new ( MessageKind :: Info , format ! ( " {label}" ) ) ) ;
@@ -266,7 +282,8 @@ fn session_info_messages(sessions: &[SessionInfo], pending_label: Option<&str>)
266282/// Set up the idle-mode footer for the TUI.
267283fn idle_footer ( ) -> Line < ' static > {
268284 Line :: from ( vec ! [
269- Span :: styled( " /new" , Style :: default ( ) . fg( Color :: Cyan ) ) ,
285+ Span :: styled( " /pair" , Style :: default ( ) . fg( Color :: Cyan ) ) ,
286+ Span :: styled( " [name]" , Style :: default ( ) . fg( Color :: DarkGray ) ) ,
270287 Span :: raw( " create session " ) ,
271288 Span :: styled( "/exit" , Style :: default ( ) . fg( Color :: Cyan ) ) ,
272289 Span :: raw( " quit " ) ,
@@ -280,7 +297,7 @@ fn idle_footer() -> Line<'static> {
280297/// in a single `select!` loop. The `client_handle` is aborted on exit.
281298///
282299/// The TUI state (`app`, `term`, `reader`) is owned by the caller so that
283- /// it survives across `/new ` session restarts without flickering.
300+ /// it survives across `/pair ` session restarts without flickering.
284301async fn run_event_loop (
285302 app : & mut App ,
286303 term : & mut ratatui:: DefaultTerminal ,
@@ -296,7 +313,7 @@ async fn run_event_loop(
296313 app. set_mode ( Mode :: TextInput ) ;
297314 app. set_session_panel ( session_info_messages ( sessions, None ) ) ;
298315 app. footer = idle_footer ( ) ;
299- app. commands = & [ "/new " , "/exit" ] ;
316+ app. commands = & [ "/pair [name] " , "/exit" ] ;
300317
301318 let mut tick_interval = tokio:: time:: interval ( std:: time:: Duration :: from_millis ( 150 ) ) ;
302319
@@ -315,8 +332,11 @@ async fn run_event_loop(
315332 if let Some ( action) = app. handle_key( key) {
316333 match ( & phase, & action) {
317334 // Idle commands
318- ( Phase :: Idle , AppAction :: Submit ( s) ) if s == "/new" => {
319- break EventLoopExit :: NewSession ;
335+ ( Phase :: Idle , AppAction :: Submit ( s) ) if s. starts_with( "/pair" ) => {
336+ let name = s. strip_prefix( "/pair " )
337+ . map( |n| n. trim( ) . to_string( ) )
338+ . filter( |n| !n. is_empty( ) ) ;
339+ break EventLoopExit :: NewSession { name } ;
320340 }
321341 ( Phase :: Idle , AppAction :: Submit ( s) ) if s == "/exit" => {
322342 break EventLoopExit :: Quit ;
@@ -337,7 +357,7 @@ async fn run_event_loop(
337357 phase = Phase :: Idle ;
338358 app. set_mode( Mode :: TextInput ) ;
339359 app. footer = idle_footer( ) ;
340- app. commands = & [ "/new " , "/exit" ] ;
360+ app. commands = & [ "/pair [name] " , "/exit" ] ;
341361 }
342362
343363 // Credential approval
@@ -371,7 +391,7 @@ async fn run_event_loop(
371391 }
372392 app. set_mode( Mode :: TextInput ) ;
373393 app. footer = idle_footer( ) ;
374- app. commands = & [ "/new " , "/exit" ] ;
394+ app. commands = & [ "/pair [name] " , "/exit" ] ;
375395 }
376396
377397 ( _, AppAction :: Quit ) => break EventLoopExit :: Quit ,
@@ -462,9 +482,11 @@ async fn run_event_loop(
462482 // Update session panel with pending label
463483 app. set_session_panel( session_info_messages( sessions, Some ( "New session (awaiting connection)" ) ) ) ;
464484 }
465- UserClientEvent :: SessionRefreshed { .. } => {
466- // Known device reconnected — clear pending label
467- app. set_session_panel( session_info_messages( sessions, None ) ) ;
485+ UserClientEvent :: SessionRefreshed { .. }
486+ | UserClientEvent :: FingerprintVerified { .. } => {
487+ // Session store was updated — reload from disk
488+ let fresh = reload_sessions( ) ;
489+ app. set_session_panel( session_info_messages( & fresh, None ) ) ;
468490 }
469491 _ => { }
470492 }
@@ -491,10 +513,11 @@ async fn run_user_client_session(proxy_url: String, psk_mode: bool) -> Result<()
491513 local
492514 . run_until ( async move {
493515 // First iteration: if cached sessions exist, listen on those immediately.
494- // On `/new `, we loop back and start a fresh rendezvous/psk session.
516+ // On `/pair `, we loop back and start a fresh rendezvous/psk session.
495517 let mut force_new_session = false ;
518+ let mut pending_session_name: Option < String > = None ;
496519
497- // Create TUI state once — it survives across `/new ` restarts.
520+ // Create TUI state once — it survives across `/pair ` restarts.
498521 let mut app = App :: new ( ) ;
499522 app. client_label = "User client" ;
500523 let bw_status = check_bw_status ( ) ;
@@ -506,7 +529,8 @@ async fn run_user_client_session(proxy_url: String, psk_mode: bool) -> Result<()
506529 loop {
507530 let identity_provider =
508531 Box :: new ( FileIdentityStorage :: load_or_generate ( "user_client" ) ?) ;
509- let session_store = Box :: new ( FileSessionCache :: load_or_create ( "user_client" ) ?) ;
532+ let session_cache = FileSessionCache :: load_or_create ( "user_client" ) ?;
533+ let session_store = Box :: new ( session_cache) ;
510534 let cached_sessions = session_store. list_sessions ( ) ;
511535
512536 let has_cached = !cached_sessions. is_empty ( ) && !force_new_session;
@@ -521,6 +545,7 @@ async fn run_user_client_session(proxy_url: String, psk_mode: bool) -> Result<()
521545
522546 let sessions = session_store. list_sessions ( ) ;
523547
548+ let session_name = pending_session_name. take ( ) ;
524549 let client_handle = tokio:: task:: spawn_local ( async move {
525550 let mut client = UserClient :: listen (
526551 identity_provider as Box < dyn IdentityProvider > ,
@@ -529,6 +554,10 @@ async fn run_user_client_session(proxy_url: String, psk_mode: bool) -> Result<()
529554 )
530555 . await ?;
531556
557+ if let Some ( name) = session_name {
558+ client. set_pending_session_name ( name) ;
559+ }
560+
532561 if has_cached {
533562 client. listen_cached_only ( event_tx, response_rx) . await
534563 } else if psk_mode {
@@ -549,8 +578,9 @@ async fn run_user_client_session(proxy_url: String, psk_mode: bool) -> Result<()
549578 )
550579 . await ?
551580 {
552- EventLoopExit :: NewSession => {
581+ EventLoopExit :: NewSession { name } => {
553582 force_new_session = true ;
583+ pending_session_name = name;
554584 continue ;
555585 }
556586 EventLoopExit :: Quit => break ,
0 commit comments