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