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(