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