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 /// The default profile to use in the Agent.
437 ///
438 /// Default: write
439 default_profile: Option<Arc<str>>,
440 /// The available agent profiles.
441 pub profiles: Option<IndexMap<Arc<str>, AgentProfileContent>>,
442 /// Whenever a tool action would normally wait for your confirmation
443 /// that you allow it, always choose to allow it.
444 ///
445 /// Default: false
446 always_allow_tool_actions: Option<bool>,
447 /// Where to show a popup notification when the agent is waiting for user input.
448 ///
449 /// Default: "primary_screen"
450 notify_when_agent_waiting: Option<NotifyWhenAgentWaiting>,
451}
452
453#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
454pub struct LanguageModelSelection {
455 #[schemars(schema_with = "providers_schema")]
456 pub provider: String,
457 pub model: String,
458}
459
460fn providers_schema(_: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
461 schemars::schema::SchemaObject {
462 enum_values: Some(vec![
463 "anthropic".into(),
464 "bedrock".into(),
465 "google".into(),
466 "lmstudio".into(),
467 "ollama".into(),
468 "openai".into(),
469 "zed.dev".into(),
470 "copilot_chat".into(),
471 "deepseek".into(),
472 ]),
473 ..Default::default()
474 }
475 .into()
476}
477
478impl Default for LanguageModelSelection {
479 fn default() -> Self {
480 Self {
481 provider: "openai".to_string(),
482 model: "gpt-4".to_string(),
483 }
484 }
485}
486
487#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)]
488pub struct AgentProfileContent {
489 pub name: Arc<str>,
490 #[serde(default)]
491 pub tools: IndexMap<Arc<str>, bool>,
492 /// Whether all context servers are enabled by default.
493 pub enable_all_context_servers: Option<bool>,
494 #[serde(default)]
495 pub context_servers: IndexMap<Arc<str>, ContextServerPresetContent>,
496}
497
498#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema)]
499pub struct ContextServerPresetContent {
500 pub tools: IndexMap<Arc<str>, bool>,
501}
502
503#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
504pub struct AssistantSettingsContentV1 {
505 /// Whether the Assistant is enabled.
506 ///
507 /// Default: true
508 enabled: Option<bool>,
509 /// Whether to show the assistant panel button in the status bar.
510 ///
511 /// Default: true
512 button: Option<bool>,
513 /// Where to dock the assistant.
514 ///
515 /// Default: right
516 dock: Option<AssistantDockPosition>,
517 /// Default width in pixels when the assistant is docked to the left or right.
518 ///
519 /// Default: 640
520 default_width: Option<f32>,
521 /// Default height in pixels when the assistant is docked to the bottom.
522 ///
523 /// Default: 320
524 default_height: Option<f32>,
525 /// The provider of the assistant service.
526 ///
527 /// This can be "openai", "anthropic", "ollama", "lmstudio", "deepseek", "zed.dev"
528 /// each with their respective default models and configurations.
529 provider: Option<AssistantProviderContentV1>,
530}
531
532#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
533pub struct LegacyAssistantSettingsContent {
534 /// Whether to show the assistant panel button in the status bar.
535 ///
536 /// Default: true
537 pub button: Option<bool>,
538 /// Where to dock the assistant.
539 ///
540 /// Default: right
541 pub dock: Option<AssistantDockPosition>,
542 /// Default width in pixels when the assistant is docked to the left or right.
543 ///
544 /// Default: 640
545 pub default_width: Option<f32>,
546 /// Default height in pixels when the assistant is docked to the bottom.
547 ///
548 /// Default: 320
549 pub default_height: Option<f32>,
550 /// The default OpenAI model to use when creating new chats.
551 ///
552 /// Default: gpt-4-1106-preview
553 pub default_open_ai_model: Option<OpenAiModel>,
554 /// OpenAI API base URL to use when creating new chats.
555 ///
556 /// Default: <https://api.openai.com/v1>
557 pub openai_api_url: Option<String>,
558}
559
560impl Settings for AssistantSettings {
561 const KEY: Option<&'static str> = Some("assistant");
562
563 const PRESERVED_KEYS: Option<&'static [&'static str]> = Some(&["version"]);
564
565 type FileContent = AssistantSettingsContent;
566
567 fn load(
568 sources: SettingsSources<Self::FileContent>,
569 _: &mut gpui::App,
570 ) -> anyhow::Result<Self> {
571 let mut settings = AssistantSettings::default();
572
573 for value in sources.defaults_and_customizations() {
574 if value.is_version_outdated() {
575 settings.using_outdated_settings_version = true;
576 }
577
578 let value = value.upgrade();
579 merge(&mut settings.enabled, value.enabled);
580 merge(&mut settings.button, value.button);
581 merge(&mut settings.dock, value.dock);
582 merge(
583 &mut settings.default_width,
584 value.default_width.map(Into::into),
585 );
586 merge(
587 &mut settings.default_height,
588 value.default_height.map(Into::into),
589 );
590 merge(&mut settings.default_model, value.default_model);
591 merge(&mut settings.editor_model, value.editor_model);
592 merge(&mut settings.inline_alternatives, value.inline_alternatives);
593 merge(
594 &mut settings.enable_experimental_live_diffs,
595 value.enable_experimental_live_diffs,
596 );
597 merge(
598 &mut settings.always_allow_tool_actions,
599 value.always_allow_tool_actions,
600 );
601 merge(
602 &mut settings.notify_when_agent_waiting,
603 value.notify_when_agent_waiting,
604 );
605 merge(&mut settings.default_profile, value.default_profile);
606
607 if let Some(profiles) = value.profiles {
608 settings
609 .profiles
610 .extend(profiles.into_iter().map(|(id, profile)| {
611 (
612 id,
613 AgentProfile {
614 name: profile.name.into(),
615 tools: profile.tools,
616 enable_all_context_servers: profile
617 .enable_all_context_servers
618 .unwrap_or_default(),
619 context_servers: profile
620 .context_servers
621 .into_iter()
622 .map(|(context_server_id, preset)| {
623 (
624 context_server_id,
625 ContextServerPreset {
626 tools: preset.tools.clone(),
627 },
628 )
629 })
630 .collect(),
631 },
632 )
633 }));
634 }
635 }
636
637 Ok(settings)
638 }
639}
640
641fn merge<T>(target: &mut T, value: Option<T>) {
642 if let Some(value) = value {
643 *target = value;
644 }
645}
646
647#[cfg(test)]
648mod tests {
649 use fs::Fs;
650 use gpui::{ReadGlobal, TestAppContext};
651
652 use super::*;
653
654 #[gpui::test]
655 async fn test_deserialize_assistant_settings_with_version(cx: &mut TestAppContext) {
656 let fs = fs::FakeFs::new(cx.executor().clone());
657 fs.create_dir(paths::settings_file().parent().unwrap())
658 .await
659 .unwrap();
660
661 cx.update(|cx| {
662 let test_settings = settings::SettingsStore::test(cx);
663 cx.set_global(test_settings);
664 AssistantSettings::register(cx);
665 });
666
667 cx.update(|cx| {
668 assert!(!AssistantSettings::get_global(cx).using_outdated_settings_version);
669 assert_eq!(
670 AssistantSettings::get_global(cx).default_model,
671 LanguageModelSelection {
672 provider: "zed.dev".into(),
673 model: "claude-3-5-sonnet-latest".into(),
674 }
675 );
676 });
677
678 cx.update(|cx| {
679 settings::SettingsStore::global(cx).update_settings_file::<AssistantSettings>(
680 fs.clone(),
681 |settings, _| {
682 *settings = AssistantSettingsContent::Versioned(
683 VersionedAssistantSettingsContent::V2(AssistantSettingsContentV2 {
684 default_model: Some(LanguageModelSelection {
685 provider: "test-provider".into(),
686 model: "gpt-99".into(),
687 }),
688 editor_model: Some(LanguageModelSelection {
689 provider: "test-provider".into(),
690 model: "gpt-99".into(),
691 }),
692 inline_alternatives: None,
693 enabled: None,
694 button: None,
695 dock: None,
696 default_width: None,
697 default_height: None,
698 enable_experimental_live_diffs: None,
699 default_profile: None,
700 profiles: None,
701 always_allow_tool_actions: None,
702 notify_when_agent_waiting: None,
703 }),
704 )
705 },
706 );
707 });
708
709 cx.run_until_parked();
710
711 let raw_settings_value = fs.load(paths::settings_file()).await.unwrap();
712 assert!(raw_settings_value.contains(r#""version": "2""#));
713
714 #[derive(Debug, Deserialize)]
715 struct AssistantSettingsTest {
716 assistant: AssistantSettingsContent,
717 }
718
719 let assistant_settings: AssistantSettingsTest =
720 serde_json_lenient::from_str(&raw_settings_value).unwrap();
721
722 assert!(!assistant_settings.assistant.is_version_outdated());
723 }
724}