Detailed changes
@@ -71,6 +71,7 @@ dependencies = [
"db",
"editor",
"extension",
+ "extension_host",
"feature_flags",
"file_icons",
"fs",
@@ -127,7 +128,6 @@ dependencies = [
"time",
"time_format",
"ui",
- "ui_input",
"urlencoding",
"util",
"uuid",
@@ -0,0 +1,4 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<rect opacity="0.3" x="2" y="2" width="12" height="12" rx="2" stroke="black" stroke-width="1.5"/>
@@ -0,0 +1,4 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<rect opacity="0.3" x="2" y="2" width="12" height="12" rx="2" stroke="black" stroke-width="1.5"/>
@@ -36,6 +36,7 @@ convert_case.workspace = true
db.workspace = true
editor.workspace = true
extension.workspace = true
+extension_host.workspace = true
feature_flags.workspace = true
file_icons.workspace = true
fs.workspace = true
@@ -90,7 +91,6 @@ thiserror.workspace = true
time.workspace = true
time_format.workspace = true
ui.workspace = true
-ui_input.workspace = true
urlencoding.workspace = true
util.workspace = true
uuid.workspace = true
@@ -46,7 +46,7 @@ use settings::{Settings as _, SettingsStore};
use thread::ThreadId;
pub use crate::active_thread::ActiveThread;
-use crate::agent_configuration::{AddContextServerModal, ManageProfilesModal};
+use crate::agent_configuration::{ConfigureContextServerModal, ManageProfilesModal};
pub use crate::agent_panel::{AgentPanel, ConcreteAssistantPanelDelegate};
pub use crate::context::{ContextLoadResult, LoadedContext};
pub use crate::inline_assistant::InlineAssistant;
@@ -162,7 +162,7 @@ pub fn init(
assistant_slash_command::init(cx);
thread_store::init(cx);
agent_panel::init(cx);
- context_server_configuration::init(language_registry, fs.clone(), cx);
+ context_server_configuration::init(language_registry.clone(), fs.clone(), cx);
register_slash_commands(cx);
inline_assistant::init(
@@ -178,7 +178,10 @@ pub fn init(
cx,
);
indexed_docs::init(cx);
- cx.observe_new(AddContextServerModal::register).detach();
+ cx.observe_new(move |workspace, window, cx| {
+ ConfigureContextServerModal::register(workspace, language_registry.clone(), window, cx)
+ })
+ .detach();
cx.observe_new(ManageProfilesModal::register).detach();
}
@@ -1,4 +1,3 @@
-mod add_context_server_modal;
mod configure_context_server_modal;
mod manage_profiles_modal;
mod tool_picker;
@@ -9,22 +8,29 @@ use agent_settings::AgentSettings;
use assistant_tool::{ToolSource, ToolWorkingSet};
use collections::HashMap;
use context_server::ContextServerId;
+use extension::ExtensionManifest;
+use extension_host::ExtensionStore;
use fs::Fs;
use gpui::{
- Action, Animation, AnimationExt as _, AnyView, App, Entity, EventEmitter, FocusHandle,
- Focusable, ScrollHandle, Subscription, Transformation, percentage,
+ Action, Animation, AnimationExt as _, AnyView, App, Corner, Entity, EventEmitter, FocusHandle,
+ Focusable, ScrollHandle, Subscription, Task, Transformation, WeakEntity, percentage,
};
+use language::LanguageRegistry;
use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
-use project::context_server_store::{ContextServerStatus, ContextServerStore};
+use notifications::status_toast::{StatusToast, ToastIcon};
+use project::{
+ context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
+ project_settings::ProjectSettings,
+};
use settings::{Settings, update_settings_file};
use ui::{
- Disclosure, ElevationIndex, Indicator, Scrollbar, ScrollbarState, Switch, SwitchColor, Tooltip,
- prelude::*,
+ ContextMenu, Disclosure, ElevationIndex, Indicator, PopoverMenu, Scrollbar, ScrollbarState,
+ Switch, SwitchColor, Tooltip, prelude::*,
};
use util::ResultExt as _;
+use workspace::Workspace;
use zed_actions::ExtensionCategoryFilter;
-pub(crate) use add_context_server_modal::AddContextServerModal;
pub(crate) use configure_context_server_modal::ConfigureContextServerModal;
pub(crate) use manage_profiles_modal::ManageProfilesModal;
@@ -32,6 +38,8 @@ use crate::AddContextServer;
pub struct AgentConfiguration {
fs: Arc<dyn Fs>,
+ language_registry: Arc<LanguageRegistry>,
+ workspace: WeakEntity<Workspace>,
focus_handle: FocusHandle,
configuration_views_by_provider: HashMap<LanguageModelProviderId, AnyView>,
context_server_store: Entity<ContextServerStore>,
@@ -48,6 +56,8 @@ impl AgentConfiguration {
fs: Arc<dyn Fs>,
context_server_store: Entity<ContextServerStore>,
tools: Entity<ToolWorkingSet>,
+ language_registry: Arc<LanguageRegistry>,
+ workspace: WeakEntity<Workspace>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
@@ -70,11 +80,16 @@ impl AgentConfiguration {
},
);
+ cx.subscribe(&context_server_store, |_, _, _, cx| cx.notify())
+ .detach();
+
let scroll_handle = ScrollHandle::new();
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
let mut this = Self {
fs,
+ language_registry,
+ workspace,
focus_handle,
configuration_views_by_provider: HashMap::default(),
context_server_store,
@@ -460,9 +475,22 @@ impl AgentConfiguration {
.read(cx)
.status_for_server(&context_server_id)
.unwrap_or(ContextServerStatus::Stopped);
+ let server_configuration = self
+ .context_server_store
+ .read(cx)
+ .configuration_for_server(&context_server_id);
let is_running = matches!(server_status, ContextServerStatus::Running);
let item_id = SharedString::from(context_server_id.0.clone());
+ let is_from_extension = server_configuration
+ .as_ref()
+ .map(|config| {
+ matches!(
+ config.as_ref(),
+ ContextServerConfiguration::Extension { .. }
+ )
+ })
+ .unwrap_or(false);
let error = if let ContextServerStatus::Error(error) = server_status.clone() {
Some(error)
@@ -484,6 +512,18 @@ impl AgentConfiguration {
let border_color = cx.theme().colors().border.opacity(0.6);
+ let (source_icon, source_tooltip) = if is_from_extension {
+ (
+ IconName::ZedMcpExtension,
+ "This MCP server was installed from an extension.",
+ )
+ } else {
+ (
+ IconName::ZedMcpCustom,
+ "This custom MCP server was installed directly.",
+ )
+ };
+
let (status_indicator, tooltip_text) = match server_status {
ContextServerStatus::Starting => (
Icon::new(IconName::LoadCircle)
@@ -511,6 +551,105 @@ impl AgentConfiguration {
),
};
+ let context_server_configuration_menu = PopoverMenu::new("context-server-config-menu")
+ .trigger_with_tooltip(
+ IconButton::new("context-server-config-menu", IconName::Settings)
+ .icon_color(Color::Muted)
+ .icon_size(IconSize::Small),
+ Tooltip::text("Open MCP server options"),
+ )
+ .anchor(Corner::TopRight)
+ .menu({
+ let fs = self.fs.clone();
+ let context_server_id = context_server_id.clone();
+ let language_registry = self.language_registry.clone();
+ let context_server_store = self.context_server_store.clone();
+ let workspace = self.workspace.clone();
+ move |window, cx| {
+ Some(ContextMenu::build(window, cx, |menu, _window, _cx| {
+ menu.entry("Configure Server", None, {
+ let context_server_id = context_server_id.clone();
+ let language_registry = language_registry.clone();
+ let workspace = workspace.clone();
+ move |window, cx| {
+ ConfigureContextServerModal::show_modal_for_existing_server(
+ context_server_id.clone(),
+ language_registry.clone(),
+ workspace.clone(),
+ window,
+ cx,
+ )
+ .detach_and_log_err(cx);
+ }
+ })
+ .separator()
+ .entry("Delete", None, {
+ let fs = fs.clone();
+ let context_server_id = context_server_id.clone();
+ let context_server_store = context_server_store.clone();
+ let workspace = workspace.clone();
+ move |_, cx| {
+ let is_provided_by_extension = context_server_store
+ .read(cx)
+ .configuration_for_server(&context_server_id)
+ .as_ref()
+ .map(|config| {
+ matches!(
+ config.as_ref(),
+ ContextServerConfiguration::Extension { .. }
+ )
+ })
+ .unwrap_or(false);
+
+ let uninstall_extension_task = match (
+ is_provided_by_extension,
+ resolve_extension_for_context_server(&context_server_id, cx),
+ ) {
+ (true, Some((id, manifest))) => {
+ if extension_only_provides_context_server(manifest.as_ref())
+ {
+ ExtensionStore::global(cx).update(cx, |store, cx| {
+ store.uninstall_extension(id, cx)
+ })
+ } else {
+ workspace.update(cx, |workspace, cx| {
+ show_unable_to_uninstall_extension_with_context_server(workspace, context_server_id.clone(), cx);
+ }).log_err();
+ Task::ready(Ok(()))
+ }
+ }
+ _ => Task::ready(Ok(())),
+ };
+
+ cx.spawn({
+ let fs = fs.clone();
+ let context_server_id = context_server_id.clone();
+ async move |cx| {
+ uninstall_extension_task.await?;
+ cx.update(|cx| {
+ update_settings_file::<ProjectSettings>(
+ fs.clone(),
+ cx,
+ {
+ let context_server_id =
+ context_server_id.clone();
+ move |settings, _| {
+ settings
+ .context_servers
+ .remove(&context_server_id.0);
+ }
+ },
+ )
+ })
+ }
+ })
+ .detach_and_log_err(cx);
+ }
+ })
+ }))
+ }
+ });
+
v_flex()
.id(item_id.clone())
.border_1()
@@ -556,7 +695,19 @@ impl AgentConfiguration {
.tooltip(Tooltip::text(tooltip_text))
.child(status_indicator),
)
- .child(Label::new(item_id).ml_0p5().mr_1p5())
+ .child(Label::new(item_id).ml_0p5())
+ .child(
+ div()
+ .id("extension-source")
+ .mt_0p5()
+ .mx_1()
+ .tooltip(Tooltip::text(source_tooltip))
+ .child(
+ Icon::new(source_icon)
+ .size(IconSize::Small)
+ .color(Color::Muted),
+ ),
+ )
.when(is_running, |this| {
this.child(
Label::new(if tool_count == 1 {
@@ -570,28 +721,37 @@ impl AgentConfiguration {
}),
)
.child(
- Switch::new("context-server-switch", is_running.into())
- .color(SwitchColor::Accent)
- .on_click({
- let context_server_manager = self.context_server_store.clone();
- let context_server_id = context_server_id.clone();
- move |state, _window, cx| match state {
- ToggleState::Unselected | ToggleState::Indeterminate => {
- context_server_manager.update(cx, |this, cx| {
- this.stop_server(&context_server_id, cx).log_err();
- });
- }
- ToggleState::Selected => {
- context_server_manager.update(cx, |this, cx| {
- if let Some(server) =
- this.get_server(&context_server_id)
- {
- this.start_server(server, cx);
+ h_flex()
+ .gap_1()
+ .child(context_server_configuration_menu)
+ .child(
+ Switch::new("context-server-switch", is_running.into())
+ .color(SwitchColor::Accent)
+ .on_click({
+ let context_server_manager =
+ self.context_server_store.clone();
+ let context_server_id = context_server_id.clone();
+
+ move |state, _window, cx| match state {
+ ToggleState::Unselected
+ | ToggleState::Indeterminate => {
+ context_server_manager.update(cx, |this, cx| {
+ this.stop_server(&context_server_id, cx)
+ .log_err();
+ });
}
- })
- }
- }
- }),
+ ToggleState::Selected => {
+ context_server_manager.update(cx, |this, cx| {
+ if let Some(server) =
+ this.get_server(&context_server_id)
+ {
+ this.start_server(server, cx);
+ }
+ })
+ }
+ }
+ }),
+ ),
),
)
.map(|parent| {
@@ -701,3 +861,51 @@ impl Render for AgentConfiguration {
)
}
}
+
+fn extension_only_provides_context_server(manifest: &ExtensionManifest) -> bool {
+ manifest.context_servers.len() == 1
+ && manifest.themes.is_empty()
+ && manifest.icon_themes.is_empty()
+ && manifest.languages.is_empty()
+ && manifest.grammars.is_empty()
+ && manifest.language_servers.is_empty()
+ && manifest.slash_commands.is_empty()
+ && manifest.indexed_docs_providers.is_empty()
+ && manifest.snippets.is_none()
+ && manifest.debug_locators.is_empty()
+}
+
+pub(crate) fn resolve_extension_for_context_server(
+ id: &ContextServerId,
+ cx: &App,
+) -> Option<(Arc<str>, Arc<ExtensionManifest>)> {
+ ExtensionStore::global(cx)
+ .read(cx)
+ .installed_extensions()
+ .iter()
+ .find(|(_, entry)| entry.manifest.context_servers.contains_key(&id.0))
+ .map(|(id, entry)| (id.clone(), entry.manifest.clone()))
+}
+
+// This notification appears when trying to delete
+// an MCP server extension that not only provides
+// the server, but other things, too, like language servers and more.
+fn show_unable_to_uninstall_extension_with_context_server(
+ workspace: &mut Workspace,
+ id: ContextServerId,
+ cx: &mut App,
+) {
+ let status_toast = StatusToast::new(
+ format!(
+ "Unable to uninstall the {} extension, as it provides more than just the MCP server.",
+ id.0
+ ),
+ cx,
+ |this, _cx| {
+ this.icon(ToastIcon::new(IconName::Warning).color(Color::Warning))
+ .action("Dismiss", |_, _| {})
+ },
+ );
+
+ workspace.toggle_status_toast(status_toast, cx);
+}
@@ -1,195 +0,0 @@
-use context_server::ContextServerCommand;
-use gpui::{DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, WeakEntity, prelude::*};
-use project::project_settings::{ContextServerSettings, ProjectSettings};
-use settings::update_settings_file;
-use ui::{KeyBinding, Modal, ModalFooter, ModalHeader, Section, Tooltip, prelude::*};
-use ui_input::SingleLineInput;
-use workspace::{ModalView, Workspace};
-
-use crate::AddContextServer;
-
-pub struct AddContextServerModal {
- workspace: WeakEntity<Workspace>,
- name_editor: Entity<SingleLineInput>,
- command_editor: Entity<SingleLineInput>,
-}
-
-impl AddContextServerModal {
- pub fn register(
- workspace: &mut Workspace,
- _window: Option<&mut Window>,
- _cx: &mut Context<Workspace>,
- ) {
- workspace.register_action(|workspace, _: &AddContextServer, window, cx| {
- let workspace_handle = cx.entity().downgrade();
- workspace.toggle_modal(window, cx, |window, cx| {
- Self::new(workspace_handle, window, cx)
- })
- });
- }
-
- pub fn new(
- workspace: WeakEntity<Workspace>,
- window: &mut Window,
- cx: &mut Context<Self>,
- ) -> Self {
- let name_editor =
- cx.new(|cx| SingleLineInput::new(window, cx, "my-custom-server").label("Name"));
- let command_editor = cx.new(|cx| {
- SingleLineInput::new(window, cx, "Command").label("Command to run the MCP server")
- });
-
- Self {
- name_editor,
- command_editor,
- workspace,
- }
- }
-
- fn confirm(&mut self, _: &menu::Confirm, cx: &mut Context<Self>) {
- let name = self
- .name_editor
- .read(cx)
- .editor()
- .read(cx)
- .text(cx)
- .trim()
- .to_string();
- let command = self
- .command_editor
- .read(cx)
- .editor()
- .read(cx)
- .text(cx)
- .trim()
- .to_string();
-
- if name.is_empty() || command.is_empty() {
- return;
- }
-
- let mut command_parts = command.split(' ').map(|part| part.trim().to_string());
- let Some(path) = command_parts.next() else {
- return;
- };
- let args = command_parts.collect::<Vec<_>>();
-
- if let Some(workspace) = self.workspace.upgrade() {
- workspace.update(cx, |workspace, cx| {
- let fs = workspace.app_state().fs.clone();
- update_settings_file::<ProjectSettings>(fs.clone(), cx, |settings, _| {
- settings.context_servers.insert(
- name.into(),
- ContextServerSettings::Custom {
- command: ContextServerCommand {
- path,
- args,
- env: None,
- },
- },
- );
- });
- });
- }
-
- cx.emit(DismissEvent);
- }
-
- fn cancel(&mut self, _: &menu::Cancel, cx: &mut Context<Self>) {
- cx.emit(DismissEvent);
- }
-}
-
-impl ModalView for AddContextServerModal {}
-
-impl Focusable for AddContextServerModal {
- fn focus_handle(&self, cx: &App) -> FocusHandle {
- self.name_editor.focus_handle(cx).clone()
- }
-}
-
-impl EventEmitter<DismissEvent> for AddContextServerModal {}
-
-impl Render for AddContextServerModal {
- fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
- let is_name_empty = self.name_editor.read(cx).is_empty(cx);
- let is_command_empty = self.command_editor.read(cx).is_empty(cx);
-
- let focus_handle = self.focus_handle(cx);
-
- div()
- .elevation_3(cx)
- .w(rems(34.))
- .key_context("AddContextServerModal")
- .on_action(
- cx.listener(|this, _: &menu::Cancel, _window, cx| this.cancel(&menu::Cancel, cx)),
- )
- .on_action(
- cx.listener(|this, _: &menu::Confirm, _window, cx| {
- this.confirm(&menu::Confirm, cx)
- }),
- )
- .capture_any_mouse_down(cx.listener(|this, _, window, cx| {
- this.focus_handle(cx).focus(window);
- }))
- .on_mouse_down_out(cx.listener(|_this, _, _, cx| cx.emit(DismissEvent)))
- .child(
- Modal::new("add-context-server", None)
- .header(ModalHeader::new().headline("Add MCP Server"))
- .section(
- Section::new().child(
- v_flex()
- .gap_2()
- .child(self.name_editor.clone())
- .child(self.command_editor.clone()),
- ),
- )
- .footer(
- ModalFooter::new().end_slot(
- h_flex()
- .gap_2()
- .child(
- Button::new("cancel", "Cancel")
- .key_binding(
- KeyBinding::for_action_in(
- &menu::Cancel,
- &focus_handle,
- window,
- cx,
- )
- .map(|kb| kb.size(rems_from_px(12.))),
- )
- .on_click(cx.listener(|this, _event, _window, cx| {
- this.cancel(&menu::Cancel, cx)
- })),
- )
- .child(
- Button::new("add-server", "Add Server")
- .disabled(is_name_empty || is_command_empty)
- .key_binding(
- KeyBinding::for_action_in(
- &menu::Confirm,
- &focus_handle,
- window,
- cx,
- )
- .map(|kb| kb.size(rems_from_px(12.))),
- )
- .map(|button| {
- if is_name_empty {
- button.tooltip(Tooltip::text("Name is required"))
- } else if is_command_empty {
- button.tooltip(Tooltip::text("Command is required"))
- } else {
- button
- }
- })
- .on_click(cx.listener(|this, _event, _window, cx| {
- this.confirm(&menu::Confirm, cx)
- })),
- ),
- ),
- ),
- )
- }
-}
@@ -3,215 +3,384 @@ use std::{
time::Duration,
};
-use anyhow::Context as _;
-use context_server::ContextServerId;
+use anyhow::{Context as _, Result};
+use context_server::{ContextServerCommand, ContextServerId};
use editor::{Editor, EditorElement, EditorStyle};
use gpui::{
- Animation, AnimationExt, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Task,
- TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, percentage,
+ Animation, AnimationExt as _, AsyncWindowContext, DismissEvent, Entity, EventEmitter,
+ FocusHandle, Focusable, Task, TextStyle, TextStyleRefinement, Transformation, UnderlineStyle,
+ WeakEntity, percentage, prelude::*,
};
use language::{Language, LanguageRegistry};
use markdown::{Markdown, MarkdownElement, MarkdownStyle};
use notifications::status_toast::{StatusToast, ToastIcon};
use project::{
- context_server_store::{ContextServerStatus, ContextServerStore},
+ context_server_store::{
+ ContextServerStatus, ContextServerStore, registry::ContextServerDescriptorRegistry,
+ },
project_settings::{ContextServerSettings, ProjectSettings},
+ worktree_store::WorktreeStore,
};
use settings::{Settings as _, update_settings_file};
use theme::ThemeSettings;
use ui::{KeyBinding, Modal, ModalFooter, ModalHeader, Section, Tooltip, prelude::*};
-use util::ResultExt;
+use util::ResultExt as _;
use workspace::{ModalView, Workspace};
-pub(crate) struct ConfigureContextServerModal {
- workspace: WeakEntity<Workspace>,
- focus_handle: FocusHandle,
- context_servers_to_setup: Vec<ContextServerSetup>,
- context_server_store: Entity<ContextServerStore>,
+use crate::AddContextServer;
+
+enum ConfigurationTarget {
+ New,
+ Existing {
+ id: ContextServerId,
+ command: ContextServerCommand,
+ },
+ Extension {
+ id: ContextServerId,
+ repository_url: Option<SharedString>,
+ installation: Option<extension::ContextServerConfiguration>,
+ },
}
-enum Configuration {
- NotAvailable,
- Required(ConfigurationRequiredState),
+enum ConfigurationSource {
+ New {
+ editor: Entity<Editor>,
+ },
+ Existing {
+ editor: Entity<Editor>,
+ },
+ Extension {
+ id: ContextServerId,
+ editor: Option<Entity<Editor>>,
+ repository_url: Option<SharedString>,
+ installation_instructions: Option<Entity<markdown::Markdown>>,
+ settings_validator: Option<jsonschema::Validator>,
+ },
}
-struct ConfigurationRequiredState {
- installation_instructions: Entity<markdown::Markdown>,
- settings_validator: Option<jsonschema::Validator>,
- settings_editor: Entity<Editor>,
- last_error: Option<SharedString>,
- waiting_for_context_server: bool,
-}
+impl ConfigurationSource {
+ fn has_configuration_options(&self) -> bool {
+ !matches!(self, ConfigurationSource::Extension { editor: None, .. })
+ }
-struct ContextServerSetup {
- id: ContextServerId,
- repository_url: Option<SharedString>,
- configuration: Configuration,
-}
+ fn is_new(&self) -> bool {
+ matches!(self, ConfigurationSource::New { .. })
+ }
-impl ConfigureContextServerModal {
- pub fn new(
- configurations: impl Iterator<Item = crate::context_server_configuration::Configuration>,
- context_server_store: Entity<ContextServerStore>,
- jsonc_language: Option<Arc<Language>>,
+ fn from_target(
+ target: ConfigurationTarget,
language_registry: Arc<LanguageRegistry>,
- workspace: WeakEntity<Workspace>,
+ jsonc_language: Option<Arc<Language>>,
window: &mut Window,
- cx: &mut Context<Self>,
+ cx: &mut App,
) -> Self {
- let context_servers_to_setup = configurations
- .map(|config| match config {
- crate::context_server_configuration::Configuration::NotAvailable(
- context_server_id,
- repository_url,
- ) => ContextServerSetup {
- id: context_server_id,
- repository_url,
- configuration: Configuration::NotAvailable,
- },
- crate::context_server_configuration::Configuration::Required(
- context_server_id,
- repository_url,
- config,
- ) => {
- let jsonc_language = jsonc_language.clone();
- let settings_validator = jsonschema::validator_for(&config.settings_schema)
- .context("Failed to load JSON schema for context server settings")
- .log_err();
- let state = ConfigurationRequiredState {
- installation_instructions: cx.new(|cx| {
- Markdown::new(
- config.installation_instructions.clone().into(),
- Some(language_registry.clone()),
- None,
- cx,
- )
- }),
- settings_validator,
- settings_editor: cx.new(|cx| {
- let mut editor = Editor::auto_height(1, 16, window, cx);
- editor.set_text(config.default_settings.trim(), window, cx);
- editor.set_show_gutter(false, cx);
- editor.set_soft_wrap_mode(
- language::language_settings::SoftWrap::None,
- cx,
- );
- if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
- buffer.update(cx, |buffer, cx| {
- buffer.set_language(jsonc_language, cx)
- })
- }
- editor
- }),
- waiting_for_context_server: false,
- last_error: None,
- };
- ContextServerSetup {
- id: context_server_id,
- repository_url,
- configuration: Configuration::Required(state),
- }
+ fn create_editor(
+ json: String,
+ jsonc_language: Option<Arc<Language>>,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> Entity<Editor> {
+ cx.new(|cx| {
+ let mut editor = Editor::auto_height(4, 16, window, cx);
+ editor.set_text(json, window, cx);
+ editor.set_show_gutter(false, cx);
+ editor.set_soft_wrap_mode(language::language_settings::SoftWrap::None, cx);
+ if let Some(buffer) = editor.buffer().read(cx).as_singleton() {
+ buffer.update(cx, |buffer, cx| buffer.set_language(jsonc_language, cx))
}
+ editor
})
- .collect::<Vec<_>>();
+ }
+
+ match target {
+ ConfigurationTarget::New => ConfigurationSource::New {
+ editor: create_editor(context_server_input(None), jsonc_language, window, cx),
+ },
+ ConfigurationTarget::Existing { id, command } => ConfigurationSource::Existing {
+ editor: create_editor(
+ context_server_input(Some((id, command))),
+ jsonc_language,
+ window,
+ cx,
+ ),
+ },
+ ConfigurationTarget::Extension {
+ id,
+ repository_url,
+ installation,
+ } => {
+ let settings_validator = installation.as_ref().and_then(|installation| {
+ jsonschema::validator_for(&installation.settings_schema)
+ .context("Failed to load JSON schema for context server settings")
+ .log_err()
+ });
+ let installation_instructions = installation.as_ref().map(|installation| {
+ cx.new(|cx| {
+ Markdown::new(
+ installation.installation_instructions.clone().into(),
+ Some(language_registry.clone()),
+ None,
+ cx,
+ )
+ })
+ });
+ ConfigurationSource::Extension {
+ id,
+ repository_url,
+ installation_instructions,
+ settings_validator,
+ editor: installation.map(|installation| {
+ create_editor(installation.default_settings, jsonc_language, window, cx)
+ }),
+ }
+ }
+ }
+ }
- Self {
- workspace,
- focus_handle: cx.focus_handle(),
- context_servers_to_setup,
- context_server_store,
+ fn output(&self, cx: &mut App) -> Result<(ContextServerId, ContextServerSettings)> {
+ match self {
+ ConfigurationSource::New { editor } | ConfigurationSource::Existing { editor } => {
+ parse_input(&editor.read(cx).text(cx))
+ .map(|(id, command)| (id, ContextServerSettings::Custom { command }))
+ }
+ ConfigurationSource::Extension {
+ id,
+ editor,
+ settings_validator,
+ ..
+ } => {
+ let text = editor
+ .as_ref()
+ .context("No output available")?
+ .read(cx)
+ .text(cx);
+ let settings = serde_json_lenient::from_str::<serde_json::Value>(&text)?;
+ if let Some(settings_validator) = settings_validator {
+ if let Err(error) = settings_validator.validate(&settings) {
+ return Err(anyhow::anyhow!(error.to_string()));
+ }
+ }
+ Ok((id.clone(), ContextServerSettings::Extension { settings }))
+ }
}
}
}
-impl ConfigureContextServerModal {
- pub fn confirm(&mut self, cx: &mut Context<Self>) {
- if self.context_servers_to_setup.is_empty() {
- self.dismiss(cx);
- return;
+fn context_server_input(existing: Option<(ContextServerId, ContextServerCommand)>) -> String {
+ let (name, path, args, env) = match existing {
+ Some((id, cmd)) => {
+ let args = serde_json::to_string(&cmd.args).unwrap();
+ let env = serde_json::to_string(&cmd.env.unwrap_or_default()).unwrap();
+ (id.0.to_string(), cmd.path, args, env)
}
+ None => (
+ "some-mcp-server".to_string(),
+ "".to_string(),
+ "[]".to_string(),
+ "{}".to_string(),
+ ),
+ };
+
+ format!(
+ r#"{{
+ /// The name of your MCP server
+ "{name}": {{
+ "command": {{
+ /// The path to the executable
+ "path": "{path}",
+ /// The arguments to pass to the executable
+ "args": {args},
+ /// The environment variables to set for the executable
+ "env": {env}
+ }}
+ }}
+}}"#
+ )
+}
- let Some(workspace) = self.workspace.upgrade() else {
- return;
- };
+fn resolve_context_server_extension(
+ id: ContextServerId,
+ worktree_store: Entity<WorktreeStore>,
+ cx: &mut App,
+) -> Task<Option<ConfigurationTarget>> {
+ let registry = ContextServerDescriptorRegistry::default_global(cx).read(cx);
+
+ let Some(descriptor) = registry.context_server_descriptor(&id.0) else {
+ return Task::ready(None);
+ };
+
+ let extension = crate::agent_configuration::resolve_extension_for_context_server(&id, cx);
+ cx.spawn(async move |cx| {
+ let installation = descriptor
+ .configuration(worktree_store, cx)
+ .await
+ .context("Failed to resolve context server configuration")
+ .log_err()
+ .flatten();
+
+ Some(ConfigurationTarget::Extension {
+ id,
+ repository_url: extension
+ .and_then(|(_, manifest)| manifest.repository.clone().map(SharedString::from)),
+ installation,
+ })
+ })
+}
- let id = self.context_servers_to_setup[0].id.clone();
- let configuration = match &mut self.context_servers_to_setup[0].configuration {
- Configuration::NotAvailable => {
- self.context_servers_to_setup.remove(0);
- if self.context_servers_to_setup.is_empty() {
- self.dismiss(cx);
- }
- return;
- }
- Configuration::Required(state) => state,
- };
+enum State {
+ Idle,
+ Waiting,
+ Error(SharedString),
+}
- configuration.last_error.take();
- if configuration.waiting_for_context_server {
- return;
- }
+pub struct ConfigureContextServerModal {
+ context_server_store: Entity<ContextServerStore>,
+ workspace: WeakEntity<Workspace>,
+ source: ConfigurationSource,
+ state: State,
+}
- let settings_value = match serde_json_lenient::from_str::<serde_json::Value>(
- &configuration.settings_editor.read(cx).text(cx),
- ) {
- Ok(value) => value,
- Err(error) => {
- configuration.last_error = Some(error.to_string().into());
- cx.notify();
- return;
+impl ConfigureContextServerModal {
+ pub fn register(
+ workspace: &mut Workspace,
+ language_registry: Arc<LanguageRegistry>,
+ _window: Option<&mut Window>,
+ _cx: &mut Context<Workspace>,
+ ) {
+ 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,
+ language_registry,
+ workspace_handle,
+ cx,
+ )
+ .await
+ })
+ .detach_and_log_err(cx);
}
+ });
+ }
+
+ pub fn show_modal_for_existing_server(
+ server_id: ContextServerId,
+ language_registry: Arc<LanguageRegistry>,
+ workspace: WeakEntity<Workspace>,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> Task<Result<()>> {
+ let Some(settings) = ProjectSettings::get_global(cx)
+ .context_servers
+ .get(&server_id.0)
+ .cloned()
+ .or_else(|| {
+ ContextServerDescriptorRegistry::default_global(cx)
+ .read(cx)
+ .context_server_descriptor(&server_id.0)
+ .map(|_| ContextServerSettings::Extension {
+ settings: serde_json::json!({}),
+ })
+ })
+ else {
+ return Task::ready(Err(anyhow::anyhow!("Context server not found")));
};
- if let Some(validator) = configuration.settings_validator.as_ref() {
- if let Err(error) = validator.validate(&settings_value) {
- configuration.last_error = Some(error.to_string().into());
- cx.notify();
- return;
+ window.spawn(cx, async move |cx| {
+ let target = match settings {
+ ContextServerSettings::Custom { command } => Some(ConfigurationTarget::Existing {
+ id: server_id,
+ command,
+ }),
+ ContextServerSettings::Extension { .. } => {
+ match workspace
+ .update(cx, |workspace, cx| {
+ resolve_context_server_extension(
+ server_id,
+ workspace.project().read(cx).worktree_store(),
+ cx,
+ )
+ })
+ .ok()
+ {
+ Some(task) => task.await,
+ None => None,
+ }
+ }
+ };
+
+ match target {
+ Some(target) => Self::show_modal(target, language_registry, workspace, cx).await,
+ None => Err(anyhow::anyhow!("Failed to resolve context server")),
}
- }
- let id = id.clone();
+ })
+ }
- let settings_changed = ProjectSettings::get_global(cx)
- .context_servers
- .get(&id.0)
- .map_or(true, |settings| match settings {
- ContextServerSettings::Custom { .. } => false,
- ContextServerSettings::Extension { settings } => settings != &settings_value,
- });
+ fn show_modal(
+ target: ConfigurationTarget,
+ language_registry: Arc<LanguageRegistry>,
+ workspace: WeakEntity<Workspace>,
+ cx: &mut AsyncWindowContext,
+ ) -> Task<Result<()>> {
+ cx.spawn(async move |cx| {
+ let jsonc_language = language_registry.language_for_name("jsonc").await.ok();
+ workspace.update_in(cx, |workspace, window, cx| {
+ let workspace_handle = cx.weak_entity();
+ let context_server_store = workspace.project().read(cx).context_server_store();
+ workspace.toggle_modal(window, cx, |window, cx| Self {
+ context_server_store,
+ workspace: workspace_handle,
+ state: State::Idle,
+ source: ConfigurationSource::from_target(
+ target,
+ language_registry,
+ jsonc_language,
+ window,
+ cx,
+ ),
+ })
+ })
+ })
+ }
- let is_running = self.context_server_store.read(cx).status_for_server(&id)
- == Some(ContextServerStatus::Running);
+ fn set_error(&mut self, err: impl Into<SharedString>, cx: &mut Context<Self>) {
+ self.state = State::Error(err.into());
+ cx.notify();
+ }
- if !settings_changed && is_running {
- self.complete_setup(id, cx);
+ fn confirm(&mut self, _: &menu::Confirm, cx: &mut Context<Self>) {
+ self.state = State::Idle;
+ let Some(workspace) = self.workspace.upgrade() else {
return;
- }
+ };
- configuration.waiting_for_context_server = true;
+ let (id, settings) = match self.source.output(cx) {
+ Ok(val) => val,
+ Err(error) => {
+ self.set_error(error.to_string(), cx);
+ return;
+ }
+ };
- let task = wait_for_context_server(&self.context_server_store, id.clone(), cx);
+ self.state = State::Waiting;
+ let wait_for_context_server_task =
+ wait_for_context_server(&self.context_server_store, id.clone(), cx);
cx.spawn({
let id = id.clone();
async move |this, cx| {
- let result = task.await;
+ let result = wait_for_context_server_task.await;
this.update(cx, |this, cx| match result {
Ok(_) => {
- this.complete_setup(id, cx);
+ this.state = State::Idle;
+ this.show_configured_context_server_toast(id, cx);
+ cx.emit(DismissEvent);
}
Err(err) => {
- if let Some(setup) = this.context_servers_to_setup.get_mut(0) {
- match &mut setup.configuration {
- Configuration::NotAvailable => {}
- Configuration::Required(state) => {
- state.last_error = Some(err.into());
- state.waiting_for_context_server = false;
- }
- }
- } else {
- this.dismiss(cx);
- }
- cx.notify();
+ this.set_error(err, cx);
}
})
}
@@ -219,32 +388,24 @@ impl ConfigureContextServerModal {
.detach();
// When we write the settings to the file, the context server will be restarted.
- update_settings_file::<ProjectSettings>(workspace.read(cx).app_state().fs.clone(), cx, {
- let id = id.clone();
- |settings, _| {
- settings.context_servers.insert(
- id.0,
- ContextServerSettings::Extension {
- settings: settings_value,
- },
- );
- }
+ workspace.update(cx, |workspace, cx| {
+ let fs = workspace.app_state().fs.clone();
+ update_settings_file::<ProjectSettings>(fs.clone(), cx, |project_settings, _| {
+ project_settings.context_servers.insert(id.0, settings);
+ });
});
}
- fn complete_setup(&mut self, id: ContextServerId, cx: &mut Context<Self>) {
- self.context_servers_to_setup.remove(0);
- cx.notify();
-
- if !self.context_servers_to_setup.is_empty() {
- return;
- }
+ fn cancel(&mut self, _: &menu::Cancel, cx: &mut Context<Self>) {
+ cx.emit(DismissEvent);
+ }
+ fn show_configured_context_server_toast(&self, id: ContextServerId, cx: &mut App) {
self.workspace
.update(cx, {
|workspace, cx| {
let status_toast = StatusToast::new(
- format!("{} configured successfully.", id),
+ format!("{} configured successfully.", id.0),
cx,
|this, _cx| {
this.icon(ToastIcon::new(IconName::Hammer).color(Color::Muted))
@@ -256,12 +417,264 @@ impl ConfigureContextServerModal {
}
})
.log_err();
+ }
+}
+
+fn parse_input(text: &str) -> Result<(ContextServerId, ContextServerCommand)> {
+ let value: serde_json::Value = serde_json_lenient::from_str(text)?;
+ let object = value.as_object().context("Expected object")?;
+ anyhow::ensure!(object.len() == 1, "Expected exactly one key-value pair");
+ let (context_server_name, value) = object.into_iter().next().unwrap();
+ let command = value.get("command").context("Expected command")?;
+ let command: ContextServerCommand = serde_json::from_value(command.clone())?;
+ Ok((ContextServerId(context_server_name.clone().into()), command))
+}
+
+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::Existing { editor, .. } => editor.focus_handle(cx),
+ ConfigurationSource::Extension { editor, .. } => editor
+ .as_ref()
+ .map(|editor| editor.focus_handle(cx))
+ .unwrap_or_else(|| cx.focus_handle()),
+ }
+ }
+}
- self.dismiss(cx);
+impl EventEmitter<DismissEvent> for ConfigureContextServerModal {}
+
+impl ConfigureContextServerModal {
+ fn render_modal_header(&self) -> ModalHeader {
+ let text: SharedString = match &self.source {
+ ConfigurationSource::New { .. } => "Add MCP Server".into(),
+ ConfigurationSource::Existing { .. } => "Configure MCP Server".into(),
+ ConfigurationSource::Extension { id, .. } => format!("Configure {}", id.0).into(),
+ };
+ ModalHeader::new().headline(text)
}
- fn dismiss(&self, cx: &mut Context<Self>) {
- cx.emit(DismissEvent);
+ fn render_modal_description(&self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
+ const MODAL_DESCRIPTION: &'static str = "Visit the MCP server configuration docs to find all necessary arguments and environment variables.";
+
+ if let ConfigurationSource::Extension {
+ installation_instructions: Some(installation_instructions),
+ ..
+ } = &self.source
+ {
+ div()
+ .pb_2()
+ .text_sm()
+ .child(MarkdownElement::new(
+ installation_instructions.clone(),
+ default_markdown_style(window, cx),
+ ))
+ .into_any_element()
+ } else {
+ Label::new(MODAL_DESCRIPTION)
+ .color(Color::Muted)
+ .into_any_element()
+ }
+ }
+
+ fn render_modal_content(&self, cx: &App) -> AnyElement {
+ let editor = match &self.source {
+ ConfigurationSource::New { editor } => editor,
+ ConfigurationSource::Existing { editor } => editor,
+ ConfigurationSource::Extension { editor, .. } => {
+ let Some(editor) = editor else {
+ return Label::new(
+ "No configuration options available for this context server. Visit the Repository for any further instructions.",
+ )
+ .color(Color::Muted).into_any_element();
+ };
+ editor
+ }
+ };
+
+ 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_modal_footer(&self, window: &mut Window, cx: &mut Context<Self>) -> ModalFooter {
+ let focus_handle = self.focus_handle(cx);
+ let is_connecting = matches!(self.state, State::Waiting);
+
+ ModalFooter::new()
+ .start_slot::<Button>(
+ if let ConfigurationSource::Extension {
+ repository_url: Some(repository_url),
+ ..
+ } = &self.source
+ {
+ Some(
+ Button::new("open-repository", "Open Repository")
+ .icon(IconName::ArrowUpRight)
+ .icon_color(Color::Muted)
+ .icon_size(IconSize::XSmall)
+ .tooltip({
+ let repository_url = repository_url.clone();
+ move |window, cx| {
+ Tooltip::with_meta(
+ "Open Repository",
+ None,
+ repository_url.clone(),
+ window,
+ cx,
+ )
+ }
+ })
+ .on_click({
+ let repository_url = repository_url.clone();
+ move |_, _, cx| cx.open_url(&repository_url)
+ }),
+ )
+ } else {
+ None
+ },
+ )
+ .end_slot(
+ h_flex()
+ .gap_2()
+ .child(
+ Button::new(
+ "cancel",
+ if self.source.has_configuration_options() {
+ "Cancel"
+ } else {
+ "Dismiss"
+ },
+ )
+ .key_binding(
+ KeyBinding::for_action_in(&menu::Cancel, &focus_handle, window, cx)
+ .map(|kb| kb.size(rems_from_px(12.))),
+ )
+ .on_click(
+ cx.listener(|this, _event, _window, cx| this.cancel(&menu::Cancel, cx)),
+ ),
+ )
+ .children(self.source.has_configuration_options().then(|| {
+ Button::new(
+ "add-server",
+ if self.source.is_new() {
+ "Add Server"
+ } else {
+ "Configure Server"
+ },
+ )
+ .disabled(is_connecting)
+ .key_binding(
+ KeyBinding::for_action_in(&menu::Confirm, &focus_handle, window, cx)
+ .map(|kb| kb.size(rems_from_px(12.))),
+ )
+ .on_click(
+ cx.listener(|this, _event, _window, cx| {
+ this.confirm(&menu::Confirm, cx)
+ }),
+ )
+ })),
+ )
+ }
+
+ fn render_waiting_for_context_server() -> Div {
+ h_flex()
+ .gap_2()
+ .child(
+ Icon::new(IconName::ArrowCircle)
+ .size(IconSize::XSmall)
+ .color(Color::Info)
+ .with_animation(
+ "arrow-circle",
+ Animation::new(Duration::from_secs(2)).repeat(),
+ |icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
+ )
+ .into_any_element(),
+ )
+ .child(
+ Label::new("Waiting for Context Server")
+ .size(LabelSize::Small)
+ .color(Color::Muted),
+ )
+ }
+
+ fn render_modal_error(error: SharedString) -> Div {
+ h_flex()
+ .gap_2()
+ .child(
+ Icon::new(IconName::Warning)
+ .size(IconSize::XSmall)
+ .color(Color::Warning),
+ )
+ .child(
+ div()
+ .w_full()
+ .child(Label::new(error).size(LabelSize::Small).color(Color::Muted)),
+ )
+ }
+}
+
+impl Render for ConfigureContextServerModal {
+ fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ div()
+ .elevation_3(cx)
+ .w(rems(34.))
+ .key_context("ConfigureContextServerModal")
+ .on_action(
+ cx.listener(|this, _: &menu::Cancel, _window, cx| this.cancel(&menu::Cancel, cx)),
+ )
+ .on_action(
+ cx.listener(|this, _: &menu::Confirm, _window, cx| {
+ this.confirm(&menu::Confirm, cx)
+ }),
+ )
+ .capture_any_mouse_down(cx.listener(|this, _, window, cx| {
+ this.focus_handle(cx).focus(window);
+ }))
+ .child(
+ Modal::new("configure-context-server", None)
+ .header(self.render_modal_header())
+ .section(
+ Section::new()
+ .child(self.render_modal_description(window, cx))
+ .child(self.render_modal_content(cx))
+ .child(match &self.state {
+ State::Idle => div(),
+ State::Waiting => Self::render_waiting_for_context_server(),
+ State::Error(error) => Self::render_modal_error(error.clone()),
+ }),
+ )
+ .footer(self.render_modal_footer(window, cx)),
+ )
}
}
@@ -309,199 +722,6 @@ fn wait_for_context_server(
})
}
-impl Render for ConfigureContextServerModal {
- fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
- let Some(setup) = self.context_servers_to_setup.first() else {
- return div().into_any_element();
- };
-
- let focus_handle = self.focus_handle(cx);
-
- div()
- .elevation_3(cx)
- .w(rems(42.))
- .key_context("ConfigureContextServerModal")
- .track_focus(&focus_handle)
- .on_action(cx.listener(|this, _: &menu::Confirm, _window, cx| this.confirm(cx)))
- .on_action(cx.listener(|this, _: &menu::Cancel, _window, cx| this.dismiss(cx)))
- .capture_any_mouse_down(cx.listener(|this, _, window, cx| {
- this.focus_handle(cx).focus(window);
- }))
- .child(
- Modal::new("configure-context-server", None)
- .header(ModalHeader::new().headline(format!("Configure {}", setup.id)))
- .section(match &setup.configuration {
- Configuration::NotAvailable => Section::new().child(
- Label::new(
- "No configuration options available for this context server. Visit the Repository for any further instructions.",
- )
- .color(Color::Muted),
- ),
- Configuration::Required(configuration) => Section::new()
- .child(div().pb_2().text_sm().child(MarkdownElement::new(
- configuration.installation_instructions.clone(),
- default_markdown_style(window, cx),
- )))
- .child(
- div()
- .p_2()
- .rounded_md()
- .border_1()
- .border_color(cx.theme().colors().border_variant)
- .bg(cx.theme().colors().editor_background)
- .gap_1()
- .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(
- &configuration.settings_editor,
- EditorStyle {
- background: cx.theme().colors().editor_background,
- local_player: cx.theme().players().local(),
- text: text_style,
- syntax: cx.theme().syntax().clone(),
- ..Default::default()
- },
- )
- })
- .when_some(configuration.last_error.clone(), |this, error| {
- this.child(
- h_flex()
- .gap_2()
- .px_2()
- .py_1()
- .child(
- Icon::new(IconName::Warning)
- .size(IconSize::XSmall)
- .color(Color::Warning),
- )
- .child(
- div().w_full().child(
- Label::new(error)
- .size(LabelSize::Small)
- .color(Color::Muted),
- ),
- ),
- )
- }),
- )
- .when(configuration.waiting_for_context_server, |this| {
- this.child(
- h_flex()
- .gap_1p5()
- .child(
- Icon::new(IconName::ArrowCircle)
- .size(IconSize::XSmall)
- .color(Color::Info)
- .with_animation(
- "arrow-circle",
- Animation::new(Duration::from_secs(2)).repeat(),
- |icon, delta| {
- icon.transform(Transformation::rotate(
- percentage(delta),
- ))
- },
- )
- .into_any_element(),
- )
- .child(
- Label::new("Waiting for Context Server")
- .size(LabelSize::Small)
- .color(Color::Muted),
- ),
- )
- }),
- })
- .footer(
- ModalFooter::new()
- .when_some(setup.repository_url.clone(), |this, repository_url| {
- this.start_slot(
- h_flex().w_full().child(
- Button::new("open-repository", "Open Repository")
- .icon(IconName::ArrowUpRight)
- .icon_color(Color::Muted)
- .icon_size(IconSize::XSmall)
- .tooltip({
- let repository_url = repository_url.clone();
- move |window, cx| {
- Tooltip::with_meta(
- "Open Repository",
- None,
- repository_url.clone(),
- window,
- cx,
- )
- }
- })
- .on_click(move |_, _, cx| cx.open_url(&repository_url)),
- ),
- )
- })
- .end_slot(match &setup.configuration {
- Configuration::NotAvailable => Button::new("dismiss", "Dismiss")
- .key_binding(
- KeyBinding::for_action_in(
- &menu::Cancel,
- &focus_handle,
- window,
- cx,
- )
- .map(|kb| kb.size(rems_from_px(12.))),
- )
- .on_click(
- cx.listener(|this, _event, _window, cx| this.dismiss(cx)),
- )
- .into_any_element(),
- Configuration::Required(state) => h_flex()
- .gap_2()
- .child(
- Button::new("cancel", "Cancel")
- .key_binding(
- KeyBinding::for_action_in(
- &menu::Cancel,
- &focus_handle,
- window,
- cx,
- )
- .map(|kb| kb.size(rems_from_px(12.))),
- )
- .on_click(cx.listener(|this, _event, _window, cx| {
- this.dismiss(cx)
- })),
- )
- .child(
- Button::new("configure-server", "Configure MCP")
- .disabled(state.waiting_for_context_server)
- .key_binding(
- KeyBinding::for_action_in(
- &menu::Confirm,
- &focus_handle,
- window,
- cx,
- )
- .map(|kb| kb.size(rems_from_px(12.))),
- )
- .on_click(cx.listener(|this, _event, _window, cx| {
- this.confirm(cx)
- })),
- )
- .into_any_element(),
- }),
- ),
- ).into_any_element()
- }
-}
-
pub(crate) fn default_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
let theme_settings = ThemeSettings::get_global(cx);
let colors = cx.theme().colors();
@@ -1184,8 +1184,17 @@ impl AgentPanel {
let fs = self.fs.clone();
self.set_active_view(ActiveView::Configuration, window, cx);
- self.configuration =
- Some(cx.new(|cx| AgentConfiguration::new(fs, context_server_store, tools, window, cx)));
+ self.configuration = Some(cx.new(|cx| {
+ AgentConfiguration::new(
+ fs,
+ context_server_store,
+ tools,
+ self.language_registry.clone(),
+ self.workspace.clone(),
+ window,
+ cx,
+ )
+ }));
if let Some(configuration) = self.configuration.as_ref() {
self.configuration_subscription = Some(cx.subscribe_in(
@@ -1,15 +1,11 @@
use std::sync::Arc;
-use anyhow::Context as _;
use context_server::ContextServerId;
-use extension::{ContextServerConfiguration, ExtensionManifest};
+use extension::ExtensionManifest;
use fs::Fs;
-use gpui::Task;
+use gpui::WeakEntity;
use language::LanguageRegistry;
-use project::{
- context_server_store::registry::ContextServerDescriptorRegistry,
- project_settings::ProjectSettings,
-};
+use project::project_settings::ProjectSettings;
use settings::update_settings_file;
use ui::prelude::*;
use util::ResultExt;
@@ -27,12 +23,12 @@ pub(crate) fn init(language_registry: Arc<LanguageRegistry>, fs: Arc<dyn Fs>, cx
cx.subscribe_in(extension_events, window, {
let language_registry = language_registry.clone();
let fs = fs.clone();
- move |workspace, _, event, window, cx| match event {
+ move |_, _, event, window, cx| match event {
extension::Event::ExtensionInstalled(manifest) => {
show_configure_mcp_modal(
language_registry.clone(),
manifest,
- workspace,
+ cx.weak_entity(),
window,
cx,
);
@@ -49,7 +45,7 @@ pub(crate) fn init(language_registry: Arc<LanguageRegistry>, fs: Arc<dyn Fs>, cx
show_configure_mcp_modal(
language_registry.clone(),
manifest,
- workspace,
+ cx.weak_entity(),
window,
cx,
);
@@ -80,19 +76,10 @@ fn remove_context_server_settings(
});
}
-pub enum Configuration {
- NotAvailable(ContextServerId, Option<SharedString>),
- Required(
- ContextServerId,
- Option<SharedString>,
- ContextServerConfiguration,
- ),
-}
-
fn show_configure_mcp_modal(
language_registry: Arc<LanguageRegistry>,
manifest: &Arc<ExtensionManifest>,
- workspace: &mut Workspace,
+ workspace: WeakEntity<Workspace>,
window: &mut Window,
cx: &mut Context<'_, Workspace>,
) {
@@ -100,70 +87,30 @@ fn show_configure_mcp_modal(
return;
}
- let context_server_store = workspace.project().read(cx).context_server_store();
- let repository: Option<SharedString> = manifest.repository.as_ref().map(|s| s.clone().into());
+ let ids = manifest.context_servers.keys().cloned().collect::<Vec<_>>();
+ if ids.is_empty() {
+ return;
+ }
- let registry = ContextServerDescriptorRegistry::default_global(cx).read(cx);
- let worktree_store = workspace.project().read(cx).worktree_store();
- let configuration_tasks = manifest
- .context_servers
- .keys()
- .cloned()
- .map({
- |key| {
- let Some(descriptor) = registry.context_server_descriptor(&key) else {
- return Task::ready(Configuration::NotAvailable(
- ContextServerId(key),
- repository.clone(),
- ));
+ window
+ .spawn(cx, async move |cx| {
+ for id in ids {
+ let Some(task) = cx
+ .update(|window, cx| {
+ ConfigureContextServerModal::show_modal_for_existing_server(
+ ContextServerId(id.clone()),
+ language_registry.clone(),
+ workspace.clone(),
+ window,
+ cx,
+ )
+ })
+ .ok()
+ else {
+ continue;
};
- cx.spawn({
- let repository_url = repository.clone();
- let worktree_store = worktree_store.clone();
- async move |_, cx| {
- let configuration = descriptor
- .configuration(worktree_store.clone(), &cx)
- .await
- .context("Failed to resolve context server configuration")
- .log_err()
- .flatten();
-
- match configuration {
- Some(config) => Configuration::Required(
- ContextServerId(key),
- repository_url,
- config,
- ),
- None => {
- Configuration::NotAvailable(ContextServerId(key), repository_url)
- }
- }
- }
- })
+ task.await.log_err();
}
})
- .collect::<Vec<_>>();
-
- let jsonc_language = language_registry.language_for_name("jsonc");
-
- cx.spawn_in(window, async move |this, cx| {
- let configurations = futures::future::join_all(configuration_tasks).await;
- let jsonc_language = jsonc_language.await.ok();
-
- this.update_in(cx, |this, window, cx| {
- let workspace = cx.entity().downgrade();
- this.toggle_modal(window, cx, |window, cx| {
- ConfigureContextServerModal::new(
- configurations.into_iter(),
- context_server_store,
- jsonc_language,
- language_registry,
- workspace,
- window,
- cx,
- )
- });
- })
- })
- .detach();
+ .detach();
}
@@ -838,7 +838,11 @@ impl ExtensionStore {
self.install_or_upgrade_extension_at_endpoint(extension_id, url, operation, cx)
}
- pub fn uninstall_extension(&mut self, extension_id: Arc<str>, cx: &mut Context<Self>) {
+ pub fn uninstall_extension(
+ &mut self,
+ extension_id: Arc<str>,
+ cx: &mut Context<Self>,
+ ) -> Task<Result<()>> {
let extension_dir = self.installed_dir.join(extension_id.as_ref());
let work_dir = self.wasm_host.work_dir.join(extension_id.as_ref());
let fs = self.fs.clone();
@@ -846,7 +850,7 @@ impl ExtensionStore {
let extension_manifest = self.extension_manifest_for_id(&extension_id).cloned();
match self.outstanding_operations.entry(extension_id.clone()) {
- btree_map::Entry::Occupied(_) => return,
+ btree_map::Entry::Occupied(_) => return Task::ready(Ok(())),
btree_map::Entry::Vacant(e) => e.insert(ExtensionOperation::Remove),
};
@@ -894,7 +898,6 @@ impl ExtensionStore {
anyhow::Ok(())
})
- .detach_and_log_err(cx)
}
pub fn install_dev_extension(
@@ -482,7 +482,9 @@ async fn test_extension_store(cx: &mut TestAppContext) {
});
store.update(cx, |store, cx| {
- store.uninstall_extension("zed-ruby".into(), cx)
+ store
+ .uninstall_extension("zed-ruby".into(), cx)
+ .detach_and_log_err(cx);
});
cx.executor().advance_clock(RELOAD_DEBOUNCE_DURATION);
@@ -583,7 +583,7 @@ impl ExtensionsPage {
let extension_id = extension.id.clone();
move |_, _, cx| {
ExtensionStore::global(cx).update(cx, |store, cx| {
- store.uninstall_extension(extension_id.clone(), cx)
+ store.uninstall_extension(extension_id.clone(), cx).detach_and_log_err(cx);
});
}
}),
@@ -983,7 +983,9 @@ impl ExtensionsPage {
move |_, _, cx| {
telemetry::event!("Extension Uninstalled", extension_id);
ExtensionStore::global(cx).update(cx, |store, cx| {
- store.uninstall_extension(extension_id.clone(), cx)
+ store
+ .uninstall_extension(extension_id.clone(), cx)
+ .detach_and_log_err(cx);
});
}
}),
@@ -263,6 +263,8 @@ pub enum IconName {
ZedAssistantFilled,
ZedBurnMode,
ZedBurnModeOn,
+ ZedMcpCustom,
+ ZedMcpExtension,
ZedPredict,
ZedPredictDisabled,
ZedPredictDown,
@@ -235,6 +235,13 @@ impl ContextServerStore {
self.servers.get(id).map(ContextServerStatus::from_state)
}
+ pub fn configuration_for_server(
+ &self,
+ id: &ContextServerId,
+ ) -> Option<Arc<ContextServerConfiguration>> {
+ self.servers.get(id).map(|state| state.configuration())
+ }
+
pub fn all_server_ids(&self) -> Vec<ContextServerId> {
self.servers.keys().cloned().collect()
}
@@ -1496,25 +1496,24 @@ fn replace_value_in_json_text(
if between_comma_and_key.trim().is_empty() {
removal_start = comma_pos;
}
- } else {
- // No preceding comma, check for trailing comma
- if let Some(remaining_text) = text.get(existing_value_range.end..) {
- let mut chars = remaining_text.char_indices();
- while let Some((offset, ch)) = chars.next() {
- if ch == ',' {
- removal_end = existing_value_range.end + offset + 1;
- // Also consume whitespace after the comma
- while let Some((_, next_ch)) = chars.next() {
- if next_ch.is_whitespace() {
- removal_end += next_ch.len_utf8();
- } else {
- break;
- }
+ }
+
+ if let Some(remaining_text) = text.get(existing_value_range.end..) {
+ let mut chars = remaining_text.char_indices();
+ while let Some((offset, ch)) = chars.next() {
+ if ch == ',' {
+ removal_end = existing_value_range.end + offset + 1;
+ // Also consume whitespace after the comma
+ while let Some((_, next_ch)) = chars.next() {
+ if next_ch.is_whitespace() {
+ removal_end += next_ch.len_utf8();
+ } else {
+ break;
}
- break;
- } else if !ch.is_whitespace() {
- break;
}
+ break;
+ } else if !ch.is_whitespace() {
+ break;
}
}
}