assistant_settings.rs

  1mod agent_profile;
  2
  3use std::sync::Arc;
  4
  5use ::open_ai::Model as OpenAiModel;
  6use anthropic::Model as AnthropicModel;
  7use anyhow::{Result, bail};
  8use deepseek::Model as DeepseekModel;
  9use feature_flags::{Assistant2FeatureFlag, FeatureFlagAppExt};
 10use gpui::{App, Pixels};
 11use indexmap::IndexMap;
 12use language_model::{CloudModel, LanguageModel};
 13use lmstudio::Model as LmStudioModel;
 14use ollama::Model as OllamaModel;
 15use schemars::{JsonSchema, schema::Schema};
 16use serde::{Deserialize, Serialize};
 17use settings::{Settings, SettingsSources};
 18
 19pub use crate::agent_profile::*;
 20
 21#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
 22#[serde(rename_all = "snake_case")]
 23pub enum AssistantDockPosition {
 24    Left,
 25    #[default]
 26    Right,
 27    Bottom,
 28}
 29
 30#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
 31#[serde(rename_all = "snake_case")]
 32pub enum NotifyWhenAgentWaiting {
 33    #[default]
 34    PrimaryScreen,
 35    AllScreens,
 36    Never,
 37}
 38
 39#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
 40#[serde(tag = "name", rename_all = "snake_case")]
 41pub enum AssistantProviderContentV1 {
 42    #[serde(rename = "zed.dev")]
 43    ZedDotDev { default_model: Option<CloudModel> },
 44    #[serde(rename = "openai")]
 45    OpenAi {
 46        default_model: Option<OpenAiModel>,
 47        api_url: Option<String>,
 48        available_models: Option<Vec<OpenAiModel>>,
 49    },
 50    #[serde(rename = "anthropic")]
 51    Anthropic {
 52        default_model: Option<AnthropicModel>,
 53        api_url: Option<String>,
 54    },
 55    #[serde(rename = "ollama")]
 56    Ollama {
 57        default_model: Option<OllamaModel>,
 58        api_url: Option<String>,
 59    },
 60    #[serde(rename = "lmstudio")]
 61    LmStudio {
 62        default_model: Option<LmStudioModel>,
 63        api_url: Option<String>,
 64    },
 65    #[serde(rename = "deepseek")]
 66    DeepSeek {
 67        default_model: Option<DeepseekModel>,
 68        api_url: Option<String>,
 69    },
 70}
 71
 72#[derive(Debug, Default)]
 73pub struct AssistantSettings {
 74    pub enabled: bool,
 75    pub button: bool,
 76    pub dock: AssistantDockPosition,
 77    pub default_width: Pixels,
 78    pub default_height: Pixels,
 79    pub default_model: LanguageModelSelection,
 80    pub editor_model: LanguageModelSelection,
 81    pub inline_alternatives: Vec<LanguageModelSelection>,
 82    pub using_outdated_settings_version: bool,
 83    pub enable_experimental_live_diffs: bool,
 84    pub default_profile: AgentProfileId,
 85    pub profiles: IndexMap<AgentProfileId, AgentProfile>,
 86    pub always_allow_tool_actions: bool,
 87    pub notify_when_agent_waiting: NotifyWhenAgentWaiting,
 88}
 89
 90impl AssistantSettings {
 91    pub fn are_live_diffs_enabled(&self, cx: &App) -> bool {
 92        if cx.has_flag::<Assistant2FeatureFlag>() {
 93            return false;
 94        }
 95
 96        cx.is_staff() || self.enable_experimental_live_diffs
 97    }
 98}
 99
