assistant_settings.rs

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