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