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