Detailed changes
@@ -68,6 +68,7 @@ dependencies = [
"convert_case 0.8.0",
"db",
"editor",
+ "extension",
"feature_flags",
"file_icons",
"fs",
@@ -81,6 +82,7 @@ dependencies = [
"indexmap",
"indoc",
"itertools 0.14.0",
+ "jsonschema",
"language",
"language_model",
"language_model_selector",
@@ -90,6 +92,7 @@ dependencies = [
"markdown",
"menu",
"multi_buffer",
+ "notifications",
"ordered-float 2.10.1",
"parking_lot",
"paths",
@@ -106,6 +109,7 @@ dependencies = [
"schemars",
"serde",
"serde_json",
+ "serde_json_lenient",
"settings",
"smallvec",
"smol",
@@ -148,7 +152,9 @@ checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011"
dependencies = [
"cfg-if",
"const-random",
+ "getrandom 0.2.15",
"once_cell",
+ "serde",
"version_check",
"zerocopy 0.7.35",
]
@@ -2186,6 +2192,12 @@ dependencies = [
"piper",
]
+[[package]]
+name = "borrow-or-share"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3eeab4423108c5d7c744f4d234de88d18d636100093ae04caf4825134b9c3a32"
+
[[package]]
name = "borsh"
version = "1.5.7"
@@ -2301,6 +2313,12 @@ dependencies = [
"syn 1.0.109",
]
+[[package]]
+name = "bytecount"
+version = "0.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ce89b21cab1437276d2650d57e971f9d548a2d9037cc231abdc0562b97498ce"
+
[[package]]
name = "bytemuck"
version = "1.22.0"
@@ -4783,6 +4801,15 @@ dependencies = [
"zeroize",
]
+[[package]]
+name = "email_address"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e079f19b08ca6239f47f8ba8509c11cf3ea30095831f7fed61441475edd8c449"
+dependencies = [
+ "serde",
+]
+
[[package]]
name = "embed-resource"
version = "3.0.2"
@@ -5198,6 +5225,7 @@ dependencies = [
"collections",
"db",
"editor",
+ "extension",
"extension_host",
"fs",
"fuzzy",
@@ -5430,6 +5458,17 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8"
+[[package]]
+name = "fluent-uri"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1918b65d96df47d3591bed19c5cca17e3fa5d0707318e4b5ef2eae01764df7e5"
+dependencies = [
+ "borrow-or-share",
+ "ref-cast",
+ "serde",
+]
+
[[package]]
name = "flume"
version = "0.11.1"
@@ -5584,6 +5623,16 @@ dependencies = [
"percent-encoding",
]
+[[package]]
+name = "fraction"
+version = "0.15.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f158e3ff0a1b334408dc9fb811cd99b446986f4d8b741bb08f9df1604085ae7"
+dependencies = [
+ "lazy_static",
+ "num",
+]
+
[[package]]
name = "freetype-sys"
version = "0.20.1"
@@ -7587,6 +7636,33 @@ dependencies = [
"wasm-bindgen",
]
+[[package]]
+name = "jsonschema"
+version = "0.30.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1b46a0365a611fbf1d2143104dcf910aada96fafd295bab16c60b802bf6fa1d"
+dependencies = [
+ "ahash 0.8.11",
+ "base64 0.22.1",
+ "bytecount",
+ "email_address",
+ "fancy-regex 0.14.0",
+ "fraction",
+ "idna",
+ "itoa",
+ "num-cmp",
+ "num-traits",
+ "once_cell",
+ "percent-encoding",
+ "referencing",
+ "regex",
+ "regex-syntax 0.8.5",
+ "reqwest 0.12.15 (registry+https://github.com/rust-lang/crates.io-index)",
+ "serde",
+ "serde_json",
+ "uuid-simd",
+]
+
[[package]]
name = "jsonwebtoken"
version = "9.3.1"
@@ -8271,7 +8347,7 @@ dependencies = [
"prost 0.9.0",
"prost-build 0.9.0",
"prost-types 0.9.0",
- "reqwest 0.12.15",
+ "reqwest 0.12.15 (git+https://github.com/zed-industries/reqwest.git?rev=951c770a32f1998d6e999cef3e59e0013e6c4415)",
"serde",
"workspace-hack",
]
@@ -9181,6 +9257,12 @@ dependencies = [
"zeroize",
]
+[[package]]
+name = "num-cmp"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63335b2e2c34fae2fb0aa2cecfd9f0832a1e24b3b32ecec612c3426d46dc8aaa"
+
[[package]]
name = "num-complex"
version = "0.4.6"
@@ -11774,6 +11856,20 @@ dependencies = [
"syn 2.0.100",
]
+[[package]]
+name = "referencing"
+version = "0.30.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8eff4fa778b5c2a57e85c5f2fe3a709c52f0e60d23146e2151cbef5893f420e"
+dependencies = [
+ "ahash 0.8.11",
+ "fluent-uri",
+ "once_cell",
+ "parking_lot",
+ "percent-encoding",
+ "serde_json",
+]
+
[[package]]
name = "refineable"
version = "0.1.0"
@@ -12043,6 +12139,43 @@ dependencies = [
"winreg 0.50.0",
]
+[[package]]
+name = "reqwest"
+version = "0.12.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d19c46a6fdd48bc4dab94b6103fccc55d34c67cc0ad04653aad4ea2a07cd7bbb"
+dependencies = [
+ "base64 0.22.1",
+ "bytes 1.10.1",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "http 1.3.1",
+ "http-body 1.0.1",
+ "http-body-util",
+ "hyper 1.6.0",
+ "hyper-util",
+ "ipnet",
+ "js-sys",
+ "log",
+ "mime",
+ "once_cell",
+ "percent-encoding",
+ "pin-project-lite",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper 1.0.2",
+ "tokio",
+ "tower 0.5.2",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "windows-registry 0.4.0",
+]
+
[[package]]
name = "reqwest"
version = "0.12.15"
@@ -12103,7 +12236,7 @@ dependencies = [
"http_client_tls",
"log",
"regex",
- "reqwest 0.12.15",
+ "reqwest 0.12.15 (git+https://github.com/zed-industries/reqwest.git?rev=951c770a32f1998d6e999cef3e59e0013e6c4415)",
"serde",
"smol",
"tokio",
@@ -15954,6 +16087,17 @@ dependencies = [
"sha1_smol",
]
+[[package]]
+name = "uuid-simd"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8"
+dependencies = [
+ "outref",
+ "uuid",
+ "vsimd",
+]
+
[[package]]
name = "v_frame"
version = "0.3.8"
@@ -18054,6 +18198,7 @@ dependencies = [
"hmac",
"hyper 0.14.32",
"hyper-rustls 0.27.5",
+ "idna",
"indexmap",
"inout",
"itertools 0.12.1",
@@ -18077,6 +18222,7 @@ dependencies = [
"num-bigint-dig",
"num-integer",
"num-iter",
+ "num-rational",
"num-traits",
"object",
"once_cell",
@@ -462,6 +462,7 @@ indexmap = { version = "2.7.0", features = ["serde"] }
indoc = "2"
inventory = "0.3.19"
itertools = "0.14.0"
+jsonschema = "0.30.0"
jsonwebtoken = "9.3"
jupyter-protocol = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
jupyter-websocket-client = { git = "https://github.com/ConradIrwin/runtimed" ,rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
@@ -963,6 +963,14 @@
"escape": "menu::Cancel"
}
},
+ {
+ "context": "ConfigureContextServerModal > Editor",
+ "bindings": {
+ "escape": "menu::Cancel",
+ "enter": "editor::Newline",
+ "ctrl-enter": "menu::Confirm"
+ }
+ },
{
"context": "Diagnostics",
"use_key_equivalents": true,
@@ -1069,6 +1069,15 @@
"escape": "menu::Cancel"
}
},
+ {
+ "context": "ConfigureContextServerModal > Editor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "escape": "menu::Cancel",
+ "enter": "editor::Newline",
+ "cmd-enter": "menu::Confirm"
+ }
+ },
{
"context": "Diagnostics",
"use_key_equivalents": true,
@@ -35,6 +35,7 @@ context_server.workspace = true
convert_case.workspace = true
db.workspace = true
editor.workspace = true
+extension.workspace = true
feature_flags.workspace = true
file_icons.workspace = true
fs.workspace = true
@@ -47,6 +48,7 @@ html_to_markdown.workspace = true
http_client.workspace = true
indexmap.workspace = true
itertools.workspace = true
+jsonschema.workspace = true
language.workspace = true
language_model.workspace = true
language_model_selector.workspace = true
@@ -56,6 +58,7 @@ lsp.workspace = true
markdown.workspace = true
menu.workspace = true
multi_buffer.workspace = true
+notifications.workspace = true
ordered-float.workspace = true
parking_lot.workspace = true
paths.workspace = true
@@ -71,6 +74,7 @@ rope.workspace = true
schemars.workspace = true
serde.workspace = true
serde_json.workspace = true
+serde_json_lenient.workspace = true
settings.workspace = true
smallvec.workspace = true
smol.workspace = true
@@ -6,6 +6,7 @@ mod assistant_panel;
mod buffer_codegen;
mod context;
mod context_picker;
+mod context_server_configuration;
mod context_store;
mod context_strip;
mod history_store;
@@ -30,6 +31,7 @@ use command_palette_hooks::CommandPaletteFilter;
use feature_flags::{Assistant2FeatureFlag, FeatureFlagAppExt};
use fs::Fs;
use gpui::{App, actions, impl_actions};
+use language::LanguageRegistry;
use prompt_store::PromptBuilder;
use schemars::JsonSchema;
use serde::Deserialize;
@@ -107,11 +109,13 @@ pub fn init(
fs: Arc<dyn Fs>,
client: Arc<Client>,
prompt_builder: Arc<PromptBuilder>,
+ language_registry: Arc<LanguageRegistry>,
cx: &mut App,
) {
AssistantSettings::register(cx);
thread_store::init(cx);
assistant_panel::init(cx);
+ context_server_configuration::init(language_registry, cx);
inline_assistant::init(
fs.clone(),
@@ -1,16 +1,18 @@
mod add_context_server_modal;
+mod configure_context_server_modal;
mod manage_profiles_modal;
mod tool_picker;
-use std::sync::Arc;
+use std::{sync::Arc, time::Duration};
use assistant_settings::AssistantSettings;
use assistant_tool::{ToolSource, ToolWorkingSet};
use collections::HashMap;
-use context_server::manager::ContextServerManager;
+use context_server::manager::{ContextServer, ContextServerManager, ContextServerStatus};
use fs::Fs;
use gpui::{
- Action, AnyView, App, Entity, EventEmitter, FocusHandle, Focusable, ScrollHandle, Subscription,
+ Action, Animation, AnimationExt as _, AnyView, App, Entity, EventEmitter, FocusHandle,
+ Focusable, ScrollHandle, Subscription, pulsating_between,
};
use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
use settings::{Settings, update_settings_file};
@@ -22,6 +24,7 @@ use util::ResultExt as _;
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;
use crate::AddContextServer;
@@ -256,8 +259,6 @@ impl AssistantConfiguration {
fn render_context_servers_section(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
let context_servers = self.context_server_manager.read(cx).all_servers().clone();
- let tools_by_source = self.tools.read(cx).tools_by_source(cx);
- let empty = Vec::new();
const SUBHEADING: &str = "Connect to context servers via the Model Context Protocol either via Zed extensions or directly.";
@@ -272,136 +273,11 @@ impl AssistantConfiguration {
.child(Headline::new("Model Context Protocol (MCP) Servers"))
.child(Label::new(SUBHEADING).color(Color::Muted)),
)
- .children(context_servers.into_iter().map(|context_server| {
- let is_running = context_server.client().is_some();
- let are_tools_expanded = self
- .expanded_context_server_tools
- .get(&context_server.id())
- .copied()
- .unwrap_or_default();
-
- let tools = tools_by_source
- .get(&ToolSource::ContextServer {
- id: context_server.id().into(),
- })
- .unwrap_or_else(|| &empty);
- let tool_count = tools.len();
-
- v_flex()
- .id(SharedString::from(context_server.id()))
- .border_1()
- .rounded_md()
- .border_color(cx.theme().colors().border)
- .bg(cx.theme().colors().background.opacity(0.25))
- .child(
- h_flex()
- .p_1()
- .justify_between()
- .when(are_tools_expanded && tool_count > 1, |element| {
- element
- .border_b_1()
- .border_color(cx.theme().colors().border)
- })
- .child(
- h_flex()
- .gap_2()
- .child(
- Disclosure::new("tool-list-disclosure", are_tools_expanded)
- .disabled(tool_count == 0)
- .on_click(cx.listener({
- let context_server_id = context_server.id();
- move |this, _event, _window, _cx| {
- let is_open = this
- .expanded_context_server_tools
- .entry(context_server_id.clone())
- .or_insert(false);
-
- *is_open = !*is_open;
- }
- })),
- )
- .child(Indicator::dot().color(if is_running {
- Color::Success
- } else {
- Color::Error
- }))
- .child(Label::new(context_server.id()))
- .child(
- Label::new(format!("{tool_count} tools"))
- .color(Color::Muted)
- .size(LabelSize::Small),
- ),
- )
- .child(
- Switch::new("context-server-switch", is_running.into())
- .color(SwitchColor::Accent)
- .on_click({
- let context_server_manager =
- self.context_server_manager.clone();
- let context_server = context_server.clone();
- move |state, _window, cx| match state {
- ToggleState::Unselected
- | ToggleState::Indeterminate => {
- context_server_manager.update(cx, |this, cx| {
- this.stop_server(context_server.clone(), cx)
- .log_err();
- });
- }
- ToggleState::Selected => {
- cx.spawn({
- let context_server_manager =
- context_server_manager.clone();
- let context_server = context_server.clone();
- async move |cx| {
- if let Some(start_server_task) =
- context_server_manager
- .update(cx, |this, cx| {
- this.start_server(
- context_server,
- cx,
- )
- })
- .log_err()
- {
- start_server_task.await.log_err();
- }
- }
- })
- .detach();
- }
- }
- }),
- ),
- )
- .map(|parent| {
- if !are_tools_expanded {
- return parent;
- }
-
- parent.child(v_flex().py_1p5().px_1().gap_1().children(
- tools.into_iter().enumerate().map(|(ix, tool)| {
- h_flex()
- .id(("tool-item", ix))
- .px_1()
- .gap_2()
- .justify_between()
- .hover(|style| style.bg(cx.theme().colors().element_hover))
- .rounded_sm()
- .child(
- Label::new(tool.name())
- .buffer_font(cx)
- .size(LabelSize::Small),
- )
- .child(
- Icon::new(IconName::Info)
- .size(IconSize::Small)
- .color(Color::Ignored),
- )
- .tooltip(Tooltip::text(tool.description()))
- }),
- ))
- })
- }))
+ .children(
+ context_servers
+ .into_iter()
+ .map(|context_server| self.render_context_server(context_server, cx)),
+ )
.child(
h_flex()
.justify_between()
@@ -447,6 +323,190 @@ impl AssistantConfiguration {
),
)
}
+
+ fn render_context_server(
+ &self,
+ context_server: Arc<ContextServer>,
+ cx: &mut Context<Self>,
+ ) -> impl use<> + IntoElement {
+ let tools_by_source = self.tools.read(cx).tools_by_source(cx);
+ let server_status = self
+ .context_server_manager
+ .read(cx)
+ .status_for_server(&context_server.id());
+
+ let is_running = matches!(server_status, Some(ContextServerStatus::Running));
+
+ let error = if let Some(ContextServerStatus::Error(error)) = server_status.clone() {
+ Some(error)
+ } else {
+ None
+ };
+
+ let are_tools_expanded = self
+ .expanded_context_server_tools
+ .get(&context_server.id())
+ .copied()
+ .unwrap_or_default();
+
+ let tools = tools_by_source
+ .get(&ToolSource::ContextServer {
+ id: context_server.id().into(),
+ })
+ .map_or([].as_slice(), |tools| tools.as_slice());
+ let tool_count = tools.len();
+
+ v_flex()
+ .id(SharedString::from(context_server.id()))
+ .border_1()
+ .rounded_md()
+ .border_color(cx.theme().colors().border)
+ .bg(cx.theme().colors().background.opacity(0.25))
+ .child(
+ h_flex()
+ .p_1()
+ .justify_between()
+ .when(are_tools_expanded && tool_count > 1, |element| {
+ element
+ .border_b_1()
+ .border_color(cx.theme().colors().border)
+ })
+ .child(
+ h_flex()
+ .gap_2()
+ .child(
+ Disclosure::new(
+ "tool-list-disclosure",
+ are_tools_expanded || error.is_some(),
+ )
+ .disabled(tool_count == 0)
+ .on_click(cx.listener({
+ let context_server_id = context_server.id();
+ move |this, _event, _window, _cx| {
+ let is_open = this
+ .expanded_context_server_tools
+ .entry(context_server_id.clone())
+ .or_insert(false);
+
+ *is_open = !*is_open;
+ }
+ })),
+ )
+ .child(match server_status {
+ Some(ContextServerStatus::Starting) => {
+ let color = Color::Success.color(cx);
+ Indicator::dot()
+ .color(Color::Success)
+ .with_animation(
+ SharedString::from(format!(
+ "{}-starting",
+ context_server.id(),
+ )),
+ Animation::new(Duration::from_secs(2))
+ .repeat()
+ .with_easing(pulsating_between(0.4, 1.)),
+ move |this, delta| {
+ this.color(color.alpha(delta).into())
+ },
+ )
+ .into_any_element()
+ }
+ Some(ContextServerStatus::Running) => {
+ Indicator::dot().color(Color::Success).into_any_element()
+ }
+ Some(ContextServerStatus::Error(_)) => {
+ Indicator::dot().color(Color::Error).into_any_element()
+ }
+ None => Indicator::dot().color(Color::Muted).into_any_element(),
+ })
+ .child(Label::new(context_server.id()))
+ .when(is_running, |this| {
+ this.child(
+ Label::new(if tool_count == 1 {
+ SharedString::from("1 tool")
+ } else {
+ SharedString::from(format!("{} tools", tool_count))
+ })
+ .color(Color::Muted)
+ .size(LabelSize::Small),
+ )
+ }),
+ )
+ .child(
+ Switch::new("context-server-switch", is_running.into())
+ .color(SwitchColor::Accent)
+ .on_click({
+ let context_server_manager = self.context_server_manager.clone();
+ let context_server = context_server.clone();
+ move |state, _window, cx| match state {
+ ToggleState::Unselected | ToggleState::Indeterminate => {
+ context_server_manager.update(cx, |this, cx| {
+ this.stop_server(context_server.clone(), cx).log_err();
+ });
+ }
+ ToggleState::Selected => {
+ cx.spawn({
+ let context_server_manager =
+ context_server_manager.clone();
+ let context_server = context_server.clone();
+ async move |cx| {
+ if let Some(start_server_task) =
+ context_server_manager
+ .update(cx, |this, cx| {
+ this.start_server(context_server, cx)
+ })
+ .log_err()
+ {
+ start_server_task.await.log_err();
+ }
+ }
+ })
+ .detach();
+ }
+ }
+ }),
+ ),
+ )
+ .map(|parent| {
+ if let Some(error) = error {
+ return parent.child(
+ div().py_1p5().px_2().child(
+ Label::new(error)
+ .color(Color::Muted)
+ .buffer_font(cx)
+ .size(LabelSize::Small),
+ ),
+ );
+ }
+
+ if !are_tools_expanded || tools.is_empty() {
+ return parent;
+ }
+
+ parent.child(v_flex().py_1p5().px_1().gap_1().children(
+ tools.into_iter().enumerate().map(|(ix, tool)| {
+ h_flex()
+ .id(("tool-item", ix))
+ .px_1()
+ .gap_2()
+ .justify_between()
+ .hover(|style| style.bg(cx.theme().colors().element_hover))
+ .rounded_sm()
+ .child(
+ Label::new(tool.name())
+ .buffer_font(cx)
+ .size(LabelSize::Small),
+ )
+ .child(
+ Icon::new(IconName::Info)
+ .size(IconSize::Small)
+ .color(Color::Ignored),
+ )
+ .tooltip(Tooltip::text(tool.description()))
+ }),
+ ))
+ })
+ }
}
impl Render for AssistantConfiguration {
@@ -0,0 +1,443 @@
+use std::{
+ sync::{Arc, Mutex},
+ time::Duration,
+};
+
+use anyhow::Context as _;
+use context_server::manager::{ContextServerManager, ContextServerStatus};
+use editor::{Editor, EditorElement, EditorStyle};
+use extension::ContextServerConfiguration;
+use gpui::{
+ Animation, AnimationExt, App, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, Task,
+ TextStyle, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity, percentage,
+};
+use language::{Language, LanguageRegistry};
+use markdown::{Markdown, MarkdownElement, MarkdownStyle};
+use notifications::status_toast::{StatusToast, ToastIcon};
+use settings::{Settings as _, update_settings_file};
+use theme::ThemeSettings;
+use ui::{KeyBinding, Modal, ModalFooter, ModalHeader, Section, prelude::*};
+use util::ResultExt;
+use workspace::{ModalView, Workspace};
+
+pub(crate) struct ConfigureContextServerModal {
+ workspace: WeakEntity<Workspace>,
+ context_servers_to_setup: Vec<ConfigureContextServer>,
+ context_server_manager: Entity<ContextServerManager>,
+}
+
+struct ConfigureContextServer {
+ id: Arc<str>,
+ installation_instructions: Entity<markdown::Markdown>,
+ settings_validator: Option<jsonschema::Validator>,
+ settings_editor: Entity<Editor>,
+ last_error: Option<SharedString>,
+ waiting_for_context_server: bool,
+}
+
+impl ConfigureContextServerModal {
+ pub fn new(
+ configurations: impl Iterator<Item = (Arc<str>, ContextServerConfiguration)>,
+ jsonc_language: Option<Arc<Language>>,
+ context_server_manager: Entity<ContextServerManager>,
+ language_registry: Arc<LanguageRegistry>,
+ workspace: WeakEntity<Workspace>,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> Option<Self> {
+ let context_servers_to_setup = configurations
+ .map(|(id, manifest)| {
+ let jsonc_language = jsonc_language.clone();
+ let settings_validator = jsonschema::validator_for(&manifest.settings_schema)
+ .context("Failed to load JSON schema for context server settings")
+ .log_err();
+ ConfigureContextServer {
+ id: id.clone(),
+ installation_instructions: cx.new(|cx| {
+ Markdown::new(
+ manifest.installation_instructions.clone().into(),
+ Some(language_registry.clone()),
+ None,
+ cx,
+ )
+ }),
+ settings_validator,
+ settings_editor: cx.new(|cx| {
+ let mut editor = Editor::auto_height(16, window, cx);
+ editor.set_text(manifest.default_settings.trim(), window, 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,
+ }
+ })
+ .collect::<Vec<_>>();
+
+ if context_servers_to_setup.is_empty() {
+ return None;
+ }
+
+ Some(Self {
+ workspace,
+ context_servers_to_setup,
+ context_server_manager,
+ })
+ }
+}
+
+impl ConfigureContextServerModal {
+ pub fn confirm(&mut self, cx: &mut Context<Self>) {
+ if self.context_servers_to_setup.is_empty() {
+ return;
+ }
+
+ let Some(workspace) = self.workspace.upgrade() else {
+ return;
+ };
+
+ let configuration = &mut self.context_servers_to_setup[0];
+ if configuration.waiting_for_context_server {
+ return;
+ }
+
+ 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;
+ }
+ };
+
+ 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;
+ }
+ }
+ let id = configuration.id.clone();
+
+ let settings_changed = context_server::ContextServerSettings::get_global(cx)
+ .context_servers
+ .get(&id)
+ .map_or(true, |config| {
+ config.settings.as_ref() != Some(&settings_value)
+ });
+
+ let is_running = self.context_server_manager.read(cx).status_for_server(&id)
+ == Some(ContextServerStatus::Running);
+
+ if !settings_changed && is_running {
+ self.complete_setup(id, cx);
+ return;
+ }
+
+ configuration.waiting_for_context_server = true;
+
+ let task = wait_for_context_server(&self.context_server_manager, id.clone(), cx);
+ cx.spawn({
+ let id = id.clone();
+ async move |this, cx| {
+ let result = task.await;
+ this.update(cx, |this, cx| match result {
+ Ok(_) => {
+ this.complete_setup(id, cx);
+ }
+ Err(err) => {
+ if let Some(configuration) = this.context_servers_to_setup.get_mut(0) {
+ configuration.last_error = Some(err.into());
+ configuration.waiting_for_context_server = false;
+ } else {
+ this.dismiss(cx);
+ }
+ cx.notify();
+ }
+ })
+ }
+ })
+ .detach();
+
+ // When we write the settings to the file, the context server will be restarted.
+ update_settings_file::<context_server::ContextServerSettings>(
+ workspace.read(cx).app_state().fs.clone(),
+ cx,
+ {
+ let id = id.clone();
+ |settings, _| {
+ if let Some(server_config) = settings.context_servers.get_mut(&id) {
+ server_config.settings = Some(settings_value);
+ } else {
+ settings.context_servers.insert(
+ id,
+ context_server::ServerConfig {
+ settings: Some(settings_value),
+ ..Default::default()
+ },
+ );
+ }
+ }
+ },
+ );
+ }
+
+ fn complete_setup(&mut self, id: Arc<str>, cx: &mut Context<Self>) {
+ self.context_servers_to_setup.remove(0);
+ cx.notify();
+
+ if !self.context_servers_to_setup.is_empty() {
+ return;
+ }
+
+ self.workspace
+ .update(cx, {
+ |workspace, cx| {
+ let status_toast = StatusToast::new(
+ format!("{} MCP configured successfully", id),
+ cx,
+ |this, _cx| {
+ this.icon(ToastIcon::new(IconName::DatabaseZap).color(Color::Muted))
+ .action("Dismiss", |_, _| {})
+ },
+ );
+
+ workspace.toggle_status_toast(status_toast, cx);
+ }
+ })
+ .log_err();
+
+ self.dismiss(cx);
+ }
+
+ fn dismiss(&self, cx: &mut Context<Self>) {
+ cx.emit(DismissEvent);
+ }
+}
+
+fn wait_for_context_server(
+ context_server_manager: &Entity<ContextServerManager>,
+ context_server_id: Arc<str>,
+ cx: &mut App,
+) -> Task<Result<(), Arc<str>>> {
+ let (tx, rx) = futures::channel::oneshot::channel();
+ let tx = Arc::new(Mutex::new(Some(tx)));
+
+ let subscription = cx.subscribe(context_server_manager, move |_, event, _cx| match event {
+ context_server::manager::Event::ServerStatusChanged { server_id, status } => match status {
+ Some(ContextServerStatus::Running) => {
+ if server_id == &context_server_id {
+ if let Some(tx) = tx.lock().unwrap().take() {
+ let _ = tx.send(Ok(()));
+ }
+ }
+ }
+ Some(ContextServerStatus::Error(error)) => {
+ if server_id == &context_server_id {
+ if let Some(tx) = tx.lock().unwrap().take() {
+ let _ = tx.send(Err(error.clone()));
+ }
+ }
+ }
+ _ => {}
+ },
+ });
+
+ cx.spawn(async move |_cx| {
+ let result = rx.await.unwrap();
+ drop(subscription);
+ result
+ })
+}
+
+impl Render for ConfigureContextServerModal {
+ fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ let Some(configuration) = self.context_servers_to_setup.first() else {
+ return div().child("No context servers to setup");
+ };
+
+ let focus_handle = self.focus_handle(cx);
+
+ div()
+ .elevation_3(cx)
+ .w(rems(34.))
+ .key_context("ConfigureContextServerModal")
+ .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 {}", configuration.id)))
+ .section(
+ Section::new()
+ .child(div().py_2().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().end_slot(
+ h_flex()
+ .gap_1()
+ .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(configuration.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)
+ })),
+ ),
+ ),
+ ),
+ )
+ }
+}
+
+pub(crate) fn default_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
+ let theme_settings = ThemeSettings::get_global(cx);
+ let colors = cx.theme().colors();
+ let mut text_style = window.text_style();
+ text_style.refine(&TextStyleRefinement {
+ font_family: Some(theme_settings.ui_font.family.clone()),
+ font_fallbacks: theme_settings.ui_font.fallbacks.clone(),
+ font_features: Some(theme_settings.ui_font.features.clone()),
+ font_size: Some(TextSize::XSmall.rems(cx).into()),
+ color: Some(colors.text_muted),
+ ..Default::default()
+ });
+
+ MarkdownStyle {
+ base_text_style: text_style.clone(),
+ selection_background_color: cx.theme().players().local().selection,
+ link: TextStyleRefinement {
+ background_color: Some(colors.editor_foreground.opacity(0.025)),
+ underline: Some(UnderlineStyle {
+ color: Some(colors.text_accent.opacity(0.5)),
+ thickness: px(1.),
+ ..Default::default()
+ }),
+ ..Default::default()
+ },
+ ..Default::default()
+ }
+}
+
+impl ModalView for ConfigureContextServerModal {}
+impl EventEmitter<DismissEvent> for ConfigureContextServerModal {}
+impl Focusable for ConfigureContextServerModal {
+ fn focus_handle(&self, cx: &App) -> FocusHandle {
+ if let Some(current) = self.context_servers_to_setup.first() {
+ current.settings_editor.read(cx).focus_handle(cx)
+ } else {
+ cx.focus_handle()
+ }
+ }
+}
@@ -0,0 +1,120 @@
+use std::sync::Arc;
+
+use anyhow::Context as _;
+use context_server::ContextServerDescriptorRegistry;
+use extension::ExtensionManifest;
+use language::LanguageRegistry;
+use ui::prelude::*;
+use util::ResultExt;
+use workspace::Workspace;
+
+use crate::{AssistantPanel, assistant_configuration::ConfigureContextServerModal};
+
+pub(crate) fn init(language_registry: Arc<LanguageRegistry>, cx: &mut App) {
+ cx.observe_new(move |_: &mut Workspace, window, cx| {
+ let Some(window) = window else {
+ return;
+ };
+
+ if let Some(extension_events) = extension::ExtensionEvents::try_global(cx).as_ref() {
+ cx.subscribe_in(extension_events, window, {
+ let language_registry = language_registry.clone();
+ move |workspace, _, event, window, cx| match event {
+ extension::Event::ExtensionInstalled(manifest) => {
+ show_configure_mcp_modal(
+ language_registry.clone(),
+ manifest,
+ workspace,
+ window,
+ cx,
+ );
+ }
+ extension::Event::ConfigureExtensionRequested(manifest) => {
+ if !manifest.context_servers.is_empty() {
+ show_configure_mcp_modal(
+ language_registry.clone(),
+ manifest,
+ workspace,
+ window,
+ cx,
+ );
+ }
+ }
+ _ => {}
+ }
+ })
+ .detach();
+ } else {
+ log::info!(
+ "No extension events global found. Skipping context server configuration wizard"
+ );
+ }
+ })
+ .detach();
+}
+
+fn show_configure_mcp_modal(
+ language_registry: Arc<LanguageRegistry>,
+ manifest: &Arc<ExtensionManifest>,
+ workspace: &mut Workspace,
+ window: &mut Window,
+ cx: &mut Context<'_, Workspace>,
+) {
+ let Some(context_server_manager) = workspace.panel::<AssistantPanel>(cx).map(|panel| {
+ panel
+ .read(cx)
+ .thread_store()
+ .read(cx)
+ .context_server_manager()
+ }) else {
+ return;
+ };
+
+ let registry = ContextServerDescriptorRegistry::global(cx).read(cx);
+ let project = workspace.project().clone();
+ let configuration_tasks = manifest
+ .context_servers
+ .keys()
+ .cloned()
+ .filter_map({
+ |key| {
+ let descriptor = registry.context_server_descriptor(&key)?;
+ Some(cx.spawn({
+ let project = project.clone();
+ async move |_, cx| {
+ descriptor
+ .configuration(project, &cx)
+ .await
+ .context("Failed to resolve context server configuration")
+ .log_err()
+ .flatten()
+ .map(|config| (key, config))
+ }
+ }))
+ }
+ })
+ .collect::<Vec<_>>();
+
+ let jsonc_language = language_registry.language_for_name("jsonc");
+
+ cx.spawn_in(window, async move |this, cx| {
+ let descriptors = futures::future::join_all(configuration_tasks).await;
+ let jsonc_language = jsonc_language.await.ok();
+
+ this.update_in(cx, |this, window, cx| {
+ let modal = ConfigureContextServerModal::new(
+ descriptors.into_iter().flatten(),
+ jsonc_language,
+ context_server_manager,
+ language_registry,
+ cx.entity().downgrade(),
+ window,
+ cx,
+ );
+ if let Some(modal) = modal {
+ this.toggle_modal(window, cx, |_, _| modal);
+ }
+ })
+ })
+ .detach();
+}
@@ -9,8 +9,8 @@ use assistant_settings::{AgentProfile, AgentProfileId, AssistantSettings};
use assistant_tool::{ToolId, ToolSource, ToolWorkingSet};
use chrono::{DateTime, Utc};
use collections::HashMap;
-use context_server::manager::ContextServerManager;
-use context_server::{ContextServerFactoryRegistry, ContextServerTool};
+use context_server::manager::{ContextServerManager, ContextServerStatus};
+use context_server::{ContextServerDescriptorRegistry, ContextServerTool};
use futures::channel::{mpsc, oneshot};
use futures::future::{self, BoxFuture, Shared};
use futures::{FutureExt as _, StreamExt as _};
@@ -108,7 +108,7 @@ impl ThreadStore {
prompt_store: Option<Entity<PromptStore>>,
cx: &mut Context<Self>,
) -> (Self, oneshot::Receiver<()>) {
- let context_server_factory_registry = ContextServerFactoryRegistry::default_global(cx);
+ let context_server_factory_registry = ContextServerDescriptorRegistry::default_global(cx);
let context_server_manager = cx.new(|cx| {
ContextServerManager::new(context_server_factory_registry, project.clone(), cx)
});
@@ -555,62 +555,68 @@ impl ThreadStore {
) {
let tool_working_set = self.tools.clone();
match event {
- context_server::manager::Event::ServerStarted { server_id } => {
- if let Some(server) = context_server_manager.read(cx).get_server(server_id) {
- let context_server_manager = context_server_manager.clone();
- cx.spawn({
- let server = server.clone();
- let server_id = server_id.clone();
- async move |this, cx| {
- let Some(protocol) = server.client() else {
- return;
- };
-
- if protocol.capable(context_server::protocol::ServerCapability::Tools) {
- if let Some(tools) = protocol.list_tools().await.log_err() {
- let tool_ids = tool_working_set
- .update(cx, |tool_working_set, _| {
- tools
- .tools
- .into_iter()
- .map(|tool| {
- log::info!(
- "registering context server tool: {:?}",
- tool.name
- );
- tool_working_set.insert(Arc::new(
- ContextServerTool::new(
- context_server_manager.clone(),
- server.id(),
- tool,
- ),
- ))
+ context_server::manager::Event::ServerStatusChanged { server_id, status } => {
+ match status {
+ Some(ContextServerStatus::Running) => {
+ if let Some(server) = context_server_manager.read(cx).get_server(server_id)
+ {
+ let context_server_manager = context_server_manager.clone();
+ cx.spawn({
+ let server = server.clone();
+ let server_id = server_id.clone();
+ async move |this, cx| {
+ let Some(protocol) = server.client() else {
+ return;
+ };
+
+ if protocol.capable(context_server::protocol::ServerCapability::Tools) {
+ if let Some(tools) = protocol.list_tools().await.log_err() {
+ let tool_ids = tool_working_set
+ .update(cx, |tool_working_set, _| {
+ tools
+ .tools
+ .into_iter()
+ .map(|tool| {
+ log::info!(
+ "registering context server tool: {:?}",
+ tool.name
+ );
+ tool_working_set.insert(Arc::new(
+ ContextServerTool::new(
+ context_server_manager.clone(),
+ server.id(),
+ tool,
+ ),
+ ))
+ })
+ .collect::<Vec<_>>()
})
- .collect::<Vec<_>>()
- })
- .log_err();
-
- if let Some(tool_ids) = tool_ids {
- this.update(cx, |this, cx| {
- this.context_server_tool_ids
- .insert(server_id, tool_ids);
- this.load_default_profile(cx);
- })
- .log_err();
+ .log_err();
+
+ if let Some(tool_ids) = tool_ids {
+ this.update(cx, |this, cx| {
+ this.context_server_tool_ids
+ .insert(server_id, tool_ids);
+ this.load_default_profile(cx);
+ })
+ .log_err();
+ }
+ }
}
}
- }
+ })
+ .detach();
}
- })
- .detach();
- }
- }
- context_server::manager::Event::ServerStopped { server_id } => {
- if let Some(tool_ids) = self.context_server_tool_ids.remove(server_id) {
- tool_working_set.update(cx, |tool_working_set, _| {
- tool_working_set.remove(&tool_ids);
- });
- self.load_default_profile(cx);
+ }
+ None => {
+ if let Some(tool_ids) = self.context_server_tool_ids.remove(server_id) {
+ tool_working_set.update(cx, |tool_working_set, _| {
+ tool_working_set.remove(&tool_ids);
+ });
+ self.load_default_profile(cx);
+ }
+ }
+ _ => {}
}
}
}
@@ -7,8 +7,8 @@ use assistant_slash_command::{SlashCommandId, SlashCommandWorkingSet};
use client::{Client, TypedEnvelope, proto, telemetry::Telemetry};
use clock::ReplicaId;
use collections::HashMap;
-use context_server::ContextServerFactoryRegistry;
-use context_server::manager::ContextServerManager;
+use context_server::ContextServerDescriptorRegistry;
+use context_server::manager::{ContextServerManager, ContextServerStatus};
use fs::{Fs, RemoveOptions};
use futures::StreamExt;
use fuzzy::StringMatchCandidate;
@@ -99,7 +99,7 @@ impl ContextStore {
let this = cx.new(|cx: &mut Context<Self>| {
let context_server_factory_registry =
- ContextServerFactoryRegistry::default_global(cx);
+ ContextServerDescriptorRegistry::default_global(cx);
let context_server_manager = cx.new(|cx| {
ContextServerManager::new(context_server_factory_registry, project.clone(), cx)
});
@@ -831,54 +831,60 @@ impl ContextStore {
) {
let slash_command_working_set = self.slash_commands.clone();
match event {
- context_server::manager::Event::ServerStarted { server_id } => {
- if let Some(server) = context_server_manager.read(cx).get_server(server_id) {
- let context_server_manager = context_server_manager.clone();
- cx.spawn({
- let server = server.clone();
- let server_id = server_id.clone();
- async move |this, cx| {
- let Some(protocol) = server.client() else {
- return;
- };
-
- if protocol.capable(context_server::protocol::ServerCapability::Prompts) {
- if let Some(prompts) = protocol.list_prompts().await.log_err() {
- let slash_command_ids = prompts
- .into_iter()
- .filter(assistant_slash_commands::acceptable_prompt)
- .map(|prompt| {
- log::info!(
- "registering context server command: {:?}",
- prompt.name
- );
- slash_command_working_set.insert(Arc::new(
- assistant_slash_commands::ContextServerSlashCommand::new(
- context_server_manager.clone(),
- &server,
- prompt,
- ),
- ))
- })
- .collect::<Vec<_>>();
-
- this.update( cx, |this, _cx| {
- this.context_server_slash_command_ids
- .insert(server_id.clone(), slash_command_ids);
- })
- .log_err();
+ context_server::manager::Event::ServerStatusChanged { server_id, status } => {
+ match status {
+ Some(ContextServerStatus::Running) => {
+ if let Some(server) = context_server_manager.read(cx).get_server(server_id)
+ {
+ let context_server_manager = context_server_manager.clone();
+ cx.spawn({
+ let server = server.clone();
+ let server_id = server_id.clone();
+ async move |this, cx| {
+ let Some(protocol) = server.client() else {
+ return;
+ };
+
+ if protocol.capable(context_server::protocol::ServerCapability::Prompts) {
+ if let Some(prompts) = protocol.list_prompts().await.log_err() {
+ let slash_command_ids = prompts
+ .into_iter()
+ .filter(assistant_slash_commands::acceptable_prompt)
+ .map(|prompt| {
+ log::info!(
+ "registering context server command: {:?}",
+ prompt.name
+ );
+ slash_command_working_set.insert(Arc::new(
+ assistant_slash_commands::ContextServerSlashCommand::new(
+ context_server_manager.clone(),
+ &server,
+ prompt,
+ ),
+ ))
+ })
+ .collect::<Vec<_>>();
+
+ this.update( cx, |this, _cx| {
+ this.context_server_slash_command_ids
+ .insert(server_id.clone(), slash_command_ids);
+ })
+ .log_err();
+ }
+ }
}
- }
+ })
+ .detach();
}
- })
- .detach();
- }
- }
- context_server::manager::Event::ServerStopped { server_id } => {
- if let Some(slash_command_ids) =
- self.context_server_slash_command_ids.remove(server_id)
- {
- slash_command_working_set.remove(&slash_command_ids);
+ }
+ None => {
+ if let Some(slash_command_ids) =
+ self.context_server_slash_command_ids.remove(server_id)
+ {
+ slash_command_working_set.remove(&slash_command_ids);
+ }
+ }
+ _ => {}
}
}
}
@@ -34,3 +34,7 @@ smol.workspace = true
url = { workspace = true, features = ["serde"] }
util.workspace = true
workspace-hack.workspace = true
+
+[dev-dependencies]
+gpui = { workspace = true, features = ["test-support"] }
+project = { workspace = true, features = ["test-support"] }
@@ -140,7 +140,7 @@ impl Client {
/// This function initializes a new Client by spawning a child process for the context server,
/// setting up communication channels, and initializing handlers for input/output operations.
/// It takes a server ID, binary information, and an async app context as input.
- pub fn new(
+ pub fn stdio(
server_id: ContextServerId,
binary: ModelContextServerBinary,
cx: AsyncApp,
@@ -158,7 +158,16 @@ impl Client {
.unwrap_or_else(String::new);
let transport = Arc::new(StdioTransport::new(binary, &cx)?);
+ Self::new(server_id, server_name.into(), transport, cx)
+ }
+ /// Creates a new Client instance for a context server.
+ pub fn new(
+ server_id: ContextServerId,
+ server_name: Arc<str>,
+ transport: Arc<dyn Transport>,
+ cx: AsyncApp,
+ ) -> Result<Self> {
let (outbound_tx, outbound_rx) = channel::unbounded::<String>();
let (output_done_tx, output_done_rx) = barrier::channel();
@@ -167,7 +176,7 @@ impl Client {
let response_handlers =
Arc::new(Mutex::new(Some(HashMap::<_, ResponseHandler>::default())));
- let stdout_input_task = cx.spawn({
+ let receive_input_task = cx.spawn({
let notification_handlers = notification_handlers.clone();
let response_handlers = response_handlers.clone();
let transport = transport.clone();
@@ -177,13 +186,13 @@ impl Client {
.await
}
});
- let stderr_input_task = cx.spawn({
+ let receive_err_task = cx.spawn({
let transport = transport.clone();
- async move |_| Self::handle_stderr(transport).log_err().await
+ async move |_| Self::handle_err(transport).log_err().await
});
let input_task = cx.spawn(async move |_| {
- let (stdout, stderr) = futures::join!(stdout_input_task, stderr_input_task);
- stdout.or(stderr)
+ let (input, err) = futures::join!(receive_input_task, receive_err_task);
+ input.or(err)
});
let output_task = cx.background_spawn({
@@ -201,7 +210,7 @@ impl Client {
server_id,
notification_handlers,
response_handlers,
- name: server_name.into(),
+ name: server_name,
next_id: Default::default(),
outbound_tx,
executor: cx.background_executor().clone(),
@@ -247,7 +256,7 @@ impl Client {
/// Handles the stderr output from the context server.
/// Continuously reads and logs any error messages from the server.
- async fn handle_stderr(transport: Arc<dyn Transport>) -> anyhow::Result<()> {
+ async fn handle_err(transport: Arc<dyn Transport>) -> anyhow::Result<()> {
while let Some(err) = transport.receive_err().next().await {
log::warn!("context server stderr: {}", err.trim());
}
@@ -12,7 +12,7 @@ pub use context_server_settings::{ContextServerSettings, ServerCommand, ServerCo
use gpui::{App, actions};
pub use crate::context_server_tool::ContextServerTool;
-pub use crate::registry::ContextServerFactoryRegistry;
+pub use crate::registry::ContextServerDescriptorRegistry;
actions!(context_servers, [Restart]);
@@ -21,7 +21,7 @@ pub const CONTEXT_SERVERS_NAMESPACE: &'static str = "context_servers";
pub fn init(cx: &mut App) {
context_server_settings::init(cx);
- ContextServerFactoryRegistry::default_global(cx);
+ ContextServerDescriptorRegistry::default_global(cx);
extension_context_server::init(cx);
CommandPaletteFilter::update_global(cx, |filter, _cx| {
@@ -1,9 +1,21 @@
use std::sync::Arc;
-use extension::{Extension, ExtensionContextServerProxy, ExtensionHostProxy, ProjectDelegate};
-use gpui::{App, Entity};
+use anyhow::Result;
+use extension::{
+ ContextServerConfiguration, Extension, ExtensionContextServerProxy, ExtensionHostProxy,
+ ProjectDelegate,
+};
+use gpui::{App, AsyncApp, Entity, Task};
+use project::Project;
-use crate::{ContextServerFactoryRegistry, ServerCommand};
+use crate::{ContextServerDescriptorRegistry, ServerCommand, registry};
+
+pub fn init(cx: &mut App) {
+ let proxy = ExtensionHostProxy::default_global(cx);
+ proxy.register_context_server_proxy(ContextServerDescriptorRegistryProxy {
+ context_server_factory_registry: ContextServerDescriptorRegistry::global(cx),
+ });
+}
struct ExtensionProject {
worktree_ids: Vec<u64>,
@@ -15,60 +27,78 @@ impl ProjectDelegate for ExtensionProject {
}
}
-pub fn init(cx: &mut App) {
- let proxy = ExtensionHostProxy::default_global(cx);
- proxy.register_context_server_proxy(ContextServerFactoryRegistryProxy {
- context_server_factory_registry: ContextServerFactoryRegistry::global(cx),
- });
+struct ContextServerDescriptor {
+ id: Arc<str>,
+ extension: Arc<dyn Extension>,
}
-struct ContextServerFactoryRegistryProxy {
- context_server_factory_registry: Entity<ContextServerFactoryRegistry>,
+fn extension_project(project: Entity<Project>, cx: &mut AsyncApp) -> Result<Arc<ExtensionProject>> {
+ project.update(cx, |project, cx| {
+ Arc::new(ExtensionProject {
+ worktree_ids: project
+ .visible_worktrees(cx)
+ .map(|worktree| worktree.read(cx).id().to_proto())
+ .collect(),
+ })
+ })
}
-impl ExtensionContextServerProxy for ContextServerFactoryRegistryProxy {
- fn register_context_server(&self, extension: Arc<dyn Extension>, id: Arc<str>, cx: &mut App) {
- self.context_server_factory_registry
- .update(cx, |registry, _| {
- registry.register_server_factory(
- id.clone(),
- Arc::new({
- move |project, cx| {
- log::info!(
- "loading command for context server {id} from extension {}",
- extension.manifest().id
- );
+impl registry::ContextServerDescriptor for ContextServerDescriptor {
+ fn command(&self, project: Entity<Project>, cx: &AsyncApp) -> Task<Result<ServerCommand>> {
+ let id = self.id.clone();
+ let extension = self.extension.clone();
+ cx.spawn(async move |cx| {
+ let extension_project = extension_project(project, cx)?;
+ let mut command = extension
+ .context_server_command(id.clone(), extension_project.clone())
+ .await?;
+ command.command = extension
+ .path_from_extension(command.command.as_ref())
+ .to_string_lossy()
+ .to_string();
- let id = id.clone();
- let extension = extension.clone();
- cx.spawn(async move |cx| {
- let extension_project = project.update(cx, |project, cx| {
- Arc::new(ExtensionProject {
- worktree_ids: project
- .visible_worktrees(cx)
- .map(|worktree| worktree.read(cx).id().to_proto())
- .collect(),
- })
- })?;
+ log::info!("loaded command for context server {id}: {command:?}");
- let mut command = extension
- .context_server_command(id.clone(), extension_project)
- .await?;
- command.command = extension
- .path_from_extension(command.command.as_ref())
- .to_string_lossy()
- .to_string();
+ Ok(ServerCommand {
+ path: command.command,
+ args: command.args,
+ env: Some(command.env.into_iter().collect()),
+ })
+ })
+ }
+
+ fn configuration(
+ &self,
+ project: Entity<Project>,
+ cx: &AsyncApp,
+ ) -> Task<Result<Option<ContextServerConfiguration>>> {
+ let id = self.id.clone();
+ let extension = self.extension.clone();
+ cx.spawn(async move |cx| {
+ let extension_project = extension_project(project, cx)?;
+ let configuration = extension
+ .context_server_configuration(id.clone(), extension_project)
+ .await?;
+
+ log::debug!("loaded configuration for context server {id}: {configuration:?}");
- log::info!("loaded command for context server {id}: {command:?}");
+ Ok(configuration)
+ })
+ }
+}
- Ok(ServerCommand {
- path: command.command,
- args: command.args,
- env: Some(command.env.into_iter().collect()),
- })
- })
- }
- }),
+struct ContextServerDescriptorRegistryProxy {
+ context_server_factory_registry: Entity<ContextServerDescriptorRegistry>,
+}
+
+impl ExtensionContextServerProxy for ContextServerDescriptorRegistryProxy {
+ fn register_context_server(&self, extension: Arc<dyn Extension>, id: Arc<str>, cx: &mut App) {
+ self.context_server_factory_registry
+ .update(cx, |registry, _| {
+ registry.register_context_server_descriptor(
+ id.clone(),
+ Arc::new(ContextServerDescriptor { id, extension })
+ as Arc<dyn registry::ContextServerDescriptor>,
)
});
}
@@ -27,18 +27,27 @@ use project::Project;
use settings::{Settings, SettingsStore};
use util::ResultExt as _;
+use crate::transport::Transport;
use crate::{ContextServerSettings, ServerConfig};
use crate::{
- CONTEXT_SERVERS_NAMESPACE, ContextServerFactoryRegistry,
+ CONTEXT_SERVERS_NAMESPACE, ContextServerDescriptorRegistry,
client::{self, Client},
types,
};
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub enum ContextServerStatus {
+ Starting,
+ Running,
+ Error(Arc<str>),
+}
+
pub struct ContextServer {
pub id: Arc<str>,
pub config: Arc<ServerConfig>,
pub client: RwLock<Option<Arc<crate::protocol::InitializedContextServerProtocol>>>,
+ transport: Option<Arc<dyn Transport>>,
}
impl ContextServer {
@@ -47,9 +56,20 @@ impl ContextServer {
id,
config,
client: RwLock::new(None),
+ transport: None,
}
}
+ #[cfg(any(test, feature = "test-support"))]
+ pub fn test(id: Arc<str>, transport: Arc<dyn crate::transport::Transport>) -> Arc<Self> {
+ Arc::new(Self {
+ id,
+ client: RwLock::new(None),
+ config: Arc::new(ServerConfig::default()),
+ transport: Some(transport),
+ })
+ }
+
pub fn id(&self) -> Arc<str> {
self.id.clone()
}
@@ -63,20 +83,32 @@ impl ContextServer {
}
pub async fn start(self: Arc<Self>, cx: &AsyncApp) -> Result<()> {
- log::info!("starting context server {}", self.id);
- let Some(command) = &self.config.command else {
- bail!("no command specified for server {}", self.id);
+ let client = if let Some(transport) = self.transport.clone() {
+ Client::new(
+ client::ContextServerId(self.id.clone()),
+ self.id(),
+ transport,
+ cx.clone(),
+ )?
+ } else {
+ let Some(command) = &self.config.command else {
+ bail!("no command specified for server {}", self.id);
+ };
+ Client::stdio(
+ client::ContextServerId(self.id.clone()),
+ client::ModelContextServerBinary {
+ executable: Path::new(&command.path).to_path_buf(),
+ args: command.args.clone(),
+ env: command.env.clone(),
+ },
+ cx.clone(),
+ )?
};
- let client = Client::new(
- client::ContextServerId(self.id.clone()),
- client::ModelContextServerBinary {
- executable: Path::new(&command.path).to_path_buf(),
- args: command.args.clone(),
- env: command.env.clone(),
- },
- cx.clone(),
- )?;
+ self.initialize(client).await
+ }
+ async fn initialize(&self, client: Client) -> Result<()> {
+ log::info!("starting context server {}", self.id);
let protocol = crate::protocol::ModelContextProtocol::new(client);
let client_info = types::Implementation {
name: "Zed".to_string(),
@@ -105,23 +137,26 @@ impl ContextServer {
pub struct ContextServerManager {
servers: HashMap<Arc<str>, Arc<ContextServer>>,
+ server_status: HashMap<Arc<str>, ContextServerStatus>,
project: Entity<Project>,
- registry: Entity<ContextServerFactoryRegistry>,
+ registry: Entity<ContextServerDescriptorRegistry>,
update_servers_task: Option<Task<Result<()>>>,
needs_server_update: bool,
_subscriptions: Vec<Subscription>,
}
pub enum Event {
- ServerStarted { server_id: Arc<str> },
- ServerStopped { server_id: Arc<str> },
+ ServerStatusChanged {
+ server_id: Arc<str>,
+ status: Option<ContextServerStatus>,
+ },
}
impl EventEmitter<Event> for ContextServerManager {}
impl ContextServerManager {
pub fn new(
- registry: Entity<ContextServerFactoryRegistry>,
+ registry: Entity<ContextServerDescriptorRegistry>,
project: Entity<Project>,
cx: &mut Context<Self>,
) -> Self {
@@ -138,6 +173,7 @@ impl ContextServerManager {
registry,
needs_server_update: false,
servers: HashMap::default(),
+ server_status: HashMap::default(),
update_servers_task: None,
};
this.available_context_servers_changed(cx);
@@ -153,7 +189,9 @@ impl ContextServerManager {
this.needs_server_update = false;
})?;
- Self::maintain_servers(this.clone(), cx).await?;
+ if let Err(err) = Self::maintain_servers(this.clone(), cx).await {
+ log::error!("Error maintaining context servers: {}", err);
+ }
this.update(cx, |this, cx| {
let has_any_context_servers = !this.running_servers().is_empty();
@@ -181,52 +219,37 @@ impl ContextServerManager {
.cloned()
}
+ pub fn status_for_server(&self, id: &str) -> Option<ContextServerStatus> {
+ self.server_status.get(id).cloned()
+ }
+
pub fn start_server(
&self,
server: Arc<ContextServer>,
cx: &mut Context<Self>,
- ) -> Task<anyhow::Result<()>> {
- cx.spawn(async move |this, cx| {
- let id = server.id.clone();
- server.start(&cx).await?;
- this.update(cx, |_, cx| cx.emit(Event::ServerStarted { server_id: id }))?;
- Ok(())
- })
+ ) -> Task<Result<()>> {
+ cx.spawn(async move |this, cx| Self::run_server(this, server, cx).await)
}
pub fn stop_server(
- &self,
+ &mut self,
server: Arc<ContextServer>,
cx: &mut Context<Self>,
- ) -> anyhow::Result<()> {
- server.stop()?;
- cx.emit(Event::ServerStopped {
- server_id: server.id(),
- });
+ ) -> Result<()> {
+ server.stop().log_err();
+ self.update_server_status(server.id().clone(), None, cx);
Ok(())
}
- pub fn restart_server(
- &mut self,
- id: &Arc<str>,
- cx: &mut Context<Self>,
- ) -> Task<anyhow::Result<()>> {
+ pub fn restart_server(&mut self, id: &Arc<str>, cx: &mut Context<Self>) -> Task<Result<()>> {
let id = id.clone();
cx.spawn(async move |this, cx| {
if let Some(server) = this.update(cx, |this, _cx| this.servers.remove(&id))? {
- server.stop()?;
let config = server.config();
+
+ this.update(cx, |this, cx| this.stop_server(server, cx))??;
let new_server = Arc::new(ContextServer::new(id.clone(), config));
- new_server.clone().start(&cx).await?;
- this.update(cx, |this, cx| {
- this.servers.insert(id.clone(), new_server);
- cx.emit(Event::ServerStopped {
- server_id: id.clone(),
- });
- cx.emit(Event::ServerStarted {
- server_id: id.clone(),
- });
- })?;
+ Self::run_server(this, new_server, cx).await?;
}
Ok(())
})
@@ -263,12 +286,14 @@ impl ContextServerManager {
(this.registry.clone(), this.project.clone())
})?;
- for (id, factory) in
- registry.read_with(cx, |registry, _| registry.context_server_factories())?
+ for (id, descriptor) in
+ registry.read_with(cx, |registry, _| registry.context_server_descriptors())?
{
let config = desired_servers.entry(id).or_default();
if config.command.is_none() {
- if let Some(extension_command) = factory(project.clone(), &cx).await.log_err() {
+ if let Some(extension_command) =
+ descriptor.command(project.clone(), &cx).await.log_err()
+ {
config.command = Some(extension_command);
}
}
@@ -290,28 +315,270 @@ impl ContextServerManager {
for (id, config) in desired_servers {
let existing_config = this.servers.get(&id).map(|server| server.config());
if existing_config.as_deref() != Some(&config) {
- let config = Arc::new(config);
- let server = Arc::new(ContextServer::new(id.clone(), config));
+ let server = Arc::new(ContextServer::new(id.clone(), Arc::new(config)));
servers_to_start.insert(id.clone(), server.clone());
- let old_server = this.servers.insert(id.clone(), server);
- if let Some(old_server) = old_server {
+ if let Some(old_server) = this.servers.remove(&id) {
servers_to_stop.insert(id, old_server);
}
}
}
})?;
- for (id, server) in servers_to_stop {
- server.stop().log_err();
- this.update(cx, |_, cx| cx.emit(Event::ServerStopped { server_id: id }))?;
+ for (_, server) in servers_to_stop {
+ this.update(cx, |this, cx| this.stop_server(server, cx).ok())?;
}
- for (id, server) in servers_to_start {
- if server.start(&cx).await.log_err().is_some() {
- this.update(cx, |_, cx| cx.emit(Event::ServerStarted { server_id: id }))?;
- }
+ for (_, server) in servers_to_start {
+ Self::run_server(this.clone(), server, cx).await.ok();
}
Ok(())
}
+
+ async fn run_server(
+ this: WeakEntity<Self>,
+ server: Arc<ContextServer>,
+ cx: &mut AsyncApp,
+ ) -> Result<()> {
+ let id = server.id();
+
+ this.update(cx, |this, cx| {
+ this.update_server_status(id.clone(), Some(ContextServerStatus::Starting), cx);
+ this.servers.insert(id.clone(), server.clone());
+ })?;
+
+ match server.start(&cx).await {
+ Ok(_) => {
+ log::debug!("`{}` context server started", id);
+ this.update(cx, |this, cx| {
+ this.update_server_status(id.clone(), Some(ContextServerStatus::Running), cx)
+ })?;
+ Ok(())
+ }
+ Err(err) => {
+ log::error!("`{}` context server failed to start\n{}", id, err);
+ this.update(cx, |this, cx| {
+ this.update_server_status(
+ id.clone(),
+ Some(ContextServerStatus::Error(err.to_string().into())),
+ cx,
+ )
+ })?;
+ Err(err)
+ }
+ }
+ }
+
+ fn update_server_status(
+ &mut self,
+ id: Arc<str>,
+ status: Option<ContextServerStatus>,
+ cx: &mut Context<Self>,
+ ) {
+ if let Some(status) = status.clone() {
+ self.server_status.insert(id.clone(), status);
+ } else {
+ self.server_status.remove(&id);
+ }
+
+ cx.emit(Event::ServerStatusChanged {
+ server_id: id,
+ status,
+ });
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use std::pin::Pin;
+
+ use crate::types::{
+ Implementation, InitializeResponse, ProtocolVersion, RequestType, ServerCapabilities,
+ };
+
+ use super::*;
+ use futures::{Stream, StreamExt as _, lock::Mutex};
+ use gpui::{AppContext as _, TestAppContext};
+ use project::FakeFs;
+ use serde_json::json;
+ use util::path;
+
+ #[gpui::test]
+ async fn test_context_server_status(cx: &mut TestAppContext) {
+ init_test_settings(cx);
+ let project = create_test_project(cx, json!({"code.rs": ""})).await;
+
+ let registry = cx.new(|_| ContextServerDescriptorRegistry::new());
+ let manager = cx.new(|cx| ContextServerManager::new(registry.clone(), project, cx));
+
+ let server_1_id: Arc<str> = "mcp-1".into();
+ let server_2_id: Arc<str> = "mcp-2".into();
+
+ let transport_1 = Arc::new(FakeTransport::new(
+ |_, request_type, _| match request_type {
+ Some(RequestType::Initialize) => {
+ Some(create_initialize_response("mcp-1".to_string()))
+ }
+ _ => None,
+ },
+ ));
+
+ let transport_2 = Arc::new(FakeTransport::new(
+ |_, request_type, _| match request_type {
+ Some(RequestType::Initialize) => {
+ Some(create_initialize_response("mcp-2".to_string()))
+ }
+ _ => None,
+ },
+ ));
+
+ let server_1 = ContextServer::test(server_1_id.clone(), transport_1.clone());
+ let server_2 = ContextServer::test(server_2_id.clone(), transport_2.clone());
+
+ manager
+ .update(cx, |manager, cx| manager.start_server(server_1, cx))
+ .await
+ .unwrap();
+
+ cx.update(|cx| {
+ assert_eq!(
+ manager.read(cx).status_for_server(&server_1_id),
+ Some(ContextServerStatus::Running)
+ );
+ assert_eq!(manager.read(cx).status_for_server(&server_2_id), None);
+ });
+
+ manager
+ .update(cx, |manager, cx| manager.start_server(server_2.clone(), cx))
+ .await
+ .unwrap();
+
+ cx.update(|cx| {
+ assert_eq!(
+ manager.read(cx).status_for_server(&server_1_id),
+ Some(ContextServerStatus::Running)
+ );
+ assert_eq!(
+ manager.read(cx).status_for_server(&server_2_id),
+ Some(ContextServerStatus::Running)
+ );
+ });
+
+ manager
+ .update(cx, |manager, cx| manager.stop_server(server_2, cx))
+ .unwrap();
+
+ cx.update(|cx| {
+ assert_eq!(
+ manager.read(cx).status_for_server(&server_1_id),
+ Some(ContextServerStatus::Running)
+ );
+ assert_eq!(manager.read(cx).status_for_server(&server_2_id), None);
+ });
+ }
+
+ async fn create_test_project(
+ cx: &mut TestAppContext,
+ files: serde_json::Value,
+ ) -> Entity<Project> {
+ let fs = FakeFs::new(cx.executor());
+ fs.insert_tree(path!("/test"), files).await;
+ Project::test(fs, [path!("/test").as_ref()], cx).await
+ }
+
+ fn init_test_settings(cx: &mut TestAppContext) {
+ cx.update(|cx| {
+ let settings_store = SettingsStore::test(cx);
+ cx.set_global(settings_store);
+ Project::init_settings(cx);
+ ContextServerSettings::register(cx);
+ });
+ }
+
+ fn create_initialize_response(server_name: String) -> serde_json::Value {
+ serde_json::to_value(&InitializeResponse {
+ protocol_version: ProtocolVersion(types::LATEST_PROTOCOL_VERSION.to_string()),
+ server_info: Implementation {
+ name: server_name,
+ version: "1.0.0".to_string(),
+ },
+ capabilities: ServerCapabilities::default(),
+ meta: None,
+ })
+ .unwrap()
+ }
+
+ struct FakeTransport {
+ on_request: Arc<
+ dyn Fn(u64, Option<RequestType>, serde_json::Value) -> Option<serde_json::Value>
+ + Send
+ + Sync,
+ >,
+ tx: futures::channel::mpsc::UnboundedSender<String>,
+ rx: Arc<Mutex<futures::channel::mpsc::UnboundedReceiver<String>>>,
+ }
+
+ impl FakeTransport {
+ fn new(
+ on_request: impl Fn(
+ u64,
+ Option<RequestType>,
+ serde_json::Value,
+ ) -> Option<serde_json::Value>
+ + 'static
+ + Send
+ + Sync,
+ ) -> Self {
+ let (tx, rx) = futures::channel::mpsc::unbounded();
+ Self {
+ on_request: Arc::new(on_request),
+ tx,
+ rx: Arc::new(Mutex::new(rx)),
+ }
+ }
+ }
+
+ #[async_trait::async_trait]
+ impl Transport for FakeTransport {
+ async fn send(&self, message: String) -> Result<()> {
+ if let Ok(msg) = serde_json::from_str::<serde_json::Value>(&message) {
+ let id = msg.get("id").and_then(|id| id.as_u64()).unwrap_or(0);
+
+ if let Some(method) = msg.get("method") {
+ let request_type = method
+ .as_str()
+ .and_then(|method| types::RequestType::try_from(method).ok());
+ if let Some(payload) = (self.on_request.as_ref())(id, request_type, msg) {
+ let response = serde_json::json!({
+ "jsonrpc": "2.0",
+ "id": id,
+ "result": payload
+ });
+
+ self.tx
+ .unbounded_send(response.to_string())
+ .map_err(|e| anyhow::anyhow!("Failed to send message: {}", e))?;
+ }
+ }
+ }
+ Ok(())
+ }
+
+ fn receive(&self) -> Pin<Box<dyn Stream<Item = String> + Send>> {
+ let rx = self.rx.clone();
+ Box::pin(futures::stream::unfold(rx, |rx| async move {
+ let mut rx_guard = rx.lock().await;
+ if let Some(message) = rx_guard.next().await {
+ drop(rx_guard);
+ Some((message, rx))
+ } else {
+ None
+ }
+ }))
+ }
+
+ fn receive_err(&self) -> Pin<Box<dyn Stream<Item = String> + Send>> {
+ Box::pin(futures::stream::empty())
+ }
+ }
}
@@ -2,38 +2,47 @@ use std::sync::Arc;
use anyhow::Result;
use collections::HashMap;
+use extension::ContextServerConfiguration;
use gpui::{App, AppContext as _, AsyncApp, Entity, Global, ReadGlobal, Task};
use project::Project;
use crate::ServerCommand;
-pub type ContextServerFactory =
- Arc<dyn Fn(Entity<Project>, &AsyncApp) -> Task<Result<ServerCommand>> + Send + Sync + 'static>;
+pub trait ContextServerDescriptor {
+ fn command(&self, project: Entity<Project>, cx: &AsyncApp) -> Task<Result<ServerCommand>>;
+ fn configuration(
+ &self,
+ project: Entity<Project>,
+ cx: &AsyncApp,
+ ) -> Task<Result<Option<ContextServerConfiguration>>>;
+}
-struct GlobalContextServerFactoryRegistry(Entity<ContextServerFactoryRegistry>);
+struct GlobalContextServerDescriptorRegistry(Entity<ContextServerDescriptorRegistry>);
-impl Global for GlobalContextServerFactoryRegistry {}
+impl Global for GlobalContextServerDescriptorRegistry {}
#[derive(Default)]
-pub struct ContextServerFactoryRegistry {
- context_servers: HashMap<Arc<str>, ContextServerFactory>,
+pub struct ContextServerDescriptorRegistry {
+ context_servers: HashMap<Arc<str>, Arc<dyn ContextServerDescriptor>>,
}
-impl ContextServerFactoryRegistry {
- /// Returns the global [`ContextServerFactoryRegistry`].
+impl ContextServerDescriptorRegistry {
+ /// Returns the global [`ContextServerDescriptorRegistry`].
pub fn global(cx: &App) -> Entity<Self> {
- GlobalContextServerFactoryRegistry::global(cx).0.clone()
+ GlobalContextServerDescriptorRegistry::global(cx).0.clone()
}
- /// Returns the global [`ContextServerFactoryRegistry`].
+ /// Returns the global [`ContextServerDescriptorRegistry`].
///
- /// Inserts a default [`ContextServerFactoryRegistry`] if one does not yet exist.
+ /// Inserts a default [`ContextServerDescriptorRegistry`] if one does not yet exist.
pub fn default_global(cx: &mut App) -> Entity<Self> {
- if !cx.has_global::<GlobalContextServerFactoryRegistry>() {
+ if !cx.has_global::<GlobalContextServerDescriptorRegistry>() {
let registry = cx.new(|_| Self::new());
- cx.set_global(GlobalContextServerFactoryRegistry(registry));
+ cx.set_global(GlobalContextServerDescriptorRegistry(registry));
}
- cx.global::<GlobalContextServerFactoryRegistry>().0.clone()
+ cx.global::<GlobalContextServerDescriptorRegistry>()
+ .0
+ .clone()
}
pub fn new() -> Self {
@@ -42,20 +51,28 @@ impl ContextServerFactoryRegistry {
}
}
- pub fn context_server_factories(&self) -> Vec<(Arc<str>, ContextServerFactory)> {
+ pub fn context_server_descriptors(&self) -> Vec<(Arc<str>, Arc<dyn ContextServerDescriptor>)> {
self.context_servers
.iter()
.map(|(id, factory)| (id.clone(), factory.clone()))
.collect()
}
- /// Registers the provided [`ContextServerFactory`].
- pub fn register_server_factory(&mut self, id: Arc<str>, factory: ContextServerFactory) {
- self.context_servers.insert(id, factory);
+ pub fn context_server_descriptor(&self, id: &str) -> Option<Arc<dyn ContextServerDescriptor>> {
+ self.context_servers.get(id).cloned()
+ }
+
+ /// Registers the provided [`ContextServerDescriptor`].
+ pub fn register_context_server_descriptor(
+ &mut self,
+ id: Arc<str>,
+ descriptor: Arc<dyn ContextServerDescriptor>,
+ ) {
+ self.context_servers.insert(id, descriptor);
}
- /// Unregisters the [`ContextServerFactory`] for the server with the given ID.
- pub fn unregister_server_factory_by_id(&mut self, server_id: &str) {
+ /// Unregisters the [`ContextServerDescriptor`] for the server with the given ID.
+ pub fn unregister_context_server_descriptor_by_id(&mut self, server_id: &str) {
self.context_servers.remove(server_id);
}
}
@@ -42,6 +42,30 @@ impl RequestType {
}
}
+impl TryFrom<&str> for RequestType {
+ type Error = ();
+
+ fn try_from(s: &str) -> Result<Self, Self::Error> {
+ match s {
+ "initialize" => Ok(RequestType::Initialize),
+ "tools/call" => Ok(RequestType::CallTool),
+ "resources/unsubscribe" => Ok(RequestType::ResourcesUnsubscribe),
+ "resources/subscribe" => Ok(RequestType::ResourcesSubscribe),
+ "resources/read" => Ok(RequestType::ResourcesRead),
+ "resources/list" => Ok(RequestType::ResourcesList),
+ "logging/setLevel" => Ok(RequestType::LoggingSetLevel),
+ "prompts/get" => Ok(RequestType::PromptsGet),
+ "prompts/list" => Ok(RequestType::PromptsList),
+ "completion/complete" => Ok(RequestType::CompletionComplete),
+ "ping" => Ok(RequestType::Ping),
+ "tools/list" => Ok(RequestType::ListTools),
+ "resources/templates/list" => Ok(RequestType::ListResourceTemplates),
+ "roots/list" => Ok(RequestType::ListRoots),
+ _ => Err(()),
+ }
+ }
+}
+
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(transparent)]
pub struct ProtocolVersion(pub String);
@@ -154,7 +178,7 @@ pub struct CompletionArgument {
pub value: String,
}
-#[derive(Debug, Deserialize)]
+#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InitializeResponse {
pub protocol_version: ProtocolVersion,
@@ -343,7 +367,7 @@ pub struct ClientCapabilities {
pub roots: Option<RootsCapabilities>,
}
-#[derive(Debug, Serialize, Deserialize)]
+#[derive(Default, Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ServerCapabilities {
#[serde(skip_serializing_if = "Option::is_none")]
@@ -424,7 +424,13 @@ pub fn init(cx: &mut App) -> Arc<AgentAppState> {
prompt_store::init(cx);
let stdout_is_a_pty = false;
let prompt_builder = PromptBuilder::load(fs.clone(), stdout_is_a_pty, cx);
- agent::init(fs.clone(), client.clone(), prompt_builder.clone(), cx);
+ agent::init(
+ fs.clone(),
+ client.clone(),
+ prompt_builder.clone(),
+ languages.clone(),
+ cx,
+ );
assistant_tools::init(client.http_client(), cx);
SettingsStore::update_global(cx, |store, cx| {
@@ -121,6 +121,12 @@ pub trait Extension: Send + Sync + 'static {
project: Arc<dyn ProjectDelegate>,
) -> Result<Command>;
+ async fn context_server_configuration(
+ &self,
+ context_server_id: Arc<str>,
+ project: Arc<dyn ProjectDelegate>,
+ ) -> Result<Option<ContextServerConfiguration>>;
+
async fn suggest_docs_packages(&self, provider: Arc<str>) -> Result<Vec<String>>;
async fn index_docs(
@@ -1,5 +1,9 @@
+use std::sync::Arc;
+
use gpui::{App, AppContext as _, Context, Entity, EventEmitter, Global};
+use crate::ExtensionManifest;
+
pub fn init(cx: &mut App) {
let extension_events = cx.new(ExtensionEvents::new);
cx.set_global(GlobalExtensionEvents(extension_events));
@@ -31,7 +35,9 @@ impl ExtensionEvents {
#[derive(Clone)]
pub enum Event {
+ ExtensionInstalled(Arc<ExtensionManifest>),
ExtensionsInstalledChanged,
+ ConfigureExtensionRequested(Arc<ExtensionManifest>),
}
impl EventEmitter<Event> for ExtensionEvents {}
@@ -1,8 +1,10 @@
+mod context_server;
mod lsp;
mod slash_command;
use std::ops::Range;
+pub use context_server::*;
pub use lsp::*;
pub use slash_command::*;
@@ -0,0 +1,10 @@
+/// Configuration for a context server.
+#[derive(Debug, Clone)]
+pub struct ContextServerConfiguration {
+ /// Installation instructions for the user.
+ pub installation_instructions: String,
+ /// Default settings for the context server.
+ pub default_settings: String,
+ /// JSON schema describing server settings.
+ pub settings_schema: serde_json::Value,
+}
@@ -18,6 +18,7 @@ pub use wit::{
CodeLabel, CodeLabelSpan, CodeLabelSpanLiteral, Command, DownloadedFileType, EnvVars,
KeyValueStore, LanguageServerInstallationStatus, Project, Range, Worktree, download_file,
make_file_executable,
+ zed::extension::context_server::ContextServerConfiguration,
zed::extension::github::{
GithubRelease, GithubReleaseAsset, GithubReleaseOptions, github_release_by_tag_name,
latest_github_release,
@@ -159,6 +160,15 @@ pub trait Extension: Send + Sync {
Err("`context_server_command` not implemented".to_string())
}
+ /// Returns the configuration options for the specified context server.
+ fn context_server_configuration(
+ &mut self,
+ _context_server_id: &ContextServerId,
+ _project: &Project,
+ ) -> Result<Option<ContextServerConfiguration>> {
+ Ok(None)
+ }
+
/// Returns a list of package names as suggestions to be included in the
/// search results of the `/docs` slash command.
///
@@ -342,6 +352,14 @@ impl wit::Guest for Component {
extension().context_server_command(&context_server_id, project)
}
+ fn context_server_configuration(
+ context_server_id: String,
+ project: &Project,
+ ) -> Result<Option<ContextServerConfiguration>, String> {
+ let context_server_id = ContextServerId(context_server_id);
+ extension().context_server_configuration(&context_server_id, project)
+ }
+
fn suggest_docs_packages(provider: String) -> Result<Vec<String>, String> {
extension().suggest_docs_packages(provider)
}
@@ -0,0 +1,11 @@
+interface context-server {
+ ///
+ record context-server-configuration {
+ ///
+ installation-instructions: string,
+ ///
+ settings-schema: string,
+ ///
+ default-settings: string,
+ }
+}
@@ -1,6 +1,7 @@
package zed:extension;
world extension {
+ import context-server;
import github;
import http-client;
import platform;
@@ -8,6 +9,7 @@ world extension {
import nodejs;
use common.{env-vars, range};
+ use context-server.{context-server-configuration};
use lsp.{completion, symbol};
use process.{command};
use slash-command.{slash-command, slash-command-argument-completion, slash-command-output};
@@ -139,6 +141,9 @@ world extension {
/// Returns the command used to start up a context server.
export context-server-command: func(context-server-id: string, project: borrow<project>) -> result<command, string>;
+ /// Returns the configuration for a context server.
+ export context-server-configuration: func(context-server-id: string, project: borrow<project>) -> result<option<context-server-configuration>, string>;
+
/// Returns a list of packages as suggestions to be included in the `/docs`
/// search results.
///
@@ -431,6 +431,13 @@ impl ExtensionStore {
.filter_map(|extension| extension.dev.then_some(&extension.manifest))
}
+ pub fn extension_manifest_for_id(&self, extension_id: &str) -> Option<&Arc<ExtensionManifest>> {
+ self.extension_index
+ .extensions
+ .get(extension_id)
+ .map(|extension| &extension.manifest)
+ }
+
/// Returns the names of themes provided by extensions.
pub fn extension_themes<'a>(
&'a self,
@@ -744,8 +751,18 @@ impl ExtensionStore {
.await;
if let ExtensionOperation::Install = operation {
- this.update( cx, |_, cx| {
- cx.emit(Event::ExtensionInstalled(extension_id));
+ this.update( cx, |this, cx| {
+ cx.emit(Event::ExtensionInstalled(extension_id.clone()));
+ if let Some(events) = ExtensionEvents::try_global(cx) {
+ if let Some(manifest) = this.extension_manifest_for_id(&extension_id) {
+ events.update(cx, |this, cx| {
+ this.emit(
+ extension::Event::ExtensionInstalled(manifest.clone()),
+ cx,
+ )
+ });
+ }
+ }
})
.ok();
}
@@ -935,6 +952,17 @@ impl ExtensionStore {
.await?;
this.update(cx, |this, cx| this.reload(None, cx))?.await;
+ this.update(cx, |this, cx| {
+ cx.emit(Event::ExtensionInstalled(extension_id.clone()));
+ if let Some(events) = ExtensionEvents::try_global(cx) {
+ if let Some(manifest) = this.extension_manifest_for_id(&extension_id) {
+ events.update(cx, |this, cx| {
+ this.emit(extension::Event::ExtensionInstalled(manifest.clone()), cx)
+ });
+ }
+ }
+ })?;
+
Ok(())
})
}
@@ -4,8 +4,9 @@ use crate::ExtensionManifest;
use anyhow::{Context as _, Result, anyhow, bail};
use async_trait::async_trait;
use extension::{
- CodeLabel, Command, Completion, ExtensionHostProxy, KeyValueStoreDelegate, ProjectDelegate,
- SlashCommand, SlashCommandArgumentCompletion, SlashCommandOutput, Symbol, WorktreeDelegate,
+ CodeLabel, Command, Completion, ContextServerConfiguration, ExtensionHostProxy,
+ KeyValueStoreDelegate, ProjectDelegate, SlashCommand, SlashCommandArgumentCompletion,
+ SlashCommandOutput, Symbol, WorktreeDelegate,
};
use fs::{Fs, normalize_path};
use futures::future::LocalBoxFuture;
@@ -306,6 +307,33 @@ impl extension::Extension for WasmExtension {
.await
}
+ async fn context_server_configuration(
+ &self,
+ context_server_id: Arc<str>,
+ project: Arc<dyn ProjectDelegate>,
+ ) -> Result<Option<ContextServerConfiguration>> {
+ self.call(|extension, store| {
+ async move {
+ let project_resource = store.data_mut().table().push(project)?;
+ let Some(configuration) = extension
+ .call_context_server_configuration(
+ store,
+ context_server_id.clone(),
+ project_resource,
+ )
+ .await?
+ .map_err(|err| anyhow!("{err}"))?
+ else {
+ return Ok(None);
+ };
+
+ Ok(Some(configuration.try_into()?))
+ }
+ .boxed()
+ })
+ .await
+ }
+
async fn suggest_docs_packages(&self, provider: Arc<str>) -> Result<Vec<String>> {
self.call(|extension, store| {
async move {
@@ -25,6 +25,7 @@ use wasmtime::{
pub use latest::CodeLabelSpanLiteral;
pub use latest::{
CodeLabel, CodeLabelSpan, Command, ExtensionProject, Range, SlashCommand,
+ zed::extension::context_server::ContextServerConfiguration,
zed::extension::lsp::{
Completion, CompletionKind, CompletionLabelDetails, InsertTextFormat, Symbol, SymbolKind,
},
@@ -726,6 +727,29 @@ impl Extension {
}
}
+ pub async fn call_context_server_configuration(
+ &self,
+ store: &mut Store<WasmState>,
+ context_server_id: Arc<str>,
+ project: Resource<ExtensionProject>,
+ ) -> Result<Result<Option<ContextServerConfiguration>, String>> {
+ match self {
+ Extension::V0_5_0(ext) => {
+ ext.call_context_server_configuration(store, &context_server_id, project)
+ .await
+ }
+ Extension::V0_0_1(_)
+ | Extension::V0_0_4(_)
+ | Extension::V0_0_6(_)
+ | Extension::V0_1_0(_)
+ | Extension::V0_2_0(_)
+ | Extension::V0_3_0(_)
+ | Extension::V0_4_0(_) => Err(anyhow!(
+ "`context_server_configuration` not available prior to v0.5.0"
+ )),
+ }
+ }
+
pub async fn call_suggest_docs_packages(
&self,
store: &mut Store<WasmState>,
@@ -247,6 +247,21 @@ impl From<SlashCommandArgumentCompletion> for extension::SlashCommandArgumentCom
}
}
+impl TryFrom<ContextServerConfiguration> for extension::ContextServerConfiguration {
+ type Error = anyhow::Error;
+
+ fn try_from(value: ContextServerConfiguration) -> Result<Self, Self::Error> {
+ let settings_schema: serde_json::Value = serde_json::from_str(&value.settings_schema)
+ .context("Failed to parse settings_schema")?;
+
+ Ok(Self {
+ installation_instructions: value.installation_instructions,
+ default_settings: value.default_settings,
+ settings_schema,
+ })
+ }
+}
+
impl HostKeyValueStore for WasmState {
async fn insert(
&mut self,
@@ -610,6 +625,9 @@ impl process::Host for WasmState {
#[async_trait]
impl slash_command::Host for WasmState {}
+#[async_trait]
+impl context_server::Host for WasmState {}
+
impl ExtensionImports for WasmState {
async fn get_settings(
&mut self,
@@ -17,6 +17,7 @@ client.workspace = true
collections.workspace = true
db.workspace = true
editor.workspace = true
+extension.workspace = true
extension_host.workspace = true
fs.workspace = true
fuzzy.workspace = true
@@ -246,6 +246,12 @@ fn keywords_by_feature() -> &'static BTreeMap<Feature, Vec<&'static str>> {
})
}
+struct ExtensionCardButtons {
+ install_or_uninstall: Button,
+ upgrade: Option<Button>,
+ configure: Option<Button>,
+}
+
pub struct ExtensionsPage {
workspace: WeakEntity<Workspace>,
list: UniformListScrollHandle,
@@ -522,6 +528,8 @@ impl ExtensionsPage {
let repository_url = extension.repository.clone();
+ let can_configure = !extension.context_servers.is_empty();
+
ExtensionCard::new()
.child(
h_flex()
@@ -568,7 +576,36 @@ impl ExtensionsPage {
})
.color(Color::Accent)
.disabled(matches!(status, ExtensionStatus::Removing)),
- ),
+ )
+ .when(can_configure, |this| {
+ this.child(
+ Button::new(
+ SharedString::from(format!("configure-{}", extension.id)),
+ "Configure",
+ )
+
+
+ .on_click({
+ let manifest = Arc::new(extension.clone());
+ move |_, _, cx| {
+ if let Some(events) =
+ extension::ExtensionEvents::try_global(cx)
+ {
+ events.update(cx, |this, cx| {
+ this.emit(
+ extension::Event::ConfigureExtensionRequested(
+ manifest.clone(),
+ ),
+ cx,
+ )
+ });
+ }
+ }
+ })
+ .color(Color::Accent)
+ .disabled(matches!(status, ExtensionStatus::Installing)),
+ )
+ }),
),
)
.child(
@@ -629,8 +666,7 @@ impl ExtensionsPage {
let has_dev_extension = Self::dev_extension_exists(&extension.id, cx);
let extension_id = extension.id.clone();
- let (install_or_uninstall_button, upgrade_button) =
- self.buttons_for_entry(extension, &status, has_dev_extension, cx);
+ let buttons = self.buttons_for_entry(extension, &status, has_dev_extension, cx);
let version = extension.manifest.version.clone();
let repository_url = extension.manifest.repository.clone();
let authors = extension.manifest.authors.clone();
@@ -695,8 +731,9 @@ impl ExtensionsPage {
h_flex()
.gap_2()
.justify_between()
- .children(upgrade_button)
- .child(install_or_uninstall_button),
+ .children(buttons.upgrade)
+ .children(buttons.configure)
+ .child(buttons.install_or_uninstall),
),
)
.child(
@@ -861,22 +898,35 @@ impl ExtensionsPage {
status: &ExtensionStatus,
has_dev_extension: bool,
cx: &mut Context<Self>,
- ) -> (Button, Option<Button>) {
+ ) -> ExtensionCardButtons {
let is_compatible =
extension_host::is_version_compatible(ReleaseChannel::global(cx), extension);
if has_dev_extension {
// If we have a dev extension for the given extension, just treat it as uninstalled.
// The button here is a placeholder, as it won't be interactable anyways.
- return (
- Button::new(SharedString::from(extension.id.clone()), "Install"),
- None,
- );
+ return ExtensionCardButtons {
+ install_or_uninstall: Button::new(
+ SharedString::from(extension.id.clone()),
+ "Install",
+ ),
+ configure: None,
+ upgrade: None,
+ };
}
+ let is_configurable = extension
+ .manifest
+ .provides
+ .contains(&ExtensionProvides::ContextServers);
+
match status.clone() {
- ExtensionStatus::NotInstalled => (
- Button::new(SharedString::from(extension.id.clone()), "Install").on_click({
+ ExtensionStatus::NotInstalled => ExtensionCardButtons {
+ install_or_uninstall: Button::new(
+ SharedString::from(extension.id.clone()),
+ "Install",
+ )
+ .on_click({
let extension_id = extension.id.clone();
move |_, _, cx| {
telemetry::event!("Extension Installed");
@@ -885,20 +935,41 @@ impl ExtensionsPage {
});
}
}),
- None,
- ),
- ExtensionStatus::Installing => (
- Button::new(SharedString::from(extension.id.clone()), "Install").disabled(true),
- None,
- ),
- ExtensionStatus::Upgrading => (
- Button::new(SharedString::from(extension.id.clone()), "Uninstall").disabled(true),
- Some(
+ configure: None,
+ upgrade: None,
+ },
+ ExtensionStatus::Installing => ExtensionCardButtons {
+ install_or_uninstall: Button::new(
+ SharedString::from(extension.id.clone()),
+ "Install",
+ )
+ .disabled(true),
+ configure: None,
+ upgrade: None,
+ },
+ ExtensionStatus::Upgrading => ExtensionCardButtons {
+ install_or_uninstall: Button::new(
+ SharedString::from(extension.id.clone()),
+ "Uninstall",
+ )
+ .disabled(true),
+ configure: is_configurable.then(|| {
+ Button::new(
+ SharedString::from(format!("configure-{}", extension.id.clone())),
+ "Configure",
+ )
+ .disabled(true)
+ }),
+ upgrade: Some(
Button::new(SharedString::from(extension.id.clone()), "Upgrade").disabled(true),
),
- ),
- ExtensionStatus::Installed(installed_version) => (
- Button::new(SharedString::from(extension.id.clone()), "Uninstall").on_click({
+ },
+ ExtensionStatus::Installed(installed_version) => ExtensionCardButtons {
+ install_or_uninstall: Button::new(
+ SharedString::from(extension.id.clone()),
+ "Uninstall",
+ )
+ .on_click({
let extension_id = extension.id.clone();
move |_, _, cx| {
telemetry::event!("Extension Uninstalled", extension_id);
@@ -907,7 +978,32 @@ impl ExtensionsPage {
});
}
}),
- if installed_version == extension.manifest.version {
+ configure: is_configurable.then(|| {
+ Button::new(
+ SharedString::from(format!("configure-{}", extension.id.clone())),
+ "Configure",
+ )
+ .on_click({
+ let extension_id = extension.id.clone();
+ move |_, _, cx| {
+ if let Some(manifest) = ExtensionStore::global(cx)
+ .read(cx)
+ .extension_manifest_for_id(&extension_id)
+ .cloned()
+ {
+ if let Some(events) = extension::ExtensionEvents::try_global(cx) {
+ events.update(cx, |this, cx| {
+ this.emit(
+ extension::Event::ConfigureExtensionRequested(manifest),
+ cx,
+ )
+ });
+ }
+ }
+ }
+ })
+ }),
+ upgrade: if installed_version == extension.manifest.version {
None
} else {
Some(
@@ -944,11 +1040,22 @@ impl ExtensionsPage {
}),
)
},
- ),
- ExtensionStatus::Removing => (
- Button::new(SharedString::from(extension.id.clone()), "Uninstall").disabled(true),
- None,
- ),
+ },
+ ExtensionStatus::Removing => ExtensionCardButtons {
+ install_or_uninstall: Button::new(
+ SharedString::from(extension.id.clone()),
+ "Uninstall",
+ )
+ .disabled(true),
+ configure: is_configurable.then(|| {
+ Button::new(
+ SharedString::from(format!("configure-{}", extension.id.clone())),
+ "Configure",
+ )
+ .disabled(true)
+ }),
+ upgrade: None,
+ },
}
}
@@ -3791,13 +3791,11 @@ impl LspStore {
evt: &extension::Event,
cx: &mut Context<Self>,
) {
- #[expect(
- irrefutable_let_patterns,
- reason = "Make sure to handle new event types in extension properly"
- )]
- let extension::Event::ExtensionsInstalledChanged = evt else {
- return;
- };
+ match evt {
+ extension::Event::ExtensionInstalled(_)
+ | extension::Event::ConfigureExtensionRequested(_) => return,
+ extension::Event::ExtensionsInstalledChanged => {}
+ }
if self.as_local().is_none() {
return;
}
@@ -513,6 +513,7 @@ fn main() {
app_state.fs.clone(),
app_state.client.clone(),
prompt_builder.clone(),
+ app_state.languages.clone(),
cx,
);
assistant_tools::init(app_state.client.http_client(), cx);
@@ -15,6 +15,7 @@ publish = false
### BEGIN HAKARI SECTION
[dependencies]
+ahash = { version = "0.8", features = ["serde"] }
aho-corasick = { version = "1" }
anstream = { version = "0.6" }
arrayvec = { version = "0.7", features = ["serde"] }
@@ -65,6 +66,7 @@ hashbrown-3575ec1268b04181 = { package = "hashbrown", version = "0.15", features
hashbrown-582f2526e08bb6a0 = { package = "hashbrown", version = "0.14", features = ["raw"] }
hmac = { version = "0.12", default-features = false, features = ["reset"] }
hyper = { version = "0.14", features = ["client", "http1", "http2", "runtime", "server", "stream"] }
+idna = { version = "1" }
indexmap = { version = "2", features = ["serde"] }
lazy_static = { version = "1", default-features = false, features = ["spin_no_std"] }
libc = { version = "0.2", features = ["extra_traits"] }
@@ -78,6 +80,8 @@ miniz_oxide = { version = "0.8", features = ["simd"] }
nom = { version = "7" }
num-bigint = { version = "0.4" }
num-integer = { version = "0.1", features = ["i128"] }
+num-iter = { version = "0.1", default-features = false, features = ["i128", "std"] }
+num-rational = { version = "0.4", features = ["num-bigint-std"] }
num-traits = { version = "0.2", features = ["i128", "libm"] }
once_cell = { version = "1" }
percent-encoding = { version = "2" }
@@ -125,6 +129,7 @@ wasmtime-cranelift = { version = "29", default-features = false, features = ["co
wasmtime-environ = { version = "29", default-features = false, features = ["compile", "component-model", "demangle", "gc-drc"] }
[build-dependencies]
+ahash = { version = "0.8", features = ["serde"] }
aho-corasick = { version = "1" }
anstream = { version = "0.6" }
arrayvec = { version = "0.7", features = ["serde"] }
@@ -177,6 +182,7 @@ hashbrown-582f2526e08bb6a0 = { package = "hashbrown", version = "0.14", features
heck = { version = "0.4", features = ["unicode"] }
hmac = { version = "0.12", default-features = false, features = ["reset"] }
hyper = { version = "0.14", features = ["client", "http1", "http2", "runtime", "server", "stream"] }
+idna = { version = "1" }
indexmap = { version = "2", features = ["serde"] }
itertools-594e8ee84c453af0 = { package = "itertools", version = "0.13" }
lazy_static = { version = "1", default-features = false, features = ["spin_no_std"] }
@@ -191,6 +197,8 @@ miniz_oxide = { version = "0.8", features = ["simd"] }
nom = { version = "7" }
num-bigint = { version = "0.4" }
num-integer = { version = "0.1", features = ["i128"] }
+num-iter = { version = "0.1", default-features = false, features = ["i128", "std"] }
+num-rational = { version = "0.4", features = ["num-bigint-std"] }
num-traits = { version = "0.2", features = ["i128", "libm"] }
once_cell = { version = "1" }
percent-encoding = { version = "2" }
@@ -352,7 +360,7 @@ tower = { version = "0.5", default-features = false, features = ["timeout", "uti
[target.x86_64-unknown-linux-gnu.dependencies]
aes = { version = "0.8", default-features = false, features = ["zeroize"] }
-ahash = { version = "0.8", default-features = false, features = ["compile-time-rng", "std"] }
+ahash = { version = "0.8", default-features = false, features = ["compile-time-rng"] }
bytemuck = { version = "1", default-features = false, features = ["min_const_generics"] }
cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] }
crypto-common = { version = "0.1", default-features = false, features = ["rand_core", "std"] }
@@ -372,7 +380,6 @@ mio = { version = "1", features = ["net", "os-ext"] }
naga = { version = "23", features = ["spv-out", "wgsl-in"] }
nix = { version = "0.29", features = ["fs", "pthread", "signal", "socket", "uio", "user"] }
num-bigint-dig = { version = "0.8", features = ["i128", "prime", "zeroize"] }
-num-iter = { version = "0.1", default-features = false, features = ["i128", "std"] }
object = { version = "0.36", default-features = false, features = ["archive", "read_core", "unaligned", "write"] }
proc-macro2 = { version = "1", features = ["span-locations"] }
quote = { version = "1" }
@@ -395,7 +402,7 @@ zvariant = { version = "5", default-features = false, features = ["enumflags2",
[target.x86_64-unknown-linux-gnu.build-dependencies]
aes = { version = "0.8", default-features = false, features = ["zeroize"] }
-ahash = { version = "0.8", default-features = false, features = ["compile-time-rng", "std"] }
+ahash = { version = "0.8", default-features = false, features = ["compile-time-rng"] }
bytemuck = { version = "1", default-features = false, features = ["min_const_generics"] }
cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] }
crypto-common = { version = "0.1", default-features = false, features = ["rand_core", "std"] }
@@ -415,7 +422,6 @@ mio = { version = "1", features = ["net", "os-ext"] }
naga = { version = "23", features = ["spv-out", "wgsl-in"] }
nix = { version = "0.29", features = ["fs", "pthread", "signal", "socket", "uio", "user"] }
num-bigint-dig = { version = "0.8", features = ["i128", "prime", "zeroize"] }
-num-iter = { version = "0.1", default-features = false, features = ["i128", "std"] }
object = { version = "0.36", default-features = false, features = ["archive", "read_core", "unaligned", "write"] }
proc-macro2 = { version = "1", default-features = false, features = ["span-locations"] }
rand-274715c4dabd11b0 = { package = "rand", version = "0.9" }
@@ -436,7 +442,7 @@ zvariant = { version = "5", default-features = false, features = ["enumflags2",
[target.aarch64-unknown-linux-gnu.dependencies]
aes = { version = "0.8", default-features = false, features = ["zeroize"] }
-ahash = { version = "0.8", default-features = false, features = ["compile-time-rng", "std"] }
+ahash = { version = "0.8", default-features = false, features = ["compile-time-rng"] }
bytemuck = { version = "1", default-features = false, features = ["min_const_generics"] }
cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] }
crypto-common = { version = "0.1", default-features = false, features = ["rand_core", "std"] }
@@ -456,7 +462,6 @@ mio = { version = "1", features = ["net", "os-ext"] }
naga = { version = "23", features = ["spv-out", "wgsl-in"] }
nix = { version = "0.29", features = ["fs", "pthread", "signal", "socket", "uio", "user"] }
num-bigint-dig = { version = "0.8", features = ["i128", "prime", "zeroize"] }
-num-iter = { version = "0.1", default-features = false, features = ["i128", "std"] }
object = { version = "0.36", default-features = false, features = ["archive", "read_core", "unaligned", "write"] }
proc-macro2 = { version = "1", features = ["span-locations"] }
quote = { version = "1" }
@@ -479,7 +484,7 @@ zvariant = { version = "5", default-features = false, features = ["enumflags2",
[target.aarch64-unknown-linux-gnu.build-dependencies]
aes = { version = "0.8", default-features = false, features = ["zeroize"] }
-ahash = { version = "0.8", default-features = false, features = ["compile-time-rng", "std"] }
+ahash = { version = "0.8", default-features = false, features = ["compile-time-rng"] }
bytemuck = { version = "1", default-features = false, features = ["min_const_generics"] }
cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] }
crypto-common = { version = "0.1", default-features = false, features = ["rand_core", "std"] }
@@ -499,7 +504,6 @@ mio = { version = "1", features = ["net", "os-ext"] }
naga = { version = "23", features = ["spv-out", "wgsl-in"] }
nix = { version = "0.29", features = ["fs", "pthread", "signal", "socket", "uio", "user"] }
num-bigint-dig = { version = "0.8", features = ["i128", "prime", "zeroize"] }
-num-iter = { version = "0.1", default-features = false, features = ["i128", "std"] }
object = { version = "0.36", default-features = false, features = ["archive", "read_core", "unaligned", "write"] }
proc-macro2 = { version = "1", default-features = false, features = ["span-locations"] }
rand-274715c4dabd11b0 = { package = "rand", version = "0.9" }
@@ -569,7 +573,7 @@ windows-sys-c8eced492e86ede7 = { package = "windows-sys", version = "0.48", feat
[target.x86_64-unknown-linux-musl.dependencies]
aes = { version = "0.8", default-features = false, features = ["zeroize"] }
-ahash = { version = "0.8", default-features = false, features = ["compile-time-rng", "std"] }
+ahash = { version = "0.8", default-features = false, features = ["compile-time-rng"] }
bytemuck = { version = "1", default-features = false, features = ["min_const_generics"] }
cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] }
crypto-common = { version = "0.1", default-features = false, features = ["rand_core", "std"] }
@@ -589,7 +593,6 @@ mio = { version = "1", features = ["net", "os-ext"] }
naga = { version = "23", features = ["spv-out", "wgsl-in"] }
nix = { version = "0.29", features = ["fs", "pthread", "signal", "socket", "uio", "user"] }
num-bigint-dig = { version = "0.8", features = ["i128", "prime", "zeroize"] }
-num-iter = { version = "0.1", default-features = false, features = ["i128", "std"] }
object = { version = "0.36", default-features = false, features = ["archive", "read_core", "unaligned", "write"] }
proc-macro2 = { version = "1", features = ["span-locations"] }
quote = { version = "1" }
@@ -612,7 +615,7 @@ zvariant = { version = "5", default-features = false, features = ["enumflags2",
[target.x86_64-unknown-linux-musl.build-dependencies]
aes = { version = "0.8", default-features = false, features = ["zeroize"] }
-ahash = { version = "0.8", default-features = false, features = ["compile-time-rng", "std"] }
+ahash = { version = "0.8", default-features = false, features = ["compile-time-rng"] }
bytemuck = { version = "1", default-features = false, features = ["min_const_generics"] }
cipher = { version = "0.4", default-features = false, features = ["block-padding", "rand_core", "zeroize"] }
crypto-common = { version = "0.1", default-features = false, features = ["rand_core", "std"] }
@@ -632,7 +635,6 @@ mio = { version = "1", features = ["net", "os-ext"] }
naga = { version = "23", features = ["spv-out", "wgsl-in"] }
nix = { version = "0.29", features = ["fs", "pthread", "signal", "socket", "uio", "user"] }
num-bigint-dig = { version = "0.8", features = ["i128", "prime", "zeroize"] }
-num-iter = { version = "0.1", default-features = false, features = ["i128", "std"] }
object = { version = "0.36", default-features = false, features = ["archive", "read_core", "unaligned", "write"] }
proc-macro2 = { version = "1", default-features = false, features = ["span-locations"] }
rand-274715c4dabd11b0 = { package = "rand", version = "0.9" }