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