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}