agent_ui: Allow to uninstall agent servers from the settings view (#42445)

Danilo Leal created

This PR also adds items within the "Add Agent" menu to:
1. Add more agent servers from extensions, opening up the extensions
page with "Agent Servers" already filtered
2. Go to the agent server + ACP docs to learn more about them

I feel like having them there is a nice way to promote this knowledge
from within the product and have users learn more about them.

<img width="500" height="540" alt="Screenshot 2025-11-11 at 10  46 3@2x"
src="https://github.com/user-attachments/assets/9449df2e-1568-44d8-83ca-87cbb9eefdd2"
/>

Release Notes:

- agent: Enabled uninstalled agent servers from the agent panel's
settings view.

Change summary

crates/agent_ui/src/agent_configuration.rs | 201 ++++++++++++++++++-----
crates/client/src/zed_urls.rs              |   8 
crates/project/src/agent_server_store.rs   |  12 +
3 files changed, 174 insertions(+), 47 deletions(-)

Detailed changes

crates/agent_ui/src/agent_configuration.rs 🔗

@@ -8,6 +8,7 @@ use std::{ops::Range, sync::Arc};
 
 use agent::ContextServerRegistry;
 use anyhow::Result;
+use client::zed_urls;
 use cloud_llm_client::{Plan, PlanV1, PlanV2};
 use collections::HashMap;
 use context_server::ContextServerId;
@@ -26,18 +27,20 @@ use language_model::{
 use language_models::AllLanguageModelSettings;
 use notifications::status_toast::{StatusToast, ToastIcon};
 use project::{
-    agent_server_store::{AgentServerStore, CLAUDE_CODE_NAME, CODEX_NAME, GEMINI_NAME},
+    agent_server_store::{
+        AgentServerStore, CLAUDE_CODE_NAME, CODEX_NAME, ExternalAgentServerName, GEMINI_NAME,
+    },
     context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
 };
 use settings::{Settings, SettingsStore, update_settings_file};
 use ui::{
-    Button, ButtonStyle, Chip, CommonAnimationExt, ContextMenu, Disclosure, Divider, DividerColor,
-    ElevationIndex, IconName, IconPosition, IconSize, Indicator, LabelSize, PopoverMenu, Switch,
-    SwitchColor, Tooltip, WithScrollbar, prelude::*,
+    Button, ButtonStyle, Chip, CommonAnimationExt, ContextMenu, ContextMenuEntry, Disclosure,
+    Divider, DividerColor, ElevationIndex, IconName, IconPosition, IconSize, Indicator, LabelSize,
+    PopoverMenu, Switch, SwitchColor, Tooltip, WithScrollbar, prelude::*,
 };
 use util::ResultExt as _;
 use workspace::{Workspace, create_and_open_local_file};
-use zed_actions::ExtensionCategoryFilter;
+use zed_actions::{ExtensionCategoryFilter, OpenBrowser};
 
 pub(crate) use configure_context_server_modal::ConfigureContextServerModal;
 pub(crate) use configure_context_server_tools_modal::ConfigureContextServerToolsModal;
@@ -415,6 +418,7 @@ impl AgentConfiguration {
         cx: &mut Context<Self>,
     ) -> impl IntoElement {
         let providers = LanguageModelRegistry::read_global(cx).providers();
+
         let popover_menu = PopoverMenu::new("add-provider-popover")
             .trigger(
                 Button::new("add-provider", "Add Provider")
@@ -425,7 +429,6 @@ impl AgentConfiguration {
                     .icon_color(Color::Muted)
                     .label_size(LabelSize::Small),
             )
-            .anchor(gpui::Corner::TopRight)
             .menu({
                 let workspace = self.workspace.clone();
                 move |window, cx| {
@@ -447,6 +450,11 @@ impl AgentConfiguration {
                         })
                     }))
                 }
+            })
+            .anchor(gpui::Corner::TopRight)
+            .offset(gpui::Point {
+                x: px(0.0),
+                y: px(2.0),
             });
 
         v_flex()
@@ -541,7 +549,6 @@ impl AgentConfiguration {
                     .icon_color(Color::Muted)
                     .label_size(LabelSize::Small),
             )
-            .anchor(gpui::Corner::TopRight)
             .menu({
                 move |window, cx| {
                     Some(ContextMenu::build(window, cx, |menu, _window, _cx| {
@@ -564,6 +571,11 @@ impl AgentConfiguration {
                         })
                     }))
                 }
+            })
+            .anchor(gpui::Corner::TopRight)
+            .offset(gpui::Point {
+                x: px(0.0),
+                y: px(2.0),
             });
 
         v_flex()
