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    /// Default: false
273    always_allow_tool_actions: Option<bool>,
274    /// Where to show a popup notification when the agent is waiting for user input.
275    ///
276    /// Default: "primary_screen"
277    notify_when_agent_waiting: Option<NotifyWhenAgentWaiting>,
278    /// Whether to play a sound when the agent has either completed its response, or needs user input.
279    ///
280    /// Default: false
281    play_sound_when_agent_done: Option<bool>,
282    /// Whether to stream edits from the agent as they are received.
283    ///
284    /// Default: false
285    stream_edits: Option<bool>,
286    /// Whether to display agent edits in single-file editors in addition to the review multibuffer pane.
287    ///
288    /// Default: true
289    single_file_review: Option<bool>,
290    /// Additional parameters for language model requests. When making a request
291    /// to a model, parameters will be taken from the last entry in this list
292    /// that matches the model's provider and name. In each entry, both provider
293    /// and model are optional, so that you can specify parameters for either
294    /// one.
295    ///
296    /// Default: []
297    #[serde(default)]
298    model_parameters: Vec<LanguageModelParameters>,
299    /// What completion mode to enable for new threads
300    ///
301    /// Default: normal
302    preferred_completion_mode: Option<CompletionMode>,
303    /// Whether to show thumb buttons for feedback in the agent panel.
304    ///
305    /// Default: true
306    enable_feedback: Option<bool>,
307    /// Whether to have edit cards in the agent panel expanded, showing a preview of the full diff.
308    ///
309    /// Default: true
310    expand_edit_card: Option<bool>,
311    /// Whether to have terminal cards in the agent panel expanded, showing the whole command output.
312    ///
313    /// Default: true
314    expand_terminal_card: Option<bool>,
315    /// Whether to always use cmd-enter (or ctrl-enter on Linux or Windows) to send messages in the agent panel.
316    ///
317    /// Default: false
318    use_modifier_to_send: Option<bool>,
319}
320
321#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)]
322#[serde(rename_all = "snake_case")]
323pub enum CompletionMode {
324    #[default]
325    Normal,
326    #[serde(alias = "max")]
327    Burn,
328}
329
330impl From<CompletionMode> for cloud_llm_client::CompletionMode {
331    fn from(value: CompletionMode) -> Self {
332        match value {
333            CompletionMode::Normal => cloud_llm_client::CompletionMode::Normal,
334            CompletionMode::Burn => cloud_llm_client::CompletionMode::Max,
335        }
336    }
337}
338
339#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
340pub struct LanguageModelSelection {
341    pub provider: LanguageModelProviderSetting,
342    pub model: String,
343}
344
345#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
346pub struct LanguageModelProviderSetting(pub String);
347
348impl JsonSchema for LanguageModelProviderSetting {
349    fn schema_name() -> Cow<'static, str> {
350        "LanguageModelProviderSetting".into()
351    }
352
353    fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
354        json_schema!({
355            "enum": [
356                "amazon-bedrock",
357                "anthropic",
358                "copilot_chat",
359                "deepseek",
360                "google",
361                "lmstudio",
362                "mistral",
363                "ollama",
364                "openai",
365                "openrouter",
366                "vercel",
367                "x_ai",
368                "zed.dev"
369            ]
370        })
371    }
372}
373
374impl From<String> for LanguageModelProviderSetting {
375    fn from(provider: String) -> Self {
376        Self(provider)
377    }
378}
379
380impl From<&str> for LanguageModelProviderSetting {
381    fn from(provider: &str) -> Self {
382        Self(provider.to_string())
383    }
384}
385
386#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)]
387pub struct AgentProfileContent {
388    pub name: Arc<str>,
389    #[serde(default)]
390    pub tools: IndexMap<Arc<str>, bool>,
391    /// Whether all context servers are enabled by default.
392    pub enable_all_context_servers: Option<bool>,
393    #[serde(default)]
394    pub context_servers: IndexMap<Arc<str>, ContextServerPresetContent>,
395}
396
397#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema)]
398pub struct ContextServerPresetContent {
399    pub tools: IndexMap<Arc<str>, bool>,
400}
401
402impl Settings for AgentSettings {
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!(
507            !sources.default.always_allow_tool_actions.unwrap_or(false),
508            "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!"
509        );
510
511        // For security reasons, only trust the user's global settings for whether to always allow tool actions.
512        // If this could be overridden locally, an attacker could (e.g. by committing to source control and
513        // convincing you to switch branches) modify your project-local settings to disable the agent's safety checks.
514        settings.always_allow_tool_actions = sources
515            .user
516            .and_then(|setting| setting.always_allow_tool_actions)
517            .unwrap_or(false);
518
519        Ok(settings)
520    }
521
522    fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
523        if let Some(b) = vscode
524            .read_value("chat.agent.enabled")
525            .and_then(|b| b.as_bool())
526        {
527            current.enabled = Some(b);
528            current.button = Some(b);
529        }
530    }
531}
532
533fn merge<T>(target: &mut T, value: Option<T>) {
534    if let Some(value) = value {
535        *target = value;
536    }
537}