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(&model, None, None)),
319                                api_url,
320                            });
321                        }
322                        "lmstudio" => {
323                            let api_url = match &settings.provider {
324                                Some(AssistantProviderContentV1::LmStudio { api_url, .. }) => {
325                                    api_url.clone()
326                                }
327                                _ => None,
328                            };
329                            settings.provider = Some(AssistantProviderContentV1::LmStudio {
330                                default_model: Some(lmstudio::Model::new(&model, None, None)),
331                                api_url,
332                            });
333                        }
334                        "openai" => {
335                            let (api_url, available_models) = match &settings.provider {
336                                Some(AssistantProviderContentV1::OpenAi {
337                                    api_url,
338                                    available_models,
339                                    ..
340                                }) => (api_url.clone(), available_models.clone()),
341                                _ => (None, None),
342                            };
343                            settings.provider = Some(AssistantProviderContentV1::OpenAi {
344                                default_model: OpenAiModel::from_id(&model).ok(),
345                                api_url,
346                                available_models,
347                            });
348                        }
349                        "deepseek" => {
350                            let api_url = match &settings.provider {
351                                Some(AssistantProviderContentV1::DeepSeek { api_url, .. }) => {
352                                    api_url.clone()
353                                }
354                                _ => None,
355                            };
356                            settings.provider = Some(AssistantProviderContentV1::DeepSeek {
357                                default_model: DeepseekModel::from_id(&model).ok(),
358                                api_url,
359                            });
360                        }
361                        _ => {}
362                    }
363                }
364                VersionedAssistantSettingsContent::V2(ref mut settings) => {
365                    settings.default_model = Some(LanguageModelSelection { provider, model });
366                }
367            },
368            Some(AssistantSettingsContentInner::Legacy(settings)) => {
369                if let Ok(model) = OpenAiModel::from_id(&language_model.id().0) {
370                    settings.default_open_ai_model = Some(model);
371                }
372            }
373            None => {
374                self.inner = Some(AssistantSettingsContentInner::for_v2(
375                    AssistantSettingsContentV2 {
376                        default_model: Some(LanguageModelSelection { provider, model }),
377                        ..Default::default()
378                    },
379                ));
380            }
381        }
382    }
383
384    pub fn set_inline_assistant_model(&mut self, provider: String, model: String) {
385        self.v2_setting(|setting| {
386            setting.inline_assistant_model = Some(LanguageModelSelection { provider, model });
387            Ok(())
388        })
389        .ok();
390    }
391
392    pub fn set_commit_message_model(&mut self, provider: String, model: String) {
393        self.v2_setting(|setting| {
394            setting.commit_message_model = Some(LanguageModelSelection { provider, model });
395            Ok(())
396        })
397        .ok();
398    }
399
400    pub fn v2_setting(
401        &mut self,
402        f: impl FnOnce(&mut AssistantSettingsContentV2) -> anyhow::Result<()>,
403    ) -> anyhow::Result<()> {
404        match self.inner.get_or_insert_with(|| {
405            AssistantSettingsContentInner::for_v2(AssistantSettingsContentV2 {
406                ..Default::default()
407            })
408        }) {
409            AssistantSettingsContentInner::Versioned(boxed) => {
410                if let VersionedAssistantSettingsContent::V2(ref mut settings) = **boxed {
411                    f(settings)
412                } else {
413                    Ok(())
414                }
415            }
416            _ => Ok(()),
417        }
418    }
419
420    pub fn set_thread_summary_model(&mut self, provider: String, model: String) {
421        self.v2_setting(|setting| {
422            setting.thread_summary_model = Some(LanguageModelSelection { provider, model });
423            Ok(())
424        })
425        .ok();
426    }
427
428    pub fn set_always_allow_tool_actions(&mut self, allow: bool) {
429        self.v2_setting(|setting| {
430            setting.always_allow_tool_actions = Some(allow);
431            Ok(())
432        })
433        .ok();
434    }
435
436    pub fn set_single_file_review(&mut self, allow: bool) {
437        self.v2_setting(|setting| {
438            setting.single_file_review = Some(allow);
439            Ok(())
440        })
441        .ok();
442    }
443
444    pub fn set_profile(&mut self, profile_id: AgentProfileId) {
445        self.v2_setting(|setting| {
446            setting.default_profile = Some(profile_id);
447            Ok(())
448        })
449        .ok();
450    }
451
452    pub fn create_profile(
453        &mut self,
454        profile_id: AgentProfileId,
455        profile: AgentProfile,
456    ) -> Result<()> {
457        self.v2_setting(|settings| {
458            let profiles = settings.profiles.get_or_insert_default();
459            if profiles.contains_key(&profile_id) {
460                bail!("profile with ID '{profile_id}' already exists");
461            }
462
463            profiles.insert(
464                profile_id,
465                AgentProfileContent {
466                    name: profile.name.into(),
467                    tools: profile.tools,
468                    enable_all_context_servers: Some(profile.enable_all_context_servers),
469                    context_servers: profile
470                        .context_servers
471                        .into_iter()
472                        .map(|(server_id, preset)| {
473                            (
474                                server_id,
475                                ContextServerPresetContent {
476                                    tools: preset.tools,
477                                },
478                            )
479                        })
480                        .collect(),
481                },
482            );
483
484            Ok(())
485        })
486    }
487}
488
489#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
490#[serde(tag = "version")]
491pub enum VersionedAssistantSettingsContent {
492    #[serde(rename = "1")]
493    V1(AssistantSettingsContentV1),
494    #[serde(rename = "2")]
495    V2(AssistantSettingsContentV2),
496}
497
498impl Default for VersionedAssistantSettingsContent {
499    fn default() -> Self {
500        Self::V2(AssistantSettingsContentV2 {
501            enabled: None,
502            button: None,
503            dock: None,
504            default_width: None,
505            default_height: None,
506            default_model: None,
507            inline_assistant_model: None,
508            commit_message_model: None,
509            thread_summary_model: None,
510            inline_alternatives: None,
511            enable_experimental_live_diffs: None,
512            default_profile: None,
513            profiles: None,
514            always_allow_tool_actions: None,
515            notify_when_agent_waiting: None,
516            stream_edits: None,
517            single_file_review: None,
518        })
519    }
520}
521
522#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default)]
523pub struct AssistantSettingsContentV2 {
524    /// Whether the Assistant is enabled.
525    ///
526    /// Default: true
527    enabled: Option<bool>,
528    /// Whether to show the assistant panel button in the status bar.
529    ///
530    /// Default: true
531    button: Option<bool>,
532    /// Where to dock the assistant.
533    ///
534    /// Default: right
535    dock: Option<AssistantDockPosition>,
536    /// Default width in pixels when the assistant is docked to the left or right.
537    ///
538    /// Default: 640
539    default_width: Option<f32>,
540    /// Default height in pixels when the assistant is docked to the bottom.
541    ///
542    /// Default: 320
543    default_height: Option<f32>,
544    /// The default model to use when creating new chats and for other features when a specific model is not specified.
545    default_model: Option<LanguageModelSelection>,
546    /// Model to use for the inline assistant. Defaults to default_model when not specified.
547    inline_assistant_model: Option<LanguageModelSelection>,
548    /// Model to use for generating git commit messages. Defaults to default_model when not specified.
549    commit_message_model: Option<LanguageModelSelection>,
550    /// Model to use for generating thread summaries. Defaults to default_model when not specified.
551    thread_summary_model: Option<LanguageModelSelection>,
552    /// Additional models with which to generate alternatives when performing inline assists.
553    inline_alternatives: Option<Vec<LanguageModelSelection>>,
554    /// Enable experimental live diffs in the assistant panel.
555    ///
556    /// Default: false
557    enable_experimental_live_diffs: Option<bool>,
558    /// The default profile to use in the Agent.
559    ///
560    /// Default: write
561    default_profile: Option<AgentProfileId>,
562    /// The available agent profiles.
563    pub profiles: Option<IndexMap<AgentProfileId, AgentProfileContent>>,
564    /// Whenever a tool action would normally wait for your confirmation
565    /// that you allow it, always choose to allow it.
566    ///
567    /// Default: false
568    always_allow_tool_actions: Option<bool>,
569    /// Where to show a popup notification when the agent is waiting for user input.
570    ///
571    /// Default: "primary_screen"
572    notify_when_agent_waiting: Option<NotifyWhenAgentWaiting>,
573    /// Whether to stream edits from the agent as they are received.
574    ///
575    /// Default: false
576    stream_edits: Option<bool>,
577    /// Whether to display agent edits in single-file editors in addition to the review multibuffer pane.
578    ///
579    /// Default: true
580    single_file_review: Option<bool>,
581}
582
583#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
584pub struct LanguageModelSelection {
585    #[schemars(schema_with = "providers_schema")]
586    pub provider: String,
587    pub model: String,
588}
589
590fn providers_schema(_: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
591    schemars::schema::SchemaObject {
592        enum_values: Some(vec![
593            "anthropic".into(),
594            "bedrock".into(),
595            "google".into(),
596            "lmstudio".into(),
597            "ollama".into(),
598            "openai".into(),
599            "zed.dev".into(),
600            "copilot_chat".into(),
601            "deepseek".into(),
602        ]),
603        ..Default::default()
604    }
605    .into()
606}
607
608impl Default for LanguageModelSelection {
609    fn default() -> Self {
610        Self {
611            provider: "openai".to_string(),
612            model: "gpt-4".to_string(),
613        }
614    }
615}
616
617#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)]
618pub struct AgentProfileContent {
619    pub name: Arc<str>,
620    #[serde(default)]
621    pub tools: IndexMap<Arc<str>, bool>,
622    /// Whether all context servers are enabled by default.
623    pub enable_all_context_servers: Option<bool>,
624    #[serde(default)]
625    pub context_servers: IndexMap<Arc<str>, ContextServerPresetContent>,
626}
627
628#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema)]
629pub struct ContextServerPresetContent {
630    pub tools: IndexMap<Arc<str>, bool>,
631}
632
633#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
634pub struct AssistantSettingsContentV1 {
635    /// Whether the Assistant is enabled.
636    ///
637    /// Default: true
638    enabled: Option<bool>,
639    /// Whether to show the assistant panel button in the status bar.
640    ///
641    /// Default: true
642    button: Option<bool>,
643    /// Where to dock the assistant.
644    ///
645    /// Default: right
646    dock: Option<AssistantDockPosition>,
647    /// Default width in pixels when the assistant is docked to the left or right.
648    ///
649    /// Default: 640
650    default_width: Option<f32>,
651    /// Default height in pixels when the assistant is docked to the bottom.
652    ///
653    /// Default: 320
654    default_height: Option<f32>,
655    /// The provider of the assistant service.
656    ///
657    /// This can be "openai", "anthropic", "ollama", "lmstudio", "deepseek", "zed.dev"
658    /// each with their respective default models and configurations.
659    provider: Option<AssistantProviderContentV1>,
660}
661
662#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
663pub struct LegacyAssistantSettingsContent {
664    /// Whether to show the assistant panel button in the status bar.
665    ///
666    /// Default: true
667    pub button: Option<bool>,
668    /// Where to dock the assistant.
669    ///
670    /// Default: right
671    pub dock: Option<AssistantDockPosition>,
672    /// Default width in pixels when the assistant is docked to the left or right.
673    ///
674    /// Default: 640
675    pub default_width: Option<f32>,
676    /// Default height in pixels when the assistant is docked to the bottom.
677    ///
678    /// Default: 320
679    pub default_height: Option<f32>,
680    /// The default OpenAI model to use when creating new chats.
681    ///
682    /// Default: gpt-4-1106-preview
683    pub default_open_ai_model: Option<OpenAiModel>,
684    /// OpenAI API base URL to use when creating new chats.
685    ///
686    /// Default: <https://api.openai.com/v1>
687    pub openai_api_url: Option<String>,
688}
689
690impl Settings for AssistantSettings {
691    const KEY: Option<&'static str> = Some("assistant");
692
693    const PRESERVED_KEYS: Option<&'static [&'static str]> = Some(&["version"]);
694
695    type FileContent = AssistantSettingsContent;
696
697    fn load(
698        sources: SettingsSources<Self::FileContent>,
699        _: &mut gpui::App,
700    ) -> anyhow::Result<Self> {
701        let mut settings = AssistantSettings::default();
702
703        for value in sources.defaults_and_customizations() {
704            if value.is_version_outdated() {
705                settings.using_outdated_settings_version = true;
706            }
707
708            let value = value.upgrade();
709            merge(&mut settings.enabled, value.enabled);
710            merge(&mut settings.button, value.button);
711            merge(&mut settings.dock, value.dock);
712            merge(
713                &mut settings.default_width,
714                value.default_width.map(Into::into),
715            );
716            merge(
717                &mut settings.default_height,
718                value.default_height.map(Into::into),
719            );
720            merge(&mut settings.default_model, value.default_model);
721            settings.inline_assistant_model = value
722                .inline_assistant_model
723                .or(settings.inline_assistant_model.take());
724            settings.commit_message_model = value
725                .commit_message_model
726                .or(settings.commit_message_model.take());
727            settings.thread_summary_model = value
728                .thread_summary_model
729                .or(settings.thread_summary_model.take());
730            merge(&mut settings.inline_alternatives, value.inline_alternatives);
731            merge(
732                &mut settings.enable_experimental_live_diffs,
733                value.enable_experimental_live_diffs,
734            );
735            merge(
736                &mut settings.always_allow_tool_actions,
737                value.always_allow_tool_actions,
738            );
739            merge(
740                &mut settings.notify_when_agent_waiting,
741                value.notify_when_agent_waiting,
742            );
743            merge(&mut settings.stream_edits, value.stream_edits);
744            merge(&mut settings.single_file_review, value.single_file_review);
745            merge(&mut settings.default_profile, value.default_profile);
746
747            if let Some(profiles) = value.profiles {
748                settings
749                    .profiles
750                    .extend(profiles.into_iter().map(|(id, profile)| {
751                        (
752                            id,
753                            AgentProfile {
754                                name: profile.name.into(),
755                                tools: profile.tools,
756                                enable_all_context_servers: profile
757                                    .enable_all_context_servers
758                                    .unwrap_or_default(),
759                                context_servers: profile
760                                    .context_servers
761                                    .into_iter()
762                                    .map(|(context_server_id, preset)| {
763                                        (
764                                            context_server_id,
765                                            ContextServerPreset {
766                                                tools: preset.tools.clone(),
767                                            },
768                                        )
769                                    })
770                                    .collect(),
771                            },
772                        )
773                    }));
774            }
775        }
776
777        Ok(settings)
778    }
779
780    fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
781        if let Some(b) = vscode
782            .read_value("chat.agent.enabled")
783            .and_then(|b| b.as_bool())
784        {
785            match &mut current.inner {
786                Some(AssistantSettingsContentInner::Versioned(versioned)) => {
787                    match versioned.as_mut() {
788                        VersionedAssistantSettingsContent::V1(setting) => {
789                            setting.enabled = Some(b);
790                            setting.button = Some(b);
791                        }
792
793                        VersionedAssistantSettingsContent::V2(setting) => {
794                            setting.enabled = Some(b);
795                            setting.button = Some(b);
796                        }
797                    }
798                }
799                Some(AssistantSettingsContentInner::Legacy(setting)) => setting.button = Some(b),
800                None => {
801                    current.inner = Some(AssistantSettingsContentInner::for_v2(
802                        AssistantSettingsContentV2 {
803                            enabled: Some(b),
804                            button: Some(b),
805                            ..Default::default()
806                        },
807                    ));
808                }
809            }
810        }
811    }
812}
813
814fn merge<T>(target: &mut T, value: Option<T>) {
815    if let Some(value) = value {
816        *target = value;
817    }
818}
819
820#[cfg(test)]
821mod tests {
822    use fs::Fs;
823    use gpui::{ReadGlobal, TestAppContext};
824
825    use super::*;
826
827    #[gpui::test]
828    async fn test_deserialize_assistant_settings_with_version(cx: &mut TestAppContext) {
829        let fs = fs::FakeFs::new(cx.executor().clone());
830        fs.create_dir(paths::settings_file().parent().unwrap())
831            .await
832            .unwrap();
833
834        cx.update(|cx| {
835            let test_settings = settings::SettingsStore::test(cx);
836            cx.set_global(test_settings);
837            AssistantSettings::register(cx);
838        });
839
840        cx.update(|cx| {
841            assert!(!AssistantSettings::get_global(cx).using_outdated_settings_version);
842            assert_eq!(
843                AssistantSettings::get_global(cx).default_model,
844                LanguageModelSelection {
845                    provider: "zed.dev".into(),
846                    model: "claude-3-7-sonnet-latest".into(),
847                }
848            );
849        });
850
851        cx.update(|cx| {
852            settings::SettingsStore::global(cx).update_settings_file::<AssistantSettings>(
853                fs.clone(),
854                |settings, _| {
855                    *settings = AssistantSettingsContent {
856                        inner: Some(AssistantSettingsContentInner::for_v2(
857                            AssistantSettingsContentV2 {
858                                default_model: Some(LanguageModelSelection {
859                                    provider: "test-provider".into(),
860                                    model: "gpt-99".into(),
861                                }),
862                                inline_assistant_model: None,
863                                commit_message_model: None,
864                                thread_summary_model: None,
865                                inline_alternatives: None,
866                                enabled: None,
867                                button: None,
868                                dock: None,
869                                default_width: None,
870                                default_height: None,
871                                enable_experimental_live_diffs: None,
872                                default_profile: None,
873                                profiles: None,
874                                always_allow_tool_actions: None,
875                                notify_when_agent_waiting: None,
876                                stream_edits: None,
877                                single_file_review: None,
878                            },
879                        )),
880                    }
881                },
882            );
883        });
884
885        cx.run_until_parked();
886
887        let raw_settings_value = fs.load(paths::settings_file()).await.unwrap();
888        assert!(raw_settings_value.contains(r#""version": "2""#));
889
890        #[derive(Debug, Deserialize)]
891        struct AssistantSettingsTest {
892            assistant: AssistantSettingsContent,
893        }
894
895        let assistant_settings: AssistantSettingsTest =
896            serde_json_lenient::from_str(&raw_settings_value).unwrap();
897
898        assert!(!assistant_settings.assistant.is_version_outdated());
899    }
900}