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::{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(Debug, Default)]
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 editor_model: LanguageModelSelection,
81 pub inline_alternatives: Vec<LanguageModelSelection>,
82 pub using_outdated_settings_version: bool,
83 pub enable_experimental_live_diffs: bool,
84 pub default_profile: AgentProfileId,
85 pub profiles: IndexMap<AgentProfileId, AgentProfile>,
86 pub always_allow_tool_actions: bool,
87 pub notify_when_agent_waiting: NotifyWhenAgentWaiting,
88}
89
90impl AssistantSettings {
91 pub fn are_live_diffs_enabled(&self, cx: &App) -> bool {
92 if cx.has_flag::<Assistant2FeatureFlag>() {
93 return false;
94 }
95
96 cx.is_staff() || self.enable_experimental_live_diffs
97 }
98}
99
100/// Assistant panel settings
101#[derive(Clone, Serialize, Deserialize, Debug)]
102#[serde(untagged)]
103pub enum AssistantSettingsContent {
104 Versioned(VersionedAssistantSettingsContent),
105 Legacy(LegacyAssistantSettingsContent),
106}
107
108impl JsonSchema for AssistantSettingsContent {
109 fn schema_name() -> String {
110 VersionedAssistantSettingsContent::schema_name()
111 }
112
113 fn json_schema(r#gen: &mut schemars::r#gen::SchemaGenerator) -> Schema {
114 VersionedAssistantSettingsContent::json_schema(r#gen)
115 }
116
117 fn is_referenceable() -> bool {
118 VersionedAssistantSettingsContent::is_referenceable()
119 }
120}
121
122impl Default for AssistantSettingsContent {
123 fn default() -> Self {
124 Self::Versioned(VersionedAssistantSettingsContent::default())
125 }
126}
127
128impl AssistantSettingsContent {
129 pub fn is_version_outdated(&self) -> bool {
130 match self {
131 AssistantSettingsContent::Versioned(settings) => match settings {
132 VersionedAssistantSettingsContent::V1(_) => true,
133 VersionedAssistantSettingsContent::V2(_) => false,
134 },
135 AssistantSettingsContent::Legacy(_) => true,
136 }
137 }
138
139 fn upgrade(&self) -> AssistantSettingsContentV2 {
140 match self {
141 AssistantSettingsContent::Versioned(settings) => match settings {
142 VersionedAssistantSettingsContent::V1(settings) => AssistantSettingsContentV2 {
143 enabled: settings.enabled,
144 button: settings.button,
145 dock: settings.dock,
146 default_width: settings.default_width,
147 default_height: settings.default_width,
148 default_model: settings
149 .provider
150 .clone()
151 .and_then(|provider| match provider {
152 AssistantProviderContentV1::ZedDotDev { default_model } => {
153 default_model.map(|model| LanguageModelSelection {
154 provider: "zed.dev".to_string(),
155 model: model.id().to_string(),
156 })
157 }
158 AssistantProviderContentV1::OpenAi { default_model, .. } => {
159 default_model.map(|model| LanguageModelSelection {
160 provider: "openai".to_string(),
161 model: model.id().to_string(),
162 })
163 }
164 AssistantProviderContentV1::Anthropic { default_model, .. } => {
165 default_model.map(|model| LanguageModelSelection {
166 provider: "anthropic".to_string(),
167 model: model.id().to_string(),
168 })
169 }
170 AssistantProviderContentV1::Ollama { default_model, .. } => {
171 default_model.map(|model| LanguageModelSelection {
172 provider: "ollama".to_string(),
173 model: model.id().to_string(),
174 })
175 }
176 AssistantProviderContentV1::LmStudio { default_model, .. } => {
177 default_model.map(|model| LanguageModelSelection {
178 provider: "lmstudio".to_string(),
179 model: model.id().to_string(),
180 })
181 }
182 AssistantProviderContentV1::DeepSeek { default_model, .. } => {
183 default_model.map(|model| LanguageModelSelection {
184 provider: "deepseek".to_string(),
185 model: model.id().to_string(),
186 })
187 }
188 }),
189 editor_model: None,
190 inline_alternatives: None,
191 enable_experimental_live_diffs: None,
192 default_profile: None,
193 profiles: None,
194 always_allow_tool_actions: None,
195 notify_when_agent_waiting: None,
196 },
197 VersionedAssistantSettingsContent::V2(settings) => settings.clone(),
198 },
199 AssistantSettingsContent::Legacy(settings) => AssistantSettingsContentV2 {
200 enabled: None,
201 button: settings.button,
202 dock: settings.dock,
203 default_width: settings.default_width,
204 default_height: settings.default_height,
205 default_model: Some(LanguageModelSelection {
206 provider: "openai".to_string(),
207 model: settings
208 .default_open_ai_model
209 .clone()
210 .unwrap_or_default()
211 .id()
212 .to_string(),
213 }),
214 editor_model: None,
215 inline_alternatives: None,
216 enable_experimental_live_diffs: None,
217 default_profile: None,
218 profiles: None,
219 always_allow_tool_actions: None,
220 notify_when_agent_waiting: None,
221 },
222 }
223 }
224
225 pub fn set_dock(&mut self, dock: AssistantDockPosition) {
226 match self {
227 AssistantSettingsContent::Versioned(settings) => match settings {
228 VersionedAssistantSettingsContent::V1(settings) => {
229 settings.dock = Some(dock);
230 }
231 VersionedAssistantSettingsContent::V2(settings) => {
232 settings.dock = Some(dock);
233 }
234 },
235 AssistantSettingsContent::Legacy(settings) => {
236 settings.dock = Some(dock);
237 }
238 }
239 }
240
241 pub fn set_model(&mut self, language_model: Arc<dyn LanguageModel>) {
242 let model = language_model.id().0.to_string();
243 let provider = language_model.provider_id().0.to_string();
244
245 match self {
246 AssistantSettingsContent::Versioned(settings) => match settings {
247 VersionedAssistantSettingsContent::V1(settings) => match provider.as_ref() {
248 "zed.dev" => {
249 log::warn!("attempted to set zed.dev model on outdated settings");
250 }
251 "anthropic" => {
252 let api_url = match &settings.provider {
253 Some(AssistantProviderContentV1::Anthropic { api_url, .. }) => {
254 api_url.clone()
255 }
256 _ => None,
257 };
258 settings.provider = Some(AssistantProviderContentV1::Anthropic {
259 default_model: AnthropicModel::from_id(&model).ok(),
260 api_url,
261 });
262 }
263 "ollama" => {
264 let api_url = match &settings.provider {
265 Some(AssistantProviderContentV1::Ollama { api_url, .. }) => {
266 api_url.clone()
267 }
268 _ => None,
269 };
270 settings.provider = Some(AssistantProviderContentV1::Ollama {
271 default_model: Some(ollama::Model::new(&model, None, None)),
272 api_url,
273 });
274 }
275 "lmstudio" => {
276 let api_url = match &settings.provider {
277 Some(AssistantProviderContentV1::LmStudio { api_url, .. }) => {
278 api_url.clone()
279 }
280 _ => None,
281 };
282 settings.provider = Some(AssistantProviderContentV1::LmStudio {
283 default_model: Some(lmstudio::Model::new(&model, None, None)),
284 api_url,
285 });
286 }
287 "openai" => {
288 let (api_url, available_models) = match &settings.provider {
289 Some(AssistantProviderContentV1::OpenAi {
290 api_url,
291 available_models,
292 ..
293 }) => (api_url.clone(), available_models.clone()),
294 _ => (None, None),
295 };
296 settings.provider = Some(AssistantProviderContentV1::OpenAi {
297 default_model: OpenAiModel::from_id(&model).ok(),
298 api_url,
299 available_models,
300 });
301 }
302 "deepseek" => {
303 let api_url = match &settings.provider {
304 Some(AssistantProviderContentV1::DeepSeek { api_url, .. }) => {
305 api_url.clone()
306 }
307 _ => None,
308 };
309 settings.provider = Some(AssistantProviderContentV1::DeepSeek {
310 default_model: DeepseekModel::from_id(&model).ok(),
311 api_url,
312 });
313 }
314 _ => {}
315 },
316 VersionedAssistantSettingsContent::V2(settings) => {
317 settings.default_model = Some(LanguageModelSelection { provider, model });
318 }
319 },
320 AssistantSettingsContent::Legacy(settings) => {
321 if let Ok(model) = OpenAiModel::from_id(&language_model.id().0) {
322 settings.default_open_ai_model = Some(model);
323 }
324 }
325 }
326 }
327
328 pub fn set_always_allow_tool_actions(&mut self, allow: bool) {
329 let AssistantSettingsContent::Versioned(VersionedAssistantSettingsContent::V2(settings)) =
330 self
331 else {
332 return;
333 };
334 settings.always_allow_tool_actions = Some(allow);
335 }
336
337 pub fn set_profile(&mut self, profile_id: AgentProfileId) {
338 let AssistantSettingsContent::Versioned(VersionedAssistantSettingsContent::V2(settings)) =
339 self
340 else {
341 return;
342 };
343
344 settings.default_profile = Some(profile_id);
345 }
346
347 pub fn create_profile(
348 &mut self,
349 profile_id: AgentProfileId,
350 profile: AgentProfile,
351 ) -> Result<()> {
352 let AssistantSettingsContent::Versioned(VersionedAssistantSettingsContent::V2(settings)) =
353 self
354 else {
355 return Ok(());
356 };
357
358 let profiles = settings.profiles.get_or_insert_default();
359 if profiles.contains_key(&profile_id) {
360 bail!("profile with ID '{profile_id}' already exists");
361 }
362
363 profiles.insert(
364 profile_id,
365 AgentProfileContent {
366 name: profile.name.into(),
367 tools: profile.tools,
368 enable_all_context_servers: Some(profile.enable_all_context_servers),
369 context_servers: profile
370 .context_servers
371 .into_iter()
372 .map(|(server_id, preset)| {
373 (
374 server_id,
375 ContextServerPresetContent {
376 tools: preset.tools,
377 },
378 )
379 })
380 .collect(),
381 },
382 );
383
384 Ok(())
385 }
386}
387
388#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
389#[serde(tag = "version")]
390pub enum VersionedAssistantSettingsContent {
391 #[serde(rename = "1")]
392 V1(AssistantSettingsContentV1),
393 #[serde(rename = "2")]
394 V2(AssistantSettingsContentV2),
395}
396
397impl Default for VersionedAssistantSettingsContent {
398 fn default() -> Self {
399 Self::V2(AssistantSettingsContentV2 {
400 enabled: None,
401 button: None,
402 dock: None,
403 default_width: None,
404 default_height: None,
405 default_model: None,
406 editor_model: None,
407 inline_alternatives: None,
408 enable_experimental_live_diffs: None,
409 default_profile: None,
410 profiles: None,
411 always_allow_tool_actions: None,
412 notify_when_agent_waiting: None,
413 })
414 }
415}
416
417#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
418pub struct AssistantSettingsContentV2 {
419 /// Whether the Assistant is enabled.
420 ///
421 /// Default: true
422 enabled: Option<bool>,
423 /// Whether to show the assistant panel button in the status bar.
424 ///
425 /// Default: true
426 button: Option<bool>,
427 /// Where to dock the assistant.
428 ///
429 /// Default: right
430 dock: Option<AssistantDockPosition>,
431 /// Default width in pixels when the assistant is docked to the left or right.
432 ///
433 /// Default: 640
434 default_width: Option<f32>,
435 /// Default height in pixels when the assistant is docked to the bottom.
436 ///
437 /// Default: 320
438 default_height: Option<f32>,
439 /// The default model to use when creating new chats.
440 default_model: Option<LanguageModelSelection>,
441 /// The model to use when applying edits from the assistant.
442 editor_model: Option<LanguageModelSelection>,
443 /// Additional models with which to generate alternatives when performing inline assists.
444 inline_alternatives: Option<Vec<LanguageModelSelection>>,
445 /// Enable experimental live diffs in the assistant panel.
446 ///
447 /// Default: false
448 enable_experimental_live_diffs: Option<bool>,
449 /// The default profile to use in the Agent.
450 ///
451 /// Default: write
452 default_profile: Option<AgentProfileId>,
453 /// The available agent profiles.
454 pub profiles: Option<IndexMap<AgentProfileId, AgentProfileContent>>,
455 /// Whenever a tool action would normally wait for your confirmation
456 /// that you allow it, always choose to allow it.
457 ///
458 /// Default: false
459 always_allow_tool_actions: Option<bool>,
460 /// Where to show a popup notification when the agent is waiting for user input.
461 ///
462 /// Default: "primary_screen"
463 notify_when_agent_waiting: Option<NotifyWhenAgentWaiting>,
464}
465
466#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
467pub struct LanguageModelSelection {
468 #[schemars(schema_with = "providers_schema")]
469 pub provider: String,
470 pub model: String,
471}
472
473fn providers_schema(_: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
474 schemars::schema::SchemaObject {
475 enum_values: Some(vec![
476 "anthropic".into(),
477 "bedrock".into(),
478 "google".into(),
479 "lmstudio".into(),
480 "ollama".into(),
481 "openai".into(),
482 "zed.dev".into(),
483 "copilot_chat".into(),
484 "deepseek".into(),
485 ]),
486 ..Default::default()
487 }
488 .into()
489}
490
491impl Default for LanguageModelSelection {
492 fn default() -> Self {
493 Self {
494 provider: "openai".to_string(),
495 model: "gpt-4".to_string(),
496 }
497 }
498}
499
500#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)]
501pub struct AgentProfileContent {
502 pub name: Arc<str>,
503 #[serde(default)]
504 pub tools: IndexMap<Arc<str>, bool>,
505 /// Whether all context servers are enabled by default.
506 pub enable_all_context_servers: Option<bool>,
507 #[serde(default)]
508 pub context_servers: IndexMap<Arc<str>, ContextServerPresetContent>,
509}
510
511#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema)]
512pub struct ContextServerPresetContent {
513 pub tools: IndexMap<Arc<str>, bool>,
514}
515
516#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
517pub struct AssistantSettingsContentV1 {
518 /// Whether the Assistant is enabled.
519 ///
520 /// Default: true
521 enabled: Option<bool>,
522 /// Whether to show the assistant panel button in the status bar.
523 ///
524 /// Default: true
525 button: Option<bool>,
526 /// Where to dock the assistant.
527 ///
528 /// Default: right
529 dock: Option<AssistantDockPosition>,
530 /// Default width in pixels when the assistant is docked to the left or right.
531 ///
532 /// Default: 640
533 default_width: Option<f32>,
534 /// Default height in pixels when the assistant is docked to the bottom.
535 ///
536 /// Default: 320
537 default_height: Option<f32>,
538 /// The provider of the assistant service.
539 ///
540 /// This can be "openai", "anthropic", "ollama", "lmstudio", "deepseek", "zed.dev"
541 /// each with their respective default models and configurations.
542 provider: Option<AssistantProviderContentV1>,
543}
544
545#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
546pub struct LegacyAssistantSettingsContent {
547 /// Whether to show the assistant panel button in the status bar.
548 ///
549 /// Default: true
550 pub button: Option<bool>,
551 /// Where to dock the assistant.
552 ///
553 /// Default: right
554 pub dock: Option<AssistantDockPosition>,
555 /// Default width in pixels when the assistant is docked to the left or right.
556 ///
557 /// Default: 640
558 pub default_width: Option<f32>,
559 /// Default height in pixels when the assistant is docked to the bottom.
560 ///
561 /// Default: 320
562 pub default_height: Option<f32>,
563 /// The default OpenAI model to use when creating new chats.
564 ///
565 /// Default: gpt-4-1106-preview
566 pub default_open_ai_model: Option<OpenAiModel>,
567 /// OpenAI API base URL to use when creating new chats.
568 ///
569 /// Default: <https://api.openai.com/v1>
570 pub openai_api_url: Option<String>,
571}
572
573impl Settings for AssistantSettings {
574 const KEY: Option<&'static str> = Some("assistant");
575
576 const PRESERVED_KEYS: Option<&'static [&'static str]> = Some(&["version"]);
577
578 type FileContent = AssistantSettingsContent;
579
580 fn load(
581 sources: SettingsSources<Self::FileContent>,
582 _: &mut gpui::App,
583 ) -> anyhow::Result<Self> {
584 let mut settings = AssistantSettings::default();
585
586 for value in sources.defaults_and_customizations() {
587 if value.is_version_outdated() {
588 settings.using_outdated_settings_version = true;
589 }
590
591 let value = value.upgrade();
592 merge(&mut settings.enabled, value.enabled);
593 merge(&mut settings.button, value.button);
594 merge(&mut settings.dock, value.dock);
595 merge(
596 &mut settings.default_width,
597 value.default_width.map(Into::into),
598 );
599 merge(
600 &mut settings.default_height,
601 value.default_height.map(Into::into),
602 );
603 merge(&mut settings.default_model, value.default_model);
604 merge(&mut settings.editor_model, value.editor_model);
605 merge(&mut settings.inline_alternatives, value.inline_alternatives);
606 merge(
607 &mut settings.enable_experimental_live_diffs,
608 value.enable_experimental_live_diffs,
609 );
610 merge(
611 &mut settings.always_allow_tool_actions,
612 value.always_allow_tool_actions,
613 );
614 merge(
615 &mut settings.notify_when_agent_waiting,
616 value.notify_when_agent_waiting,
617 );
618 merge(&mut settings.default_profile, value.default_profile);
619
620 if let Some(profiles) = value.profiles {
621 settings
622 .profiles
623 .extend(profiles.into_iter().map(|(id, profile)| {
624 (
625 id,
626 AgentProfile {
627 name: profile.name.into(),
628 tools: profile.tools,
629 enable_all_context_servers: profile
630 .enable_all_context_servers
631 .unwrap_or_default(),
632 context_servers: profile
633 .context_servers
634 .into_iter()
635 .map(|(context_server_id, preset)| {
636 (
637 context_server_id,
638 ContextServerPreset {
639 tools: preset.tools.clone(),
640 },
641 )
642 })
643 .collect(),
644 },
645 )
646 }));
647 }
648 }
649
650 Ok(settings)
651 }
652}
653
654fn merge<T>(target: &mut T, value: Option<T>) {
655 if let Some(value) = value {
656 *target = value;
657 }
658}
659
660#[cfg(test)]
661mod tests {
662 use fs::Fs;
663 use gpui::{ReadGlobal, TestAppContext};
664
665 use super::*;
666
667 #[gpui::test]
668 async fn test_deserialize_assistant_settings_with_version(cx: &mut TestAppContext) {
669 let fs = fs::FakeFs::new(cx.executor().clone());
670 fs.create_dir(paths::settings_file().parent().unwrap())
671 .await
672 .unwrap();
673
674 cx.update(|cx| {
675 let test_settings = settings::SettingsStore::test(cx);
676 cx.set_global(test_settings);
677 AssistantSettings::register(cx);
678 });
679
680 cx.update(|cx| {
681 assert!(!AssistantSettings::get_global(cx).using_outdated_settings_version);
682 assert_eq!(
683 AssistantSettings::get_global(cx).default_model,
684 LanguageModelSelection {
685 provider: "zed.dev".into(),
686 model: "claude-3-5-sonnet-latest".into(),
687 }
688 );
689 });
690
691 cx.update(|cx| {
692 settings::SettingsStore::global(cx).update_settings_file::<AssistantSettings>(
693 fs.clone(),
694 |settings, _| {
695 *settings = AssistantSettingsContent::Versioned(
696 VersionedAssistantSettingsContent::V2(AssistantSettingsContentV2 {
697 default_model: Some(LanguageModelSelection {
698 provider: "test-provider".into(),
699 model: "gpt-99".into(),
700 }),
701 editor_model: Some(LanguageModelSelection {
702 provider: "test-provider".into(),
703 model: "gpt-99".into(),
704 }),
705 inline_alternatives: None,
706 enabled: None,
707 button: None,
708 dock: None,
709 default_width: None,
710 default_height: None,
711 enable_experimental_live_diffs: None,
712 default_profile: None,
713 profiles: None,
714 always_allow_tool_actions: None,
715 notify_when_agent_waiting: None,
716 }),
717 )
718 },
719 );
720 });
721
722 cx.run_until_parked();
723
724 let raw_settings_value = fs.load(paths::settings_file()).await.unwrap();
725 assert!(raw_settings_value.contains(r#""version": "2""#));
726
727 #[derive(Debug, Deserialize)]
728 struct AssistantSettingsTest {
729 assistant: AssistantSettingsContent,
730 }
731
732 let assistant_settings: AssistantSettingsTest =
733 serde_json_lenient::from_str(&raw_settings_value).unwrap();
734
735 assert!(!assistant_settings.assistant.is_version_outdated());
736 }
737}