@@ -943,7 +955,7 @@ impl AgentConfiguration {
             .cloned()
             .collect::<Vec<_>>();
 
-        let user_defined_agents = user_defined_agents
+        let user_defined_agents: Vec<_> = user_defined_agents
             .into_iter()
             .map(|name| {
                 let icon = if let Some(icon_path) = agent_server_store.agent_icon(&name) {
@@ -951,27 +963,93 @@ impl AgentConfiguration {
                 } else {
                     AgentIcon::Name(IconName::Ai)
                 };
-                self.render_agent_server(icon, name, true)
-                    .into_any_element()
+                (name, icon)
             })
-            .collect::<Vec<_>>();
+            .collect();
 
-        let add_agens_button = Button::new("add-agent", "Add Agent")
-            .style(ButtonStyle::Outlined)
-            .icon_position(IconPosition::Start)
-            .icon(IconName::Plus)
-            .icon_size(IconSize::Small)
-            .icon_color(Color::Muted)
-            .label_size(LabelSize::Small)
-            .on_click(move |_, window, cx| {
-                if let Some(workspace) = window.root().flatten() {
-                    let workspace = workspace.downgrade();
-                    window
-                        .spawn(cx, async |cx| {
-                            open_new_agent_servers_entry_in_settings_editor(workspace, cx).await
+        let add_agent_popover = PopoverMenu::new("add-agent-server-popover")
+            .trigger(
+                Button::new("add-agent", "Add Agent")
+                    .style(ButtonStyle::Outlined)
+                    .icon_position(IconPosition::Start)
+                    .icon(IconName::Plus)
+                    .icon_size(IconSize::Small)
+                    .icon_color(Color::Muted)
+                    .label_size(LabelSize::Small),
+            )
+            .menu({
+                move |window, cx| {
+                    Some(ContextMenu::build(window, cx, |menu, _window, _cx| {
+                        menu.entry("Install from Extensions", None, {
+                            |window, cx| {
+                                window.dispatch_action(
+                                    zed_actions::Extensions {
+                                        category_filter: Some(
+                                            ExtensionCategoryFilter::AgentServers,
+                                        ),
+                                        id: None,
+                                    }
+                                    .boxed_clone(),
+                                    cx,
+                                )
+                            }
                         })
-                        .detach_and_log_err(cx);
+                        .entry("Add Custom Agent", None, {
+                            move |window, cx| {
+                                if let Some(workspace) = window.root().flatten() {
+                                    let workspace = workspace.downgrade();
+                                    window
+                                        .spawn(cx, async |cx| {
+                                            open_new_agent_servers_entry_in_settings_editor(
+                                                workspace, cx,
+                                            )
+                                            .await
+                                        })
+                                        .detach_and_log_err(cx);
+                                }
+                            }
+                        })
+                        .separator()
+                        .header("Learn More")
+                        .item(
+                            ContextMenuEntry::new("Agent Servers Docs")
+                                .icon(IconName::ArrowUpRight)
+                                .icon_color(Color::Muted)
+                                .icon_position(IconPosition::End)
+                                .handler({
+                                    move |window, cx| {
+                                        window.dispatch_action(
+                                            Box::new(OpenBrowser {
+                                                url: zed_urls::agent_server_docs(cx),
+                                            }),
+                                            cx,
+                                        );
+                                    }
+                                }),
+                        )
+                        .item(
+                            ContextMenuEntry::new("ACP Docs")
+                                .icon(IconName::ArrowUpRight)
+                                .icon_color(Color::Muted)
+                                .icon_position(IconPosition::End)
+                                .handler({
+                                    move |window, cx| {
+                                        window.dispatch_action(
+                                            Box::new(OpenBrowser {
+                                                url: "https://agentclientprotocol.com/".into(),
+                                            }),
+                                            cx,
+                                        );
+                                    }
+                                }),
+                        )
+                    }))
                 }
+            })
+            .anchor(gpui::Corner::TopRight)
+            .offset(gpui::Point {
+                x: px(0.0),
+                y: px(2.0),
             });
 
         v_flex()
@@ -982,7 +1060,7 @@ impl AgentConfiguration {
                     .child(self.render_section_title(
                         "External Agents",
                         "All agents connected through the Agent Client Protocol.",
-                        add_agens_button.into_any_element(),
+                        add_agent_popover.into_any_element(),
                     ))
                     .child(
                         v_flex()
@@ -993,26 +1071,29 @@ impl AgentConfiguration {
                                 AgentIcon::Name(IconName::AiClaude),
                                 "Claude Code",
                                 false,
+                                cx,
                             ))
                             .child(Divider::horizontal().color(DividerColor::BorderFaded))
                             .child(self.render_agent_server(
                                 AgentIcon::Name(IconName::AiOpenAi),
                                 "Codex CLI",
                                 false,
+                                cx,
                             ))
                             .child(Divider::horizontal().color(DividerColor::BorderFaded))
                             .child(self.render_agent_server(
                                 AgentIcon::Name(IconName::AiGemini),
                                 "Gemini CLI",
                                 false,
+                                cx,
                             ))
                             .map(|mut parent| {
-                                for agent in user_defined_agents {
+                                for (name, icon) in user_defined_agents {
                                     parent = parent
                                         .child(
                                             Divider::horizontal().color(DividerColor::BorderFaded),
                                         )
-                                        .child(agent);
+                                        .child(self.render_agent_server(icon, name, true, cx));
                                 }
                                 parent
                             }),
@@ -1025,6 +1106,7 @@ impl AgentConfiguration {
         icon: AgentIcon,
         name: impl Into<SharedString>,
         external: bool,
+        cx: &mut Context<Self>,
     ) -> impl IntoElement {
         let name = name.into();
         let icon = match icon {
@@ -1039,28 +1121,53 @@ impl AgentConfiguration {
         let tooltip_id = SharedString::new(format!("agent-source-{}", name));
         let tooltip_message = format!("The {} agent was installed from an extension.", name);
 
+        let agent_server_name = ExternalAgentServerName(name.clone());
+
+        let uninstall_btn_id = SharedString::from(format!("uninstall-{}", name));
+        let uninstall_button = IconButton::new(uninstall_btn_id, IconName::Trash)
+            .icon_color(Color::Muted)
+            .icon_size(IconSize::Small)
+            .tooltip(Tooltip::text("Uninstall Agent Extension"))
+            .on_click(cx.listener(move |this, _, _window, cx| {
+                let agent_name = agent_server_name.clone();
+
+                if let Some(ext_id) = this.agent_server_store.update(cx, |store, _cx| {
+                    store.get_extension_id_for_agent(&agent_name)
+                }) {
+                    ExtensionStore::global(cx)
+                        .update(cx, |store, cx| store.uninstall_extension(ext_id, cx))
+                        .detach_and_log_err(cx);
+                }
+            }));
+
         h_flex()
-            .gap_1p5()
-            .child(icon)
-            .child(Label::new(name))
-            .when(external, |this| {
-                this.child(
-                    div()
-                        .id(tooltip_id)
-                        .flex_none()
-                        .tooltip(Tooltip::text(tooltip_message))
-                        .child(
-                            Icon::new(IconName::ZedSrcExtension)
-                                .size(IconSize::Small)
-                                .color(Color::Muted),
-                        ),
-                )
-            })
+            .gap_1()
+            .justify_between()
             .child(
-                Icon::new(IconName::Check)
-                    .color(Color::Success)
-                    .size(IconSize::Small),
+                h_flex()
+                    .gap_1p5()
+                    .child(icon)
+                    .child(Label::new(name))
+                    .when(external, |this| {
+                        this.child(
+                            div()
+                                .id(tooltip_id)
+                                .flex_none()
+                                .tooltip(Tooltip::text(tooltip_message))
+                                .child(
+                                    Icon::new(IconName::ZedSrcExtension)
+                                        .size(IconSize::Small)
+                                        .color(Color::Muted),
+                                ),
+                        )
+                    })
+                    .child(
+                        Icon::new(IconName::Check)
+                            .color(Color::Success)
+                            .size(IconSize::Small),
+                    ),
             )
+            .when(external, |this| this.child(uninstall_button))
     }
 }
 

crates/client/src/zed_urls.rs 🔗

@@ -51,3 +51,11 @@ pub fn external_agents_docs(cx: &App) -> String {
         server_url = server_url(cx)
     )
 }
+
+/// Returns the URL to Zed agent servers documentation.
+pub fn agent_server_docs(cx: &App) -> String {
+    format!(
+        "{server_url}/docs/extensions/agent-servers",
+        server_url = server_url(cx)
+    )
+}

crates/project/src/agent_server_store.rs 🔗

@@ -759,6 +759,18 @@ impl AgentServerStore {
             }
         })
     }
+
+    pub fn get_extension_id_for_agent(
+        &mut self,
+        name: &ExternalAgentServerName,
+    ) -> Option<Arc<str>> {
+        self.external_agents.get_mut(name).and_then(|agent| {
+            agent
+                .as_any_mut()
+                .downcast_ref::<LocalExtensionArchiveAgent>()
+                .map(|ext_agent| ext_agent.extension_id.clone())
+        })
+    }
 }
 
 fn get_or_npm_install_builtin_agent(