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