100/// Assistant panel settings
101#[derive(Clone, Serialize, Deserialize, Debug)]
102#[serde(untagged)]
103pub enum AssistantSettingsContent {
104    Versioned(VersionedAssistantSettingsContent),
105    Legacy(LegacyAssistantSettingsContent),
106}
107
108impl JsonSchema for AssistantSettingsContent {
109    fn schema_name() -> String {
110        VersionedAssistantSettingsContent::schema_name()
111    }
112
113    fn json_schema(r#gen: &mut schemars::r#gen::SchemaGenerator) -> Schema {
114        VersionedAssistantSettingsContent::json_schema(r#gen)
115    }
116
117    fn is_referenceable() -> bool {
118        VersionedAssistantSettingsContent::is_referenceable()
119    }
120}
121
122impl Default for AssistantSettingsContent {
123    fn default() -> Self {
124        Self::Versioned(VersionedAssistantSettingsContent::default())
125    }
126}
127
128impl AssistantSettingsContent {
129    pub fn is_version_outdated(&self) -> bool {
130        match self {
131            AssistantSettingsContent::Versioned(settings) => match settings {
132                VersionedAssistantSettingsContent::V1(_) => true,
133                VersionedAssistantSettingsContent::V2(_) => false,
134            },
135            AssistantSettingsContent::Legacy(_) => true,
136        }
137    }
138
139    fn upgrade(&self) -> AssistantSettingsContentV2 {
140        match self {
141            AssistantSettingsContent::Versioned(settings) => match settings {
142                VersionedAssistantSettingsContent::V1(settings) => AssistantSettingsContentV2 {
143                    enabled: settings.enabled,
144                    button: settings.button,
145                    dock: settings.dock,
146                    default_width: settings.default_width,
147                    default_height: settings.default_width,
148                    default_model: settings
149                        .provider
150                        .clone()
151                        .and_then(|provider| match provider {
152                            AssistantProviderContentV1::ZedDotDev { default_model } => {
153                                default_model.map(|model| LanguageModelSelection {
154                                    provider: "zed.dev".to_string(),
155                                    model: model.id().to_string(),
156                                })
157                            }
158                            AssistantProviderContentV1::OpenAi { default_model, .. } => {
159                                default_model.map(|model| LanguageModelSelection {
160                                    provider: "openai".to_string(),
161                                    model: model.id().to_string(),
162                                })
163                            }
164                            AssistantProviderContentV1::Anthropic { default_model, .. } => {
165                                default_model.map(|model| LanguageModelSelection {
166                                    provider: "anthropic".to_string(),
167                                    model: model.id().to_string(),
168                                })
169                            }
170                            AssistantProviderContentV1::Ollama { default_model, .. } => {
171                                default_model.map(|model| LanguageModelSelection {
172                                    provider: "ollama".to_string(),
173                                    model: model.id().to_string(),
174                                })
175                            }
176                            AssistantProviderContentV1::LmStudio { default_model, .. } => {
177                                default_model.map(|model| LanguageModelSelection {
178                                    provider: "lmstudio".to_string(),
179                                    model: model.id().to_string(),
180                                })
181                            }
182                            AssistantProviderContentV1::DeepSeek { default_model, .. } => {
183                                default_model.map(|model| LanguageModelSelection {
184                                    provider: "deepseek".to_string(),
185                                    model: model.id().to_string(),
186                                })
187                            }
188                        }),
189                    editor_model: None,
190                    inline_alternatives: None,
191                    enable_experimental_live_diffs: None,
192                    default_profile: None,
193                    profiles: None,
194                    always_allow_tool_actions: None,
195                    notify_when_agent_waiting: None,
196                },
197                VersionedAssistantSettingsContent::V2(settings) => settings.clone(),
198            },
199            AssistantSettingsContent::Legacy(settings) => AssistantSettingsContentV2 {
200                enabled: None,
201                button: settings.button,
202                dock: settings.dock,
203                default_width: settings.default_width,
204                default_height: settings.default_height,
205                default_model: Some(LanguageModelSelection {
206                    provider: "openai".to_string(),
207                    model: settings
208                        .default_open_ai_model
209                        .clone()
210                        .unwrap_or_default()
211                        .id()
212                        .to_string(),
213                }),
214                editor_model: None,
215                inline_alternatives: None,
216                enable_experimental_live_diffs: None,
217                default_profile: None,
218                profiles: None,
219                always_allow_tool_actions: None,
220                notify_when_agent_waiting: None,
221            },
222        }
223    }
224
225    pub fn set_dock(&mut self, dock: AssistantDockPosition) {
226        match self {
227            AssistantSettingsContent::Versioned(settings) => match settings {
228                VersionedAssistantSettingsContent::V1(settings) => {
229                    settings.dock = Some(dock);
230                }
231                VersionedAssistantSettingsContent::V2(settings) => {
232                    settings.dock = Some(dock);
233                }
234            },
235            AssistantSettingsContent::Legacy(settings) => {
236                settings.dock = Some(dock);
237            }
238        }
239    }
240
241    pub fn set_model(&mut self, language_model: Arc<dyn LanguageModel>) {
242        let model = language_model.id().0.to_string();
243        let provider = language_model.provider_id().0.to_string();
244
245        match self {
246            AssistantSettingsContent::Versioned(settings) => match settings {
247                VersionedAssistantSettingsContent::V1(settings) => match provider.as_ref() {
248                    "zed.dev" => {
249                        log::warn!("attempted to set zed.dev model on outdated settings");
250                    }
251                    "anthropic" => {
252                        let api_url = match &settings.provider {
253                            Some(AssistantProviderContentV1::Anthropic { api_url, .. }) => {
254                                api_url.clone()
255                            }
256                            _ => None,
257                        };
258                        settings.provider = Some(AssistantProviderContentV1::Anthropic {
259                            default_model: AnthropicModel::from_id(&model).ok(),
260                            api_url,
261                        });
262                    }
263                    "ollama" => {
264                        let api_url = match &settings.provider {
265                            Some(AssistantProviderContentV1::Ollama { api_url, .. }) => {
266                                api_url.clone()
267                            }
268                            _ => None,
269                        };
270                        settings.provider = Some(AssistantProviderContentV1::Ollama {
271                            default_model: Some(ollama::Model::new(&model, None, None)),
272                            api_url,
273                        });
274                    }
275                    "lmstudio" => {
276                        let api_url = match &settings.provider {
277                            Some(AssistantProviderContentV1::LmStudio { api_url, .. }) => {
278                                api_url.clone()
279                            }
280                            _ => None,
281                        };
282                        settings.provider = Some(AssistantProviderContentV1::LmStudio {
283                            default_model: Some(lmstudio::Model::new(&model, None, None)),
284                            api_url,
285                        });
286                    }
287                    "openai" => {
288                        let (api_url, available_models) = match &settings.provider {
289                            Some(AssistantProviderContentV1::OpenAi {
290                                api_url,
291                                available_models,
292                                ..
293                            }) => (api_url.clone(), available_models.clone()),
294                            _ => (None, None),
295                        };
296                        settings.provider = Some(AssistantProviderContentV1::OpenAi {
297                            default_model: OpenAiModel::from_id(&model).ok(),
298                            api_url,
299                            available_models,
300                        });
301                    }
302                    "deepseek" => {
303                        let api_url = match &settings.provider {
304                            Some(AssistantProviderContentV1::DeepSeek { api_url, .. }) => {
305                                api_url.clone()
306                            }
307                            _ => None,
308                        };
309                        settings.provider = Some(AssistantProviderContentV1::DeepSeek {
310                            default_model: DeepseekModel::from_id(&model).ok(),
311                            api_url,
312                        });
313                    }
314                    _ => {}
315                },
316                VersionedAssistantSettingsContent::V2(settings) => {
317                    settings.default_model = Some(LanguageModelSelection { provider, model });
318                }
319            },
320            AssistantSettingsContent::Legacy(settings) => {
321                if let Ok(model) = OpenAiModel::from_id(&language_model.id().0) {
322                    settings.default_open_ai_model = Some(model);
323                }
324            }
325        }
326    }
327
328    pub fn set_always_allow_tool_actions(&mut self, allow: bool) {
329        let AssistantSettingsContent::Versioned(VersionedAssistantSettingsContent::V2(settings)) =
330            self
331        else {
332            return;
333        };
334        settings.always_allow_tool_actions = Some(allow);
335    }
336
337    pub fn set_profile(&mut self, profile_id: AgentProfileId) {
338        let AssistantSettingsContent::Versioned(VersionedAssistantSettingsContent::V2(settings)) =
339            self
340        else {
341            return;
342        };
343
344        settings.default_profile = Some(profile_id);
345    }
346
347    pub fn create_profile(
348        &mut self,
349        profile_id: AgentProfileId,
350        profile: AgentProfile,
351    ) -> Result<()> {
352        let AssistantSettingsContent::Versioned(VersionedAssistantSettingsContent::V2(settings)) =
353            self
354        else {
355            return Ok(());
356        };
357
358        let profiles = settings.profiles.get_or_insert_default();
359        if profiles.contains_key(&profile_id) {
360            bail!("profile with ID '{profile_id}' already exists");
361        }
362
363        profiles.insert(
364            profile_id,
365            AgentProfileContent {
366                name: profile.name.into(),
367                tools: profile.tools,
368                enable_all_context_servers: Some(profile.enable_all_context_servers),
369                context_servers: profile
370                    .context_servers
371                    .into_iter()
372                    .map(|(server_id, preset)| {
373                        (
374                            server_id,
375                            ContextServerPresetContent {
376                                tools: preset.tools,
377                            },
378                        )
379                    })
380                    .collect(),
381            },
382        );
383
384        Ok(())
385    }
386}
387
388#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
389#[serde(tag = "version")]
390pub enum VersionedAssistantSettingsContent {
391    #[serde(rename = "1")]
392    V1(AssistantSettingsContentV1),
393    #[serde(rename = "2")]
394    V2(AssistantSettingsContentV2),
395}
396
397impl Default for VersionedAssistantSettingsContent {
398    fn default() -> Self {
399        Self::V2(AssistantSettingsContentV2 {
400            enabled: None,
401            button: None,
402            dock: None,
403            default_width: None,
404            default_height: None,
405            default_model: None,
406            editor_model: None,
407            inline_alternatives: None,
408            enable_experimental_live_diffs: None,
409            default_profile: None,
410            profiles: None,
411            always_allow_tool_actions: None,
412            notify_when_agent_waiting: None,
413        })
414    }
415}
416
417#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
418pub struct AssistantSettingsContentV2 {
419    /// Whether the Assistant is enabled.
420    ///
421    /// Default: true
422    enabled: Option<bool>,
423    /// Whether to show the assistant panel button in the status bar.
424    ///
425    /// Default: true
426    button: Option<bool>,
427    /// Where to dock the assistant.
428    ///
429    /// Default: right
430    dock: Option<AssistantDockPosition>,
431    /// Default width in pixels when the assistant is docked to the left or right.
432    ///
433    /// Default: 640
434    default_width: Option<f32>,
435    /// Default height in pixels when the assistant is docked to the bottom.
436    ///
437    /// Default: 320
438    default_height: Option<f32>,
439    /// The default model to use when creating new chats.
440    default_model: Option<LanguageModelSelection>,
441    /// The model to use when applying edits from the assistant.
442    editor_model: Option<LanguageModelSelection>,
443    /// Additional models with which to generate alternatives when performing inline assists.
444    inline_alternatives: Option<Vec<LanguageModelSelection>>,
445    /// Enable experimental live diffs in the assistant panel.
446    ///
447    /// Default: false
448    enable_experimental_live_diffs: Option<bool>,
449    /// The default profile to use in the Agent.
450    ///
451    /// Default: write
452    default_profile: Option<AgentProfileId>,
453    /// The available agent profiles.
454    pub profiles: Option<IndexMap<AgentProfileId, AgentProfileContent>>,
455    /// Whenever a tool action would normally wait for your confirmation
456    /// that you allow it, always choose to allow it.
457    ///
458    /// Default: false
459    always_allow_tool_actions: Option<bool>,
460    /// Where to show a popup notification when the agent is waiting for user input.
461    ///
462    /// Default: "primary_screen"
463    notify_when_agent_waiting: Option<NotifyWhenAgentWaiting>,
464}
465
466#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
467pub struct LanguageModelSelection {
468    #[schemars(schema_with = "providers_schema")]
469    pub provider: String,
470    pub model: String,
471}
472
473fn providers_schema(_: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
474    schemars::schema::SchemaObject {
475        enum_values: Some(vec![
476            "anthropic".into(),
477            "bedrock".into(),
478            "google".into(),
479            "lmstudio".into(),
480            "ollama".into(),
481            "openai".into(),
482            "zed.dev".into(),
483            "copilot_chat".into(),
484            "deepseek".into(),
485        ]),
486        ..Default::default()
487    }
488    .into()
489}
490
491impl Default for LanguageModelSelection {
492    fn default() -> Self {
493        Self {
494            provider: "openai".to_string(),
495            model: "gpt-4".to_string(),
496        }
497    }
498}
499
500#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)]
501pub struct AgentProfileContent {
502    pub name: Arc<str>,
503    #[serde(default)]
504    pub tools: IndexMap<Arc<str>, bool>,
505    /// Whether all context servers are enabled by default.
506    pub enable_all_context_servers: Option<bool>,
507    #[serde(default)]
508    pub context_servers: IndexMap<Arc<str>, ContextServerPresetContent>,
509}
510
511#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema)]
512pub struct ContextServerPresetContent {
513    pub tools: IndexMap<Arc<str>, bool>,
514}
515
516#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
517pub struct AssistantSettingsContentV1 {
518    /// Whether the Assistant is enabled.
519    ///
520    /// Default: true
521    enabled: Option<bool>,
522    /// Whether to show the assistant panel button in the status bar.
523    ///
524    /// Default: true
525    button: Option<bool>,
526    /// Where to dock the assistant.
527    ///
528    /// Default: right
529    dock: Option<AssistantDockPosition>,
530    /// Default width in pixels when the assistant is docked to the left or right.
531    ///
532    /// Default: 640
533    default_width: Option<f32>,
534    /// Default height in pixels when the assistant is docked to the bottom.
535    ///
536    /// Default: 320
537    default_height: Option<f32>,
538    /// The provider of the assistant service.
539    ///
540    /// This can be "openai", "anthropic", "ollama", "lmstudio", "deepseek", "zed.dev"
541    /// each with their respective default models and configurations.
542    provider: Option<AssistantProviderContentV1>,
543}
544
545#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
546pub struct LegacyAssistantSettingsContent {
547    /// Whether to show the assistant panel button in the status bar.
548    ///
549    /// Default: true
550    pub button: Option<bool>,
551    /// Where to dock the assistant.
552    ///
553    /// Default: right
554    pub dock: Option<AssistantDockPosition>,
555    /// Default width in pixels when the assistant is docked to the left or right.
556    ///
557    /// Default: 640
558    pub default_width: Option<f32>,
559    /// Default height in pixels when the assistant is docked to the bottom.
560    ///
561    /// Default: 320
562    pub default_height: Option<f32>,
563    /// The default OpenAI model to use when creating new chats.
564    ///
565    /// Default: gpt-4-1106-preview
566    pub default_open_ai_model: Option<OpenAiModel>,
567    /// OpenAI API base URL to use when creating new chats.
568    ///
569    /// Default: <https://api.openai.com/v1>
570    pub openai_api_url: Option<String>,
571}
572
573impl Settings for AssistantSettings {
574    const KEY: Option<&'static str> = Some("assistant");
575
576    const PRESERVED_KEYS: Option<&'static [&'static str]> = Some(&["version"]);
577
578    type FileContent = AssistantSettingsContent;
579
580    fn load(
581        sources: SettingsSources<Self::FileContent>,
582        _: &mut gpui::App,
583    ) -> anyhow::Result<Self> {
584        let mut settings = AssistantSettings::default();
585
586        for value in sources.defaults_and_customizations() {
587            if value.is_version_outdated() {
588                settings.using_outdated_settings_version = true;
589            }
590
591            let value = value.upgrade();
592            merge(&mut settings.enabled, value.enabled);
593            merge(&mut settings.button, value.button);
594            merge(&mut settings.dock, value.dock);
595            merge(
596                &mut settings.default_width,
597                value.default_width.map(Into::into),
598            );
599            merge(
600                &mut settings.default_height,
601                value.default_height.map(Into::into),
602            );
603            merge(&mut settings.default_model, value.default_model);
604            merge(&mut settings.editor_model, value.editor_model);
605            merge(&mut settings.inline_alternatives, value.inline_alternatives);
606            merge(
607                &mut settings.enable_experimental_live_diffs,
608                value.enable_experimental_live_diffs,
609            );
610            merge(
611                &mut settings.always_allow_tool_actions,
612                value.always_allow_tool_actions,
613            );
614            merge(
615                &mut settings.notify_when_agent_waiting,
616                value.notify_when_agent_waiting,
617            );
618            merge(&mut settings.default_profile, value.default_profile);
619
620            if let Some(profiles) = value.profiles {
621                settings
622                    .profiles
623                    .extend(profiles.into_iter().map(|(id, profile)| {
624                        (
625                            id,
626                            AgentProfile {
627                                name: profile.name.into(),
628                                tools: profile.tools,
629                                enable_all_context_servers: profile
630                                    .enable_all_context_servers
631                                    .unwrap_or_default(),
632                                context_servers: profile
633                                    .context_servers
634                                    .into_iter()
635                                    .map(|(context_server_id, preset)| {
636                                        (
637                                            context_server_id,
638                                            ContextServerPreset {
639                                                tools: preset.tools.clone(),
640                                            },
641                                        )
642                                    })
643                                    .collect(),
644                            },
645                        )
646                    }));
647            }
648        }
649
650        Ok(settings)
651    }
652}
653
654fn merge<T>(target: &mut T, value: Option<T>) {
655    if let Some(value) = value {
656        *target = value;
657    }
658}
659
660#[cfg(test)]
661mod tests {
662    use fs::Fs;
663    use gpui::{ReadGlobal, TestAppContext};
664
665    use super::*;
666
667    #[gpui::test]
668    async fn test_deserialize_assistant_settings_with_version(cx: &mut TestAppContext) {
669        let fs = fs::FakeFs::new(cx.executor().clone());
670        fs.create_dir(paths::settings_file().parent().unwrap())
671            .await
672            .unwrap();
673
674        cx.update(|cx| {
675            let test_settings = settings::SettingsStore::test(cx);
676            cx.set_global(test_settings);
677            AssistantSettings::register(cx);
678        });
679
680        cx.update(|cx| {
681            assert!(!AssistantSettings::get_global(cx).using_outdated_settings_version);
682            assert_eq!(
683                AssistantSettings::get_global(cx).default_model,
684                LanguageModelSelection {
685                    provider: "zed.dev".into(),
686                    model: "claude-3-5-sonnet-latest".into(),
687                }
688            );
689        });
690
691        cx.update(|cx| {
692            settings::SettingsStore::global(cx).update_settings_file::<AssistantSettings>(
693                fs.clone(),
694                |settings, _| {
695                    *settings = AssistantSettingsContent::Versioned(
696                        VersionedAssistantSettingsContent::V2(AssistantSettingsContentV2 {
697                            default_model: Some(LanguageModelSelection {
698                                provider: "test-provider".into(),
699                                model: "gpt-99".into(),
700                            }),
701                            editor_model: Some(LanguageModelSelection {
702                                provider: "test-provider".into(),
703                                model: "gpt-99".into(),
704                            }),
705                            inline_alternatives: None,
706                            enabled: None,
707                            button: None,
708                            dock: None,
709                            default_width: None,
710                            default_height: None,
711                            enable_experimental_live_diffs: None,
712                            default_profile: None,
713                            profiles: None,
714                            always_allow_tool_actions: None,
715                            notify_when_agent_waiting: None,
716                        }),
717                    )
718                },
719            );
720        });
721
722        cx.run_until_parked();
723
724        let raw_settings_value = fs.load(paths::settings_file()).await.unwrap();
725        assert!(raw_settings_value.contains(r#""version": "2""#));
726
727        #[derive(Debug, Deserialize)]
728        struct AssistantSettingsTest {
729            assistant: AssistantSettingsContent,
730        }
731
732        let assistant_settings: AssistantSettingsTest =
733            serde_json_lenient::from_str(&raw_settings_value).unwrap();
734
735        assert!(!assistant_settings.assistant.is_version_outdated());
736    }
737}