@@ -471,6 +471,18 @@ impl AppRunner {
471471 }
472472
473473 /// Run using direct provider mode (new architecture).
474+ ///
475+ /// ## Performance Optimizations
476+ ///
477+ /// This function is optimized for fast TUI startup by:
478+ /// 1. **Deferring non-critical HTTP requests**: Session validation, model fetching,
479+ /// and user info are fetched in the background after the terminal is initialized
480+ /// 2. **Parallel I/O operations**: Session history loading runs concurrently with
481+ /// other startup tasks using `tokio::spawn`
482+ /// 3. **Non-blocking validation**: Server-side session validation happens
483+ /// asynchronously and doesn't block the TUI from appearing
484+ ///
485+ /// The TUI should appear almost instantly after trust verification and auth check.
474486 async fn run_direct_provider ( self ) -> Result < AppExitInfo > {
475487 // Trust verification before anything else
476488 let workspace = std:: env:: current_dir ( ) ?;
@@ -490,7 +502,7 @@ impl AppRunner {
490502 }
491503 }
492504
493- // Check authentication before starting
505+ // Check authentication before starting (fast local check)
494506 let cortex_home = dirs:: home_dir ( )
495507 . map ( |h| h. join ( ".cortex" ) )
496508 . unwrap_or_else ( || PathBuf :: from ( ".cortex" ) ) ;
@@ -508,6 +520,7 @@ impl AppRunner {
508520 }
509521
510522 // Check if user is authenticated (OAuth/API key login) or has API keys configured
523+ // This is a fast local check - no network calls
511524 let mut auth_status = match load_auth ( & cortex_home, CredentialsStoreMode :: default ( ) ) {
512525 Ok ( Some ( auth) ) if !auth. is_expired ( ) => {
513526 tracing:: info!( "User authenticated via {}" , auth. mode) ;
@@ -544,32 +557,8 @@ impl AppRunner {
544557 }
545558 } ;
546559
547- // If locally authenticated, validate with the server
548- // This catches cases where the token was revoked server-side
549- if auth_status == AuthStatus :: Authenticated {
550- match provider_manager. validate_session ( ) . await {
551- Ok ( true ) => {
552- tracing:: debug!( "Session validated with server" ) ;
553- }
554- Ok ( false ) => {
555- tracing:: warn!(
556- "Server rejected authentication - session may have been revoked"
557- ) ;
558- // Delete the invalidated credentials
559- if let Err ( e) = logout_with_fallback ( & cortex_home) {
560- tracing:: warn!( "Failed to remove invalidated credentials: {}" , e) ;
561- }
562- auth_status = AuthStatus :: Expired ;
563- }
564- Err ( e) => {
565- // Network error - don't block the user, let them try
566- tracing:: warn!( "Could not validate session with server: {}" , e) ;
567- // Continue with local auth status
568- }
569- }
570- }
571-
572560 // If not authenticated, show the login screen TUI
561+ // (We skip server validation here and do it in the background later)
573562 if auth_status != AuthStatus :: Authenticated {
574563 use super :: login_screen:: { LoginResult , LoginScreen } ;
575564
@@ -591,6 +580,7 @@ impl AppRunner {
591580 } else {
592581 tracing:: warn!( "Login succeeded but could not load token from keyring" ) ;
593582 }
583+ auth_status = AuthStatus :: Authenticated ;
594584 }
595585 LoginResult :: ContinueWithApiKey => {
596586 // Show API key setup instructions and exit
@@ -622,26 +612,11 @@ impl AppRunner {
622612 }
623613 }
624614
625- // Pre-fetch models from API for the models picker
626- // Use is_available() which checks auth_token, env var, and keyring
627- if provider_manager. is_available ( ) {
628- tracing:: debug!( "Pre-fetching models from API..." ) ;
629- match provider_manager. fetch_models ( ) . await {
630- Ok ( models) => {
631- tracing:: info!( "Loaded {} models from API" , models. len( ) ) ;
632- }
633- Err ( e) => {
634- tracing:: warn!( "Failed to fetch models from API: {}" , e) ;
635- // Continue with hardcoded fallback
636- }
637- }
638- } else {
639- tracing:: warn!(
640- "Not authenticated - models will not be available. Run 'cortex login' to authenticate."
641- ) ;
642- }
615+ // ====================================================================
616+ // PERFORMANCE OPTIMIZATION: Start TUI immediately, fetch data in background
617+ // ====================================================================
643618
644- // Initialize terminal
619+ // Initialize terminal FIRST to minimize perceived latency
645620 let mut terminal = CortexTerminal :: with_options ( self . terminal_options ) ?;
646621 terminal. set_title ( "Cortex" ) ?;
647622
@@ -664,49 +639,121 @@ impl AppRunner {
664639 CortexSession :: new ( & provider, & model) ?
665640 } ;
666641
667- let session_id = cortex_session. id ( ) . to_string ( ) ;
642+ let _session_id = cortex_session. id ( ) . to_string ( ) ;
668643
669644 // Create app state
670645 let mut app_state = AppState :: new ( )
671646 . with_model ( model. clone ( ) )
672647 . with_provider ( provider. clone ( ) )
673648 . with_terminal_size ( width, height) ;
674649
675- // Fetch user info from API for welcome screen
676- if let Some ( token) = cortex_login:: get_auth_token ( ) {
677- if let Ok ( client) = cortex_engine:: create_default_client ( ) {
678- let resp = client
679- . get ( "https://api.cortex.foundation/auth/me" )
680- . bearer_auth ( & token)
681- . send ( )
682- . await ;
683- if let Ok ( resp) = resp {
684- if resp. status ( ) . is_success ( ) {
685- if let Ok ( json) = resp. json :: < serde_json:: Value > ( ) . await {
686- if let Some ( name) = json. get ( "name" ) . and_then ( |v| v. as_str ( ) ) {
687- app_state. user_name = Some ( name. to_string ( ) ) ;
688- }
689- if let Some ( email) = json. get ( "email" ) . and_then ( |v| v. as_str ( ) ) {
690- app_state. user_email = Some ( email. to_string ( ) ) ;
650+ // ====================================================================
651+ // BACKGROUND TASKS: Spawn non-blocking operations in parallel
652+ // ====================================================================
653+
654+ // 1. Session history loading (file I/O) - spawn in background
655+ let session_history_task =
656+ tokio:: task:: spawn_blocking ( || CortexSession :: list_recent ( 50 ) . ok ( ) ) ;
657+
658+ // 2. Fetch user info from API (HTTP request) - spawn in background
659+ let user_info_task = {
660+ let token = cortex_login:: get_auth_token ( ) ;
661+ tokio:: spawn ( async move {
662+ if let Some ( token) = token {
663+ if let Ok ( client) = cortex_engine:: create_default_client ( ) {
664+ if let Ok ( resp) = client
665+ . get ( "https://api.cortex.foundation/auth/me" )
666+ . bearer_auth ( & token)
667+ . timeout ( std:: time:: Duration :: from_secs ( 5 ) )
668+ . send ( )
669+ . await
670+ {
671+ if resp. status ( ) . is_success ( ) {
672+ if let Ok ( json) = resp. json :: < serde_json:: Value > ( ) . await {
673+ return Some ( json) ;
674+ }
691675 }
692- if let Some ( orgs) = json. get ( "organizations" ) . and_then ( |v| v. as_array ( ) )
676+ }
677+ }
678+ }
679+ None
680+ } )
681+ } ;
682+
683+ // 3. Models prefetch and session validation - spawn in background
684+ // We use a channel to receive results and update provider_manager later
685+ let models_and_validation_task = {
686+ let api_url = provider_manager. api_url ( ) . to_string ( ) ;
687+ let token = cortex_login:: get_auth_token ( ) ;
688+ let cortex_home_clone = cortex_home. clone ( ) ;
689+ tokio:: spawn ( async move {
690+ let mut validation_failed = false ;
691+ let mut models: Option < Vec < cortex_engine:: client:: CortexModel > > = None ;
692+
693+ if let Some ( token) = token {
694+ // Create a client with timeout for faster failure on network issues
695+ if let Ok ( client) = cortex_engine:: create_client_builder ( )
696+ . connect_timeout ( std:: time:: Duration :: from_secs ( 3 ) )
697+ . timeout ( std:: time:: Duration :: from_secs ( 10 ) )
698+ . build ( )
699+ {
700+ // Session validation (lightweight)
701+ tracing:: debug!( "Background: validating session with server..." ) ;
702+ if let Ok ( resp) = client
703+ . get ( format ! ( "{}/v1/models" , api_url) )
704+ . header ( "Authorization" , format ! ( "Bearer {}" , token) )
705+ . send ( )
706+ . await
707+ {
708+ let status = resp. status ( ) ;
709+ if status == reqwest:: StatusCode :: UNAUTHORIZED
710+ || status == reqwest:: StatusCode :: FORBIDDEN
693711 {
694- if let Some ( first_org) = orgs. first ( ) {
695- if let Some ( org_name) =
696- first_org. get ( "org_name" ) . and_then ( |v| v. as_str ( ) )
712+ tracing:: warn!(
713+ "Background: session validation failed ({})" ,
714+ status
715+ ) ;
716+ // Delete invalidated credentials
717+ if let Err ( e) = logout_with_fallback ( & cortex_home_clone) {
718+ tracing:: warn!(
719+ "Failed to remove invalidated credentials: {}" ,
720+ e
721+ ) ;
722+ }
723+ validation_failed = true ;
724+ } else if status. is_success ( ) {
725+ // Parse models from the same response to avoid another request
726+ if let Ok ( json) = resp. json :: < serde_json:: Value > ( ) . await {
727+ if let Some ( data) = json. get ( "data" ) . and_then ( |d| d. as_array ( ) )
697728 {
698- app_state. org_name = Some ( org_name. to_string ( ) ) ;
729+ let parsed: Vec < cortex_engine:: client:: CortexModel > = data
730+ . iter ( )
731+ . filter_map ( |m| serde_json:: from_value ( m. clone ( ) ) . ok ( ) )
732+ . collect ( ) ;
733+ if !parsed. is_empty ( ) {
734+ tracing:: info!(
735+ "Background: loaded {} models from API" ,
736+ parsed. len( )
737+ ) ;
738+ models = Some ( parsed) ;
739+ }
699740 }
700741 }
701742 }
702743 }
703744 }
704745 }
705- }
706- }
707746
708- // Load session history from Cortex storage
709- if let Ok ( sessions) = CortexSession :: list_recent ( 50 ) {
747+ ( validation_failed, models)
748+ } )
749+ } ;
750+
751+ // ====================================================================
752+ // Collect background task results (with timeout to not block forever)
753+ // ====================================================================
754+
755+ // Wait for session history (file I/O should be fast)
756+ if let Ok ( Some ( sessions) ) = session_history_task. await {
710757 use crate :: app:: SessionSummary ;
711758 for session in sessions {
712759 if let Ok ( session_uuid) = uuid:: Uuid :: parse_str ( & session. id ) {
@@ -722,6 +769,48 @@ impl AppRunner {
722769 ) ;
723770 }
724771
772+ // Wait for user info (with short timeout - don't block TUI)
773+ if let Ok ( Some ( json) ) =
774+ tokio:: time:: timeout ( std:: time:: Duration :: from_millis ( 500 ) , user_info_task)
775+ . await
776+ . unwrap_or ( Ok ( None ) )
777+ {
778+ if let Some ( name) = json. get ( "name" ) . and_then ( |v| v. as_str ( ) ) {
779+ app_state. user_name = Some ( name. to_string ( ) ) ;
780+ }
781+ if let Some ( email) = json. get ( "email" ) . and_then ( |v| v. as_str ( ) ) {
782+ app_state. user_email = Some ( email. to_string ( ) ) ;
783+ }
784+ if let Some ( orgs) = json. get ( "organizations" ) . and_then ( |v| v. as_array ( ) ) {
785+ if let Some ( first_org) = orgs. first ( ) {
786+ if let Some ( org_name) = first_org. get ( "org_name" ) . and_then ( |v| v. as_str ( ) ) {
787+ app_state. org_name = Some ( org_name. to_string ( ) ) ;
788+ }
789+ }
790+ }
791+ }
792+
793+ // Check validation result (with short timeout - don't block TUI)
794+ // We'll handle models update after event loop is created
795+ let validation_result = tokio:: time:: timeout (
796+ std:: time:: Duration :: from_millis ( 500 ) ,
797+ models_and_validation_task,
798+ )
799+ . await ;
800+
801+ // If validation failed in background, update auth status
802+ // (This would show a toast in the TUI asking user to re-login)
803+ if let Ok ( Ok ( ( true , _) ) ) = & validation_result {
804+ tracing:: warn!( "Session was invalidated by server - user should re-login" ) ;
805+ // The credentials are already deleted in the background task
806+ // We continue with the TUI but the user will get auth errors on API calls
807+ }
808+
809+ // Extract and apply models if we got them from background task
810+ if let Ok ( Ok ( ( _, Some ( models) ) ) ) = validation_result {
811+ provider_manager. set_cached_models ( models) ;
812+ }
813+
725814 // Create unified tool executor for Task and Batch tools
726815 // This requires an API key for the subagent's model client
727816 let unified_executor = {
0 commit comments