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}
416
417#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
418pub struct AssistantSettingsContentV1 {
419 /// Whether the Assistant is enabled.
420 ///
421 /// Default: true
422 enabled: Option<bool>,
423 /// Whether to show the assistant panel button in the status bar.
424 ///
425 /// Default: true
426 button: Option<bool>,
427 /// Where to dock the assistant.
428 ///
429 /// Default: right
430 dock: Option<AssistantDockPosition>,
431 /// Default width in pixels when the assistant is docked to the left or right.
432 ///
433 /// Default: 640
434 default_width: Option<f32>,
435 /// Default height in pixels when the assistant is docked to the bottom.
436 ///
437 /// Default: 320
438 default_height: Option<f32>,
439 /// The provider of the assistant service.
440 ///
441 /// This can be "openai", "anthropic", "ollama", "lmstudio", "deepseek", "zed.dev"
442 /// each with their respective default models and configurations.
443 provider: Option<AssistantProviderContentV1>,
444}
445
446#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
447pub struct LegacyAssistantSettingsContent {
448 /// Whether to show the assistant panel button in the status bar.
449 ///
450 /// Default: true
451 pub button: Option<bool>,
452 /// Where to dock the assistant.
453 ///
454 /// Default: right
455 pub dock: Option<AssistantDockPosition>,
456 /// Default width in pixels when the assistant is docked to the left or right.
457 ///
458 /// Default: 640
459 pub default_width: Option<f32>,
460 /// Default height in pixels when the assistant is docked to the bottom.
461 ///
462 /// Default: 320
463 pub default_height: Option<f32>,
464 /// The default OpenAI model to use when creating new chats.
465 ///
466 /// Default: gpt-4-1106-preview
467 pub default_open_ai_model: Option<OpenAiModel>,
468 /// OpenAI API base URL to use when creating new chats.
469 ///
470 /// Default: <https://api.openai.com/v1>
471 pub openai_api_url: Option<String>,
472}
473
474impl Settings for AssistantSettings {
475 const KEY: Option<&'static str> = Some("assistant");
476
477 const PRESERVED_KEYS: Option<&'static [&'static str]> = Some(&["version"]);
478
479 type FileContent = AssistantSettingsContent;
480
481 fn load(
482 sources: SettingsSources<Self::FileContent>,
483 _: &mut gpui::App,
484 ) -> anyhow::Result<Self> {
485 let mut settings = AssistantSettings::default();
486
487 for value in sources.defaults_and_customizations() {
488 if value.is_version_outdated() {
489 settings.using_outdated_settings_version = true;
490 }
491
492 let value = value.upgrade();
493 merge(&mut settings.enabled, value.enabled);
494 merge(&mut settings.button, value.button);
495 merge(&mut settings.dock, value.dock);
496 merge(
497 &mut settings.default_width,
498 value.default_width.map(Into::into),
499 );
500 merge(
501 &mut settings.default_height,
502 value.default_height.map(Into::into),
503 );
504 merge(&mut settings.default_model, value.default_model);
505 merge(&mut settings.editor_model, value.editor_model);
506 merge(&mut settings.inline_alternatives, value.inline_alternatives);
507 merge(
508 &mut settings.enable_experimental_live_diffs,
509 value.enable_experimental_live_diffs,
510 );
511 merge(
512 &mut settings.always_allow_tool_actions,
513 value.always_allow_tool_actions,
514 );
515
516 if let Some(profiles) = value.profiles {
517 settings
518 .profiles
519 .extend(profiles.into_iter().map(|(id, profile)| {
520 (
521 id,
522 AgentProfile {
523 name: profile.name.into(),
524 tools: profile.tools,
525 context_servers: IndexMap::default(),
526 },
527 )
528 }));
529 }
530 }
531
532 Ok(settings)
533 }
534}
535
536fn merge<T>(target: &mut T, value: Option<T>) {
537 if let Some(value) = value {
538 *target = value;
539 }
540}
541
542#[cfg(test)]
543mod tests {
544 use fs::Fs;
545 use gpui::{ReadGlobal, TestAppContext};
546
547 use super::*;
548
549 #[gpui::test]
550 async fn test_deserialize_assistant_settings_with_version(cx: &mut TestAppContext) {
551 let fs = fs::FakeFs::new(cx.executor().clone());
552 fs.create_dir(paths::settings_file().parent().unwrap())
553 .await
554 .unwrap();
555
556 cx.update(|cx| {
557 let test_settings = settings::SettingsStore::test(cx);
558 cx.set_global(test_settings);
559 AssistantSettings::register(cx);
560 });
561
562 cx.update(|cx| {
563 assert!(!AssistantSettings::get_global(cx).using_outdated_settings_version);
564 assert_eq!(
565 AssistantSettings::get_global(cx).default_model,
566 LanguageModelSelection {
567 provider: "zed.dev".into(),
568 model: "claude-3-5-sonnet-latest".into(),
569 }
570 );
571 });
572
573 cx.update(|cx| {
574 settings::SettingsStore::global(cx).update_settings_file::<AssistantSettings>(
575 fs.clone(),
576 |settings, _| {
577 *settings = AssistantSettingsContent::Versioned(
578 VersionedAssistantSettingsContent::V2(AssistantSettingsContentV2 {
579 default_model: Some(LanguageModelSelection {
580 provider: "test-provider".into(),
581 model: "gpt-99".into(),
582 }),
583 editor_model: Some(LanguageModelSelection {
584 provider: "test-provider".into(),
585 model: "gpt-99".into(),
586 }),
587 inline_alternatives: None,
588 enabled: None,
589 button: None,
590 dock: None,
591 default_width: None,
592 default_height: None,
593 enable_experimental_live_diffs: None,
594 profiles: None,
595 always_allow_tool_actions: None,
596 }),
597 )
598 },
599 );
600 });
601
602 cx.run_until_parked();
603
604 let raw_settings_value = fs.load(paths::settings_file()).await.unwrap();
605 assert!(raw_settings_value.contains(r#""version": "2""#));
606
607 #[derive(Debug, Deserialize)]
608 struct AssistantSettingsTest {
609 assistant: AssistantSettingsContent,
610 }
611
612 let assistant_settings: AssistantSettingsTest =
613 serde_json_lenient::from_str(&raw_settings_value).unwrap();
614
615 assert!(!assistant_settings.assistant.is_version_outdated());
616 }
617}