assistant_settings.rs

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