agent_settings.rs

  1mod agent_profile;
  2
  3use std::sync::Arc;
  4
  5use anyhow::{Result, bail};
  6use collections::IndexMap;
  7use gpui::{App, Pixels, SharedString};
  8use language_model::LanguageModel;
  9use schemars::{JsonSchema, schema::Schema};
 10use serde::{Deserialize, Serialize};
 11use settings::{Settings, SettingsSources};
 12
 13pub use crate::agent_profile::*;
 14
 15pub fn init(cx: &mut App) {
 16    AgentSettings::register(cx);
 17}
 18
 19#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
 20#[serde(rename_all = "snake_case")]
 21pub enum AgentDockPosition {
 22    Left,
 23    #[default]
 24    Right,
 25    Bottom,
 26}
 27
 28#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
 29#[serde(rename_all = "snake_case")]
 30pub enum DefaultView {
 31    #[default]
 32    Thread,
 33    TextThread,
 34}
 35
 36#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
 37#[serde(rename_all = "snake_case")]
 38pub enum NotifyWhenAgentWaiting {
 39    #[default]
 40    PrimaryScreen,
 41    AllScreens,
 42    Never,
 43}
 44
 45#[derive(Default, Clone, Debug)]
 46pub struct AgentSettings {
 47    pub enabled: bool,
 48    pub button: bool,
 49    pub dock: AgentDockPosition,
 50    pub default_width: Pixels,
 51    pub default_height: Pixels,
 52    pub default_model: LanguageModelSelection,
 53    pub inline_assistant_model: Option<LanguageModelSelection>,
 54    pub commit_message_model: Option<LanguageModelSelection>,
 55    pub thread_summary_model: Option<LanguageModelSelection>,
 56    pub inline_alternatives: Vec<LanguageModelSelection>,
 57    pub using_outdated_settings_version: bool,
 58    pub default_profile: AgentProfileId,
 59    pub default_view: DefaultView,
 60    pub profiles: IndexMap<AgentProfileId, AgentProfileSettings>,
 61    pub always_allow_tool_actions: bool,
 62    pub notify_when_agent_waiting: NotifyWhenAgentWaiting,
 63    pub play_sound_when_agent_done: bool,
 64    pub stream_edits: bool,
 65    pub single_file_review: bool,
 66    pub model_parameters: Vec<LanguageModelParameters>,
 67    pub preferred_completion_mode: CompletionMode,
 68    pub enable_feedback: bool,
 69}
 70
 71impl AgentSettings {
 72    pub fn temperature_for_model(model: &Arc<dyn LanguageModel>, cx: &App) -> Option<f32> {
 73        let settings = Self::get_global(cx);
 74        settings
 75            .model_parameters
 76            .iter()
 77            .rfind(|setting| setting.matches(model))
 78            .and_then(|m| m.temperature)
 79    }
 80
 81    pub fn set_inline_assistant_model(&mut self, provider: String, model: String) {
 82        self.inline_assistant_model = Some(LanguageModelSelection {
 83            provider: provider.into(),
 84            model,
 85        });
 86    }
 87
 88    pub fn set_commit_message_model(&mut self, provider: String, model: String) {
 89        self.commit_message_model = Some(LanguageModelSelection {
 90            provider: provider.into(),
 91            model,
 92        });
 93    }
 94
 95    pub fn set_thread_summary_model(&mut self, provider: String, model: String) {
 96        self.thread_summary_model = Some(LanguageModelSelection {
 97            provider: provider.into(),
 98            model,
 99        });
100    }
101}
102
103#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
104pub struct LanguageModelParameters {
105    pub provider: Option<LanguageModelProviderSetting>,
106    pub model: Option<SharedString>,
107    pub temperature: Option<f32>,
108}
109
110impl LanguageModelParameters {
111    pub fn matches(&self, model: &Arc<dyn LanguageModel>) -> bool {
112        if let Some(provider) = &self.provider {
113            if provider.0 != model.provider_id().0 {
114                return false;
115            }
116        }
117        if let Some(setting_model) = &self.model {
118            if *setting_model != model.id().0 {
119                return false;
120            }
121        }
122        true
123    }
124}
125
126impl AgentSettingsContent {
127    pub fn set_dock(&mut self, dock: AgentDockPosition) {
128        self.dock = Some(dock);
129    }
130
131    pub fn set_model(&mut self, language_model: Arc<dyn LanguageModel>) {
132        let model = language_model.id().0.to_string();
133        let provider = language_model.provider_id().0.to_string();
134
135        self.default_model = Some(LanguageModelSelection {
136            provider: provider.into(),
137            model,
138        });
139    }
140
141    pub fn set_inline_assistant_model(&mut self, provider: String, model: String) {
142        self.inline_assistant_model = Some(LanguageModelSelection {
143            provider: provider.into(),
144            model,
145        });
146    }
147
148    pub fn set_commit_message_model(&mut self, provider: String, model: String) {
149        self.commit_message_model = Some(LanguageModelSelection {
150            provider: provider.into(),
151            model,
152        });
153    }
154
155    pub fn set_thread_summary_model(&mut self, provider: String, model: String) {
156        self.thread_summary_model = Some(LanguageModelSelection {
157            provider: provider.into(),
158            model,
159        });
160    }
161
162    pub fn set_always_allow_tool_actions(&mut self, allow: bool) {
163        self.always_allow_tool_actions = Some(allow);
164    }
165
166    pub fn set_play_sound_when_agent_done(&mut self, allow: bool) {
167        self.play_sound_when_agent_done = Some(allow);
168    }
169
170    pub fn set_single_file_review(&mut self, allow: bool) {
171        self.single_file_review = Some(allow);
172    }
173
174    pub fn set_profile(&mut self, profile_id: AgentProfileId) {
175        self.default_profile = Some(profile_id);
176    }
177
178    pub fn create_profile(
179        &mut self,
180        profile_id: AgentProfileId,
181        profile_settings: AgentProfileSettings,
182    ) -> Result<()> {
183        let profiles = self.profiles.get_or_insert_default();
184        if profiles.contains_key(&profile_id) {
185            bail!("profile with ID '{profile_id}' already exists");
186        }
187
188        profiles.insert(
189            profile_id,
190            AgentProfileContent {
191                name: profile_settings.name.into(),
192                tools: profile_settings.tools,
193                enable_all_context_servers: Some(profile_settings.enable_all_context_servers),
194                context_servers: profile_settings
195                    .context_servers
196                    .into_iter()
197                    .map(|(server_id, preset)| {
198                        (
199                            server_id,
200                            ContextServerPresetContent {
201                                tools: preset.tools,
202                            },
203                        )
204                    })
205                    .collect(),
206            },
207        );
208
209        Ok(())
210    }
211}
212
213#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default)]
214#[schemars(deny_unknown_fields)]
215pub struct AgentSettingsContent {
216    /// Whether the Agent is enabled.
217    ///
218    /// Default: true
219    enabled: Option<bool>,
220    /// Whether to show the agent panel button in the status bar.
221    ///
222    /// Default: true
223    button: Option<bool>,
224    /// Where to dock the agent panel.
225    ///
226    /// Default: right
227    dock: Option<AgentDockPosition>,
228    /// Default width in pixels when the agent panel is docked to the left or right.
229    ///
230    /// Default: 640
231    default_width: Option<f32>,
232    /// Default height in pixels when the agent panel is docked to the bottom.
233    ///
234    /// Default: 320
235    default_height: Option<f32>,
236    /// The default model to use when creating new chats and for other features when a specific model is not specified.
237    default_model: Option<LanguageModelSelection>,
238    /// Model to use for the inline assistant. Defaults to default_model when not specified.
239    inline_assistant_model: Option<LanguageModelSelection>,
240    /// Model to use for generating git commit messages. Defaults to default_model when not specified.
241    commit_message_model: Option<LanguageModelSelection>,
242    /// Model to use for generating thread summaries. Defaults to default_model when not specified.
243    thread_summary_model: Option<LanguageModelSelection>,
244    /// Additional models with which to generate alternatives when performing inline assists.
245    inline_alternatives: Option<Vec<LanguageModelSelection>>,
246    /// The default profile to use in the Agent.
247    ///
248    /// Default: write
249    default_profile: Option<AgentProfileId>,
250    /// Which view type to show by default in the agent panel.
251    ///
252    /// Default: "thread"
253    default_view: Option<DefaultView>,
254    /// The available agent profiles.
255    pub profiles: Option<IndexMap<AgentProfileId, AgentProfileContent>>,
256    /// Whenever a tool action would normally wait for your confirmation
257    /// that you allow it, always choose to allow it.
258    ///
259    /// Default: false
260    always_allow_tool_actions: Option<bool>,
261    /// Where to show a popup notification when the agent is waiting for user input.
262    ///
263    /// Default: "primary_screen"
264    notify_when_agent_waiting: Option<NotifyWhenAgentWaiting>,
265    /// Whether to play a sound when the agent has either completed its response, or needs user input.
266    ///
267    /// Default: false
268    play_sound_when_agent_done: Option<bool>,
269    /// Whether to stream edits from the agent as they are received.
270    ///
271    /// Default: false
272    stream_edits: Option<bool>,
273    /// Whether to display agent edits in single-file editors in addition to the review multibuffer pane.
274    ///
275    /// Default: true
276    single_file_review: Option<bool>,
277    /// Additional parameters for language model requests. When making a request
278    /// to a model, parameters will be taken from the last entry in this list
279    /// that matches the model's provider and name. In each entry, both provider
280    /// and model are optional, so that you can specify parameters for either
281    /// one.
282    ///
283    /// Default: []
284    #[serde(default)]
285    model_parameters: Vec<LanguageModelParameters>,
286    /// What completion mode to enable for new threads
287    ///
288    /// Default: normal
289    preferred_completion_mode: Option<CompletionMode>,
290    /// Whether to show thumb buttons for feedback in the agent panel.
291    ///
292    /// Default: true
293    enable_feedback: Option<bool>,
294}
295
296#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)]
297#[serde(rename_all = "snake_case")]
298pub enum CompletionMode {
299    #[default]
300    Normal,
301    #[serde(alias = "max")]
302    Burn,
303}
304
305impl From<CompletionMode> for zed_llm_client::CompletionMode {
306    fn from(value: CompletionMode) -> Self {
307        match value {
308            CompletionMode::Normal => zed_llm_client::CompletionMode::Normal,
309            CompletionMode::Burn => zed_llm_client::CompletionMode::Max,
310        }
311    }
312}
313
314#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
315pub struct LanguageModelSelection {
316    pub provider: LanguageModelProviderSetting,
317    pub model: String,
318}
319
320#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
321pub struct LanguageModelProviderSetting(pub String);
322
323impl JsonSchema for LanguageModelProviderSetting {
324    fn schema_name() -> String {
325        "LanguageModelProviderSetting".into()
326    }
327
328    fn json_schema(_: &mut schemars::r#gen::SchemaGenerator) -> Schema {
329        schemars::schema::SchemaObject {
330            enum_values: Some(vec![
331                "anthropic".into(),
332                "amazon-bedrock".into(),
333                "google".into(),
334                "lmstudio".into(),
335                "ollama".into(),
336                "openai".into(),
337                "zed.dev".into(),
338                "copilot_chat".into(),
339                "deepseek".into(),
340                "openrouter".into(),
341                "mistral".into(),
342                "vercel".into(),
343            ]),
344            ..Default::default()
345        }
346        .into()
347    }
348}
349
350impl From<String> for LanguageModelProviderSetting {
351    fn from(provider: String) -> Self {
352        Self(provider)
353    }
354}
355
356impl From<&str> for LanguageModelProviderSetting {
357    fn from(provider: &str) -> Self {
358        Self(provider.to_string())
359    }
360}
361
362impl Default for LanguageModelSelection {
363    fn default() -> Self {
364        Self {
365            provider: LanguageModelProviderSetting("openai".to_string()),
366            model: "gpt-4".to_string(),
367        }
368    }
369}
370
371#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)]
372pub struct AgentProfileContent {
373    pub name: Arc<str>,
374    #[serde(default)]
375    pub tools: IndexMap<Arc<str>, bool>,
376    /// Whether all context servers are enabled by default.
377    pub enable_all_context_servers: Option<bool>,
378    #[serde(default)]
379    pub context_servers: IndexMap<Arc<str>, ContextServerPresetContent>,
380}
381
382#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema)]
383pub struct ContextServerPresetContent {
384    pub tools: IndexMap<Arc<str>, bool>,
385}
386
387impl Settings for AgentSettings {
388    const KEY: Option<&'static str> = Some("agent");
389
390    const FALLBACK_KEY: Option<&'static str> = Some("assistant");
391
392    const PRESERVED_KEYS: Option<&'static [&'static str]> = Some(&["version"]);
393
394    type FileContent = AgentSettingsContent;
395
396    fn load(
397        sources: SettingsSources<Self::FileContent>,
398        _: &mut gpui::App,
399    ) -> anyhow::Result<Self> {
400        let mut settings = AgentSettings::default();
401
402        for value in sources.defaults_and_customizations() {
403            merge(&mut settings.enabled, value.enabled);
404            merge(&mut settings.button, value.button);
405            merge(&mut settings.dock, value.dock);
406            merge(
407                &mut settings.default_width,
408                value.default_width.map(Into::into),
409            );
410            merge(
411                &mut settings.default_height,
412                value.default_height.map(Into::into),
413            );
414            merge(&mut settings.default_model, value.default_model.clone());
415            settings.inline_assistant_model = value
416                .inline_assistant_model
417                .clone()
418                .or(settings.inline_assistant_model.take());
419            settings.commit_message_model = value
420                .clone()
421                .commit_message_model
422                .or(settings.commit_message_model.take());
423            settings.thread_summary_model = value
424                .clone()
425                .thread_summary_model
426                .or(settings.thread_summary_model.take());
427            merge(
428                &mut settings.inline_alternatives,
429                value.inline_alternatives.clone(),
430            );
431            merge(
432                &mut settings.always_allow_tool_actions,
433                value.always_allow_tool_actions,
434            );
435            merge(
436                &mut settings.notify_when_agent_waiting,
437                value.notify_when_agent_waiting,
438            );
439            merge(
440                &mut settings.play_sound_when_agent_done,
441                value.play_sound_when_agent_done,
442            );
443            merge(&mut settings.stream_edits, value.stream_edits);
444            merge(&mut settings.single_file_review, value.single_file_review);
445            merge(&mut settings.default_profile, value.default_profile.clone());
446            merge(&mut settings.default_view, value.default_view);
447            merge(
448                &mut settings.preferred_completion_mode,
449                value.preferred_completion_mode,
450            );
451            merge(&mut settings.enable_feedback, value.enable_feedback);
452
453            settings
454                .model_parameters
455                .extend_from_slice(&value.model_parameters);
456
457            if let Some(profiles) = value.profiles.as_ref() {
458                settings
459                    .profiles
460                    .extend(profiles.into_iter().map(|(id, profile)| {
461                        (
462                            id.clone(),
463                            AgentProfileSettings {
464                                name: profile.name.clone().into(),
465                                tools: profile.tools.clone(),
466                                enable_all_context_servers: profile
467                                    .enable_all_context_servers
468                                    .unwrap_or_default(),
469                                context_servers: profile
470                                    .context_servers
471                                    .iter()
472                                    .map(|(context_server_id, preset)| {
473                                        (
474                                            context_server_id.clone(),
475                                            ContextServerPreset {
476                                                tools: preset.tools.clone(),
477                                            },
478                                        )
479                                    })
480                                    .collect(),
481                            },
482                        )
483                    }));
484            }
485        }
486
487        Ok(settings)
488    }
489
490    fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
491        if let Some(b) = vscode
492            .read_value("chat.agent.enabled")
493            .and_then(|b| b.as_bool())
494        {
495            current.enabled = Some(b);
496            current.button = Some(b);
497        }
498    }
499}
500
501fn merge<T>(target: &mut T, value: Option<T>) {
502    if let Some(value) = value {
503        *target = value;
504    }
505}