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