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