1use anyhow::{Context as _, Result};
2use collections::HashMap;
3use context_server::{ContextServerCommand, ContextServerId};
4use editor::{Editor, EditorElement, EditorStyle};
5
6use gpui::{
7 AsyncWindowContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, ScrollHandle,
8 Subscription, Task, TextStyle, TextStyleRefinement, UnderlineStyle, WeakEntity, prelude::*,
9};
10use language::{Language, LanguageRegistry};
11use markdown::{Markdown, MarkdownElement, MarkdownStyle};
12use notifications::status_toast::{StatusToast, ToastIcon};
13use parking_lot::Mutex;
14use project::{
15 context_server_store::{
16 ContextServerStatus, ContextServerStore, ServerStatusChangedEvent,
17 registry::ContextServerDescriptorRegistry,
18 },
19 project_settings::{ContextServerSettings, ProjectSettings},
20 worktree_store::WorktreeStore,
21};
22use serde::Deserialize;
23use settings::{Settings as _, update_settings_file};
24use std::sync::Arc;
25use theme_settings::ThemeSettings;
26use ui::{
27 CommonAnimationExt, KeyBinding, Modal, ModalFooter, ModalHeader, Section, Tooltip,
28 WithScrollbar, prelude::*,
29};
30use util::ResultExt as _;
31use workspace::{ModalView, Workspace};
32
33use crate::AddContextServer;
34
35enum ConfigurationTarget {
36 New,
37 Existing {
38 id: ContextServerId,
39 command: ContextServerCommand,
40 },
41 ExistingHttp {
42 id: ContextServerId,
43 url: String,
44 headers: HashMap<String, String>,
45 },
46 Extension {
47 id: ContextServerId,
48 repository_url: Option<SharedString>,
49 installation: Option<extension::ContextServerConfiguration>,
50 },
51}
52
53enum ConfigurationSource {
54 New {
55 editor: Entity<Editor>,
56 is_http: bool,
57 },
58 Existing {
59 editor: Entity<Editor>,
60 is_http: bool,
61 },
62 Extension {
63 id: ContextServerId,
64 editor: Option<Entity<Editor>>,
65 repository_url: Option<SharedString>,
66 installation_instructions: Option<Entity<markdown::Markdown>>,
67 settings_validator: Option<jsonschema::Validator>,
68 },
69}
70
71impl ConfigurationSource {
72 fn has_configuration_options(&self) -> bool {
73 !matches!(self, ConfigurationSource::Extension { editor: None, .. })
74 }
75
76 fn is_new(&self) -> bool {
77 matches!(self, ConfigurationSource::New { .. })
78 }
79
80 fn from_target(
81 target: ConfigurationTarget,
82 language_registry: Arc<LanguageRegistry>,
83 jsonc_language: Option<Arc<Language>>,
84 window: &mut Window,
85 cx: &mut App,
86 ) -> Self {
87 fn create_editor(
88 json: String,
89 jsonc_language: Option<Arc<Language>>,
90 window: &mut Window,
91 cx: &mut App,
92 ) -> Entity<Editor> {
93 cx.new(|cx| {
94 let mut editor = Editor::auto_height(4, 16, window, cx);
95 editor.set_text(json, window, cx);
96 editor.set_show_gutter(false, cx);
97 editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx);
98 if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
99 buffer.update(cx, |buffer, cx| buffer.set_language(jsonc_language, cx))
100 }
101 editor
102 })
103 }
104
105 match target {
106 ConfigurationTarget::New => ConfigurationSource::New {
107 editor: create_editor(context_server_input(None), jsonc_language, window, cx),
108 is_http: false,
109 },
110 ConfigurationTarget::Existing { id, command } => ConfigurationSource::Existing {
111 editor: create_editor(
112 context_server_input(Some((id, command))),
113 jsonc_language,
114 window,
115 cx,
116 ),
117 is_http: false,
118 },
119 ConfigurationTarget::ExistingHttp {
120 id,
121 url,
122 headers: auth,
123 } => ConfigurationSource::Existing {
124 editor: create_editor(
125 context_server_http_input(Some((id, url, auth))),
126 jsonc_language,
127 window,
128 cx,
129 ),
130 is_http: true,
131 },
132 ConfigurationTarget::Extension {
133 id,
134 repository_url,
135 installation,
136 } => {
137 let settings_validator = installation.as_ref().and_then(|installation| {
138 jsonschema::validator_for(&installation.settings_schema)
139 .context("Failed to load JSON schema for context server settings")
140 .log_err()
141 });
142 let installation_instructions = installation.as_ref().map(|installation| {
143 cx.new(|cx| {
144 Markdown::new(
145 installation.installation_instructions.clone().into(),
146 Some(language_registry.clone()),
147 None,
148 cx,
149 )
150 })
151 });
152 ConfigurationSource::Extension {
153 id,
154 repository_url,
155 installation_instructions,
156 settings_validator,
157 editor: installation.map(|installation| {
158 create_editor(installation.default_settings, jsonc_language, window, cx)
159 }),
160 }
161 }
162 }
163 }
164
165 fn output(&self, cx: &mut App) -> Result<(ContextServerId, ContextServerSettings)> {
166 match self {
167 ConfigurationSource::New { editor, is_http }
168 | ConfigurationSource::Existing { editor, is_http } => {
169 if *is_http {
170 parse_http_input(&editor.read(cx).text(cx)).map(|(id, url, auth)| {
171 (
172 id,
173 ContextServerSettings::Http {
174 enabled: true,
175 url,
176 headers: auth,
177 timeout: None,
178 },
179 )
180 })
181 } else {
182 parse_input(&editor.read(cx).text(cx)).map(|(id, command)| {
183 (
184 id,
185 ContextServerSettings::Stdio {
186 enabled: true,
187 remote: false,
188 command,
189 },
190 )
191 })
192 }
193 }
194 ConfigurationSource::Extension {
195 id,
196 editor,
197 settings_validator,
198 ..
199 } => {
200 let text = editor
201 .as_ref()
202 .context("No output available")?
203 .read(cx)
204 .text(cx);
205 let settings = serde_json_lenient::from_str::<serde_json::Value>(&text)?;
206 if let Some(settings_validator) = settings_validator
207 && let Err(error) = settings_validator.validate(&settings)
208 {
209 return Err(anyhow::anyhow!(error.to_string()));
210 }
211 Ok((
212 id.clone(),
213 ContextServerSettings::Extension {
214 enabled: true,
215 remote: false,
216 settings,
217 },
218 ))
219 }
220 }
221 }
222}
223
224fn context_server_input(existing: Option<(ContextServerId, ContextServerCommand)>) -> String {
225 let (name, command, args, env) = match existing {
226 Some((id, cmd)) => {
227 let args = serde_json::to_string(&cmd.args).unwrap();
228 let env = serde_json::to_string(&cmd.env.unwrap_or_default()).unwrap();
229 let cmd_path = serde_json::to_string(&cmd.path).unwrap();
230 (id.0.to_string(), cmd_path, args, env)
231 }
232 None => (
233 "some-mcp-server".to_string(),
234 "".to_string(),
235 "[]".to_string(),
236 "{}".to_string(),
237 ),
238 };
239
240 format!(
241 r#"{{
242 /// Configure an MCP server that runs locally via stdin/stdout
243 ///
244 /// The name of your MCP server
245 "{name}": {{
246 /// The command which runs the MCP server
247 "command": {command},
248 /// The arguments to pass to the MCP server
249 "args": {args},
250 /// The environment variables to set
251 "env": {env}
252 }}
253}}"#
254 )
255}
256
257fn context_server_http_input(
258 existing: Option<(ContextServerId, String, HashMap<String, String>)>,
259) -> String {
260 let (name, url, headers) = match existing {
261 Some((id, url, headers)) => {
262 let header = if headers.is_empty() {
263 r#"// "Authorization": "Bearer <token>"#.to_string()
264 } else {
265 let json = serde_json::to_string_pretty(&headers).unwrap();
266 let mut lines = json.split("\n").collect::<Vec<_>>();
267 if lines.len() > 1 {
268 lines.remove(0);
269 lines.pop();
270 }
271 lines
272 .into_iter()
273 .map(|line| format!(" {}", line))
274 .collect::<String>()
275 };
276 (id.0.to_string(), url, header)
277 }
278 None => (
279 "some-remote-server".to_string(),
280 "https://example.com/mcp".to_string(),
281 r#"// "Authorization": "Bearer <token>"#.to_string(),
282 ),
283 };
284
285 format!(
286 r#"{{
287 /// Configure an MCP server that you connect to over HTTP
288 ///
289 /// The name of your remote MCP server
290 "{name}": {{
291 /// The URL of the remote MCP server
292 "url": "{url}",
293 "headers": {{
294 /// Any headers to send along
295 {headers}
296 }}
297 }}
298}}"#
299 )
300}
301
302fn parse_http_input(text: &str) -> Result<(ContextServerId, String, HashMap<String, String>)> {
303 #[derive(Deserialize)]
304 struct Temp {
305 url: String,
306 #[serde(default)]
307 headers: HashMap<String, String>,
308 }
309 let value: HashMap<String, Temp> = serde_json_lenient::from_str(text)?;
310 if value.len() != 1 {
311 anyhow::bail!("Expected exactly one context server configuration");
312 }
313
314 let (key, value) = value.into_iter().next().unwrap();
315
316 Ok((ContextServerId(key.into()), value.url, value.headers))
317}
318
319fn resolve_context_server_extension(
320 id: ContextServerId,
321 worktree_store: Entity<WorktreeStore>,
322 cx: &mut App,
323) -> Task<Option<ConfigurationTarget>> {
324 let registry = ContextServerDescriptorRegistry::default_global(cx).read(cx);
325
326 let Some(descriptor) = registry.context_server_descriptor(&id.0) else {
327 return Task::ready(None);
328 };
329
330 let extension = crate::agent_configuration::resolve_extension_for_context_server(&id, cx);
331 cx.spawn(async move |cx| {
332 let installation = descriptor
333 .configuration(worktree_store, cx)
334 .await
335 .context("Failed to resolve context server configuration")
336 .log_err()
337 .flatten();
338
339 Some(ConfigurationTarget::Extension {
340 id,
341 repository_url: extension
342 .and_then(|(_, manifest)| manifest.repository.clone().map(SharedString::from)),
343 installation,
344 })
345 })
346}
347
348enum State {
349 Idle,
350 Waiting,
351 AuthRequired { server_id: ContextServerId },
352 Authenticating { _server_id: ContextServerId },
353 Error(SharedString),
354}
355
356pub struct ConfigureContextServerModal {
357 context_server_store: Entity<ContextServerStore>,
358 workspace: WeakEntity<Workspace>,
359 source: ConfigurationSource,
360 state: State,
361 original_server_id: Option<ContextServerId>,
362 scroll_handle: ScrollHandle,
363 _auth_subscription: Option<Subscription>,
364}
365
366impl ConfigureContextServerModal {
367 pub fn register(
368 workspace: &mut Workspace,
369 language_registry: Arc<LanguageRegistry>,
370 _window: Option<&mut Window>,
371 _cx: &mut Context<Workspace>,
372 ) {
373 workspace.register_action({
374 move |_workspace, _: &AddContextServer, window, cx| {
375 let workspace_handle = cx.weak_entity();
376 let language_registry = language_registry.clone();
377 window
378 .spawn(cx, async move |cx| {
379 Self::show_modal(
380 ConfigurationTarget::New,
381 language_registry,
382 workspace_handle,
383 cx,
384 )
385 .await
386 })
387 .detach_and_log_err(cx);
388 }
389 });
390 }
391
392 pub fn show_modal_for_existing_server(
393 server_id: ContextServerId,
394 language_registry: Arc<LanguageRegistry>,
395 workspace: WeakEntity<Workspace>,
396 window: &mut Window,
397 cx: &mut App,
398 ) -> Task<Result<()>> {
399 let Some(settings) = ProjectSettings::get_global(cx)
400 .context_servers
401 .get(&server_id.0)
402 .cloned()
403 .or_else(|| {
404 ContextServerDescriptorRegistry::default_global(cx)
405 .read(cx)
406 .context_server_descriptor(&server_id.0)
407 .map(|_| ContextServerSettings::default_extension())
408 })
409 else {
410 return Task::ready(Err(anyhow::anyhow!("Context server not found")));
411 };
412
413 window.spawn(cx, async move |cx| {
414 let target = match settings {
415 ContextServerSettings::Stdio {
416 enabled: _,
417 command,
418 ..
419 } => Some(ConfigurationTarget::Existing {
420 id: server_id,
421 command,
422 }),
423 ContextServerSettings::Http {
424 enabled: _,
425 url,
426 headers,
427 timeout: _,
428 ..
429 } => Some(ConfigurationTarget::ExistingHttp {
430 id: server_id,
431 url,
432 headers,
433 }),
434 ContextServerSettings::Extension { .. } => {
435 match workspace
436 .update(cx, |workspace, cx| {
437 resolve_context_server_extension(
438 server_id,
439 workspace.project().read(cx).worktree_store(),
440 cx,
441 )
442 })
443 .ok()
444 {
445 Some(task) => task.await,
446 None => None,
447 }
448 }
449 };
450
451 match target {
452 Some(target) => Self::show_modal(target, language_registry, workspace, cx).await,
453 None => Err(anyhow::anyhow!("Failed to resolve context server")),
454 }
455 })
456 }
457
458 fn show_modal(
459 target: ConfigurationTarget,
460 language_registry: Arc<LanguageRegistry>,
461 workspace: WeakEntity<Workspace>,
462 cx: &mut AsyncWindowContext,
463 ) -> Task<Result<()>> {
464 cx.spawn(async move |cx| {
465 let jsonc_language = language_registry.language_for_name("jsonc").await.ok();
466 workspace.update_in(cx, |workspace, window, cx| {
467 let workspace_handle = cx.weak_entity();
468 let context_server_store = workspace.project().read(cx).context_server_store();
469 workspace.toggle_modal(window, cx, |window, cx| Self {
470 context_server_store,
471 workspace: workspace_handle,
472 state: State::Idle,
473 original_server_id: match &target {
474 ConfigurationTarget::Existing { id, .. } => Some(id.clone()),
475 ConfigurationTarget::ExistingHttp { id, .. } => Some(id.clone()),
476 ConfigurationTarget::Extension { id, .. } => Some(id.clone()),
477 ConfigurationTarget::New => None,
478 },
479 source: ConfigurationSource::from_target(
480 target,
481 language_registry,
482 jsonc_language,
483 window,
484 cx,
485 ),
486 scroll_handle: ScrollHandle::new(),
487 _auth_subscription: None,
488 })
489 })
490 })
491 }
492
493 fn set_error(&mut self, err: impl Into<SharedString>, cx: &mut Context<Self>) {
494 self.state = State::Error(err.into());
495 cx.notify();
496 }
497
498 fn confirm(&mut self, _: &menu::Confirm, cx: &mut Context<Self>) {
499 if matches!(
500 self.state,
501 State::Waiting | State::AuthRequired { .. } | State::Authenticating { .. }
502 ) {
503 return;
504 }
505
506 self.state = State::Idle;
507 let Some(workspace) = self.workspace.upgrade() else {
508 return;
509 };
510
511 let (id, settings) = match self.source.output(cx) {
512 Ok(val) => val,
513 Err(error) => {
514 self.set_error(error.to_string(), cx);
515 return;
516 }
517 };
518
519 self.state = State::Waiting;
520
521 let existing_server = self.context_server_store.read(cx).get_running_server(&id);
522 if existing_server.is_some() {
523 self.context_server_store.update(cx, |store, cx| {
524 store.stop_server(&id, cx).log_err();
525 });
526 }
527
528 let wait_for_context_server_task =
529 wait_for_context_server(&self.context_server_store, id.clone(), cx);
530 cx.spawn({
531 let id = id.clone();
532 async move |this, cx| {
533 let result = wait_for_context_server_task.await;
534 this.update(cx, |this, cx| match result {
535 Ok(ContextServerStatus::Running) => {
536 this.state = State::Idle;
537 this.show_configured_context_server_toast(id, cx);
538 cx.emit(DismissEvent);
539 }
540 Ok(ContextServerStatus::AuthRequired) => {
541 this.state = State::AuthRequired { server_id: id };
542 cx.notify();
543 }
544 Err(err) => {
545 this.set_error(err, cx);
546 }
547 Ok(_) => {}
548 })
549 }
550 })
551 .detach();
552
553 let settings_changed =
554 ProjectSettings::get_global(cx).context_servers.get(&id.0) != Some(&settings);
555
556 if settings_changed {
557 // When we write the settings to the file, the context server will be restarted.
558 workspace.update(cx, |workspace, cx| {
559 let fs = workspace.app_state().fs.clone();
560 let original_server_id = self.original_server_id.clone();
561 update_settings_file(fs.clone(), cx, move |current, _| {
562 if let Some(original_id) = original_server_id {
563 if original_id != id {
564 current.project.context_servers.remove(&original_id.0);
565 }
566 }
567 current
568 .project
569 .context_servers
570 .insert(id.0, settings.into());
571 });
572 });
573 } else if let Some(existing_server) = existing_server {
574 self.context_server_store
575 .update(cx, |store, cx| store.start_server(existing_server, cx));
576 }
577 }
578
579 fn cancel(&mut self, _: &menu::Cancel, cx: &mut Context<Self>) {
580 cx.emit(DismissEvent);
581 }
582
583 fn authenticate(&mut self, server_id: ContextServerId, cx: &mut Context<Self>) {
584 self.context_server_store.update(cx, |store, cx| {
585 store.authenticate_server(&server_id, cx).log_err();
586 });
587
588 self.state = State::Authenticating {
589 _server_id: server_id.clone(),
590 };
591
592 self._auth_subscription = Some(cx.subscribe(
593 &self.context_server_store,
594 move |this, _, event: &ServerStatusChangedEvent, cx| {
595 if event.server_id != server_id {
596 return;
597 }
598 match &event.status {
599 ContextServerStatus::Running => {
600 this._auth_subscription = None;
601 this.state = State::Idle;
602 this.show_configured_context_server_toast(event.server_id.clone(), cx);
603 cx.emit(DismissEvent);
604 }
605 ContextServerStatus::AuthRequired => {
606 this._auth_subscription = None;
607 this.state = State::AuthRequired {
608 server_id: event.server_id.clone(),
609 };
610 cx.notify();
611 }
612 ContextServerStatus::Error(error) => {
613 this._auth_subscription = None;
614 this.set_error(error.clone(), cx);
615 }
616 ContextServerStatus::Authenticating
617 | ContextServerStatus::Starting
618 | ContextServerStatus::Stopped => {}
619 }
620 },
621 ));
622
623 cx.notify();
624 }
625
626 fn show_configured_context_server_toast(&self, id: ContextServerId, cx: &mut App) {
627 self.workspace
628 .update(cx, {
629 |workspace, cx| {
630 let status_toast = StatusToast::new(
631 format!("{} configured successfully.", id.0),
632 cx,
633 |this, _cx| {
634 this.icon(ToastIcon::new(IconName::ToolHammer).color(Color::Muted))
635 .action("Dismiss", |_, _| {})
636 },
637 );
638
639 workspace.toggle_status_toast(status_toast, cx);
640 }
641 })
642 .log_err();
643 }
644}
645
646fn parse_input(text: &str) -> Result<(ContextServerId, ContextServerCommand)> {
647 let value: serde_json::Value = serde_json_lenient::from_str(text)?;
648 let object = value.as_object().context("Expected object")?;
649 anyhow::ensure!(object.len() == 1, "Expected exactly one key-value pair");
650 let (context_server_name, value) = object.into_iter().next().unwrap();
651 let command: ContextServerCommand = serde_json::from_value(value.clone())?;
652 Ok((ContextServerId(context_server_name.clone().into()), command))
653}
654
655impl ModalView for ConfigureContextServerModal {}
656
657impl Focusable for ConfigureContextServerModal {
658 fn focus_handle(&self, cx: &App) -> FocusHandle {
659 match &self.source {
660 ConfigurationSource::New { editor, .. } => editor.focus_handle(cx),
661 ConfigurationSource::Existing { editor, .. } => editor.focus_handle(cx),
662 ConfigurationSource::Extension { editor, .. } => editor
663 .as_ref()
664 .map(|editor| editor.focus_handle(cx))
665 .unwrap_or_else(|| cx.focus_handle()),
666 }
667 }
668}
669
670impl EventEmitter<DismissEvent> for ConfigureContextServerModal {}
671
672impl ConfigureContextServerModal {
673 fn render_modal_header(&self) -> ModalHeader {
674 let text: SharedString = match &self.source {
675 ConfigurationSource::New { .. } => "Add MCP Server".into(),
676 ConfigurationSource::Existing { .. } => "Configure MCP Server".into(),
677 ConfigurationSource::Extension { id, .. } => format!("Configure {}", id.0).into(),
678 };
679 ModalHeader::new().headline(text)
680 }
681
682 fn render_modal_description(&self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
683 const MODAL_DESCRIPTION: &str =
684 "Check the server docs for required arguments and environment variables.";
685
686 if let ConfigurationSource::Extension {
687 installation_instructions: Some(installation_instructions),
688 ..
689 } = &self.source
690 {
691 div()
692 .pb_2()
693 .text_sm()
694 .child(MarkdownElement::new(
695 installation_instructions.clone(),
696 default_markdown_style(window, cx),
697 ))
698 .into_any_element()
699 } else {
700 Label::new(MODAL_DESCRIPTION)
701 .color(Color::Muted)
702 .into_any_element()
703 }
704 }
705
706 fn render_tab_bar(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
707 let is_http = match &self.source {
708 ConfigurationSource::New { is_http, .. } => *is_http,
709 _ => return None,
710 };
711
712 let tab = |label: &'static str, active: bool| {
713 div()
714 .id(label)
715 .cursor_pointer()
716 .p_1()
717 .text_sm()
718 .border_b_1()
719 .when(active, |this| {
720 this.border_color(cx.theme().colors().border_focused)
721 })
722 .when(!active, |this| {
723 this.border_color(gpui::transparent_black())
724 .text_color(cx.theme().colors().text_muted)
725 .hover(|s| s.text_color(cx.theme().colors().text))
726 })
727 .child(label)
728 };
729
730 Some(
731 h_flex()
732 .pt_1()
733 .mb_2p5()
734 .gap_1()
735 .border_b_1()
736 .border_color(cx.theme().colors().border.opacity(0.5))
737 .child(
738 tab("Local", !is_http).on_click(cx.listener(|this, _, window, cx| {
739 if let ConfigurationSource::New { editor, is_http } = &mut this.source {
740 if *is_http {
741 *is_http = false;
742 let new_text = context_server_input(None);
743 editor.update(cx, |editor, cx| {
744 editor.set_text(new_text, window, cx);
745 });
746 }
747 }
748 })),
749 )
750 .child(
751 tab("Remote", is_http).on_click(cx.listener(|this, _, window, cx| {
752 if let ConfigurationSource::New { editor, is_http } = &mut this.source {
753 if !*is_http {
754 *is_http = true;
755 let new_text = context_server_http_input(None);
756 editor.update(cx, |editor, cx| {
757 editor.set_text(new_text, window, cx);
758 });
759 }
760 }
761 })),
762 )
763 .into_any_element(),
764 )
765 }
766
767 fn render_modal_content(&self, cx: &App) -> AnyElement {
768 let editor = match &self.source {
769 ConfigurationSource::New { editor, .. } => editor,
770 ConfigurationSource::Existing { editor, .. } => editor,
771 ConfigurationSource::Extension { editor, .. } => {
772 let Some(editor) = editor else {
773 return div().into_any_element();
774 };
775 editor
776 }
777 };
778
779 div()
780 .p_2()
781 .rounded_md()
782 .border_1()
783 .border_color(cx.theme().colors().border_variant)
784 .bg(cx.theme().colors().editor_background)
785 .child({
786 let settings = ThemeSettings::get_global(cx);
787 let text_style = TextStyle {
788 color: cx.theme().colors().text,
789 font_family: settings.buffer_font.family.clone(),
790 font_fallbacks: settings.buffer_font.fallbacks.clone(),
791 font_size: settings.buffer_font_size(cx).into(),
792 font_weight: settings.buffer_font.weight,
793 line_height: relative(settings.buffer_line_height.value()),
794 ..Default::default()
795 };
796 EditorElement::new(
797 editor,
798 EditorStyle {
799 background: cx.theme().colors().editor_background,
800 local_player: cx.theme().players().local(),
801 text: text_style,
802 syntax: cx.theme().syntax().clone(),
803 ..Default::default()
804 },
805 )
806 })
807 .into_any_element()
808 }
809
810 fn render_modal_footer(&self, cx: &mut Context<Self>) -> ModalFooter {
811 let focus_handle = self.focus_handle(cx);
812 let is_busy = matches!(
813 self.state,
814 State::Waiting | State::AuthRequired { .. } | State::Authenticating { .. }
815 );
816
817 ModalFooter::new()
818 .start_slot::<Button>(
819 if let ConfigurationSource::Extension {
820 repository_url: Some(repository_url),
821 ..
822 } = &self.source
823 {
824 Some(
825 Button::new("open-repository", "Open Repository")
826 .end_icon(
827 Icon::new(IconName::ArrowUpRight)
828 .size(IconSize::Small)
829 .color(Color::Muted),
830 )
831 .tooltip({
832 let repository_url = repository_url.clone();
833 move |_window, cx| {
834 Tooltip::with_meta(
835 "Open Repository",
836 None,
837 repository_url.clone(),
838 cx,
839 )
840 }
841 })
842 .on_click({
843 let repository_url = repository_url.clone();
844 move |_, _, cx| cx.open_url(&repository_url)
845 }),
846 )
847 } else {
848 None
849 },
850 )
851 .end_slot(
852 h_flex()
853 .gap_2()
854 .child(
855 Button::new(
856 "cancel",
857 if self.source.has_configuration_options() {
858 "Cancel"
859 } else {
860 "Dismiss"
861 },
862 )
863 .key_binding(
864 KeyBinding::for_action_in(&menu::Cancel, &focus_handle, cx)
865 .map(|kb| kb.size(rems_from_px(12.))),
866 )
867 .on_click(
868 cx.listener(|this, _event, _window, cx| this.cancel(&menu::Cancel, cx)),
869 ),
870 )
871 .children(self.source.has_configuration_options().then(|| {
872 Button::new(
873 "add-server",
874 if self.source.is_new() {
875 "Add Server"
876 } else {
877 "Configure Server"
878 },
879 )
880 .disabled(is_busy)
881 .key_binding(
882 KeyBinding::for_action_in(&menu::Confirm, &focus_handle, cx)
883 .map(|kb| kb.size(rems_from_px(12.))),
884 )
885 .on_click(
886 cx.listener(|this, _event, _window, cx| {
887 this.confirm(&menu::Confirm, cx)
888 }),
889 )
890 })),
891 )
892 }
893
894 fn render_loading(&self, label: impl Into<SharedString>) -> Div {
895 h_flex()
896 .h_8()
897 .gap_1p5()
898 .justify_center()
899 .child(
900 Icon::new(IconName::LoadCircle)
901 .size(IconSize::XSmall)
902 .color(Color::Muted)
903 .with_rotate_animation(3),
904 )
905 .child(Label::new(label).size(LabelSize::Small).color(Color::Muted))
906 }
907
908 fn render_auth_required(&self, server_id: &ContextServerId, cx: &mut Context<Self>) -> Div {
909 h_flex()
910 .h_8()
911 .min_w_0()
912 .w_full()
913 .gap_2()
914 .justify_center()
915 .child(
916 h_flex()
917 .gap_1p5()
918 .child(
919 Icon::new(IconName::Info)
920 .size(IconSize::Small)
921 .color(Color::Muted),
922 )
923 .child(
924 Label::new("Authenticate to connect this server")
925 .size(LabelSize::Small)
926 .color(Color::Muted),
927 ),
928 )
929 .child(
930 Button::new("authenticate-server", "Authenticate")
931 .style(ButtonStyle::Outlined)
932 .label_size(LabelSize::Small)
933 .on_click({
934 let server_id = server_id.clone();
935 cx.listener(move |this, _event, _window, cx| {
936 this.authenticate(server_id.clone(), cx);
937 })
938 }),
939 )
940 }
941
942 fn render_modal_error(error: SharedString) -> Div {
943 h_flex()
944 .h_8()
945 .gap_1p5()
946 .justify_center()
947 .child(
948 Icon::new(IconName::Warning)
949 .size(IconSize::Small)
950 .color(Color::Warning),
951 )
952 .child(
953 div()
954 .w_full()
955 .child(Label::new(error).size(LabelSize::Small).color(Color::Muted)),
956 )
957 }
958}
959
960impl Render for ConfigureContextServerModal {
961 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
962 div()
963 .elevation_3(cx)
964 .w(rems(40.))
965 .key_context("ConfigureContextServerModal")
966 .on_action(
967 cx.listener(|this, _: &menu::Cancel, _window, cx| this.cancel(&menu::Cancel, cx)),
968 )
969 .on_action(
970 cx.listener(|this, _: &menu::Confirm, _window, cx| {
971 this.confirm(&menu::Confirm, cx)
972 }),
973 )
974 .capture_any_mouse_down(cx.listener(|this, _, window, cx| {
975 this.focus_handle(cx).focus(window, cx);
976 }))
977 .child(
978 Modal::new("configure-context-server", None)
979 .header(self.render_modal_header())
980 .section(
981 Section::new().child(
982 div()
983 .size_full()
984 .child(
985 div()
986 .id("modal-content")
987 .max_h(vh(0.7, window))
988 .overflow_y_scroll()
989 .track_scroll(&self.scroll_handle)
990 .child(self.render_modal_description(window, cx))
991 .children(self.render_tab_bar(cx))
992 .child(self.render_modal_content(cx))
993 .child(match &self.state {
994 State::Idle => div(),
995 State::Waiting => {
996 self.render_loading("Connecting Server…")
997 }
998 State::AuthRequired { server_id } => {
999 self.render_auth_required(&server_id.clone(), cx)
1000 }
1001 State::Authenticating { .. } => {
1002 self.render_loading("Authenticating…")
1003 }
1004 State::Error(error) => {
1005 Self::render_modal_error(error.clone())
1006 }
1007 }),
1008 )
1009 .vertical_scrollbar_for(&self.scroll_handle, window, cx),
1010 ),
1011 )
1012 .footer(self.render_modal_footer(cx)),
1013 )
1014 }
1015}
1016
1017fn wait_for_context_server(
1018 context_server_store: &Entity<ContextServerStore>,
1019 context_server_id: ContextServerId,
1020 cx: &mut App,
1021) -> Task<Result<ContextServerStatus, Arc<str>>> {
1022 use std::time::Duration;
1023
1024 const WAIT_TIMEOUT: Duration = Duration::from_secs(120);
1025
1026 let (tx, rx) = futures::channel::oneshot::channel();
1027 let tx = Arc::new(Mutex::new(Some(tx)));
1028
1029 let context_server_id_for_timeout = context_server_id.clone();
1030 let subscription = cx.subscribe(context_server_store, move |_, event, _cx| {
1031 let ServerStatusChangedEvent { server_id, status } = event;
1032
1033 if server_id != &context_server_id {
1034 return;
1035 }
1036
1037 match status {
1038 ContextServerStatus::Running | ContextServerStatus::AuthRequired => {
1039 if let Some(tx) = tx.lock().take() {
1040 let _ = tx.send(Ok(status.clone()));
1041 }
1042 }
1043 ContextServerStatus::Stopped => {
1044 if let Some(tx) = tx.lock().take() {
1045 let _ = tx.send(Err("Context server stopped running".into()));
1046 }
1047 }
1048 ContextServerStatus::Error(error) => {
1049 if let Some(tx) = tx.lock().take() {
1050 let _ = tx.send(Err(error.clone()));
1051 }
1052 }
1053 ContextServerStatus::Starting | ContextServerStatus::Authenticating => {}
1054 }
1055 });
1056
1057 cx.spawn(async move |cx| {
1058 let timeout = cx.background_executor().timer(WAIT_TIMEOUT);
1059 let result = futures::future::select(rx, timeout).await;
1060 drop(subscription);
1061 match result {
1062 futures::future::Either::Left((Ok(inner), _)) => inner,
1063 futures::future::Either::Left((Err(_), _)) => {
1064 Err(Arc::from("Context server store was dropped"))
1065 }
1066 futures::future::Either::Right(_) => Err(Arc::from(format!(
1067 "Timed out waiting for context server `{}` to start. Check the Zed log for details.",
1068 context_server_id_for_timeout
1069 ))),
1070 }
1071 })
1072}
1073
1074pub(crate) fn default_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
1075 let theme_settings = ThemeSettings::get_global(cx);
1076 let colors = cx.theme().colors();
1077 let mut text_style = window.text_style();
1078 text_style.refine(&TextStyleRefinement {
1079 font_family: Some(theme_settings.ui_font.family.clone()),
1080 font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
1081 font_features: Some(theme_settings.ui_font.features.clone()),
1082 font_size: Some(TextSize::XSmall.rems(cx).into()),
1083 color: Some(colors.text_muted),
1084 ..Default::default()
1085 });
1086
1087 MarkdownStyle {
1088 base_text_style: text_style.clone(),
1089 selection_background_color: colors.element_selection_background,
1090 link: TextStyleRefinement {
1091 background_color: Some(colors.editor_foreground.opacity(0.025)),
1092 underline: Some(UnderlineStyle {
1093 color: Some(colors.text_accent.opacity(0.5)),
1094 thickness: px(1.),
1095 ..Default::default()
1096 }),
1097 ..Default::default()
1098 },
1099 ..Default::default()
1100 }
1101}