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