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: Arc<str>,
85 pub profiles: IndexMap<Arc<str>, 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_profile(&mut self, profile_id: Arc<str>) {
329 let AssistantSettingsContent::Versioned(VersionedAssistantSettingsContent::V2(settings)) =
330 self
331 else {
332 return;
333 };
334
335 settings.default_profile = Some(profile_id);
336 }
337
338 pub fn create_profile(&mut self, profile_id: Arc<str>, profile: AgentProfile) -> Result<()> {
339 let AssistantSettingsContent::Versioned(VersionedAssistantSettingsContent::V2(settings)) =
340 self
341 else {
342 return Ok(());
343 };
344
345 let profiles = settings.profiles.get_or_insert_default();
346 if profiles.contains_key(&profile_id) {
347 bail!("profile with ID '{profile_id}' already exists");
348 }
349
350 profiles.insert(
351 profile_id,
352 AgentProfileContent {
353 name: profile.name.into(),
354 tools: profile.tools,
355 enable_all_context_servers: Some(profile.enable_all_context_servers),
356 context_servers: profile
357 .context_servers
358 .into_iter()
359 .map(|(server_id, preset)| {
360 (
361 server_id,
362 ContextServerPresetContent {
363 tools: preset.tools,
364 },
365 )
366 })
367 .collect(),
368 },
369 );
370
371 Ok(())
372 }
373}
374
375#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
376#[serde(tag = "version")]
377pub enum VersionedAssistantSettingsContent {
378 #[serde(rename = "1")]
379 V1(AssistantSettingsContentV1),
380 #[serde(rename = "2")]
381 V2(AssistantSettingsContentV2),
382}
383
384impl Default for VersionedAssistantSettingsContent {
385 fn default() -> Self {
386 Self::V2(AssistantSettingsContentV2 {
387 enabled: None,
388 button: None,
389 dock: None,
390 default_width: None,
391 default_height: None,
392 default_model: None,
393 editor_model: None,
394 inline_alternatives: None,
395 enable_experimental_live_diffs: None,
396 default_profile: None,
397 profiles: None,
398 always_allow_tool_actions: None,
399 notify_when_agent_waiting: None,
400 })
401 }
402}
403
404#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
405pub struct AssistantSettingsContentV2 {
406 /// Whether the Assistant is enabled.
407 ///
408 /// Default: true
409 enabled: Option<bool>,
410 /// Whether to show the assistant panel button in the status bar.
411 ///
412 /// Default: true
413 button: Option<bool>,
414 /// Where to dock the assistant.
415 ///
416 /// Default: right
417 dock: Option<AssistantDockPosition>,
418 /// Default width in pixels when the assistant is docked to the left or right.
419 ///
420 /// Default: 640
421 default_width: Option<f32>,
422 /// Default height in pixels when the assistant is docked to the bottom.
423 ///
424 /// Default: 320
425 default_height: Option<f32>,
426 /// The default model to use when creating new chats.
427 default_model: Option<LanguageModelSelection>,
428 /// The model to use when applying edits from the assistant.
429 editor_model: Option<LanguageModelSelection>,
430 /// Additional models with which to generate alternatives when performing inline assists.
431 inline_alternatives: Option<Vec<LanguageModelSelection>>,
432 /// Enable experimental live diffs in the assistant panel.
433 ///
434 /// Default: false
435 enable_experimental_live_diffs: Option<bool>,
436 #[schemars(skip)]
437 default_profile: Option<Arc<str>>,
438 #[schemars(skip)]
439 pub profiles: Option<IndexMap<Arc<str>, AgentProfileContent>>,
440 /// Whenever a tool action would normally wait for your confirmation
441 /// that you allow it, always choose to allow it.
442 ///
443 /// Default: false
444 always_allow_tool_actions: Option<bool>,
445 /// Where to show a popup notification when the agent is waiting for user input.
446 ///
447 /// Default: "primary_screen"
448 notify_when_agent_waiting: Option<NotifyWhenAgentWaiting>,
449}
450
451#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
452pub struct LanguageModelSelection {
453 #[schemars(schema_with = "providers_schema")]
454 pub provider: String,
455 pub model: String,
456}
457
458fn providers_schema(_: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
459 schemars::schema::SchemaObject {
460 enum_values: Some(vec![
461 "anthropic".into(),
462 "bedrock".into(),
463 "google".into(),
464 "lmstudio".into(),
465 "ollama".into(),
466 "openai".into(),
467 "zed.dev".into(),
468 "copilot_chat".into(),
469 "deepseek".into(),
470 ]),
471 ..Default::default()
472 }
473 .into()
474}
475
476impl Default for LanguageModelSelection {
477 fn default() -> Self {
478 Self {
479 provider: "openai".to_string(),
480 model: "gpt-4".to_string(),
481 }
482 }
483}
484
485#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)]
486pub struct AgentProfileContent {
487 pub name: Arc<str>,
488 pub tools: IndexMap<Arc<str>, bool>,
489 /// Whether all context servers are enabled by default.
490 pub enable_all_context_servers: Option<bool>,
491 #[serde(default)]
492 pub context_servers: IndexMap<Arc<str>, ContextServerPresetContent>,
493}
494
495#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema)]
496pub struct ContextServerPresetContent {
497 pub tools: IndexMap<Arc<str>, bool>,
498}
499
500#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
501pub struct AssistantSettingsContentV1 {
502 /// Whether the Assistant is enabled.
503 ///
504 /// Default: true
505 enabled: Option<bool>,
506 /// Whether to show the assistant panel button in the status bar.
507 ///
508 /// Default: true
509 button: Option<bool>,
510 /// Where to dock the assistant.
511 ///
512 /// Default: right
513 dock: Option<AssistantDockPosition>,
514 /// Default width in pixels when the assistant is docked to the left or right.
515 ///
516 /// Default: 640
517 default_width: Option<f32>,
518 /// Default height in pixels when the assistant is docked to the bottom.
519 ///
520 /// Default: 320
521 default_height: Option<f32>,
522 /// The provider of the assistant service.
523 ///
524 /// This can be "openai", "anthropic", "ollama", "lmstudio", "deepseek", "zed.dev"
525 /// each with their respective default models and configurations.
526 provider: Option<AssistantProviderContentV1>,
527}
528
529#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
530pub struct LegacyAssistantSettingsContent {
531 /// Whether to show the assistant panel button in the status bar.
532 ///
533 /// Default: true
534 pub button: Option<bool>,
535 /// Where to dock the assistant.
536 ///
537 /// Default: right
538 pub dock: Option<AssistantDockPosition>,
539 /// Default width in pixels when the assistant is docked to the left or right.
540 ///
541 /// Default: 640
542 pub default_width: Option<f32>,
543 /// Default height in pixels when the assistant is docked to the bottom.
544 ///
545 /// Default: 320
546 pub default_height: Option<f32>,
547 /// The default OpenAI model to use when creating new chats.
548 ///
549 /// Default: gpt-4-1106-preview
550 pub default_open_ai_model: Option<OpenAiModel>,
551 /// OpenAI API base URL to use when creating new chats.
552 ///
553 /// Default: <https://api.openai.com/v1>
554 pub openai_api_url: Option<String>,
555}
556
557impl Settings for AssistantSettings {
558 const KEY: Option<&'static str> = Some("assistant");
559
560 const PRESERVED_KEYS: Option<&'static [&'static str]> = Some(&["version"]);
561
562 type FileContent = AssistantSettingsContent;
563
564 fn load(
565 sources: SettingsSources<Self::FileContent>,
566 _: &mut gpui::App,
567 ) -> anyhow::Result<Self> {
568 let mut settings = AssistantSettings::default();
569
570 for value in sources.defaults_and_customizations() {
571 if value.is_version_outdated() {
572 settings.using_outdated_settings_version = true;
573 }
574
575 let value = value.upgrade();
576 merge(&mut settings.enabled, value.enabled);
577 merge(&mut settings.button, value.button);
578 merge(&mut settings.dock, value.dock);
579 merge(
580 &mut settings.default_width,
581 value.default_width.map(Into::into),
582 );
583 merge(
584 &mut settings.default_height,
585 value.default_height.map(Into::into),
586 );
587 merge(&mut settings.default_model, value.default_model);
588 merge(&mut settings.editor_model, value.editor_model);
589 merge(&mut settings.inline_alternatives, value.inline_alternatives);
590 merge(
591 &mut settings.enable_experimental_live_diffs,
592 value.enable_experimental_live_diffs,
593 );
594 merge(
595 &mut settings.always_allow_tool_actions,
596 value.always_allow_tool_actions,
597 );
598 merge(
599 &mut settings.notify_when_agent_waiting,
600 value.notify_when_agent_waiting,
601 );
602 merge(&mut settings.default_profile, value.default_profile);
603
604 if let Some(profiles) = value.profiles {
605 settings
606 .profiles
607 .extend(profiles.into_iter().map(|(id, profile)| {
608 (
609 id,
610 AgentProfile {
611 name: profile.name.into(),
612 tools: profile.tools,
613 enable_all_context_servers: profile
614 .enable_all_context_servers
615 .unwrap_or_default(),
616 context_servers: profile
617 .context_servers
618 .into_iter()
619 .map(|(context_server_id, preset)| {
620 (
621 context_server_id,
622 ContextServerPreset {
623 tools: preset.tools.clone(),
624 },
625 )
626 })
627 .collect(),
628 },
629 )
630 }));
631 }
632 }
633
634 Ok(settings)
635 }
636}
637
638fn merge<T>(target: &mut T, value: Option<T>) {
639 if let Some(value) = value {
640 *target = value;
641 }
642}
643
644#[cfg(test)]
645mod tests {
646 use fs::Fs;
647 use gpui::{ReadGlobal, TestAppContext};
648
649 use super::*;
650
651 #[gpui::test]
652 async fn test_deserialize_assistant_settings_with_version(cx: &mut TestAppContext) {
653 let fs = fs::FakeFs::new(cx.executor().clone());
654 fs.create_dir(paths::settings_file().parent().unwrap())
655 .await
656 .unwrap();
657
658 cx.update(|cx| {
659 let test_settings = settings::SettingsStore::test(cx);
660 cx.set_global(test_settings);
661 AssistantSettings::register(cx);
662 });
663
664 cx.update(|cx| {
665 assert!(!AssistantSettings::get_global(cx).using_outdated_settings_version);
666 assert_eq!(
667 AssistantSettings::get_global(cx).default_model,
668 LanguageModelSelection {
669 provider: "zed.dev".into(),
670 model: "claude-3-5-sonnet-latest".into(),
671 }
672 );
673 });
674
675 cx.update(|cx| {
676 settings::SettingsStore::global(cx).update_settings_file::<AssistantSettings>(
677 fs.clone(),
678 |settings, _| {
679 *settings = AssistantSettingsContent::Versioned(
680 VersionedAssistantSettingsContent::V2(AssistantSettingsContentV2 {
681 default_model: Some(LanguageModelSelection {
682 provider: "test-provider".into(),
683 model: "gpt-99".into(),
684 }),
685 editor_model: Some(LanguageModelSelection {
686 provider: "test-provider".into(),
687 model: "gpt-99".into(),
688 }),
689 inline_alternatives: None,
690 enabled: None,
691 button: None,
692 dock: None,
693 default_width: None,
694 default_height: None,
695 enable_experimental_live_diffs: None,
696 default_profile: None,
697 profiles: None,
698 always_allow_tool_actions: None,
699 notify_when_agent_waiting: None,
700 }),
701 )
702 },
703 );
704 });
705
706 cx.run_until_parked();
707
708 let raw_settings_value = fs.load(paths::settings_file()).await.unwrap();
709 assert!(raw_settings_value.contains(r#""version": "2""#));
710
711 #[derive(Debug, Deserialize)]
712 struct AssistantSettingsTest {
713 assistant: AssistantSettingsContent,
714 }
715
716 let assistant_settings: AssistantSettingsTest =
717 serde_json_lenient::from_str(&raw_settings_value).unwrap();
718
719 assert!(!assistant_settings.assistant.is_version_outdated());
720 }
721}