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