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