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