agent_ui: Allow to configure a default model for profiles through modal (#42359)

Danilo Leal created

Follow-up to https://github.com/zed-industries/zed/pull/39220

This PR allows to configure a default model for a given profile through
the profile management modal.

| Option In Picker | Model Selector |
|--------|--------|
| <img width="1172" height="538" alt="Screenshot 2025-11-10 at 12  24
2@2x"
src="https://github.com/user-attachments/assets/33dfb6f1-f8fd-42f9-b824-3dab807094da"
/> | <img width="1172" height="1120" alt="Screenshot 2025-11-10 at 12 
24@2x"
src="https://github.com/user-attachments/assets/50360b0a-fbb1-455e-9cf7-9fa987345038"
/> |

Release Notes:

- N/A

Change summary

crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs | 161 +
crates/agent_ui/src/agent_model_selector.rs                      |   1 
crates/agent_ui/src/language_model_selector.rs                   |  29 
crates/agent_ui/src/text_thread_editor.rs                        |   1 
4 files changed, 185 insertions(+), 7 deletions(-)

Detailed changes

crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs 🔗

@@ -7,8 +7,10 @@ use agent_settings::{AgentProfile, AgentProfileId, AgentSettings, builtin_profil
 use editor::Editor;
 use fs::Fs;
 use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Subscription, prelude::*};
-use language_model::LanguageModel;
-use settings::Settings as _;
+use language_model::{LanguageModel, LanguageModelRegistry};
+use settings::{
+    LanguageModelProviderSetting, LanguageModelSelection, Settings as _, update_settings_file,
+};
 use ui::{
     KeyBinding, ListItem, ListItemSpacing, ListSeparator, Navigable, NavigableEntry, prelude::*,
 };
@@ -16,6 +18,7 @@ use workspace::{ModalView, Workspace};
 
 use crate::agent_configuration::manage_profiles_modal::profile_modal_header::ProfileModalHeader;
 use crate::agent_configuration::tool_picker::{ToolPicker, ToolPickerDelegate};
+use crate::language_model_selector::{LanguageModelSelector, language_model_selector};
 use crate::{AgentPanel, ManageProfiles};
 
 enum Mode {
@@ -32,6 +35,11 @@ enum Mode {
         tool_picker: Entity<ToolPicker>,
         _subscription: Subscription,
     },
+    ConfigureDefaultModel {
+        profile_id: AgentProfileId,
+        model_picker: Entity<LanguageModelSelector>,
+        _subscription: Subscription,
+    },
 }
 
 impl Mode {
@@ -83,6 +91,7 @@ pub struct ChooseProfileMode {
 pub struct ViewProfileMode {
     profile_id: AgentProfileId,
     fork_profile: NavigableEntry,
+    configure_default_model: NavigableEntry,
     configure_tools: NavigableEntry,
     configure_mcps: NavigableEntry,
     cancel_item: NavigableEntry,
@@ -180,6 +189,7 @@ impl ManageProfilesModal {
         self.mode = Mode::ViewProfile(ViewProfileMode {
             profile_id,
             fork_profile: NavigableEntry::focusable(cx),
+            configure_default_model: NavigableEntry::focusable(cx),
             configure_tools: NavigableEntry::focusable(cx),
             configure_mcps: NavigableEntry::focusable(cx),
             cancel_item: NavigableEntry::focusable(cx),
@@ -187,6 +197,83 @@ impl ManageProfilesModal {
         self.focus_handle(cx).focus(window);
     }
 
+    fn configure_default_model(
+        &mut self,
+        profile_id: AgentProfileId,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let fs = self.fs.clone();
+        let profile_id_for_closure = profile_id.clone();
+
+        let model_picker = cx.new(|cx| {
+            let fs = fs.clone();
+            let profile_id = profile_id_for_closure.clone();
+
+            language_model_selector(
+                {
+                    let profile_id = profile_id.clone();
+                    move |cx| {
+                        let settings = AgentSettings::get_global(cx);
+
+                        settings
+                            .profiles
+                            .get(&profile_id)
+                            .and_then(|profile| profile.default_model.as_ref())
+                            .and_then(|selection| {
+                                let registry = LanguageModelRegistry::read_global(cx);
+                                let provider_id = language_model::LanguageModelProviderId(
+                                    gpui::SharedString::from(selection.provider.0.clone()),
+                                );
+                                let provider = registry.provider(&provider_id)?;
+                                let model = provider
+                                    .provided_models(cx)
+                                    .iter()
+                                    .find(|m| m.id().0 == selection.model.as_str())?
+                                    .clone();
+                                Some(language_model::ConfiguredModel { provider, model })
+                            })
+                    }
+                },
+                move |model, cx| {
+                    let provider = model.provider_id().0.to_string();
+                    let model_id = model.id().0.to_string();
+                    let profile_id = profile_id.clone();
+
+                    update_settings_file(fs.clone(), cx, move |settings, _cx| {
+                        let agent_settings = settings.agent.get_or_insert_default();
+                        if let Some(profiles) = agent_settings.profiles.as_mut() {
+                            if let Some(profile) = profiles.get_mut(profile_id.0.as_ref()) {
+                                profile.default_model = Some(LanguageModelSelection {
+                                    provider: LanguageModelProviderSetting(provider.clone()),
+                                    model: model_id.clone(),
+                                });
+                            }
+                        }
+                    });
+                },
+                false, // Do not use popover styles for the model picker
+                window,
+                cx,
+            )
+            .modal(false)
+        });
+
+        let dismiss_subscription = cx.subscribe_in(&model_picker, window, {
+            let profile_id = profile_id.clone();
+            move |this, _picker, _: &DismissEvent, window, cx| {
+                this.view_profile(profile_id.clone(), window, cx);
+            }
+        });
+
+        self.mode = Mode::ConfigureDefaultModel {
+            profile_id,
+            model_picker,
+            _subscription: dismiss_subscription,
+        };
+        self.focus_handle(cx).focus(window);
+    }
+
     fn configure_mcp_tools(
         &mut self,
         profile_id: AgentProfileId,
@@ -277,6 +364,7 @@ impl ManageProfilesModal {
             Mode::ViewProfile(_) => {}
             Mode::ConfigureTools { .. } => {}
             Mode::ConfigureMcps { .. } => {}
+            Mode::ConfigureDefaultModel { .. } => {}
         }
     }
 
@@ -299,6 +387,9 @@ impl ManageProfilesModal {
             Mode::ConfigureMcps { profile_id, .. } => {
                 self.view_profile(profile_id.clone(), window, cx)
             }
+            Mode::ConfigureDefaultModel { profile_id, .. } => {
+                self.view_profile(profile_id.clone(), window, cx)
+            }
         }
     }
 }
@@ -313,6 +404,7 @@ impl Focusable for ManageProfilesModal {
             Mode::ViewProfile(_) => self.focus_handle.clone(),
             Mode::ConfigureTools { tool_picker, .. } => tool_picker.focus_handle(cx),
             Mode::ConfigureMcps { tool_picker, .. } => tool_picker.focus_handle(cx),
+            Mode::ConfigureDefaultModel { model_picker, .. } => model_picker.focus_handle(cx),
         }
     }
 }
@@ -544,6 +636,47 @@ impl ManageProfilesModal {
                                         }),
                                 ),
                         )
