2340 lines · 82.5 KB · Rust
RawDownload
use std::{    any::Any,    path::{Path, PathBuf},    sync::{Arc, LazyLock},    time::Duration,}; use anyhow::{Context as _, Result, bail};use collections::HashMap;use fs::{Fs, RemoveOptions};use futures::StreamExt;use gpui::{    AppContext as _, AsyncApp, Context, Entity, EventEmitter, SharedString, Subscription, Task,    TaskExt,};use http_client::{HttpClient, github::AssetKind};use node_runtime::NodeRuntime;use percent_encoding::percent_decode_str;use remote::RemoteClient;use rpc::{AnyProtoClient, TypedEnvelope, proto};use schemars::JsonSchema;use semver::Version;use serde::{Deserialize, Serialize};use settings::{AgentConfigOptionValue, RegisterSetting, SettingsStore, update_settings_file};use sha2::{Digest, Sha256};use url::Url;use util::{ResultExt as _, debug_panic}; use crate::ProjectEnvironment;use crate::agent_registry_store::{AgentRegistryStore, RegistryAgent, RegistryTargetConfig}; use crate::worktree_store::WorktreeStore; #[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)]pub struct AgentServerCommand {    #[serde(rename = "command")]    pub path: PathBuf,    #[serde(default)]    pub args: Vec<String>,    pub env: Option<HashMap<String, String>>,} impl std::fmt::Debug for AgentServerCommand {    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {        let filtered_env = self.env.as_ref().map(|env| {            env.iter()                .map(|(k, v)| {                    (                        k,                        if util::redact::should_redact(k) {                            "[REDACTED]"                        } else {                            v                        },                    )                })                .collect::<Vec<_>>()        });         f.debug_struct("AgentServerCommand")            .field("path", &self.path)            .field("args", &self.args)            .field("env", &filtered_env)            .finish()    }} #[derive(    Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize, JsonSchema,)]#[serde(transparent)]pub struct AgentId(pub SharedString); impl AgentId {    pub fn new(id: impl Into<SharedString>) -> Self {        AgentId(id.into())    }} impl std::fmt::Display for AgentId {    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {        write!(f, "{}", self.0)    }} impl From<&'static str> for AgentId {    fn from(value: &'static str) -> Self {        AgentId(value.into())    }} impl From<AgentId> for SharedString {    fn from(value: AgentId) -> Self {        value.0    }} impl AsRef<str> for AgentId {    fn as_ref(&self) -> &str {        &self.0    }} impl std::borrow::Borrow<str> for AgentId {    fn borrow(&self) -> &str {        &self.0    }} #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]pub enum ExternalAgentSource {    #[default]    Custom,    Registry,} pub trait ExternalAgentServer {    fn get_command(        &mut self,        extra_args: Vec<String>,        extra_env: HashMap<String, String>,        cx: &mut AsyncApp,    ) -> Task<Result<AgentServerCommand>>;     fn version(&self) -> Option<&SharedString> {        None    }     fn take_new_version_available_tx(&mut self) -> Option<watch::Sender<Option<String>>> {        None    }     fn set_new_version_available_tx(&mut self, _tx: watch::Sender<Option<String>>) {}     fn take_loading_status_tx(&mut self) -> Option<watch::Sender<Option<String>>> {        None    }     fn set_loading_status_tx(&mut self, _tx: watch::Sender<Option<String>>) {}     fn as_any(&self) -> &dyn Any;    fn as_any_mut(&mut self) -> &mut dyn Any;} enum AgentServerStoreState {    Local {        node_runtime: NodeRuntime,        fs: Arc<dyn Fs>,        project_environment: Entity<ProjectEnvironment>,        downstream_client: Option<(u64, AnyProtoClient)>,        settings: Option<AllAgentServersSettings>,        http_client: Arc<dyn HttpClient>,        _subscriptions: Vec<Subscription>,    },    Remote {        project_id: u64,        upstream_client: Entity<RemoteClient>,        worktree_store: Entity<WorktreeStore>,    },    Collab,} pub struct ExternalAgentEntry {    server: Box<dyn ExternalAgentServer>,    icon: Option<SharedString>,    display_name: Option<SharedString>,    pub source: ExternalAgentSource,} impl ExternalAgentEntry {    pub fn new(        server: Box<dyn ExternalAgentServer>,        source: ExternalAgentSource,        icon: Option<SharedString>,        display_name: Option<SharedString>,    ) -> Self {        Self {            server,            icon,            display_name,            source,        }    }} pub struct AgentServerStore {    state: AgentServerStoreState,    pub external_agents: HashMap<AgentId, ExternalAgentEntry>,} pub struct AgentServersUpdated; impl EventEmitter<AgentServersUpdated> for AgentServerStore {} static EXTENSION_TO_REGISTRY_IDS: LazyLock<HashMap<&'static str, &'static str>> =    LazyLock::new(|| {        HashMap::from_iter([            ("opencode", "opencode"),            ("mistral-vibe", "mistral-vibe"),            ("auggie", "auggie"),            ("stakpak", "stakpak"),            ("codebuddy", "codebuddy-code"),            ("autohand-acp", "autohand"),            ("corust-agent", "corust-agent"),            ("factory-droid", "factory-droid"),            // Unmaintained            // ("qqcode", ""),        ])    }); impl AgentServerStore {    pub fn migrate_agent_server_from_extensions(        &mut self,        id: Arc<str>,        fs: Arc<dyn Fs>,        cx: &mut Context<Self>,    ) {        let Some(registry_id) = EXTENSION_TO_REGISTRY_IDS.get(id.as_ref()) else {            return;        };         update_settings_file(fs, cx, move |settings, _| {            let agent_servers = settings.agent_servers.get_or_insert_default();            // Take the old settings            let settings = agent_servers.remove(id.as_ref());            // If they had both installed, just remove the extension settings, leave theirregistry settings alone            if agent_servers.contains_key(*registry_id) {                return;            }            // Insert the old settings, or write new ones so it is "installed" via the registry            agent_servers.insert(                registry_id.to_string(),                settings.unwrap_or_else(|| settings::CustomAgentServerSettings::Registry {                    default_mode: None,                    env: Default::default(),                    default_config_options: HashMap::default(),                    favorite_config_option_values: HashMap::default(),                }),            );        });    }     pub fn agent_icon(&self, id: &AgentId) -> Option<SharedString> {        self.external_agents            .get(id)            .and_then(|entry| entry.icon.clone())    }     pub fn agent_source(&self, name: &AgentId) -> Option<ExternalAgentSource> {        self.external_agents.get(name).map(|entry| entry.source)    }} impl AgentServerStore {    pub fn agent_display_name(&self, name: &AgentId) -> Option<SharedString> {        self.external_agents            .get(name)            .and_then(|entry| entry.display_name.clone())    }     pub fn init_remote(session: &AnyProtoClient) {        session.add_entity_message_handler(Self::handle_external_agents_updated);        session.add_entity_message_handler(Self::handle_loading_status_updated);        session.add_entity_message_handler(Self::handle_new_version_available);    }     pub fn init_headless(session: &AnyProtoClient) {        session.add_entity_request_handler(Self::handle_get_agent_server_command);    }     fn agent_servers_settings_changed(&mut self, cx: &mut Context<Self>) {        let AgentServerStoreState::Local {            settings: old_settings,            ..        } = &mut self.state        else {            debug_panic!(                "should not be subscribed to agent server settings changes in non-local project"            );            return;        };         let new_settings = cx            .global::<SettingsStore>()            .get::<AllAgentServersSettings>(None)            .clone();        if Some(&new_settings) == old_settings.as_ref() {            return;        }         self.reregister_agents(cx);    }     fn reregister_agents(&mut self, cx: &mut Context<Self>) {        let AgentServerStoreState::Local {            node_runtime,            fs,            project_environment,            downstream_client,            settings: old_settings,            http_client,            ..        } = &mut self.state        else {            debug_panic!("Non-local projects should never attempt to reregister. This is a bug!");             return;        };         let new_settings = cx            .global::<SettingsStore>()            .get::<AllAgentServersSettings>(None)            .clone();         // If we don't have agents from the registry loaded yet, trigger a        // refresh, which will cause this function to be called again        let registry_store = AgentRegistryStore::try_global(cx);        if new_settings.has_registry_agents()            && let Some(registry) = registry_store.as_ref()        {            registry.update(cx, |registry, cx| registry.refresh_if_stale(cx));        }         let registry_agents_by_id = registry_store            .as_ref()            .map(|store| {                store                    .read(cx)                    .agents()                    .iter()                    .cloned()                    .map(|agent| (agent.id().to_string(), agent))                    .collect::<HashMap<_, _>>()            })            .unwrap_or_default();         // Drain the existing versioned agents, extracting reconnect state        // from any active connection so we can preserve it or trigger a        // reconnect when the version changes.        let mut old_versioned_agents: HashMap<            AgentId,            (                SharedString,                Option<watch::Sender<Option<String>>>,                Option<watch::Sender<Option<String>>>,            ),        > = HashMap::default();        for (name, mut entry) in self.external_agents.drain() {            if let Some(version) = entry.server.version().cloned() {                let new_version_available_tx = entry.server.take_new_version_available_tx();                let loading_status_tx = entry.server.take_loading_status_tx();                if new_version_available_tx.is_some() || loading_status_tx.is_some() {                    old_versioned_agents                        .insert(name, (version, new_version_available_tx, loading_status_tx));                }            }        }         for (name, settings) in new_settings.iter() {            match settings {                CustomAgentServerSettings::Custom { command, .. } => {                    let agent_name = AgentId(name.clone().into());                    self.external_agents.insert(                        agent_name.clone(),                        ExternalAgentEntry::new(                            Box::new(LocalCustomAgent {                                command: command.clone(),                                project_environment: project_environment.clone(),                            }) as Box<dyn ExternalAgentServer>,                            ExternalAgentSource::Custom,                            None,                            None,                        ),                    );                }                CustomAgentServerSettings::Registry { env, .. } => {                    let Some(agent) = registry_agents_by_id.get(name) else {                        if registry_store.is_some() {                            log::debug!("Registry agent '{}' not found in ACP registry", name);                        }                        continue;                    };                     let agent_name = AgentId(name.clone().into());                    match agent {                        RegistryAgent::Binary(agent) => {                            if !agent.supports_current_platform {                                log::warn!(                                    "Registry agent '{}' has no compatible binary for this platform",                                    name                                );                                continue;                            }                             self.external_agents.insert(                                agent_name.clone(),                                ExternalAgentEntry::new(                                    Box::new(LocalRegistryArchiveAgent {                                        fs: fs.clone(),                                        http_client: http_client.clone(),                                        node_runtime: node_runtime.clone(),                                        project_environment: project_environment.clone(),                                        installation_dir: paths::external_agents_dir()                                            .join("registry")                                            .join(sanitize_path_component(name)),                                        version: agent.metadata.version.clone(),                                        targets: agent.targets.clone(),                                        env: env.clone(),                                        new_version_available_tx: None,                                        loading_status_tx: None,                                    })                                        as Box<dyn ExternalAgentServer>,                                    ExternalAgentSource::Registry,                                    agent.metadata.icon_path.clone(),                                    Some(agent.metadata.name.clone()),                                ),                            );                        }                        RegistryAgent::Npx(agent) => {                            self.external_agents.insert(                                agent_name.clone(),                                ExternalAgentEntry::new(                                    Box::new(LocalRegistryNpxAgent {                                        fs: fs.clone(),                                        node_runtime: node_runtime.clone(),                                        project_environment: project_environment.clone(),                                        registry_id: Arc::from(name.as_str()),                                        version: agent.metadata.version.clone(),                                        package: agent.package.clone(),                                        args: agent.args.clone(),                                        distribution_env: agent.env.clone(),                                        settings_env: env.clone(),                                        new_version_available_tx: None,                                    })                                        as Box<dyn ExternalAgentServer>,                                    ExternalAgentSource::Registry,                                    agent.metadata.icon_path.clone(),                                    Some(agent.metadata.name.clone()),                                ),                            );                        }                    }                }            }        }         // For each rebuilt versioned agent, compare the version. If it        // changed, notify the active connection to reconnect. Otherwise,        // transfer the channel to the new entry so future updates can use it.        for (name, entry) in &mut self.external_agents {            let Some((old_version, new_version_available_tx, loading_status_tx)) =                old_versioned_agents.remove(name)            else {                continue;            };            let Some(new_version) = entry.server.version() else {                continue;            };             if new_version != &old_version {                if let Some(mut tx) = new_version_available_tx {                    tx.send(Some(new_version.to_string())).ok();                }            } else {                if let Some(tx) = new_version_available_tx {                    entry.server.set_new_version_available_tx(tx);                }                if let Some(tx) = loading_status_tx {                    entry.server.set_loading_status_tx(tx);                }            }        }         *old_settings = Some(new_settings);         if let Some((project_id, downstream_client)) = downstream_client {            downstream_client                .send(proto::ExternalAgentsUpdated {                    project_id: *project_id,                    names: self                        .external_agents                        .keys()                        .map(|name| name.to_string())                        .collect(),                })                .log_err();        }        cx.emit(AgentServersUpdated);    }     pub fn node_runtime(&self) -> Option<NodeRuntime> {        match &self.state {            AgentServerStoreState::Local { node_runtime, .. } => Some(node_runtime.clone()),            _ => None,        }    }     pub fn local(        node_runtime: NodeRuntime,        fs: Arc<dyn Fs>,

Lines 1–500 of 2340.