assistant_settings.rs

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