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