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| buffer.set_language(jsonc_language, cx))
101 }
102 editor
103 })
104 }
105
106 match target {
107 ConfigurationTarget::New => ConfigurationSource::New {
108 editor: create_editor(context_server_input(None), jsonc_language, window, cx),
109 is_http: false,
110 },
111 ConfigurationTarget::Existing { id, command } => ConfigurationSource::Existing {
112 editor: create_editor(
113 context_server_input(Some((id, command))),
114 jsonc_language,
115 window,
116 cx,
117 ),
118 is_http: false,
119 },
120 ConfigurationTarget::ExistingHttp {
121 id,
122 url,
123 headers: auth,
124 } => ConfigurationSource::Existing {
125 editor: create_editor(
126 context_server_http_input(Some((id, url, auth))),
127 jsonc_language,
128 window,
129 cx,
130 ),
131 is_http: true,
132 },
133 ConfigurationTarget::Extension {
134 id,
135 repository_url,
136 installation,
137 } => {
138 let settings_validator = installation.as_ref().and_then(|installation| {
139 jsonschema::validator_for(&installation.settings_schema)
140 .context("Failed to load JSON schema for context server settings")
141 .log_err()
142 });
143 let installation_instructions = installation.as_ref().map(|installation| {
144 cx.new(|cx| {
145 Markdown::new(
146 installation.installation_instructions.clone().into(),
147 Some(language_registry.clone()),
148 None,
149 cx,
150 )
151 })
152 });
153 ConfigurationSource::Extension {
154 id,
155 repository_url,
156 installation_instructions,
157 settings_validator,
158 editor: installation.map(|installation| {
159 create_editor(installation.default_settings, jsonc_language, window, cx)
160 }),
161 }
162 }
163 }
164 }
165
166 fn output(&self, cx: &mut App) -> Result<(ContextServerId, ContextServerSettings)> {
167 match self {
168 ConfigurationSource::New { editor, is_http }
169 | ConfigurationSource::Existing { editor, is_http } => {
170 if *is_http {
171 parse_http_input(&editor.read(cx).text(cx)).map(|(id, url, auth)| {
172 (
173 id,
174 ContextServerSettings::Http {
175 enabled: true,
176 url,
177 headers: auth,
178 },
179 )
180 })
181 } else {
182 parse_input(&editor.read(cx).text(cx)).map(|(id, command)| {
183 (
184 id,
185 ContextServerSettings::Stdio {
186 enabled: true,
187 command,
188 },
189 )
190 })
191 }
192 }
193 ConfigurationSource::Extension {
194 id,
195 editor,
196 settings_validator,
197 ..
198 } => {
199 let text = editor
200 .as_ref()
201 .context("No output available")?
202 .read(cx)
203 .text(cx);
204 let settings = serde_json_lenient::from_str::<serde_json::Value>(&text)?;
205 if let Some(settings_validator) = settings_validator
206 && let Err(error) = settings_validator.validate(&settings)
207 {
208 return Err(anyhow::anyhow!(error.to_string()));
209 }
210 Ok((
211 id.clone(),
212 ContextServerSettings::Extension {
213 enabled: true,
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 (id.0.to_string(), cmd.path, args, env)
228 }
229 None => (
230 "some-mcp-server".to_string(),
231 PathBuf::new(),
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": "{}",
243 /// The arguments to pass to the MCP server
244 "args": {args},
245 /// The environment variables to set
246 "env": {env}
247 }}
248}}"#,
249 command.display()
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 } => Some(ConfigurationTarget::Existing {
410 id: server_id,
411 command,
412 }),
413 ContextServerSettings::Http {
414 enabled: _,
415 url,
416 headers,
417 } => Some(ConfigurationTarget::ExistingHttp {
418 id: server_id,
419 url,
420 headers,
421 }),
422 ContextServerSettings::Extension { .. } => {
423 match workspace
424 .update(cx, |workspace, cx| {
425 resolve_context_server_extension(
426 server_id,
427 workspace.project().read(cx).worktree_store(),
428 cx,
429 )
430 })
431 .ok()
432 {
433 Some(task) => task.await,
434 None => None,
435 }
436 }
437 };
438
439 match target {
440 Some(target) => Self::show_modal(target, language_registry, workspace, cx).await,
441 None => Err(anyhow::anyhow!("Failed to resolve context server")),
442 }
443 })
444 }
445
446 fn show_modal(
447 target: ConfigurationTarget,
448 language_registry: Arc<LanguageRegistry>,
449 workspace: WeakEntity<Workspace>,
450 cx: &mut AsyncWindowContext,
451 ) -> Task<Result<()>> {
452 cx.spawn(async move |cx| {
453 let jsonc_language = language_registry.language_for_name("jsonc").await.ok();
454 workspace.update_in(cx, |workspace, window, cx| {
455 let workspace_handle = cx.weak_entity();
456 let context_server_store = workspace.project().read(cx).context_server_store();
457 workspace.toggle_modal(window, cx, |window, cx| Self {
458 context_server_store,
459 workspace: workspace_handle,
460 state: State::Idle,
461 original_server_id: match &target {
462 ConfigurationTarget::Existing { id, .. } => Some(id.clone()),
463 ConfigurationTarget::ExistingHttp { id, .. } => Some(id.clone()),
464 ConfigurationTarget::Extension { id, .. } => Some(id.clone()),
465 ConfigurationTarget::New => None,
466 },
467 source: ConfigurationSource::from_target(
468 target,
469 language_registry,
470 jsonc_language,
471 window,
472 cx,
473 ),
474 scroll_handle: ScrollHandle::new(),
475 })
476 })
477 })
478 }
479
480 fn set_error(&mut self, err: impl Into<SharedString>, cx: &mut Context<Self>) {
481 self.state = State::Error(err.into());
482 cx.notify();
483 }
484
485 fn confirm(&mut self, _: &menu::Confirm, cx: &mut Context<Self>) {
486 self.state = State::Idle;
487 let Some(workspace) = self.workspace.upgrade() else {
488 return;
489 };
490
491 let (id, settings) = match self.source.output(cx) {
492 Ok(val) => val,
493 Err(error) => {
494 self.set_error(error.to_string(), cx);
495 return;
496 }
497 };
498
499 self.state = State::Waiting;
500
501 let existing_server = self.context_server_store.read(cx).get_running_server(&id);
502 if existing_server.is_some() {
503 self.context_server_store.update(cx, |store, cx| {
504 store.stop_server(&id, cx).log_err();
505 });
506 }
507
508 let wait_for_context_server_task =
509 wait_for_context_server(&self.context_server_store, id.clone(), cx);
510 cx.spawn({
511 let id = id.clone();
512 async move |this, cx| {
513 let result = wait_for_context_server_task.await;
514 this.update(cx, |this, cx| match result {
515 Ok(_) => {
516 this.state = State::Idle;
517 this.show_configured_context_server_toast(id, cx);
518 cx.emit(DismissEvent);
519 }
520 Err(err) => {
521 this.set_error(err, cx);
522 }
523 })
524 }
525 })
526 .detach();
527
528 let settings_changed =
529 ProjectSettings::get_global(cx).context_servers.get(&id.0) != Some(&settings);
530
531 if settings_changed {
532 // When we write the settings to the file, the context server will be restarted.
533 workspace.update(cx, |workspace, cx| {
534 let fs = workspace.app_state().fs.clone();
535 let original_server_id = self.original_server_id.clone();
536 update_settings_file(fs.clone(), cx, move |current, _| {
537 if let Some(original_id) = original_server_id {
538 if original_id != id {
539 current.project.context_servers.remove(&original_id.0);
540 }
541 }
542 current
543 .project
544 .context_servers
545 .insert(id.0, settings.into());
546 });
547 });
548 } else if let Some(existing_server) = existing_server {
549 self.context_server_store
550 .update(cx, |store, cx| store.start_server(existing_server, cx));
551 }
552 }
553
554 fn cancel(&mut self, _: &menu::Cancel, cx: &mut Context<Self>) {
555 cx.emit(DismissEvent);
556 }
557
558 fn show_configured_context_server_toast(&self, id: ContextServerId, cx: &mut App) {
559 self.workspace
560 .update(cx, {
561 |workspace, cx| {
562 let status_toast = StatusToast::new(
563 format!("{} configured successfully.", id.0),
564 cx,
565 |this, _cx| {
566 this.icon(ToastIcon::new(IconName::ToolHammer).color(Color::Muted))
567 .action("Dismiss", |_, _| {})
568 },
569 );
570
571 workspace.toggle_status_toast(status_toast, cx);
572 }
573 })
574 .log_err();
575 }
576}
577
578fn parse_input(text: &str) -> Result<(ContextServerId, ContextServerCommand)> {
579 let value: serde_json::Value = serde_json_lenient::from_str(text)?;
580 let object = value.as_object().context("Expected object")?;
581 anyhow::ensure!(object.len() == 1, "Expected exactly one key-value pair");
582 let (context_server_name, value) = object.into_iter().next().unwrap();
583 let command: ContextServerCommand = serde_json::from_value(value.clone())?;
584 Ok((ContextServerId(context_server_name.clone().into()), command))
585}
586
587impl ModalView for ConfigureContextServerModal {}
588
589impl Focusable for ConfigureContextServerModal {
590 fn focus_handle(&self, cx: &App) -> FocusHandle {
591 match &self.source {
592 ConfigurationSource::New { editor, .. } => editor.focus_handle(cx),
593 ConfigurationSource::Existing { editor, .. } => editor.focus_handle(cx),
594 ConfigurationSource::Extension { editor, .. } => editor
595 .as_ref()
596 .map(|editor| editor.focus_handle(cx))
597 .unwrap_or_else(|| cx.focus_handle()),
598 }
599 }
600}
601
602impl EventEmitter<DismissEvent> for ConfigureContextServerModal {}
603
604impl ConfigureContextServerModal {
605 fn render_modal_header(&self) -> ModalHeader {
606 let text: SharedString = match &self.source {
607 ConfigurationSource::New { .. } => "Add MCP Server".into(),
608 ConfigurationSource::Existing { .. } => "Configure MCP Server".into(),
609 ConfigurationSource::Extension { id, .. } => format!("Configure {}", id.0).into(),
610 };
611 ModalHeader::new().headline(text)
612 }
613
614 fn render_modal_description(&self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
615 const MODAL_DESCRIPTION: &str = "Visit the MCP server configuration docs to find all necessary arguments and environment variables.";
616
617 if let ConfigurationSource::Extension {
618 installation_instructions: Some(installation_instructions),
619 ..
620 } = &self.source
621 {
622 div()
623 .pb_2()
624 .text_sm()
625 .child(MarkdownElement::new(
626 installation_instructions.clone(),
627 default_markdown_style(window, cx),
628 ))
629 .into_any_element()
630 } else {
631 Label::new(MODAL_DESCRIPTION)
632 .color(Color::Muted)
633 .into_any_element()
634 }
635 }
636
637 fn render_modal_content(&self, cx: &App) -> AnyElement {
638 let editor = match &self.source {
639 ConfigurationSource::New { editor, .. } => editor,
640 ConfigurationSource::Existing { editor, .. } => editor,
641 ConfigurationSource::Extension { editor, .. } => {
642 let Some(editor) = editor else {
643 return div().into_any_element();
644 };
645 editor
646 }
647 };
648
649 div()
650 .p_2()
651 .rounded_md()
652 .border_1()
653 .border_color(cx.theme().colors().border_variant)
654 .bg(cx.theme().colors().editor_background)
655 .child({
656 let settings = ThemeSettings::get_global(cx);
657 let text_style = TextStyle {
658 color: cx.theme().colors().text,
659 font_family: settings.buffer_font.family.clone(),
660 font_fallbacks: settings.buffer_font.fallbacks.clone(),
661 font_size: settings.buffer_font_size(cx).into(),
662 font_weight: settings.buffer_font.weight,
663 line_height: relative(settings.buffer_line_height.value()),
664 ..Default::default()
665 };
666 EditorElement::new(
667 editor,
668 EditorStyle {
669 background: cx.theme().colors().editor_background,
670 local_player: cx.theme().players().local(),
671 text: text_style,
672 syntax: cx.theme().syntax().clone(),
673 ..Default::default()
674 },
675 )
676 })
677 .into_any_element()
678 }
679
680 fn render_modal_footer(&self, cx: &mut Context<Self>) -> ModalFooter {
681 let focus_handle = self.focus_handle(cx);
682 let is_connecting = matches!(self.state, State::Waiting);
683
684 ModalFooter::new()
685 .start_slot::<Button>(
686 if let ConfigurationSource::Extension {
687 repository_url: Some(repository_url),
688 ..
689 } = &self.source
690 {
691 Some(
692 Button::new("open-repository", "Open Repository")
693 .icon(IconName::ArrowUpRight)
694 .icon_color(Color::Muted)
695 .icon_size(IconSize::Small)
696 .tooltip({
697 let repository_url = repository_url.clone();
698 move |_window, cx| {
699 Tooltip::with_meta(
700 "Open Repository",
701 None,
702 repository_url.clone(),
703 cx,
704 )
705 }
706 })
707 .on_click({
708 let repository_url = repository_url.clone();
709 move |_, _, cx| cx.open_url(&repository_url)
710 }),
711 )
712 } else if let ConfigurationSource::New { is_http, .. } = &self.source {
713 let label = if *is_http {
714 "Configure Local"
715 } else {
716 "Configure Remote"
717 };
718 let tooltip = if *is_http {
719 "Configure an MCP server that runs on stdin/stdout."
720 } else {
721 "Configure an MCP server that you connect to over HTTP"
722 };
723
724 Some(
725 Button::new("toggle-kind", label)
726 .tooltip(Tooltip::text(tooltip))
727 .on_click(cx.listener(|this, _, window, cx| match &mut this.source {
728 ConfigurationSource::New { editor, is_http } => {
729 *is_http = !*is_http;
730 let new_text = if *is_http {
731 context_server_http_input(None)
732 } else {
733 context_server_input(None)
734 };
735 editor.update(cx, |editor, cx| {
736 editor.set_text(new_text, window, cx);
737 })
738 }
739 _ => {}
740 })),
741 )
742 } else {
743 None
744 },
745 )
746 .end_slot(
747 h_flex()
748 .gap_2()
749 .child(
750 Button::new(
751 "cancel",
752 if self.source.has_configuration_options() {
753 "Cancel"
754 } else {
755 "Dismiss"
756 },
757 )
758 .key_binding(
759 KeyBinding::for_action_in(&menu::Cancel, &focus_handle, cx)
760 .map(|kb| kb.size(rems_from_px(12.))),
761 )
762 .on_click(
763 cx.listener(|this, _event, _window, cx| this.cancel(&menu::Cancel, cx)),
764 ),
765 )
766 .children(self.source.has_configuration_options().then(|| {
767 Button::new(
768 "add-server",
769 if self.source.is_new() {
770 "Add Server"
771 } else {
772 "Configure Server"
773 },
774 )
775 .disabled(is_connecting)
776 .key_binding(
777 KeyBinding::for_action_in(&menu::Confirm, &focus_handle, cx)
778 .map(|kb| kb.size(rems_from_px(12.))),
779 )
780 .on_click(
781 cx.listener(|this, _event, _window, cx| {
782 this.confirm(&menu::Confirm, cx)
783 }),
784 )
785 })),
786 )
787 }
788
789 fn render_waiting_for_context_server() -> Div {
790 h_flex()
791 .gap_2()
792 .child(
793 Icon::new(IconName::ArrowCircle)
794 .size(IconSize::XSmall)
795 .color(Color::Info)
796 .with_rotate_animation(2)
797 .into_any_element(),
798 )
799 .child(
800 Label::new("Waiting for Context Server")
801 .size(LabelSize::Small)
802 .color(Color::Muted),
803 )
804 }
805
806 fn render_modal_error(error: SharedString) -> Div {
807 h_flex()
808 .gap_2()
809 .child(
810 Icon::new(IconName::Warning)
811 .size(IconSize::XSmall)
812 .color(Color::Warning),
813 )
814 .child(
815 div()
816 .w_full()
817 .child(Label::new(error).size(LabelSize::Small).color(Color::Muted)),
818 )
819 }
820}
821
822impl Render for ConfigureContextServerModal {
823 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
824 div()
825 .elevation_3(cx)
826 .w(rems(34.))
827 .key_context("ConfigureContextServerModal")
828 .on_action(
829 cx.listener(|this, _: &menu::Cancel, _window, cx| this.cancel(&menu::Cancel, cx)),
830 )
831 .on_action(
832 cx.listener(|this, _: &menu::Confirm, _window, cx| {
833 this.confirm(&menu::Confirm, cx)
834 }),
835 )
836 .capture_any_mouse_down(cx.listener(|this, _, window, cx| {
837 this.focus_handle(cx).focus(window);
838 }))
839 .child(
840 Modal::new("configure-context-server", None)
841 .header(self.render_modal_header())
842 .section(
843 Section::new().child(
844 div()
845 .size_full()
846 .child(
847 div()
848 .id("modal-content")
849 .max_h(vh(0.7, window))
850 .overflow_y_scroll()
851 .track_scroll(&self.scroll_handle)
852 .child(self.render_modal_description(window, cx))
853 .child(self.render_modal_content(cx))
854 .child(match &self.state {
855 State::Idle => div(),
856 State::Waiting => {
857 Self::render_waiting_for_context_server()
858 }
859 State::Error(error) => {
860 Self::render_modal_error(error.clone())
861 }
862 }),
863 )
864 .vertical_scrollbar_for(&self.scroll_handle, window, cx),
865 ),
866 )
867 .footer(self.render_modal_footer(cx)),
868 )
869 }
870}
871
872fn wait_for_context_server(
873 context_server_store: &Entity<ContextServerStore>,
874 context_server_id: ContextServerId,
875 cx: &mut App,
876) -> Task<Result<(), Arc<str>>> {
877 let (tx, rx) = futures::channel::oneshot::channel();
878 let tx = Arc::new(Mutex::new(Some(tx)));
879
880 let subscription = cx.subscribe(context_server_store, move |_, event, _cx| match event {
881 project::context_server_store::Event::ServerStatusChanged { server_id, status } => {
882 match status {
883 ContextServerStatus::Running => {
884 if server_id == &context_server_id
885 && let Some(tx) = tx.lock().unwrap().take()
886 {
887 let _ = tx.send(Ok(()));
888 }
889 }
890 ContextServerStatus::Stopped => {
891 if server_id == &context_server_id
892 && let Some(tx) = tx.lock().unwrap().take()
893 {
894 let _ = tx.send(Err("Context server stopped running".into()));
895 }
896 }
897 ContextServerStatus::Error(error) => {
898 if server_id == &context_server_id
899 && let Some(tx) = tx.lock().unwrap().take()
900 {
901 let _ = tx.send(Err(error.clone()));
902 }
903 }
904 _ => {}
905 }
906 }
907 });
908
909 cx.spawn(async move |_cx| {
910 let result = rx
911 .await
912 .map_err(|_| Arc::from("Context server store was dropped"))?;
913 drop(subscription);
914 result
915 })
916}
917
918pub(crate) fn default_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
919 let theme_settings = ThemeSettings::get_global(cx);
920 let colors = cx.theme().colors();
921 let mut text_style = window.text_style();
922 text_style.refine(&TextStyleRefinement {
923 font_family: Some(theme_settings.ui_font.family.clone()),
924 font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
925 font_features: Some(theme_settings.ui_font.features.clone()),
926 font_size: Some(TextSize::XSmall.rems(cx).into()),
927 color: Some(colors.text_muted),
928 ..Default::default()
929 });
930
931 MarkdownStyle {
932 base_text_style: text_style.clone(),
933 selection_background_color: colors.element_selection_background,
934 link: TextStyleRefinement {
935 background_color: Some(colors.editor_foreground.opacity(0.025)),
936 underline: Some(UnderlineStyle {
937 color: Some(colors.text_accent.opacity(0.5)),
938 thickness: px(1.),
939 ..Default::default()
940 }),
941 ..Default::default()
942 },
943 ..Default::default()
944 }
945}