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