+                        .child(
+                            div()
+                                .id("configure-default-model")
+                                .track_focus(&mode.configure_default_model.focus_handle)
+                                .on_action({
+                                    let profile_id = mode.profile_id.clone();
+                                    cx.listener(move |this, _: &menu::Confirm, window, cx| {
+                                        this.configure_default_model(
+                                            profile_id.clone(),
+                                            window,
+                                            cx,
+                                        );
+                                    })
+                                })
+                                .child(
+                                    ListItem::new("model-item")
+                                        .toggle_state(
+                                            mode.configure_default_model
+                                                .focus_handle
+                                                .contains_focused(window, cx),
+                                        )
+                                        .inset(true)
+                                        .spacing(ListItemSpacing::Sparse)
+                                        .start_slot(
+                                            Icon::new(IconName::ZedAssistant)
+                                                .size(IconSize::Small)
+                                                .color(Color::Muted),
+                                        )
+                                        .child(Label::new("Configure Default Model"))
+                                        .on_click({
+                                            let profile_id = mode.profile_id.clone();
+                                            cx.listener(move |this, _, window, cx| {
+                                                this.configure_default_model(
+                                                    profile_id.clone(),
+                                                    window,
+                                                    cx,
+                                                );
+                                            })
+                                        }),
+                                ),
+                        )
                         .child(
                             div()
                                 .id("configure-builtin-tools")
@@ -668,6 +801,7 @@ impl ManageProfilesModal {
                 .into_any_element(),
         )
         .entry(mode.fork_profile)
+        .entry(mode.configure_default_model)
         .entry(mode.configure_tools)
         .entry(mode.configure_mcps)
         .entry(mode.cancel_item)
@@ -753,6 +887,29 @@ impl Render for ManageProfilesModal {
                         .child(go_back_item)
                         .into_any_element()
                 }
+                Mode::ConfigureDefaultModel {
+                    profile_id,
+                    model_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 Default Model"),
+                            Some(IconName::Ai),
+                        ))
+                        .child(ListSeparator)
+                        .child(v_flex().w(rems(34.)).child(model_picker.clone()))
+                        .child(ListSeparator)
+                        .child(go_back_item)
+                        .into_any_element()
+                }
                 Mode::ConfigureMcps {
                     profile_id,
                     tool_picker,

crates/agent_ui/src/language_model_selector.rs 🔗

@@ -19,14 +19,26 @@ pub type LanguageModelSelector = Picker<LanguageModelPickerDelegate>;
 pub fn language_model_selector(
     get_active_model: impl Fn(&App) -> Option<ConfiguredModel> + 'static,
     on_model_changed: impl Fn(Arc<dyn LanguageModel>, &mut App) + 'static,
+    popover_styles: bool,
     window: &mut Window,
     cx: &mut Context<LanguageModelSelector>,
 ) -> LanguageModelSelector {
-    let delegate = LanguageModelPickerDelegate::new(get_active_model, on_model_changed, window, cx);
-    Picker::list(delegate, window, cx)
-        .show_scrollbar(true)
-        .width(rems(20.))
-        .max_height(Some(rems(20.).into()))
+    let delegate = LanguageModelPickerDelegate::new(
+        get_active_model,
+        on_model_changed,
+        popover_styles,
+        window,
+        cx,
+    );
+
+    if popover_styles {
+        Picker::list(delegate, window, cx)
+            .show_scrollbar(true)
+            .width(rems(20.))
+            .max_height(Some(rems(20.).into()))
+    } else {
+        Picker::list(delegate, window, cx).show_scrollbar(true)
+    }
 }
 
 fn all_models(cx: &App) -> GroupedModels {
@@ -75,12 +87,14 @@ pub struct LanguageModelPickerDelegate {
     selected_index: usize,
     _authenticate_all_providers_task: Task<()>,
     _subscriptions: Vec<Subscription>,
+    popover_styles: bool,
 }
 
 impl LanguageModelPickerDelegate {
     fn new(
         get_active_model: impl Fn(&App) -> Option<ConfiguredModel> + 'static,
         on_model_changed: impl Fn(Arc<dyn LanguageModel>, &mut App) + 'static,
+        popover_styles: bool,
         window: &mut Window,
         cx: &mut Context<Picker<Self>>,
     ) -> Self {
@@ -113,6 +127,7 @@ impl LanguageModelPickerDelegate {
                     }
                 },
             )],
+            popover_styles,
         }
     }
 
@@ -530,6 +545,10 @@ impl PickerDelegate for LanguageModelPickerDelegate {
         _window: &mut Window,
         cx: &mut Context<Picker<Self>>,
     ) -> Option<gpui::AnyElement> {
+        if !self.popover_styles {
+            return None;
+        }
+
         Some(
             h_flex()
                 .w_full()