Don't construct an agent panel when `disable_ai` is set (#39689)

Cole Miller created

Follow-up to #39649, possible fix for #39669

This implements an alternate strategy for showing/hiding the agent panel
in response to `disable_ai`. We don't load the panel at all if AI is
disabled at startup, and when the value of `disable_ai` changes, we load
the panel or destroy it as needed.

Release Notes:

- N/A

Change summary

crates/agent_ui/src/agent_panel.rs | 50 +------------------------
crates/workspace/src/workspace.rs  | 13 ++++++
crates/zed/src/zed.rs              | 62 ++++++++++++++++++++++++-------
3 files changed, 64 insertions(+), 61 deletions(-)

Detailed changes

crates/agent_ui/src/agent_panel.rs 🔗

@@ -48,12 +48,12 @@ use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
 use fs::Fs;
 use gpui::{
     Action, AnyElement, App, AsyncWindowContext, Corner, DismissEvent, Entity, EventEmitter,
-    ExternalPaths, FocusHandle, Focusable, KeyContext, Pixels, ReadGlobal as _, Subscription, Task,
-    UpdateGlobal, WeakEntity, prelude::*,
+    ExternalPaths, FocusHandle, Focusable, KeyContext, Pixels, Subscription, Task, UpdateGlobal,
+    WeakEntity, prelude::*,
 };
 use language::LanguageRegistry;
 use language_model::{ConfigurationError, LanguageModelRegistry};
-use project::{DisableAiSettings, Project, ProjectPath, Worktree};
+use project::{Project, ProjectPath, Worktree};
 use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
 use rules_library::{RulesLibrary, open_rules_library};
 use search::{BufferSearchBar, buffer_search};
@@ -520,13 +520,6 @@ impl AgentPanel {
                     )
                 });
 
-                if SettingsStore::global(cx)
-                    .get::<DisableAiSettings>(None)
-                    .disable_ai
-                {
-                    return panel;
-                }
-
                 panel.as_mut(cx).loading = true;
                 if let Some(serialized_panel) = serialized_panel {
                     panel.update(cx, |panel, cx| {
@@ -678,43 +671,6 @@ impl AgentPanel {
             )
         });
 
-        let mut old_disable_ai = false;
-        cx.observe_global_in::<SettingsStore>(window, move |panel, window, cx| {
-            let disable_ai = DisableAiSettings::get_global(cx).disable_ai;
-            if old_disable_ai != disable_ai {
-                let agent_panel_id = cx.entity_id();
-                let agent_panel_visible = panel
-                    .workspace
-                    .update(cx, |workspace, cx| {
-                        let agent_dock_position = panel.position(window, cx);
-                        let agent_dock = workspace.dock_at_position(agent_dock_position);
-                        let agent_panel_focused = agent_dock
-                            .read(cx)
-                            .active_panel()
-                            .is_some_and(|panel| panel.panel_id() == agent_panel_id);
-
-                        let active_panel_visible = agent_dock
-                            .read(cx)
-                            .visible_panel()
-                            .is_some_and(|panel| panel.panel_id() == agent_panel_id);
-
-                        if agent_panel_focused {
-                            cx.dispatch_action(&ToggleFocus);
-                        }
-
-                        active_panel_visible
-                    })
-                    .unwrap_or_default();
-
-                if agent_panel_visible {
-                    cx.emit(PanelEvent::Close);
-                }
-
-                old_disable_ai = disable_ai;
-            }
-        })
-        .detach();
-
         Self {
             active_view,
             workspace,

crates/workspace/src/workspace.rs 🔗

@@ -1748,6 +1748,19 @@ impl Workspace {
         });
     }
 
+    pub fn remove_panel<T: Panel>(
+        &mut self,
+        panel: &Entity<T>,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] {
+            dock.update(cx, |dock, cx| {
+                dock.remove_panel(panel, window, cx);
+            })
+        }
+    }
+
     pub fn status_bar(&self) -> &Entity<StatusBar> {
         &self.status_bar
     }

crates/zed/src/zed.rs 🔗

@@ -46,7 +46,7 @@ use paths::{
     local_debug_file_relative_path, local_settings_file_relative_path,
     local_tasks_file_relative_path,
 };
-use project::{DirectoryLister, ProjectItem};
+use project::{DirectoryLister, DisableAiSettings, ProjectItem};
 use project_panel::ProjectPanel;
 use prompt_store::PromptBuilder;
 use quick_action_bar::QuickActionBar;
@@ -604,21 +604,55 @@ fn initialize_panels(
             workspace.add_panel(debug_panel, window, cx);
         })?;
 
-        let is_assistant2_enabled = !cfg!(test);
-        let agent_panel = if is_assistant2_enabled {
-            let agent_panel =
-                agent_ui::AgentPanel::load(workspace_handle.clone(), prompt_builder, cx.clone())
-                    .await?;
+        fn setup_or_teardown_agent_panel(
+            workspace: &mut Workspace,
+            prompt_builder: Arc<PromptBuilder>,
+            window: &mut Window,
+            cx: &mut Context<Workspace>,
+        ) -> Task<anyhow::Result<()>> {
+            let disable_ai = SettingsStore::global(cx)
+                .get::<DisableAiSettings>(None)
+                .disable_ai
+                || cfg!(test);
+            let existing_panel = workspace.panel::<agent_ui::AgentPanel>(cx);
+            match (disable_ai, existing_panel) {
+                (false, None) => cx.spawn_in(window, async move |workspace, cx| {
+                    let panel =
+                        agent_ui::AgentPanel::load(workspace.clone(), prompt_builder, cx.clone())
+                            .await?;
+                    workspace.update_in(cx, |workspace, window, cx| {
+                        let disable_ai = SettingsStore::global(cx)
+                            .get::<DisableAiSettings>(None)
+                            .disable_ai;
+                        let have_panel = workspace.panel::<agent_ui::AgentPanel>(cx).is_some();
+                        if !disable_ai && !have_panel {
+                            workspace.add_panel(panel, window, cx);
+                        }
+                    })
+                }),
+                (true, Some(existing_panel)) => {
+                    workspace.remove_panel::<agent_ui::AgentPanel>(&existing_panel, window, cx);
+                    Task::ready(Ok(()))
+                }
+                _ => Task::ready(Ok(())),
+            }
+        }
 
-            Some(agent_panel)
-        } else {
-            None
-        };
+        workspace_handle
+            .update_in(cx, |workspace, window, cx| {
+                setup_or_teardown_agent_panel(workspace, prompt_builder.clone(), window, cx)
+            })?
+            .await?;
 
         workspace_handle.update_in(cx, |workspace, window, cx| {
-            if let Some(agent_panel) = agent_panel {
-                workspace.add_panel(agent_panel, window, cx);
-            }
+            cx.observe_global_in::<SettingsStore>(window, {
+                let prompt_builder = prompt_builder.clone();
+                move |workspace, window, cx| {
+                    setup_or_teardown_agent_panel(workspace, prompt_builder.clone(), window, cx)
+                        .detach_and_log_err(cx);
+                }
+            })
+            .detach();
 
             // Register the actions that are shared between `assistant` and `assistant2`.
             //
@@ -626,7 +660,7 @@ fn initialize_panels(
             // functions so that we only register the actions once.
             //
             // Once we ship `assistant2` we can push this back down into `agent::agent_panel::init`.
-            if is_assistant2_enabled {
+            if !cfg!(test) {
                 <dyn AgentPanelDelegate>::set_global(
                     Arc::new(agent_ui::ConcreteAssistantPanelDelegate),
                     cx,