Fix a bug where switching the disable AI flag would cause a panic (#45050) (cherry-pick to preview) (#45140)

zed-zippy[bot] and Mikayla Maki created

Cherry-pick of #45050 to preview

----
Also quiet some noisy logs

Release Notes:

- N/A

Co-authored-by: Mikayla Maki <mikayla@zed.dev>

Change summary

crates/agent/src/history_store.rs  | 13 +++++--------
crates/agent_ui/Cargo.toml         |  2 +-
crates/agent_ui_v2/Cargo.toml      |  7 +++++++
crates/search/src/buffer_search.rs |  5 ++++-
crates/workspace/src/dock.rs       | 14 +++++---------
crates/workspace/src/workspace.rs  | 20 ++++++++++++++++----
crates/zed/Cargo.toml              |  4 ++++
crates/zed/src/zed.rs              | 25 +++++++++++++++++++++++--
8 files changed, 65 insertions(+), 25 deletions(-)

Detailed changes

crates/agent/src/history_store.rs 🔗

@@ -216,14 +216,10 @@ impl HistoryStore {
     }
 
     pub fn reload(&self, cx: &mut Context<Self>) {
-        let database_future = ThreadsDatabase::connect(cx);
+        let database_connection = ThreadsDatabase::connect(cx);
         cx.spawn(async move |this, cx| {
-            let threads = database_future
-                .await
-                .map_err(|err| anyhow!(err))?
-                .list_threads()
-                .await?;
-
+            let database = database_connection.await;
+            let threads = database.map_err(|err| anyhow!(err))?.list_threads().await?;
             this.update(cx, |this, cx| {
                 if this.recently_opened_entries.len() < MAX_RECENTLY_OPENED_ENTRIES {
                     for thread in threads
@@ -344,7 +340,8 @@ impl HistoryStore {
     fn load_recently_opened_entries(cx: &AsyncApp) -> Task<Result<VecDeque<HistoryEntryId>>> {
         cx.background_spawn(async move {
             if cfg!(any(feature = "test-support", test)) {
-                anyhow::bail!("history store does not persist in tests");
+                log::warn!("history store does not persist in tests");
+                return Ok(VecDeque::new());
             }
             let json = KEY_VALUE_STORE
                 .read_kvp(RECENTLY_OPENED_THREADS_KEY)?

crates/agent_ui/Cargo.toml 🔗

@@ -13,7 +13,7 @@ path = "src/agent_ui.rs"
 doctest = false
 
 [features]
-test-support = ["assistant_text_thread/test-support", "eval_utils", "gpui/test-support", "language/test-support", "reqwest_client", "workspace/test-support"]
+test-support = ["assistant_text_thread/test-support", "eval_utils", "gpui/test-support", "language/test-support", "reqwest_client", "workspace/test-support", "agent/test-support"]
 unit-eval = []
 
 [dependencies]

crates/agent_ui_v2/Cargo.toml 🔗

@@ -12,6 +12,10 @@ workspace = true
 path = "src/agent_ui_v2.rs"
 doctest = false
 
+[features]
+test-support = ["agent/test-support"]
+
+
 [dependencies]
 agent.workspace = true
 agent_servers.workspace = true
@@ -38,3 +42,6 @@ time_format.workspace = true
 ui.workspace = true
 util.workspace = true
 workspace.workspace = true
+
+[dev-dependencies]
+agent = { workspace = true, features = ["test-support"] }

crates/search/src/buffer_search.rs 🔗

@@ -7,7 +7,6 @@ use crate::{
     search_bar::{ActionButtonState, input_base_styles, render_action_button, render_text_input},
 };
 use any_vec::AnyVec;
-use anyhow::Context as _;
 use collections::HashMap;
 use editor::{
     DisplayPoint, Editor, EditorSettings, MultiBufferOffset,
@@ -634,15 +633,19 @@ impl BufferSearchBar {
                 .read(cx)
                 .as_singleton()
                 .expect("query editor should be backed by a singleton buffer");
+
             query_buffer
                 .read(cx)
                 .set_language_registry(languages.clone());
 
             cx.spawn(async move |buffer_search_bar, cx| {
+                use anyhow::Context as _;
+
                 let regex_language = languages
                     .language_for_name("regex")
                     .await
                     .context("loading regex language")?;
+
                 buffer_search_bar
                     .update(cx, |buffer_search_bar, cx| {
                         buffer_search_bar.regex_language = Some(regex_language);

crates/workspace/src/dock.rs 🔗

@@ -1,5 +1,4 @@
 use crate::persistence::model::DockData;
-use crate::utility_pane::utility_slot_for_dock_position;
 use crate::{DraggedDock, Event, ModalLayer, Pane};
 use crate::{Workspace, status_bar::StatusItemView};
 use anyhow::Context as _;
@@ -705,7 +704,7 @@ impl Dock {
         panel: &Entity<T>,
         window: &mut Window,
         cx: &mut Context<Self>,
-    ) {
+    ) -> bool {
         if let Some(panel_ix) = self
             .panel_entries
             .iter()
@@ -724,15 +723,12 @@ impl Dock {
                 }
             }
 
-            let slot = utility_slot_for_dock_position(self.position);
-            if let Some(workspace) = self.workspace.upgrade() {
-                workspace.update(cx, |workspace, cx| {
-                    workspace.clear_utility_pane_if_provider(slot, Entity::entity_id(panel), cx);
-                });
-            }
-
             self.panel_entries.remove(panel_ix);
             cx.notify();
+
+            true
+        } else {
+            false
         }
     }
 

crates/workspace/src/workspace.rs 🔗

@@ -130,7 +130,9 @@ pub use workspace_settings::{
 use zed_actions::{Spawn, feedback::FileBugReport};
 
 use crate::{
-    item::ItemBufferKind, notifications::NotificationId, utility_pane::UTILITY_PANE_MIN_WIDTH,
+    item::ItemBufferKind,
+    notifications::NotificationId,
+    utility_pane::{UTILITY_PANE_MIN_WIDTH, utility_slot_for_dock_position},
 };
 use crate::{
     persistence::{
@@ -974,6 +976,7 @@ impl AppState {
 
     #[cfg(any(test, feature = "test-support"))]
     pub fn test(cx: &mut App) -> Arc<Self> {
+        use fs::Fs;
         use node_runtime::NodeRuntime;
         use session::Session;
         use settings::SettingsStore;
@@ -984,6 +987,7 @@ impl AppState {
         }
 
         let fs = fs::FakeFs::new(cx.background_executor().clone());
+        <dyn Fs>::set_global(fs.clone(), cx);
         let languages = Arc::new(LanguageRegistry::test(cx.background_executor().clone()));
         let clock = Arc::new(clock::FakeSystemClock::new());
         let http_client = http_client::FakeHttpClient::with_404_response();
@@ -1789,10 +1793,18 @@ impl Workspace {
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
+        let mut found_in_dock = None;
         for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
-            dock.update(cx, |dock, cx| {
-                dock.remove_panel(panel, window, cx);
-            })
+            let found = dock.update(cx, |dock, cx| dock.remove_panel(panel, window, cx));
+
+            if found {
+                found_in_dock = Some(dock.clone());
+            }
+        }
+        if let Some(found_in_dock) = found_in_dock {
+            let position = found_in_dock.read(cx).position();
+            let slot = utility_slot_for_dock_position(position);
+            self.clear_utility_pane_if_provider(slot, Entity::entity_id(panel), cx);
         }
     }
 

crates/zed/Cargo.toml 🔗

@@ -195,6 +195,10 @@ terminal_view = { workspace = true, features = ["test-support"] }
 tree-sitter-md.workspace = true
 tree-sitter-rust.workspace = true
 workspace = { workspace = true, features = ["test-support"] }
+agent_ui = { workspace = true, features = ["test-support"] }
+agent_ui_v2 = { workspace = true, features = ["test-support"] }
+search = { workspace = true, features = ["test-support"] }
+
 
 [package.metadata.bundle-dev]
 icon = ["resources/app-icon-dev@2x.png", "resources/app-icon-dev.png"]

crates/zed/src/zed.rs 🔗

@@ -705,7 +705,6 @@ fn setup_or_teardown_ai_panel<P: Panel>(
         .disable_ai
         || cfg!(test);
     let existing_panel = workspace.panel::<P>(cx);
-
     match (disable_ai, existing_panel) {
         (false, None) => cx.spawn_in(window, async move |workspace, cx| {
             let panel = load_panel(workspace.clone(), cx.clone()).await?;
@@ -2311,7 +2310,7 @@ mod tests {
     use project::{Project, ProjectPath};
     use semver::Version;
     use serde_json::json;
-    use settings::{SettingsStore, watch_config_file};
+    use settings::{SaturatingBool, SettingsStore, watch_config_file};
     use std::{
         path::{Path, PathBuf},
         time::Duration,
@@ -5155,6 +5154,28 @@ mod tests {
         );
     }
 
+    #[gpui::test]
+    async fn test_disable_ai_crash(cx: &mut gpui::TestAppContext) {
+        let app_state = init_test(cx);
+        cx.update(init);
+        let project = Project::test(app_state.fs.clone(), [], cx).await;
+        let _window = cx.add_window(|window, cx| Workspace::test_new(project, window, cx));
+
+        cx.run_until_parked();
+
+        cx.update(|cx| {
+            SettingsStore::update_global(cx, |settings_store, cx| {
+                settings_store.update_user_settings(cx, |settings| {
+                    settings.disable_ai = Some(SaturatingBool(true));
+                });
+            });
+        });
+
+        cx.run_until_parked();
+
+        // If this panics, the test has failed
+    }
+
     #[gpui::test]
     async fn test_prefer_focused_window(cx: &mut gpui::TestAppContext) {
         let app_state = init_test(cx);