Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T05:05:43.870Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

configure_context_server_modal.rs

1312 lines · 48.6 KB · rust
1use anyhow::{Context as _, Result};
2use collections::HashMap;
3use context_server::{ContextServerCommand, ContextServerId};
4use editor::{Editor, EditorElement, EditorStyle};
5
6use extension_host::ExtensionStore;
7use gpui::{
8    AsyncWindowContext, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, ScrollHandle,
9    Subscription, Task, TextStyle, TextStyleRefinement, UnderlineStyle, WeakEntity, prelude::*,
10};
11use language::{Language, LanguageRegistry};
12use markdown::{Markdown, MarkdownElement, MarkdownStyle};
13use notifications::status_toast::StatusToast;
14use parking_lot::Mutex;
15use project::{
16    context_server_store::{
17        ContextServerStatus, ContextServerStore, ServerStatusChangedEvent,
18        registry::ContextServerDescriptorRegistry,
19    },
20    project_settings::{ContextServerSettings, OAuthClientSettings, ProjectSettings},
21    worktree_store::WorktreeStore,
22};
23use serde::Deserialize;
24use settings::{Settings as _, update_settings_file};
25use std::sync::Arc;
26use theme_settings::ThemeSettings;
27use ui::{
28    CommonAnimationExt, KeyBinding, Modal, ModalFooter, ModalHeader, Section, Tooltip,
29    WithScrollbar, prelude::*,
30};
31use util::ResultExt as _;
32use workspace::{ModalView, Workspace};
33
34enum ConfigurationTarget {
35    Existing {
36        id: ContextServerId,
37        command: ContextServerCommand,
38    },
39    ExistingHttp {
40        id: ContextServerId,
41        url: String,
42        headers: HashMap<String, String>,
43        oauth: Option<OAuthClientSettings>,
44    },
45
46    Extension {
47        id: ContextServerId,
48        repository_url: Option<SharedString>,
49        installation: Option<extension::ContextServerConfiguration>,
50    },
51}
52
53enum ExistingServerType {
54    Local,
55    Remote,
56}
57
58enum ConfigurationSource {
59    Existing {
60        editor: Entity<Editor>,
61        server_type: ExistingServerType,
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 from_target(
78        target: ConfigurationTarget,
79        language_registry: Arc<LanguageRegistry>,
80        jsonc_language: Option<Arc<Language>>,
81        window: &mut Window,
82        cx: &mut App,
83    ) -> Self {
84        fn create_editor(
85            json: String,
86            jsonc_language: Option<Arc<Language>>,
87            window: &mut Window,
88            cx: &mut App,
89        ) -> Entity<Editor> {
90            cx.new(|cx| {
91                let mut editor = Editor::auto_height(4, 16, window, cx);
92                editor.set_text(json, window, cx);
93                editor.set_show_gutter(false, cx);
94                editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx);
95                if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
96                    buffer.update(cx, |buffer, cx| buffer.set_language(jsonc_language, cx))
97                }
98                editor
99            })
100        }
101
102        match target {
103            ConfigurationTarget::Existing { id, command } => ConfigurationSource::Existing {
104                editor: create_editor(
105                    context_server_input(Some((id, command))),
106                    jsonc_language,
107                    window,
108                    cx,
109                ),
110                server_type: ExistingServerType::Local,
111            },
112            ConfigurationTarget::ExistingHttp {
113                id,
114                url,
115                headers: auth,
116                oauth,
117            } => ConfigurationSource::Existing {
118                editor: create_editor(
119                    context_server_http_input(Some((id, url, auth, oauth))),
120                    jsonc_language,
121                    window,
122                    cx,
123                ),
124                server_type: ExistingServerType::Remote,
125            },
126
127            ConfigurationTarget::Extension {
128                id,
129                repository_url,
130                installation,
131            } => {
132                let settings_validator = installation.as_ref().and_then(|installation| {
133                    jsonschema::validator_for(&installation.settings_schema)
134                        .context("Failed to load JSON schema for context server settings")
135                        .log_err()
136                });
137                let installation_instructions = installation.as_ref().map(|installation| {
138                    cx.new(|cx| {
139                        Markdown::new(
140                            installation.installation_instructions.clone().into(),
141                            Some(language_registry.clone()),
142                            None,
143                            cx,
144                        )
145                    })
146                });
147                ConfigurationSource::Extension {
148                    id,
149                    repository_url,
150                    installation_instructions,
151                    settings_validator,
152                    editor: installation.map(|installation| {
153                        create_editor(installation.default_settings, jsonc_language, window, cx)
154                    }),
155                }
156            }
157        }
158    }
159
160    fn output(&self, cx: &mut App) -> Result<(ContextServerId, ContextServerSettings)> {
161        match self {
162            ConfigurationSource::Existing {
163                editor,
164                server_type,
165            } => match *server_type {
166                ExistingServerType::Remote => {
167                    parse_http_input(&editor.read(cx).text(cx)).map(|(id, url, auth, oauth)| {
168                        (
169                            id,
170                            ContextServerSettings::Http {
171                                enabled: true,
172                                url,
173                                headers: auth,
174                                timeout: None,
175                                oauth,
176                            },
177                        )
178                    })
179                }
180                ExistingServerType::Local => {
181                    parse_input(&editor.read(cx).text(cx)).map(|(id, command)| {
182                        (
183                            id,
184                            ContextServerSettings::Stdio {
185                                enabled: true,
186                                remote: false,
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                        remote: false,
215                        settings,
216                    },
217                ))
218            }
219        }
220    }
221}
222
223fn context_server_input(existing: Option<(ContextServerId, ContextServerCommand)>) -> String {
224    let (name, command, args, env) = match existing {
225        Some((id, cmd)) => {
226            let args = serde_json::to_string(&cmd.args).unwrap();
227            let env = serde_json::to_string(&cmd.env.unwrap_or_default()).unwrap();
228            let cmd_path = serde_json::to_string(&cmd.path).unwrap();
229            (id.0.to_string(), cmd_path, args, env)
230        }
231        None => (
232            "some-mcp-server".to_string(),
233            "".to_string(),
234            "[]".to_string(),
235            "{}".to_string(),
236        ),
237    };
238
239    format!(
240        r#"{{
241  /// Configure an MCP server that runs locally via stdin/stdout
242  ///
243  /// The name of your MCP server
244  "{name}": {{
245    /// The command which runs the MCP server
246    "command": {command},
247    /// The arguments to pass to the MCP server
248    "args": {args},
249    /// The environment variables to set
250    "env": {env}
251  }}
252}}"#
253    )
254}
255
256fn context_server_http_input(
257    existing: Option<(
258        ContextServerId,
259        String,
260        HashMap<String, String>,
261        Option<OAuthClientSettings>,
262    )>,
263) -> String {
264    let (name, url, headers, oauth) = match existing {
265        Some((id, url, headers, oauth)) => {
266            let headers = if headers.is_empty() {
267                r#"// "Authorization": "Bearer <token>"#.to_string()
268            } else {
269                let json = serde_json::to_string_pretty(&headers).unwrap();
270                let mut lines = json.split("\n").collect::<Vec<_>>();
271                if lines.len() > 1 {
272                    lines.remove(0);
273                    lines.pop();
274                }
275                lines
276                    .into_iter()
277                    .map(|line| format!("  {}", line))
278                    .collect::<String>()
279            };
280            (id.0.to_string(), url, headers, oauth)
281        }
282        None => (
283            "some-remote-server".to_string(),
284            "https://example.com/mcp".to_string(),
285            r#"// "Authorization": "Bearer <token>"#.to_string(),
286            None,
287        ),
288    };
289
290    let oauth = oauth.map_or_else(
291        || {
292            r#"
293    /// Uncomment to use a pre-registered OAuth client. You can include the client secret here as well, otherwise it will be prompted interactively and saved in the system keychain.
294    // "oauth": {
295    //   "client_id": "your-client-id",
296    // },"#
297                .to_string()
298        },
299
300        |oauth| {
301            let mut lines = vec![
302                String::from("\n    \"oauth\": {"),
303
304                format!("      \"client_id\": {},", serde_json::to_string(&oauth.client_id).unwrap()),
305            ];
306            if let Some(client_secret) = oauth.client_secret {
307                lines.push(format!(
308                    "      \"client_secret\": {}",
309                    serde_json::to_string(&client_secret).unwrap()
310                ));
311            } else {
312                lines.push(String::from(
313                    "      /// Optional client secret for confidential clients\n      // \"client_secret\": \"your-client-secret\"",
314                ));
315            }
316            lines.push(String::from("    },"));
317
318            lines.join("\n")
319        },
320    );
321
322    format!(
323        r#"{{
324  /// Configure an MCP server that you connect to over HTTP
325  ///
326  /// The name of your remote MCP server
327  "{name}": {{
328    /// The URL of the remote MCP server
329    "url": "{url}",{oauth}
330    "headers": {{
331     /// Any headers to send along
332     {headers}
333    }}
334  }}
335}}"#
336    )
337}
338
339fn parse_http_input(
340    text: &str,
341) -> Result<(
342    ContextServerId,
343    String,
344    HashMap<String, String>,
345    Option<OAuthClientSettings>,
346)> {
347    #[derive(Deserialize)]
348    struct Temp {
349        url: String,
350        #[serde(default)]
351        headers: HashMap<String, String>,
352        #[serde(default)]
353        oauth: Option<OAuthClientSettings>,
354    }
355    let value: HashMap<String, Temp> = serde_json_lenient::from_str(text)?;
356    if value.len() != 1 {
357        anyhow::bail!("Expected exactly one context server configuration");
358    }
359
360    let (key, value) = value.into_iter().next().unwrap();
361
362    Ok((
363        ContextServerId(key.into()),
364        value.url,
365        value.headers,
366        value.oauth,
367    ))
368}
369
370fn resolve_context_server_extension(
371    id: ContextServerId,
372    worktree_store: Entity<WorktreeStore>,
373    cx: &mut App,
374) -> Task<Option<ConfigurationTarget>> {
375    let registry = ContextServerDescriptorRegistry::default_global(cx).read(cx);
376
377    let Some(descriptor) = registry.context_server_descriptor(&id.0) else {
378        return Task::ready(None);
379    };
380
381    let extension = ExtensionStore::global(cx)
382        .read(cx)
383        .installed_extensions()
384        .iter()
385        .find(|(_, entry)| entry.manifest.context_servers.contains_key(&id.0))
386        .map(|(id, entry)| (id.clone(), entry.manifest.clone()));
387    cx.spawn(async move |cx| {
388        let installation = descriptor
389            .configuration(worktree_store, cx)
390            .await
391            .context("Failed to resolve context server configuration")
392            .log_err()
393            .flatten();
394
395        Some(ConfigurationTarget::Extension {
396            id,
397            repository_url: extension
398                .and_then(|(_, manifest)| manifest.repository.clone().map(SharedString::from)),
399            installation,
400        })
401    })
402}
403
404enum State {
405    Idle,
406    Waiting,
407    AuthRequired {
408        server_id: ContextServerId,
409    },
410    ClientSecretRequired {
411        server_id: ContextServerId,
412        error: Option<SharedString>,
413    },
414    Authenticating {
415        server_id: ContextServerId,
416    },
417    Error(SharedString),
418}
419
420pub struct ConfigureContextServerModal {
421    context_server_store: Entity<ContextServerStore>,
422    workspace: WeakEntity<Workspace>,
423    source: ConfigurationSource,
424    state: State,
425    original_server_id: Option<ContextServerId>,
426    scroll_handle: ScrollHandle,
427    secret_editor: Entity<Editor>,
428    _auth_subscription: Option<Subscription>,
429}
430
431impl ConfigureContextServerModal {
432    fn initial_state(
433        context_server_store: &Entity<ContextServerStore>,
434        target: &ConfigurationTarget,
435        cx: &App,
436    ) -> State {
437        let server_id = match target {
438            ConfigurationTarget::Existing { id, .. }
439            | ConfigurationTarget::ExistingHttp { id, .. }
440            | ConfigurationTarget::Extension { id, .. } => id,
441        };
442
443        match context_server_store.read(cx).status_for_server(server_id) {
444            Some(ContextServerStatus::AuthRequired) => State::AuthRequired {
445                server_id: server_id.clone(),
446            },
447            Some(ContextServerStatus::ClientSecretRequired { error }) => {
448                State::ClientSecretRequired {
449                    server_id: server_id.clone(),
450                    error: error.map(SharedString::from),
451                }
452            }
453            Some(ContextServerStatus::Authenticating) => State::Authenticating {
454                server_id: server_id.clone(),
455            },
456            Some(ContextServerStatus::Error(error)) => State::Error(error.into()),
457
458            Some(ContextServerStatus::Starting)
459            | Some(ContextServerStatus::Running)
460            | Some(ContextServerStatus::Stopped)
461            | None => State::Idle,
462        }
463    }
464
465    pub fn show_modal_for_existing_server(
466        server_id: ContextServerId,
467        language_registry: Arc<LanguageRegistry>,
468        workspace: WeakEntity<Workspace>,
469        window: &mut Window,
470        cx: &mut App,
471    ) -> Task<Result<()>> {
472        let Some(settings) = ProjectSettings::get_global(cx)
473            .context_servers
474            .get(&server_id.0)
475            .cloned()
476            .or_else(|| {
477                ContextServerDescriptorRegistry::default_global(cx)
478                    .read(cx)
479                    .context_server_descriptor(&server_id.0)
480                    .map(|_| ContextServerSettings::default_extension())
481            })
482        else {
483            return Task::ready(Err(anyhow::anyhow!("Context server not found")));
484        };
485
486        window.spawn(cx, async move |cx| {
487            let target = match settings {
488                ContextServerSettings::Stdio {
489                    enabled: _,
490                    command,
491                    ..
492                } => Some(ConfigurationTarget::Existing {
493                    id: server_id,
494                    command,
495                }),
496                ContextServerSettings::Http {
497                    enabled: _,
498                    url,
499                    headers,
500                    timeout: _,
501                    oauth,
502                } => Some(ConfigurationTarget::ExistingHttp {
503                    id: server_id,
504                    url,
505                    headers,
506                    oauth,
507                }),
508
509                ContextServerSettings::Extension { .. } => {
510                    match workspace
511                        .update(cx, |workspace, cx| {
512                            resolve_context_server_extension(
513                                server_id,
514                                workspace.project().read(cx).worktree_store(),
515                                cx,
516                            )
517                        })
518                        .ok()
519                    {
520                        Some(task) => task.await,
521                        None => None,
522                    }
523                }
524            };
525
526            match target {
527                Some(target) => Self::show_modal(target, language_registry, workspace, cx).await,
528                None => Err(anyhow::anyhow!("Failed to resolve context server")),
529            }
530        })
531    }
532
533    fn show_modal(
534        target: ConfigurationTarget,
535        language_registry: Arc<LanguageRegistry>,
536        workspace: WeakEntity<Workspace>,
537        cx: &mut AsyncWindowContext,
538    ) -> Task<Result<()>> {
539        cx.spawn(async move |cx| {
540            let jsonc_language = language_registry.language_for_name("jsonc").await.ok();
541            workspace.update_in(cx, |workspace, window, cx| {
542                let workspace_handle = cx.weak_entity();
543                let context_server_store = workspace.project().read(cx).context_server_store();
544                workspace.toggle_modal(window, cx, |window, cx| Self {
545                    context_server_store: context_server_store.clone(),
546                    workspace: workspace_handle,
547                    state: Self::initial_state(&context_server_store, &target, cx),
548
549                    original_server_id: Some(match &target {
550                        ConfigurationTarget::Existing { id, .. }
551                        | ConfigurationTarget::ExistingHttp { id, .. }
552                        | ConfigurationTarget::Extension { id, .. } => id.clone(),
553                    }),
554                    source: ConfigurationSource::from_target(
555                        target,
556                        language_registry,
557                        jsonc_language,
558                        window,
559                        cx,
560                    ),
561                    scroll_handle: ScrollHandle::new(),
562                    secret_editor: cx.new(|cx| {
563                        let mut editor = Editor::single_line(window, cx);
564                        editor.set_placeholder_text(
565                            "Enter client secret (leave empty for public clients)",
566                            window,
567                            cx,
568                        );
569                        editor.set_masked(true, cx);
570                        editor
571                    }),
572                    _auth_subscription: None,
573                })
574            })
575        })
576    }
577
578    fn set_error(&mut self, err: impl Into<SharedString>, cx: &mut Context<Self>) {
579        self.state = State::Error(err.into());
580        cx.notify();
581    }
582
583    fn confirm(&mut self, _: &menu::Confirm, cx: &mut Context<Self>) {
584        if matches!(self.state, State::Waiting | State::Authenticating { .. }) {
585            return;
586        }
587
588        self._auth_subscription = None;
589
590        self.state = State::Idle;
591        let Some(workspace) = self.workspace.upgrade() else {
592            return;
593        };
594
595        let (id, settings) = match self.source.output(cx) {
596            Ok(val) => val,
597            Err(error) => {
598                self.set_error(error.to_string(), cx);
599                return;
600            }
601        };
602
603        self.state = State::Waiting;
604
605        let existing_server = self.context_server_store.read(cx).get_server(&id);
606        if existing_server.is_some() {
607            self.context_server_store.update(cx, |store, cx| {
608                store.stop_server(&id, cx).log_err();
609            });
610        }
611
612        let wait_for_context_server_task =
613            wait_for_context_server(&self.context_server_store, id.clone(), cx);
614        cx.spawn({
615            let id = id.clone();
616            async move |this, cx| {
617                let result = wait_for_context_server_task.await;
618                this.update(cx, |this, cx| match result {
619                    Ok(ContextServerStatus::Running) => {
620                        this.state = State::Idle;
621                        this.show_configured_context_server_toast(id, cx);
622                        cx.emit(DismissEvent);
623                    }
624                    Ok(ContextServerStatus::AuthRequired) => {
625                        this.state = State::AuthRequired { server_id: id };
626                        cx.notify();
627                    }
628                    Ok(ContextServerStatus::ClientSecretRequired { error }) => {
629                        this.state = State::ClientSecretRequired {
630                            server_id: id,
631                            error: error.map(SharedString::from),
632                        };
633                        cx.notify();
634                    }
635                    Err(err) => {
636                        this.set_error(err, cx);
637                    }
638                    Ok(_) => {}
639                })
640            }
641        })
642        .detach();
643
644        let settings_changed =
645            ProjectSettings::get_global(cx).context_servers.get(&id.0) != Some(&settings);
646
647        if settings_changed {
648            // When we write the settings to the file, the context server will be restarted.
649            workspace.update(cx, |workspace, cx| {
650                let fs = workspace.app_state().fs.clone();
651                let original_server_id = self.original_server_id.clone();
652                update_settings_file(fs.clone(), cx, move |current, _| {
653                    if let Some(original_id) = original_server_id {
654                        if original_id != id {
655                            current.project.context_servers.remove(&original_id.0);
656                        }
657                    }
658                    current
659                        .project
660                        .context_servers
661                        .insert(id.0, settings.into());
662                });
663            });
664        } else if let Some(existing_server) = existing_server {
665            self.context_server_store
666                .update(cx, |store, cx| store.start_server(existing_server, cx));
667        }
668    }
669
670    fn cancel(&mut self, _: &menu::Cancel, cx: &mut Context<Self>) {
671        cx.emit(DismissEvent);
672    }
673
674    fn cancel_authentication(&mut self, server_id: &ContextServerId, cx: &mut Context<Self>) {
675        self._auth_subscription = None;
676        self.context_server_store.update(cx, |store, cx| {
677            store.stop_server(server_id, cx).log_err();
678        });
679        self.state = State::Idle;
680        cx.notify();
681    }
682
683    fn authenticate(&mut self, server_id: ContextServerId, cx: &mut Context<Self>) {
684        self.context_server_store.update(cx, |store, cx| {
685            store.authenticate_server(&server_id, cx).log_err();
686        });
687        self.await_auth_outcome(server_id, cx);
688    }
689
690    fn submit_client_secret(&mut self, server_id: ContextServerId, cx: &mut Context<Self>) {
691        let secret = self.secret_editor.read(cx).text(cx);
692        self.context_server_store.update(cx, |store, cx| {
693            store.submit_client_secret(&server_id, secret, cx).log_err();
694        });
695        self.await_auth_outcome(server_id, cx);
696    }
697
698    fn await_auth_outcome(&mut self, server_id: ContextServerId, cx: &mut Context<Self>) {
699        self.state = State::Authenticating {
700            server_id: server_id.clone(),
701        };
702
703        self._auth_subscription = Some(cx.subscribe(
704            &self.context_server_store,
705            move |this, _, event: &ServerStatusChangedEvent, cx| {
706                if event.server_id != server_id {
707                    return;
708                }
709                match &event.status {
710                    ContextServerStatus::Running => {
711                        this._auth_subscription = None;
712                        this.state = State::Idle;
713                        this.show_configured_context_server_toast(event.server_id.clone(), cx);
714                        cx.emit(DismissEvent);
715                    }
716                    ContextServerStatus::AuthRequired => {
717                        this._auth_subscription = None;
718                        this.state = State::AuthRequired {
719                            server_id: event.server_id.clone(),
720                        };
721                        cx.notify();
722                    }
723                    ContextServerStatus::ClientSecretRequired { error } => {
724                        this._auth_subscription = None;
725                        this.state = State::ClientSecretRequired {
726                            server_id: event.server_id.clone(),
727                            error: error.clone().map(SharedString::from),
728                        };
729                        cx.notify();
730                    }
731                    ContextServerStatus::Error(error) => {
732                        this._auth_subscription = None;
733                        this.set_error(error.clone(), cx);
734                    }
735                    ContextServerStatus::Authenticating
736                    | ContextServerStatus::Starting
737                    | ContextServerStatus::Stopped => {}
738                }
739            },
740        ));
741
742        cx.notify();
743    }
744
745    fn show_configured_context_server_toast(&self, id: ContextServerId, cx: &mut App) {
746        self.workspace
747            .update(cx, {
748                |workspace, cx| {
749                    let status_toast = StatusToast::new(
750                        format!("{} configured successfully.", id.0),
751                        cx,
752                        |this, _cx| {
753                            this.icon(
754                                Icon::new(IconName::ToolHammer)
755                                    .size(IconSize::Small)
756                                    .color(Color::Muted),
757                            )
758                            .action("Dismiss", |_, _| {})
759                        },
760                    );
761
762                    workspace.toggle_status_toast(status_toast, cx);
763                }
764            })
765            .log_err();
766    }
767}
768
769fn parse_input(text: &str) -> Result<(ContextServerId, ContextServerCommand)> {
770    let value: serde_json::Value = serde_json_lenient::from_str(text)?;
771    let object = value.as_object().context("Expected object")?;
772    anyhow::ensure!(object.len() == 1, "Expected exactly one key-value pair");
773    let (context_server_name, value) = object.into_iter().next().unwrap();
774    let command: ContextServerCommand = serde_json::from_value(value.clone())?;
775    Ok((ContextServerId(context_server_name.clone().into()), command))
776}
777
778impl ModalView for ConfigureContextServerModal {}
779
780impl Focusable for ConfigureContextServerModal {
781    fn focus_handle(&self, cx: &App) -> FocusHandle {
782        match &self.source {
783            ConfigurationSource::Existing { editor, .. } => editor.focus_handle(cx),
784            ConfigurationSource::Extension { editor, .. } => editor
785                .as_ref()
786                .map(|editor| editor.focus_handle(cx))
787                .unwrap_or_else(|| cx.focus_handle()),
788        }
789    }
790}
791
792impl EventEmitter<DismissEvent> for ConfigureContextServerModal {}
793
794impl ConfigureContextServerModal {
795    fn render_modal_header(&self) -> ModalHeader {
796        let text: SharedString = match &self.source {
797            ConfigurationSource::Existing { .. } => "Configure MCP Server".into(),
798            ConfigurationSource::Extension { id, .. } => format!("Configure {}", id.0).into(),
799        };
800        ModalHeader::new().headline(text)
801    }
802
803    fn render_modal_description(&self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
804        const MODAL_DESCRIPTION: &str =
805            "Check the server docs for required arguments and environment variables.";
806
807        if let ConfigurationSource::Extension {
808            installation_instructions: Some(installation_instructions),
809            ..
810        } = &self.source
811        {
812            div()
813                .pb_2()
814                .text_sm()
815                .child(MarkdownElement::new(
816                    installation_instructions.clone(),
817                    default_markdown_style(window, cx),
818                ))
819                .into_any_element()
820        } else {
821            Label::new(MODAL_DESCRIPTION)
822                .color(Color::Muted)
823                .into_any_element()
824        }
825    }
826
827    fn render_modal_content(&self, cx: &App) -> AnyElement {
828        let editor = match &self.source {
829            ConfigurationSource::Existing { editor, .. } => editor,
830            ConfigurationSource::Extension { editor, .. } => {
831                let Some(editor) = editor else {
832                    return div().into_any_element();
833                };
834                editor
835            }
836        };
837
838        div()
839            .p_2()
840            .rounded_md()
841            .border_1()
842            .border_color(cx.theme().colors().border_variant)
843            .bg(cx.theme().colors().editor_background)
844            .child({
845                let settings = ThemeSettings::get_global(cx);
846                let text_style = TextStyle {
847                    color: cx.theme().colors().text,
848                    font_family: settings.buffer_font.family.clone(),
849                    font_fallbacks: settings.buffer_font.fallbacks.clone(),
850                    font_size: settings.buffer_font_size(cx).into(),
851                    font_weight: settings.buffer_font.weight,
852                    line_height: relative(settings.buffer_line_height.value()),
853                    ..Default::default()
854                };
855                EditorElement::new(
856                    editor,
857                    EditorStyle {
858                        background: cx.theme().colors().editor_background,
859                        local_player: cx.theme().players().local(),
860                        text: text_style,
861                        syntax: cx.theme().syntax().clone(),
862                        ..Default::default()
863                    },
864                )
865            })
866            .into_any_element()
867    }
868
869    fn render_modal_footer(&self, cx: &mut Context<Self>) -> ModalFooter {
870        let focus_handle = self.focus_handle(cx);
871        let is_busy = matches!(self.state, State::Waiting | State::Authenticating { .. });
872
873        ModalFooter::new()
874            .start_slot::<Button>(
875                if let ConfigurationSource::Extension {
876                    repository_url: Some(repository_url),
877                    ..
878                } = &self.source
879                {
880                    Some(
881                        Button::new("open-repository", "Open Repository")
882                            .end_icon(
883                                Icon::new(IconName::ArrowUpRight)
884                                    .size(IconSize::Small)
885                                    .color(Color::Muted),
886                            )
887                            .tooltip({
888                                let repository_url = repository_url.clone();
889                                move |_window, cx| {
890                                    Tooltip::with_meta(
891                                        "Open Repository",
892                                        None,
893                                        repository_url.clone(),
894                                        cx,
895                                    )
896                                }
897                            })
898                            .on_click({
899                                let repository_url = repository_url.clone();
900                                move |_, _, cx| cx.open_url(&repository_url)
901                            }),
902                    )
903                } else {
904                    None
905                },
906            )
907            .end_slot(
908                h_flex()
909                    .gap_2()
910                    .child(
911                        Button::new(
912                            "cancel",
913                            if self.source.has_configuration_options() {
914                                "Cancel"
915                            } else {
916                                "Dismiss"
917                            },
918                        )
919                        .key_binding(
920                            KeyBinding::for_action_in(&menu::Cancel, &focus_handle, cx)
921                                .map(|kb| kb.size(rems_from_px(12.))),
922                        )
923                        .on_click(
924                            cx.listener(|this, _event, _window, cx| this.cancel(&menu::Cancel, cx)),
925                        ),
926                    )
927                    .children(self.source.has_configuration_options().then(|| {
928                        Button::new("configure-server", "Configure Server")
929                            .disabled(is_busy)
930                            .key_binding(
931                                KeyBinding::for_action_in(&menu::Confirm, &focus_handle, cx)
932                                    .map(|kb| kb.size(rems_from_px(12.))),
933                            )
934                            .on_click(cx.listener(|this, _event, _window, cx| {
935                                this.confirm(&menu::Confirm, cx)
936                            }))
937                    })),
938            )
939    }
940
941    fn render_loading(&self, label: impl Into<SharedString>) -> Div {
942        h_flex()
943            .h_8()
944            .gap_1p5()
945            .justify_center()
946            .child(
947                Icon::new(IconName::LoadCircle)
948                    .size(IconSize::XSmall)
949                    .color(Color::Muted)
950                    .with_rotate_animation(3),
951            )
952            .child(Label::new(label).size(LabelSize::Small).color(Color::Muted))
953    }
954
955    fn render_auth_required(&self, server_id: &ContextServerId, cx: &mut Context<Self>) -> Div {
956        h_flex()
957            .h_8()
958            .min_w_0()
959            .w_full()
960            .gap_2()
961            .justify_center()
962            .child(
963                h_flex()
964                    .gap_1p5()
965                    .child(
966                        Icon::new(IconName::Info)
967                            .size(IconSize::Small)
968                            .color(Color::Muted),
969                    )
970                    .child(
971                        Label::new("Authenticate to connect this server")
972                            .size(LabelSize::Small)
973                            .color(Color::Muted),
974                    ),
975            )
976            .child(
977                Button::new("authenticate-server", "Authenticate")
978                    .style(ButtonStyle::Outlined)
979                    .label_size(LabelSize::Small)
980                    .on_click({
981                        let server_id = server_id.clone();
982                        cx.listener(move |this, _event, _window, cx| {
983                            this.authenticate(server_id.clone(), cx);
984                        })
985                    }),
986            )
987    }
988
989    fn render_client_secret_required(
990        &self,
991        server_id: &ContextServerId,
992        error: Option<SharedString>,
993        cx: &mut Context<Self>,
994    ) -> Div {
995        let settings = ThemeSettings::get_global(cx);
996        let text_style = TextStyle {
997            color: cx.theme().colors().text,
998            font_family: settings.buffer_font.family.clone(),
999            font_fallbacks: settings.buffer_font.fallbacks.clone(),
1000            font_size: settings.buffer_font_size(cx).into(),
1001            font_weight: settings.buffer_font.weight,
1002            line_height: relative(settings.buffer_line_height.value()),
1003            ..Default::default()
1004        };
1005
1006        v_flex()
1007            .w_full()
1008            .gap_2()
1009            .when_some(error, |this, error| {
1010                this.child(Self::render_modal_error(error))
1011            })
1012            .child(
1013                h_flex()
1014                    .gap_1p5()
1015                    .child(
1016                        Icon::new(IconName::Info)
1017                            .size(IconSize::Small)
1018                            .color(Color::Muted),
1019                    )
1020                    .child(
1021                        Label::new(
1022                            "Enter your OAuth client secret, or leave empty for public clients",
1023                        )
1024                        .size(LabelSize::Small)
1025                        .color(Color::Muted),
1026                    ),
1027            )
1028            .child(
1029                h_flex()
1030                    .w_full()
1031                    .gap_2()
1032                    .capture_action({
1033                        let server_id = server_id.clone();
1034                        cx.listener(move |this, _: &editor::actions::Newline, _window, cx| {
1035                            this.submit_client_secret(server_id.clone(), cx);
1036                        })
1037                    })
1038                    .child(div().flex_1().child(EditorElement::new(
1039                        &self.secret_editor,
1040                        EditorStyle {
1041                            background: cx.theme().colors().editor_background,
1042                            local_player: cx.theme().players().local(),
1043                            text: text_style,
1044                            syntax: cx.theme().syntax().clone(),
1045                            ..Default::default()
1046                        },
1047                    )))
1048                    .child(
1049                        Button::new("submit-client-secret", "Submit")
1050                            .style(ButtonStyle::Outlined)
1051                            .label_size(LabelSize::Small)
1052                            .on_click({
1053                                let server_id = server_id.clone();
1054                                cx.listener(move |this, _event, _window, cx| {
1055                                    this.submit_client_secret(server_id.clone(), cx);
1056                                })
1057                            }),
1058                    ),
1059            )
1060    }
1061
1062    fn render_authenticating(&self, server_id: &ContextServerId, cx: &mut Context<Self>) -> Div {
1063        h_flex()
1064            .h_8()
1065            .gap_2()
1066            .justify_center()
1067            .child(
1068                h_flex()
1069                    .gap_1p5()
1070                    .child(
1071                        Icon::new(IconName::LoadCircle)
1072                            .size(IconSize::XSmall)
1073                            .color(Color::Muted)
1074                            .with_rotate_animation(3),
1075                    )
1076                    .child(
1077                        Label::new("Authenticating…")
1078                            .size(LabelSize::Small)
1079                            .color(Color::Muted),
1080                    ),
1081            )
1082            .child(
1083                Button::new("cancel-authentication", "Cancel")
1084                    .style(ButtonStyle::Outlined)
1085                    .label_size(LabelSize::Small)
1086                    .on_click({
1087                        let server_id = server_id.clone();
1088                        cx.listener(move |this, _event, _window, cx| {
1089                            this.cancel_authentication(&server_id, cx);
1090                        })
1091                    }),
1092            )
1093    }
1094
1095    fn render_modal_error(error: SharedString) -> Div {
1096        h_flex()
1097            .h_8()
1098            .gap_1p5()
1099            .justify_center()
1100            .child(
1101                Icon::new(IconName::Warning)
1102                    .size(IconSize::Small)
1103                    .color(Color::Warning),
1104            )
1105            .child(
1106                div()
1107                    .w_full()
1108                    .child(Label::new(error).size(LabelSize::Small).color(Color::Muted)),
1109            )
1110    }
1111}
1112
1113impl Render for ConfigureContextServerModal {
1114    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1115        div()
1116            .elevation_3(cx)
1117            .w(rems(40.))
1118            .key_context("ConfigureContextServerModal")
1119            .on_action(
1120                cx.listener(|this, _: &menu::Cancel, _window, cx| this.cancel(&menu::Cancel, cx)),
1121            )
1122            .on_action(
1123                cx.listener(|this, _: &menu::Confirm, _window, cx| {
1124                    this.confirm(&menu::Confirm, cx)
1125                }),
1126            )
1127            .capture_any_mouse_down(cx.listener(|this, _, window, cx| {
1128                this.focus_handle(cx).focus(window, cx);
1129            }))
1130            .child(
1131                Modal::new("configure-context-server", None)
1132                    .header(self.render_modal_header())
1133                    .section(
1134                        Section::new().child(
1135                            div()
1136                                .size_full()
1137                                .child(
1138                                    div()
1139                                        .id("modal-content")
1140                                        .max_h(vh(0.7, window))
1141                                        .overflow_y_scroll()
1142                                        .track_scroll(&self.scroll_handle)
1143                                        .child(self.render_modal_description(window, cx))
1144                                        .child(self.render_modal_content(cx))
1145                                        .child(match &self.state {
1146                                            State::Idle => div(),
1147                                            State::Waiting => {
1148                                                self.render_loading("Connecting Server…")
1149                                            }
1150                                            State::AuthRequired { server_id } => {
1151                                                self.render_auth_required(&server_id.clone(), cx)
1152                                            }
1153                                            State::ClientSecretRequired { server_id, error } => {
1154                                                self.render_client_secret_required(
1155                                                    &server_id.clone(),
1156                                                    error.clone(),
1157                                                    cx,
1158                                                )
1159                                            }
1160                                            State::Authenticating { server_id } => {
1161                                                self.render_authenticating(&server_id.clone(), cx)
1162                                            }
1163                                            State::Error(error) => {
1164                                                Self::render_modal_error(error.clone())
1165                                            }
1166                                        }),
1167                                )
1168                                .vertical_scrollbar_for(&self.scroll_handle, window, cx),
1169                        ),
1170                    )
1171                    .footer(self.render_modal_footer(cx)),
1172            )
1173    }
1174}
1175
1176fn wait_for_context_server(
1177    context_server_store: &Entity<ContextServerStore>,
1178    context_server_id: ContextServerId,
1179    cx: &mut App,
1180) -> Task<Result<ContextServerStatus, Arc<str>>> {
1181    use std::time::Duration;
1182
1183    const WAIT_TIMEOUT: Duration = Duration::from_secs(120);
1184
1185    let (tx, rx) = futures::channel::oneshot::channel();
1186    let tx = Arc::new(Mutex::new(Some(tx)));
1187
1188    let context_server_id_for_timeout = context_server_id.clone();
1189    let subscription = cx.subscribe(context_server_store, move |_, event, _cx| {
1190        let ServerStatusChangedEvent { server_id, status } = event;
1191
1192        if server_id != &context_server_id {
1193            return;
1194        }
1195
1196        match status {
1197            ContextServerStatus::Running
1198            | ContextServerStatus::AuthRequired
1199            | ContextServerStatus::ClientSecretRequired { .. } => {
1200                if let Some(tx) = tx.lock().take() {
1201                    let _ = tx.send(Ok(status.clone()));
1202                }
1203            }
1204            ContextServerStatus::Stopped => {
1205                if let Some(tx) = tx.lock().take() {
1206                    let _ = tx.send(Err("Context server stopped running".into()));
1207                }
1208            }
1209            ContextServerStatus::Error(error) => {
1210                if let Some(tx) = tx.lock().take() {
1211                    let _ = tx.send(Err(error.clone()));
1212                }
1213            }
1214            ContextServerStatus::Starting | ContextServerStatus::Authenticating => {}
1215        }
1216    });
1217
1218    cx.spawn(async move |cx| {
1219        let timeout = cx.background_executor().timer(WAIT_TIMEOUT);
1220        let result = futures::future::select(rx, timeout).await;
1221        drop(subscription);
1222        match result {
1223            futures::future::Either::Left((Ok(inner), _)) => inner,
1224            futures::future::Either::Left((Err(_), _)) => {
1225                Err(Arc::from("Context server store was dropped"))
1226            }
1227            futures::future::Either::Right(_) => Err(Arc::from(format!(
1228                "Timed out waiting for context server `{}` to start. Check the Omega log for details.",
1229                context_server_id_for_timeout
1230            ))),
1231        }
1232    })
1233}
1234
1235pub(crate) fn default_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
1236    let theme_settings = ThemeSettings::get_global(cx);
1237    let colors = cx.theme().colors();
1238    let mut text_style = window.text_style();
1239    text_style.refine(&TextStyleRefinement {
1240        font_family: Some(theme_settings.ui_font.family.clone()),
1241        font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
1242        font_features: Some(theme_settings.ui_font.features.clone()),
1243        font_size: Some(TextSize::XSmall.rems(cx).into()),
1244        color: Some(colors.text_muted),
1245        ..Default::default()
1246    });
1247
1248    MarkdownStyle {
1249        base_text_style: text_style.clone(),
1250        selection_background_color: colors.element_selection_background,
1251        link: TextStyleRefinement {
1252            background_color: Some(colors.editor_foreground.opacity(0.025)),
1253            underline: Some(UnderlineStyle {
1254                color: Some(colors.text_accent.opacity(0.5)),
1255                thickness: px(1.),
1256                ..Default::default()
1257            }),
1258            ..Default::default()
1259        },
1260        ..Default::default()
1261    }
1262}
1263
1264#[cfg(test)]
1265mod tests {
1266    use super::*;
1267
1268    #[test]
1269    fn parse_http_input_reads_oauth_settings() {
1270        let (id, url, headers, oauth) = parse_http_input(
1271            r#"{
1272  "figma": {
1273    "url": "https://mcp.figma.com/mcp",
1274    "oauth": {
1275      "client_id": "client-id",
1276      "client_secret": "client-secret"
1277    },
1278    "headers": {
1279      "X-Test": "test"
1280    }
1281  }
1282}"#,
1283        )
1284        .unwrap();
1285
1286        assert_eq!(id, ContextServerId("figma".into()));
1287        assert_eq!(url, "https://mcp.figma.com/mcp");
1288        assert_eq!(headers.get("X-Test"), Some(&String::from("test")));
1289        let oauth = oauth.expect("oauth should be present");
1290        assert_eq!(oauth.client_id, "client-id");
1291        assert_eq!(oauth.client_secret.as_deref(), Some("client-secret"));
1292    }
1293
1294    #[test]
1295    fn context_server_http_input_preserves_existing_oauth_settings() {
1296        let text = context_server_http_input(Some((
1297            ContextServerId("figma".into()),
1298            String::from("https://mcp.figma.com/mcp"),
1299            HashMap::default(),
1300            Some(OAuthClientSettings {
1301                client_id: String::from("client-id"),
1302                client_secret: Some(String::from("client-secret")),
1303            }),
1304        )));
1305
1306        let (_, _, _, oauth) = parse_http_input(&text).unwrap();
1307        let oauth = oauth.expect("oauth should be present");
1308        assert_eq!(oauth.client_id, "client-id");
1309        assert_eq!(oauth.client_secret.as_deref(), Some("client-secret"));
1310    }
1311}
1312
Served at tenant.openagents/omega Member data and write actions are omitted.