From 3c81ee6ba647f1ae7b172c0a259ce95e7dbd4af3 Mon Sep 17 00:00:00 2001
From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com>
Date: Mon, 10 Nov 2025 13:12:13 -0300
Subject: [PATCH] agent_ui: Allow to configure a default model for profiles
through modal (#42359)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
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 |
|--------|--------|
|
|
|
Release Notes:
- N/A
---
.../manage_profiles_modal.rs | 161 +++++++++++++++++-
crates/agent_ui/src/agent_model_selector.rs | 1 +
.../agent_ui/src/language_model_selector.rs | 29 +++-
crates/agent_ui/src/text_thread_editor.rs | 1 +
4 files changed, 185 insertions(+), 7 deletions(-)
diff --git a/crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs b/crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs
index e583bb7d5425ec4c6f233ac0eed67c358ccac98d..210cf5f5dd6612855b32e358a2d3ec38e8259373 100644
--- a/crates/agent_ui/src/agent_configuration/manage_profiles_modal.rs
+++ b/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,
_subscription: Subscription,
},
+ ConfigureDefaultModel {
+ profile_id: AgentProfileId,
+ model_picker: Entity,
+ _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,
+ ) {
+ 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,
diff --git a/crates/agent_ui/src/agent_model_selector.rs b/crates/agent_ui/src/agent_model_selector.rs
index df7d166064da20aa4bc958ebd6a9df806164eb7a..900ca0b683670a30b3353655d17c2ef79cd5523b 100644
--- a/crates/agent_ui/src/agent_model_selector.rs
+++ b/crates/agent_ui/src/agent_model_selector.rs
@@ -47,6 +47,7 @@ impl AgentModelSelector {
}
}
},
+ true, // Use popover styles for picker
window,
cx,
)
diff --git a/crates/agent_ui/src/language_model_selector.rs b/crates/agent_ui/src/language_model_selector.rs
index c8838fd9bc8b33ccb87f3198126c5c470328c810..0f7b83e3edba6c8d97c2c12a939a65cb71c39dca 100644
--- a/crates/agent_ui/src/language_model_selector.rs
+++ b/crates/agent_ui/src/language_model_selector.rs
@@ -19,14 +19,26 @@ pub type LanguageModelSelector = Picker;
pub fn language_model_selector(
get_active_model: impl Fn(&App) -> Option + 'static,
on_model_changed: impl Fn(Arc, &mut App) + 'static,
+ popover_styles: bool,
window: &mut Window,
cx: &mut Context,
) -> 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,
+ popover_styles: bool,
}
impl LanguageModelPickerDelegate {
fn new(
get_active_model: impl Fn(&App) -> Option + 'static,
on_model_changed: impl Fn(Arc, &mut App) + 'static,
+ popover_styles: bool,
window: &mut Window,
cx: &mut Context>,
) -> Self {
@@ -113,6 +127,7 @@ impl LanguageModelPickerDelegate {
}
},
)],
+ popover_styles,
}
}
@@ -530,6 +545,10 @@ impl PickerDelegate for LanguageModelPickerDelegate {
_window: &mut Window,
cx: &mut Context>,
) -> Option {
+ if !self.popover_styles {
+ return None;
+ }
+
Some(
h_flex()
.w_full()
diff --git a/crates/agent_ui/src/text_thread_editor.rs b/crates/agent_ui/src/text_thread_editor.rs
index be77ec05fe3afbecab5373bd97ec23ee77dd614a..19063075f9cf7382270c4dbaf4930596a7592676 100644
--- a/crates/agent_ui/src/text_thread_editor.rs
+++ b/crates/agent_ui/src/text_thread_editor.rs
@@ -314,6 +314,7 @@ impl TextThreadEditor {
)
});
},
+ true, // Use popover styles for picker
window,
cx,
)