agent_settings.rs

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