@@ -8,7 +8,8 @@ use std::path::PathBuf;
88use std:: time:: { Duration , Instant } ;
99
1010use anyhow:: Result ;
11- use crossterm:: event:: { self , Event , KeyCode , KeyEvent } ;
11+ use crossterm:: event:: { Event , EventStream , KeyCode , KeyEvent } ;
12+ use futures:: StreamExt ;
1213use ratatui:: Terminal ;
1314use ratatui:: backend:: CrosstermBackend ;
1415use ratatui:: layout:: { Constraint , Direction , Layout , Rect } ;
@@ -169,18 +170,15 @@ impl LoginScreen {
169170 & mut self ,
170171 terminal : & mut Terminal < CrosstermBackend < std:: io:: Stdout > > ,
171172 ) -> Result < LoginResult > {
172- // Flush any pending input events to prevent stale keypresses
173- while event:: poll ( Duration :: from_millis ( 0 ) ) ? {
174- let _ = event:: read ( ) ?;
175- }
173+ // Create an async event stream - this is crucial for non-blocking event handling
174+ // that allows the tokio runtime to process async messages concurrently
175+ let mut event_stream = EventStream :: new ( ) ;
176176
177177 // Small delay to let terminal settle
178- std :: thread :: sleep ( Duration :: from_millis ( 50 ) ) ;
178+ tokio :: time :: sleep ( Duration :: from_millis ( 50 ) ) . await ;
179179
180- // Flush again after delay
181- while event:: poll ( Duration :: from_millis ( 0 ) ) ? {
182- let _ = event:: read ( ) ?;
183- }
180+ // Timer for UI updates (60fps refresh rate)
181+ let mut render_interval = tokio:: time:: interval ( Duration :: from_millis ( 16 ) ) ;
184182
185183 loop {
186184 self . frame_count = self . frame_count . wrapping_add ( 1 ) ;
@@ -195,21 +193,7 @@ impl LoginScreen {
195193 // Render
196194 terminal. draw ( |f| self . render ( f) ) ?;
197195
198- // Check async messages
199- self . process_async_messages ( ) ;
200-
201- // Handle events - only process KeyPress, not KeyRelease or KeyRepeat
202- if event:: poll ( Duration :: from_millis ( 80 ) ) ?
203- && let Event :: Key ( key) = event:: read ( ) ?
204- {
205- // Filter to only handle key press events (not release)
206- if key. kind == crossterm:: event:: KeyEventKind :: Press
207- && let Some ( result) = self . handle_key ( key)
208- {
209- return Ok ( result) ;
210- }
211- }
212-
196+ // Check state before waiting for events
213197 match self . state {
214198 LoginState :: Success => {
215199 return Ok ( LoginResult :: LoggedIn ) ;
@@ -223,6 +207,42 @@ impl LoginScreen {
223207 }
224208 _ => { }
225209 }
210+
211+ // Use tokio::select! to concurrently wait for:
212+ // 1. Terminal events (keyboard input)
213+ // 2. Async messages from background tasks (token polling)
214+ // 3. Render timer tick
215+ // This prevents blocking the async runtime and ensures responsive UI
216+ tokio:: select! {
217+ // Handle keyboard/terminal events
218+ maybe_event = event_stream. next( ) => {
219+ if let Some ( Ok ( Event :: Key ( key) ) ) = maybe_event
220+ && key. kind == crossterm:: event:: KeyEventKind :: Press
221+ && let Some ( result) = self . handle_key( key)
222+ {
223+ return Ok ( result) ;
224+ }
225+ }
226+
227+ // Handle async messages from token polling
228+ msg = async {
229+ if let Some ( ref mut rx) = self . async_rx {
230+ rx. recv( ) . await
231+ } else {
232+ // No receiver, wait forever (will be cancelled by other branches)
233+ std:: future:: pending:: <Option <AsyncMessage >>( ) . await
234+ }
235+ } => {
236+ if let Some ( msg) = msg {
237+ self . handle_async_message( msg) ;
238+ }
239+ }
240+
241+ // Periodic render tick to keep UI responsive
242+ _ = render_interval. tick( ) => {
243+ // Just continue to re-render
244+ }
245+ }
226246 }
227247 }
228248
@@ -499,18 +519,28 @@ impl LoginScreen {
499519 fn handle_waiting_key ( & mut self , key : KeyEvent ) -> Option < LoginResult > {
500520 match key. code {
501521 KeyCode :: Esc => {
522+ // Cancel the current auth flow and go back to method selection
502523 self . state = LoginState :: SelectMethod ;
503524 self . error_message = None ;
525+ self . user_code = None ;
526+ self . verification_uri = None ;
527+ // Drop the receiver to signal the async task it can stop
528+ // (the task will get a send error and terminate)
504529 self . async_rx = None ;
505530 }
506531 KeyCode :: Char ( 'c' ) | KeyCode :: Char ( 'C' ) => {
532+ // Only handle 'c' for copy if NOT Ctrl+C (Ctrl+C is handled in handle_key)
507533 // Copy URL to clipboard using the safe clipboard function
508534 // This properly handles Linux (with wait()) and Windows clipboard behavior
509535 let url = self . get_direct_url ( ) ;
510536 if super :: terminal:: safe_clipboard_copy ( & url) {
511537 self . copied_notification = Some ( Instant :: now ( ) ) ;
512538 }
513539 }
540+ KeyCode :: Char ( 'q' ) | KeyCode :: Char ( 'Q' ) => {
541+ // Also allow 'q' to exit from waiting screen for better UX
542+ return Some ( LoginResult :: Exit ) ;
543+ }
514544 _ => { }
515545 }
516546 None
@@ -534,73 +564,67 @@ impl LoginScreen {
534564 tx
535565 }
536566
537- fn process_async_messages ( & mut self ) {
538- let messages: Vec < AsyncMessage > = if let Some ( ref mut rx) = self . async_rx {
539- let mut msgs = Vec :: new ( ) ;
540- while let Ok ( msg) = rx. try_recv ( ) {
541- msgs. push ( msg) ;
542- }
543- msgs
544- } else {
545- Vec :: new ( )
546- } ;
547-
548- for msg in messages {
549- match msg {
550- AsyncMessage :: DeviceCodeReceived {
551- user_code,
552- device_code,
553- verification_uri : _,
554- } => {
555- let auth_url = format ! ( "{}/device" , AUTH_BASE_URL ) ;
556- self . user_code = Some ( user_code. clone ( ) ) ;
557- self . verification_uri = Some ( auth_url. clone ( ) ) ;
558-
559- // Open browser
560- let link_url = format ! ( "{}?code={}" , auth_url, user_code) ;
561- #[ cfg( target_os = "macos" ) ]
562- {
563- let _ = std:: process:: Command :: new ( "open" )
564- . arg ( & link_url)
565- . stdout ( std:: process:: Stdio :: null ( ) )
566- . stderr ( std:: process:: Stdio :: null ( ) )
567- . spawn ( ) ;
568- }
569- #[ cfg( target_os = "linux" ) ]
570- {
571- let _ = std:: process:: Command :: new ( "xdg-open" )
572- . arg ( & link_url)
573- . stdout ( std:: process:: Stdio :: null ( ) )
574- . stderr ( std:: process:: Stdio :: null ( ) )
575- . spawn ( ) ;
576- }
577- #[ cfg( target_os = "windows" ) ]
578- {
579- let _ = std:: process:: Command :: new ( "cmd" )
580- . args ( [ "/C" , "start" , "" , & link_url] )
581- . stdout ( std:: process:: Stdio :: null ( ) )
582- . stderr ( std:: process:: Stdio :: null ( ) )
583- . spawn ( ) ;
584- }
585-
586- // Start token polling
587- let cortex_home = self . cortex_home . clone ( ) ;
588- let tx = self . create_async_channel ( ) ;
589- tokio:: spawn ( async move {
590- poll_for_token_async ( cortex_home, device_code, tx) . await ;
591- } ) ;
592- }
593- AsyncMessage :: DeviceCodeError ( e) => {
594- self . state = LoginState :: SelectMethod ;
595- self . error_message = Some ( e) ;
567+ fn handle_async_message ( & mut self , msg : AsyncMessage ) {
568+ match msg {
569+ AsyncMessage :: DeviceCodeReceived {
570+ user_code,
571+ device_code,
572+ verification_uri : _,
573+ } => {
574+ tracing:: info!( "Device code received: {}" , user_code) ;
575+ let auth_url = format ! ( "{}/device" , AUTH_BASE_URL ) ;
576+ self . user_code = Some ( user_code. clone ( ) ) ;
577+ self . verification_uri = Some ( auth_url. clone ( ) ) ;
578+
579+ // Open browser
580+ let link_url = format ! ( "{}?code={}" , auth_url, user_code) ;
581+ tracing:: debug!( "Opening browser to: {}" , link_url) ;
582+ #[ cfg( target_os = "macos" ) ]
583+ {
584+ let _ = std:: process:: Command :: new ( "open" )
585+ . arg ( & link_url)
586+ . stdout ( std:: process:: Stdio :: null ( ) )
587+ . stderr ( std:: process:: Stdio :: null ( ) )
588+ . spawn ( ) ;
596589 }
597- AsyncMessage :: TokenReceived => {
598- self . state = LoginState :: Success ;
590+ #[ cfg( target_os = "linux" ) ]
591+ {
592+ let _ = std:: process:: Command :: new ( "xdg-open" )
593+ . arg ( & link_url)
594+ . stdout ( std:: process:: Stdio :: null ( ) )
595+ . stderr ( std:: process:: Stdio :: null ( ) )
596+ . spawn ( ) ;
599597 }
600- AsyncMessage :: TokenError ( e) => {
601- self . state = LoginState :: SelectMethod ;
602- self . error_message = Some ( e) ;
598+ #[ cfg( target_os = "windows" ) ]
599+ {
600+ let _ = std:: process:: Command :: new ( "cmd" )
601+ . args ( [ "/C" , "start" , "" , & link_url] )
602+ . stdout ( std:: process:: Stdio :: null ( ) )
603+ . stderr ( std:: process:: Stdio :: null ( ) )
604+ . spawn ( ) ;
603605 }
606+
607+ // Start token polling - create new channel for this phase
608+ tracing:: debug!( "Starting token polling for device code" ) ;
609+ let cortex_home = self . cortex_home . clone ( ) ;
610+ let tx = self . create_async_channel ( ) ;
611+ tokio:: spawn ( async move {
612+ poll_for_token_async ( cortex_home, device_code, tx) . await ;
613+ } ) ;
614+ }
615+ AsyncMessage :: DeviceCodeError ( e) => {
616+ tracing:: error!( "Device code error: {}" , e) ;
617+ self . state = LoginState :: SelectMethod ;
618+ self . error_message = Some ( e) ;
619+ }
620+ AsyncMessage :: TokenReceived => {
621+ tracing:: info!( "Authentication token received - login successful" ) ;
622+ self . state = LoginState :: Success ;
623+ }
624+ AsyncMessage :: TokenError ( e) => {
625+ tracing:: error!( "Token error: {}" , e) ;
626+ self . state = LoginState :: SelectMethod ;
627+ self . error_message = Some ( e) ;
604628 }
605629 }
606630 }
@@ -688,34 +712,51 @@ async fn poll_for_token_async(
688712 device_code : String ,
689713 tx : mpsc:: Sender < AsyncMessage > ,
690714) {
715+ tracing:: debug!( "Token polling started" ) ;
716+
691717 let client = match cortex_engine:: create_default_client ( ) {
692718 Ok ( c) => c,
693719 Err ( e) => {
720+ tracing:: error!( "Failed to create HTTP client: {}" , e) ;
694721 let _ = tx. send ( AsyncMessage :: TokenError ( e. to_string ( ) ) ) . await ;
695722 return ;
696723 }
697724 } ;
698725
699726 let interval = Duration :: from_secs ( 5 ) ;
700- let max_attempts = 180 ;
727+ let max_attempts = 180 ; // 15 minutes total
701728
702- for _ in 0 ..max_attempts {
729+ for attempt in 0 ..max_attempts {
703730 tokio:: time:: sleep ( interval) . await ;
704731
732+ // Check if the receiver was dropped (user cancelled)
733+ // This is a cheap check that allows us to exit early
734+ if tx. is_closed ( ) {
735+ tracing:: debug!( "Token polling cancelled (receiver dropped)" ) ;
736+ return ;
737+ }
738+
739+ tracing:: trace!( "Polling for token (attempt {}/{})" , attempt + 1 , max_attempts) ;
740+
705741 let response = match client
706742 . post ( format ! ( "{}/auth/device/token" , API_BASE_URL ) )
707743 . json ( & serde_json:: json!( { "device_code" : device_code } ) )
708744 . send ( )
709745 . await
710746 {
711747 Ok ( r) => r,
712- Err ( _) => continue ,
748+ Err ( e) => {
749+ tracing:: debug!( "Token poll request failed: {}" , e) ;
750+ continue ;
751+ }
713752 } ;
714753
715754 let status = response. status ( ) ;
716755 let body = response. text ( ) . await . unwrap_or_default ( ) ;
717756
718757 if status. is_success ( ) {
758+ tracing:: debug!( "Token response received (success)" ) ;
759+
719760 #[ derive( serde:: Deserialize ) ]
720761 struct TokenResponse {
721762 access_token : String ,
@@ -733,7 +774,12 @@ async fn poll_for_token_async(
733774 match save_auth_with_fallback ( & cortex_home, & auth_data) {
734775 Ok ( mode) => {
735776 tracing:: info!( "Auth credentials saved using {:?} storage" , mode) ;
736- let _ = tx. send ( AsyncMessage :: TokenReceived ) . await ;
777+ // Send the success message - this is the critical moment
778+ if let Err ( e) = tx. send ( AsyncMessage :: TokenReceived ) . await {
779+ tracing:: error!( "Failed to send TokenReceived message: {}" , e) ;
780+ } else {
781+ tracing:: debug!( "TokenReceived message sent successfully" ) ;
782+ }
737783 return ;
738784 }
739785 Err ( e) => {
@@ -747,6 +793,8 @@ async fn poll_for_token_async(
747793 return ;
748794 }
749795 }
796+ } else {
797+ tracing:: warn!( "Failed to parse token response" ) ;
750798 }
751799 continue ;
752800 }
@@ -755,24 +803,36 @@ async fn poll_for_token_async(
755803 && let Some ( err) = error. get ( "error" ) . and_then ( |e| e. as_str ( ) )
756804 {
757805 match err {
758- "authorization_pending" | "slow_down" => continue ,
806+ "authorization_pending" => {
807+ tracing:: trace!( "Authorization pending..." ) ;
808+ continue ;
809+ }
810+ "slow_down" => {
811+ tracing:: debug!( "Server requested slow down" ) ;
812+ continue ;
813+ }
759814 "expired_token" => {
815+ tracing:: warn!( "Device code expired" ) ;
760816 let _ = tx
761817 . send ( AsyncMessage :: TokenError ( "Device code expired" . to_string ( ) ) )
762818 . await ;
763819 return ;
764820 }
765821 "access_denied" => {
822+ tracing:: warn!( "Access denied by user" ) ;
766823 let _ = tx
767824 . send ( AsyncMessage :: TokenError ( "Access denied" . to_string ( ) ) )
768825 . await ;
769826 return ;
770827 }
771- _ => { }
828+ _ => {
829+ tracing:: debug!( "Unknown error response: {}" , err) ;
830+ }
772831 }
773832 }
774833 }
775834
835+ tracing:: warn!( "Token polling timed out after {} attempts" , max_attempts) ;
776836 let _ = tx
777837 . send ( AsyncMessage :: TokenError (
778838 "Authentication timed out" . to_string ( ) ,
0 commit comments