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