diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index 8ace684234e90c5203528cae360a28b30798bea3..125dc223796f6d9b7e96bee452bee25a2409adb1 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/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, ) -> 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::>(); - 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::>(); + .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, external: bool, + cx: &mut Context, ) -> 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)) } } diff --git a/crates/client/src/zed_urls.rs b/crates/client/src/zed_urls.rs index 7193c099473c95794796c2fc4d3eaaf2f06eb1ac..957d6c68f773db025b4ee604666f5b3d8101148b 100644 --- a/crates/client/src/zed_urls.rs +++ b/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) + ) +} diff --git a/crates/project/src/agent_server_store.rs b/crates/project/src/agent_server_store.rs index ef8079bd014ecc7b26102aafc931029f9ab1cafa..d3c078ffa101c8c66d1c5ab75fb8b59d7748127a 100644 --- a/crates/project/src/agent_server_store.rs +++ b/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> { + self.external_agents.get_mut(name).and_then(|agent| { + agent + .as_any_mut() + .downcast_ref::() + .map(|ext_agent| ext_agent.extension_id.clone()) + }) + } } fn get_or_npm_install_builtin_agent(