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