1mod agent_profile;
2
3use std::sync::Arc;
4
5use ::open_ai::Model as OpenAiModel;
6use anthropic::Model as AnthropicModel;
7use anyhow::{Result, bail};
8use collections::IndexMap;
9use deepseek::Model as DeepseekModel;
10use gpui::{App, Pixels, SharedString};
11use language_model::LanguageModel;
12use lmstudio::Model as LmStudioModel;
13use mistral::Model as MistralModel;
14use ollama::Model as OllamaModel;
15use schemars::{JsonSchema, schema::Schema};
16use serde::{Deserialize, Serialize};
17use settings::{Settings, SettingsSources};
18
19pub use crate::agent_profile::*;
20
21pub fn init(cx: &mut App) {
22 AgentSettings::register(cx);
23}
24
25#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
26#[serde(rename_all = "snake_case")]
27pub enum AgentDockPosition {
28 Left,
29 #[default]
30 Right,
31 Bottom,
32}
33
34#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
35#[serde(rename_all = "snake_case")]
36pub enum DefaultView {
37 #[default]
38 Thread,
39 TextThread,
40}
41
42#[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
43#[serde(rename_all = "snake_case")]
44pub enum NotifyWhenAgentWaiting {
45 #[default]
46 PrimaryScreen,
47 AllScreens,
48 Never,
49}
50
51#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
52#[serde(tag = "name", rename_all = "snake_case")]
53#[schemars(deny_unknown_fields)]
54pub enum AgentProviderContentV1 {
55 #[serde(rename = "zed.dev")]
56 ZedDotDev { default_model: Option<String> },
57 #[serde(rename = "openai")]
58 OpenAi {
59 default_model: Option<OpenAiModel>,
60 api_url: Option<String>,
61 available_models: Option<Vec<OpenAiModel>>,
62 },
63 #[serde(rename = "anthropic")]
64 Anthropic {
65 default_model: Option<AnthropicModel>,
66 api_url: Option<String>,
67 },
68 #[serde(rename = "ollama")]
69 Ollama {
70 default_model: Option<OllamaModel>,
71 api_url: Option<String>,
72 },
73 #[serde(rename = "lmstudio")]
74 LmStudio {
75 default_model: Option<LmStudioModel>,
76 api_url: Option<String>,
77 },
78 #[serde(rename = "deepseek")]
79 DeepSeek {
80 default_model: Option<DeepseekModel>,
81 api_url: Option<String>,
82 },
83 #[serde(rename = "mistral")]
84 Mistral {
85 default_model: Option<MistralModel>,
86 api_url: Option<String>,
87 },
88}
89
90#[derive(Default, Clone, Debug)]
91pub struct AgentSettings {
92 pub enabled: bool,
93 pub button: bool,
94 pub dock: AgentDockPosition,
95 pub default_width: Pixels,
96 pub default_height: Pixels,
97 pub default_model: LanguageModelSelection,
98 pub inline_assistant_model: Option<LanguageModelSelection>,
99 pub commit_message_model: Option<LanguageModelSelection>,
100 pub thread_summary_model: Option<LanguageModelSelection>,
101 pub inline_alternatives: Vec<LanguageModelSelection>,
102 pub using_outdated_settings_version: bool,
103 pub default_profile: AgentProfileId,
104 pub default_view: DefaultView,
105 pub profiles: IndexMap<AgentProfileId, AgentProfileSettings>,
106 pub always_allow_tool_actions: bool,
107 pub notify_when_agent_waiting: NotifyWhenAgentWaiting,
108 pub play_sound_when_agent_done: bool,
109 pub stream_edits: bool,
110 pub single_file_review: bool,
111 pub model_parameters: Vec<LanguageModelParameters>,
112 pub preferred_completion_mode: CompletionMode,
113 pub enable_feedback: bool,
114}
115
116impl AgentSettings {
117 pub fn temperature_for_model(model: &Arc<dyn LanguageModel>, cx: &App) -> Option<f32> {
118 let settings = Self::get_global(cx);
119 settings
120 .model_parameters
121 .iter()
122 .rfind(|setting| setting.matches(model))
123 .and_then(|m| m.temperature)
124 }
125
126 pub fn set_inline_assistant_model(&mut self, provider: String, model: String) {
127 self.inline_assistant_model = Some(LanguageModelSelection {
128 provider: provider.into(),
129 model,
130 });
131 }
132
133 pub fn set_commit_message_model(&mut self, provider: String, model: String) {
134 self.commit_message_model = Some(LanguageModelSelection {
135 provider: provider.into(),
136 model,
137 });
138 }
139
140 pub fn set_thread_summary_model(&mut self, provider: String, model: String) {
141 self.thread_summary_model = Some(LanguageModelSelection {
142 provider: provider.into(),
143 model,
144 });
145 }
146}
147
148#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
149pub struct LanguageModelParameters {
150 pub provider: Option<LanguageModelProviderSetting>,
151 pub model: Option<SharedString>,
152 pub temperature: Option<f32>,
153}
154
155impl LanguageModelParameters {
156 pub fn matches(&self, model: &Arc<dyn LanguageModel>) -> bool {
157 if let Some(provider) = &self.provider {
158 if provider.0 != model.provider_id().0 {
159 return false;
160 }
161 }
162 if let Some(setting_model) = &self.model {
163 if *setting_model != model.id().0 {
164 return false;
165 }
166 }
167 true
168 }
169}
170
171/// Agent panel settings
172#[derive(Clone, Serialize, Deserialize, Debug, Default)]
173pub struct AgentSettingsContent {
174 #[serde(flatten)]
175 pub inner: Option<AgentSettingsContentInner>,
176}
177
178#[derive(Clone, Serialize, Deserialize, Debug)]
179#[serde(untagged)]
180pub enum AgentSettingsContentInner {
181 Versioned(Box<VersionedAgentSettingsContent>),
182 Legacy(LegacyAgentSettingsContent),
183}
184
185impl AgentSettingsContentInner {
186 fn for_v2(content: AgentSettingsContentV2) -> Self {
187 AgentSettingsContentInner::Versioned(Box::new(VersionedAgentSettingsContent::V2(content)))
188 }
189}
190
191impl JsonSchema for AgentSettingsContent {
192 fn schema_name() -> String {
193 VersionedAgentSettingsContent::schema_name()
194 }
195
196 fn json_schema(r#gen: &mut schemars::r#gen::SchemaGenerator) -> Schema {
197 VersionedAgentSettingsContent::json_schema(r#gen)
198 }
199
200 fn is_referenceable() -> bool {
201 VersionedAgentSettingsContent::is_referenceable()
202 }
203}
204
205impl AgentSettingsContent {
206 pub fn is_version_outdated(&self) -> bool {
207 match &self.inner {
208 Some(AgentSettingsContentInner::Versioned(settings)) => match **settings {
209 VersionedAgentSettingsContent::V1(_) => true,
210 VersionedAgentSettingsContent::V2(_) => false,
211 },
212 Some(AgentSettingsContentInner::Legacy(_)) => true,
213 None => false,
214 }
215 }
216
217 fn upgrade(&self) -> AgentSettingsContentV2 {
218 match &self.inner {
219 Some(AgentSettingsContentInner::Versioned(settings)) => match **settings {
220 VersionedAgentSettingsContent::V1(ref settings) => AgentSettingsContentV2 {
221 enabled: settings.enabled,
222 button: settings.button,
223 dock: settings.dock,
224 default_width: settings.default_width,
225 default_height: settings.default_width,
226 default_model: settings
227 .provider
228 .clone()
229 .and_then(|provider| match provider {
230 AgentProviderContentV1::ZedDotDev { default_model } => default_model
231 .map(|model| LanguageModelSelection {
232 provider: "zed.dev".into(),
233 model,
234 }),
235 AgentProviderContentV1::OpenAi { default_model, .. } => default_model
236 .map(|model| LanguageModelSelection {
237 provider: "openai".into(),
238 model: model.id().to_string(),
239 }),
240 AgentProviderContentV1::Anthropic { default_model, .. } => {
241 default_model.map(|model| LanguageModelSelection {
242 provider: "anthropic".into(),
243 model: model.id().to_string(),
244 })
245 }
246 AgentProviderContentV1::Ollama { default_model, .. } => default_model
247 .map(|model| LanguageModelSelection {
248 provider: "ollama".into(),
249 model: model.id().to_string(),
250 }),
251 AgentProviderContentV1::LmStudio { default_model, .. } => default_model
252 .map(|model| LanguageModelSelection {
253 provider: "lmstudio".into(),
254 model: model.id().to_string(),
255 }),
256 AgentProviderContentV1::DeepSeek { default_model, .. } => default_model
257 .map(|model| LanguageModelSelection {
258 provider: "deepseek".into(),
259 model: model.id().to_string(),
260 }),
261 AgentProviderContentV1::Mistral { default_model, .. } => default_model
262 .map(|model| LanguageModelSelection {
263 provider: "mistral".into(),
264 model: model.id().to_string(),
265 }),
266 }),
267 inline_assistant_model: None,
268 commit_message_model: None,
269 thread_summary_model: None,
270 inline_alternatives: None,
271 default_profile: None,
272 default_view: None,
273 profiles: None,
274 always_allow_tool_actions: None,
275 notify_when_agent_waiting: None,
276 stream_edits: None,
277 single_file_review: None,
278 model_parameters: Vec::new(),
279 preferred_completion_mode: None,
280 enable_feedback: None,
281 play_sound_when_agent_done: None,
282 },
283 VersionedAgentSettingsContent::V2(ref settings) => settings.clone(),
284 },
285 Some(AgentSettingsContentInner::Legacy(settings)) => AgentSettingsContentV2 {
286 enabled: None,
287 button: settings.button,
288 dock: settings.dock,
289 default_width: settings.default_width,
290 default_height: settings.default_height,
291 default_model: Some(LanguageModelSelection {
292 provider: "openai".into(),
293 model: settings
294 .default_open_ai_model
295 .clone()
296 .unwrap_or_default()
297 .id()
298 .to_string(),
299 }),
300 inline_assistant_model: None,
301 commit_message_model: None,
302 thread_summary_model: None,
303 inline_alternatives: None,
304 default_profile: None,
305 default_view: None,
306 profiles: None,
307 always_allow_tool_actions: None,
308 notify_when_agent_waiting: None,
309 stream_edits: None,
310 single_file_review: None,
311 model_parameters: Vec::new(),
312 preferred_completion_mode: None,
313 enable_feedback: None,
314 play_sound_when_agent_done: None,
315 },
316 None => AgentSettingsContentV2::default(),
317 }
318 }
319
320 pub fn set_dock(&mut self, dock: AgentDockPosition) {
321 match &mut self.inner {
322 Some(AgentSettingsContentInner::Versioned(settings)) => match **settings {
323 VersionedAgentSettingsContent::V1(ref mut settings) => {
324 settings.dock = Some(dock);
325 }
326 VersionedAgentSettingsContent::V2(ref mut settings) => {
327 settings.dock = Some(dock);
328 }
329 },
330 Some(AgentSettingsContentInner::Legacy(settings)) => {
331 settings.dock = Some(dock);
332 }
333 None => {
334 self.inner = Some(AgentSettingsContentInner::for_v2(AgentSettingsContentV2 {
335 dock: Some(dock),
336 ..Default::default()
337 }))
338 }
339 }
340 }
341
342 pub fn set_model(&mut self, language_model: Arc<dyn LanguageModel>) {
343 let model = language_model.id().0.to_string();
344 let provider = language_model.provider_id().0.to_string();
345
346 match &mut self.inner {
347 Some(AgentSettingsContentInner::Versioned(settings)) => match **settings {
348 VersionedAgentSettingsContent::V1(ref mut settings) => match provider.as_ref() {
349 "zed.dev" => {
350 log::warn!("attempted to set zed.dev model on outdated settings");
351 }
352 "anthropic" => {
353 let api_url = match &settings.provider {
354 Some(AgentProviderContentV1::Anthropic { api_url, .. }) => {
355 api_url.clone()
356 }
357 _ => None,
358 };
359 settings.provider = Some(AgentProviderContentV1::Anthropic {
360 default_model: AnthropicModel::from_id(&model).ok(),
361 api_url,
362 });
363 }
364 "ollama" => {
365 let api_url = match &settings.provider {
366 Some(AgentProviderContentV1::Ollama { api_url, .. }) => api_url.clone(),
367 _ => None,
368 };
369 settings.provider = Some(AgentProviderContentV1::Ollama {
370 default_model: Some(ollama::Model::new(
371 &model,
372 None,
373 None,
374 Some(language_model.supports_tools()),
375 Some(language_model.supports_images()),
376 None,
377 )),
378 api_url,
379 });
380 }
381 "lmstudio" => {
382 let api_url = match &settings.provider {
383 Some(AgentProviderContentV1::LmStudio { api_url, .. }) => {
384 api_url.clone()
385 }
386 _ => None,
387 };
388 settings.provider = Some(AgentProviderContentV1::LmStudio {
389 default_model: Some(lmstudio::Model::new(
390 &model, None, None, false, false,
391 )),
392 api_url,
393 });
394 }
395 "openai" => {
396 let (api_url, available_models) = match &settings.provider {
397 Some(AgentProviderContentV1::OpenAi {
398 api_url,
399 available_models,
400 ..
401 }) => (api_url.clone(), available_models.clone()),
402 _ => (None, None),
403 };
404 settings.provider = Some(AgentProviderContentV1::OpenAi {
405 default_model: OpenAiModel::from_id(&model).ok(),
406 api_url,
407 available_models,
408 });
409 }
410 "deepseek" => {
411 let api_url = match &settings.provider {
412 Some(AgentProviderContentV1::DeepSeek { api_url, .. }) => {
413 api_url.clone()
414 }
415 _ => None,
416 };
417 settings.provider = Some(AgentProviderContentV1::DeepSeek {
418 default_model: DeepseekModel::from_id(&model).ok(),
419 api_url,
420 });
421 }
422 _ => {}
423 },
424 VersionedAgentSettingsContent::V2(ref mut settings) => {
425 settings.default_model = Some(LanguageModelSelection {
426 provider: provider.into(),
427 model,
428 });
429 }
430 },
431 Some(AgentSettingsContentInner::Legacy(settings)) => {
432 if let Ok(model) = OpenAiModel::from_id(&language_model.id().0) {
433 settings.default_open_ai_model = Some(model);
434 }
435 }
436 None => {
437 self.inner = Some(AgentSettingsContentInner::for_v2(AgentSettingsContentV2 {
438 default_model: Some(LanguageModelSelection {
439 provider: provider.into(),
440 model,
441 }),
442 ..Default::default()
443 }));
444 }
445 }
446 }
447
448 pub fn set_inline_assistant_model(&mut self, provider: String, model: String) {
449 self.v2_setting(|setting| {
450 setting.inline_assistant_model = Some(LanguageModelSelection {
451 provider: provider.into(),
452 model,
453 });
454 Ok(())
455 })
456 .ok();
457 }
458
459 pub fn set_commit_message_model(&mut self, provider: String, model: String) {
460 self.v2_setting(|setting| {
461 setting.commit_message_model = Some(LanguageModelSelection {
462 provider: provider.into(),
463 model,
464 });
465 Ok(())
466 })
467 .ok();
468 }
469
470 pub fn v2_setting(
471 &mut self,
472 f: impl FnOnce(&mut AgentSettingsContentV2) -> anyhow::Result<()>,
473 ) -> anyhow::Result<()> {
474 match self.inner.get_or_insert_with(|| {
475 AgentSettingsContentInner::for_v2(AgentSettingsContentV2 {
476 ..Default::default()
477 })
478 }) {
479 AgentSettingsContentInner::Versioned(boxed) => {
480 if let VersionedAgentSettingsContent::V2(ref mut settings) = **boxed {
481 f(settings)
482 } else {
483 Ok(())
484 }
485 }
486 _ => Ok(()),
487 }
488 }
489
490 pub fn set_thread_summary_model(&mut self, provider: String, model: String) {
491 self.v2_setting(|setting| {
492 setting.thread_summary_model = Some(LanguageModelSelection {
493 provider: provider.into(),
494 model,
495 });
496 Ok(())
497 })
498 .ok();
499 }
500
501 pub fn set_always_allow_tool_actions(&mut self, allow: bool) {
502 self.v2_setting(|setting| {
503 setting.always_allow_tool_actions = Some(allow);
504 Ok(())
505 })
506 .ok();
507 }
508
509 pub fn set_play_sound_when_agent_done(&mut self, allow: bool) {
510 self.v2_setting(|setting| {
511 setting.play_sound_when_agent_done = Some(allow);
512 Ok(())
513 })
514 .ok();
515 }
516
517 pub fn set_single_file_review(&mut self, allow: bool) {
518 self.v2_setting(|setting| {
519 setting.single_file_review = Some(allow);
520 Ok(())
521 })
522 .ok();
523 }
524
525 pub fn set_profile(&mut self, profile_id: AgentProfileId) {
526 self.v2_setting(|setting| {
527 setting.default_profile = Some(profile_id);
528 Ok(())
529 })
530 .ok();
531 }
532
533 pub fn create_profile(
534 &mut self,
535 profile_id: AgentProfileId,
536 profile_settings: AgentProfileSettings,
537 ) -> Result<()> {
538 self.v2_setting(|settings| {
539 let profiles = settings.profiles.get_or_insert_default();
540 if profiles.contains_key(&profile_id) {
541 bail!("profile with ID '{profile_id}' already exists");
542 }
543
544 profiles.insert(
545 profile_id,
546 AgentProfileContent {
547 name: profile_settings.name.into(),
548 tools: profile_settings.tools,
549 enable_all_context_servers: Some(profile_settings.enable_all_context_servers),
550 context_servers: profile_settings
551 .context_servers
552 .into_iter()
553 .map(|(server_id, preset)| {
554 (
555 server_id,
556 ContextServerPresetContent {
557 tools: preset.tools,
558 },
559 )
560 })
561 .collect(),
562 },
563 );
564
565 Ok(())
566 })
567 }
568}
569
570#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
571#[serde(tag = "version")]
572#[schemars(deny_unknown_fields)]
573pub enum VersionedAgentSettingsContent {
574 #[serde(rename = "1")]
575 V1(AgentSettingsContentV1),
576 #[serde(rename = "2")]
577 V2(AgentSettingsContentV2),
578}
579
580impl Default for VersionedAgentSettingsContent {
581 fn default() -> Self {
582 Self::V2(AgentSettingsContentV2 {
583 enabled: None,
584 button: None,
585 dock: None,
586 default_width: None,
587 default_height: None,
588 default_model: None,
589 inline_assistant_model: None,
590 commit_message_model: None,
591 thread_summary_model: None,
592 inline_alternatives: None,
593 default_profile: None,
594 default_view: None,
595 profiles: None,
596 always_allow_tool_actions: None,
597 notify_when_agent_waiting: None,
598 stream_edits: None,
599 single_file_review: None,
600 model_parameters: Vec::new(),
601 preferred_completion_mode: None,
602 enable_feedback: None,
603 play_sound_when_agent_done: None,
604 })
605 }
606}
607
608#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default)]
609#[schemars(deny_unknown_fields)]
610pub struct AgentSettingsContentV2 {
611 /// Whether the Agent is enabled.
612 ///
613 /// Default: true
614 enabled: Option<bool>,
615 /// Whether to show the agent panel button in the status bar.
616 ///
617 /// Default: true
618 button: Option<bool>,
619 /// Where to dock the agent panel.
620 ///
621 /// Default: right
622 dock: Option<AgentDockPosition>,
623 /// Default width in pixels when the agent panel is docked to the left or right.
624 ///
625 /// Default: 640
626 default_width: Option<f32>,
627 /// Default height in pixels when the agent panel is docked to the bottom.
628 ///
629 /// Default: 320
630 default_height: Option<f32>,
631 /// The default model to use when creating new chats and for other features when a specific model is not specified.
632 default_model: Option<LanguageModelSelection>,
633 /// Model to use for the inline assistant. Defaults to default_model when not specified.
634 inline_assistant_model: Option<LanguageModelSelection>,
635 /// Model to use for generating git commit messages. Defaults to default_model when not specified.
636 commit_message_model: Option<LanguageModelSelection>,
637 /// Model to use for generating thread summaries. Defaults to default_model when not specified.
638 thread_summary_model: Option<LanguageModelSelection>,
639 /// Additional models with which to generate alternatives when performing inline assists.
640 inline_alternatives: Option<Vec<LanguageModelSelection>>,
641 /// The default profile to use in the Agent.
642 ///
643 /// Default: write
644 default_profile: Option<AgentProfileId>,
645 /// Which view type to show by default in the agent panel.
646 ///
647 /// Default: "thread"
648 default_view: Option<DefaultView>,
649 /// The available agent profiles.
650 pub profiles: Option<IndexMap<AgentProfileId, AgentProfileContent>>,
651 /// Whenever a tool action would normally wait for your confirmation
652 /// that you allow it, always choose to allow it.
653 ///
654 /// Default: false
655 always_allow_tool_actions: Option<bool>,
656 /// Where to show a popup notification when the agent is waiting for user input.
657 ///
658 /// Default: "primary_screen"
659 notify_when_agent_waiting: Option<NotifyWhenAgentWaiting>,
660 /// Whether to play a sound when the agent has either completed its response, or needs user input.
661 ///
662 /// Default: false
663 play_sound_when_agent_done: Option<bool>,
664 /// Whether to stream edits from the agent as they are received.
665 ///
666 /// Default: false
667 stream_edits: Option<bool>,
668 /// Whether to display agent edits in single-file editors in addition to the review multibuffer pane.
669 ///
670 /// Default: true
671 single_file_review: Option<bool>,
672 /// Additional parameters for language model requests. When making a request
673 /// to a model, parameters will be taken from the last entry in this list
674 /// that matches the model's provider and name. In each entry, both provider
675 /// and model are optional, so that you can specify parameters for either
676 /// one.
677 ///
678 /// Default: []
679 #[serde(default)]
680 model_parameters: Vec<LanguageModelParameters>,
681 /// What completion mode to enable for new threads
682 ///
683 /// Default: normal
684 preferred_completion_mode: Option<CompletionMode>,
685 /// Whether to show thumb buttons for feedback in the agent panel.
686 ///
687 /// Default: true
688 enable_feedback: Option<bool>,
689}
690
691#[derive(Clone, Copy, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Default)]
692#[serde(rename_all = "snake_case")]
693pub enum CompletionMode {
694 #[default]
695 Normal,
696 #[serde(alias = "max")]
697 Burn,
698}
699
700impl From<CompletionMode> for zed_llm_client::CompletionMode {
701 fn from(value: CompletionMode) -> Self {
702 match value {
703 CompletionMode::Normal => zed_llm_client::CompletionMode::Normal,
704 CompletionMode::Burn => zed_llm_client::CompletionMode::Max,
705 }
706 }
707}
708
709#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
710pub struct LanguageModelSelection {
711 pub provider: LanguageModelProviderSetting,
712 pub model: String,
713}
714
715#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
716pub struct LanguageModelProviderSetting(pub String);
717
718impl JsonSchema for LanguageModelProviderSetting {
719 fn schema_name() -> String {
720 "LanguageModelProviderSetting".into()
721 }
722
723 fn json_schema(_: &mut schemars::r#gen::SchemaGenerator) -> Schema {
724 schemars::schema::SchemaObject {
725 enum_values: Some(vec![
726 "anthropic".into(),
727 "amazon-bedrock".into(),
728 "google".into(),
729 "lmstudio".into(),
730 "ollama".into(),
731 "openai".into(),
732 "zed.dev".into(),
733 "copilot_chat".into(),
734 "deepseek".into(),
735 "openrouter".into(),
736 "mistral".into(),
737 "vercel".into(),
738 ]),
739 ..Default::default()
740 }
741 .into()
742 }
743}
744
745impl From<String> for LanguageModelProviderSetting {
746 fn from(provider: String) -> Self {
747 Self(provider)
748 }
749}
750
751impl From<&str> for LanguageModelProviderSetting {
752 fn from(provider: &str) -> Self {
753 Self(provider.to_string())
754 }
755}
756
757impl Default for LanguageModelSelection {
758 fn default() -> Self {
759 Self {
760 provider: LanguageModelProviderSetting("openai".to_string()),
761 model: "gpt-4".to_string(),
762 }
763 }
764}
765
766#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)]
767pub struct AgentProfileContent {
768 pub name: Arc<str>,
769 #[serde(default)]
770 pub tools: IndexMap<Arc<str>, bool>,
771 /// Whether all context servers are enabled by default.
772 pub enable_all_context_servers: Option<bool>,
773 #[serde(default)]
774 pub context_servers: IndexMap<Arc<str>, ContextServerPresetContent>,
775}
776
777#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema)]
778pub struct ContextServerPresetContent {
779 pub tools: IndexMap<Arc<str>, bool>,
780}
781
782#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
783#[schemars(deny_unknown_fields)]
784pub struct AgentSettingsContentV1 {
785 /// Whether the Agent is enabled.
786 ///
787 /// Default: true
788 enabled: Option<bool>,
789 /// Whether to show the Agent panel button in the status bar.
790 ///
791 /// Default: true
792 button: Option<bool>,
793 /// Where to dock the Agent.
794 ///
795 /// Default: right
796 dock: Option<AgentDockPosition>,
797 /// Default width in pixels when the Agent is docked to the left or right.
798 ///
799 /// Default: 640
800 default_width: Option<f32>,
801 /// Default height in pixels when the Agent is docked to the bottom.
802 ///
803 /// Default: 320
804 default_height: Option<f32>,
805 /// The provider of the Agent service.
806 ///
807 /// This can be "openai", "anthropic", "ollama", "lmstudio", "deepseek", "zed.dev"
808 /// each with their respective default models and configurations.
809 provider: Option<AgentProviderContentV1>,
810}
811
812#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
813#[schemars(deny_unknown_fields)]
814pub struct LegacyAgentSettingsContent {
815 /// Whether to show the Agent panel button in the status bar.
816 ///
817 /// Default: true
818 pub button: Option<bool>,
819 /// Where to dock the Agent.
820 ///
821 /// Default: right
822 pub dock: Option<AgentDockPosition>,
823 /// Default width in pixels when the Agent is docked to the left or right.
824 ///
825 /// Default: 640
826 pub default_width: Option<f32>,
827 /// Default height in pixels when the Agent is docked to the bottom.
828 ///
829 /// Default: 320
830 pub default_height: Option<f32>,
831 /// The default OpenAI model to use when creating new chats.
832 ///
833 /// Default: gpt-4-1106-preview
834 pub default_open_ai_model: Option<OpenAiModel>,
835 /// OpenAI API base URL to use when creating new chats.
836 ///
837 /// Default: <https://api.openai.com/v1>
838 pub openai_api_url: Option<String>,
839}
840
841impl Settings for AgentSettings {
842 const KEY: Option<&'static str> = Some("agent");
843
844 const FALLBACK_KEY: Option<&'static str> = Some("assistant");
845
846 const PRESERVED_KEYS: Option<&'static [&'static str]> = Some(&["version"]);
847
848 type FileContent = AgentSettingsContent;
849
850 fn load(
851 sources: SettingsSources<Self::FileContent>,
852 _: &mut gpui::App,
853 ) -> anyhow::Result<Self> {
854 let mut settings = AgentSettings::default();
855
856 for value in sources.defaults_and_customizations() {
857 if value.is_version_outdated() {
858 settings.using_outdated_settings_version = true;
859 }
860
861 let value = value.upgrade();
862 merge(&mut settings.enabled, value.enabled);
863 merge(&mut settings.button, value.button);
864 merge(&mut settings.dock, value.dock);
865 merge(
866 &mut settings.default_width,
867 value.default_width.map(Into::into),
868 );
869 merge(
870 &mut settings.default_height,
871 value.default_height.map(Into::into),
872 );
873 merge(&mut settings.default_model, value.default_model);
874 settings.inline_assistant_model = value
875 .inline_assistant_model
876 .or(settings.inline_assistant_model.take());
877 settings.commit_message_model = value
878 .commit_message_model
879 .or(settings.commit_message_model.take());
880 settings.thread_summary_model = value
881 .thread_summary_model
882 .or(settings.thread_summary_model.take());
883 merge(&mut settings.inline_alternatives, value.inline_alternatives);
884 merge(
885 &mut settings.always_allow_tool_actions,
886 value.always_allow_tool_actions,
887 );
888 merge(
889 &mut settings.notify_when_agent_waiting,
890 value.notify_when_agent_waiting,
891 );
892 merge(
893 &mut settings.play_sound_when_agent_done,
894 value.play_sound_when_agent_done,
895 );
896 merge(&mut settings.stream_edits, value.stream_edits);
897 merge(&mut settings.single_file_review, value.single_file_review);
898 merge(&mut settings.default_profile, value.default_profile);
899 merge(&mut settings.default_view, value.default_view);
900 merge(
901 &mut settings.preferred_completion_mode,
902 value.preferred_completion_mode,
903 );
904 merge(&mut settings.enable_feedback, value.enable_feedback);
905
906 settings
907 .model_parameters
908 .extend_from_slice(&value.model_parameters);
909
910 if let Some(profiles) = value.profiles {
911 settings
912 .profiles
913 .extend(profiles.into_iter().map(|(id, profile)| {
914 (
915 id,
916 AgentProfileSettings {
917 name: profile.name.into(),
918 tools: profile.tools,
919 enable_all_context_servers: profile
920 .enable_all_context_servers
921 .unwrap_or_default(),
922 context_servers: profile
923 .context_servers
924 .into_iter()
925 .map(|(context_server_id, preset)| {
926 (
927 context_server_id,
928 ContextServerPreset {
929 tools: preset.tools.clone(),
930 },
931 )
932 })
933 .collect(),
934 },
935 )
936 }));
937 }
938 }
939
940 Ok(settings)
941 }
942
943 fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
944 if let Some(b) = vscode
945 .read_value("chat.agent.enabled")
946 .and_then(|b| b.as_bool())
947 {
948 match &mut current.inner {
949 Some(AgentSettingsContentInner::Versioned(versioned)) => match versioned.as_mut() {
950 VersionedAgentSettingsContent::V1(setting) => {
951 setting.enabled = Some(b);
952 setting.button = Some(b);
953 }
954
955 VersionedAgentSettingsContent::V2(setting) => {
956 setting.enabled = Some(b);
957 setting.button = Some(b);
958 }
959 },
960 Some(AgentSettingsContentInner::Legacy(setting)) => setting.button = Some(b),
961 None => {
962 current.inner =
963 Some(AgentSettingsContentInner::for_v2(AgentSettingsContentV2 {
964 enabled: Some(b),
965 button: Some(b),
966 ..Default::default()
967 }));
968 }
969 }
970 }
971 }
972}
973
974fn merge<T>(target: &mut T, value: Option<T>) {
975 if let Some(value) = value {
976 *target = value;
977 }
978}
979
980#[cfg(test)]
981mod tests {
982 use fs::Fs;
983 use gpui::{ReadGlobal, TestAppContext};
984 use settings::SettingsStore;
985
986 use super::*;
987
988 #[gpui::test]
989 async fn test_deserialize_agent_settings_with_version(cx: &mut TestAppContext) {
990 let fs = fs::FakeFs::new(cx.executor().clone());
991 fs.create_dir(paths::settings_file().parent().unwrap())
992 .await
993 .unwrap();
994
995 cx.update(|cx| {
996 let test_settings = settings::SettingsStore::test(cx);
997 cx.set_global(test_settings);
998 AgentSettings::register(cx);
999 });
1000
1001 cx.update(|cx| {
1002 assert!(!AgentSettings::get_global(cx).using_outdated_settings_version);
1003 assert_eq!(
1004 AgentSettings::get_global(cx).default_model,
1005 LanguageModelSelection {
1006 provider: "zed.dev".into(),
1007 model: "claude-sonnet-4".into(),
1008 }
1009 );
1010 });
1011
1012 cx.update(|cx| {
1013 settings::SettingsStore::global(cx).update_settings_file::<AgentSettings>(
1014 fs.clone(),
1015 |settings, _| {
1016 *settings = AgentSettingsContent {
1017 inner: Some(AgentSettingsContentInner::for_v2(AgentSettingsContentV2 {
1018 default_model: Some(LanguageModelSelection {
1019 provider: "test-provider".into(),
1020 model: "gpt-99".into(),
1021 }),
1022 inline_assistant_model: None,
1023 commit_message_model: None,
1024 thread_summary_model: None,
1025 inline_alternatives: None,
1026 enabled: None,
1027 button: None,
1028 dock: None,
1029 default_width: None,
1030 default_height: None,
1031 default_profile: None,
1032 default_view: None,
1033 profiles: None,
1034 always_allow_tool_actions: None,
1035 play_sound_when_agent_done: None,
1036 notify_when_agent_waiting: None,
1037 stream_edits: None,
1038 single_file_review: None,
1039 enable_feedback: None,
1040 model_parameters: Vec::new(),
1041 preferred_completion_mode: None,
1042 })),
1043 }
1044 },
1045 );
1046 });
1047
1048 cx.run_until_parked();
1049
1050 let raw_settings_value = fs.load(paths::settings_file()).await.unwrap();
1051 assert!(raw_settings_value.contains(r#""version": "2""#));
1052
1053 #[derive(Debug, Deserialize)]
1054 struct AgentSettingsTest {
1055 agent: AgentSettingsContent,
1056 }
1057
1058 let agent_settings: AgentSettingsTest =
1059 serde_json_lenient::from_str(&raw_settings_value).unwrap();
1060
1061 assert!(!agent_settings.agent.is_version_outdated());
1062 }
1063
1064 #[gpui::test]
1065 async fn test_load_settings_from_old_key(cx: &mut TestAppContext) {
1066 let fs = fs::FakeFs::new(cx.executor().clone());
1067 fs.create_dir(paths::settings_file().parent().unwrap())
1068 .await
1069 .unwrap();
1070
1071 cx.update(|cx| {
1072 let mut test_settings = settings::SettingsStore::test(cx);
1073 let user_settings_content = r#"{
1074 "assistant": {
1075 "enabled": true,
1076 "version": "2",
1077 "default_model": {
1078 "provider": "zed.dev",
1079 "model": "gpt-99"
1080 },
1081 }}"#;
1082 test_settings
1083 .set_user_settings(user_settings_content, cx)
1084 .unwrap();
1085 cx.set_global(test_settings);
1086 AgentSettings::register(cx);
1087 });
1088
1089 cx.run_until_parked();
1090
1091 let agent_settings = cx.update(|cx| AgentSettings::get_global(cx).clone());
1092 assert!(agent_settings.enabled);
1093 assert!(!agent_settings.using_outdated_settings_version);
1094 assert_eq!(agent_settings.default_model.model, "gpt-99");
1095
1096 cx.update_global::<SettingsStore, _>(|settings_store, cx| {
1097 settings_store.update_user_settings::<AgentSettings>(cx, |settings| {
1098 *settings = AgentSettingsContent {
1099 inner: Some(AgentSettingsContentInner::for_v2(AgentSettingsContentV2 {
1100 enabled: Some(false),
1101 default_model: Some(LanguageModelSelection {
1102 provider: "xai".to_owned().into(),
1103 model: "grok".to_owned(),
1104 }),
1105 ..Default::default()
1106 })),
1107 };
1108 });
1109 });
1110
1111 cx.run_until_parked();
1112
1113 let settings = cx.update(|cx| SettingsStore::global(cx).raw_user_settings().clone());
1114
1115 #[derive(Debug, Deserialize)]
1116 struct AgentSettingsTest {
1117 assistant: AgentSettingsContent,
1118 agent: Option<serde_json_lenient::Value>,
1119 }
1120
1121 let agent_settings: AgentSettingsTest = serde_json::from_value(settings).unwrap();
1122 assert!(agent_settings.agent.is_none());
1123 }
1124}