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