agent_registry_ui.rs

  1use std::collections::{BTreeMap, BTreeSet};
  2use std::ops::Range;
  3use std::sync::OnceLock;
  4
  5use client::zed_urls;
  6use collections::HashMap;
  7use editor::{Editor, EditorElement, EditorStyle};
  8use fs::Fs;
  9use gpui::{
 10    AnyElement, App, Context, Entity, EventEmitter, Focusable, KeyContext, ParentElement, Render,
 11    RenderOnce, SharedString, Styled, TextStyle, UniformListScrollHandle, Window, point,
 12    uniform_list,
 13};
 14use project::agent_server_store::{AllAgentServersSettings, CustomAgentServerSettings};
 15use project::{AgentRegistryStore, RegistryAgent};
 16use settings::{Settings, SettingsStore, update_settings_file};
 17use theme::ThemeSettings;
 18use ui::{
 19    Banner, ButtonStyle, ScrollableHandle, Severity, ToggleButtonGroup, ToggleButtonGroupSize,
 20    ToggleButtonGroupStyle, ToggleButtonSimple, Tooltip, WithScrollbar, prelude::*,
 21};
 22use workspace::{
 23    Workspace,
 24    item::{Item, ItemEvent},
 25};
 26
 27/// Registry IDs for built-in agents that Zed already provides first-class support for.
 28/// These are filtered out of the ACP Agent Registry UI to avoid showing duplicates.
 29const BUILT_IN_REGISTRY_IDS: [&str; 4] = ["claude-acp", "claude-code-acp", "codex-acp", "gemini"];
 30
 31#[derive(Clone, Copy, Debug, PartialEq, Eq)]
 32enum RegistryFilter {
 33    All,
 34    Installed,
 35    NotInstalled,
 36}
 37
 38#[derive(Clone, Copy, Debug, PartialEq, Eq)]
 39enum RegistryInstallStatus {
 40    NotInstalled,
 41    InstalledRegistry,
 42    InstalledCustom,
 43    InstalledExtension,
 44}
 45
 46#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
 47enum BuiltInAgent {
 48    Claude,
 49    Codex,
 50    Gemini,
 51}
 52
 53fn keywords_by_agent_feature() -> &'static BTreeMap<BuiltInAgent, Vec<&'static str>> {
 54    static KEYWORDS_BY_FEATURE: OnceLock<BTreeMap<BuiltInAgent, Vec<&'static str>>> =
 55        OnceLock::new();
 56    KEYWORDS_BY_FEATURE.get_or_init(|| {
 57        BTreeMap::from_iter([
 58            (
 59                BuiltInAgent::Claude,
 60                vec!["claude", "claude code", "claude agent"],
 61            ),
 62            (BuiltInAgent::Codex, vec!["codex", "codex cli"]),
 63            (BuiltInAgent::Gemini, vec!["gemini", "gemini cli"]),
 64        ])
 65    })
 66}
 67
 68#[derive(IntoElement)]
 69struct AgentRegistryCard {
 70    children: Vec<AnyElement>,
 71}
 72
 73impl AgentRegistryCard {
 74    fn new() -> Self {
 75        Self {
 76            children: Vec::new(),
 77        }
 78    }
 79}
 80
 81impl ParentElement for AgentRegistryCard {
 82    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
 83        self.children.extend(elements)
 84    }
 85}
 86
 87impl RenderOnce for AgentRegistryCard {
 88    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
 89        div().w_full().child(
 90            v_flex()
 91                .p_3()
 92                .mt_4()
 93                .w_full()
 94                .min_h(rems_from_px(86.))
 95                .gap_2()
 96                .bg(cx.theme().colors().elevated_surface_background.opacity(0.5))
 97                .border_1()
 98                .border_color(cx.theme().colors().border_variant)
 99                .rounded_md()
100                .children(self.children),
101        )
102    }
103}
104
105pub struct AgentRegistryPage {
106    registry_store: Entity<AgentRegistryStore>,
107    list: UniformListScrollHandle,
108    registry_agents: Vec<RegistryAgent>,
109    filtered_registry_indices: Vec<usize>,
110    installed_statuses: HashMap<String, RegistryInstallStatus>,
111    query_editor: Entity<Editor>,
112    filter: RegistryFilter,
113    upsells: BTreeSet<BuiltInAgent>,
114    _subscriptions: Vec<gpui::Subscription>,
115}
116
117impl AgentRegistryPage {
118    pub fn new(
119        _workspace: &Workspace,
120        window: &mut Window,
121        cx: &mut Context<Workspace>,
122    ) -> Entity<Self> {
123        cx.new(|cx| {
124            let registry_store = AgentRegistryStore::global(cx);
125            let query_editor = cx.new(|cx| {
126                let mut input = Editor::single_line(window, cx);
127                input.set_placeholder_text("Search agents...", window, cx);
128                input
129            });
130            cx.subscribe(&query_editor, Self::on_query_change).detach();
131
132            let mut subscriptions = Vec::new();
133            subscriptions.push(cx.observe(&registry_store, |this, _, cx| {
134                this.reload_registry_agents(cx);
135            }));
136            subscriptions.push(cx.observe_global::<SettingsStore>(|this, cx| {
137                this.filter_registry_agents(cx);
138            }));
139
140            let mut this = Self {
141                registry_store,
142                list: UniformListScrollHandle::new(),
143                registry_agents: Vec::new(),
144                filtered_registry_indices: Vec::new(),
145                installed_statuses: HashMap::default(),
146                query_editor,
147                filter: RegistryFilter::All,
148                upsells: BTreeSet::new(),
149                _subscriptions: subscriptions,
150            };
151
152            this.reload_registry_agents(cx);
153            this.registry_store
154                .update(cx, |store, cx| store.refresh(cx));
155
156            this
157        })
158    }
159
160    fn reload_registry_agents(&mut self, cx: &mut Context<Self>) {
161        self.registry_agents = self.registry_store.read(cx).agents().to_vec();
162        self.registry_agents.sort_by(|left, right| {
163            left.name()
164                .as_ref()
165                .cmp(right.name().as_ref())
166                .then_with(|| left.id().as_ref().cmp(right.id().as_ref()))
167        });
168        self.filter_registry_agents(cx);
169    }
170
171    fn refresh_installed_statuses(&mut self, cx: &mut Context<Self>) {
172        let settings = cx
173            .global::<SettingsStore>()
174            .get::<AllAgentServersSettings>(None);
175        self.installed_statuses.clear();
176        for (id, settings) in settings.iter() {
177            let status = match settings {
178                CustomAgentServerSettings::Registry { .. } => {
179                    RegistryInstallStatus::InstalledRegistry
180                }
181                CustomAgentServerSettings::Custom { .. } => RegistryInstallStatus::InstalledCustom,
182                CustomAgentServerSettings::Extension { .. } => {
183                    RegistryInstallStatus::InstalledExtension
184                }
185            };
186            self.installed_statuses.insert(id.clone(), status);
187        }
188    }
189
190    fn install_status(&self, id: &str) -> RegistryInstallStatus {
191        self.installed_statuses
192            .get(id)
193            .copied()
194            .unwrap_or(RegistryInstallStatus::NotInstalled)
195    }
196
197    fn search_query(&self, cx: &mut App) -> Option<String> {
198        let search = self.query_editor.read(cx).text(cx);
199        if search.trim().is_empty() {
200            None
201        } else {
202            Some(search)
203        }
204    }
205
206    fn filter_registry_agents(&mut self, cx: &mut Context<Self>) {
207        self.refresh_installed_statuses(cx);
208        self.refresh_feature_upsells(cx);
209        let search = self.search_query(cx).map(|search| search.to_lowercase());
210        let filter = self.filter;
211        let installed_statuses = self.installed_statuses.clone();
212
213        let filtered_indices = self
214            .registry_agents
215            .iter()
216            .enumerate()
217            .filter(|(_, agent)| {
218                // Filter out built-in agents since they already appear in the main
219                // agent configuration UI and don't need to be installed from the registry.
220                if BUILT_IN_REGISTRY_IDS.contains(&agent.id().as_ref()) {
221                    return false;
222                }
223
224                let matches_search = search.as_ref().is_none_or(|query| {
225                    let query = query.as_str();
226                    agent.id().as_ref().to_lowercase().contains(query)
227                        || agent.name().as_ref().to_lowercase().contains(query)
228                        || agent.description().as_ref().to_lowercase().contains(query)
229                });
230
231                let install_status = installed_statuses
232                    .get(agent.id().as_ref())
233                    .copied()
234                    .unwrap_or(RegistryInstallStatus::NotInstalled);
235                let matches_filter = match filter {
236                    RegistryFilter::All => true,
237                    RegistryFilter::Installed => {
238                        install_status != RegistryInstallStatus::NotInstalled
239                    }
240                    RegistryFilter::NotInstalled => {
241                        install_status == RegistryInstallStatus::NotInstalled
242                    }
243                };
244
245                matches_search && matches_filter
246            })
247            .map(|(index, _)| index)
248            .collect();
249
250        self.filtered_registry_indices = filtered_indices;
251
252        cx.notify();
253    }
254
255    fn scroll_to_top(&mut self, cx: &mut Context<Self>) {
256        self.list.set_offset(point(px(0.), px(0.)));
257        cx.notify();
258    }
259
260    fn on_query_change(
261        &mut self,
262        _: Entity<Editor>,
263        event: &editor::EditorEvent,
264        cx: &mut Context<Self>,
265    ) {
266        if let editor::EditorEvent::Edited { .. } = event {
267            self.filter_registry_agents(cx);
268            self.scroll_to_top(cx);
269        }
270    }
271
272    fn refresh_feature_upsells(&mut self, cx: &mut Context<Self>) {
273        let Some(search) = self.search_query(cx) else {
274            self.upsells.clear();
275            return;
276        };
277
278        let search = search.to_lowercase();
279        let search_terms = search
280            .split_whitespace()
281            .map(|term| term.trim())
282            .collect::<Vec<_>>();
283
284        for (feature, keywords) in keywords_by_agent_feature() {
285            if keywords
286                .iter()
287                .any(|keyword| search_terms.contains(keyword))
288            {
289                self.upsells.insert(*feature);
290            } else {
291                self.upsells.remove(feature);
292            }
293        }
294    }
295
296    fn render_feature_upsell_banner(
297        &self,
298        label: SharedString,
299        docs_url: SharedString,
300    ) -> impl IntoElement {
301        let docs_url_button = Button::new("open_docs", "View Documentation")
302            .icon(IconName::ArrowUpRight)
303            .icon_size(IconSize::Small)
304            .icon_position(IconPosition::End)
305            .icon_color(Color::Muted)
306            .on_click({
307                move |_event, _window, cx| {
308                    telemetry::event!(
309                        "Documentation Viewed",
310                        source = "Agent Registry Feature Upsell",
311                        url = docs_url,
312                    );
313                    cx.open_url(&docs_url)
314                }
315            });
316
317        div().pt_4().px_4().child(
318            Banner::new()
319                .severity(Severity::Success)
320                .child(Label::new(label).mt_0p5())
321                .action_slot(docs_url_button),
322        )
323    }
324
325    fn render_feature_upsells(&self) -> impl IntoElement {
326        let mut container = v_flex();
327
328        for feature in &self.upsells {
329            let banner = match feature {
330                BuiltInAgent::Claude => self.render_feature_upsell_banner(
331                    "Claude Agent support is built-in to Zed!".into(),
332                    "https://zed.dev/docs/ai/external-agents#claude-agent".into(),
333                ),
334                BuiltInAgent::Codex => self.render_feature_upsell_banner(
335                    "Codex CLI support is built-in to Zed!".into(),
336                    "https://zed.dev/docs/ai/external-agents#codex-cli".into(),
337                ),
338                BuiltInAgent::Gemini => self.render_feature_upsell_banner(
339                    "Gemini CLI support is built-in to Zed!".into(),
340                    "https://zed.dev/docs/ai/external-agents#gemini-cli".into(),
341                ),
342            };
343            container = container.child(banner);
344        }
345
346        container
347    }
348
349    fn render_search(&self, cx: &mut Context<Self>) -> Div {
350        let mut key_context = KeyContext::new_with_defaults();
351        key_context.add("BufferSearchBar");
352
353        h_flex()
354            .key_context(key_context)
355            .h_8()
356            .min_w(rems_from_px(384.))
357            .flex_1()
358            .pl_1p5()
359            .pr_2()
360            .gap_2()
361            .border_1()
362            .border_color(cx.theme().colors().border)
363            .rounded_md()
364            .child(Icon::new(IconName::MagnifyingGlass).color(Color::Muted))
365            .child(self.render_text_input(&self.query_editor, cx))
366    }
367
368    fn render_text_input(
369        &self,
370        editor: &Entity<Editor>,
371        cx: &mut Context<Self>,
372    ) -> impl IntoElement {
373        let settings = ThemeSettings::get_global(cx);
374        let text_style = TextStyle {
375            color: if editor.read(cx).read_only(cx) {
376                cx.theme().colors().text_disabled
377            } else {
378                cx.theme().colors().text
379            },
380            font_family: settings.ui_font.family.clone(),
381            font_features: settings.ui_font.features.clone(),
382            font_fallbacks: settings.ui_font.fallbacks.clone(),
383            font_size: rems(0.875).into(),
384            font_weight: settings.ui_font.weight,
385            line_height: relative(1.3),
386            ..Default::default()
387        };
388
389        EditorElement::new(
390            editor,
391            EditorStyle {
392                background: cx.theme().colors().editor_background,
393                local_player: cx.theme().players().local(),
394                text: text_style,
395                ..Default::default()
396            },
397        )
398    }
399
400    fn render_empty_state(&self, cx: &mut Context<Self>) -> impl IntoElement {
401        let has_search = self.search_query(cx).is_some();
402        let registry_store = self.registry_store.read(cx);
403
404        let message = if registry_store.is_fetching() {
405            "Loading registry..."
406        } else if registry_store.fetch_error().is_some() {
407            "Failed to load the agent registry. Please check your connection and try again."
408        } else {
409            match self.filter {
410                RegistryFilter::All => {
411                    if has_search {
412                        "No agents match your search."
413                    } else {
414                        "No agents available."
415                    }
416                }
417                RegistryFilter::Installed => {
418                    if has_search {
419                        "No installed agents match your search."
420                    } else {
421                        "No installed agents."
422                    }
423                }
424                RegistryFilter::NotInstalled => {
425                    if has_search {
426                        "No uninstalled agents match your search."
427                    } else {
428                        "No uninstalled agents."
429                    }
430                }
431            }
432        };
433
434        h_flex()
435            .py_4()
436            .gap_1p5()
437            .when(registry_store.fetch_error().is_some(), |this| {
438                this.child(
439                    Icon::new(IconName::Warning)
440                        .size(IconSize::Small)
441                        .color(Color::Warning),
442                )
443            })
444            .child(Label::new(message))
445    }
446
447    fn render_agents(
448        &mut self,
449        range: Range<usize>,
450        _: &mut Window,
451        cx: &mut Context<Self>,
452    ) -> Vec<AgentRegistryCard> {
453        range
454            .map(|index| {
455                let Some(agent_index) = self.filtered_registry_indices.get(index).copied() else {
456                    return self.render_missing_agent();
457                };
458                let Some(agent) = self.registry_agents.get(agent_index) else {
459                    return self.render_missing_agent();
460                };
461                self.render_registry_agent(agent, cx)
462            })
463            .collect()
464    }
465
466    fn render_missing_agent(&self) -> AgentRegistryCard {
467        AgentRegistryCard::new().child(
468            Label::new("Missing registry entry.")
469                .size(LabelSize::Small)
470                .color(Color::Muted),
471        )
472    }
473
474    fn render_registry_agent(
475        &self,
476        agent: &RegistryAgent,
477        cx: &mut Context<Self>,
478    ) -> AgentRegistryCard {
479        let install_status = self.install_status(agent.id().as_ref());
480        let supports_current_platform = agent.supports_current_platform();
481
482        let icon = match agent.icon_path() {
483            Some(icon_path) => Icon::from_external_svg(icon_path.clone()),
484            None => Icon::new(IconName::Sparkle),
485        }
486        .size(IconSize::Medium)
487        .color(Color::Muted);
488
489        let install_button =
490            self.install_button(agent, install_status, supports_current_platform, cx);
491
492        let repository_button = agent.repository().map(|repository| {
493            let repository_for_tooltip: SharedString = repository.to_string().into();
494            let repository_for_click = repository.to_string();
495
496            IconButton::new(
497                SharedString::from(format!("agent-repo-{}", agent.id())),
498                IconName::Github,
499            )
500            .icon_size(IconSize::Small)
501            .tooltip(move |_, cx| {
502                Tooltip::with_meta(
503                    "Visit Agent Repository",
504                    None,
505                    repository_for_tooltip.clone(),
506                    cx,
507                )
508            })
509            .on_click(move |_, _, cx| {
510                cx.open_url(&repository_for_click);
511            })
512        });
513
514        AgentRegistryCard::new()
515            .child(
516                h_flex()
517                    .justify_between()
518                    .child(
519                        h_flex()
520                            .gap_2()
521                            .child(icon)
522                            .child(Headline::new(agent.name().clone()).size(HeadlineSize::Small))
523                            .child(Label::new(format!("v{}", agent.version())).color(Color::Muted))
524                            .when(!supports_current_platform, |this| {
525                                this.child(
526                                    Label::new("Not supported on this platform")
527                                        .size(LabelSize::Small)
528                                        .color(Color::Warning),
529                                )
530                            }),
531                    )
532                    .child(install_button),
533            )
534            .child(
535                h_flex()
536                    .gap_2()
537                    .justify_between()
538                    .child(
539                        Label::new(agent.description().clone())
540                            .size(LabelSize::Small)
541                            .truncate(),
542                    )
543                    .child(
544                        h_flex()
545                            .gap_1()
546                            .child(
547                                Label::new(format!("ID: {}", agent.id()))
548                                    .size(LabelSize::Small)
549                                    .color(Color::Muted)
550                                    .truncate(),
551                            )
552                            .when_some(repository_button, |this, button| this.child(button)),
553                    ),
554            )
555    }
556
557    fn install_button(
558        &self,
559        agent: &RegistryAgent,
560        install_status: RegistryInstallStatus,
561        supports_current_platform: bool,
562        cx: &mut Context<Self>,
563    ) -> Button {
564        let button_id = SharedString::from(format!("install-agent-{}", agent.id()));
565
566        if !supports_current_platform {
567            return Button::new(button_id, "Unavailable")
568                .style(ButtonStyle::OutlinedGhost)
569                .disabled(true);
570        }
571
572        match install_status {
573            RegistryInstallStatus::NotInstalled => {
574                let fs = <dyn Fs>::global(cx);
575                let agent_id = agent.id().to_string();
576                Button::new(button_id, "Install")
577                    .style(ButtonStyle::Tinted(ui::TintColor::Accent))
578                    .icon(IconName::Download)
579                    .icon_size(IconSize::Small)
580                    .icon_color(Color::Muted)
581                    .icon_position(IconPosition::Start)
582                    .on_click(move |_, _, cx| {
583                        let agent_id = agent_id.clone();
584                        update_settings_file(fs.clone(), cx, move |settings, _| {
585                            let agent_servers = settings.agent_servers.get_or_insert_default();
586                            agent_servers.entry(agent_id).or_insert_with(|| {
587                                settings::CustomAgentServerSettings::Registry {
588                                    default_mode: None,
589                                    default_model: None,
590                                    env: Default::default(),
591                                    favorite_models: Vec::new(),
592                                    default_config_options: HashMap::default(),
593                                    favorite_config_option_values: HashMap::default(),
594                                }
595                            });
596                        });
597                    })
598            }
599            RegistryInstallStatus::InstalledRegistry => {
600                let fs = <dyn Fs>::global(cx);
601                let agent_id = agent.id().to_string();
602                Button::new(button_id, "Remove")
603                    .style(ButtonStyle::OutlinedGhost)
604                    .on_click(move |_, _, cx| {
605                        let agent_id = agent_id.clone();
606                        update_settings_file(fs.clone(), cx, move |settings, _| {
607                            let Some(agent_servers) = settings.agent_servers.as_mut() else {
608                                return;
609                            };
610                            if let Some(entry) = agent_servers.get(agent_id.as_str())
611                                && matches!(
612                                    entry,
613                                    settings::CustomAgentServerSettings::Registry { .. }
614                                )
615                            {
616                                agent_servers.remove(agent_id.as_str());
617                            }
618                        });
619                    })
620            }
621            RegistryInstallStatus::InstalledCustom => Button::new(button_id, "Installed")
622                .style(ButtonStyle::OutlinedGhost)
623                .disabled(true),
624            RegistryInstallStatus::InstalledExtension => Button::new(button_id, "Installed")
625                .style(ButtonStyle::OutlinedGhost)
626                .disabled(true),
627        }
628    }
629}
630
631impl Render for AgentRegistryPage {
632    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
633        v_flex()
634            .size_full()
635            .bg(cx.theme().colors().editor_background)
636            .child(
637                v_flex()
638                    .p_4()
639                    .gap_4()
640                    .border_b_1()
641                    .border_color(cx.theme().colors().border_variant)
642                    .child(
643                        h_flex()
644                            .w_full()
645                            .gap_1p5()
646                            .justify_between()
647                            .child(Headline::new("ACP Registry").size(HeadlineSize::Large))
648                            .child(
649                                Button::new("learn-more", "Learn More")
650                                    .style(ButtonStyle::Outlined)
651                                    .size(ButtonSize::Medium)
652                                    .icon(IconName::ArrowUpRight)
653                                    .icon_color(Color::Muted)
654                                    .icon_size(IconSize::Small)
655                                    .on_click(move |_, _, cx| {
656                                        cx.open_url(&zed_urls::acp_registry_blog(cx))
657                                    }),
658                            ),
659                    )
660                    .child(
661                        h_flex()
662                            .w_full()
663                            .flex_wrap()
664                            .gap_2()
665                            .child(self.render_search(cx))
666                            .child(
667                                div().child(
668                                    ToggleButtonGroup::single_row(
669                                        "registry-filter-buttons",
670                                        [
671                                            ToggleButtonSimple::new(
672                                                "All",
673                                                cx.listener(|this, _event, _, cx| {
674                                                    this.filter = RegistryFilter::All;
675                                                    this.filter_registry_agents(cx);
676                                                    this.scroll_to_top(cx);
677                                                }),
678                                            ),
679                                            ToggleButtonSimple::new(
680                                                "Installed",
681                                                cx.listener(|this, _event, _, cx| {
682                                                    this.filter = RegistryFilter::Installed;
683                                                    this.filter_registry_agents(cx);
684                                                    this.scroll_to_top(cx);
685                                                }),
686                                            ),
687                                            ToggleButtonSimple::new(
688                                                "Not Installed",
689                                                cx.listener(|this, _event, _, cx| {
690                                                    this.filter = RegistryFilter::NotInstalled;
691                                                    this.filter_registry_agents(cx);
692                                                    this.scroll_to_top(cx);
693                                                }),
694                                            ),
695                                        ],
696                                    )
697                                    .style(ToggleButtonGroupStyle::Outlined)
698                                    .size(ToggleButtonGroupSize::Custom(rems_from_px(30.)))
699                                    .label_size(LabelSize::Default)
700                                    .auto_width()
701                                    .selected_index(match self.filter {
702                                        RegistryFilter::All => 0,
703                                        RegistryFilter::Installed => 1,
704                                        RegistryFilter::NotInstalled => 2,
705                                    })
706                                    .into_any_element(),
707                                ),
708                            ),
709                    ),
710            )
711            .child(self.render_feature_upsells())
712            .child(v_flex().px_4().size_full().overflow_y_hidden().map(|this| {
713                let count = self.filtered_registry_indices.len();
714                let has_upsells = !self.upsells.is_empty();
715                if count == 0 && !has_upsells {
716                    this.child(self.render_empty_state(cx)).into_any_element()
717                } else if count == 0 {
718                    this.into_any_element()
719                } else {
720                    let scroll_handle = &self.list;
721                    this.child(
722                        uniform_list("registry-entries", count, cx.processor(Self::render_agents))
723                            .flex_grow()
724                            .pb_4()
725                            .track_scroll(scroll_handle),
726                    )
727                    .vertical_scrollbar_for(scroll_handle, window, cx)
728                    .into_any_element()
729                }
730            }))
731    }
732}
733
734impl EventEmitter<ItemEvent> for AgentRegistryPage {}
735
736impl Focusable for AgentRegistryPage {
737    fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
738        self.query_editor.read(cx).focus_handle(cx)
739    }
740}
741
742impl Item for AgentRegistryPage {
743    type Event = ItemEvent;
744
745    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
746        "ACP Registry".into()
747    }
748
749    fn telemetry_event_text(&self) -> Option<&'static str> {
750        Some("ACP Registry Page Opened")
751    }
752
753    fn show_toolbar(&self) -> bool {
754        false
755    }
756
757    fn to_item_events(event: &Self::Event, f: &mut dyn FnMut(workspace::item::ItemEvent)) {
758        f(*event)
759    }
760}