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