From 4cdcb0c15eef921a13ee54fe0df81cb0ee854253 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Mon, 3 Nov 2025 22:22:50 -0300 Subject: [PATCH] agent_ui: Improve display of external agents in configuration view (#41869) This PR makes the agent panel's configuration view use the icon from an external agent that comes directly from the extension, as well as some other clean ups. Release Notes: - N/A --- ...{zed_mcp_custom.svg => zed_src_custom.svg} | 0 ...cp_extension.svg => zed_src_extension.svg} | 0 crates/agent_ui/src/agent_configuration.rs | 428 +++++++++--------- crates/icons/src/icons.rs | 4 +- 4 files changed, 224 insertions(+), 208 deletions(-) rename assets/icons/{zed_mcp_custom.svg => zed_src_custom.svg} (100%) rename assets/icons/{zed_mcp_extension.svg => zed_src_extension.svg} (100%) diff --git a/assets/icons/zed_mcp_custom.svg b/assets/icons/zed_src_custom.svg similarity index 100% rename from assets/icons/zed_mcp_custom.svg rename to assets/icons/zed_src_custom.svg diff --git a/assets/icons/zed_mcp_extension.svg b/assets/icons/zed_src_extension.svg similarity index 100% rename from assets/icons/zed_mcp_extension.svg rename to assets/icons/zed_src_extension.svg diff --git a/crates/agent_ui/src/agent_configuration.rs b/crates/agent_ui/src/agent_configuration.rs index cf154f3e95e25e5058e43f693e14a032ac04826a..4ece220b200ca5262148b41bb55f029780099281 100644 --- a/crates/agent_ui/src/agent_configuration.rs +++ b/crates/agent_ui/src/agent_configuration.rs @@ -154,7 +154,42 @@ pub enum AssistantConfigurationEvent { impl EventEmitter for AgentConfiguration {} +enum AgentIcon { + Name(IconName), + Path(SharedString), +} + impl AgentConfiguration { + fn render_section_title( + &mut self, + title: impl Into, + description: impl Into, + menu: AnyElement, + ) -> impl IntoElement { + h_flex() + .p_4() + .pb_0() + .mb_2p5() + .items_start() + .justify_between() + .child( + v_flex() + .w_full() + .gap_0p5() + .child( + h_flex() + .pr_1() + .w_full() + .gap_2() + .justify_between() + .flex_wrap() + .child(Headline::new(title.into())) + .child(menu), + ) + .child(Label::new(description.into()).color(Color::Muted)), + ) + } + fn render_provider_configuration_block( &mut self, provider: &Arc, @@ -289,7 +324,7 @@ impl AgentConfiguration { "Start New Thread", ) .full_width() - .style(ButtonStyle::Filled) + .style(ButtonStyle::Outlined) .layer(ElevationIndex::ModalSurface) .icon_position(IconPosition::Start) .icon(IconName::Thread) @@ -380,80 +415,47 @@ 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") + .style(ButtonStyle::Outlined) + .icon_position(IconPosition::Start) + .icon(IconName::Plus) + .icon_size(IconSize::Small) + .icon_color(Color::Muted) + .label_size(LabelSize::Small), + ) + .anchor(gpui::Corner::TopRight) + .menu({ + let workspace = self.workspace.clone(); + move |window, cx| { + Some(ContextMenu::build(window, cx, |menu, _window, _cx| { + menu.header("Compatible APIs").entry("OpenAI", None, { + let workspace = workspace.clone(); + move |window, cx| { + workspace + .update(cx, |workspace, cx| { + AddLlmProviderModal::toggle( + LlmCompatibleProvider::OpenAi, + workspace, + window, + cx, + ); + }) + .log_err(); + } + }) + })) + } + }); v_flex() .w_full() - .child( - h_flex() - .p(DynamicSpacing::Base16.rems(cx)) - .pr(DynamicSpacing::Base20.rems(cx)) - .pb_0() - .mb_2p5() - .items_start() - .justify_between() - .child( - v_flex() - .w_full() - .gap_0p5() - .child( - h_flex() - .pr_1() - .w_full() - .gap_2() - .justify_between() - .child(Headline::new("LLM Providers")) - .child( - PopoverMenu::new("add-provider-popover") - .trigger( - Button::new("add-provider", "Add Provider") - .style(ButtonStyle::Filled) - .layer(ElevationIndex::ModalSurface) - .icon_position(IconPosition::Start) - .icon(IconName::Plus) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .label_size(LabelSize::Small), - ) - .anchor(gpui::Corner::TopRight) - .menu({ - let workspace = self.workspace.clone(); - move |window, cx| { - Some(ContextMenu::build( - window, - cx, - |menu, _window, _cx| { - menu.header("Compatible APIs").entry( - "OpenAI", - None, - { - let workspace = - workspace.clone(); - move |window, cx| { - workspace - .update(cx, |workspace, cx| { - AddLlmProviderModal::toggle( - LlmCompatibleProvider::OpenAi, - workspace, - window, - cx, - ); - }) - .log_err(); - } - }, - ) - }, - )) - } - }), - ), - ) - .child( - Label::new("Add at least one provider to use AI-powered features with Zed's native agent.") - .color(Color::Muted), - ), - ), - ) + .child(self.render_section_title( + "LLM Providers", + "Add at least one provider to use AI-powered features with Zed's native agent.", + popover_menu.into_any_element(), + )) .child( div() .w_full() @@ -532,8 +534,7 @@ impl AgentConfiguration { let add_server_popover = PopoverMenu::new("add-server-popover") .trigger( Button::new("add-server", "Add Server") - .style(ButtonStyle::Filled) - .layer(ElevationIndex::ModalSurface) + .style(ButtonStyle::Outlined) .icon_position(IconPosition::Start) .icon(IconName::Plus) .icon_size(IconSize::Small) @@ -566,61 +567,57 @@ impl AgentConfiguration { }); v_flex() - .p(DynamicSpacing::Base16.rems(cx)) - .pr(DynamicSpacing::Base20.rems(cx)) - .gap_2() .border_b_1() .border_color(cx.theme().colors().border) + .child(self.render_section_title( + "Model Context Protocol (MCP) Servers", + "All MCP servers connected directly or via a Zed extension.", + add_server_popover.into_any_element(), + )) .child( - h_flex() + v_flex() + .pl_4() + .pb_4() + .pr_5() .w_full() - .items_start() - .justify_between() .gap_1() - .child( - v_flex() - .gap_0p5() - .child(Headline::new("Model Context Protocol (MCP) Servers")) - .child( - Label::new( - "All MCP servers connected directly or via a Zed extension.", - ) - .color(Color::Muted), - ), - ) - .child(add_server_popover), - ) - .child(v_flex().w_full().gap_1().map(|mut parent| { - if context_server_ids.is_empty() { - parent.child( - h_flex() - .p_4() - .justify_center() - .border_1() - .border_dashed() - .border_color(cx.theme().colors().border.opacity(0.6)) - .rounded_sm() - .child( - Label::new("No MCP servers added yet.") - .color(Color::Muted) - .size(LabelSize::Small), - ), - ) - } else { - for (index, context_server_id) in context_server_ids.into_iter().enumerate() { - if index > 0 { - parent = parent.child( - Divider::horizontal() - .color(DividerColor::BorderFaded) - .into_any_element(), - ); + .map(|mut parent| { + if context_server_ids.is_empty() { + parent.child( + h_flex() + .p_4() + .justify_center() + .border_1() + .border_dashed() + .border_color(cx.theme().colors().border.opacity(0.6)) + .rounded_sm() + .child( + Label::new("No MCP servers added yet.") + .color(Color::Muted) + .size(LabelSize::Small), + ), + ) + } else { + for (index, context_server_id) in + context_server_ids.into_iter().enumerate() + { + if index > 0 { + parent = parent.child( + Divider::horizontal() + .color(DividerColor::BorderFaded) + .into_any_element(), + ); + } + parent = parent.child(self.render_context_server( + context_server_id, + window, + cx, + )); + } + parent } - parent = - parent.child(self.render_context_server(context_server_id, window, cx)); - } - parent - } - })) + }), + ) } fn render_context_server( @@ -665,12 +662,12 @@ impl AgentConfiguration { let (source_icon, source_tooltip) = if is_from_extension { ( - IconName::ZedMcpExtension, + IconName::ZedSrcExtension, "This MCP server was installed from an extension.", ) } else { ( - IconName::ZedMcpCustom, + IconName::ZedSrcCustom, "This custom MCP server was installed directly.", ) }; @@ -952,9 +949,9 @@ impl AgentConfiguration { } fn render_agent_servers_section(&mut self, cx: &mut Context) -> impl IntoElement { - let user_defined_agents = self - .agent_server_store - .read(cx) + let agent_server_store = self.agent_server_store.read(cx); + + let user_defined_agents = agent_server_store .external_agents() .filter(|name| { name.0 != GEMINI_NAME && name.0 != CLAUDE_CODE_NAME && name.0 != CODEX_NAME @@ -965,102 +962,121 @@ impl AgentConfiguration { let user_defined_agents = user_defined_agents .into_iter() .map(|name| { - self.render_agent_server(IconName::Ai, name) + let icon = if let Some(icon_path) = agent_server_store.agent_icon(&name) { + AgentIcon::Path(icon_path) + } else { + AgentIcon::Name(IconName::Ai) + }; + self.render_agent_server(icon, name, true) .into_any_element() }) .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 + }) + .detach_and_log_err(cx); + } + }); + v_flex() .border_b_1() .border_color(cx.theme().colors().border) .child( v_flex() - .p(DynamicSpacing::Base16.rems(cx)) - .pr(DynamicSpacing::Base20.rems(cx)) - .gap_2() + .child(self.render_section_title( + "External Agents", + "All agents connected through the Agent Client Protocol.", + add_agens_button.into_any_element(), + )) .child( v_flex() - .gap_0p5() - .child( - h_flex() - .pr_1() - .w_full() - .gap_2() - .justify_between() - .child(Headline::new("External Agents")) - .child( - Button::new("add-agent", "Add Agent") - .style(ButtonStyle::Filled) - .layer(ElevationIndex::ModalSurface) - .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 - }) - .detach_and_log_err(cx); - } - } - ), - ) - ) - .child( - Label::new( - "All agents connected through the Agent Client Protocol.", - ) - .color(Color::Muted), - ), - ) - .child(self.render_agent_server( - IconName::AiClaude, - "Claude Code", - )) - .child(Divider::horizontal().color(DividerColor::BorderFaded)) - .child(self.render_agent_server( - IconName::AiOpenAi, - "Codex", - )) - .child(Divider::horizontal().color(DividerColor::BorderFaded)) - .child(self.render_agent_server( - IconName::AiGemini, - "Gemini CLI", - )) - .map(|mut parent| { - for agent in user_defined_agents { - parent = parent.child(Divider::horizontal().color(DividerColor::BorderFaded)) - .child(agent); - } - parent - }) + .p_4() + .pt_0() + .gap_2() + .child(self.render_agent_server( + AgentIcon::Name(IconName::AiClaude), + "Claude Code", + false, + )) + .child(Divider::horizontal().color(DividerColor::BorderFaded)) + .child(self.render_agent_server( + AgentIcon::Name(IconName::AiOpenAi), + "Codex", + false, + )) + .child(Divider::horizontal().color(DividerColor::BorderFaded)) + .child(self.render_agent_server( + AgentIcon::Name(IconName::AiGemini), + "Gemini CLI", + false, + )) + .map(|mut parent| { + for agent in user_defined_agents { + parent = parent + .child( + Divider::horizontal().color(DividerColor::BorderFaded), + ) + .child(agent); + } + parent + }), + ), ) } fn render_agent_server( &self, - icon: IconName, + icon: AgentIcon, name: impl Into, + external: bool, ) -> impl IntoElement { - h_flex().gap_1p5().justify_between().child( - h_flex() - .gap_1p5() - .child(Icon::new(icon).size(IconSize::Small).color(Color::Muted)) - .child(Label::new(name.into())) - .child( - Icon::new(IconName::Check) - .size(IconSize::Small) - .color(Color::Success), - ), - ) + let name = name.into(); + let icon = match icon { + AgentIcon::Name(icon_name) => Icon::new(icon_name) + .size(IconSize::Small) + .color(Color::Muted), + AgentIcon::Path(icon_path) => Icon::from_path(icon_path) + .size(IconSize::Small) + .color(Color::Muted), + }; + + let tooltip_id = SharedString::new(format!("agent-source-{}", name)); + let tooltip_message = format!("The {} agent was installed from an extension.", name); + + 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), + ) } } diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index a4da8c6ccdf04f453a368b902af8543625100436..fb45ca1eb5f8334190c11ad811a31128396ba23a 100644 --- a/crates/icons/src/icons.rs +++ b/crates/icons/src/icons.rs @@ -258,8 +258,8 @@ pub enum IconName { ZedAssistant, ZedBurnMode, ZedBurnModeOn, - ZedMcpCustom, - ZedMcpExtension, + ZedSrcCustom, + ZedSrcExtension, ZedPredict, ZedPredictDisabled, ZedPredictDown,