mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
Deprecate and migrate ACP extensions (#57133)
Self-Review Checklist: - [X] I've reviewed my own diff for quality, security, and reliability - [N/A] Unsafe blocks (if any) have justifying comments - [X] The content is consistent with the [UI/UX checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) - [ ] Tests cover the new/changed behavior - [X] Performance impact has been considered and is acceptable Release Notes: - Removed support for ACP extensions. Installed ACP extensions will be migrated to use the ACP servers as provided by the ACP registry instead. --------- Co-authored-by: Ben Brandt <benjamin.j.brandt@gmail.com> Co-authored-by: Marshall Bowers <git@maxdeviant.com>
This commit is contained in:
parent
4558d14cf8
commit
c84c22dab5
20 changed files with 134 additions and 1459 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
|
@ -6166,7 +6166,6 @@ name = "extensions_ui"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"client",
|
|
||||||
"cloud_api_types",
|
"cloud_api_types",
|
||||||
"collections",
|
"collections",
|
||||||
"db",
|
"db",
|
||||||
|
|
|
||||||
|
|
@ -88,22 +88,18 @@ impl AgentServer for CustomAgentServer {
|
||||||
let config_id = config_id.to_string();
|
let config_id = config_id.to_string();
|
||||||
let value_id = value_id.to_string();
|
let value_id = value_id.to_string();
|
||||||
|
|
||||||
update_settings_file(fs, cx, move |settings, cx| {
|
update_settings_file(fs, cx, move |settings, _cx| {
|
||||||
let settings = settings
|
let settings = settings
|
||||||
.agent_servers
|
.agent_servers
|
||||||
.get_or_insert_default()
|
.get_or_insert_default()
|
||||||
.entry(agent_id.0.to_string())
|
.entry(agent_id.0.to_string())
|
||||||
.or_insert_with(|| default_settings_for_agent(agent_id, cx));
|
.or_insert_with(default_settings_for_agent);
|
||||||
|
|
||||||
match settings {
|
match settings {
|
||||||
settings::CustomAgentServerSettings::Custom {
|
settings::CustomAgentServerSettings::Custom {
|
||||||
favorite_config_option_values,
|
favorite_config_option_values,
|
||||||
..
|
..
|
||||||
}
|
}
|
||||||
| settings::CustomAgentServerSettings::Extension {
|
|
||||||
favorite_config_option_values,
|
|
||||||
..
|
|
||||||
}
|
|
||||||
| settings::CustomAgentServerSettings::Registry {
|
| settings::CustomAgentServerSettings::Registry {
|
||||||
favorite_config_option_values,
|
favorite_config_option_values,
|
||||||
..
|
..
|
||||||
|
|
@ -129,16 +125,15 @@ impl AgentServer for CustomAgentServer {
|
||||||
|
|
||||||
fn set_default_mode(&self, mode_id: Option<acp::SessionModeId>, fs: Arc<dyn Fs>, cx: &mut App) {
|
fn set_default_mode(&self, mode_id: Option<acp::SessionModeId>, fs: Arc<dyn Fs>, cx: &mut App) {
|
||||||
let agent_id = self.agent_id();
|
let agent_id = self.agent_id();
|
||||||
update_settings_file(fs, cx, move |settings, cx| {
|
update_settings_file(fs, cx, move |settings, _cx| {
|
||||||
let settings = settings
|
let settings = settings
|
||||||
.agent_servers
|
.agent_servers
|
||||||
.get_or_insert_default()
|
.get_or_insert_default()
|
||||||
.entry(agent_id.0.to_string())
|
.entry(agent_id.0.to_string())
|
||||||
.or_insert_with(|| default_settings_for_agent(agent_id, cx));
|
.or_insert_with(default_settings_for_agent);
|
||||||
|
|
||||||
match settings {
|
match settings {
|
||||||
settings::CustomAgentServerSettings::Custom { default_mode, .. }
|
settings::CustomAgentServerSettings::Custom { default_mode, .. }
|
||||||
| settings::CustomAgentServerSettings::Extension { default_mode, .. }
|
|
||||||
| settings::CustomAgentServerSettings::Registry { default_mode, .. } => {
|
| settings::CustomAgentServerSettings::Registry { default_mode, .. } => {
|
||||||
*default_mode = mode_id.map(|m| m.to_string());
|
*default_mode = mode_id.map(|m| m.to_string());
|
||||||
}
|
}
|
||||||
|
|
@ -161,16 +156,15 @@ impl AgentServer for CustomAgentServer {
|
||||||
|
|
||||||
fn set_default_model(&self, model_id: Option<acp::ModelId>, fs: Arc<dyn Fs>, cx: &mut App) {
|
fn set_default_model(&self, model_id: Option<acp::ModelId>, fs: Arc<dyn Fs>, cx: &mut App) {
|
||||||
let agent_id = self.agent_id();
|
let agent_id = self.agent_id();
|
||||||
update_settings_file(fs, cx, move |settings, cx| {
|
update_settings_file(fs, cx, move |settings, _cx| {
|
||||||
let settings = settings
|
let settings = settings
|
||||||
.agent_servers
|
.agent_servers
|
||||||
.get_or_insert_default()
|
.get_or_insert_default()
|
||||||
.entry(agent_id.0.to_string())
|
.entry(agent_id.0.to_string())
|
||||||
.or_insert_with(|| default_settings_for_agent(agent_id, cx));
|
.or_insert_with(default_settings_for_agent);
|
||||||
|
|
||||||
match settings {
|
match settings {
|
||||||
settings::CustomAgentServerSettings::Custom { default_model, .. }
|
settings::CustomAgentServerSettings::Custom { default_model, .. }
|
||||||
| settings::CustomAgentServerSettings::Extension { default_model, .. }
|
|
||||||
| settings::CustomAgentServerSettings::Registry { default_model, .. } => {
|
| settings::CustomAgentServerSettings::Registry { default_model, .. } => {
|
||||||
*default_model = model_id.map(|m| m.to_string());
|
*default_model = model_id.map(|m| m.to_string());
|
||||||
}
|
}
|
||||||
|
|
@ -205,20 +199,17 @@ impl AgentServer for CustomAgentServer {
|
||||||
cx: &App,
|
cx: &App,
|
||||||
) {
|
) {
|
||||||
let agent_id = self.agent_id();
|
let agent_id = self.agent_id();
|
||||||
update_settings_file(fs, cx, move |settings, cx| {
|
update_settings_file(fs, cx, move |settings, _cx| {
|
||||||
let settings = settings
|
let settings = settings
|
||||||
.agent_servers
|
.agent_servers
|
||||||
.get_or_insert_default()
|
.get_or_insert_default()
|
||||||
.entry(agent_id.0.to_string())
|
.entry(agent_id.0.to_string())
|
||||||
.or_insert_with(|| default_settings_for_agent(agent_id, cx));
|
.or_insert_with(default_settings_for_agent);
|
||||||
|
|
||||||
let favorite_models = match settings {
|
let favorite_models = match settings {
|
||||||
settings::CustomAgentServerSettings::Custom {
|
settings::CustomAgentServerSettings::Custom {
|
||||||
favorite_models, ..
|
favorite_models, ..
|
||||||
}
|
}
|
||||||
| settings::CustomAgentServerSettings::Extension {
|
|
||||||
favorite_models, ..
|
|
||||||
}
|
|
||||||
| settings::CustomAgentServerSettings::Registry {
|
| settings::CustomAgentServerSettings::Registry {
|
||||||
favorite_models, ..
|
favorite_models, ..
|
||||||
} => favorite_models,
|
} => favorite_models,
|
||||||
|
|
@ -258,22 +249,18 @@ impl AgentServer for CustomAgentServer {
|
||||||
let agent_id = self.agent_id();
|
let agent_id = self.agent_id();
|
||||||
let config_id = config_id.to_string();
|
let config_id = config_id.to_string();
|
||||||
let value_id = value_id.map(|s| s.to_string());
|
let value_id = value_id.map(|s| s.to_string());
|
||||||
update_settings_file(fs, cx, move |settings, cx| {
|
update_settings_file(fs, cx, move |settings, _cx| {
|
||||||
let settings = settings
|
let settings = settings
|
||||||
.agent_servers
|
.agent_servers
|
||||||
.get_or_insert_default()
|
.get_or_insert_default()
|
||||||
.entry(agent_id.0.to_string())
|
.entry(agent_id.0.to_string())
|
||||||
.or_insert_with(|| default_settings_for_agent(agent_id, cx));
|
.or_insert_with(default_settings_for_agent);
|
||||||
|
|
||||||
match settings {
|
match settings {
|
||||||
settings::CustomAgentServerSettings::Custom {
|
settings::CustomAgentServerSettings::Custom {
|
||||||
default_config_options,
|
default_config_options,
|
||||||
..
|
..
|
||||||
}
|
}
|
||||||
| settings::CustomAgentServerSettings::Extension {
|
|
||||||
default_config_options,
|
|
||||||
..
|
|
||||||
}
|
|
||||||
| settings::CustomAgentServerSettings::Registry {
|
| settings::CustomAgentServerSettings::Registry {
|
||||||
default_config_options,
|
default_config_options,
|
||||||
..
|
..
|
||||||
|
|
@ -307,10 +294,6 @@ impl AgentServer for CustomAgentServer {
|
||||||
default_config_options,
|
default_config_options,
|
||||||
..
|
..
|
||||||
}
|
}
|
||||||
| project::agent_server_store::CustomAgentServerSettings::Extension {
|
|
||||||
default_config_options,
|
|
||||||
..
|
|
||||||
}
|
|
||||||
| project::agent_server_store::CustomAgentServerSettings::Registry {
|
| project::agent_server_store::CustomAgentServerSettings::Registry {
|
||||||
default_config_options,
|
default_config_options,
|
||||||
..
|
..
|
||||||
|
|
@ -422,28 +405,14 @@ fn is_registry_agent(agent_id: impl Into<AgentId>, cx: &App) -> bool {
|
||||||
is_in_registry || is_settings_registry
|
is_in_registry || is_settings_registry
|
||||||
}
|
}
|
||||||
|
|
||||||
fn default_settings_for_agent(
|
fn default_settings_for_agent() -> settings::CustomAgentServerSettings {
|
||||||
agent_id: impl Into<AgentId>,
|
settings::CustomAgentServerSettings::Registry {
|
||||||
cx: &App,
|
default_model: None,
|
||||||
) -> settings::CustomAgentServerSettings {
|
default_mode: None,
|
||||||
if is_registry_agent(agent_id, cx) {
|
env: Default::default(),
|
||||||
settings::CustomAgentServerSettings::Registry {
|
favorite_models: Vec::new(),
|
||||||
default_model: None,
|
default_config_options: Default::default(),
|
||||||
default_mode: None,
|
favorite_config_option_values: Default::default(),
|
||||||
env: Default::default(),
|
|
||||||
favorite_models: Vec::new(),
|
|
||||||
default_config_options: Default::default(),
|
|
||||||
favorite_config_option_values: Default::default(),
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
settings::CustomAgentServerSettings::Extension {
|
|
||||||
default_model: None,
|
|
||||||
default_mode: None,
|
|
||||||
env: Default::default(),
|
|
||||||
favorite_models: Vec::new(),
|
|
||||||
default_config_options: Default::default(),
|
|
||||||
favorite_config_option_values: Default::default(),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -547,53 +516,4 @@ mod tests {
|
||||||
assert!(is_registry_agent("agent-from-settings", cx));
|
assert!(is_registry_agent("agent-from-settings", cx));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
fn test_agent_with_extension_settings_type_is_not_registry(cx: &mut TestAppContext) {
|
|
||||||
init_test(cx);
|
|
||||||
set_agent_server_settings(
|
|
||||||
cx,
|
|
||||||
vec![(
|
|
||||||
"my-extension-agent",
|
|
||||||
settings::CustomAgentServerSettings::Extension {
|
|
||||||
env: HashMap::default(),
|
|
||||||
default_mode: None,
|
|
||||||
default_model: None,
|
|
||||||
favorite_models: Vec::new(),
|
|
||||||
default_config_options: HashMap::default(),
|
|
||||||
favorite_config_option_values: HashMap::default(),
|
|
||||||
},
|
|
||||||
)],
|
|
||||||
);
|
|
||||||
cx.update(|cx| {
|
|
||||||
assert!(!is_registry_agent("my-extension-agent", cx));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
fn test_default_settings_for_extension_agent(cx: &mut TestAppContext) {
|
|
||||||
init_test(cx);
|
|
||||||
cx.update(|cx| {
|
|
||||||
assert!(matches!(
|
|
||||||
default_settings_for_agent("some-extension-agent", cx),
|
|
||||||
settings::CustomAgentServerSettings::Extension { .. }
|
|
||||||
));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
fn test_default_settings_for_agent_in_registry(cx: &mut TestAppContext) {
|
|
||||||
init_test(cx);
|
|
||||||
init_registry_with_agents(cx, &["new-registry-agent"]);
|
|
||||||
cx.update(|cx| {
|
|
||||||
assert!(matches!(
|
|
||||||
default_settings_for_agent("new-registry-agent", cx),
|
|
||||||
settings::CustomAgentServerSettings::Registry { .. }
|
|
||||||
));
|
|
||||||
assert!(matches!(
|
|
||||||
default_settings_for_agent("not-in-registry", cx),
|
|
||||||
settings::CustomAgentServerSettings::Extension { .. }
|
|
||||||
));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1174,7 +1174,6 @@ impl AgentConfiguration {
|
||||||
};
|
};
|
||||||
|
|
||||||
let source_kind = match source {
|
let source_kind = match source {
|
||||||
ExternalAgentSource::Extension => AiSettingItemSource::Extension,
|
|
||||||
ExternalAgentSource::Registry => AiSettingItemSource::Registry,
|
ExternalAgentSource::Registry => AiSettingItemSource::Registry,
|
||||||
ExternalAgentSource::Custom => AiSettingItemSource::Custom,
|
ExternalAgentSource::Custom => AiSettingItemSource::Custom,
|
||||||
};
|
};
|
||||||
|
|
@ -1218,26 +1217,6 @@ impl AgentConfiguration {
|
||||||
});
|
});
|
||||||
|
|
||||||
let uninstall_button = match source {
|
let uninstall_button = match source {
|
||||||
ExternalAgentSource::Extension => Some(
|
|
||||||
IconButton::new(
|
|
||||||
SharedString::from(format!("uninstall-{}", id)),
|
|
||||||
IconName::Trash,
|
|
||||||
)
|
|
||||||
.icon_color(Color::Muted)
|
|
||||||
.icon_size(IconSize::Small)
|
|
||||||
.tooltip(Tooltip::text("Uninstall Agent Extension"))
|
|
||||||
.on_click(cx.listener(move |this, _, _window, cx| {
|
|
||||||
let agent_name = agent_server_name.clone();
|
|
||||||
|
|
||||||
if let Some(ext_id) = this.agent_server_store.update(cx, |store, _cx| {
|
|
||||||
store.get_extension_id_for_agent(&agent_name)
|
|
||||||
}) {
|
|
||||||
ExtensionStore::global(cx)
|
|
||||||
.update(cx, |store, cx| store.uninstall_extension(ext_id, cx))
|
|
||||||
.detach_and_log_err(cx);
|
|
||||||
}
|
|
||||||
})),
|
|
||||||
),
|
|
||||||
ExternalAgentSource::Registry => {
|
ExternalAgentSource::Registry => {
|
||||||
let fs = self.fs.clone();
|
let fs = self.fs.clone();
|
||||||
Some(
|
Some(
|
||||||
|
|
|
||||||
|
|
@ -59,7 +59,6 @@ use client::UserStore;
|
||||||
use cloud_api_types::Plan;
|
use cloud_api_types::Plan;
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use editor::{Editor, MultiBuffer};
|
use editor::{Editor, MultiBuffer};
|
||||||
use extension::ExtensionEvents;
|
|
||||||
use extension_host::ExtensionStore;
|
use extension_host::ExtensionStore;
|
||||||
|
|
||||||
use fs::Fs;
|
use fs::Fs;
|
||||||
|
|
@ -1234,21 +1233,14 @@ impl AgentPanel {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Subscribe to extension events to sync agent servers when extensions change
|
// Subscribe to extension events to sync agent servers when extensions change
|
||||||
let extension_subscription = if let Some(extension_events) = ExtensionEvents::try_global(cx)
|
let extension_subscription = ExtensionStore::try_global(cx).map(|store| {
|
||||||
{
|
cx.subscribe(&store, |this, _source, event, cx| match event {
|
||||||
Some(
|
extension_host::Event::ExtensionUninstalled(id) => {
|
||||||
cx.subscribe(&extension_events, |this, _source, event, cx| match event {
|
this.migrate_agent_server_from_extensions(id.clone(), cx);
|
||||||
extension::Event::ExtensionInstalled(_)
|
}
|
||||||
| extension::Event::ExtensionUninstalled(_)
|
_ => {}
|
||||||
| extension::Event::ExtensionsInstalledChanged => {
|
})
|
||||||
this.sync_agent_servers_from_extensions(cx);
|
});
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let connection_store = cx.new(|cx| AgentConnectionStore::new(project.clone(), cx));
|
let connection_store = cx.new(|cx| AgentConnectionStore::new(project.clone(), cx));
|
||||||
let _project_subscription =
|
let _project_subscription =
|
||||||
|
|
@ -1280,7 +1272,7 @@ impl AgentPanel {
|
||||||
})
|
})
|
||||||
.detach();
|
.detach();
|
||||||
|
|
||||||
let mut panel = Self {
|
let panel = Self {
|
||||||
workspace_id,
|
workspace_id,
|
||||||
base_view,
|
base_view,
|
||||||
last_created_entry_kind: AgentPanelEntryKind::Thread,
|
last_created_entry_kind: AgentPanelEntryKind::Thread,
|
||||||
|
|
@ -1321,8 +1313,6 @@ impl AgentPanel {
|
||||||
is_active: false,
|
is_active: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Initial sync of agent servers from extensions
|
|
||||||
panel.sync_agent_servers_from_extensions(cx);
|
|
||||||
panel.ensure_native_agent_connection(cx);
|
panel.ensure_native_agent_connection(cx);
|
||||||
panel
|
panel
|
||||||
}
|
}
|
||||||
|
|
@ -3901,29 +3891,12 @@ impl AgentPanel {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn sync_agent_servers_from_extensions(&mut self, cx: &mut Context<Self>) {
|
fn migrate_agent_server_from_extensions(&mut self, id: Arc<str>, cx: &mut Context<Self>) {
|
||||||
if let Some(extension_store) = ExtensionStore::try_global(cx) {
|
self.project.update(cx, |project, cx| {
|
||||||
let (manifests, extensions_dir) = {
|
project.agent_server_store().update(cx, |store, cx| {
|
||||||
let store = extension_store.read(cx);
|
store.migrate_agent_server_from_extensions(id, project.fs().clone(), cx);
|
||||||
let installed = store.installed_extensions();
|
|
||||||
let manifests: Vec<_> = installed
|
|
||||||
.iter()
|
|
||||||
.map(|(id, entry)| (id.clone(), entry.manifest.clone()))
|
|
||||||
.collect();
|
|
||||||
let extensions_dir = paths::extensions_dir().join("installed");
|
|
||||||
(manifests, extensions_dir)
|
|
||||||
};
|
|
||||||
|
|
||||||
self.project.update(cx, |project, cx| {
|
|
||||||
project.agent_server_store().update(cx, |store, cx| {
|
|
||||||
let manifest_refs: Vec<_> = manifests
|
|
||||||
.iter()
|
|
||||||
.map(|(id, manifest)| (id.as_ref(), manifest.as_ref()))
|
|
||||||
.collect();
|
|
||||||
store.sync_extension_agents(manifest_refs, extensions_dir, cx);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_agent_thread_with_external_source_prompt(
|
pub fn new_agent_thread_with_external_source_prompt(
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,6 @@ enum RegistryInstallStatus {
|
||||||
NotInstalled,
|
NotInstalled,
|
||||||
InstalledRegistry,
|
InstalledRegistry,
|
||||||
InstalledCustom,
|
InstalledCustom,
|
||||||
InstalledExtension,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(IntoElement)]
|
#[derive(IntoElement)]
|
||||||
|
|
@ -155,9 +154,6 @@ impl AgentRegistryPage {
|
||||||
RegistryInstallStatus::InstalledRegistry
|
RegistryInstallStatus::InstalledRegistry
|
||||||
}
|
}
|
||||||
CustomAgentServerSettings::Custom { .. } => RegistryInstallStatus::InstalledCustom,
|
CustomAgentServerSettings::Custom { .. } => RegistryInstallStatus::InstalledCustom,
|
||||||
CustomAgentServerSettings::Extension { .. } => {
|
|
||||||
RegistryInstallStatus::InstalledExtension
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
self.installed_statuses.insert(id.clone(), status);
|
self.installed_statuses.insert(id.clone(), status);
|
||||||
}
|
}
|
||||||
|
|
@ -560,9 +556,6 @@ impl AgentRegistryPage {
|
||||||
RegistryInstallStatus::InstalledCustom => Button::new(button_id, "Installed")
|
RegistryInstallStatus::InstalledCustom => Button::new(button_id, "Installed")
|
||||||
.style(ButtonStyle::OutlinedGhost)
|
.style(ButtonStyle::OutlinedGhost)
|
||||||
.disabled(true),
|
.disabled(true),
|
||||||
RegistryInstallStatus::InstalledExtension => Button::new(button_id, "Installed")
|
|
||||||
.style(ButtonStyle::OutlinedGhost)
|
|
||||||
.disabled(true),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,8 +42,11 @@ pub enum ExtensionProvides {
|
||||||
Grammars,
|
Grammars,
|
||||||
LanguageServers,
|
LanguageServers,
|
||||||
ContextServers,
|
ContextServers,
|
||||||
|
/// Deprecated
|
||||||
AgentServers,
|
AgentServers,
|
||||||
|
/// Deprecated
|
||||||
SlashCommands,
|
SlashCommands,
|
||||||
|
/// Deprecated
|
||||||
IndexedDocsProviders,
|
IndexedDocsProviders,
|
||||||
Snippets,
|
Snippets,
|
||||||
DebugAdapters,
|
DebugAdapters,
|
||||||
|
|
|
||||||
|
|
@ -107,8 +107,6 @@ pub struct ExtensionManifest {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub context_servers: BTreeMap<Arc<str>, ContextServerManifestEntry>,
|
pub context_servers: BTreeMap<Arc<str>, ContextServerManifestEntry>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub agent_servers: BTreeMap<Arc<str>, AgentServerManifestEntry>,
|
|
||||||
#[serde(default)]
|
|
||||||
pub slash_commands: BTreeMap<Arc<str>, SlashCommandManifestEntry>,
|
pub slash_commands: BTreeMap<Arc<str>, SlashCommandManifestEntry>,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub snippets: Option<ExtensionSnippets>,
|
pub snippets: Option<ExtensionSnippets>,
|
||||||
|
|
@ -150,10 +148,6 @@ impl ExtensionManifest {
|
||||||
provides.insert(ExtensionProvides::ContextServers);
|
provides.insert(ExtensionProvides::ContextServers);
|
||||||
}
|
}
|
||||||
|
|
||||||
if !self.agent_servers.is_empty() {
|
|
||||||
provides.insert(ExtensionProvides::AgentServers);
|
|
||||||
}
|
|
||||||
|
|
||||||
if self.snippets.is_some() {
|
if self.snippets.is_some() {
|
||||||
provides.insert(ExtensionProvides::Snippets);
|
provides.insert(ExtensionProvides::Snippets);
|
||||||
}
|
}
|
||||||
|
|
@ -433,7 +427,6 @@ fn manifest_from_old_manifest(
|
||||||
.collect(),
|
.collect(),
|
||||||
language_servers: Default::default(),
|
language_servers: Default::default(),
|
||||||
context_servers: BTreeMap::default(),
|
context_servers: BTreeMap::default(),
|
||||||
agent_servers: BTreeMap::default(),
|
|
||||||
slash_commands: BTreeMap::default(),
|
slash_commands: BTreeMap::default(),
|
||||||
snippets: None,
|
snippets: None,
|
||||||
capabilities: Vec::new(),
|
capabilities: Vec::new(),
|
||||||
|
|
@ -445,7 +438,6 @@ fn manifest_from_old_manifest(
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use indoc::indoc;
|
|
||||||
use pretty_assertions::assert_eq;
|
use pretty_assertions::assert_eq;
|
||||||
use util::rel_path::rel_path_buf;
|
use util::rel_path::rel_path_buf;
|
||||||
|
|
||||||
|
|
@ -469,7 +461,6 @@ mod tests {
|
||||||
grammars: BTreeMap::default(),
|
grammars: BTreeMap::default(),
|
||||||
language_servers: BTreeMap::default(),
|
language_servers: BTreeMap::default(),
|
||||||
context_servers: BTreeMap::default(),
|
context_servers: BTreeMap::default(),
|
||||||
agent_servers: BTreeMap::default(),
|
|
||||||
slash_commands: BTreeMap::default(),
|
slash_commands: BTreeMap::default(),
|
||||||
snippets: None,
|
snippets: None,
|
||||||
capabilities: vec![],
|
capabilities: vec![],
|
||||||
|
|
@ -578,6 +569,8 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
#[cfg(target_os = "windows")]
|
#[cfg(target_os = "windows")]
|
||||||
fn test_deserialize_manifest_with_windows_separators() {
|
fn test_deserialize_manifest_with_windows_separators() {
|
||||||
|
use indoc::indoc;
|
||||||
|
|
||||||
let content = indoc! {r#"
|
let content = indoc! {r#"
|
||||||
id = "test-manifest"
|
id = "test-manifest"
|
||||||
name = "Test Manifest"
|
name = "Test Manifest"
|
||||||
|
|
@ -588,32 +581,4 @@ mod tests {
|
||||||
let manifest: ExtensionManifest = toml::from_str(&content).expect("manifest should parse");
|
let manifest: ExtensionManifest = toml::from_str(&content).expect("manifest should parse");
|
||||||
assert_eq!(manifest.languages, vec![rel_path_buf("foo/bar")]);
|
assert_eq!(manifest.languages, vec![rel_path_buf("foo/bar")]);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_manifest_with_agent_server_archive_launcher() {
|
|
||||||
let toml_src = indoc! {r#"
|
|
||||||
id = "example.agent-server-ext"
|
|
||||||
name = "Agent Server Example"
|
|
||||||
version = "1.0.0"
|
|
||||||
schema_version = 0
|
|
||||||
|
|
||||||
[agent_servers.foo]
|
|
||||||
name = "Foo Agent"
|
|
||||||
|
|
||||||
[agent_servers.foo.targets.linux-x86_64]
|
|
||||||
archive = "https://example.com/agent-linux-x64.tar.gz"
|
|
||||||
cmd = "./agent"
|
|
||||||
args = ["--serve"]
|
|
||||||
"#};
|
|
||||||
|
|
||||||
let manifest: ExtensionManifest = toml::from_str(toml_src).expect("manifest should parse");
|
|
||||||
assert_eq!(manifest.id.as_ref(), "example.agent-server-ext");
|
|
||||||
assert!(manifest.agent_servers.contains_key("foo"));
|
|
||||||
let entry = manifest.agent_servers.get("foo").unwrap();
|
|
||||||
assert!(entry.targets.contains_key("linux-x86_64"));
|
|
||||||
let target = entry.targets.get("linux-x86_64").unwrap();
|
|
||||||
assert_eq!(target.archive, "https://example.com/agent-linux-x64.tar.gz");
|
|
||||||
assert_eq!(target.cmd, "./agent");
|
|
||||||
assert_eq!(target.args, vec!["--serve"]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -207,21 +207,6 @@ async fn copy_extension_resources(
|
||||||
.context("failed to copy icons")?;
|
.context("failed to copy icons")?;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (_, agent_entry) in &manifest.agent_servers {
|
|
||||||
if let Some(icon_path) = &agent_entry.icon {
|
|
||||||
let source_icon = extension_path.join(icon_path);
|
|
||||||
let dest_icon = output_dir.join(icon_path);
|
|
||||||
|
|
||||||
// Create parent directory if needed
|
|
||||||
if let Some(parent) = dest_icon.parent() {
|
|
||||||
fs::create_dir_all(parent)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
fs::copy(&source_icon, &dest_icon)
|
|
||||||
.with_context(|| format!("failed to copy agent server icon '{}'", icon_path))?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !manifest.languages.is_empty() {
|
if !manifest.languages.is_empty() {
|
||||||
let output_languages_dir = output_dir.join("languages");
|
let output_languages_dir = output_dir.join("languages");
|
||||||
fs::create_dir_all(&output_languages_dir)?;
|
fs::create_dir_all(&output_languages_dir)?;
|
||||||
|
|
|
||||||
|
|
@ -137,7 +137,6 @@ fn manifest() -> ExtensionManifest {
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.collect(),
|
.collect(),
|
||||||
context_servers: BTreeMap::default(),
|
context_servers: BTreeMap::default(),
|
||||||
agent_servers: BTreeMap::default(),
|
|
||||||
slash_commands: BTreeMap::default(),
|
slash_commands: BTreeMap::default(),
|
||||||
snippets: None,
|
snippets: None,
|
||||||
capabilities: vec![ExtensionCapability::ProcessExec(
|
capabilities: vec![ExtensionCapability::ProcessExec(
|
||||||
|
|
|
||||||
|
|
@ -107,7 +107,6 @@ mod tests {
|
||||||
grammars: BTreeMap::default(),
|
grammars: BTreeMap::default(),
|
||||||
language_servers: BTreeMap::default(),
|
language_servers: BTreeMap::default(),
|
||||||
context_servers: BTreeMap::default(),
|
context_servers: BTreeMap::default(),
|
||||||
agent_servers: BTreeMap::default(),
|
|
||||||
slash_commands: BTreeMap::default(),
|
slash_commands: BTreeMap::default(),
|
||||||
snippets: None,
|
snippets: None,
|
||||||
capabilities: vec![],
|
capabilities: vec![],
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,7 @@ use async_compression::futures::bufread::GzipDecoder;
|
||||||
use async_tar::Archive;
|
use async_tar::Archive;
|
||||||
use client::{Client, proto, telemetry::Telemetry};
|
use client::{Client, proto, telemetry::Telemetry};
|
||||||
use cloud_api_types::{ExtensionMetadata, ExtensionProvides, GetExtensionsResponse};
|
use cloud_api_types::{ExtensionMetadata, ExtensionProvides, GetExtensionsResponse};
|
||||||
use collections::{BTreeMap, BTreeSet, HashSet, btree_map};
|
use collections::{BTreeMap, BTreeSet, FxHashSet, HashSet, btree_map};
|
||||||
pub use extension::ExtensionManifest;
|
pub use extension::ExtensionManifest;
|
||||||
use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder};
|
use extension::extension_builder::{CompileExtensionOptions, ExtensionBuilder};
|
||||||
use extension::{
|
use extension::{
|
||||||
|
|
@ -48,6 +48,7 @@ use serde::{Deserialize, Serialize};
|
||||||
use settings::{SemanticTokenRules, Settings, SettingsStore};
|
use settings::{SemanticTokenRules, Settings, SettingsStore};
|
||||||
use std::ops::RangeInclusive;
|
use std::ops::RangeInclusive;
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
use std::sync::LazyLock;
|
||||||
use std::{
|
use std::{
|
||||||
cmp::Ordering,
|
cmp::Ordering,
|
||||||
path::{self, Path, PathBuf},
|
path::{self, Path, PathBuf},
|
||||||
|
|
@ -77,7 +78,25 @@ const CURRENT_SCHEMA_VERSION: SchemaVersion = SchemaVersion(1);
|
||||||
///
|
///
|
||||||
/// These snippets should no longer be downloaded or loaded, because their
|
/// These snippets should no longer be downloaded or loaded, because their
|
||||||
/// functionality has been integrated into the core editor.
|
/// functionality has been integrated into the core editor.
|
||||||
const SUPPRESSED_EXTENSIONS: &[&str] = &["snippets", "ruff", "ty", "basedpyright", "basher"];
|
static SUPPRESSED_EXTENSIONS: LazyLock<FxHashSet<&str>> = LazyLock::new(|| {
|
||||||
|
FxHashSet::from_iter([
|
||||||
|
"snippets",
|
||||||
|
"ruff",
|
||||||
|
"ty",
|
||||||
|
"basedpyright",
|
||||||
|
"basher",
|
||||||
|
// ACP
|
||||||
|
"opencode",
|
||||||
|
"mistral-vibe",
|
||||||
|
"auggie",
|
||||||
|
"stakpak",
|
||||||
|
"codebuddy",
|
||||||
|
"autohand-acp",
|
||||||
|
"corust-agent",
|
||||||
|
"factory-droid",
|
||||||
|
"qqcode",
|
||||||
|
])
|
||||||
|
});
|
||||||
|
|
||||||
/// Returns the [`SchemaVersion`] range that is compatible with this version of Zed.
|
/// Returns the [`SchemaVersion`] range that is compatible with this version of Zed.
|
||||||
pub fn schema_version_range() -> RangeInclusive<SchemaVersion> {
|
pub fn schema_version_range() -> RangeInclusive<SchemaVersion> {
|
||||||
|
|
@ -604,7 +623,7 @@ impl ExtensionStore {
|
||||||
.extension_index
|
.extension_index
|
||||||
.extensions
|
.extensions
|
||||||
.contains_key(extension_id.as_ref());
|
.contains_key(extension_id.as_ref());
|
||||||
!is_already_installed && !SUPPRESSED_EXTENSIONS.contains(&extension_id.as_ref())
|
!is_already_installed && !SUPPRESSED_EXTENSIONS.contains(extension_id.as_ref())
|
||||||
})
|
})
|
||||||
.cloned()
|
.cloned()
|
||||||
.collect::<Vec<_>>();
|
.collect::<Vec<_>>();
|
||||||
|
|
@ -687,7 +706,7 @@ impl ExtensionStore {
|
||||||
|
|
||||||
response
|
response
|
||||||
.data
|
.data
|
||||||
.retain(|extension| !SUPPRESSED_EXTENSIONS.contains(&extension.id.as_ref()));
|
.retain(|extension| !SUPPRESSED_EXTENSIONS.contains(extension.id.as_ref()));
|
||||||
|
|
||||||
Ok(response.data)
|
Ok(response.data)
|
||||||
})
|
})
|
||||||
|
|
@ -1112,9 +1131,12 @@ impl ExtensionStore {
|
||||||
) -> Task<()> {
|
) -> Task<()> {
|
||||||
let old_index = &self.extension_index;
|
let old_index = &self.extension_index;
|
||||||
|
|
||||||
new_index
|
let suppressed_extensions_to_remove = new_index
|
||||||
.extensions
|
.extensions
|
||||||
.retain(|extension_id, _| !SUPPRESSED_EXTENSIONS.contains(&extension_id.as_ref()));
|
.extract_if(.., |extension_id, _| {
|
||||||
|
SUPPRESSED_EXTENSIONS.contains(extension_id.as_ref())
|
||||||
|
})
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
// Determine which extensions need to be loaded and unloaded, based
|
// Determine which extensions need to be loaded and unloaded, based
|
||||||
// on the changes to the manifest and the extensions that we know have been
|
// on the changes to the manifest and the extensions that we know have been
|
||||||
|
|
@ -1155,8 +1177,16 @@ impl ExtensionStore {
|
||||||
self.modified_extensions.clear();
|
self.modified_extensions.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let trigger_suppressed_extension_removal =
|
||||||
|
move |this: &mut ExtensionStore, cx: &mut Context<ExtensionStore>| {
|
||||||
|
for (id, _) in suppressed_extensions_to_remove {
|
||||||
|
this.uninstall_extension(id, cx).detach_and_log_err(cx);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if extensions_to_load.is_empty() && extensions_to_unload.is_empty() {
|
if extensions_to_load.is_empty() && extensions_to_unload.is_empty() {
|
||||||
self.reload_complete_senders.clear();
|
self.reload_complete_senders.clear();
|
||||||
|
trigger_suppressed_extension_removal(self, cx);
|
||||||
return Task::ready(());
|
return Task::ready(());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1496,6 +1526,7 @@ impl ExtensionStore {
|
||||||
this.proxy.set_extensions_loaded();
|
this.proxy.set_extensions_loaded();
|
||||||
this.proxy.reload_current_theme(cx);
|
this.proxy.reload_current_theme(cx);
|
||||||
this.proxy.reload_current_icon_theme(cx);
|
this.proxy.reload_current_icon_theme(cx);
|
||||||
|
trigger_suppressed_extension_removal(this, cx);
|
||||||
|
|
||||||
if let Some(events) = ExtensionEvents::try_global(cx) {
|
if let Some(events) = ExtensionEvents::try_global(cx) {
|
||||||
events.update(cx, |this, cx| {
|
events.update(cx, |this, cx| {
|
||||||
|
|
@ -1566,10 +1597,6 @@ impl ExtensionStore {
|
||||||
let mut extension_manifest = ExtensionManifest::load(fs.clone(), &extension_dir).await?;
|
let mut extension_manifest = ExtensionManifest::load(fs.clone(), &extension_dir).await?;
|
||||||
let extension_id = extension_manifest.id.clone();
|
let extension_id = extension_manifest.id.clone();
|
||||||
|
|
||||||
if SUPPRESSED_EXTENSIONS.contains(&extension_id.as_ref()) {
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: distinguish dev extensions more explicitly, by the absence
|
// TODO: distinguish dev extensions more explicitly, by the absence
|
||||||
// of a checksum file that we'll create when downloading normal extensions.
|
// of a checksum file that we'll create when downloading normal extensions.
|
||||||
let is_dev = fs
|
let is_dev = fs
|
||||||
|
|
|
||||||
|
|
@ -162,7 +162,6 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
||||||
.collect(),
|
.collect(),
|
||||||
language_servers: BTreeMap::default(),
|
language_servers: BTreeMap::default(),
|
||||||
context_servers: BTreeMap::default(),
|
context_servers: BTreeMap::default(),
|
||||||
agent_servers: BTreeMap::default(),
|
|
||||||
slash_commands: BTreeMap::default(),
|
slash_commands: BTreeMap::default(),
|
||||||
snippets: None,
|
snippets: None,
|
||||||
capabilities: Vec::new(),
|
capabilities: Vec::new(),
|
||||||
|
|
@ -194,7 +193,6 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
||||||
grammars: BTreeMap::default(),
|
grammars: BTreeMap::default(),
|
||||||
language_servers: BTreeMap::default(),
|
language_servers: BTreeMap::default(),
|
||||||
context_servers: BTreeMap::default(),
|
context_servers: BTreeMap::default(),
|
||||||
agent_servers: BTreeMap::default(),
|
|
||||||
slash_commands: BTreeMap::default(),
|
slash_commands: BTreeMap::default(),
|
||||||
snippets: None,
|
snippets: None,
|
||||||
capabilities: Vec::new(),
|
capabilities: Vec::new(),
|
||||||
|
|
@ -377,7 +375,6 @@ async fn test_extension_store(cx: &mut TestAppContext) {
|
||||||
grammars: BTreeMap::default(),
|
grammars: BTreeMap::default(),
|
||||||
language_servers: BTreeMap::default(),
|
language_servers: BTreeMap::default(),
|
||||||
context_servers: BTreeMap::default(),
|
context_servers: BTreeMap::default(),
|
||||||
agent_servers: BTreeMap::default(),
|
|
||||||
slash_commands: BTreeMap::default(),
|
slash_commands: BTreeMap::default(),
|
||||||
snippets: None,
|
snippets: None,
|
||||||
capabilities: Vec::new(),
|
capabilities: Vec::new(),
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ path = "src/extensions_ui.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
client.workspace = true
|
|
||||||
cloud_api_types.workspace = true
|
cloud_api_types.workspace = true
|
||||||
collections.workspace = true
|
collections.workspace = true
|
||||||
db.workspace = true
|
db.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,6 @@ use std::time::Duration;
|
||||||
use std::{ops::Range, sync::Arc};
|
use std::{ops::Range, sync::Arc};
|
||||||
|
|
||||||
use anyhow::Context as _;
|
use anyhow::Context as _;
|
||||||
use client::zed_urls;
|
|
||||||
use cloud_api_types::{ExtensionMetadata, ExtensionProvides};
|
use cloud_api_types::{ExtensionMetadata, ExtensionProvides};
|
||||||
use collections::{BTreeMap, BTreeSet};
|
use collections::{BTreeMap, BTreeSet};
|
||||||
use editor::{Editor, EditorElement, EditorStyle};
|
use editor::{Editor, EditorElement, EditorStyle};
|
||||||
|
|
@ -68,7 +67,6 @@ pub fn init(cx: &mut App) {
|
||||||
ExtensionCategoryFilter::ContextServers => {
|
ExtensionCategoryFilter::ContextServers => {
|
||||||
ExtensionProvides::ContextServers
|
ExtensionProvides::ContextServers
|
||||||
}
|
}
|
||||||
ExtensionCategoryFilter::AgentServers => ExtensionProvides::AgentServers,
|
|
||||||
ExtensionCategoryFilter::Snippets => ExtensionProvides::Snippets,
|
ExtensionCategoryFilter::Snippets => ExtensionProvides::Snippets,
|
||||||
ExtensionCategoryFilter::DebugAdapters => ExtensionProvides::DebugAdapters,
|
ExtensionCategoryFilter::DebugAdapters => ExtensionProvides::DebugAdapters,
|
||||||
});
|
});
|
||||||
|
|
@ -287,19 +285,6 @@ fn keywords_by_feature() -> &'static BTreeMap<Feature, Vec<&'static str>> {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn acp_registry_upsell_keywords() -> &'static [&'static str] {
|
|
||||||
&[
|
|
||||||
"opencode",
|
|
||||||
"mistral",
|
|
||||||
"auggie",
|
|
||||||
"stakpak",
|
|
||||||
"codebuddy",
|
|
||||||
"autohand",
|
|
||||||
"factory droid",
|
|
||||||
"corust",
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn extension_button_id(extension_id: &Arc<str>, operation: ExtensionOperation) -> ElementId {
|
fn extension_button_id(extension_id: &Arc<str>, operation: ExtensionOperation) -> ElementId {
|
||||||
(SharedString::from(extension_id.clone()), operation as usize).into()
|
(SharedString::from(extension_id.clone()), operation as usize).into()
|
||||||
}
|
}
|
||||||
|
|
@ -326,7 +311,6 @@ pub struct ExtensionsPage {
|
||||||
_subscriptions: [gpui::Subscription; 2],
|
_subscriptions: [gpui::Subscription; 2],
|
||||||
extension_fetch_task: Option<Task<()>>,
|
extension_fetch_task: Option<Task<()>>,
|
||||||
upsells: BTreeSet<Feature>,
|
upsells: BTreeSet<Feature>,
|
||||||
show_acp_registry_upsell: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ExtensionsPage {
|
impl ExtensionsPage {
|
||||||
|
|
@ -389,7 +373,6 @@ impl ExtensionsPage {
|
||||||
_subscriptions: subscriptions,
|
_subscriptions: subscriptions,
|
||||||
query_editor,
|
query_editor,
|
||||||
upsells: BTreeSet::default(),
|
upsells: BTreeSet::default(),
|
||||||
show_acp_registry_upsell: false,
|
|
||||||
};
|
};
|
||||||
this.fetch_extensions(
|
this.fetch_extensions(
|
||||||
this.search_query(cx),
|
this.search_query(cx),
|
||||||
|
|
@ -824,7 +807,8 @@ impl ExtensionsPage {
|
||||||
.iter()
|
.iter()
|
||||||
.filter_map(|provides| {
|
.filter_map(|provides| {
|
||||||
match provides {
|
match provides {
|
||||||
ExtensionProvides::SlashCommands
|
ExtensionProvides::AgentServers
|
||||||
|
| ExtensionProvides::SlashCommands
|
||||||
| ExtensionProvides::IndexedDocsProviders => {
|
| ExtensionProvides::IndexedDocsProviders => {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
@ -1416,13 +1400,11 @@ impl ExtensionsPage {
|
||||||
fn refresh_feature_upsells(&mut self, cx: &mut Context<Self>) {
|
fn refresh_feature_upsells(&mut self, cx: &mut Context<Self>) {
|
||||||
let Some(search) = self.search_query(cx) else {
|
let Some(search) = self.search_query(cx) else {
|
||||||
self.upsells.clear();
|
self.upsells.clear();
|
||||||
self.show_acp_registry_upsell = false;
|
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some(id) = search.strip_prefix("id:") {
|
if let Some(id) = search.strip_prefix("id:") {
|
||||||
self.upsells.clear();
|
self.upsells.clear();
|
||||||
self.show_acp_registry_upsell = false;
|
|
||||||
|
|
||||||
let upsell = match id.to_lowercase().as_str() {
|
let upsell = match id.to_lowercase().as_str() {
|
||||||
"ruff" => Some(Feature::ExtensionRuff),
|
"ruff" => Some(Feature::ExtensionRuff),
|
||||||
|
|
@ -1454,61 +1436,6 @@ impl ExtensionsPage {
|
||||||
self.upsells.remove(feature);
|
self.upsells.remove(feature);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.show_acp_registry_upsell = acp_registry_upsell_keywords()
|
|
||||||
.iter()
|
|
||||||
.any(|keyword| search_terms.iter().any(|term| keyword.contains(term)));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn render_acp_registry_upsell(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
|
||||||
let registry_url = zed_urls::acp_registry_blog(cx);
|
|
||||||
|
|
||||||
let view_registry = Button::new("view_registry", "View Registry")
|
|
||||||
.style(ButtonStyle::Tinted(ui::TintColor::Warning))
|
|
||||||
.on_click({
|
|
||||||
let registry_url = registry_url.clone();
|
|
||||||
move |_, window, cx| {
|
|
||||||
telemetry::event!(
|
|
||||||
"ACP Registry Opened from Extensions",
|
|
||||||
source = "ACP Registry Upsell",
|
|
||||||
url = registry_url,
|
|
||||||
);
|
|
||||||
window.dispatch_action(Box::new(zed_actions::AcpRegistry), cx)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
let open_registry_button = Button::new("open_registry", "Learn More")
|
|
||||||
.end_icon(
|
|
||||||
Icon::new(IconName::ArrowUpRight)
|
|
||||||
.size(IconSize::Small)
|
|
||||||
.color(Color::Muted),
|
|
||||||
)
|
|
||||||
.on_click({
|
|
||||||
move |_event, _window, cx| {
|
|
||||||
telemetry::event!(
|
|
||||||
"ACP Registry Viewed",
|
|
||||||
source = "ACP Registry Upsell",
|
|
||||||
url = registry_url,
|
|
||||||
);
|
|
||||||
cx.open_url(®istry_url)
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
div().pt_4().px_4().child(
|
|
||||||
Banner::new()
|
|
||||||
.severity(Severity::Warning)
|
|
||||||
.child(
|
|
||||||
Label::new(
|
|
||||||
"Agent Server extensions will be deprecated in favor of the ACP registry.",
|
|
||||||
)
|
|
||||||
.mt_0p5(),
|
|
||||||
)
|
|
||||||
.action_slot(
|
|
||||||
h_flex()
|
|
||||||
.gap_1()
|
|
||||||
.child(open_registry_button)
|
|
||||||
.child(view_registry),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn render_feature_upsell_banner(
|
fn render_feature_upsell_banner(
|
||||||
|
|
@ -1832,11 +1759,6 @@ impl Render for ExtensionsPage {
|
||||||
)
|
)
|
||||||
})),
|
})),
|
||||||
)
|
)
|
||||||
.when(
|
|
||||||
self.provides_filter == Some(ExtensionProvides::AgentServers)
|
|
||||||
|| self.show_acp_registry_upsell,
|
|
||||||
|this| this.child(self.render_acp_registry_upsell(cx)),
|
|
||||||
)
|
|
||||||
.child(self.render_feature_upsells(cx))
|
.child(self.render_feature_upsells(cx))
|
||||||
.child(v_flex().px_4().size_full().overflow_y_hidden().map(|this| {
|
.child(v_flex().px_4().size_full().overflow_y_hidden().map(|this| {
|
||||||
let mut count = self.filtered_remote_extension_indices.len();
|
let mut count = self.filtered_remote_extension_indices.len();
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
use std::{
|
use std::{
|
||||||
any::Any,
|
any::Any,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
sync::Arc,
|
sync::{Arc, LazyLock},
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -17,14 +17,11 @@ use http_client::{HttpClient, github::AssetKind};
|
||||||
use node_runtime::NodeRuntime;
|
use node_runtime::NodeRuntime;
|
||||||
use percent_encoding::percent_decode_str;
|
use percent_encoding::percent_decode_str;
|
||||||
use remote::RemoteClient;
|
use remote::RemoteClient;
|
||||||
use rpc::{
|
use rpc::{AnyProtoClient, TypedEnvelope, proto};
|
||||||
AnyProtoClient, TypedEnvelope,
|
|
||||||
proto::{self, ExternalExtensionAgent},
|
|
||||||
};
|
|
||||||
use schemars::JsonSchema;
|
use schemars::JsonSchema;
|
||||||
use semver::Version;
|
use semver::Version;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use settings::{RegisterSetting, SettingsStore};
|
use settings::{RegisterSetting, SettingsStore, update_settings_file};
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
use util::{ResultExt as _, debug_panic};
|
use util::{ResultExt as _, debug_panic};
|
||||||
|
|
@ -114,7 +111,6 @@ impl std::borrow::Borrow<str> for AgentId {
|
||||||
pub enum ExternalAgentSource {
|
pub enum ExternalAgentSource {
|
||||||
#[default]
|
#[default]
|
||||||
Custom,
|
Custom,
|
||||||
Extension,
|
|
||||||
Registry,
|
Registry,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -140,16 +136,6 @@ pub trait ExternalAgentServer {
|
||||||
fn as_any_mut(&mut self) -> &mut dyn Any;
|
fn as_any_mut(&mut self) -> &mut dyn Any;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct ExtensionAgentEntry {
|
|
||||||
agent_name: Arc<str>,
|
|
||||||
extension_id: String,
|
|
||||||
targets: HashMap<String, extension::TargetConfig>,
|
|
||||||
env: HashMap<String, String>,
|
|
||||||
icon_path: Option<String>,
|
|
||||||
display_name: Option<SharedString>,
|
|
||||||
version: Option<SharedString>,
|
|
||||||
}
|
|
||||||
|
|
||||||
enum AgentServerStoreState {
|
enum AgentServerStoreState {
|
||||||
Local {
|
Local {
|
||||||
node_runtime: NodeRuntime,
|
node_runtime: NodeRuntime,
|
||||||
|
|
@ -158,7 +144,6 @@ enum AgentServerStoreState {
|
||||||
downstream_client: Option<(u64, AnyProtoClient)>,
|
downstream_client: Option<(u64, AnyProtoClient)>,
|
||||||
settings: Option<AllAgentServersSettings>,
|
settings: Option<AllAgentServersSettings>,
|
||||||
http_client: Arc<dyn HttpClient>,
|
http_client: Arc<dyn HttpClient>,
|
||||||
extension_agents: Vec<ExtensionAgentEntry>,
|
|
||||||
_subscriptions: Vec<Subscription>,
|
_subscriptions: Vec<Subscription>,
|
||||||
},
|
},
|
||||||
Remote {
|
Remote {
|
||||||
|
|
@ -201,123 +186,54 @@ pub struct AgentServersUpdated;
|
||||||
|
|
||||||
impl EventEmitter<AgentServersUpdated> for AgentServerStore {}
|
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 {
|
impl AgentServerStore {
|
||||||
/// Synchronizes extension-provided agent servers with the store.
|
pub fn migrate_agent_server_from_extensions(
|
||||||
pub fn sync_extension_agents<'a, I>(
|
|
||||||
&mut self,
|
&mut self,
|
||||||
manifests: I,
|
id: Arc<str>,
|
||||||
extensions_dir: PathBuf,
|
fs: Arc<dyn Fs>,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) where
|
) {
|
||||||
I: IntoIterator<Item = (&'a str, &'a extension::ExtensionManifest)>,
|
let Some(registry_id) = EXTENSION_TO_REGISTRY_IDS.get(id.as_ref()) else {
|
||||||
{
|
return;
|
||||||
// Collect manifests first so we can iterate twice
|
};
|
||||||
let manifests: Vec<_> = manifests.into_iter().collect();
|
|
||||||
|
|
||||||
// Remove all extension-provided agents
|
update_settings_file(fs, cx, move |settings, _| {
|
||||||
// (They will be re-added below if they're in the currently installed extensions)
|
let agent_servers = settings.agent_servers.get_or_insert_default();
|
||||||
self.external_agents
|
// Take the old settings
|
||||||
.retain(|_, entry| entry.source != ExternalAgentSource::Extension);
|
let settings = agent_servers.remove(id.as_ref());
|
||||||
|
// If they had both installed, just remove the extension settings, leave theirregistry settings alone
|
||||||
// Insert agent servers from extension manifests
|
if agent_servers.contains_key(*registry_id) {
|
||||||
match &mut self.state {
|
return;
|
||||||
AgentServerStoreState::Local {
|
|
||||||
extension_agents, ..
|
|
||||||
} => {
|
|
||||||
extension_agents.clear();
|
|
||||||
for (ext_id, manifest) in manifests {
|
|
||||||
for (agent_name, agent_entry) in &manifest.agent_servers {
|
|
||||||
let display_name = SharedString::from(agent_entry.name.clone());
|
|
||||||
let icon_path = agent_entry.icon.as_ref().and_then(|icon| {
|
|
||||||
resolve_extension_icon_path(&extensions_dir, ext_id, icon)
|
|
||||||
});
|
|
||||||
|
|
||||||
extension_agents.push(ExtensionAgentEntry {
|
|
||||||
agent_name: agent_name.clone(),
|
|
||||||
extension_id: ext_id.to_owned(),
|
|
||||||
targets: agent_entry.targets.clone(),
|
|
||||||
env: agent_entry.env.clone(),
|
|
||||||
icon_path,
|
|
||||||
display_name: Some(display_name),
|
|
||||||
version: Some(SharedString::from(manifest.version.clone())),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
self.reregister_agents(cx);
|
|
||||||
}
|
}
|
||||||
AgentServerStoreState::Remote {
|
// Insert the old settings, or write new ones so it is "installed" via the registry
|
||||||
project_id,
|
agent_servers.insert(
|
||||||
upstream_client,
|
registry_id.to_string(),
|
||||||
worktree_store,
|
settings.unwrap_or_else(|| settings::CustomAgentServerSettings::Registry {
|
||||||
} => {
|
default_mode: None,
|
||||||
let mut agents = vec![];
|
default_model: None,
|
||||||
for (ext_id, manifest) in manifests {
|
env: Default::default(),
|
||||||
for (agent_name, agent_entry) in &manifest.agent_servers {
|
favorite_models: Vec::new(),
|
||||||
let display_name = SharedString::from(agent_entry.name.clone());
|
default_config_options: HashMap::default(),
|
||||||
let icon_path = agent_entry.icon.as_ref().and_then(|icon| {
|
favorite_config_option_values: HashMap::default(),
|
||||||
resolve_extension_icon_path(&extensions_dir, ext_id, icon)
|
}),
|
||||||
});
|
);
|
||||||
let icon_shared = icon_path
|
});
|
||||||
.as_ref()
|
|
||||||
.map(|path| SharedString::from(path.clone()));
|
|
||||||
let icon = icon_path;
|
|
||||||
let agent_server_name = AgentId(agent_name.clone().into());
|
|
||||||
self.external_agents
|
|
||||||
.entry(agent_server_name.clone())
|
|
||||||
.and_modify(|entry| {
|
|
||||||
entry.icon = icon_shared.clone();
|
|
||||||
entry.display_name = Some(display_name.clone());
|
|
||||||
entry.source = ExternalAgentSource::Extension;
|
|
||||||
})
|
|
||||||
.or_insert_with(|| {
|
|
||||||
ExternalAgentEntry::new(
|
|
||||||
Box::new(RemoteExternalAgentServer {
|
|
||||||
project_id: *project_id,
|
|
||||||
upstream_client: upstream_client.clone(),
|
|
||||||
worktree_store: worktree_store.clone(),
|
|
||||||
name: agent_server_name.clone(),
|
|
||||||
new_version_available_tx: None,
|
|
||||||
})
|
|
||||||
as Box<dyn ExternalAgentServer>,
|
|
||||||
ExternalAgentSource::Extension,
|
|
||||||
icon_shared.clone(),
|
|
||||||
Some(display_name.clone()),
|
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
agents.push(ExternalExtensionAgent {
|
|
||||||
name: agent_name.to_string(),
|
|
||||||
icon_path: icon,
|
|
||||||
extension_id: ext_id.to_string(),
|
|
||||||
targets: agent_entry
|
|
||||||
.targets
|
|
||||||
.iter()
|
|
||||||
.map(|(k, v)| (k.clone(), v.to_proto()))
|
|
||||||
.collect(),
|
|
||||||
env: agent_entry
|
|
||||||
.env
|
|
||||||
.iter()
|
|
||||||
.map(|(k, v)| (k.clone(), v.clone()))
|
|
||||||
.collect(),
|
|
||||||
version: Some(manifest.version.to_string()),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
upstream_client
|
|
||||||
.read(cx)
|
|
||||||
.proto_client()
|
|
||||||
.send(proto::ExternalExtensionAgentsUpdated {
|
|
||||||
project_id: *project_id,
|
|
||||||
agents,
|
|
||||||
})
|
|
||||||
.log_err();
|
|
||||||
}
|
|
||||||
AgentServerStoreState::Collab => {
|
|
||||||
// Do nothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cx.emit(AgentServersUpdated);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn agent_icon(&self, id: &AgentId) -> Option<SharedString> {
|
pub fn agent_icon(&self, id: &AgentId) -> Option<SharedString> {
|
||||||
|
|
@ -331,46 +247,6 @@ impl AgentServerStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Safely resolves an extension icon path, ensuring it stays within the extension directory.
|
|
||||||
/// Returns `None` if the path would escape the extension directory (path traversal attack).
|
|
||||||
pub fn resolve_extension_icon_path(
|
|
||||||
extensions_dir: &Path,
|
|
||||||
extension_id: &str,
|
|
||||||
icon_relative_path: &str,
|
|
||||||
) -> Option<String> {
|
|
||||||
let extension_root = extensions_dir.join(extension_id);
|
|
||||||
let icon_path = extension_root.join(icon_relative_path);
|
|
||||||
|
|
||||||
// Canonicalize both paths to resolve symlinks and normalize the paths.
|
|
||||||
// For the extension root, we need to handle the case where it might be a symlink
|
|
||||||
// (common for dev extensions).
|
|
||||||
let canonical_extension_root = extension_root.canonicalize().unwrap_or(extension_root);
|
|
||||||
let canonical_icon_path = match icon_path.canonicalize() {
|
|
||||||
Ok(path) => path,
|
|
||||||
Err(err) => {
|
|
||||||
log::warn!(
|
|
||||||
"Failed to canonicalize icon path for extension '{}': {} (path: {})",
|
|
||||||
extension_id,
|
|
||||||
err,
|
|
||||||
icon_relative_path
|
|
||||||
);
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Verify the resolved icon path is within the extension directory
|
|
||||||
if canonical_icon_path.starts_with(&canonical_extension_root) {
|
|
||||||
Some(canonical_icon_path.to_string_lossy().to_string())
|
|
||||||
} else {
|
|
||||||
log::warn!(
|
|
||||||
"Icon path '{}' for extension '{}' escapes extension directory, ignoring for security",
|
|
||||||
icon_relative_path,
|
|
||||||
extension_id
|
|
||||||
);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl AgentServerStore {
|
impl AgentServerStore {
|
||||||
pub fn agent_display_name(&self, name: &AgentId) -> Option<SharedString> {
|
pub fn agent_display_name(&self, name: &AgentId) -> Option<SharedString> {
|
||||||
self.external_agents
|
self.external_agents
|
||||||
|
|
@ -384,7 +260,6 @@ impl AgentServerStore {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn init_headless(session: &AnyProtoClient) {
|
pub fn init_headless(session: &AnyProtoClient) {
|
||||||
session.add_entity_message_handler(Self::handle_external_extension_agents_updated);
|
|
||||||
session.add_entity_request_handler(Self::handle_get_agent_server_command);
|
session.add_entity_request_handler(Self::handle_get_agent_server_command);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -419,7 +294,6 @@ impl AgentServerStore {
|
||||||
downstream_client,
|
downstream_client,
|
||||||
settings: old_settings,
|
settings: old_settings,
|
||||||
http_client,
|
http_client,
|
||||||
extension_agents,
|
|
||||||
..
|
..
|
||||||
} = &mut self.state
|
} = &mut self.state
|
||||||
else {
|
else {
|
||||||
|
|
@ -470,47 +344,6 @@ impl AgentServerStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Insert extension agents before custom/registry so registry entries override extensions.
|
|
||||||
for entry in extension_agents.iter() {
|
|
||||||
let name = AgentId(entry.agent_name.clone().into());
|
|
||||||
let mut env = entry.env.clone();
|
|
||||||
if let Some(settings_env) =
|
|
||||||
new_settings
|
|
||||||
.get(entry.agent_name.as_ref())
|
|
||||||
.and_then(|settings| match settings {
|
|
||||||
CustomAgentServerSettings::Extension { env, .. } => Some(env.clone()),
|
|
||||||
_ => None,
|
|
||||||
})
|
|
||||||
{
|
|
||||||
env.extend(settings_env);
|
|
||||||
}
|
|
||||||
let icon = entry
|
|
||||||
.icon_path
|
|
||||||
.as_ref()
|
|
||||||
.map(|path| SharedString::from(path.clone()));
|
|
||||||
|
|
||||||
self.external_agents.insert(
|
|
||||||
name.clone(),
|
|
||||||
ExternalAgentEntry::new(
|
|
||||||
Box::new(LocalExtensionArchiveAgent {
|
|
||||||
fs: fs.clone(),
|
|
||||||
http_client: http_client.clone(),
|
|
||||||
node_runtime: node_runtime.clone(),
|
|
||||||
project_environment: project_environment.clone(),
|
|
||||||
extension_id: Arc::from(&*entry.extension_id),
|
|
||||||
targets: entry.targets.clone(),
|
|
||||||
env,
|
|
||||||
agent_id: entry.agent_name.clone(),
|
|
||||||
version: entry.version.clone(),
|
|
||||||
new_version_available_tx: None,
|
|
||||||
}) as Box<dyn ExternalAgentServer>,
|
|
||||||
ExternalAgentSource::Extension,
|
|
||||||
icon,
|
|
||||||
entry.display_name.clone(),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (name, settings) in new_settings.iter() {
|
for (name, settings) in new_settings.iter() {
|
||||||
match settings {
|
match settings {
|
||||||
CustomAgentServerSettings::Custom { command, .. } => {
|
CustomAgentServerSettings::Custom { command, .. } => {
|
||||||
|
|
@ -593,7 +426,6 @@ impl AgentServerStore {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
CustomAgentServerSettings::Extension { .. } => {}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -662,12 +494,10 @@ impl AgentServerStore {
|
||||||
http_client,
|
http_client,
|
||||||
downstream_client: None,
|
downstream_client: None,
|
||||||
settings: None,
|
settings: None,
|
||||||
extension_agents: vec![],
|
|
||||||
_subscriptions: subscriptions,
|
_subscriptions: subscriptions,
|
||||||
},
|
},
|
||||||
external_agents: HashMap::default(),
|
external_agents: HashMap::default(),
|
||||||
};
|
};
|
||||||
if let Some(_events) = extension::ExtensionEvents::try_global(cx) {}
|
|
||||||
this.agent_servers_settings_changed(cx);
|
this.agent_servers_settings_changed(cx);
|
||||||
this
|
this
|
||||||
}
|
}
|
||||||
|
|
@ -900,52 +730,6 @@ impl AgentServerStore {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn handle_external_extension_agents_updated(
|
|
||||||
this: Entity<Self>,
|
|
||||||
envelope: TypedEnvelope<proto::ExternalExtensionAgentsUpdated>,
|
|
||||||
mut cx: AsyncApp,
|
|
||||||
) -> Result<()> {
|
|
||||||
this.update(&mut cx, |this, cx| {
|
|
||||||
let AgentServerStoreState::Local {
|
|
||||||
extension_agents, ..
|
|
||||||
} = &mut this.state
|
|
||||||
else {
|
|
||||||
panic!(
|
|
||||||
"handle_external_extension_agents_updated \
|
|
||||||
should not be called for a non-remote project"
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
extension_agents.clear();
|
|
||||||
for ExternalExtensionAgent {
|
|
||||||
name,
|
|
||||||
icon_path,
|
|
||||||
extension_id,
|
|
||||||
targets,
|
|
||||||
env,
|
|
||||||
version,
|
|
||||||
} in envelope.payload.agents
|
|
||||||
{
|
|
||||||
extension_agents.push(ExtensionAgentEntry {
|
|
||||||
agent_name: Arc::from(&*name),
|
|
||||||
extension_id,
|
|
||||||
targets: targets
|
|
||||||
.into_iter()
|
|
||||||
.map(|(k, v)| (k, extension::TargetConfig::from_proto(v)))
|
|
||||||
.collect(),
|
|
||||||
env: env.into_iter().collect(),
|
|
||||||
icon_path,
|
|
||||||
display_name: None,
|
|
||||||
version: version.map(SharedString::from),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.reregister_agents(cx);
|
|
||||||
cx.emit(AgentServersUpdated);
|
|
||||||
Ok(())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn handle_new_version_available(
|
async fn handle_new_version_available(
|
||||||
this: Entity<Self>,
|
this: Entity<Self>,
|
||||||
envelope: TypedEnvelope<proto::NewExternalAgentVersionAvailable>,
|
envelope: TypedEnvelope<proto::NewExternalAgentVersionAvailable>,
|
||||||
|
|
@ -961,16 +745,6 @@ impl AgentServerStore {
|
||||||
});
|
});
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_extension_id_for_agent(&self, name: &AgentId) -> Option<Arc<str>> {
|
|
||||||
self.external_agents.get(name).and_then(|entry| {
|
|
||||||
entry
|
|
||||||
.server
|
|
||||||
.as_any()
|
|
||||||
.downcast_ref::<LocalExtensionArchiveAgent>()
|
|
||||||
.map(|ext_agent| ext_agent.extension_id.clone())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct RemoteExternalAgentServer {
|
struct RemoteExternalAgentServer {
|
||||||
|
|
@ -1196,213 +970,6 @@ async fn remove_stale_versioned_archive_cache_dirs(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct LocalExtensionArchiveAgent {
|
|
||||||
pub fs: Arc<dyn Fs>,
|
|
||||||
pub http_client: Arc<dyn HttpClient>,
|
|
||||||
pub node_runtime: NodeRuntime,
|
|
||||||
pub project_environment: Entity<ProjectEnvironment>,
|
|
||||||
pub extension_id: Arc<str>,
|
|
||||||
pub agent_id: Arc<str>,
|
|
||||||
pub targets: HashMap<String, extension::TargetConfig>,
|
|
||||||
pub env: HashMap<String, String>,
|
|
||||||
pub version: Option<SharedString>,
|
|
||||||
pub new_version_available_tx: Option<watch::Sender<Option<String>>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ExternalAgentServer for LocalExtensionArchiveAgent {
|
|
||||||
fn version(&self) -> Option<&SharedString> {
|
|
||||||
self.version.as_ref()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn take_new_version_available_tx(&mut self) -> Option<watch::Sender<Option<String>>> {
|
|
||||||
self.new_version_available_tx.take()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn set_new_version_available_tx(&mut self, tx: watch::Sender<Option<String>>) {
|
|
||||||
self.new_version_available_tx = Some(tx);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_command(
|
|
||||||
&self,
|
|
||||||
extra_args: Vec<String>,
|
|
||||||
extra_env: HashMap<String, String>,
|
|
||||||
cx: &mut AsyncApp,
|
|
||||||
) -> Task<Result<AgentServerCommand>> {
|
|
||||||
let fs = self.fs.clone();
|
|
||||||
let http_client = self.http_client.clone();
|
|
||||||
let node_runtime = self.node_runtime.clone();
|
|
||||||
let project_environment = self.project_environment.downgrade();
|
|
||||||
let extension_id = self.extension_id.clone();
|
|
||||||
let agent_id = self.agent_id.clone();
|
|
||||||
let targets = self.targets.clone();
|
|
||||||
let base_env = self.env.clone();
|
|
||||||
let version = self.version.clone();
|
|
||||||
|
|
||||||
cx.spawn(async move |cx| {
|
|
||||||
// Get project environment
|
|
||||||
let mut env = project_environment
|
|
||||||
.update(cx, |project_environment, cx| {
|
|
||||||
project_environment.default_environment(cx)
|
|
||||||
})?
|
|
||||||
.await
|
|
||||||
.unwrap_or_default();
|
|
||||||
|
|
||||||
// Merge manifest env and extra env
|
|
||||||
env.extend(base_env);
|
|
||||||
env.extend(extra_env);
|
|
||||||
|
|
||||||
let cache_key = format!("{}/{}", extension_id, agent_id);
|
|
||||||
let dir = paths::external_agents_dir().join(&cache_key);
|
|
||||||
fs.create_dir(&dir).await?;
|
|
||||||
|
|
||||||
// Determine platform key
|
|
||||||
let os = if cfg!(target_os = "macos") {
|
|
||||||
"darwin"
|
|
||||||
} else if cfg!(target_os = "linux") {
|
|
||||||
"linux"
|
|
||||||
} else if cfg!(target_os = "windows") {
|
|
||||||
"windows"
|
|
||||||
} else {
|
|
||||||
anyhow::bail!("unsupported OS");
|
|
||||||
};
|
|
||||||
|
|
||||||
let arch = if cfg!(target_arch = "aarch64") {
|
|
||||||
"aarch64"
|
|
||||||
} else if cfg!(target_arch = "x86_64") {
|
|
||||||
"x86_64"
|
|
||||||
} else {
|
|
||||||
anyhow::bail!("unsupported architecture");
|
|
||||||
};
|
|
||||||
|
|
||||||
let platform_key = format!("{}-{}", os, arch);
|
|
||||||
let target_config = targets.get(&platform_key).with_context(|| {
|
|
||||||
format!(
|
|
||||||
"no target specified for platform '{}'. Available platforms: {}",
|
|
||||||
platform_key,
|
|
||||||
targets
|
|
||||||
.keys()
|
|
||||||
.map(|k| k.as_str())
|
|
||||||
.collect::<Vec<_>>()
|
|
||||||
.join(", ")
|
|
||||||
)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
let archive_url = &target_config.archive;
|
|
||||||
let version_dir = versioned_archive_cache_dir(
|
|
||||||
&dir,
|
|
||||||
version.as_ref().map(|version| version.as_ref()),
|
|
||||||
archive_url,
|
|
||||||
);
|
|
||||||
|
|
||||||
if !fs.is_dir(&version_dir).await {
|
|
||||||
// Determine SHA256 for verification
|
|
||||||
let sha256 = if let Some(provided_sha) = &target_config.sha256 {
|
|
||||||
// Use provided SHA256
|
|
||||||
Some(provided_sha.clone())
|
|
||||||
} else if let Some(github_archive) = github_release_archive_from_url(archive_url) {
|
|
||||||
// Try to fetch SHA256 from GitHub API
|
|
||||||
if let Ok(release) = ::http_client::github::get_release_by_tag_name(
|
|
||||||
&github_archive.repo_name_with_owner,
|
|
||||||
&github_archive.tag,
|
|
||||||
http_client.clone(),
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
{
|
|
||||||
// Find matching asset
|
|
||||||
if let Some(asset) = release
|
|
||||||
.assets
|
|
||||||
.iter()
|
|
||||||
.find(|a| a.name == github_archive.asset_name)
|
|
||||||
{
|
|
||||||
// Strip "sha256:" prefix if present
|
|
||||||
asset.digest.as_ref().map(|d| {
|
|
||||||
d.strip_prefix("sha256:")
|
|
||||||
.map(|s| s.to_string())
|
|
||||||
.unwrap_or_else(|| d.clone())
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
let asset_kind = asset_kind_for_archive_url(archive_url)?;
|
|
||||||
|
|
||||||
// Download and extract
|
|
||||||
::http_client::github_download::download_server_binary(
|
|
||||||
&*http_client,
|
|
||||||
archive_url,
|
|
||||||
sha256.as_deref(),
|
|
||||||
&version_dir,
|
|
||||||
asset_kind,
|
|
||||||
)
|
|
||||||
.await?;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate and resolve cmd path
|
|
||||||
let cmd = &target_config.cmd;
|
|
||||||
|
|
||||||
let cmd_path = if cmd == "node" {
|
|
||||||
// Use Zed's managed Node.js runtime
|
|
||||||
node_runtime.binary_path().await?
|
|
||||||
} else {
|
|
||||||
if cmd.contains("..") {
|
|
||||||
anyhow::bail!("command path cannot contain '..': {}", cmd);
|
|
||||||
}
|
|
||||||
|
|
||||||
if cmd.starts_with("./") || cmd.starts_with(".\\") {
|
|
||||||
// Relative to extraction directory
|
|
||||||
let cmd_path = version_dir.join(&cmd[2..]);
|
|
||||||
anyhow::ensure!(
|
|
||||||
fs.is_file(&cmd_path).await,
|
|
||||||
"Missing command {} after extraction",
|
|
||||||
cmd_path.to_string_lossy()
|
|
||||||
);
|
|
||||||
cmd_path
|
|
||||||
} else {
|
|
||||||
// On PATH
|
|
||||||
anyhow::bail!("command must be relative (start with './'): {}", cmd);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
cx.background_spawn({
|
|
||||||
let fs = fs.clone();
|
|
||||||
let dir = dir.clone();
|
|
||||||
let version_dir = version_dir.clone();
|
|
||||||
async move {
|
|
||||||
remove_stale_versioned_archive_cache_dirs(fs, &dir, &version_dir)
|
|
||||||
.await
|
|
||||||
.log_err();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
let mut args = target_config.args.clone();
|
|
||||||
args.extend(extra_args);
|
|
||||||
|
|
||||||
let command = AgentServerCommand {
|
|
||||||
path: cmd_path,
|
|
||||||
args,
|
|
||||||
env: Some(env),
|
|
||||||
};
|
|
||||||
|
|
||||||
Ok(command)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn as_any(&self) -> &dyn Any {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn as_any_mut(&mut self) -> &mut dyn Any {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct LocalRegistryArchiveAgent {
|
struct LocalRegistryArchiveAgent {
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
http_client: Arc<dyn HttpClient>,
|
http_client: Arc<dyn HttpClient>,
|
||||||
|
|
@ -1813,40 +1380,6 @@ pub enum CustomAgentServerSettings {
|
||||||
/// Default: {}
|
/// Default: {}
|
||||||
favorite_config_option_values: HashMap<String, Vec<String>>,
|
favorite_config_option_values: HashMap<String, Vec<String>>,
|
||||||
},
|
},
|
||||||
Extension {
|
|
||||||
/// Additional environment variables to pass to the agent.
|
|
||||||
///
|
|
||||||
/// Default: {}
|
|
||||||
env: HashMap<String, String>,
|
|
||||||
/// The default mode to use for this agent.
|
|
||||||
///
|
|
||||||
/// Note: Not only all agents support modes.
|
|
||||||
///
|
|
||||||
/// Default: None
|
|
||||||
default_mode: Option<String>,
|
|
||||||
/// The default model to use for this agent.
|
|
||||||
///
|
|
||||||
/// This should be the model ID as reported by the agent.
|
|
||||||
///
|
|
||||||
/// Default: None
|
|
||||||
default_model: Option<String>,
|
|
||||||
/// The favorite models for this agent.
|
|
||||||
///
|
|
||||||
/// Default: []
|
|
||||||
favorite_models: Vec<String>,
|
|
||||||
/// Default values for session config options.
|
|
||||||
///
|
|
||||||
/// This is a map from config option ID to value ID.
|
|
||||||
///
|
|
||||||
/// Default: {}
|
|
||||||
default_config_options: HashMap<String, String>,
|
|
||||||
/// Favorited values for session config options.
|
|
||||||
///
|
|
||||||
/// This is a map from config option ID to a list of favorited value IDs.
|
|
||||||
///
|
|
||||||
/// Default: {}
|
|
||||||
favorite_config_option_values: HashMap<String, Vec<String>>,
|
|
||||||
},
|
|
||||||
Registry {
|
Registry {
|
||||||
/// Additional environment variables to pass to the agent.
|
/// Additional environment variables to pass to the agent.
|
||||||
///
|
///
|
||||||
|
|
@ -1887,15 +1420,13 @@ impl CustomAgentServerSettings {
|
||||||
pub fn command(&self) -> Option<&AgentServerCommand> {
|
pub fn command(&self) -> Option<&AgentServerCommand> {
|
||||||
match self {
|
match self {
|
||||||
CustomAgentServerSettings::Custom { command, .. } => Some(command),
|
CustomAgentServerSettings::Custom { command, .. } => Some(command),
|
||||||
CustomAgentServerSettings::Extension { .. }
|
CustomAgentServerSettings::Registry { .. } => None,
|
||||||
| CustomAgentServerSettings::Registry { .. } => None,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn default_mode(&self) -> Option<&str> {
|
pub fn default_mode(&self) -> Option<&str> {
|
||||||
match self {
|
match self {
|
||||||
CustomAgentServerSettings::Custom { default_mode, .. }
|
CustomAgentServerSettings::Custom { default_mode, .. }
|
||||||
| CustomAgentServerSettings::Extension { default_mode, .. }
|
|
||||||
| CustomAgentServerSettings::Registry { default_mode, .. } => default_mode.as_deref(),
|
| CustomAgentServerSettings::Registry { default_mode, .. } => default_mode.as_deref(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1903,7 +1434,6 @@ impl CustomAgentServerSettings {
|
||||||
pub fn default_model(&self) -> Option<&str> {
|
pub fn default_model(&self) -> Option<&str> {
|
||||||
match self {
|
match self {
|
||||||
CustomAgentServerSettings::Custom { default_model, .. }
|
CustomAgentServerSettings::Custom { default_model, .. }
|
||||||
| CustomAgentServerSettings::Extension { default_model, .. }
|
|
||||||
| CustomAgentServerSettings::Registry { default_model, .. } => default_model.as_deref(),
|
| CustomAgentServerSettings::Registry { default_model, .. } => default_model.as_deref(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1913,9 +1443,6 @@ impl CustomAgentServerSettings {
|
||||||
CustomAgentServerSettings::Custom {
|
CustomAgentServerSettings::Custom {
|
||||||
favorite_models, ..
|
favorite_models, ..
|
||||||
}
|
}
|
||||||
| CustomAgentServerSettings::Extension {
|
|
||||||
favorite_models, ..
|
|
||||||
}
|
|
||||||
| CustomAgentServerSettings::Registry {
|
| CustomAgentServerSettings::Registry {
|
||||||
favorite_models, ..
|
favorite_models, ..
|
||||||
} => favorite_models,
|
} => favorite_models,
|
||||||
|
|
@ -1928,10 +1455,6 @@ impl CustomAgentServerSettings {
|
||||||
default_config_options,
|
default_config_options,
|
||||||
..
|
..
|
||||||
}
|
}
|
||||||
| CustomAgentServerSettings::Extension {
|
|
||||||
default_config_options,
|
|
||||||
..
|
|
||||||
}
|
|
||||||
| CustomAgentServerSettings::Registry {
|
| CustomAgentServerSettings::Registry {
|
||||||
default_config_options,
|
default_config_options,
|
||||||
..
|
..
|
||||||
|
|
@ -1945,10 +1468,6 @@ impl CustomAgentServerSettings {
|
||||||
favorite_config_option_values,
|
favorite_config_option_values,
|
||||||
..
|
..
|
||||||
}
|
}
|
||||||
| CustomAgentServerSettings::Extension {
|
|
||||||
favorite_config_option_values,
|
|
||||||
..
|
|
||||||
}
|
|
||||||
| CustomAgentServerSettings::Registry {
|
| CustomAgentServerSettings::Registry {
|
||||||
favorite_config_option_values,
|
favorite_config_option_values,
|
||||||
..
|
..
|
||||||
|
|
@ -1983,21 +1502,6 @@ impl From<settings::CustomAgentServerSettings> for CustomAgentServerSettings {
|
||||||
default_config_options,
|
default_config_options,
|
||||||
favorite_config_option_values,
|
favorite_config_option_values,
|
||||||
},
|
},
|
||||||
settings::CustomAgentServerSettings::Extension {
|
|
||||||
env,
|
|
||||||
default_mode,
|
|
||||||
default_model,
|
|
||||||
default_config_options,
|
|
||||||
favorite_models,
|
|
||||||
favorite_config_option_values,
|
|
||||||
} => CustomAgentServerSettings::Extension {
|
|
||||||
env,
|
|
||||||
default_mode,
|
|
||||||
default_model,
|
|
||||||
default_config_options,
|
|
||||||
favorite_models,
|
|
||||||
favorite_config_option_values,
|
|
||||||
},
|
|
||||||
settings::CustomAgentServerSettings::Registry {
|
settings::CustomAgentServerSettings::Registry {
|
||||||
env,
|
env,
|
||||||
default_mode,
|
default_mode,
|
||||||
|
|
@ -2024,7 +1528,15 @@ impl settings::Settings for AllAgentServersSettings {
|
||||||
agent_settings
|
agent_settings
|
||||||
.0
|
.0
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.map(|(k, v)| (k, v.into()))
|
.map(|(k, v)| {
|
||||||
|
(
|
||||||
|
EXTENSION_TO_REGISTRY_IDS
|
||||||
|
.get(&k.as_str())
|
||||||
|
.map(|v| v.to_string())
|
||||||
|
.unwrap_or(k),
|
||||||
|
v.into(),
|
||||||
|
)
|
||||||
|
})
|
||||||
.collect(),
|
.collect(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,224 +0,0 @@
|
||||||
use anyhow::Result;
|
|
||||||
use collections::HashMap;
|
|
||||||
use gpui::{AsyncApp, SharedString, Task};
|
|
||||||
use project::agent_server_store::*;
|
|
||||||
use std::{any::Any, collections::HashSet, fmt::Write as _, path::PathBuf};
|
|
||||||
// A simple fake that implements ExternalAgentServer without needing async plumbing.
|
|
||||||
struct NoopExternalAgent;
|
|
||||||
|
|
||||||
impl ExternalAgentServer for NoopExternalAgent {
|
|
||||||
fn get_command(
|
|
||||||
&self,
|
|
||||||
_extra_args: Vec<String>,
|
|
||||||
_extra_env: HashMap<String, String>,
|
|
||||||
_cx: &mut AsyncApp,
|
|
||||||
) -> Task<Result<AgentServerCommand>> {
|
|
||||||
Task::ready(Ok(AgentServerCommand {
|
|
||||||
path: PathBuf::from("noop"),
|
|
||||||
args: Vec::new(),
|
|
||||||
env: None,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn as_any(&self) -> &dyn Any {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn as_any_mut(&mut self) -> &mut dyn Any {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn external_agent_server_name_display() {
|
|
||||||
let name = AgentId(SharedString::from("Ext: Tool"));
|
|
||||||
let mut s = String::new();
|
|
||||||
write!(&mut s, "{name}").unwrap();
|
|
||||||
assert_eq!(s, "Ext: Tool");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn sync_extension_agents_removes_previous_extension_entries() {
|
|
||||||
let mut store = AgentServerStore::collab();
|
|
||||||
|
|
||||||
// Seed with a couple of agents that will be replaced by extensions
|
|
||||||
store.external_agents.insert(
|
|
||||||
AgentId(SharedString::from("foo-agent")),
|
|
||||||
ExternalAgentEntry::new(
|
|
||||||
Box::new(NoopExternalAgent) as Box<dyn ExternalAgentServer>,
|
|
||||||
ExternalAgentSource::Custom,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
store.external_agents.insert(
|
|
||||||
AgentId(SharedString::from("bar-agent")),
|
|
||||||
ExternalAgentEntry::new(
|
|
||||||
Box::new(NoopExternalAgent) as Box<dyn ExternalAgentServer>,
|
|
||||||
ExternalAgentSource::Custom,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
store.external_agents.insert(
|
|
||||||
AgentId(SharedString::from("custom")),
|
|
||||||
ExternalAgentEntry::new(
|
|
||||||
Box::new(NoopExternalAgent) as Box<dyn ExternalAgentServer>,
|
|
||||||
ExternalAgentSource::Custom,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Simulate the removal phase: if we're syncing extensions that provide
|
|
||||||
// "foo-agent" and "bar-agent", those should be removed first
|
|
||||||
let extension_agent_names: HashSet<String> = ["foo-agent".to_string(), "bar-agent".to_string()]
|
|
||||||
.into_iter()
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
let keys_to_remove: Vec<_> = store
|
|
||||||
.external_agents
|
|
||||||
.keys()
|
|
||||||
.filter(|name| extension_agent_names.contains(name.0.as_ref()))
|
|
||||||
.cloned()
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
for key in keys_to_remove {
|
|
||||||
store.external_agents.remove(&key);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only the custom entry should remain.
|
|
||||||
let remaining: Vec<_> = store
|
|
||||||
.external_agents
|
|
||||||
.keys()
|
|
||||||
.map(|k| k.0.to_string())
|
|
||||||
.collect();
|
|
||||||
assert_eq!(remaining, vec!["custom".to_string()]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn resolve_extension_icon_path_allows_valid_paths() {
|
|
||||||
// Create a temporary directory structure for testing
|
|
||||||
let temp_dir = tempfile::tempdir().unwrap();
|
|
||||||
let extensions_dir = temp_dir.path();
|
|
||||||
let ext_dir = extensions_dir.join("my-extension");
|
|
||||||
std::fs::create_dir_all(&ext_dir).unwrap();
|
|
||||||
|
|
||||||
// Create a valid icon file
|
|
||||||
let icon_path = ext_dir.join("icon.svg");
|
|
||||||
std::fs::write(&icon_path, "<svg></svg>").unwrap();
|
|
||||||
|
|
||||||
// Test that a valid relative path works
|
|
||||||
let result = project::agent_server_store::resolve_extension_icon_path(
|
|
||||||
extensions_dir,
|
|
||||||
"my-extension",
|
|
||||||
"icon.svg",
|
|
||||||
);
|
|
||||||
assert!(result.is_some());
|
|
||||||
assert!(result.unwrap().ends_with("icon.svg"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn resolve_extension_icon_path_allows_nested_paths() {
|
|
||||||
let temp_dir = tempfile::tempdir().unwrap();
|
|
||||||
let extensions_dir = temp_dir.path();
|
|
||||||
let ext_dir = extensions_dir.join("my-extension");
|
|
||||||
let icons_dir = ext_dir.join("assets").join("icons");
|
|
||||||
std::fs::create_dir_all(&icons_dir).unwrap();
|
|
||||||
|
|
||||||
let icon_path = icons_dir.join("logo.svg");
|
|
||||||
std::fs::write(&icon_path, "<svg></svg>").unwrap();
|
|
||||||
|
|
||||||
let result = project::agent_server_store::resolve_extension_icon_path(
|
|
||||||
extensions_dir,
|
|
||||||
"my-extension",
|
|
||||||
"assets/icons/logo.svg",
|
|
||||||
);
|
|
||||||
assert!(result.is_some());
|
|
||||||
assert!(result.unwrap().ends_with("logo.svg"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn resolve_extension_icon_path_blocks_path_traversal() {
|
|
||||||
let temp_dir = tempfile::tempdir().unwrap();
|
|
||||||
let extensions_dir = temp_dir.path();
|
|
||||||
|
|
||||||
// Create two extension directories
|
|
||||||
let ext1_dir = extensions_dir.join("extension1");
|
|
||||||
let ext2_dir = extensions_dir.join("extension2");
|
|
||||||
std::fs::create_dir_all(&ext1_dir).unwrap();
|
|
||||||
std::fs::create_dir_all(&ext2_dir).unwrap();
|
|
||||||
|
|
||||||
// Create a file in extension2
|
|
||||||
let secret_file = ext2_dir.join("secret.svg");
|
|
||||||
std::fs::write(&secret_file, "<svg>secret</svg>").unwrap();
|
|
||||||
|
|
||||||
// Try to access extension2's file from extension1 using path traversal
|
|
||||||
let result = project::agent_server_store::resolve_extension_icon_path(
|
|
||||||
extensions_dir,
|
|
||||||
"extension1",
|
|
||||||
"../extension2/secret.svg",
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
result.is_none(),
|
|
||||||
"Path traversal to sibling extension should be blocked"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn resolve_extension_icon_path_blocks_absolute_escape() {
|
|
||||||
let temp_dir = tempfile::tempdir().unwrap();
|
|
||||||
let extensions_dir = temp_dir.path();
|
|
||||||
let ext_dir = extensions_dir.join("my-extension");
|
|
||||||
std::fs::create_dir_all(&ext_dir).unwrap();
|
|
||||||
|
|
||||||
// Create a file outside the extensions directory
|
|
||||||
let outside_file = temp_dir.path().join("outside.svg");
|
|
||||||
std::fs::write(&outside_file, "<svg>outside</svg>").unwrap();
|
|
||||||
|
|
||||||
// Try to escape to parent directory
|
|
||||||
let result = project::agent_server_store::resolve_extension_icon_path(
|
|
||||||
extensions_dir,
|
|
||||||
"my-extension",
|
|
||||||
"../outside.svg",
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
result.is_none(),
|
|
||||||
"Path traversal to parent directory should be blocked"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn resolve_extension_icon_path_blocks_deep_traversal() {
|
|
||||||
let temp_dir = tempfile::tempdir().unwrap();
|
|
||||||
let extensions_dir = temp_dir.path();
|
|
||||||
let ext_dir = extensions_dir.join("my-extension");
|
|
||||||
std::fs::create_dir_all(&ext_dir).unwrap();
|
|
||||||
|
|
||||||
// Try deep path traversal
|
|
||||||
let result = project::agent_server_store::resolve_extension_icon_path(
|
|
||||||
extensions_dir,
|
|
||||||
"my-extension",
|
|
||||||
"../../../../../../etc/passwd",
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
result.is_none(),
|
|
||||||
"Deep path traversal should be blocked (file doesn't exist)"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn resolve_extension_icon_path_returns_none_for_nonexistent() {
|
|
||||||
let temp_dir = tempfile::tempdir().unwrap();
|
|
||||||
let extensions_dir = temp_dir.path();
|
|
||||||
let ext_dir = extensions_dir.join("my-extension");
|
|
||||||
std::fs::create_dir_all(&ext_dir).unwrap();
|
|
||||||
|
|
||||||
// Try to access a file that doesn't exist
|
|
||||||
let result = project::agent_server_store::resolve_extension_icon_path(
|
|
||||||
extensions_dir,
|
|
||||||
"my-extension",
|
|
||||||
"nonexistent.svg",
|
|
||||||
);
|
|
||||||
assert!(result.is_none(), "Nonexistent file should return None");
|
|
||||||
}
|
|
||||||
|
|
@ -1,332 +0,0 @@
|
||||||
use anyhow::Result;
|
|
||||||
use collections::HashMap;
|
|
||||||
use gpui::{AppContext, AsyncApp, SharedString, Task, TestAppContext};
|
|
||||||
use node_runtime::NodeRuntime;
|
|
||||||
use project::worktree_store::WorktreeStore;
|
|
||||||
use project::{agent_server_store::*, worktree_store::WorktreeIdCounter};
|
|
||||||
use std::{any::Any, path::PathBuf, sync::Arc};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn extension_agent_constructs_proper_display_names() {
|
|
||||||
// Verify the display name format for extension-provided agents
|
|
||||||
let name1 = AgentId(SharedString::from("Extension: Agent"));
|
|
||||||
assert!(name1.0.contains(": "));
|
|
||||||
|
|
||||||
let name2 = AgentId(SharedString::from("MyExt: MyAgent"));
|
|
||||||
assert_eq!(name2.0, "MyExt: MyAgent");
|
|
||||||
|
|
||||||
// Non-extension agents shouldn't have the separator
|
|
||||||
let custom = AgentId(SharedString::from("custom"));
|
|
||||||
assert!(!custom.0.contains(": "));
|
|
||||||
}
|
|
||||||
|
|
||||||
struct NoopExternalAgent;
|
|
||||||
|
|
||||||
impl ExternalAgentServer for NoopExternalAgent {
|
|
||||||
fn get_command(
|
|
||||||
&self,
|
|
||||||
_extra_args: Vec<String>,
|
|
||||||
_extra_env: HashMap<String, String>,
|
|
||||||
_cx: &mut AsyncApp,
|
|
||||||
) -> Task<Result<AgentServerCommand>> {
|
|
||||||
Task::ready(Ok(AgentServerCommand {
|
|
||||||
path: PathBuf::from("noop"),
|
|
||||||
args: Vec::new(),
|
|
||||||
env: None,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn as_any(&self) -> &dyn Any {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
fn as_any_mut(&mut self) -> &mut dyn Any {
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn sync_removes_only_extension_provided_agents() {
|
|
||||||
let mut store = AgentServerStore::collab();
|
|
||||||
|
|
||||||
// Seed with extension agents (contain ": ") and custom agents (don't contain ": ")
|
|
||||||
store.external_agents.insert(
|
|
||||||
AgentId(SharedString::from("Ext1: Agent1")),
|
|
||||||
ExternalAgentEntry::new(
|
|
||||||
Box::new(NoopExternalAgent) as Box<dyn ExternalAgentServer>,
|
|
||||||
ExternalAgentSource::Extension,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
store.external_agents.insert(
|
|
||||||
AgentId(SharedString::from("Ext2: Agent2")),
|
|
||||||
ExternalAgentEntry::new(
|
|
||||||
Box::new(NoopExternalAgent) as Box<dyn ExternalAgentServer>,
|
|
||||||
ExternalAgentSource::Extension,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
store.external_agents.insert(
|
|
||||||
AgentId(SharedString::from("custom-agent")),
|
|
||||||
ExternalAgentEntry::new(
|
|
||||||
Box::new(NoopExternalAgent) as Box<dyn ExternalAgentServer>,
|
|
||||||
ExternalAgentSource::Custom,
|
|
||||||
None,
|
|
||||||
None,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Simulate removal phase
|
|
||||||
store
|
|
||||||
.external_agents
|
|
||||||
.retain(|_, entry| entry.source != ExternalAgentSource::Extension);
|
|
||||||
|
|
||||||
// Only custom-agent should remain
|
|
||||||
assert_eq!(store.external_agents.len(), 1);
|
|
||||||
assert!(
|
|
||||||
store
|
|
||||||
.external_agents
|
|
||||||
.contains_key(&AgentId(SharedString::from("custom-agent")))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn archive_launcher_constructs_with_all_fields() {
|
|
||||||
use extension::AgentServerManifestEntry;
|
|
||||||
|
|
||||||
let mut env = HashMap::default();
|
|
||||||
env.insert("GITHUB_TOKEN".into(), "secret".into());
|
|
||||||
|
|
||||||
let mut targets = HashMap::default();
|
|
||||||
targets.insert(
|
|
||||||
"darwin-aarch64".to_string(),
|
|
||||||
extension::TargetConfig {
|
|
||||||
archive:
|
|
||||||
"https://github.com/owner/repo/releases/download/v1.0.0/agent-darwin-arm64.zip"
|
|
||||||
.into(),
|
|
||||||
cmd: "./agent".into(),
|
|
||||||
args: vec![],
|
|
||||||
sha256: None,
|
|
||||||
env: Default::default(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
let _entry = AgentServerManifestEntry {
|
|
||||||
name: "GitHub Agent".into(),
|
|
||||||
targets,
|
|
||||||
env,
|
|
||||||
icon: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Verify display name construction
|
|
||||||
let expected_name = AgentId(SharedString::from("GitHub Agent"));
|
|
||||||
assert_eq!(expected_name.0, "GitHub Agent");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn archive_agent_uses_extension_and_agent_id_for_cache_key(cx: &mut TestAppContext) {
|
|
||||||
let fs = fs::FakeFs::new(cx.background_executor.clone());
|
|
||||||
let http_client = http_client::FakeHttpClient::with_404_response();
|
|
||||||
let worktree_store =
|
|
||||||
cx.new(|cx| WorktreeStore::local(false, fs.clone(), WorktreeIdCounter::get(cx)));
|
|
||||||
let project_environment = cx.new(|cx| {
|
|
||||||
crate::ProjectEnvironment::new(None, worktree_store.downgrade(), None, false, cx)
|
|
||||||
});
|
|
||||||
|
|
||||||
let agent = LocalExtensionArchiveAgent {
|
|
||||||
fs,
|
|
||||||
http_client,
|
|
||||||
node_runtime: node_runtime::NodeRuntime::unavailable(),
|
|
||||||
project_environment,
|
|
||||||
extension_id: Arc::from("my-extension"),
|
|
||||||
agent_id: Arc::from("my-agent"),
|
|
||||||
version: Some(SharedString::from("1.0.0")),
|
|
||||||
targets: {
|
|
||||||
let mut map = HashMap::default();
|
|
||||||
map.insert(
|
|
||||||
"darwin-aarch64".to_string(),
|
|
||||||
extension::TargetConfig {
|
|
||||||
archive: "https://example.com/my-agent-darwin-arm64.zip".into(),
|
|
||||||
cmd: "./my-agent".into(),
|
|
||||||
args: vec!["--serve".into()],
|
|
||||||
sha256: None,
|
|
||||||
env: Default::default(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
map
|
|
||||||
},
|
|
||||||
env: {
|
|
||||||
let mut map = HashMap::default();
|
|
||||||
map.insert("PORT".into(), "8080".into());
|
|
||||||
map
|
|
||||||
},
|
|
||||||
new_version_available_tx: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Verify agent is properly constructed
|
|
||||||
assert_eq!(agent.extension_id.as_ref(), "my-extension");
|
|
||||||
assert_eq!(agent.agent_id.as_ref(), "my-agent");
|
|
||||||
assert_eq!(agent.env.get("PORT"), Some(&"8080".to_string()));
|
|
||||||
assert!(agent.targets.contains_key("darwin-aarch64"));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn sync_extension_agents_registers_archive_launcher() {
|
|
||||||
use extension::AgentServerManifestEntry;
|
|
||||||
|
|
||||||
let expected_name = AgentId(SharedString::from("Release Agent"));
|
|
||||||
assert_eq!(expected_name.0, "Release Agent");
|
|
||||||
|
|
||||||
// Verify the manifest entry structure for archive-based installation
|
|
||||||
let mut env = HashMap::default();
|
|
||||||
env.insert("API_KEY".into(), "secret".into());
|
|
||||||
|
|
||||||
let mut targets = HashMap::default();
|
|
||||||
targets.insert(
|
|
||||||
"linux-x86_64".to_string(),
|
|
||||||
extension::TargetConfig {
|
|
||||||
archive: "https://github.com/org/project/releases/download/v2.1.0/release-agent-linux-x64.tar.gz".into(),
|
|
||||||
cmd: "./release-agent".into(),
|
|
||||||
args: vec!["serve".into()],
|
|
||||||
sha256: None,
|
|
||||||
env: Default::default(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
let manifest_entry = AgentServerManifestEntry {
|
|
||||||
name: "Release Agent".into(),
|
|
||||||
targets: targets.clone(),
|
|
||||||
env,
|
|
||||||
icon: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Verify target config is present
|
|
||||||
assert!(manifest_entry.targets.contains_key("linux-x86_64"));
|
|
||||||
let target = manifest_entry.targets.get("linux-x86_64").unwrap();
|
|
||||||
assert_eq!(target.cmd, "./release-agent");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_node_command_uses_managed_runtime(cx: &mut TestAppContext) {
|
|
||||||
let fs = fs::FakeFs::new(cx.background_executor.clone());
|
|
||||||
let http_client = http_client::FakeHttpClient::with_404_response();
|
|
||||||
let node_runtime = NodeRuntime::unavailable();
|
|
||||||
let worktree_store =
|
|
||||||
cx.new(|cx| WorktreeStore::local(false, fs.clone(), WorktreeIdCounter::get(cx)));
|
|
||||||
let project_environment = cx.new(|cx| {
|
|
||||||
crate::ProjectEnvironment::new(None, worktree_store.downgrade(), None, false, cx)
|
|
||||||
});
|
|
||||||
|
|
||||||
let agent = LocalExtensionArchiveAgent {
|
|
||||||
fs: fs.clone(),
|
|
||||||
http_client,
|
|
||||||
node_runtime,
|
|
||||||
project_environment,
|
|
||||||
extension_id: Arc::from("node-extension"),
|
|
||||||
agent_id: Arc::from("node-agent"),
|
|
||||||
version: Some(SharedString::from("1.0.0")),
|
|
||||||
targets: {
|
|
||||||
let mut map = HashMap::default();
|
|
||||||
map.insert(
|
|
||||||
"darwin-aarch64".to_string(),
|
|
||||||
extension::TargetConfig {
|
|
||||||
archive: "https://example.com/node-agent.zip".into(),
|
|
||||||
cmd: "node".into(),
|
|
||||||
args: vec!["index.js".into()],
|
|
||||||
sha256: None,
|
|
||||||
env: Default::default(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
map
|
|
||||||
},
|
|
||||||
env: HashMap::default(),
|
|
||||||
new_version_available_tx: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Verify that when cmd is "node", it attempts to use the node runtime
|
|
||||||
assert_eq!(agent.extension_id.as_ref(), "node-extension");
|
|
||||||
assert_eq!(agent.agent_id.as_ref(), "node-agent");
|
|
||||||
|
|
||||||
let target = agent.targets.get("darwin-aarch64").unwrap();
|
|
||||||
assert_eq!(target.cmd, "node");
|
|
||||||
assert_eq!(target.args, vec!["index.js"]);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_commands_run_in_extraction_directory(cx: &mut TestAppContext) {
|
|
||||||
let fs = fs::FakeFs::new(cx.background_executor.clone());
|
|
||||||
let http_client = http_client::FakeHttpClient::with_404_response();
|
|
||||||
let node_runtime = NodeRuntime::unavailable();
|
|
||||||
let worktree_store =
|
|
||||||
cx.new(|cx| WorktreeStore::local(false, fs.clone(), WorktreeIdCounter::get(cx)));
|
|
||||||
let project_environment = cx.new(|cx| {
|
|
||||||
crate::ProjectEnvironment::new(None, worktree_store.downgrade(), None, false, cx)
|
|
||||||
});
|
|
||||||
|
|
||||||
let agent = LocalExtensionArchiveAgent {
|
|
||||||
fs: fs.clone(),
|
|
||||||
http_client,
|
|
||||||
node_runtime,
|
|
||||||
project_environment,
|
|
||||||
extension_id: Arc::from("test-ext"),
|
|
||||||
agent_id: Arc::from("test-agent"),
|
|
||||||
version: Some(SharedString::from("1.0.0")),
|
|
||||||
targets: {
|
|
||||||
let mut map = HashMap::default();
|
|
||||||
map.insert(
|
|
||||||
"darwin-aarch64".to_string(),
|
|
||||||
extension::TargetConfig {
|
|
||||||
archive: "https://example.com/test.zip".into(),
|
|
||||||
cmd: "node".into(),
|
|
||||||
args: vec![
|
|
||||||
"server.js".into(),
|
|
||||||
"--config".into(),
|
|
||||||
"./config.json".into(),
|
|
||||||
],
|
|
||||||
sha256: None,
|
|
||||||
env: Default::default(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
map
|
|
||||||
},
|
|
||||||
env: Default::default(),
|
|
||||||
new_version_available_tx: None,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Verify the agent is configured with relative paths in args
|
|
||||||
let target = agent.targets.get("darwin-aarch64").unwrap();
|
|
||||||
assert_eq!(target.args[0], "server.js");
|
|
||||||
assert_eq!(target.args[2], "./config.json");
|
|
||||||
// These relative paths will resolve relative to the extraction directory
|
|
||||||
// when the command is executed
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_tilde_expansion_in_settings() {
|
|
||||||
let settings = settings::CustomAgentServerSettings::Custom {
|
|
||||||
path: PathBuf::from("~/custom/agent"),
|
|
||||||
args: vec!["serve".into()],
|
|
||||||
env: Default::default(),
|
|
||||||
default_mode: None,
|
|
||||||
default_model: None,
|
|
||||||
favorite_models: vec![],
|
|
||||||
default_config_options: Default::default(),
|
|
||||||
favorite_config_option_values: Default::default(),
|
|
||||||
};
|
|
||||||
|
|
||||||
let converted: CustomAgentServerSettings = settings.into();
|
|
||||||
let CustomAgentServerSettings::Custom {
|
|
||||||
command: AgentServerCommand { path, .. },
|
|
||||||
..
|
|
||||||
} = converted
|
|
||||||
else {
|
|
||||||
panic!("Expected Custom variant");
|
|
||||||
};
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
!path.to_string_lossy().starts_with("~"),
|
|
||||||
"Tilde should be expanded for custom agent path"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
@ -5,8 +5,6 @@ mod bookmark_store;
|
||||||
mod color_extractor;
|
mod color_extractor;
|
||||||
mod context_server_store;
|
mod context_server_store;
|
||||||
mod debugger;
|
mod debugger;
|
||||||
mod ext_agent_tests;
|
|
||||||
mod extension_agent_tests;
|
|
||||||
mod git_store;
|
mod git_store;
|
||||||
mod image_store;
|
mod image_store;
|
||||||
mod lsp_command;
|
mod lsp_command;
|
||||||
|
|
|
||||||
|
|
@ -523,46 +523,8 @@ pub enum CustomAgentServerSettings {
|
||||||
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
|
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
|
||||||
favorite_config_option_values: HashMap<String, Vec<String>>,
|
favorite_config_option_values: HashMap<String, Vec<String>>,
|
||||||
},
|
},
|
||||||
Extension {
|
// Used for the ACP extension migration
|
||||||
/// Additional environment variables to pass to the agent.
|
#[serde(alias = "extension")]
|
||||||
///
|
|
||||||
/// Default: {}
|
|
||||||
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
|
|
||||||
env: HashMap<String, String>,
|
|
||||||
/// The default mode to use for this agent.
|
|
||||||
///
|
|
||||||
/// Note: Not only all agents support modes.
|
|
||||||
///
|
|
||||||
/// Default: None
|
|
||||||
default_mode: Option<String>,
|
|
||||||
/// The default model to use for this agent.
|
|
||||||
///
|
|
||||||
/// This should be the model ID as reported by the agent.
|
|
||||||
///
|
|
||||||
/// Default: None
|
|
||||||
default_model: Option<String>,
|
|
||||||
/// The favorite models for this agent.
|
|
||||||
///
|
|
||||||
/// These are the model IDs as reported by the agent.
|
|
||||||
///
|
|
||||||
/// Default: []
|
|
||||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
|
||||||
favorite_models: Vec<String>,
|
|
||||||
/// Default values for session config options.
|
|
||||||
///
|
|
||||||
/// This is a map from config option ID to value ID.
|
|
||||||
///
|
|
||||||
/// Default: {}
|
|
||||||
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
|
|
||||||
default_config_options: HashMap<String, String>,
|
|
||||||
/// Favorited values for session config options.
|
|
||||||
///
|
|
||||||
/// This is a map from config option ID to a list of favorited value IDs.
|
|
||||||
///
|
|
||||||
/// Default: {}
|
|
||||||
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
|
|
||||||
favorite_config_option_values: HashMap<String, Vec<String>>,
|
|
||||||
},
|
|
||||||
Registry {
|
Registry {
|
||||||
/// Additional environment variables to pass to the agent.
|
/// Additional environment variables to pass to the agent.
|
||||||
///
|
///
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,6 @@ pub enum ExtensionCategoryFilter {
|
||||||
Grammars,
|
Grammars,
|
||||||
LanguageServers,
|
LanguageServers,
|
||||||
ContextServers,
|
ContextServers,
|
||||||
AgentServers,
|
|
||||||
Snippets,
|
Snippets,
|
||||||
DebugAdapters,
|
DebugAdapters,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue