agent: Refresh the profile selector and modal design (#29816)

Danilo Leal , Bennet Bo Fenner , Bennet Bo Fenner , and Bennet Bo Fenner created

- [x] Separate MCP servers from tools in the profile customization modal
view
- [x] Group MCP tools in the MCP picker and add a heading
- [x] Separate bult-in profiles from custom ones in the dropdown
selector
- [x] Separate bult-in profiles from custom ones in the modal
- [ ] Enable looping through items via keybinding without opening the
dropdown (will be done on a follow-up PR)
- [ ] Stretch: Focus on the currently active item upon opening the
dropdown (will be done on a follow-up PR)

Release Notes:

- N/A

---------

Co-authored-by: Bennet Bo Fenner <53836821+bennetbo@users.noreply.github.com>
Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
Co-authored-by: Bennet Bo Fenner <bennet@zed.dev>

Change summary

Cargo.lock                                                                             |   1 
assets/icons/message_bubble_dashed.svg                                                 |   1 
assets/icons/scissors.svg                                                              |   1 
crates/agent/Cargo.toml                                                                |   1 
crates/agent/src/assistant_configuration/manage_profiles_modal.rs                      | 386 
crates/agent/src/assistant_configuration/manage_profiles_modal/profile_modal_header.rs |  32 
crates/agent/src/assistant_configuration/tool_picker.rs                                | 316 
crates/agent/src/profile_selector.rs                                                   | 129 
crates/assistant_settings/src/agent_profile.rs                                         |  35 
crates/icons/src/icons.rs                                                              |   2 
crates/ui/src/components/navigable.rs                                                  |   4 
11 files changed, 635 insertions(+), 273 deletions(-)

Detailed changes

Cargo.lock ๐Ÿ”—

@@ -79,7 +79,6 @@ dependencies = [
  "heed",
  "html_to_markdown",
  "http_client",
- "indexmap",
  "indoc",
  "itertools 0.14.0",
  "jsonschema",

assets/icons/message_bubble_dashed.svg ๐Ÿ”—

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-message-circle-dashed-icon lucide-message-circle-dashed"><path d="M13.5 3.1c-.5 0-1-.1-1.5-.1s-1 .1-1.5.1"/><path d="M19.3 6.8a10.45 10.45 0 0 0-2.1-2.1"/><path d="M20.9 13.5c.1-.5.1-1 .1-1.5s-.1-1-.1-1.5"/><path d="M17.2 19.3a10.45 10.45 0 0 0 2.1-2.1"/><path d="M10.5 20.9c.5.1 1 .1 1.5.1s1-.1 1.5-.1"/><path d="M3.5 17.5 2 22l4.5-1.5"/><path d="M3.1 10.5c0 .5-.1 1-.1 1.5s.1 1 .1 1.5"/><path d="M6.8 4.7a10.45 10.45 0 0 0-2.1 2.1"/></svg>

assets/icons/scissors.svg ๐Ÿ”—

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="lucide lucide-scissors-icon lucide-scissors"><circle cx="6" cy="6" r="3"/><path d="M8.12 8.12 12 12"/><path d="M20 4 8.12 15.88"/><circle cx="6" cy="18" r="3"/><path d="M14.8 14.8 20 20"/></svg>

crates/agent/Cargo.toml ๐Ÿ”—

@@ -46,7 +46,6 @@ gpui.workspace = true
 heed.workspace = true
 html_to_markdown.workspace = true
 http_client.workspace = true
-indexmap.workspace = true
 itertools.workspace = true
 jsonschema.workspace = true
 language.workspace = true

crates/agent/src/assistant_configuration/manage_profiles_modal.rs ๐Ÿ”—

@@ -2,7 +2,7 @@ mod profile_modal_header;
 
 use std::sync::Arc;
 
-use assistant_settings::{AgentProfile, AgentProfileId, AssistantSettings};
+use assistant_settings::{AgentProfile, AgentProfileId, AssistantSettings, builtin_profiles};
 use assistant_tool::ToolWorkingSet;
 use convert_case::{Case, Casing as _};
 use editor::Editor;
@@ -22,6 +22,8 @@ use crate::assistant_configuration::manage_profiles_modal::profile_modal_header:
 use crate::assistant_configuration::tool_picker::{ToolPicker, ToolPickerDelegate};
 use crate::{AssistantPanel, ManageProfiles, ThreadStore};
 
+use super::tool_picker::ToolPickerMode;
+
 enum Mode {
     ChooseProfile(ChooseProfileMode),
     NewProfile(NewProfileMode),
@@ -31,26 +33,39 @@ enum Mode {
         tool_picker: Entity<ToolPicker>,
         _subscription: Subscription,
     },
+    ConfigureMcps {
+        profile_id: AgentProfileId,
+        tool_picker: Entity<ToolPicker>,
+        _subscription: Subscription,
+    },
 }
 
 impl Mode {
     pub fn choose_profile(_window: &mut Window, cx: &mut Context<ManageProfilesModal>) -> Self {
         let settings = AssistantSettings::get_global(cx);
 
-        let mut profiles = settings.profiles.clone();
-        profiles.sort_unstable_by(|_, a, _, b| a.name.cmp(&b.name));
+        let mut builtin_profiles = Vec::new();
+        let mut custom_profiles = Vec::new();
 
-        let profiles = profiles
-            .into_iter()
-            .map(|(id, profile)| ProfileEntry {
-                id,
-                name: profile.name,
+        for (profile_id, profile) in settings.profiles.iter() {
+            let entry = ProfileEntry {
+                id: profile_id.clone(),
+                name: profile.name.clone(),
                 navigation: NavigableEntry::focusable(cx),
-            })
-            .collect::<Vec<_>>();
+            };
+            if builtin_profiles::is_builtin(profile_id) {
+                builtin_profiles.push(entry);
+            } else {
+                custom_profiles.push(entry);
+            }
+        }
+
+        builtin_profiles.sort_unstable_by(|a, b| a.name.cmp(&b.name));
+        custom_profiles.sort_unstable_by(|a, b| a.name.cmp(&b.name));
 
         Self::ChooseProfile(ChooseProfileMode {
-            profiles,
+            builtin_profiles,
+            custom_profiles,
             add_new_profile: NavigableEntry::focusable(cx),
         })
     }
@@ -65,7 +80,8 @@ struct ProfileEntry {
 
 #[derive(Clone)]
 pub struct ChooseProfileMode {
-    profiles: Vec<ProfileEntry>,
+    builtin_profiles: Vec<ProfileEntry>,
+    custom_profiles: Vec<ProfileEntry>,
     add_new_profile: NavigableEntry,
 }
 
@@ -74,6 +90,8 @@ pub struct ViewProfileMode {
     profile_id: AgentProfileId,
     fork_profile: NavigableEntry,
     configure_tools: NavigableEntry,
+    configure_mcps: NavigableEntry,
+    cancel_item: NavigableEntry,
 }
 
 #[derive(Clone)]
@@ -166,7 +184,47 @@ impl ManageProfilesModal {
             profile_id,
             fork_profile: NavigableEntry::focusable(cx),
             configure_tools: NavigableEntry::focusable(cx),
+            configure_mcps: NavigableEntry::focusable(cx),
+            cancel_item: NavigableEntry::focusable(cx),
+        });
+        self.focus_handle(cx).focus(window);
+    }
+
+    fn configure_mcps(
+        &mut self,
+        profile_id: AgentProfileId,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let settings = AssistantSettings::get_global(cx);
+        let Some(profile) = settings.profiles.get(&profile_id).cloned() else {
+            return;
+        };
+
+        let tool_picker = cx.new(|cx| {
+            let delegate = ToolPickerDelegate::new(
+                ToolPickerMode::McpTools,
+                self.fs.clone(),
+                self.tools.clone(),
+                self.thread_store.clone(),
+                profile_id.clone(),
+                profile,
+                cx,
+            );
+            ToolPicker::mcp_tools(delegate, window, cx)
+        });
+        let dismiss_subscription = cx.subscribe_in(&tool_picker, window, {
+            let profile_id = profile_id.clone();
+            move |this, _tool_picker, _: &DismissEvent, window, cx| {
+                this.view_profile(profile_id.clone(), window, cx);
+            }
         });
+
+        self.mode = Mode::ConfigureMcps {
+            profile_id,
+            tool_picker,
+            _subscription: dismiss_subscription,
+        };
         self.focus_handle(cx).focus(window);
     }
 
@@ -183,6 +241,7 @@ impl ManageProfilesModal {
 
         let tool_picker = cx.new(|cx| {
             let delegate = ToolPickerDelegate::new(
+                ToolPickerMode::BuiltinTools,
                 self.fs.clone(),
                 self.tools.clone(),
                 self.thread_store.clone(),
@@ -190,7 +249,7 @@ impl ManageProfilesModal {
                 profile,
                 cx,
             );
-            ToolPicker::new(delegate, window, cx)
+            ToolPicker::builtin_tools(delegate, window, cx)
         });
         let dismiss_subscription = cx.subscribe_in(&tool_picker, window, {
             let profile_id = profile_id.clone();
@@ -241,6 +300,7 @@ impl ManageProfilesModal {
             }
             Mode::ViewProfile(_) => {}
             Mode::ConfigureTools { .. } => {}
+            Mode::ConfigureMcps { .. } => {}
         }
     }
 
@@ -257,7 +317,12 @@ impl ManageProfilesModal {
                 }
             }
             Mode::ViewProfile(_) => self.choose_profile(window, cx),
-            Mode::ConfigureTools { .. } => {}
+            Mode::ConfigureTools { profile_id, .. } => {
+                self.view_profile(profile_id.clone(), window, cx)
+            }
+            Mode::ConfigureMcps { profile_id, .. } => {
+                self.view_profile(profile_id.clone(), window, cx)
+            }
         }
     }
 
@@ -284,6 +349,7 @@ impl Focusable for ManageProfilesModal {
             Mode::NewProfile(mode) => mode.name_editor.focus_handle(cx),
             Mode::ViewProfile(_) => self.focus_handle.clone(),
             Mode::ConfigureTools { tool_picker, .. } => tool_picker.focus_handle(cx),
+            Mode::ConfigureMcps { tool_picker, .. } => tool_picker.focus_handle(cx),
         }
     }
 }
@@ -291,6 +357,51 @@ impl Focusable for ManageProfilesModal {
 impl EventEmitter<DismissEvent> for ManageProfilesModal {}
 
 impl ManageProfilesModal {
+    fn render_profile(
+        &self,
+        profile: &ProfileEntry,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> impl IntoElement + use<> {
+        div()
+            .id(SharedString::from(format!("profile-{}", profile.id)))
+            .track_focus(&profile.navigation.focus_handle)
+            .on_action({
+                let profile_id = profile.id.clone();
+                cx.listener(move |this, _: &menu::Confirm, window, cx| {
+                    this.view_profile(profile_id.clone(), window, cx);
+                })
+            })
+            .child(
+                ListItem::new(SharedString::from(format!("profile-{}", profile.id)))
+                    .toggle_state(profile.navigation.focus_handle.contains_focused(window, cx))
+                    .inset(true)
+                    .spacing(ListItemSpacing::Sparse)
+                    .child(Label::new(profile.name.clone()))
+                    .end_slot(
+                        h_flex()
+                            .gap_1()
+                            .child(
+                                Label::new("Customize")
+                                    .size(LabelSize::Small)
+                                    .color(Color::Muted),
+                            )
+                            .children(KeyBinding::for_action_in(
+                                &menu::Confirm,
+                                &self.focus_handle,
+                                window,
+                                cx,
+                            )),
+                    )
+                    .on_click({
+                        let profile_id = profile.id.clone();
+                        cx.listener(move |this, _, window, cx| {
+                            this.view_profile(profile_id.clone(), window, cx);
+                        })
+                    }),
+            )
+    }
+
     fn render_choose_profile(
         &mut self,
         mode: ChooseProfileMode,
@@ -301,57 +412,31 @@ impl ManageProfilesModal {
             div()
                 .track_focus(&self.focus_handle(cx))
                 .size_full()
-                .child(ProfileModalHeader::new(
-                    "Agent Profiles",
-                    IconName::ZedAssistant,
-                ))
+                .child(ProfileModalHeader::new("Agent Profiles", None))
                 .child(
                     v_flex()
                         .pb_1()
                         .child(ListSeparator)
-                        .children(mode.profiles.iter().map(|profile| {
-                            div()
-                                .id(SharedString::from(format!("profile-{}", profile.id)))
-                                .track_focus(&profile.navigation.focus_handle)
-                                .on_action({
-                                    let profile_id = profile.id.clone();
-                                    cx.listener(move |this, _: &menu::Confirm, window, cx| {
-                                        this.view_profile(profile_id.clone(), window, cx);
-                                    })
-                                })
+                        .children(
+                            mode.builtin_profiles
+                                .iter()
+                                .map(|profile| self.render_profile(profile, window, cx)),
+                        )
+                        .when(!mode.custom_profiles.is_empty(), |this| {
+                            this.child(ListSeparator)
                                 .child(
-                                    ListItem::new(SharedString::from(format!(
-                                        "profile-{}",
-                                        profile.id
-                                    )))
-                                    .toggle_state(
-                                        profile
-                                            .navigation
-                                            .focus_handle
-                                            .contains_focused(window, cx),
-                                    )
-                                    .inset(true)
-                                    .spacing(ListItemSpacing::Sparse)
-                                    .child(Label::new(profile.name.clone()))
-                                    .end_slot(
-                                        h_flex()
-                                            .gap_1()
-                                            .child(Label::new("Customize").size(LabelSize::Small))
-                                            .children(KeyBinding::for_action_in(
-                                                &menu::Confirm,
-                                                &self.focus_handle,
-                                                window,
-                                                cx,
-                                            )),
-                                    )
-                                    .on_click({
-                                        let profile_id = profile.id.clone();
-                                        cx.listener(move |this, _, window, cx| {
-                                            this.view_profile(profile_id.clone(), window, cx);
-                                        })
-                                    }),
+                                    div().pl_2().pb_1().child(
+                                        Label::new("Custom Profiles")
+                                            .size(LabelSize::Small)
+                                            .color(Color::Muted),
+                                    ),
+                                )
+                                .children(
+                                    mode.custom_profiles
+                                        .iter()
+                                        .map(|profile| self.render_profile(profile, window, cx)),
                                 )
-                        }))
+                        })
                         .child(ListSeparator)
                         .child(
                             div()
@@ -382,7 +467,10 @@ impl ManageProfilesModal {
                 .into_any_element(),
         )
         .map(|mut navigable| {
-            for profile in mode.profiles {
+            for profile in mode.builtin_profiles {
+                navigable = navigable.entry(profile.navigation);
+            }
+            for profile in mode.custom_profiles {
                 navigable = navigable.entry(profile.navigation);
             }
 
@@ -411,11 +499,14 @@ impl ManageProfilesModal {
             .id("new-profile")
             .track_focus(&self.focus_handle(cx))
             .child(ProfileModalHeader::new(
-                match base_profile_name {
+                match &base_profile_name {
                     Some(base_profile) => format!("Fork {base_profile}"),
                     None => "New Profile".into(),
                 },
-                IconName::Plus,
+                match base_profile_name {
+                    Some(_) => Some(IconName::Scissors),
+                    None => Some(IconName::Plus),
+                },
             ))
             .child(ListSeparator)
             .child(h_flex().p_2().child(mode.name_editor.clone()))
@@ -429,20 +520,24 @@ impl ManageProfilesModal {
     ) -> impl IntoElement {
         let settings = AssistantSettings::get_global(cx);
 
+        let profile_id = &settings.default_profile;
         let profile_name = settings
             .profiles
             .get(&mode.profile_id)
             .map(|profile| profile.name.clone())
             .unwrap_or_else(|| "Unknown".into());
 
+        let icon = match profile_id.as_str() {
+            "write" => IconName::Pencil,
+            "ask" => IconName::MessageBubbles,
+            _ => IconName::UserRoundPen,
+        };
+
         Navigable::new(
             div()
                 .track_focus(&self.focus_handle(cx))
                 .size_full()
-                .child(ProfileModalHeader::new(
-                    profile_name,
-                    IconName::ZedAssistant,
-                ))
+                .child(ProfileModalHeader::new(profile_name, Some(icon)))
                 .child(
                     v_flex()
                         .pb_1()
@@ -466,7 +561,11 @@ impl ManageProfilesModal {
                                         )
                                         .inset(true)
                                         .spacing(ListItemSpacing::Sparse)
-                                        .start_slot(Icon::new(IconName::GitBranch))
+                                        .start_slot(
+                                            Icon::new(IconName::Scissors)
+                                                .size(IconSize::Small)
+                                                .color(Color::Muted),
+                                        )
                                         .child(Label::new("Fork Profile"))
                                         .on_click({
                                             let profile_id = mode.profile_id.clone();
@@ -499,7 +598,11 @@ impl ManageProfilesModal {
                                         )
                                         .inset(true)
                                         .spacing(ListItemSpacing::Sparse)
-                                        .start_slot(Icon::new(IconName::Cog))
+                                        .start_slot(
+                                            Icon::new(IconName::Settings)
+                                                .size(IconSize::Small)
+                                                .color(Color::Muted),
+                                        )
                                         .child(Label::new("Configure Tools"))
                                         .on_click({
                                             let profile_id = mode.profile_id.clone();
@@ -512,12 +615,90 @@ impl ManageProfilesModal {
                                             })
                                         }),
                                 ),
+                        )
+                        .child(
+                            div()
+                                .id("configure-mcps")
+                                .track_focus(&mode.configure_mcps.focus_handle)
+                                .on_action({
+                                    let profile_id = mode.profile_id.clone();
+                                    cx.listener(move |this, _: &menu::Confirm, window, cx| {
+                                        this.configure_mcps(profile_id.clone(), window, cx);
+                                    })
+                                })
+                                .child(
+                                    ListItem::new("configure-mcps")
+                                        .toggle_state(
+                                            mode.configure_mcps
+                                                .focus_handle
+                                                .contains_focused(window, cx),
+                                        )
+                                        .inset(true)
+                                        .spacing(ListItemSpacing::Sparse)
+                                        .start_slot(
+                                            Icon::new(IconName::Hammer)
+                                                .size(IconSize::Small)
+                                                .color(Color::Muted),
+                                        )
+                                        .child(Label::new("Configure MCP Servers"))
+                                        .on_click({
+                                            let profile_id = mode.profile_id.clone();
+                                            cx.listener(move |this, _, window, cx| {
+                                                this.configure_mcps(profile_id.clone(), window, cx);
+                                            })
+                                        }),
+                                ),
+                        )
+                        .child(ListSeparator)
+                        .child(
+                            div()
+                                .id("cancel-item")
+                                .track_focus(&mode.cancel_item.focus_handle)
+                                .on_action({
+                                    cx.listener(move |this, _: &menu::Confirm, window, cx| {
+                                        this.cancel(window, cx);
+                                    })
+                                })
+                                .child(
+                                    ListItem::new("cancel-item")
+                                        .toggle_state(
+                                            mode.cancel_item
+                                                .focus_handle
+                                                .contains_focused(window, cx),
+                                        )
+                                        .inset(true)
+                                        .spacing(ListItemSpacing::Sparse)
+                                        .start_slot(
+                                            Icon::new(IconName::ArrowLeft)
+                                                .size(IconSize::Small)
+                                                .color(Color::Muted),
+                                        )
+                                        .child(Label::new("Go Back"))
+                                        .end_slot(
+                                            div().children(
+                                                KeyBinding::for_action_in(
+                                                    &menu::Cancel,
+                                                    &self.focus_handle,
+                                                    window,
+                                                    cx,
+                                                )
+                                                .map(|kb| kb.size(rems_from_px(12.))),
+                                            ),
+                                        )
+                                        .on_click({
+                                            cx.listener(move |this, _, window, cx| {
+                                                this.cancel(window, cx);
+                                            })
+                                        }),
+                                ),
                         ),
                 )
                 .into_any_element(),
         )
         .entry(mode.fork_profile)
         .entry(mode.configure_tools)
+        .entry(mode.configure_mcps)
+        .entry(mode.cancel_item)
     }
 }
 
@@ -525,6 +706,43 @@ impl Render for ManageProfilesModal {
     fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
         let settings = AssistantSettings::get_global(cx);
 
+        let go_back_item = div()
+            .id("cancel-item")
+            .track_focus(&self.focus_handle)
+            .on_action({
+                cx.listener(move |this, _: &menu::Confirm, window, cx| {
+                    this.cancel(window, cx);
+                })
+            })
+            .child(
+                ListItem::new("cancel-item")
+                    .toggle_state(self.focus_handle.contains_focused(window, cx))
+                    .inset(true)
+                    .spacing(ListItemSpacing::Sparse)
+                    .start_slot(
+                        Icon::new(IconName::ArrowLeft)
+                            .size(IconSize::Small)
+                            .color(Color::Muted),
+                    )
+                    .child(Label::new("Go Back"))
+                    .end_slot(
+                        div().children(
+                            KeyBinding::for_action_in(
+                                &menu::Cancel,
+                                &self.focus_handle,
+                                window,
+                                cx,
+                            )
+                            .map(|kb| kb.size(rems_from_px(12.))),
+                        ),
+                    )
+                    .on_click({
+                        cx.listener(move |this, _, window, cx| {
+                            this.cancel(window, cx);
+                        })
+                    }),
+            );
+
         div()
             .elevation_3(cx)
             .w(rems(34.))
@@ -556,13 +774,39 @@ impl Render for ManageProfilesModal {
                         .map(|profile| profile.name.clone())
                         .unwrap_or_else(|| "Unknown".into());
 
-                    div()
+                    v_flex()
+                        .pb_1()
                         .child(ProfileModalHeader::new(
-                            format!("{profile_name}: Configure Tools"),
-                            IconName::Cog,
+                            format!("{profile_name} โ€” Configure Tools"),
+                            Some(IconName::Cog),
                         ))
                         .child(ListSeparator)
                         .child(tool_picker.clone())
+                        .child(ListSeparator)
+                        .child(go_back_item)
+                        .into_any_element()
+                }
+                Mode::ConfigureMcps {
+                    profile_id,
+                    tool_picker,
+                    ..
+                } => {
+                    let profile_name = settings
+                        .profiles
+                        .get(profile_id)
+                        .map(|profile| profile.name.clone())
+                        .unwrap_or_else(|| "Unknown".into());
+
+                    v_flex()
+                        .pb_1()
+                        .child(ProfileModalHeader::new(
+                            format!("{profile_name} โ€” Configure MCP Servers"),
+                            Some(IconName::Hammer),
+                        ))
+                        .child(ListSeparator)
+                        .child(tool_picker.clone())
+                        .child(ListSeparator)
+                        .child(go_back_item)
                         .into_any_element()
                 }
             })

crates/agent/src/assistant_configuration/manage_profiles_modal/profile_modal_header.rs ๐Ÿ”—

@@ -3,11 +3,11 @@ use ui::prelude::*;
 #[derive(IntoElement)]
 pub struct ProfileModalHeader {
     label: SharedString,
-    icon: IconName,
+    icon: Option<IconName>,
 }
 
 impl ProfileModalHeader {
-    pub fn new(label: impl Into<SharedString>, icon: IconName) -> Self {
+    pub fn new(label: impl Into<SharedString>, icon: Option<IconName>) -> Self {
         Self {
             label: label.into(),
             icon,
@@ -17,22 +17,26 @@ impl ProfileModalHeader {
 
 impl RenderOnce for ProfileModalHeader {
     fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
-        h_flex()
+        let mut container = h_flex()
             .w_full()
             .px(DynamicSpacing::Base12.rems(cx))
             .pt(DynamicSpacing::Base08.rems(cx))
             .pb(DynamicSpacing::Base04.rems(cx))
             .rounded_t_sm()
-            .gap_1p5()
-            .child(Icon::new(self.icon).size(IconSize::XSmall))
-            .child(
-                h_flex().gap_1().overflow_x_hidden().child(
-                    div()
-                        .max_w_96()
-                        .overflow_x_hidden()
-                        .text_ellipsis()
-                        .child(Headline::new(self.label).size(HeadlineSize::XSmall)),
-                ),
-            )
+            .gap_1p5();
+
+        if let Some(icon) = self.icon {
+            container = container.child(Icon::new(icon).size(IconSize::XSmall).color(Color::Muted));
+        }
+
+        container.child(
+            h_flex().gap_1().overflow_x_hidden().child(
+                div()
+                    .max_w_96()
+                    .overflow_x_hidden()
+                    .text_ellipsis()
+                    .child(Headline::new(self.label).size(HeadlineSize::XSmall)),
+            ),
+        )
     }
 }

crates/agent/src/assistant_configuration/tool_picker.rs ๐Ÿ”—

@@ -1,4 +1,4 @@
-use std::sync::Arc;
+use std::{collections::BTreeMap, sync::Arc};
 
 use assistant_settings::{
     AgentProfile, AgentProfileContent, AgentProfileId, AssistantSettings, AssistantSettingsContent,
@@ -6,11 +6,10 @@ use assistant_settings::{
 };
 use assistant_tool::{ToolSource, ToolWorkingSet};
 use fs::Fs;
-use fuzzy::{StringMatch, StringMatchCandidate, match_strings};
 use gpui::{App, Context, DismissEvent, Entity, EventEmitter, Focusable, Task, WeakEntity, Window};
 use picker::{Picker, PickerDelegate};
 use settings::{Settings as _, update_settings_file};
-use ui::{HighlightedLabel, ListItem, ListItemSpacing, prelude::*};
+use ui::{ListItem, ListItemSpacing, prelude::*};
 use util::ResultExt as _;
 
 use crate::ThreadStore;
@@ -19,11 +18,30 @@ pub struct ToolPicker {
     picker: Entity<Picker<ToolPickerDelegate>>,
 }
 
+#[derive(Clone, Copy, Debug, PartialEq)]
+pub enum ToolPickerMode {
+    BuiltinTools,
+    McpTools,
+}
+
 impl ToolPicker {
-    pub fn new(delegate: ToolPickerDelegate, window: &mut Window, cx: &mut Context<Self>) -> Self {
+    pub fn builtin_tools(
+        delegate: ToolPickerDelegate,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Self {
         let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx).modal(false));
         Self { picker }
     }
+
+    pub fn mcp_tools(
+        delegate: ToolPickerDelegate,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) -> Self {
+        let picker = cx.new(|cx| Picker::list(delegate, window, cx).modal(false));
+        Self { picker }
+    }
 }
 
 impl EventEmitter<DismissEvent> for ToolPicker {}
@@ -41,24 +59,31 @@ impl Render for ToolPicker {
 }
 
 #[derive(Debug, Clone)]
-pub struct ToolEntry {
-    pub name: Arc<str>,
-    pub source: ToolSource,
+pub enum PickerItem {
+    Tool {
+        server_id: Option<Arc<str>>,
+        name: Arc<str>,
+    },
+    ContextServer {
+        server_id: Arc<str>,
+    },
 }
 
 pub struct ToolPickerDelegate {
     tool_picker: WeakEntity<ToolPicker>,
     thread_store: WeakEntity<ThreadStore>,
     fs: Arc<dyn Fs>,
-    tools: Vec<ToolEntry>,
+    items: Arc<Vec<PickerItem>>,
     profile_id: AgentProfileId,
     profile: AgentProfile,
-    matches: Vec<StringMatch>,
+    filtered_items: Vec<PickerItem>,
     selected_index: usize,
+    mode: ToolPickerMode,
 }
 
 impl ToolPickerDelegate {
     pub fn new(
+        mode: ToolPickerMode,
         fs: Arc<dyn Fs>,
         tool_set: Entity<ToolWorkingSet>,
         thread_store: WeakEntity<ThreadStore>,
@@ -66,33 +91,60 @@ impl ToolPickerDelegate {
         profile: AgentProfile,
         cx: &mut Context<ToolPicker>,
     ) -> Self {
-        let mut tool_entries = Vec::new();
-
-        for (source, tools) in tool_set.read(cx).tools_by_source(cx) {
-            tool_entries.extend(tools.into_iter().map(|tool| ToolEntry {
-                name: tool.name().into(),
-                source: source.clone(),
-            }));
-        }
+        let items = Arc::new(Self::resolve_items(mode, &tool_set, cx));
 
         Self {
             tool_picker: cx.entity().downgrade(),
             thread_store,
             fs,
-            tools: tool_entries,
+            items,
             profile_id,
             profile,
-            matches: Vec::new(),
+            filtered_items: Vec::new(),
             selected_index: 0,
+            mode,
         }
     }
+
+    fn resolve_items(
+        mode: ToolPickerMode,
+        tool_set: &Entity<ToolWorkingSet>,
+        cx: &mut App,
+    ) -> Vec<PickerItem> {
+        let mut items = Vec::new();
+        for (source, tools) in tool_set.read(cx).tools_by_source(cx) {
+            match source {
+                ToolSource::Native => {
+                    if mode == ToolPickerMode::BuiltinTools {
+                        items.extend(tools.into_iter().map(|tool| PickerItem::Tool {
+                            name: tool.name().into(),
+                            server_id: None,
+                        }));
+                    }
+                }
+                ToolSource::ContextServer { id } => {
+                    if mode == ToolPickerMode::McpTools && !tools.is_empty() {
+                        let server_id: Arc<str> = id.clone().into();
+                        items.push(PickerItem::ContextServer {
+                            server_id: server_id.clone(),
+                        });
+                        items.extend(tools.into_iter().map(|tool| PickerItem::Tool {
+                            name: tool.name().into(),
+                            server_id: Some(server_id.clone()),
+                        }));
+                    }
+                }
+            }
+        }
+        items
+    }
 }
 
 impl PickerDelegate for ToolPickerDelegate {
-    type ListItem = ListItem;
+    type ListItem = AnyElement;
 
     fn match_count(&self) -> usize {
-        self.matches.len()
+        self.filtered_items.len()
     }
 
     fn selected_index(&self) -> usize {
@@ -108,8 +160,25 @@ impl PickerDelegate for ToolPickerDelegate {
         self.selected_index = ix;
     }
 
+    fn can_select(
+        &mut self,
+        ix: usize,
+        _window: &mut Window,
+        _cx: &mut Context<Picker<Self>>,
+    ) -> bool {
+        let item = &self.filtered_items[ix];
+        match item {
+            PickerItem::Tool { .. } => true,
+            PickerItem::ContextServer { .. } => false,
+        }
+    }
+
     fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
-        "Search toolsโ€ฆ".into()
+        match self.mode {
+            ToolPickerMode::BuiltinTools => "Search built-in toolsโ€ฆ",
+            ToolPickerMode::McpTools => "Search MCP serversโ€ฆ",
+        }
+        .into()
     }
 
     fn update_matches(
@@ -118,74 +187,76 @@ impl PickerDelegate for ToolPickerDelegate {
         window: &mut Window,
         cx: &mut Context<Picker<Self>>,
     ) -> Task<()> {
-        let background = cx.background_executor().clone();
-        let candidates = self
-            .tools
-            .iter()
-            .enumerate()
-            .map(|(id, profile)| StringMatchCandidate::new(id, profile.name.as_ref()))
-            .collect::<Vec<_>>();
+        let all_items = self.items.clone();
 
         cx.spawn_in(window, async move |this, cx| {
-            let matches = if query.is_empty() {
-                candidates
-                    .into_iter()
-                    .enumerate()
-                    .map(|(index, candidate)| StringMatch {
-                        candidate_id: index,
-                        string: candidate.string,
-                        positions: Vec::new(),
-                        score: 0.,
-                    })
-                    .collect()
-            } else {
-                match_strings(
-                    &candidates,
-                    &query,
-                    false,
-                    100,
-                    &Default::default(),
-                    background,
-                )
-                .await
-            };
+            let filtered_items = cx
+                .background_spawn(async move {
+                    let mut tools_by_provider: BTreeMap<Option<Arc<str>>, Vec<Arc<str>>> =
+                        BTreeMap::default();
+
+                    for item in all_items.iter() {
+                        if let PickerItem::Tool { server_id, name } = item.clone() {
+                            if name.contains(&query) {
+                                tools_by_provider.entry(server_id).or_default().push(name);
+                            }
+                        }
+                    }
+
+                    let mut items = Vec::new();
+
+                    for (server_id, names) in tools_by_provider {
+                        if let Some(server_id) = server_id.clone() {
+                            items.push(PickerItem::ContextServer { server_id });
+                        }
+                        for name in names {
+                            items.push(PickerItem::Tool {
+                                server_id: server_id.clone(),
+                                name,
+                            });
+                        }
+                    }
+
+                    items
+                })
+                .await;
 
             this.update(cx, |this, _cx| {
-                this.delegate.matches = matches;
+                this.delegate.filtered_items = filtered_items;
                 this.delegate.selected_index = this
                     .delegate
                     .selected_index
-                    .min(this.delegate.matches.len().saturating_sub(1));
+                    .min(this.delegate.filtered_items.len().saturating_sub(1));
             })
             .log_err();
         })
     }
 
     fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
-        if self.matches.is_empty() {
+        if self.filtered_items.is_empty() {
             self.dismissed(window, cx);
             return;
         }
 
-        let candidate_id = self.matches[self.selected_index].candidate_id;
-        let tool = &self.tools[candidate_id];
+        let item = &self.filtered_items[self.selected_index];
 
-        let is_enabled = match &tool.source {
-            ToolSource::Native => {
-                let is_enabled = self.profile.tools.entry(tool.name.clone()).or_default();
-                *is_enabled = !*is_enabled;
-                *is_enabled
-            }
-            ToolSource::ContextServer { id } => {
-                let preset = self
-                    .profile
-                    .context_servers
-                    .entry(id.clone().into())
-                    .or_default();
-                let is_enabled = preset.tools.entry(tool.name.clone()).or_default();
-                *is_enabled = !*is_enabled;
-                *is_enabled
-            }
+        let PickerItem::Tool {
+            name: tool_name,
+            server_id,
+        } = item
+        else {
+            return;
+        };
+
+        let is_currently_enabled = if let Some(server_id) = server_id.clone() {
+            let preset = self.profile.context_servers.entry(server_id).or_default();
+            let is_enabled = *preset.tools.entry(tool_name.clone()).or_default();
+            *preset.tools.entry(tool_name.clone()).or_default() = !is_enabled;
+            is_enabled
+        } else {
+            let is_enabled = *self.profile.tools.entry(tool_name.clone()).or_default();
+            *self.profile.tools.entry(tool_name.clone()).or_default() = !is_enabled;
+            is_enabled
         };
 
         let active_profile_id = &AssistantSettings::get_global(cx).default_profile;
@@ -200,7 +271,8 @@ impl PickerDelegate for ToolPickerDelegate {
         update_settings_file::<AssistantSettings>(self.fs.clone(), cx, {
             let profile_id = self.profile_id.clone();
             let default_profile = self.profile.clone();
-            let tool = tool.clone();
+            let server_id = server_id.clone();
+            let tool_name = tool_name.clone();
             move |settings: &mut AssistantSettingsContent, _cx| {
                 settings
                     .v2_setting(|v2_settings| {
@@ -228,17 +300,11 @@ impl PickerDelegate for ToolPickerDelegate {
                                         .collect(),
                                 });
 
-                        match tool.source {
-                            ToolSource::Native => {
-                                *profile.tools.entry(tool.name).or_default() = is_enabled;
-                            }
-                            ToolSource::ContextServer { id } => {
-                                let preset = profile
-                                    .context_servers
-                                    .entry(id.clone().into())
-                                    .or_default();
-                                *preset.tools.entry(tool.name.clone()).or_default() = is_enabled;
-                            }
+                        if let Some(server_id) = server_id {
+                            let preset = profile.context_servers.entry(server_id).or_default();
+                            *preset.tools.entry(tool_name).or_default() = !is_currently_enabled;
+                        } else {
+                            *profile.tools.entry(tool_name).or_default() = !is_currently_enabled;
                         }
 
                         Ok(())
@@ -259,45 +325,53 @@ impl PickerDelegate for ToolPickerDelegate {
         ix: usize,
         selected: bool,
         _window: &mut Window,
-        _cx: &mut Context<Picker<Self>>,
+        cx: &mut Context<Picker<Self>>,
     ) -> Option<Self::ListItem> {
-        let tool_match = &self.matches[ix];
-        let tool = &self.tools[tool_match.candidate_id];
-
-        let is_enabled = match &tool.source {
-            ToolSource::Native => self.profile.tools.get(&tool.name).copied().unwrap_or(false),
-            ToolSource::ContextServer { id } => self
-                .profile
-                .context_servers
-                .get(id.as_ref())
-                .and_then(|preset| preset.tools.get(&tool.name))
-                .copied()
-                .unwrap_or(self.profile.enable_all_context_servers),
-        };
+        let item = &self.filtered_items[ix];
+        match item {
+            PickerItem::ContextServer { server_id, .. } => Some(
+                div()
+                    .px_2()
+                    .pb_1()
+                    .when(ix > 1, |this| {
+                        this.mt_1()
+                            .pt_2()
+                            .border_t_1()
+                            .border_color(cx.theme().colors().border_variant)
+                    })
+                    .child(
+                        Label::new(server_id)
+                            .size(LabelSize::XSmall)
+                            .color(Color::Muted),
+                    )
+                    .into_any_element(),
+            ),
+            PickerItem::Tool { name, server_id } => {
+                let is_enabled = if let Some(server_id) = server_id {
+                    self.profile
+                        .context_servers
+                        .get(server_id.as_ref())
+                        .and_then(|preset| preset.tools.get(name))
+                        .copied()
+                        .unwrap_or(self.profile.enable_all_context_servers)
+                } else {
+                    self.profile.tools.get(name).copied().unwrap_or(false)
+                };
 
-        Some(
-            ListItem::new(ix)
-                .inset(true)
-                .spacing(ListItemSpacing::Sparse)
-                .toggle_state(selected)
-                .child(
-                    h_flex()
-                        .gap_2()
-                        .child(HighlightedLabel::new(
-                            tool_match.string.clone(),
-                            tool_match.positions.clone(),
-                        ))
-                        .map(|parent| match &tool.source {
-                            ToolSource::Native => parent,
-                            ToolSource::ContextServer { id } => parent
-                                .child(Label::new(id).size(LabelSize::XSmall).color(Color::Muted)),
-                        }),
+                Some(
+                    ListItem::new(ix)
+                        .inset(true)
+                        .spacing(ListItemSpacing::Sparse)
+                        .toggle_state(selected)
+                        .child(Label::new(name.clone()))
+                        .end_slot::<Icon>(is_enabled.then(|| {
+                            Icon::new(IconName::Check)
+                                .size(IconSize::Small)
+                                .color(Color::Success)
+                        }))
+                        .into_any_element(),
                 )
-                .end_slot::<Icon>(is_enabled.then(|| {
-                    Icon::new(IconName::Check)
-                        .size(IconSize::Small)
-                        .color(Color::Success)
-                })),
-        )
+            }
+        }
     }
 }

crates/agent/src/profile_selector.rs ๐Ÿ”—

@@ -1,9 +1,10 @@
 use std::sync::Arc;
 
-use assistant_settings::{AgentProfile, AgentProfileId, AssistantSettings};
+use assistant_settings::{
+    AgentProfile, AgentProfileId, AssistantSettings, GroupedAgentProfiles, builtin_profiles,
+};
 use fs::Fs;
 use gpui::{Action, Entity, FocusHandle, Subscription, WeakEntity, prelude::*};
-use indexmap::IndexMap;
 use language_model::LanguageModelRegistry;
 use settings::{Settings as _, SettingsStore, update_settings_file};
 use ui::{
@@ -15,7 +16,7 @@ use util::ResultExt as _;
 use crate::{ManageProfiles, ThreadStore, ToggleProfileSelector};
 
 pub struct ProfileSelector {
-    profiles: IndexMap<AgentProfileId, AgentProfile>,
+    profiles: GroupedAgentProfiles,
     fs: Arc<dyn Fs>,
     thread_store: WeakEntity<ThreadStore>,
     focus_handle: FocusHandle,
@@ -34,17 +35,14 @@ impl ProfileSelector {
             this.refresh_profiles(cx);
         });
 
-        let mut this = Self {
-            profiles: IndexMap::default(),
+        Self {
+            profiles: GroupedAgentProfiles::from_settings(AssistantSettings::get_global(cx)),
             fs,
             thread_store,
             focus_handle,
             menu_handle: PopoverMenuHandle::default(),
             _subscriptions: vec![settings_subscription],
-        };
-        this.refresh_profiles(cx);
-
-        this
+        }
     }
 
     pub fn menu_handle(&self) -> PopoverMenuHandle<ContextMenu> {
@@ -52,9 +50,7 @@ impl ProfileSelector {
     }
 
     fn refresh_profiles(&mut self, cx: &mut Context<Self>) {
-        let settings = AssistantSettings::get_global(cx);
-
-        self.profiles = settings.profiles.clone();
+        self.profiles = GroupedAgentProfiles::from_settings(AssistantSettings::get_global(cx));
     }
 
     fn build_context_menu(
@@ -64,58 +60,21 @@ impl ProfileSelector {
     ) -> Entity<ContextMenu> {
         ContextMenu::build(window, cx, |mut menu, _window, cx| {
             let settings = AssistantSettings::get_global(cx);
-            let icon_position = IconPosition::End;
-
-            menu = menu.header("Profiles");
-            for (profile_id, profile) in self.profiles.clone() {
-                let documentation = match profile.name.to_lowercase().as_str() {
-                    "write" => Some("Get help to write anything."),
-                    "ask" => Some("Chat about your codebase."),
-                    "manual" => Some("Chat about anything; no tools."),
-                    _ => None,
-                };
-
-                let entry = ContextMenuEntry::new(profile.name.clone())
-                    .toggleable(icon_position, profile_id == settings.default_profile);
-
-                let entry = if let Some(doc_text) = documentation {
-                    entry.documentation_aside(move |_| Label::new(doc_text).into_any_element())
-                } else {
-                    entry
-                };
-
-                menu = menu.item(entry.handler({
-                    let fs = self.fs.clone();
-                    let thread_store = self.thread_store.clone();
-                    let profile_id = profile_id.clone();
-                    move |_window, cx| {
-                        update_settings_file::<AssistantSettings>(fs.clone(), cx, {
-                            let profile_id = profile_id.clone();
-                            move |settings, _cx| {
-                                settings.set_profile(profile_id.clone());
-                            }
-                        });
-
-                        thread_store
-                            .update(cx, |this, cx| {
-                                this.load_profile_by_id(profile_id.clone(), cx);
-                            })
-                            .log_err();
-                    }
-                }));
+            for (profile_id, profile) in self.profiles.builtin.iter() {
+                menu =
+                    menu.item(self.menu_entry_for_profile(profile_id.clone(), profile, settings));
             }
 
-            menu = menu.separator();
-            menu = menu.header("Customize Current Profile");
-            menu = menu.item(ContextMenuEntry::new("Toolsโ€ฆ").handler({
-                let profile_id = settings.default_profile.clone();
-                move |window, cx| {
-                    window.dispatch_action(
-                        ManageProfiles::customize_tools(profile_id.clone()).boxed_clone(),
-                        cx,
-                    );
+            if !self.profiles.custom.is_empty() {
+                menu = menu.separator().header("Custom Profiles");
+                for (profile_id, profile) in self.profiles.custom.iter() {
+                    menu = menu.item(self.menu_entry_for_profile(
+                        profile_id.clone(),
+                        profile,
+                        settings,
+                    ));
                 }
-            }));
+            }
 
             menu = menu.separator();
             menu = menu.item(ContextMenuEntry::new("Configure Profilesโ€ฆ").handler(
@@ -127,6 +86,49 @@ impl ProfileSelector {
             menu
         })
     }
+
+    fn menu_entry_for_profile(
+        &self,
+        profile_id: AgentProfileId,
+        profile: &AgentProfile,
+        settings: &AssistantSettings,
+    ) -> ContextMenuEntry {
+        let documentation = match profile.name.to_lowercase().as_str() {
+            builtin_profiles::WRITE => Some("Get help to write anything."),
+            builtin_profiles::ASK => Some("Chat about your codebase."),
+            builtin_profiles::MANUAL => Some("Chat about anything with no tools."),
+            _ => None,
+        };
+
+        let entry = ContextMenuEntry::new(profile.name.clone())
+            .toggleable(IconPosition::End, profile_id == settings.default_profile);
+
+        let entry = if let Some(doc_text) = documentation {
+            entry.documentation_aside(move |_| Label::new(doc_text).into_any_element())
+        } else {
+            entry
+        };
+
+        entry.handler({
+            let fs = self.fs.clone();
+            let thread_store = self.thread_store.clone();
+            let profile_id = profile_id.clone();
+            move |_window, cx| {
+                update_settings_file::<AssistantSettings>(fs.clone(), cx, {
+                    let profile_id = profile_id.clone();
+                    move |settings, _cx| {
+                        settings.set_profile(profile_id.clone());
+                    }
+                });
+
+                thread_store
+                    .update(cx, |this, cx| {
+                        this.load_profile_by_id(profile_id.clone(), cx);
+                    })
+                    .log_err();
+            }
+        })
+    }
 }
 
 impl Render for ProfileSelector {
@@ -145,8 +147,9 @@ impl Render for ProfileSelector {
             .map_or(false, |default| default.model.supports_tools());
 
         let icon = match profile_id.as_str() {
-            "write" => IconName::Pencil,
-            "ask" => IconName::MessageBubbles,
+            builtin_profiles::WRITE => IconName::Pencil,
+            builtin_profiles::ASK => IconName::MessageBubbles,
+            builtin_profiles::MANUAL => IconName::MessageBubbleDashed,
             _ => IconName::UserRoundPen,
         };
 

crates/assistant_settings/src/agent_profile.rs ๐Ÿ”—

@@ -5,6 +5,41 @@ use indexmap::IndexMap;
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 
+pub mod builtin_profiles {
+    use super::AgentProfileId;
+
+    pub const WRITE: &str = "write";
+    pub const ASK: &str = "ask";
+    pub const MANUAL: &str = "manual";
+
+    pub fn is_builtin(profile_id: &AgentProfileId) -> bool {
+        profile_id.as_str() == WRITE || profile_id.as_str() == ASK || profile_id.as_str() == MANUAL
+    }
+}
+
+#[derive(Default)]
+pub struct GroupedAgentProfiles {
+    pub builtin: IndexMap<AgentProfileId, AgentProfile>,
+    pub custom: IndexMap<AgentProfileId, AgentProfile>,
+}
+
+impl GroupedAgentProfiles {
+    pub fn from_settings(settings: &crate::AssistantSettings) -> Self {
+        let mut builtin = IndexMap::default();
+        let mut custom = IndexMap::default();
+
+        for (profile_id, profile) in settings.profiles.clone() {
+            if builtin_profiles::is_builtin(&profile_id) {
+                builtin.insert(profile_id, profile);
+            } else {
+                custom.insert(profile_id, profile);
+            }
+        }
+
+        Self { builtin, custom }
+    }
+}
+
 #[derive(Debug, PartialEq, Eq, Hash, Clone, Serialize, Deserialize, JsonSchema)]
 pub struct AgentProfileId(pub Arc<str>);
 

crates/icons/src/icons.rs ๐Ÿ”—

@@ -158,6 +158,7 @@ pub enum IconName {
     Maximize,
     Menu,
     MenuAlt,
+    MessageBubbleDashed,
     MessageBubbles,
     Mic,
     MicMute,
@@ -194,6 +195,7 @@ pub enum IconName {
     RotateCw,
     Route,
     Save,
+    Scissors,
     Screen,
     SearchCode,
     SearchSelection,

crates/ui/src/components/navigable.rs ๐Ÿ”—

@@ -19,14 +19,14 @@ pub struct NavigableEntry {
 
 impl NavigableEntry {
     /// Creates a new [NavigableEntry] for a given scroll handle.
-    pub fn new(scroll_handle: &ScrollHandle, cx: &mut App) -> Self {
+    pub fn new(scroll_handle: &ScrollHandle, cx: &App) -> Self {
         Self {
             focus_handle: cx.focus_handle(),
             scroll_anchor: Some(ScrollAnchor::for_handle(scroll_handle.clone())),
         }
     }
     /// Create a new [NavigableEntry] that cannot be scrolled to.
-    pub fn focusable(cx: &mut App) -> Self {
+    pub fn focusable(cx: &App) -> Self {
         Self {
             focus_handle: cx.focus_handle(),
             scroll_anchor: None,