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