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