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