use std::sync::Arc; use anyhow::{Result, bail};use collections::IndexMap;use convert_case::{Case, Casing as _};use fs::Fs;use gpui::{App, SharedString};use settings::{ AgentProfileContent, ContextServerPresetContent, LanguageModelSelection, Settings as _, SettingsContent, SettingsStore, update_settings_file,};use util::ResultExt as _; use crate::{AgentProfileId, AgentSettings}; pub mod builtin_profiles { use super::AgentProfileId; pub const WRITE: &str = "write"; pub const ASK: &str = "ask"; pub const MINIMAL: &str = "minimal"; pub fn is_builtin(profile_id: &AgentProfileId) -> bool { profile_id.as_str() == WRITE || profile_id.as_str() == ASK || profile_id.as_str() == MINIMAL }} #[derive(Clone, Debug, Eq, PartialEq)]pub struct AgentProfile { id: AgentProfileId,} pub type AvailableProfiles = IndexMap<AgentProfileId, SharedString>; impl AgentProfile { pub fn new(id: AgentProfileId) -> Self { Self { id } } pub fn id(&self) -> &AgentProfileId { &self.id } /// Saves a new profile to the settings. pub fn create( name: String, base_profile_id: Option<AgentProfileId>, fs: Arc<dyn Fs>, cx: &App, ) -> AgentProfileId { let id = AgentProfileId(name.to_case(Case::Kebab).into()); let base_profile = base_profile_id.and_then(|id| AgentSettings::get_global(cx).profiles.get(&id).cloned()); // Copy toggles from the base profile so the new profile starts with familiar defaults. let tools = base_profile .as_ref() .map(|profile| profile.tools.clone()) .unwrap_or_default(); let enable_all_context_servers = base_profile .as_ref() .map(|profile| profile.enable_all_context_servers) .unwrap_or_default(); let context_servers = base_profile .as_ref() .map(|profile| profile.context_servers.clone()) .unwrap_or_default(); // Preserve the base profile's model preference when cloning into a new profile. let default_model = base_profile .as_ref() .and_then(|profile| profile.default_model.clone()); let profile_settings = AgentProfileSettings { name: name.into(), tools, enable_all_context_servers, context_servers, default_model, }; update_settings_file(fs, cx, { let id = id.clone(); move |settings, _cx| { profile_settings.save_to_settings(id, settings).log_err(); } }); id } /// Returns a map of AgentProfileIds to their names pub fn available_profiles(cx: &App) -> AvailableProfiles { let mut profiles = AvailableProfiles::default(); for (id, profile) in AgentSettings::get_global(cx).profiles.iter() { profiles.insert(id.clone(), profile.name.clone()); } profiles }} /// A profile for the Zed Agent that controls its behavior.#[derive(Debug, Clone)]pub struct AgentProfileSettings { /// The name of the profile. pub name: SharedString, pub tools: IndexMap<Arc<str>, bool>, pub enable_all_context_servers: bool, pub context_servers: IndexMap<Arc<str>, ContextServerPreset>, /// Default language model to apply when this profile becomes active. pub default_model: Option<LanguageModelSelection>,} impl AgentProfileSettings { pub fn is_tool_enabled(&self, tool_name: &str) -> bool { self.tools.get(tool_name) == Some(&true) } /// Whether the built-in profile with the given id still matches the shipped /// default — i.e. the user has neither customized the built-in profile nor /// shadowed it with a custom profile of the same id. Custom profile ids are /// never considered unmodified defaults. pub fn is_unmodified_default(profile_id: &AgentProfileId, cx: &App) -> bool { if !builtin_profiles::is_builtin(profile_id) { return false; } let store = cx.global::<SettingsStore>(); let profile_in = |content: &SettingsContent| { content .agent .as_ref() .and_then(|agent| agent.profiles.as_ref()) .and_then(|profiles| profiles.get(profile_id.as_str())) .cloned() }; match ( profile_in(store.merged_settings()), profile_in(store.raw_default_settings()), ) { (Some(merged), Some(default)) => merged == default, _ => false, } } pub fn is_context_server_tool_enabled(&self, server_id: &str, tool_name: &str) -> bool { self.context_servers .get(server_id) .and_then(|preset| preset.tools.get(tool_name).copied()) .unwrap_or(self.enable_all_context_servers) } pub fn save_to_settings( &self, profile_id: AgentProfileId, content: &mut SettingsContent, ) -> Result<()> { let profiles = content .agent .get_or_insert_default() .profiles .get_or_insert_default(); if profiles.contains_key(&profile_id.0) { bail!("profile with ID '{profile_id}' already exists"); } profiles.insert( profile_id.0, AgentProfileContent { name: self.name.clone().into(), tools: self.tools.clone(), enable_all_context_servers: Some(self.enable_all_context_servers), context_servers: self .context_servers .clone() .into_iter() .map(|(server_id, preset)| { ( server_id, ContextServerPresetContent { tools: preset.tools, }, ) }) .collect(), default_model: self.default_model.clone(), }, ); Ok(()) }} impl From<AgentProfileContent> for AgentProfileSettings { fn from(content: AgentProfileContent) -> Self { let AgentProfileContent { name, tools, enable_all_context_servers, context_servers, default_model, } = content; Self { name: name.into(), tools, enable_all_context_servers: enable_all_context_servers.unwrap_or_default(), context_servers: context_servers .into_iter() .map(|(server_id, preset)| (server_id, preset.into())) .collect(), default_model, } }} #[derive(Debug, Clone, Default)]pub struct ContextServerPreset { pub tools: IndexMap<Arc<str>, bool>,} impl From<settings::ContextServerPresetContent> for ContextServerPreset { fn from(content: settings::ContextServerPresetContent) -> Self { Self { tools: content.tools, } }} #[cfg(test)]mod tests { use super::*; fn profile( enable_all_context_servers: bool, context_servers: IndexMap<Arc<str>, ContextServerPreset>, ) -> AgentProfileSettings { AgentProfileSettings { name: "test".into(), tools: IndexMap::default(), enable_all_context_servers, context_servers, default_model: None, } } fn preset(tools: &[(&str, bool)]) -> ContextServerPreset { ContextServerPreset { tools: tools .iter() .map(|(name, enabled)| (Arc::from(*name), *enabled)) .collect(), } } #[test] fn explicit_false_disables_tool_when_enable_all_is_true() { let mut servers = IndexMap::default(); servers.insert(Arc::from("server"), preset(&[("disabled_tool", false)])); let profile = profile(true, servers); assert!(!profile.is_context_server_tool_enabled("server", "disabled_tool")); assert!(profile.is_context_server_tool_enabled("server", "other_tool")); assert!(profile.is_context_server_tool_enabled("other_server", "any_tool")); } #[test] fn explicit_true_enables_tool_when_enable_all_is_false() { let mut servers = IndexMap::default(); servers.insert(Arc::from("server"), preset(&[("enabled_tool", true)])); let profile = profile(false, servers); assert!(profile.is_context_server_tool_enabled("server", "enabled_tool")); assert!(!profile.is_context_server_tool_enabled("server", "other_tool")); assert!(!profile.is_context_server_tool_enabled("other_server", "any_tool")); } #[gpui::test] fn unmodified_default_detection(cx: &mut gpui::App) { use gpui::UpdateGlobal as _; let store = SettingsStore::test(cx); cx.set_global(store); project::DisableAiSettings::register(cx); AgentSettings::register(cx); let write = AgentProfileId(builtin_profiles::WRITE.into()); let minimal = AgentProfileId(builtin_profiles::MINIMAL.into()); let custom = AgentProfileId("custom".into()); // Fresh defaults: the shipped built-in profiles are unmodified. assert!(AgentProfileSettings::is_unmodified_default(&write, cx)); assert!(AgentProfileSettings::is_unmodified_default(&minimal, cx)); // Custom (non-built-in) ids are never considered unmodified defaults. assert!(!AgentProfileSettings::is_unmodified_default(&custom, cx)); // The user customizes the `write` profile; `minimal` stays untouched. SettingsStore::update_global(cx, |store, cx| { store .set_user_settings( r#"{ "agent": { "profiles": { "write": { "name": "Write", "tools": { "fetch": false } } } } }"#, cx, ) .unwrap(); }); assert!(!AgentProfileSettings::is_unmodified_default(&write, cx)); assert!(AgentProfileSettings::is_unmodified_default(&minimal, cx)); }}
use std::sync::Arc;
use anyhow::{Result, bail};
use collections::IndexMap;
use convert_case::{Case, Casing as _};
use fs::Fs;
use gpui::{App, SharedString};
use settings::{
AgentProfileContent, ContextServerPresetContent, LanguageModelSelection, Settings as _,
SettingsContent, SettingsStore, update_settings_file,
};
use util::ResultExt as _;
use crate::{AgentProfileId, AgentSettings};
pub mod builtin_profiles {
use super::AgentProfileId;
pub const WRITE: &str = "write";
pub const ASK: &str = "ask";
pub const MINIMAL: &str = "minimal";
pub fn is_builtin(profile_id: &AgentProfileId) -> bool {
profile_id.as_str() == WRITE || profile_id.as_str() == ASK || profile_id.as_str() == MINIMAL
}
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct AgentProfile {
id: AgentProfileId,
}
pub type AvailableProfiles = IndexMap<AgentProfileId, SharedString>;
impl AgentProfile {
pub fn new(id: AgentProfileId) -> Self {
Self { id }
}
pub fn id(&self) -> &AgentProfileId {
&self.id
}
/// Saves a new profile to the settings.
pub fn create(
name: String,
base_profile_id: Option<AgentProfileId>,
fs: Arc<dyn Fs>,
cx: &App,
) -> AgentProfileId {
let id = AgentProfileId(name.to_case(Case::Kebab).into());
let base_profile =
base_profile_id.and_then(|id| AgentSettings::get_global(cx).profiles.get(&id).cloned());
// Copy toggles from the base profile so the new profile starts with familiar defaults.
let tools = base_profile
.as_ref()
.map(|profile| profile.tools.clone())
.unwrap_or_default();
let enable_all_context_servers = base_profile
.as_ref()
.map(|profile| profile.enable_all_context_servers)
.unwrap_or_default();
let context_servers = base_profile
.as_ref()
.map(|profile| profile.context_servers.clone())
.unwrap_or_default();
// Preserve the base profile's model preference when cloning into a new profile.
let default_model = base_profile
.as_ref()
.and_then(|profile| profile.default_model.clone());
let profile_settings = AgentProfileSettings {
name: name.into(),
tools,
enable_all_context_servers,
context_servers,
default_model,
};
update_settings_file(fs, cx, {
let id = id.clone();
move |settings, _cx| {
profile_settings.save_to_settings(id, settings).log_err();
}
});
id
}
/// Returns a map of AgentProfileIds to their names
pub fn available_profiles(cx: &App) -> AvailableProfiles {
let mut profiles = AvailableProfiles::default();
for (id, profile) in AgentSettings::get_global(cx).profiles.iter() {
profiles.insert(id.clone(), profile.name.clone());
}
profiles
}
}
/// A profile for the Zed Agent that controls its behavior.
#[derive(Debug, Clone)]
pub struct AgentProfileSettings {
/// The name of the profile.
pub name: SharedString,
pub tools: IndexMap<Arc<str>, bool>,
pub enable_all_context_servers: bool,
pub context_servers: IndexMap<Arc<str>, ContextServerPreset>,
/// Default language model to apply when this profile becomes active.
pub default_model: Option<LanguageModelSelection>,
}
impl AgentProfileSettings {
pub fn is_tool_enabled(&self, tool_name: &str) -> bool {
self.tools.get(tool_name) == Some(&true)
}
/// Whether the built-in profile with the given id still matches the shipped
/// default — i.e. the user has neither customized the built-in profile nor
/// shadowed it with a custom profile of the same id. Custom profile ids are
/// never considered unmodified defaults.
pub fn is_unmodified_default(profile_id: &AgentProfileId, cx: &App) -> bool {
if !builtin_profiles::is_builtin(profile_id) {
return false;
}
let store = cx.global::<SettingsStore>();
let profile_in = |content: &SettingsContent| {
content
.agent
.as_ref()
.and_then(|agent| agent.profiles.as_ref())
.and_then(|profiles| profiles.get(profile_id.as_str()))
.cloned()
};
match (
profile_in(store.merged_settings()),
profile_in(store.raw_default_settings()),
) {
(Some(merged), Some(default)) => merged == default,
_ => false,
}
}
pub fn is_context_server_tool_enabled(&self, server_id: &str, tool_name: &str) -> bool {
self.context_servers
.get(server_id)
.and_then(|preset| preset.tools.get(tool_name).copied())
.unwrap_or(self.enable_all_context_servers)
}
pub fn save_to_settings(
&self,
profile_id: AgentProfileId,
content: &mut SettingsContent,
) -> Result<()> {
let profiles = content
.agent
.get_or_insert_default()
.profiles
.get_or_insert_default();
if profiles.contains_key(&profile_id.0) {
bail!("profile with ID '{profile_id}' already exists");
}
profiles.insert(
profile_id.0,
AgentProfileContent {
name: self.name.clone().into(),
tools: self.tools.clone(),
enable_all_context_servers: Some(self.enable_all_context_servers),
context_servers: self
.context_servers
.clone()
.into_iter()
.map(|(server_id, preset)| {
(
server_id,
ContextServerPresetContent {
tools: preset.tools,
},
)
})
.collect(),
default_model: self.default_model.clone(),
},
);
Ok(())
}
}
impl From<AgentProfileContent> for AgentProfileSettings {
fn from(content: AgentProfileContent) -> Self {
let AgentProfileContent {
name,
tools,
enable_all_context_servers,
context_servers,
default_model,
} = content;
Self {
name: name.into(),
tools,
enable_all_context_servers: enable_all_context_servers.unwrap_or_default(),
context_servers: context_servers
.into_iter()
.map(|(server_id, preset)| (server_id, preset.into()))
.collect(),
default_model,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ContextServerPreset {
pub tools: IndexMap<Arc<str>, bool>,
}
impl From<settings::ContextServerPresetContent> for ContextServerPreset {
fn from(content: settings::ContextServerPresetContent) -> Self {
Self {
tools: content.tools,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn profile(
enable_all_context_servers: bool,
context_servers: IndexMap<Arc<str>, ContextServerPreset>,
) -> AgentProfileSettings {
AgentProfileSettings {
name: "test".into(),
tools: IndexMap::default(),
enable_all_context_servers,
context_servers,
default_model: None,
}
}
fn preset(tools: &[(&str, bool)]) -> ContextServerPreset {
ContextServerPreset {
tools: tools
.iter()
.map(|(name, enabled)| (Arc::from(*name), *enabled))
.collect(),
}
}
#[test]
fn explicit_false_disables_tool_when_enable_all_is_true() {
let mut servers = IndexMap::default();
servers.insert(Arc::from("server"), preset(&[("disabled_tool", false)]));
let profile = profile(true, servers);
assert!(!profile.is_context_server_tool_enabled("server", "disabled_tool"));
assert!(profile.is_context_server_tool_enabled("server", "other_tool"));
assert!(profile.is_context_server_tool_enabled("other_server", "any_tool"));
}
#[test]
fn explicit_true_enables_tool_when_enable_all_is_false() {
let mut servers = IndexMap::default();
servers.insert(Arc::from("server"), preset(&[("enabled_tool", true)]));
let profile = profile(false, servers);
assert!(profile.is_context_server_tool_enabled("server", "enabled_tool"));
assert!(!profile.is_context_server_tool_enabled("server", "other_tool"));
assert!(!profile.is_context_server_tool_enabled("other_server", "any_tool"));
}
#[gpui::test]
fn unmodified_default_detection(cx: &mut gpui::App) {
use gpui::UpdateGlobal as _;
let store = SettingsStore::test(cx);
cx.set_global(store);
project::DisableAiSettings::register(cx);
AgentSettings::register(cx);
let write = AgentProfileId(builtin_profiles::WRITE.into());
let minimal = AgentProfileId(builtin_profiles::MINIMAL.into());
let custom = AgentProfileId("custom".into());
// Fresh defaults: the shipped built-in profiles are unmodified.
assert!(AgentProfileSettings::is_unmodified_default(&write, cx));
assert!(AgentProfileSettings::is_unmodified_default(&minimal, cx));
// Custom (non-built-in) ids are never considered unmodified defaults.
assert!(!AgentProfileSettings::is_unmodified_default(&custom, cx));
// The user customizes the `write` profile; `minimal` stays untouched.
SettingsStore::update_global(cx, |store, cx| {
store
.set_user_settings(
r#"{ "agent": { "profiles": { "write": { "name": "Write", "tools": { "fetch": false } } } } }"#,
cx,
)
.unwrap();
});
assert!(!AgentProfileSettings::is_unmodified_default(&write, cx));
assert!(AgentProfileSettings::is_unmodified_default(&minimal, cx));
}
}