agent_ui: Allow to configure a default model for profiles through modal (#42359)
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
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -47,6 +47,7 @@ impl AgentModelSelector {
|
||||
}
|
||||
}
|
||||
},
|
||||
true, // Use popover styles for picker
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -314,6 +314,7 @@ impl TextThreadEditor {
|
||||
)
|
||||
});
|
||||
},
|
||||
true, // Use popover styles for picker
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user