1016 lines · 34.3 KB · Rust
RawDownload
pub mod extension;pub mod registry; use std::path::Path;use std::sync::Arc;use std::time::Duration; use anyhow::{Context as _, Result};use collections::{HashMap, HashSet};use context_server::{ContextServer, ContextServerCommand, ContextServerId};use futures::{FutureExt as _, future::Either, future::join_all};use gpui::{App, AsyncApp, Context, Entity, EventEmitter, Subscription, Task, WeakEntity, actions};use itertools::Itertools;use registry::ContextServerDescriptorRegistry;use remote::RemoteClient;use rpc::{AnyProtoClient, TypedEnvelope, proto};use settings::{Settings as _, SettingsStore};use util::{ResultExt as _, rel_path::RelPath}; use crate::{    DisableAiSettings, Project,    project_settings::{ContextServerSettings, ProjectSettings},    worktree_store::WorktreeStore,}; /// Maximum timeout for context server requests/// Prevents extremely large timeout values from tying up resources indefinitely.const MAX_TIMEOUT_SECS: u64 = 600; // 10 minutes pub fn init(cx: &mut App) {    extension::init(cx);} actions!(    context_server,    [        /// Restarts the context server.        Restart    ]); #[derive(Debug, Clone, PartialEq, Eq, Hash)]pub enum ContextServerStatus {    Starting,    Running,    Stopped,    Error(Arc<str>),} impl ContextServerStatus {    fn from_state(state: &ContextServerState) -> Self {        match state {            ContextServerState::Starting { .. } => ContextServerStatus::Starting,            ContextServerState::Running { .. } => ContextServerStatus::Running,            ContextServerState::Stopped { .. } => ContextServerStatus::Stopped,            ContextServerState::Error { error, .. } => ContextServerStatus::Error(error.clone()),        }    }} enum ContextServerState {    Starting {        server: Arc<ContextServer>,        configuration: Arc<ContextServerConfiguration>,        _task: Task<()>,    },    Running {        server: Arc<ContextServer>,        configuration: Arc<ContextServerConfiguration>,    },    Stopped {        server: Arc<ContextServer>,        configuration: Arc<ContextServerConfiguration>,    },    Error {        server: Arc<ContextServer>,        configuration: Arc<ContextServerConfiguration>,        error: Arc<str>,    },} impl ContextServerState {    pub fn server(&self) -> Arc<ContextServer> {        match self {            ContextServerState::Starting { server, .. } => server.clone(),            ContextServerState::Running { server, .. } => server.clone(),            ContextServerState::Stopped { server, .. } => server.clone(),            ContextServerState::Error { server, .. } => server.clone(),        }    }     pub fn configuration(&self) -> Arc<ContextServerConfiguration> {        match self {            ContextServerState::Starting { configuration, .. } => configuration.clone(),            ContextServerState::Running { configuration, .. } => configuration.clone(),            ContextServerState::Stopped { configuration, .. } => configuration.clone(),            ContextServerState::Error { configuration, .. } => configuration.clone(),        }    }} #[derive(Debug, PartialEq, Eq)]pub enum ContextServerConfiguration {    Custom {        command: ContextServerCommand,        remote: bool,    },    Extension {        command: ContextServerCommand,        settings: serde_json::Value,        remote: bool,    },    Http {        url: url::Url,        headers: HashMap<String, String>,        timeout: Option<u64>,    },} impl ContextServerConfiguration {    pub fn command(&self) -> Option<&ContextServerCommand> {        match self {            ContextServerConfiguration::Custom { command, .. } => Some(command),            ContextServerConfiguration::Extension { command, .. } => Some(command),            ContextServerConfiguration::Http { .. } => None,        }    }     pub fn remote(&self) -> bool {        match self {            ContextServerConfiguration::Custom { remote, .. } => *remote,            ContextServerConfiguration::Extension { remote, .. } => *remote,            ContextServerConfiguration::Http { .. } => false,        }    }     pub async fn from_settings(        settings: ContextServerSettings,        id: ContextServerId,        registry: Entity<ContextServerDescriptorRegistry>,        worktree_store: Entity<WorktreeStore>,        cx: &AsyncApp,    ) -> Option<Self> {        const EXTENSION_COMMAND_TIMEOUT: Duration = Duration::from_secs(30);         match settings {            ContextServerSettings::Stdio {                enabled: _,                command,                remote,            } => Some(ContextServerConfiguration::Custom { command, remote }),            ContextServerSettings::Extension {                enabled: _,                settings,                remote,            } => {                let descriptor =                    cx.update(|cx| registry.read(cx).context_server_descriptor(&id.0))?;                 let command_future = descriptor.command(worktree_store, cx);                let timeout_future = cx.background_executor().timer(EXTENSION_COMMAND_TIMEOUT);                 match futures::future::select(command_future, timeout_future).await {                    Either::Left((Ok(command), _)) => Some(ContextServerConfiguration::Extension {                        command,                        settings,                        remote,                    }),                    Either::Left((Err(e), _)) => {                        log::error!(                            "Failed to create context server configuration from settings: {e:#}"                        );                        None                    }                    Either::Right(_) => {                        log::error!(                            "Timed out resolving command for extension context server {id}"                        );                        None                    }                }            }            ContextServerSettings::Http {                enabled: _,                url,                headers: auth,                timeout,            } => {                let url = url::Url::parse(&url).log_err()?;                Some(ContextServerConfiguration::Http {                    url,                    headers: auth,                    timeout,                })            }        }    }} pub type ContextServerFactory =    Box<dyn Fn(ContextServerId, Arc<ContextServerConfiguration>) -> Arc<ContextServer>>; enum ContextServerStoreState {    Local {        downstream_client: Option<(u64, AnyProtoClient)>,        is_headless: bool,    },    Remote {        project_id: u64,        upstream_client: Entity<RemoteClient>,    },} pub struct ContextServerStore {    state: ContextServerStoreState,    context_server_settings: HashMap<Arc<str>, ContextServerSettings>,    servers: HashMap<ContextServerId, ContextServerState>,    server_ids: Vec<ContextServerId>,    worktree_store: Entity<WorktreeStore>,    project: Option<WeakEntity<Project>>,    registry: Entity<ContextServerDescriptorRegistry>,    update_servers_task: Option<Task<Result<()>>>,    context_server_factory: Option<ContextServerFactory>,    needs_server_update: bool,    ai_disabled: bool,    _subscriptions: Vec<Subscription>,} pub struct ServerStatusChangedEvent {    pub server_id: ContextServerId,    pub status: ContextServerStatus,} impl EventEmitter<ServerStatusChangedEvent> for ContextServerStore {} impl ContextServerStore {    pub fn local(        worktree_store: Entity<WorktreeStore>,        weak_project: Option<WeakEntity<Project>>,        headless: bool,        cx: &mut Context<Self>,    ) -> Self {        Self::new_internal(            !headless,            None,            ContextServerDescriptorRegistry::default_global(cx),            worktree_store,            weak_project,            ContextServerStoreState::Local {                downstream_client: None,                is_headless: headless,            },            cx,        )    }     pub fn remote(        project_id: u64,        upstream_client: Entity<RemoteClient>,        worktree_store: Entity<WorktreeStore>,        weak_project: Option<WeakEntity<Project>>,        cx: &mut Context<Self>,    ) -> Self {        Self::new_internal(            true,            None,            ContextServerDescriptorRegistry::default_global(cx),            worktree_store,            weak_project,            ContextServerStoreState::Remote {                project_id,                upstream_client,            },            cx,        )    }     pub fn init_headless(session: &AnyProtoClient) {        session.add_entity_request_handler(Self::handle_get_context_server_command);    }     pub fn shared(&mut self, project_id: u64, client: AnyProtoClient) {        if let ContextServerStoreState::Local {            downstream_client, ..        } = &mut self.state        {            *downstream_client = Some((project_id, client));        }    }     pub fn is_remote_project(&self) -> bool {        matches!(self.state, ContextServerStoreState::Remote { .. })    }     /// Returns all configured context server ids, excluding the ones that are disabled    pub fn configured_server_ids(&self) -> Vec<ContextServerId> {        self.context_server_settings            .iter()            .filter(|(_, settings)| settings.enabled())            .map(|(id, _)| ContextServerId(id.clone()))            .collect()    }     #[cfg(feature = "test-support")]    pub fn test(        registry: Entity<ContextServerDescriptorRegistry>,        worktree_store: Entity<WorktreeStore>,        weak_project: Option<WeakEntity<Project>>,        cx: &mut Context<Self>,    ) -> Self {        Self::new_internal(            false,            None,            registry,            worktree_store,            weak_project,            ContextServerStoreState::Local {                downstream_client: None,                is_headless: false,            },            cx,        )    }     #[cfg(feature = "test-support")]    pub fn test_maintain_server_loop(        context_server_factory: Option<ContextServerFactory>,        registry: Entity<ContextServerDescriptorRegistry>,        worktree_store: Entity<WorktreeStore>,        weak_project: Option<WeakEntity<Project>>,        cx: &mut Context<Self>,    ) -> Self {        Self::new_internal(            true,            context_server_factory,            registry,            worktree_store,            weak_project,            ContextServerStoreState::Local {                downstream_client: None,                is_headless: false,            },            cx,        )    }     #[cfg(feature = "test-support")]    pub fn set_context_server_factory(&mut self, factory: ContextServerFactory) {        self.context_server_factory = Some(factory);    }     #[cfg(feature = "test-support")]    pub fn registry(&self) -> &Entity<ContextServerDescriptorRegistry> {        &self.registry    }     #[cfg(feature = "test-support")]    pub fn test_start_server(&mut self, server: Arc<ContextServer>, cx: &mut Context<Self>) {        let configuration = Arc::new(ContextServerConfiguration::Custom {            command: ContextServerCommand {                path: "test".into(),                args: vec![],                env: None,                timeout: None,            },            remote: false,        });        self.run_server(server, configuration, cx);    }     fn new_internal(        maintain_server_loop: bool,        context_server_factory: Option<ContextServerFactory>,        registry: Entity<ContextServerDescriptorRegistry>,        worktree_store: Entity<WorktreeStore>,        weak_project: Option<WeakEntity<Project>>,        state: ContextServerStoreState,        cx: &mut Context<Self>,    ) -> Self {        let mut subscriptions = vec![cx.observe_global::<SettingsStore>(move |this, cx| {            let ai_disabled = DisableAiSettings::get_global(cx).disable_ai;            let ai_was_disabled = this.ai_disabled;            this.ai_disabled = ai_disabled;             let settings =                &Self::resolve_project_settings(&this.worktree_store, cx).context_servers;            let settings_changed = &this.context_server_settings != settings;             if settings_changed {                this.context_server_settings = settings.clone();            }             // When AI is disabled, stop all running servers            if ai_disabled {                let server_ids: Vec<_> = this.servers.keys().cloned().collect();                for id in server_ids {                    this.stop_server(&id, cx).log_err();                }                return;            }             // Trigger updates if AI was re-enabled or settings changed            if maintain_server_loop && (ai_was_disabled || settings_changed) {                this.available_context_servers_changed(cx);            }        })];         if maintain_server_loop {            subscriptions.push(cx.observe(&registry, |this, _registry, cx| {                if !DisableAiSettings::get_global(cx).disable_ai {                    this.available_context_servers_changed(cx);                }            }));        }         let ai_disabled = DisableAiSettings::get_global(cx).disable_ai;        let mut this = Self {            state,            _subscriptions: subscriptions,            context_server_settings: Self::resolve_project_settings(&worktree_store, cx)                .context_servers                .clone(),            worktree_store,            project: weak_project,            registry,            needs_server_update: false,            ai_disabled,            servers: HashMap::default(),            server_ids: Default::default(),            update_servers_task: None,            context_server_factory,        };        if maintain_server_loop && !DisableAiSettings::get_global(cx).disable_ai {            this.available_context_servers_changed(cx);        }        this    }     pub fn get_server(&self, id: &ContextServerId) -> Option<Arc<ContextServer>> {        self.servers.get(id).map(|state| state.server())    }     pub fn get_running_server(&self, id: &ContextServerId) -> Option<Arc<ContextServer>> {        if let Some(ContextServerState::Running { server, .. }) = self.servers.get(id) {            Some(server.clone())        } else {            None        }    }     pub fn status_for_server(&self, id: &ContextServerId) -> Option<ContextServerStatus> {        self.servers.get(id).map(ContextServerStatus::from_state)    }     pub fn configuration_for_server(        &self,        id: &ContextServerId,    ) -> Option<Arc<ContextServerConfiguration>> {        self.servers.get(id).map(|state| state.configuration())    }     /// Returns a sorted slice of available unique context server IDs. Within the    /// slice, context servers which have `mcp-server-` as a prefix in their ID will    /// appear after servers that do not have this prefix in their ID.    pub fn server_ids(&self) -> &[ContextServerId] {        self.server_ids.as_slice()    }     fn populate_server_ids(&mut self, cx: &App) {        self.server_ids = self            .servers            .keys()            .cloned()            .chain(                self.registry                    .read(cx)                    .context_server_descriptors()                    .into_iter()                    .map(|(id, _)| ContextServerId(id)),            )            .chain(                self.context_server_settings                    .keys()                    .map(|id| ContextServerId(id.clone())),            )            .unique()            .sorted_unstable_by(                // Sort context servers: ones without mcp-server- prefix first, then prefixed ones                |a, b| {                    const MCP_PREFIX: &str = "mcp-server-";                    match (a.0.strip_prefix(MCP_PREFIX), b.0.strip_prefix(MCP_PREFIX)) {                        // If one has mcp-server- prefix and other doesn't, non-mcp comes first                        (Some(_), None) => std::cmp::Ordering::Greater,                        (None, Some(_)) => std::cmp::Ordering::Less,                        // If both have same prefix status, sort by appropriate key                        (Some(a), Some(b)) => a.cmp(b),                        (None, None) => a.0.cmp(&b.0),                    }                },            )

Lines 1–500 of 1016.