@@ -93,6 +93,30 @@ pub enum IrisTaskResult {
9393 GlobalLogLoaded {
9494 entries : Vec < crate :: studio:: state:: FileLogEntry > ,
9595 } ,
96+ /// Git status loaded (async initialization)
97+ GitStatusLoaded ( Box < GitStatusData > ) ,
98+ /// Companion service initialized (async)
99+ CompanionReady ( Box < CompanionInitData > ) ,
100+ }
101+
102+ /// Data from async git status loading
103+ #[ derive( Debug , Clone ) ]
104+ pub struct GitStatusData {
105+ pub branch : String ,
106+ pub staged_files : Vec < std:: path:: PathBuf > ,
107+ pub modified_files : Vec < std:: path:: PathBuf > ,
108+ pub untracked_files : Vec < std:: path:: PathBuf > ,
109+ pub commits_ahead : usize ,
110+ pub commits_behind : usize ,
111+ pub staged_diff : Option < String > ,
112+ }
113+
114+ /// Data from async companion initialization
115+ pub struct CompanionInitData {
116+ /// The companion service
117+ pub service : crate :: companion:: CompanionService ,
118+ /// Display data for the UI
119+ pub display : super :: state:: CompanionSessionDisplay ,
96120}
97121
98122/// Type of content update triggered by chat
@@ -339,6 +363,9 @@ impl StudioApp {
339363 DataType :: ReleaseNotesCommits => {
340364 self . update_release_notes_data ( from_ref, to_ref) ;
341365 }
366+ DataType :: ExploreFiles => {
367+ self . update_explore_file_tree ( ) ;
368+ }
342369 }
343370 }
344371
@@ -400,9 +427,8 @@ impl StudioApp {
400427 } ;
401428 self . state . git_status = status;
402429
403- // Update file trees for components
430+ // Update file trees for components (explore tree is lazy-loaded on mode switch)
404431 self . update_commit_file_tree ( ) ;
405- self . update_explore_file_tree ( ) ;
406432 self . update_review_file_tree ( ) ;
407433
408434 // Load diffs into diff view
@@ -709,6 +735,138 @@ impl StudioApp {
709735 } ) ;
710736 }
711737
738+ /// Load git status asynchronously (for fast TUI startup)
739+ fn load_git_status_async ( & self ) {
740+ let Some ( repo) = & self . state . repo else {
741+ return ;
742+ } ;
743+
744+ let tx = self . iris_result_tx . clone ( ) ;
745+ let repo_path = repo. repo_path ( ) . clone ( ) ;
746+
747+ tokio:: spawn ( async move {
748+ let result = tokio:: task:: spawn_blocking ( move || {
749+ use crate :: git:: GitRepo ;
750+
751+ // Open repo once and gather all data
752+ let repo = GitRepo :: new ( & repo_path) ?;
753+
754+ let branch = repo. get_current_branch ( ) . unwrap_or_default ( ) ;
755+ let files_info = repo. extract_files_info ( false ) . ok ( ) ;
756+ let unstaged = repo. get_unstaged_files ( ) . ok ( ) ;
757+ let untracked = repo. get_untracked_files ( ) . unwrap_or_default ( ) ;
758+ let ( commits_ahead, commits_behind) = repo. get_ahead_behind ( ) ;
759+ let staged_diff = repo. get_staged_diff_full ( ) . ok ( ) ;
760+
761+ let staged_files: Vec < std:: path:: PathBuf > = files_info
762+ . as_ref ( )
763+ . map ( |f| {
764+ f. staged_files
765+ . iter ( )
766+ . map ( |s| s. path . clone ( ) . into ( ) )
767+ . collect ( )
768+ } )
769+ . unwrap_or_default ( ) ;
770+
771+ let modified_files: Vec < std:: path:: PathBuf > = unstaged
772+ . as_ref ( )
773+ . map ( |f| f. iter ( ) . map ( |s| s. path . clone ( ) . into ( ) ) . collect ( ) )
774+ . unwrap_or_default ( ) ;
775+
776+ let untracked_files: Vec < std:: path:: PathBuf > = untracked
777+ . into_iter ( )
778+ . map ( std:: path:: PathBuf :: from)
779+ . collect ( ) ;
780+
781+ Ok :: < _ , anyhow:: Error > ( GitStatusData {
782+ branch,
783+ staged_files,
784+ modified_files,
785+ untracked_files,
786+ commits_ahead,
787+ commits_behind,
788+ staged_diff,
789+ } )
790+ } )
791+ . await ;
792+
793+ match result {
794+ Ok ( Ok ( data) ) => {
795+ let _ = tx. send ( IrisTaskResult :: GitStatusLoaded ( Box :: new ( data) ) ) ;
796+ }
797+ Ok ( Err ( e) ) => {
798+ tracing:: warn!( "Failed to load git status: {}" , e) ;
799+ }
800+ Err ( e) => {
801+ tracing:: warn!( "Git status task panicked: {}" , e) ;
802+ }
803+ }
804+ } ) ;
805+ }
806+
807+ /// Load companion service asynchronously for fast TUI startup
808+ fn load_companion_async ( & self ) {
809+ let Some ( repo) = & self . state . repo else {
810+ return ;
811+ } ;
812+
813+ let tx = self . iris_result_tx . clone ( ) ;
814+ let repo_path = repo. repo_path ( ) . clone ( ) ;
815+ let branch = repo
816+ . get_current_branch ( )
817+ . unwrap_or_else ( |_| "main" . to_string ( ) ) ;
818+
819+ tokio:: spawn ( async move {
820+ let result = tokio:: task:: spawn_blocking ( move || {
821+ use crate :: companion:: { BranchMemory , CompanionService } ;
822+ use super :: state:: CompanionSessionDisplay ;
823+
824+ // Create companion service (this is the slow part - file watcher setup)
825+ let service = CompanionService :: new ( repo_path, & branch) ?;
826+
827+ // Load or create branch memory
828+ let mut branch_mem = service
829+ . load_branch_memory ( & branch)
830+ . ok ( )
831+ . flatten ( )
832+ . unwrap_or_else ( || BranchMemory :: new ( branch. clone ( ) ) ) ;
833+
834+ // Get welcome message before recording visit
835+ let welcome = branch_mem. welcome_message ( ) ;
836+
837+ // Record this visit
838+ branch_mem. record_visit ( ) ;
839+
840+ // Save updated branch memory
841+ if let Err ( e) = service. save_branch_memory ( & branch_mem) {
842+ tracing:: warn!( "Failed to save branch memory: {}" , e) ;
843+ }
844+
845+ let display = CompanionSessionDisplay {
846+ watcher_active : service. has_watcher ( ) ,
847+ welcome_message : welcome. clone ( ) ,
848+ welcome_shown_at : welcome. map ( |_| std:: time:: Instant :: now ( ) ) ,
849+ ..Default :: default ( )
850+ } ;
851+
852+ Ok :: < _ , anyhow:: Error > ( CompanionInitData { service, display } )
853+ } )
854+ . await ;
855+
856+ match result {
857+ Ok ( Ok ( data) ) => {
858+ let _ = tx. send ( IrisTaskResult :: CompanionReady ( Box :: new ( data) ) ) ;
859+ }
860+ Ok ( Err ( e) ) => {
861+ tracing:: warn!( "Failed to initialize companion: {}" , e) ;
862+ }
863+ Err ( e) => {
864+ tracing:: warn!( "Companion init task panicked: {}" , e) ;
865+ }
866+ }
867+ } ) ;
868+ }
869+
712870 /// Run the TUI application
713871 pub fn run ( & mut self ) -> Result < ExitResult > {
714872 // Install panic hook to ensure terminal is restored on panic
@@ -748,43 +906,14 @@ impl StudioApp {
748906 & mut self ,
749907 terminal : & mut Terminal < CrosstermBackend < Stdout > > ,
750908 ) -> Result < ExitResult > {
751- // Refresh git status on start
752- let _ = self . refresh_git_status ( ) ;
909+ // Start async git status loading for fast TUI startup
910+ self . state . git_status_loading = true ;
911+ self . load_git_status_async ( ) ;
753912
754- // Set initial mode based on repo state (only if no explicit mode was set)
755- let current_mode = if self . explicit_mode_set {
756- self . state . active_mode
757- } else {
758- let suggested_mode = self . state . suggest_initial_mode ( ) ;
759- self . state . switch_mode ( suggested_mode) ;
760- suggested_mode
761- } ;
913+ // Start async companion initialization (file watcher setup is slow)
914+ self . load_companion_async ( ) ;
762915
763- // Auto-generate content based on initial mode
764- match current_mode {
765- Mode :: Commit => {
766- if self . state . git_status . has_staged ( ) {
767- self . auto_generate_commit ( ) ;
768- }
769- }
770- Mode :: PR => {
771- self . update_pr_data ( None , None ) ;
772- self . auto_generate_pr ( ) ;
773- }
774- Mode :: Review => {
775- self . update_review_data ( None , None ) ;
776- self . auto_generate_review ( ) ;
777- }
778- Mode :: Changelog => {
779- self . update_changelog_data ( None , None ) ;
780- self . auto_generate_changelog ( ) ;
781- }
782- Mode :: ReleaseNotes => {
783- self . update_release_notes_data ( None , None ) ;
784- self . auto_generate_release_notes ( ) ;
785- }
786- Mode :: Explore => { }
787- }
916+ // Note: Auto-generation happens in apply_git_status_data() after async load completes
788917
789918 loop {
790919 // Process any pending file log load (deferred from initialization)
@@ -1072,12 +1201,99 @@ impl StudioApp {
10721201 IrisTaskResult :: GlobalLogLoaded { entries } => {
10731202 StudioEvent :: GlobalLogLoaded { entries }
10741203 }
1204+
1205+ IrisTaskResult :: GitStatusLoaded ( data) => {
1206+ // Apply git status data directly (not through reducer)
1207+ self . apply_git_status_data ( * data) ;
1208+ continue ; // Already handled
1209+ }
1210+
1211+ IrisTaskResult :: CompanionReady ( data) => {
1212+ // Apply companion data directly
1213+ self . state . companion = Some ( data. service ) ;
1214+ self . state . companion_display = data. display ;
1215+ self . state . mark_dirty ( ) ;
1216+ tracing:: info!( "Companion service initialized asynchronously" ) ;
1217+ continue ; // Already handled
1218+ }
10751219 } ;
10761220
10771221 self . push_event ( event) ;
10781222 }
10791223 }
10801224
1225+ /// Apply git status data from async loading
1226+ fn apply_git_status_data ( & mut self , data : GitStatusData ) {
1227+ use super :: components:: diff_view:: parse_diff;
1228+
1229+ // Update git status
1230+ self . state . git_status = super :: state:: GitStatus {
1231+ branch : data. branch ,
1232+ staged_count : data. staged_files . len ( ) ,
1233+ staged_files : data. staged_files ,
1234+ modified_count : data. modified_files . len ( ) ,
1235+ modified_files : data. modified_files ,
1236+ untracked_count : data. untracked_files . len ( ) ,
1237+ untracked_files : data. untracked_files ,
1238+ commits_ahead : data. commits_ahead ,
1239+ commits_behind : data. commits_behind ,
1240+ } ;
1241+ self . state . git_status_loading = false ;
1242+
1243+ // Update file trees
1244+ self . update_commit_file_tree ( ) ;
1245+ self . update_review_file_tree ( ) ;
1246+
1247+ // Load diffs from staged diff text
1248+ if let Some ( diff_text) = data. staged_diff {
1249+ let diffs = parse_diff ( & diff_text) ;
1250+ self . state . modes . commit . diff_view . set_diffs ( diffs) ;
1251+ }
1252+
1253+ // Sync initial file selection with diff view
1254+ if let Some ( path) = self . state . modes . commit . file_tree . selected_path ( ) {
1255+ self . state . modes . commit . diff_view . select_file_by_path ( & path) ;
1256+ }
1257+
1258+ // If no explicit mode was set, switch to suggested mode
1259+ if !self . explicit_mode_set {
1260+ let suggested = self . state . suggest_initial_mode ( ) ;
1261+ if suggested != self . state . active_mode {
1262+ self . state . switch_mode ( suggested) ;
1263+ }
1264+ }
1265+
1266+ // Auto-generate based on mode
1267+ match self . state . active_mode {
1268+ Mode :: Commit => {
1269+ if self . state . git_status . has_staged ( ) {
1270+ self . auto_generate_commit ( ) ;
1271+ }
1272+ }
1273+ Mode :: Review => {
1274+ self . update_review_data ( None , None ) ;
1275+ self . auto_generate_review ( ) ;
1276+ }
1277+ Mode :: PR => {
1278+ self . update_pr_data ( None , None ) ;
1279+ self . auto_generate_pr ( ) ;
1280+ }
1281+ Mode :: Changelog => {
1282+ self . update_changelog_data ( None , None ) ;
1283+ self . auto_generate_changelog ( ) ;
1284+ }
1285+ Mode :: ReleaseNotes => {
1286+ self . update_release_notes_data ( None , None ) ;
1287+ self . auto_generate_release_notes ( ) ;
1288+ }
1289+ Mode :: Explore => {
1290+ self . update_explore_file_tree ( ) ;
1291+ }
1292+ }
1293+
1294+ self . state . mark_dirty ( ) ;
1295+ }
1296+
10811297 /// Spawn fire-and-forget status message generation using the fast model
10821298 ///
10831299 /// This spawns an async task that generates witty status messages while
0 commit comments