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