diff --git a/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs b/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs index 4d338840143fbc..3f300b06d0e0a0 100644 --- a/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs +++ b/crates/agent_ui/src/agent_configuration/configure_context_server_modal.rs @@ -31,7 +31,7 @@ use workspace::{ModalView, Workspace}; use crate::AddContextServer; enum ConfigurationTarget { - New, + New { server_type: ServerType }, Existing { id: ContextServerId, command: ContextServerCommand, @@ -46,6 +46,7 @@ enum ConfigurationTarget { enum ConfigurationSource { New { editor: Entity, + current_template_type: ServerType, }, Existing { editor: Entity, @@ -94,8 +95,9 @@ impl ConfigurationSource { } match target { - ConfigurationTarget::New => ConfigurationSource::New { - editor: create_editor(context_server_input(None), jsonc_language, window, cx), + ConfigurationTarget::New { server_type } => ConfigurationSource::New { + editor: create_editor(context_server_input_template(server_type), jsonc_language, window, cx), + current_template_type: server_type, }, ConfigurationTarget::Existing { id, command } => ConfigurationSource::Existing { editor: create_editor( @@ -140,7 +142,7 @@ impl ConfigurationSource { fn output(&self, cx: &mut App) -> Result<(ContextServerId, ContextServerSettings)> { match self { - ConfigurationSource::New { editor } | ConfigurationSource::Existing { editor } => { + ConfigurationSource::New { editor, .. } | ConfigurationSource::Existing { editor } => { parse_input(&editor.read(cx).text(cx)).map(|(id, command)| { ( id, @@ -211,6 +213,55 @@ fn context_server_input(existing: Option<(ContextServerId, ContextServerCommand) ) } +#[derive(Clone, Copy, PartialEq)] +enum ServerType { + Local, + Remote, +} + +fn context_server_input_template(server_type: ServerType) -> String { + match server_type { + ServerType::Local => { + format!( + r#"{{ + /// The name of your MCP server + "some-mcp-server": {{ + /// The command which runs the MCP server + "command": "", + /// The arguments to pass to the MCP server + "args": [], + /// The environment variables to set + "env": {{}} + }} +}}"# + ) + } + ServerType::Remote => { + format!( + r#"{{ + /// The name of your MCP server + "some-mcp-server": {{ + /// The command which runs the MCP server + "command": "npx", + /// The arguments to pass to the MCP server + "args": [ + "-y", + "mcp-remote", + "https://mcp.atlassian.com/v1/sse", + "--header", + "Authorization:${{AUTH_HEADER}}" // note no spaces around ':' + ], + /// The environment variables to set + "env": {{ + "AUTH_HEADER": "Bearer " // spaces OK in env vars + }} + }} +}}"# + ) + } + } +} + fn resolve_context_server_extension( id: ContextServerId, worktree_store: Entity, @@ -262,13 +313,14 @@ impl ConfigureContextServerModal { _cx: &mut Context, ) { workspace.register_action({ + let language_registry = language_registry.clone(); move |_workspace, _: &AddContextServer, window, cx| { let workspace_handle = cx.weak_entity(); let language_registry = language_registry.clone(); window .spawn(cx, async move |cx| { Self::show_modal( - ConfigurationTarget::New, + ConfigurationTarget::New { server_type: ServerType::Local }, language_registry, workspace_handle, cx, @@ -352,7 +404,7 @@ impl ConfigureContextServerModal { original_server_id: match &target { ConfigurationTarget::Existing { id, .. } => Some(id.clone()), ConfigurationTarget::Extension { id, .. } => Some(id.clone()), - ConfigurationTarget::New => None, + ConfigurationTarget::New { .. } => None, }, source: ConfigurationSource::from_target( target, @@ -418,7 +470,7 @@ impl ConfigureContextServerModal { ProjectSettings::get_global(cx).context_servers.get(&id.0) != Some(&settings); if settings_changed { - // When we write the settings to the file, the context server will be restarted. + // When we write the settings to the file, the context server will be restarted automatically. workspace.update(cx, |workspace, cx| { let fs = workspace.app_state().fs.clone(); let original_server_id = self.original_server_id.clone(); @@ -445,6 +497,26 @@ impl ConfigureContextServerModal { cx.emit(DismissEvent); } + fn switch_template(&mut self, new_type: ServerType, cx: &mut Context) { + if let ConfigurationSource::New { editor, current_template_type } = &mut self.source { + if *current_template_type != new_type { + *current_template_type = new_type; + + // Update the editor content with the new template + let new_template = context_server_input_template(new_type); + editor.update(cx, |editor, cx| { + editor.buffer().update(cx, |buffer, cx| { + let snapshot = buffer.snapshot(cx); + let range = 0..snapshot.len(); + buffer.edit([(range, new_template)], None, cx); + }); + }); + + cx.notify(); + } + } + } + fn show_configured_context_server_toast(&self, id: ContextServerId, cx: &mut App) { self.workspace .update(cx, { @@ -479,7 +551,7 @@ impl ModalView for ConfigureContextServerModal {} impl Focusable for ConfigureContextServerModal { fn focus_handle(&self, cx: &App) -> FocusHandle { match &self.source { - ConfigurationSource::New { editor } => editor.focus_handle(cx), + ConfigurationSource::New { editor, .. } => editor.focus_handle(cx), ConfigurationSource::Existing { editor, .. } => editor.focus_handle(cx), ConfigurationSource::Extension { editor, .. } => editor .as_ref() @@ -524,9 +596,9 @@ impl ConfigureContextServerModal { } } - fn render_modal_content(&self, cx: &App) -> AnyElement { + fn render_modal_content(&self, cx: &mut Context) -> AnyElement { let editor = match &self.source { - ConfigurationSource::New { editor } => editor, + ConfigurationSource::New { editor, .. } => editor, ConfigurationSource::Existing { editor } => editor, ConfigurationSource::Extension { editor, .. } => { let Some(editor) = editor else { @@ -536,37 +608,85 @@ impl ConfigureContextServerModal { } }; - div() - .p_2() - .rounded_md() - .border_1() - .border_color(cx.theme().colors().border_variant) - .bg(cx.theme().colors().editor_background) - .child({ - let settings = ThemeSettings::get_global(cx); - let text_style = TextStyle { - color: cx.theme().colors().text, - font_family: settings.buffer_font.family.clone(), - font_fallbacks: settings.buffer_font.fallbacks.clone(), - font_size: settings.buffer_font_size(cx).into(), - font_weight: settings.buffer_font.weight, - line_height: relative(settings.buffer_line_height.value()), - ..Default::default() - }; - EditorElement::new( - editor, - EditorStyle { - background: cx.theme().colors().editor_background, - local_player: cx.theme().players().local(), - text: text_style, - syntax: cx.theme().syntax().clone(), - ..Default::default() - }, - ) - }) + v_flex() + .gap_2() + .when( + matches!(&self.source, ConfigurationSource::New { .. }), + |content| content.child(self.render_template_switcher(cx)) + ) + .child( + div() + .p_2() + .rounded_md() + .border_1() + .border_color(cx.theme().colors().border_variant) + .bg(cx.theme().colors().editor_background) + .child({ + let settings = ThemeSettings::get_global(cx); + let text_style = TextStyle { + color: cx.theme().colors().text, + font_family: settings.buffer_font.family.clone(), + font_fallbacks: settings.buffer_font.fallbacks.clone(), + font_size: settings.buffer_font_size(cx).into(), + font_weight: settings.buffer_font.weight, + line_height: relative(settings.buffer_line_height.value()), + ..Default::default() + }; + EditorElement::new( + editor, + EditorStyle { + background: cx.theme().colors().editor_background, + local_player: cx.theme().players().local(), + text: text_style, + syntax: cx.theme().syntax().clone(), + ..Default::default() + }, + ) + }) + ) .into_any_element() } + fn render_template_switcher(&self, cx: &mut Context) -> impl IntoElement { + let current_type = if let ConfigurationSource::New { current_template_type, .. } = &self.source { + *current_template_type + } else { + ServerType::Local // fallback, shouldn't happen + }; + + h_flex() + .gap_2() + .child( + Label::new("Template:") + .size(LabelSize::Small) + .color(Color::Muted) + ) + .child( + Button::new("template-local", "Local Server") + .style(if current_type == ServerType::Local { + ButtonStyle::Filled + } else { + ButtonStyle::Subtle + }) + .size(ButtonSize::Compact) + .on_click(cx.listener(|this, _event, _window, cx| { + this.switch_template(ServerType::Local, cx); + })) + ) + .child( + Button::new("template-remote", "Remote Server") + .style(if current_type == ServerType::Remote { + ButtonStyle::Filled + } else { + ButtonStyle::Subtle + }) + .size(ButtonSize::Compact) + .on_click(cx.listener(|this, _event, _window, cx| { + this.switch_template(ServerType::Remote, cx); + })) + ) + } + fn render_modal_footer(&self, window: &mut Window, cx: &mut Context) -> ModalFooter { let focus_handle = self.focus_handle(cx); let is_connecting = matches!(self.state, State::Waiting);