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