diff --git a/crates/ui_gpui/src/app/commands.rs b/crates/ui_gpui/src/app/commands.rs index b6dcc7dc..fbbca071 100644 --- a/crates/ui_gpui/src/app/commands.rs +++ b/crates/ui_gpui/src/app/commands.rs @@ -76,7 +76,7 @@ impl Gpui { match service.list_sessions().await { Ok(sessions) => { *gpui.session_lifecycles.lock().unwrap() = lifecycles; - *gpui.chat_sessions.lock().unwrap() = sessions.clone(); + gpui.set_chat_sessions(sessions.clone()); gpui.push_event(UiEvent::UpdateChatList { sessions }); } Err(e) => gpui.display_error(format!("Failed to list sessions: {e:#}")), diff --git a/crates/ui_gpui/src/app/event_loop.rs b/crates/ui_gpui/src/app/event_loop.rs index 86fd51a8..2e975feb 100644 --- a/crates/ui_gpui/src/app/event_loop.rs +++ b/crates/ui_gpui/src/app/event_loop.rs @@ -555,7 +555,7 @@ impl Gpui { sessions.len() ); // Update local cache - *self.chat_sessions.lock().unwrap() = sessions.clone(); + self.set_chat_sessions(sessions.clone()); let _current_session_id = self.current_session_id.lock().unwrap().clone(); // Refresh all windows to trigger re-render with new chat data diff --git a/crates/ui_gpui/src/lib.rs b/crates/ui_gpui/src/lib.rs index fff3c082..1ea9e0b0 100644 --- a/crates/ui_gpui/src/lib.rs +++ b/crates/ui_gpui/src/lib.rs @@ -226,6 +226,9 @@ pub struct Gpui { // Current chat state current_session_id: Arc>>, chat_sessions: Arc>>, + /// Set once the first session listing arrived, so an empty `chat_sessions` + /// can be told apart from one not loaded yet. + chat_sessions_loaded: Arc, /// Visits and settlement per session, mirrored from the core for the /// sidebar. Sessions absent here have the default lifecycle. session_lifecycles: @@ -591,6 +594,7 @@ impl Gpui { current_session_id: Arc::new(Mutex::new(None)), chat_sessions: Arc::new(Mutex::new(Vec::new())), + chat_sessions_loaded: Arc::new(std::sync::atomic::AtomicBool::new(false)), session_lifecycles: Arc::new(Mutex::new(HashMap::new())), current_session_activity_state: Arc::new(Mutex::new(None)), session_stop_requests: Arc::new(Mutex::new(std::collections::HashSet::new())), @@ -889,6 +893,18 @@ impl Gpui { self.chat_sessions.lock().unwrap().clone() } + /// Whether the session listing arrived at least once (it may be empty). + pub fn chat_sessions_loaded(&self) -> bool { + self.chat_sessions_loaded + .load(std::sync::atomic::Ordering::Acquire) + } + + fn set_chat_sessions(&self, sessions: Vec) { + *self.chat_sessions.lock().unwrap() = sessions; + self.chat_sessions_loaded + .store(true, std::sync::atomic::Ordering::Release); + } + pub fn get_session_lifecycles( &self, ) -> HashMap { diff --git a/crates/ui_gpui/src/main_screen/mod.rs b/crates/ui_gpui/src/main_screen/mod.rs index cc090a5d..016506ce 100644 --- a/crates/ui_gpui/src/main_screen/mod.rs +++ b/crates/ui_gpui/src/main_screen/mod.rs @@ -208,6 +208,11 @@ pub struct MainScreen { /// Form for the viewed session's oldest open `ask_question` request. question_prompt: Option>, + /// Whether the sidebar got the first session listing. Without it, an + /// empty listing equals the initial `sessions` and never reaches the + /// sidebar, which then shows no project folders. + sidebar_sessions_synced: bool, + // Sidebar animation left_animator: SidebarAnimator, right_animator: SidebarAnimator, @@ -345,6 +350,7 @@ impl MainScreen { ui_scale: initial_scale, right_sidebar_width: px(initial_sidebar_width), right_sidebar_resizing: false, + sidebar_sessions_synced: false, resize_start_x: 0.0, resize_start_width: 0.0, context_limit_cache: None, @@ -1697,11 +1703,16 @@ impl Render for MainScreen { sidebar.set_persisted_projects(persisted_projects, cx); }); + let sessions_loaded = cx + .try_global::() + .is_some_and(|g| g.chat_sessions_loaded()); // Update project sidebar if needed if self.sessions != sessions || self.session_lifecycles != session_lifecycles || self.current_session_id != current_session_id + || (sessions_loaded && !self.sidebar_sessions_synced) { + self.sidebar_sessions_synced = sessions_loaded; let previous_session_id = self.current_session_id.clone(); self.sessions = sessions.clone(); self.session_lifecycles = session_lifecycles.clone(); diff --git a/crates/ui_gpui/src/main_screen/tests/mod.rs b/crates/ui_gpui/src/main_screen/tests/mod.rs index 5f49e9d5..f1290f7d 100644 --- a/crates/ui_gpui/src/main_screen/tests/mod.rs +++ b/crates/ui_gpui/src/main_screen/tests/mod.rs @@ -4,4 +4,5 @@ mod comments; mod composer; mod end_to_end; +mod sidebar; mod stored_outputs; diff --git a/crates/ui_gpui/src/main_screen/tests/sidebar.rs b/crates/ui_gpui/src/main_screen/tests/sidebar.rs new file mode 100644 index 00000000..fe52f791 --- /dev/null +++ b/crates/ui_gpui/src/main_screen/tests/sidebar.rs @@ -0,0 +1,22 @@ +//! The session sidebar as the main screen feeds it. + +use crate::test_support::MainScreenTest; +use code_assistant_core::ui::UiEvent; +use gpui_kit::TestAppContext; + +#[gpui_kit::test] +fn saved_projects_show_before_the_first_session_exists(cx: &mut TestAppContext) { + let mut test = MainScreenTest::new(cx); + // Replace what the machine's projects.json holds. + *test.gpui.persisted_projects.lock().unwrap() = ["proj".to_string()].into(); + + // A fresh store: the listing arrives, and it is empty. + test.push(UiEvent::UpdateChatList { + sessions: Vec::new(), + }); + + let folders = test.main_screen.read_with(&test.cx, |screen, cx| { + screen.project_sidebar.read(cx).folder_projects() + }); + assert_eq!(folders, vec![Some("proj".to_string())]); +} diff --git a/crates/ui_gpui/src/sidebar/projects_view.rs b/crates/ui_gpui/src/sidebar/projects_view.rs index 494fb050..5d66f702 100644 --- a/crates/ui_gpui/src/sidebar/projects_view.rs +++ b/crates/ui_gpui/src/sidebar/projects_view.rs @@ -161,6 +161,15 @@ impl SessionSidebar { .max() } + /// The projects of the displayed folders, in order (`None`: no project). + #[cfg(test)] + pub(crate) fn folder_projects(&self) -> Vec> { + self.folders + .iter() + .map(|folder| folder.project.clone()) + .collect() + } + pub(super) fn render_folders(&self, cx: &mut Context) -> Vec { if self.folders.is_empty() { return vec![render_hint("No projects yet", cx).into_any_element()];