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