From e29522a1b74a782f84899fae1608b31ebacc80b6 Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Wed, 28 Jan 2026 10:01:53 -0300 Subject: [PATCH] acp: Add upsell banner for built-in agents in registry page (#47853) This PR adds the upsell banner for built-in agents in the ACP registry page, meaning: if you search for Claude Code, Codex, or Gemini, we'll display a banner communicating they're already available in Zed. This may change one day, though, whenever we rely on their registry implementation. I'm also removing the beta chip from the page here. Release Notes: - N/A --- crates/agent_ui/src/agent_registry_ui.rs | 134 ++++++++++++++++++----- 1 file changed, 108 insertions(+), 26 deletions(-) diff --git a/crates/agent_ui/src/agent_registry_ui.rs b/crates/agent_ui/src/agent_registry_ui.rs index 82c880bc2380d027444e94f80d9a4f43f1c48dda..0651ec32bfc3e00504e42d19aed66b7ea33f5bdd 100644 --- a/crates/agent_ui/src/agent_registry_ui.rs +++ b/crates/agent_ui/src/agent_registry_ui.rs @@ -1,4 +1,6 @@ +use std::collections::{BTreeMap, BTreeSet}; use std::ops::Range; +use std::sync::OnceLock; use client::zed_urls; use collections::HashMap; @@ -14,7 +16,7 @@ use project::{AgentRegistryStore, RegistryAgent}; use settings::{Settings, SettingsStore, update_settings_file}; use theme::ThemeSettings; use ui::{ - ButtonLink, ButtonStyle, Chip, ScrollableHandle, ToggleButtonGroup, ToggleButtonGroupSize, + Banner, ButtonStyle, ScrollableHandle, Severity, ToggleButtonGroup, ToggleButtonGroupSize, ToggleButtonGroupStyle, ToggleButtonSimple, Tooltip, WithScrollbar, prelude::*, }; use workspace::{ @@ -41,6 +43,25 @@ enum RegistryInstallStatus { InstalledExtension, } +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +enum BuiltInAgent { + Claude, + Codex, + Gemini, +} + +fn keywords_by_agent_feature() -> &'static BTreeMap> { + static KEYWORDS_BY_FEATURE: OnceLock>> = + OnceLock::new(); + KEYWORDS_BY_FEATURE.get_or_init(|| { + BTreeMap::from_iter([ + (BuiltInAgent::Claude, vec!["claude", "claude code"]), + (BuiltInAgent::Codex, vec!["codex", "codex cli"]), + (BuiltInAgent::Gemini, vec!["gemini", "gemini cli"]), + ]) + }) +} + #[derive(IntoElement)] struct AgentRegistryCard { children: Vec, @@ -86,6 +107,7 @@ pub struct AgentRegistryPage { installed_statuses: HashMap, query_editor: Entity, filter: RegistryFilter, + upsells: BTreeSet, _subscriptions: Vec, } @@ -120,6 +142,7 @@ impl AgentRegistryPage { installed_statuses: HashMap::default(), query_editor, filter: RegistryFilter::All, + upsells: BTreeSet::new(), _subscriptions: subscriptions, }; @@ -179,6 +202,7 @@ impl AgentRegistryPage { fn filter_registry_agents(&mut self, cx: &mut Context) { self.refresh_installed_statuses(cx); + self.refresh_feature_upsells(cx); let search = self.search_query(cx).map(|search| search.to_lowercase()); let filter = self.filter; let installed_statuses = self.installed_statuses.clone(); @@ -242,6 +266,83 @@ impl AgentRegistryPage { } } + fn refresh_feature_upsells(&mut self, cx: &mut Context) { + let Some(search) = self.search_query(cx) else { + self.upsells.clear(); + return; + }; + + let search = search.to_lowercase(); + let search_terms = search + .split_whitespace() + .map(|term| term.trim()) + .collect::>(); + + for (feature, keywords) in keywords_by_agent_feature() { + if keywords + .iter() + .any(|keyword| search_terms.contains(keyword)) + { + self.upsells.insert(*feature); + } else { + self.upsells.remove(feature); + } + } + } + + fn render_feature_upsell_banner( + &self, + label: SharedString, + docs_url: SharedString, + ) -> impl IntoElement { + let docs_url_button = Button::new("open_docs", "View Documentation") + .icon(IconName::ArrowUpRight) + .icon_size(IconSize::Small) + .icon_position(IconPosition::End) + .icon_color(Color::Muted) + .on_click({ + move |_event, _window, cx| { + telemetry::event!( + "Documentation Viewed", + source = "Agent Registry Feature Upsell", + url = docs_url, + ); + cx.open_url(&docs_url) + } + }); + + div().pt_4().px_4().child( + Banner::new() + .severity(Severity::Success) + .child(Label::new(label).mt_0p5()) + .action_slot(docs_url_button), + ) + } + + fn render_feature_upsells(&self) -> impl IntoElement { + let mut container = v_flex(); + + for feature in &self.upsells { + let banner = match feature { + BuiltInAgent::Claude => self.render_feature_upsell_banner( + "Claude Code support is built-in to Zed!".into(), + "https://zed.dev/docs/ai/external-agents#claude-code".into(), + ), + BuiltInAgent::Codex => self.render_feature_upsell_banner( + "Codex CLI support is built-in to Zed!".into(), + "https://zed.dev/docs/ai/external-agents#codex-cli".into(), + ), + BuiltInAgent::Gemini => self.render_feature_upsell_banner( + "Gemini CLI support is built-in to Zed!".into(), + "https://zed.dev/docs/ai/external-agents#gemini-cli".into(), + ), + }; + container = container.child(banner); + } + + container + } + fn render_search(&self, cx: &mut Context) -> Div { let mut key_context = KeyContext::new_with_defaults(); key_context.add("BufferSearchBar"); @@ -540,30 +641,7 @@ impl Render for AgentRegistryPage { .w_full() .gap_1p5() .justify_between() - .child( - h_flex() - .id("title") - .gap_2() - .child(Headline::new("ACP Registry").size(HeadlineSize::Large)) - .child(Chip::new("Beta")) - .hoverable_tooltip({ - let learn_more_url: SharedString = - zed_urls::acp_registry_blog(cx).into(); - let tooltip_fn = Tooltip::element(move |_, _| { - v_flex() - .gap_1() - .child(Label::new( - "The ACP Registry is still in testing phase.", - )) - .child(ButtonLink::new( - "Learn more about it", - learn_more_url.as_str(), - )) - .into_any_element() - }); - move |window, cx| tooltip_fn(window, cx) - }), - ) + .child(Headline::new("ACP Registry").size(HeadlineSize::Large)) .child( Button::new("learn-more", "Learn More") .style(ButtonStyle::Outlined) @@ -627,10 +705,14 @@ impl Render for AgentRegistryPage { ), ), ) + .child(self.render_feature_upsells()) .child(v_flex().px_4().size_full().overflow_y_hidden().map(|this| { let count = self.filtered_registry_indices.len(); - if count == 0 { + let has_upsells = !self.upsells.is_empty(); + if count == 0 && !has_upsells { this.child(self.render_empty_state(cx)).into_any_element() + } else if count == 0 { + this.into_any_element() } else { let scroll_handle = &self.list; this.child(