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