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