diff --git a/crates/agent_ui/src/agent_registry_ui.rs b/crates/agent_ui/src/agent_registry_ui.rs index 8809b3a19e542dc61b389cfd9376c4dc1cf5df1c..82c880bc2380d027444e94f80d9a4f43f1c48dda 100644 --- a/crates/agent_ui/src/agent_registry_ui.rs +++ b/crates/agent_ui/src/agent_registry_ui.rs @@ -1,5 +1,6 @@ use std::ops::Range; +use client::zed_urls; use collections::HashMap; use editor::{Editor, EditorElement, EditorStyle}; use fs::Fs; @@ -525,8 +526,6 @@ impl AgentRegistryPage { impl Render for AgentRegistryPage { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { - let learn_more_url = "https://zed.dev/blog/acp-registry"; - v_flex() .size_full() .bg(cx.theme().colors().editor_background) @@ -548,7 +547,8 @@ impl Render for AgentRegistryPage { .child(Headline::new("ACP Registry").size(HeadlineSize::Large)) .child(Chip::new("Beta")) .hoverable_tooltip({ - let learn_more_url = learn_more_url.to_string(); + let learn_more_url: SharedString = + zed_urls::acp_registry_blog(cx).into(); let tooltip_fn = Tooltip::element(move |_, _| { v_flex() .gap_1() @@ -571,7 +571,9 @@ impl Render for AgentRegistryPage { .icon(IconName::ArrowUpRight) .icon_color(Color::Muted) .icon_size(IconSize::Small) - .on_click(move |_, _, cx| cx.open_url(learn_more_url)), + .on_click(move |_, _, cx| { + cx.open_url(&zed_urls::acp_registry_blog(cx)) + }), ), ) .child( diff --git a/crates/client/src/zed_urls.rs b/crates/client/src/zed_urls.rs index 1f7115e384abd151d180664e44534b432563d3a5..fc96efa9c55db569e042ea28a8ef5d7debc91df5 100644 --- a/crates/client/src/zed_urls.rs +++ b/crates/client/src/zed_urls.rs @@ -68,6 +68,11 @@ pub fn edit_prediction_docs(cx: &App) -> String { ) } +/// Returns the URL to Zed's ACP registry blog post. +pub fn acp_registry_blog(cx: &App) -> String { + format!("{server_url}/blog/acp-registy", server_url = server_url(cx)) +} + pub fn shared_agent_thread_url(session_id: &str) -> String { format!("zed://agent/shared/{}", session_id) } diff --git a/crates/extensions_ui/src/extensions_ui.rs b/crates/extensions_ui/src/extensions_ui.rs index 8f19f953dd83fea76e0068df5205583b78c8b202..0b1d1f75011a64583479e6c6a5b57f7130ff7a8a 100644 --- a/crates/extensions_ui/src/extensions_ui.rs +++ b/crates/extensions_ui/src/extensions_ui.rs @@ -7,7 +7,7 @@ use std::time::Duration; use std::{ops::Range, sync::Arc}; use anyhow::Context as _; -use client::{ExtensionMetadata, ExtensionProvides}; +use client::{ExtensionMetadata, ExtensionProvides, zed_urls}; use collections::{BTreeMap, BTreeSet}; use editor::{Editor, EditorElement, EditorStyle}; use extension_host::{ExtensionManifest, ExtensionOperation, ExtensionStore}; @@ -287,6 +287,19 @@ fn keywords_by_feature() -> &'static BTreeMap> { }) } +fn acp_registry_upsell_keywords() -> &'static [&'static str] { + &[ + "opencode", + "mistral", + "auggie", + "stakpak", + "codebuddy", + "autohand", + "factory droid", + "corust", + ] +} + fn extension_button_id(extension_id: &Arc, operation: ExtensionOperation) -> ElementId { (SharedString::from(extension_id.clone()), operation as usize).into() } @@ -312,6 +325,7 @@ pub struct ExtensionsPage { _subscriptions: [gpui::Subscription; 2], extension_fetch_task: Option>, upsells: BTreeSet, + show_acp_registry_upsell: bool, } impl ExtensionsPage { @@ -373,6 +387,7 @@ impl ExtensionsPage { _subscriptions: subscriptions, query_editor, upsells: BTreeSet::default(), + show_acp_registry_upsell: false, }; this.fetch_extensions( this.search_query(cx), @@ -1375,11 +1390,13 @@ impl ExtensionsPage { fn refresh_feature_upsells(&mut self, cx: &mut Context) { let Some(search) = self.search_query(cx) else { self.upsells.clear(); + self.show_acp_registry_upsell = false; return; }; if let Some(id) = search.strip_prefix("id:") { self.upsells.clear(); + self.show_acp_registry_upsell = false; let upsell = match id.to_lowercase().as_str() { "ruff" => Some(Feature::ExtensionRuff), @@ -1411,6 +1428,60 @@ impl ExtensionsPage { self.upsells.remove(feature); } } + + self.show_acp_registry_upsell = acp_registry_upsell_keywords() + .iter() + .any(|keyword| search_terms.iter().any(|term| keyword.contains(term))); + } + + fn render_acp_registry_upsell(&self, cx: &mut Context) -> impl IntoElement { + let registry_url = zed_urls::acp_registry_blog(cx); + + let view_registry = Button::new("view_registry", "View Registry") + .style(ButtonStyle::Tinted(ui::TintColor::Warning)) + .on_click({ + let registry_url = registry_url.clone(); + move |_, window, cx| { + telemetry::event!( + "ACP Registry Opened from Extensions", + source = "ACP Registry Upsell", + url = registry_url, + ); + window.dispatch_action(Box::new(zed_actions::AcpRegistry), cx) + } + }); + let open_registry_button = Button::new("open_registry", "Learn More") + .icon(IconName::ArrowUpRight) + .icon_size(IconSize::Small) + .icon_position(IconPosition::End) + .icon_color(Color::Muted) + .on_click({ + move |_event, _window, cx| { + telemetry::event!( + "ACP Registry Viewed", + source = "ACP Registry Upsell", + url = registry_url, + ); + cx.open_url(®istry_url) + } + }); + + div().pt_4().px_4().child( + Banner::new() + .severity(Severity::Warning) + .child( + Label::new( + "Agent Server extensions will be deprecated in favor of the ACP registry.", + ) + .mt_0p5(), + ) + .action_slot( + h_flex() + .gap_1() + .child(open_registry_button) + .child(view_registry), + ), + ) } fn render_feature_upsell_banner( @@ -1712,8 +1783,7 @@ impl Render for ExtensionsPage { ) .children(ExtensionProvides::iter().filter_map(|provides| { match provides { - ExtensionProvides::AgentServers - | ExtensionProvides::SlashCommands + ExtensionProvides::SlashCommands | ExtensionProvides::IndexedDocsProviders => return None, _ => {} } @@ -1737,6 +1807,11 @@ impl Render for ExtensionsPage { ) })), ) + .when( + self.provides_filter == Some(ExtensionProvides::AgentServers) + || self.show_acp_registry_upsell, + |this| this.child(self.render_acp_registry_upsell(cx)), + ) .child(self.render_feature_upsells(cx)) .child(v_flex().px_4().size_full().overflow_y_hidden().map(|this| { let mut count = self.filtered_remote_extension_indices.len();