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