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