acp: Allow installing ACP agents from the registry (#47218)
This is still behind a feature flag as the registry is still WIP, but allows downloading binary agents from the registry on github. Release Notes: - N/A
This commit is contained in:
1 parent
07b08522f2
commit
caa5d2be95
14 files changed
+1898
-280
No files matched your search
@@ -0,0 +1,4 @@
|
||||
<svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path opacity="0.3" d="M9 1.5H3C2.17157 1.5 1.5 2.17157 1.5 3V9C1.5 9.82843 2.17157 10.5 3 10.5H9C9.82843 10.5 10.5 9.82843 10.5 9V3C10.5 2.17157 9.82843 1.5 9 1.5Z" stroke="#C6CAD0" stroke-width="0.9"/>
|
||||
<path d="M4.32058 8.53711C3.99873 8.53711 3.79672 8.34879 3.79672 8.05092C3.79672 7.96532 3.82069 7.84891 3.8652 7.72222L5.21422 4.04838C5.35802 3.64778 5.60112 3.46289 5.99144 3.46289C6.39546 3.46289 6.63514 3.64093 6.78236 4.04495L8.13823 7.72222C8.18616 7.85575 8.20328 7.9482 8.20328 8.05092C8.20328 8.3351 7.98758 8.53711 7.68627 8.53711C7.39524 8.53711 7.24117 8.40358 7.14872 8.08173L6.8885 7.30108H5.10465L4.84444 8.07146C4.74857 8.40015 4.59449 8.53711 4.32058 8.53711ZM5.33063 6.49989H6.64883L5.99487 4.47636H5.9709L5.33063 6.49989Z" fill="#C6CAD0"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 868 B |
@@ -104,6 +104,10 @@ impl AgentServer for CustomAgentServer {
|
||||
| settings::CustomAgentServerSettings::Extension {
|
||||
favorite_config_option_values,
|
||||
..
|
||||
}
|
||||
| settings::CustomAgentServerSettings::Registry {
|
||||
favorite_config_option_values,
|
||||
..
|
||||
} => {
|
||||
let entry = favorite_config_option_values
|
||||
.entry(config_id.clone())
|
||||
@@ -142,7 +146,8 @@ impl AgentServer for CustomAgentServer {
|
||||
|
||||
match settings {
|
||||
settings::CustomAgentServerSettings::Custom { default_mode, .. }
|
||||
| settings::CustomAgentServerSettings::Extension { default_mode, .. } => {
|
||||
| settings::CustomAgentServerSettings::Extension { default_mode, .. }
|
||||
| settings::CustomAgentServerSettings::Registry { default_mode, .. } => {
|
||||
*default_mode = mode_id.map(|m| m.to_string());
|
||||
}
|
||||
}
|
||||
@@ -181,7 +186,8 @@ impl AgentServer for CustomAgentServer {
|
||||
|
||||
match settings {
|
||||
settings::CustomAgentServerSettings::Custom { default_model, .. }
|
||||
| settings::CustomAgentServerSettings::Extension { default_model, .. } => {
|
||||
| settings::CustomAgentServerSettings::Extension { default_model, .. }
|
||||
| settings::CustomAgentServerSettings::Registry { default_model, .. } => {
|
||||
*default_model = model_id.map(|m| m.to_string());
|
||||
}
|
||||
}
|
||||
@@ -236,6 +242,9 @@ impl AgentServer for CustomAgentServer {
|
||||
}
|
||||
| settings::CustomAgentServerSettings::Extension {
|
||||
favorite_models, ..
|
||||
}
|
||||
| settings::CustomAgentServerSettings::Registry {
|
||||
favorite_models, ..
|
||||
} => favorite_models,
|
||||
};
|
||||
|
||||
@@ -296,6 +305,10 @@ impl AgentServer for CustomAgentServer {
|
||||
| settings::CustomAgentServerSettings::Extension {
|
||||
default_config_options,
|
||||
..
|
||||
}
|
||||
| settings::CustomAgentServerSettings::Registry {
|
||||
default_config_options,
|
||||
..
|
||||
} => {
|
||||
if let Some(value) = value_id.clone() {
|
||||
default_config_options.insert(config_id.clone(), value);
|
||||
@@ -331,6 +344,10 @@ impl AgentServer for CustomAgentServer {
|
||||
| project::agent_server_store::CustomAgentServerSettings::Extension {
|
||||
default_config_options,
|
||||
..
|
||||
}
|
||||
| project::agent_server_store::CustomAgentServerSettings::Registry {
|
||||
default_config_options,
|
||||
..
|
||||
} => default_config_options.clone(),
|
||||
})
|
||||
.unwrap_or_default()
|
||||
|
||||
@@ -15,6 +15,7 @@ use context_server::ContextServerId;
|
||||
use editor::{Editor, MultiBufferOffset, SelectionEffects, scroll::Autoscroll};
|
||||
use extension::ExtensionManifest;
|
||||
use extension_host::ExtensionStore;
|
||||
use feature_flags::{AcpBetaFeatureFlag, FeatureFlagAppExt as _};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
Action, AnyView, App, AsyncWindowContext, Corner, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
@@ -29,7 +30,8 @@ use language_models::AllLanguageModelSettings;
|
||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||
use project::{
|
||||
agent_server_store::{
|
||||
AgentServerStore, CLAUDE_CODE_NAME, CODEX_NAME, ExternalAgentServerName, GEMINI_NAME,
|
||||
AgentServerStore, CLAUDE_CODE_NAME, CODEX_NAME, ExternalAgentServerName,
|
||||
ExternalAgentSource, GEMINI_NAME,
|
||||
},
|
||||
context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
|
||||
};
|
||||
@@ -980,7 +982,8 @@ impl AgentConfiguration {
|
||||
let display_name = agent_server_store
|
||||
.agent_display_name(&name)
|
||||
.unwrap_or_else(|| name.0.clone());
|
||||
(name, icon, display_name)
|
||||
let source = agent_server_store.agent_source(&name).unwrap_or_default();
|
||||
(name, icon, display_name, source)
|
||||
})
|
||||
.collect();
|
||||
|
||||
@@ -996,7 +999,14 @@ impl AgentConfiguration {
|
||||
)
|
||||
.menu({
|
||||
move |window, cx| {
|
||||
Some(ContextMenu::build(window, cx, |menu, _window, _cx| {
|
||||
Some(ContextMenu::build(window, cx, |mut menu, _window, _cx| {
|
||||
if _cx.has_flag::<AcpBetaFeatureFlag>() {
|
||||
menu = menu.entry("Install from Registry", None, {
|
||||
|window, cx| {
|
||||
window.dispatch_action(Box::new(zed_actions::AgentRegistry), cx)
|
||||
}
|
||||
});
|
||||
}
|
||||
menu.entry("Install from Extensions", None, {
|
||||
|window, cx| {
|
||||
window.dispatch_action(
|
||||
@@ -1088,7 +1098,7 @@ impl AgentConfiguration {
|
||||
AgentIcon::Name(IconName::AiClaude),
|
||||
"Claude Code",
|
||||
"Claude Code",
|
||||
false,
|
||||
ExternalAgentSource::Builtin,
|
||||
cx,
|
||||
))
|
||||
.child(Divider::horizontal().color(DividerColor::BorderFaded))
|
||||
@@ -1096,7 +1106,7 @@ impl AgentConfiguration {
|
||||
AgentIcon::Name(IconName::AiOpenAi),
|
||||
"Codex CLI",
|
||||
"Codex CLI",
|
||||
false,
|
||||
ExternalAgentSource::Builtin,
|
||||
cx,
|
||||
))
|
||||
.child(Divider::horizontal().color(DividerColor::BorderFaded))
|
||||
@@ -1104,11 +1114,11 @@ impl AgentConfiguration {
|
||||
AgentIcon::Name(IconName::AiGemini),
|
||||
"Gemini CLI",
|
||||
"Gemini CLI",
|
||||
false,
|
||||
ExternalAgentSource::Builtin,
|
||||
cx,
|
||||
))
|
||||
.map(|mut parent| {
|
||||
for (name, icon, display_name) in user_defined_agents {
|
||||
for (name, icon, display_name, source) in user_defined_agents {
|
||||
parent = parent
|
||||
.child(
|
||||
Divider::horizontal().color(DividerColor::BorderFaded),
|
||||
@@ -1117,7 +1127,7 @@ impl AgentConfiguration {
|
||||
icon,
|
||||
name,
|
||||
display_name,
|
||||
true,
|
||||
source,
|
||||
cx,
|
||||
));
|
||||
}
|
||||
@@ -1132,7 +1142,7 @@ impl AgentConfiguration {
|
||||
icon: AgentIcon,
|
||||
id: impl Into<SharedString>,
|
||||
display_name: impl Into<SharedString>,
|
||||
external: bool,
|
||||
source: ExternalAgentSource,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let id = id.into();
|
||||
@@ -1147,30 +1157,79 @@ impl AgentConfiguration {
|
||||
.color(Color::Muted),
|
||||
};
|
||||
|
||||
let tooltip_id = SharedString::new(format!("agent-source-{}", id));
|
||||
let tooltip_message = format!(
|
||||
"The {} agent was installed from an extension.",
|
||||
display_name
|
||||
);
|
||||
let source_badge = match source {
|
||||
ExternalAgentSource::Extension => Some((
|
||||
SharedString::new(format!("agent-source-{}", id)),
|
||||
SharedString::from(format!(
|
||||
"The {} agent was installed from an extension.",
|
||||
display_name
|
||||
)),
|
||||
IconName::ZedSrcExtension,
|
||||
)),
|
||||
ExternalAgentSource::Registry => Some((
|
||||
SharedString::new(format!("agent-source-{}", id)),
|
||||
SharedString::from(format!(
|
||||
"The {} agent was installed from the ACP registry.",
|
||||
display_name
|
||||
)),
|
||||
IconName::AcpRegistry,
|
||||
)),
|
||||
ExternalAgentSource::Builtin | ExternalAgentSource::Custom => None,
|
||||
};
|
||||
|
||||
let agent_server_name = ExternalAgentServerName(id.clone());
|
||||
|
||||
let uninstall_btn_id = SharedString::from(format!("uninstall-{}", id));
|
||||
let uninstall_button = IconButton::new(uninstall_btn_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();
|
||||
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);
|
||||
}
|
||||
}));
|
||||
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 => {
|
||||
let fs = self.fs.clone();
|
||||
Some(
|
||||
IconButton::new(
|
||||
SharedString::from(format!("uninstall-{}", id)),
|
||||
IconName::Trash,
|
||||
)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(Tooltip::text("Remove Registry Agent"))
|
||||
.on_click(cx.listener(move |_, _, _window, cx| {
|
||||
let agent_name = agent_server_name.clone();
|
||||
update_settings_file(fs.clone(), cx, move |settings, _| {
|
||||
let Some(agent_servers) = settings.agent_servers.as_mut() else {
|
||||
return;
|
||||
};
|
||||
if let Some(entry) = agent_servers.custom.get(agent_name.0.as_ref())
|
||||
&& matches!(
|
||||
entry,
|
||||
settings::CustomAgentServerSettings::Registry { .. }
|
||||
)
|
||||
{
|
||||
agent_servers.custom.remove(agent_name.0.as_ref());
|
||||
}
|
||||
});
|
||||
})),
|
||||
)
|
||||
}
|
||||
ExternalAgentSource::Builtin | ExternalAgentSource::Custom => None,
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.gap_1()
|
||||
@@ -1180,17 +1239,13 @@ impl AgentConfiguration {
|
||||
.gap_1p5()
|
||||
.child(icon)
|
||||
.child(Label::new(display_name))
|
||||
.when(external, |this| {
|
||||
.when_some(source_badge, |this, (tooltip_id, tooltip_message, icon)| {
|
||||
this.child(
|
||||
div()
|
||||
.id(tooltip_id)
|
||||
.flex_none()
|
||||
.tooltip(Tooltip::text(tooltip_message))
|
||||
.child(
|
||||
Icon::new(IconName::ZedSrcExtension)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
),
|
||||
.child(Icon::new(icon).size(IconSize::Small).color(Color::Muted)),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
@@ -1199,7 +1254,9 @@ impl AgentConfiguration {
|
||||
.size(IconSize::Small),
|
||||
),
|
||||
)
|
||||
.when(external, |this| this.child(uninstall_button))
|
||||
.when_some(uninstall_button, |this, uninstall_button| {
|
||||
this.child(uninstall_button)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,632 @@
|
||||
use std::ops::Range;
|
||||
|
||||
use collections::HashMap;
|
||||
use editor::{Editor, EditorElement, EditorStyle};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
AnyElement, App, Context, Entity, EventEmitter, Focusable, KeyContext, ParentElement, Render,
|
||||
RenderOnce, SharedString, Styled, TextStyle, UniformListScrollHandle, Window, point,
|
||||
uniform_list,
|
||||
};
|
||||
use project::agent_server_store::{AllAgentServersSettings, CustomAgentServerSettings};
|
||||
use project::{AgentRegistryStore, RegistryAgent};
|
||||
use settings::{Settings, SettingsStore, update_settings_file};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
ButtonStyle, ScrollableHandle, ToggleButtonGroup, ToggleButtonGroupSize,
|
||||
ToggleButtonGroupStyle, ToggleButtonSimple, Tooltip, WithScrollbar, prelude::*,
|
||||
};
|
||||
use workspace::{
|
||||
Workspace,
|
||||
item::{Item, ItemEvent},
|
||||
};
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum RegistryFilter {
|
||||
All,
|
||||
Installed,
|
||||
NotInstalled,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
enum RegistryInstallStatus {
|
||||
NotInstalled,
|
||||
InstalledRegistry,
|
||||
InstalledCustom,
|
||||
InstalledExtension,
|
||||
}
|
||||
|
||||
#[derive(IntoElement)]
|
||||
struct AgentRegistryCard {
|
||||
children: Vec<AnyElement>,
|
||||
}
|
||||
|
||||
impl AgentRegistryCard {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
children: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ParentElement for AgentRegistryCard {
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.children.extend(elements)
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for AgentRegistryCard {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
div().w_full().child(
|
||||
v_flex()
|
||||
.mt_4()
|
||||
.w_full()
|
||||
.min_h(rems_from_px(110.))
|
||||
.p_3()
|
||||
.gap_2()
|
||||
.bg(cx.theme().colors().elevated_surface_background.opacity(0.5))
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border_variant)
|
||||
.rounded_md()
|
||||
.children(self.children),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AgentRegistryPage {
|
||||
registry_store: Entity<AgentRegistryStore>,
|
||||
list: UniformListScrollHandle,
|
||||
registry_agents: Vec<RegistryAgent>,
|
||||
filtered_registry_indices: Vec<usize>,
|
||||
installed_statuses: HashMap<String, RegistryInstallStatus>,
|
||||
query_editor: Entity<Editor>,
|
||||
filter: RegistryFilter,
|
||||
_subscriptions: Vec<gpui::Subscription>,
|
||||
}
|
||||
|
||||
impl AgentRegistryPage {
|
||||
pub fn new(
|
||||
_workspace: &Workspace,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) -> Entity<Self> {
|
||||
cx.new(|cx| {
|
||||
let registry_store = AgentRegistryStore::global(cx);
|
||||
let query_editor = cx.new(|cx| {
|
||||
let mut input = Editor::single_line(window, cx);
|
||||
input.set_placeholder_text("Search agents...", window, cx);
|
||||
input
|
||||
});
|
||||
cx.subscribe(&query_editor, Self::on_query_change).detach();
|
||||
|
||||
let mut subscriptions = Vec::new();
|
||||
subscriptions.push(cx.observe(®istry_store, |this, _, cx| {
|
||||
this.reload_registry_agents(cx);
|
||||
}));
|
||||
subscriptions.push(cx.observe_global::<SettingsStore>(|this, cx| {
|
||||
this.filter_registry_agents(cx);
|
||||
}));
|
||||
|
||||
let mut this = Self {
|
||||
registry_store,
|
||||
list: UniformListScrollHandle::new(),
|
||||
registry_agents: Vec::new(),
|
||||
filtered_registry_indices: Vec::new(),
|
||||
installed_statuses: HashMap::default(),
|
||||
query_editor,
|
||||
filter: RegistryFilter::All,
|
||||
_subscriptions: subscriptions,
|
||||
};
|
||||
|
||||
this.reload_registry_agents(cx);
|
||||
this.registry_store
|
||||
.update(cx, |store, cx| store.refresh(cx));
|
||||
|
||||
this
|
||||
})
|
||||
}
|
||||
|
||||
fn reload_registry_agents(&mut self, cx: &mut Context<Self>) {
|
||||
self.registry_agents = self.registry_store.read(cx).agents().to_vec();
|
||||
self.registry_agents.sort_by(|left, right| {
|
||||
left.name
|
||||
.as_ref()
|
||||
.cmp(right.name.as_ref())
|
||||
.then_with(|| left.id.as_ref().cmp(right.id.as_ref()))
|
||||
});
|
||||
self.filter_registry_agents(cx);
|
||||
}
|
||||
|
||||
fn refresh_installed_statuses(&mut self, cx: &mut Context<Self>) {
|
||||
let settings = cx
|
||||
.global::<SettingsStore>()
|
||||
.get::<AllAgentServersSettings>(None);
|
||||
self.installed_statuses.clear();
|
||||
for (id, settings) in &settings.custom {
|
||||
let status = match settings {
|
||||
CustomAgentServerSettings::Registry { .. } => {
|
||||
RegistryInstallStatus::InstalledRegistry
|
||||
}
|
||||
CustomAgentServerSettings::Custom { .. } => RegistryInstallStatus::InstalledCustom,
|
||||
CustomAgentServerSettings::Extension { .. } => {
|
||||
RegistryInstallStatus::InstalledExtension
|
||||
}
|
||||
};
|
||||
self.installed_statuses.insert(id.clone(), status);
|
||||
}
|
||||
}
|
||||
|
||||
fn install_status(&self, id: &str) -> RegistryInstallStatus {
|
||||
self.installed_statuses
|
||||
.get(id)
|
||||
.copied()
|
||||
.unwrap_or(RegistryInstallStatus::NotInstalled)
|
||||
}
|
||||
|
||||
fn search_query(&self, cx: &mut App) -> Option<String> {
|
||||
let search = self.query_editor.read(cx).text(cx);
|
||||
if search.trim().is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(search)
|
||||
}
|
||||
}
|
||||
|
||||
fn filter_registry_agents(&mut self, cx: &mut Context<Self>) {
|
||||
self.refresh_installed_statuses(cx);
|
||||
let search = self.search_query(cx).map(|search| search.to_lowercase());
|
||||
let filter = self.filter;
|
||||
let installed_statuses = self.installed_statuses.clone();
|
||||
|
||||
let filtered_indices = self
|
||||
.registry_agents
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, agent)| {
|
||||
let matches_search = search.as_ref().is_none_or(|query| {
|
||||
let query = query.as_str();
|
||||
agent.id.as_ref().to_lowercase().contains(query)
|
||||
|| agent.name.as_ref().to_lowercase().contains(query)
|
||||
|| agent.description.as_ref().to_lowercase().contains(query)
|
||||
});
|
||||
|
||||
let install_status = installed_statuses
|
||||
.get(agent.id.as_ref())
|
||||
.copied()
|
||||
.unwrap_or(RegistryInstallStatus::NotInstalled);
|
||||
let matches_filter = match filter {
|
||||
RegistryFilter::All => true,
|
||||
RegistryFilter::Installed => {
|
||||
install_status != RegistryInstallStatus::NotInstalled
|
||||
}
|
||||
RegistryFilter::NotInstalled => {
|
||||
install_status == RegistryInstallStatus::NotInstalled
|
||||
}
|
||||
};
|
||||
|
||||
matches_search && matches_filter
|
||||
})
|
||||
.map(|(index, _)| index)
|
||||
.collect();
|
||||
|
||||
self.filtered_registry_indices = filtered_indices;
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn scroll_to_top(&mut self, cx: &mut Context<Self>) {
|
||||
self.list.set_offset(point(px(0.), px(0.)));
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn on_query_change(
|
||||
&mut self,
|
||||
_: Entity<Editor>,
|
||||
event: &editor::EditorEvent,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let editor::EditorEvent::Edited { .. } = event {
|
||||
self.filter_registry_agents(cx);
|
||||
self.scroll_to_top(cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn render_search(&self, cx: &mut Context<Self>) -> Div {
|
||||
let mut key_context = KeyContext::new_with_defaults();
|
||||
key_context.add("BufferSearchBar");
|
||||
|
||||
h_flex()
|
||||
.key_context(key_context)
|
||||
.h_8()
|
||||
.min_w(rems_from_px(384.))
|
||||
.flex_1()
|
||||
.pl_1p5()
|
||||
.pr_2()
|
||||
.gap_2()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_md()
|
||||
.child(Icon::new(IconName::MagnifyingGlass).color(Color::Muted))
|
||||
.child(self.render_text_input(&self.query_editor, cx))
|
||||
}
|
||||
|
||||
fn render_text_input(
|
||||
&self,
|
||||
editor: &Entity<Editor>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let text_style = TextStyle {
|
||||
color: if editor.read(cx).read_only(cx) {
|
||||
cx.theme().colors().text_disabled
|
||||
} else {
|
||||
cx.theme().colors().text
|
||||
},
|
||||
font_family: settings.ui_font.family.clone(),
|
||||
font_features: settings.ui_font.features.clone(),
|
||||
font_fallbacks: settings.ui_font.fallbacks.clone(),
|
||||
font_size: rems(0.875).into(),
|
||||
font_weight: settings.ui_font.weight,
|
||||
line_height: relative(1.3),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
EditorElement::new(
|
||||
editor,
|
||||
EditorStyle {
|
||||
background: cx.theme().colors().editor_background,
|
||||
local_player: cx.theme().players().local(),
|
||||
text: text_style,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn render_empty_state(&self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let has_search = self.search_query(cx).is_some();
|
||||
let registry_store = self.registry_store.read(cx);
|
||||
|
||||
let message = if registry_store.is_fetching() {
|
||||
"Loading registry..."
|
||||
} else if registry_store.fetch_error().is_some() {
|
||||
"Failed to load the agent registry. Please check your connection and try again."
|
||||
} else {
|
||||
match self.filter {
|
||||
RegistryFilter::All => {
|
||||
if has_search {
|
||||
"No agents match your search."
|
||||
} else {
|
||||
"No agents available."
|
||||
}
|
||||
}
|
||||
RegistryFilter::Installed => {
|
||||
if has_search {
|
||||
"No installed agents match your search."
|
||||
} else {
|
||||
"No installed agents."
|
||||
}
|
||||
}
|
||||
RegistryFilter::NotInstalled => {
|
||||
if has_search {
|
||||
"No uninstalled agents match your search."
|
||||
} else {
|
||||
"No uninstalled agents."
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.py_4()
|
||||
.gap_1p5()
|
||||
.when(registry_store.fetch_error().is_some(), |this| {
|
||||
this.child(
|
||||
Icon::new(IconName::Warning)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Warning),
|
||||
)
|
||||
})
|
||||
.child(Label::new(message))
|
||||
}
|
||||
|
||||
fn render_agents(
|
||||
&mut self,
|
||||
range: Range<usize>,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Vec<AgentRegistryCard> {
|
||||
range
|
||||
.map(|index| {
|
||||
let Some(agent_index) = self.filtered_registry_indices.get(index).copied() else {
|
||||
return self.render_missing_agent();
|
||||
};
|
||||
let Some(agent) = self.registry_agents.get(agent_index) else {
|
||||
return self.render_missing_agent();
|
||||
};
|
||||
self.render_registry_agent(agent, cx)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn render_missing_agent(&self) -> AgentRegistryCard {
|
||||
AgentRegistryCard::new().child(
|
||||
Label::new("Missing registry entry.")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
}
|
||||
|
||||
fn render_registry_agent(
|
||||
&self,
|
||||
agent: &RegistryAgent,
|
||||
cx: &mut Context<Self>,
|
||||
) -> AgentRegistryCard {
|
||||
let install_status = self.install_status(agent.id.as_ref());
|
||||
let supports_current_platform = agent.supports_current_platform;
|
||||
|
||||
let icon = match agent.icon_path.as_ref() {
|
||||
Some(icon_path) => Icon::from_external_svg(icon_path.clone()),
|
||||
None => Icon::new(IconName::Sparkle),
|
||||
}
|
||||
.size(IconSize::Medium)
|
||||
.color(Color::Muted);
|
||||
|
||||
let install_button =
|
||||
self.install_button(agent, install_status, supports_current_platform, cx);
|
||||
|
||||
let repository_button = agent.repository.as_ref().map(|repository| {
|
||||
let repository = repository.clone();
|
||||
IconButton::new(
|
||||
SharedString::from(format!("agent-repo-{}", agent.id)),
|
||||
IconName::Link,
|
||||
)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(Tooltip::text("Visit agent repository"))
|
||||
.on_click(move |_, _, cx| {
|
||||
cx.open_url(repository.as_ref());
|
||||
})
|
||||
});
|
||||
|
||||
AgentRegistryCard::new()
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.items_start()
|
||||
.child(
|
||||
h_flex().gap_2().items_center().child(icon).child(
|
||||
v_flex().gap_0p5().child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.items_end()
|
||||
.child(
|
||||
Headline::new(agent.name.clone()).size(HeadlineSize::Small),
|
||||
)
|
||||
.child(
|
||||
Headline::new(format!("v{}", agent.version))
|
||||
.size(HeadlineSize::XSmall),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(install_button),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.child(
|
||||
Label::new(agent.description.clone())
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Default)
|
||||
.truncate(),
|
||||
)
|
||||
.when_some(repository_button, |this, button| this.child(button)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.justify_between()
|
||||
.child(
|
||||
Label::new(format!("ID: {}", agent.id))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.truncate(),
|
||||
)
|
||||
.when(!supports_current_platform, |this| {
|
||||
this.child(
|
||||
Label::new("Not supported on this platform")
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Warning),
|
||||
)
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn install_button(
|
||||
&self,
|
||||
agent: &RegistryAgent,
|
||||
install_status: RegistryInstallStatus,
|
||||
supports_current_platform: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Button {
|
||||
let button_id = SharedString::from(format!("install-agent-{}", agent.id));
|
||||
|
||||
if !supports_current_platform {
|
||||
return Button::new(button_id, "Unavailable")
|
||||
.style(ButtonStyle::OutlinedGhost)
|
||||
.disabled(true);
|
||||
}
|
||||
|
||||
match install_status {
|
||||
RegistryInstallStatus::NotInstalled => {
|
||||
let fs = <dyn Fs>::global(cx);
|
||||
let agent_id = agent.id.to_string();
|
||||
Button::new(button_id, "Install")
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
|
||||
.icon(IconName::Download)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.icon_position(IconPosition::Start)
|
||||
.on_click(move |_, _, cx| {
|
||||
let agent_id = agent_id.clone();
|
||||
update_settings_file(fs.clone(), cx, move |settings, _| {
|
||||
let agent_servers = settings.agent_servers.get_or_insert_default();
|
||||
agent_servers.custom.entry(agent_id).or_insert_with(|| {
|
||||
settings::CustomAgentServerSettings::Registry {
|
||||
default_mode: None,
|
||||
default_model: None,
|
||||
favorite_models: Vec::new(),
|
||||
default_config_options: HashMap::default(),
|
||||
favorite_config_option_values: HashMap::default(),
|
||||
}
|
||||
});
|
||||
});
|
||||
})
|
||||
}
|
||||
RegistryInstallStatus::InstalledRegistry => {
|
||||
let fs = <dyn Fs>::global(cx);
|
||||
let agent_id = agent.id.to_string();
|
||||
Button::new(button_id, "Remove")
|
||||
.style(ButtonStyle::OutlinedGhost)
|
||||
.on_click(move |_, _, cx| {
|
||||
let agent_id = agent_id.clone();
|
||||
update_settings_file(fs.clone(), cx, move |settings, _| {
|
||||
let Some(agent_servers) = settings.agent_servers.as_mut() else {
|
||||
return;
|
||||
};
|
||||
if let Some(entry) = agent_servers.custom.get(agent_id.as_str())
|
||||
&& matches!(
|
||||
entry,
|
||||
settings::CustomAgentServerSettings::Registry { .. }
|
||||
)
|
||||
{
|
||||
agent_servers.custom.remove(agent_id.as_str());
|
||||
}
|
||||
});
|
||||
})
|
||||
}
|
||||
RegistryInstallStatus::InstalledCustom => Button::new(button_id, "Installed")
|
||||
.style(ButtonStyle::OutlinedGhost)
|
||||
.disabled(true),
|
||||
RegistryInstallStatus::InstalledExtension => Button::new(button_id, "Installed")
|
||||
.style(ButtonStyle::OutlinedGhost)
|
||||
.disabled(true),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for AgentRegistryPage {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.size_full()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(
|
||||
v_flex()
|
||||
.gap_4()
|
||||
.pt_4()
|
||||
.px_4()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.gap_1p5()
|
||||
.justify_between()
|
||||
.child(Headline::new("ACP Agent Registry").size(HeadlineSize::XLarge)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.flex_wrap()
|
||||
.gap_2()
|
||||
.child(self.render_search(cx))
|
||||
.child(
|
||||
div().child(
|
||||
ToggleButtonGroup::single_row(
|
||||
"registry-filter-buttons",
|
||||
[
|
||||
ToggleButtonSimple::new(
|
||||
"All",
|
||||
cx.listener(|this, _event, _, cx| {
|
||||
this.filter = RegistryFilter::All;
|
||||
this.filter_registry_agents(cx);
|
||||
this.scroll_to_top(cx);
|
||||
}),
|
||||
),
|
||||
ToggleButtonSimple::new(
|
||||
"Installed",
|
||||
cx.listener(|this, _event, _, cx| {
|
||||
this.filter = RegistryFilter::Installed;
|
||||
this.filter_registry_agents(cx);
|
||||
this.scroll_to_top(cx);
|
||||
}),
|
||||
),
|
||||
ToggleButtonSimple::new(
|
||||
"Not Installed",
|
||||
cx.listener(|this, _event, _, cx| {
|
||||
this.filter = RegistryFilter::NotInstalled;
|
||||
this.filter_registry_agents(cx);
|
||||
this.scroll_to_top(cx);
|
||||
}),
|
||||
),
|
||||
],
|
||||
)
|
||||
.style(ToggleButtonGroupStyle::Outlined)
|
||||
.size(ToggleButtonGroupSize::Custom(rems_from_px(30.)))
|
||||
.label_size(LabelSize::Default)
|
||||
.auto_width()
|
||||
.selected_index(match self.filter {
|
||||
RegistryFilter::All => 0,
|
||||
RegistryFilter::Installed => 1,
|
||||
RegistryFilter::NotInstalled => 2,
|
||||
})
|
||||
.into_any_element(),
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
.child(v_flex().px_4().size_full().overflow_y_hidden().map(|this| {
|
||||
let count = self.filtered_registry_indices.len();
|
||||
if count == 0 {
|
||||
this.child(self.render_empty_state(cx)).into_any_element()
|
||||
} else {
|
||||
let scroll_handle = &self.list;
|
||||
this.child(
|
||||
uniform_list("registry-entries", count, cx.processor(Self::render_agents))
|
||||
.flex_grow()
|
||||
.pb_4()
|
||||
.track_scroll(scroll_handle),
|
||||
)
|
||||
.vertical_scrollbar_for(scroll_handle, window, cx)
|
||||
.into_any_element()
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<ItemEvent> for AgentRegistryPage {}
|
||||
|
||||
impl Focusable for AgentRegistryPage {
|
||||
fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
|
||||
self.query_editor.read(cx).focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Item for AgentRegistryPage {
|
||||
type Event = ItemEvent;
|
||||
|
||||
fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
|
||||
"ACP Agent Registry".into()
|
||||
}
|
||||
|
||||
fn telemetry_event_text(&self) -> Option<&'static str> {
|
||||
Some("ACP Agent Registry Page Opened")
|
||||
}
|
||||
|
||||
fn show_toolbar(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn to_item_events(event: &Self::Event, mut f: impl FnMut(workspace::item::ItemEvent)) {
|
||||
f(*event)
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ mod agent_configuration;
|
||||
mod agent_diff;
|
||||
mod agent_model_selector;
|
||||
mod agent_panel;
|
||||
mod agent_registry_ui;
|
||||
mod buffer_codegen;
|
||||
mod completion_provider;
|
||||
mod context;
|
||||
@@ -28,9 +29,9 @@ use agent_settings::{AgentProfileId, AgentSettings};
|
||||
use assistant_slash_command::SlashCommandRegistry;
|
||||
use client::Client;
|
||||
use command_palette_hooks::CommandPaletteFilter;
|
||||
use feature_flags::{AgentV2FeatureFlag, FeatureFlagAppExt as _};
|
||||
use feature_flags::{AcpBetaFeatureFlag, AgentV2FeatureFlag, FeatureFlagAppExt as _};
|
||||
use fs::Fs;
|
||||
use gpui::{Action, App, Entity, SharedString, actions};
|
||||
use gpui::{Action, App, Context, Entity, SharedString, Window, actions};
|
||||
use language::{
|
||||
LanguageRegistry,
|
||||
language_settings::{AllLanguageSettings, EditPredictionProvider},
|
||||
@@ -44,9 +45,11 @@ use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{LanguageModelSelection, Settings as _, SettingsStore};
|
||||
use std::any::TypeId;
|
||||
use workspace::Workspace;
|
||||
|
||||
use crate::agent_configuration::{ConfigureContextServerModal, ManageProfilesModal};
|
||||
pub use crate::agent_panel::{AgentPanel, ConcreteAssistantPanelDelegate};
|
||||
use crate::agent_registry_ui::AgentRegistryPage;
|
||||
pub use crate::inline_assistant::InlineAssistant;
|
||||
pub use agent_diff::{AgentDiffPane, AgentDiffToolbar};
|
||||
pub use text_thread_editor::{AgentPanelDelegate, TextThreadEditor};
|
||||
@@ -267,6 +270,37 @@ pub fn init(
|
||||
ConfigureContextServerModal::register(workspace, language_registry.clone(), window, cx)
|
||||
})
|
||||
.detach();
|
||||
cx.observe_new(|workspace: &mut Workspace, _window, _cx| {
|
||||
workspace.register_action(
|
||||
move |workspace: &mut Workspace,
|
||||
_: &zed_actions::AgentRegistry,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>| {
|
||||
if !cx.has_flag::<AcpBetaFeatureFlag>() {
|
||||
return;
|
||||
}
|
||||
let existing = workspace
|
||||
.active_pane()
|
||||
.read(cx)
|
||||
.items()
|
||||
.find_map(|item| item.downcast::<AgentRegistryPage>());
|
||||
|
||||
if let Some(existing) = existing {
|
||||
workspace.activate_item(&existing, true, true, window, cx);
|
||||
} else {
|
||||
let registry_page = AgentRegistryPage::new(workspace, window, cx);
|
||||
workspace.add_item_to_active_pane(
|
||||
Box::new(registry_page),
|
||||
None,
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
},
|
||||
);
|
||||
})
|
||||
.detach();
|
||||
cx.observe_new(ManageProfilesModal::register).detach();
|
||||
|
||||
// Update command palette filter based on AI settings
|
||||
|
||||
@@ -8,6 +8,7 @@ use strum::{EnumIter, EnumString, IntoStaticStr};
|
||||
)]
|
||||
#[strum(serialize_all = "snake_case")]
|
||||
pub enum IconName {
|
||||
AcpRegistry,
|
||||
Ai,
|
||||
AiAnthropic,
|
||||
AiBedrock,
|
||||
|
||||
@@ -0,0 +1,460 @@
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use anyhow::{Context as _, Result, bail};
|
||||
use collections::HashMap;
|
||||
use fs::Fs;
|
||||
use futures::AsyncReadExt;
|
||||
use gpui::{App, AppContext as _, Context, Entity, Global, SharedString, Task};
|
||||
use http_client::{AsyncBody, HttpClient};
|
||||
use serde::Deserialize;
|
||||
|
||||
const REGISTRY_URL: &str =
|
||||
"https://github.com/agentclientprotocol/registry/releases/latest/download/registry.json";
|
||||
const REGISTRY_REFRESH_INTERVAL: Duration = Duration::from_secs(60 * 60);
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RegistryAgent {
|
||||
pub id: SharedString,
|
||||
pub name: SharedString,
|
||||
pub description: SharedString,
|
||||
pub version: SharedString,
|
||||
pub repository: Option<SharedString>,
|
||||
pub icon_path: Option<SharedString>,
|
||||
pub targets: HashMap<String, RegistryTargetConfig>,
|
||||
pub supports_current_platform: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RegistryTargetConfig {
|
||||
pub archive: String,
|
||||
pub cmd: String,
|
||||
pub args: Vec<String>,
|
||||
pub sha256: Option<String>,
|
||||
pub env: HashMap<String, String>,
|
||||
}
|
||||
|
||||
struct GlobalAgentRegistryStore(Entity<AgentRegistryStore>);
|
||||
|
||||
impl Global for GlobalAgentRegistryStore {}
|
||||
|
||||
pub struct AgentRegistryStore {
|
||||
fs: Arc<dyn Fs>,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
agents: Vec<RegistryAgent>,
|
||||
is_fetching: bool,
|
||||
fetch_error: Option<SharedString>,
|
||||
pending_refresh: Option<Task<()>>,
|
||||
_poll_task: Task<Result<()>>,
|
||||
}
|
||||
|
||||
impl AgentRegistryStore {
|
||||
pub fn init_global(cx: &mut App) -> Entity<Self> {
|
||||
if let Some(store) = Self::try_global(cx) {
|
||||
return store;
|
||||
}
|
||||
|
||||
let fs = <dyn Fs>::global(cx);
|
||||
let http_client: Arc<dyn HttpClient> = cx.http_client();
|
||||
|
||||
let store = cx.new(|cx| Self::new(fs, http_client, cx));
|
||||
store.update(cx, |store, cx| {
|
||||
store.refresh(cx);
|
||||
store.start_polling(cx);
|
||||
});
|
||||
cx.set_global(GlobalAgentRegistryStore(store.clone()));
|
||||
store
|
||||
}
|
||||
|
||||
pub fn global(cx: &App) -> Entity<Self> {
|
||||
cx.global::<GlobalAgentRegistryStore>().0.clone()
|
||||
}
|
||||
|
||||
pub fn try_global(cx: &App) -> Option<Entity<Self>> {
|
||||
cx.try_global::<GlobalAgentRegistryStore>()
|
||||
.map(|store| store.0.clone())
|
||||
}
|
||||
|
||||
pub fn agents(&self) -> &[RegistryAgent] {
|
||||
&self.agents
|
||||
}
|
||||
|
||||
pub fn agent(&self, id: &str) -> Option<&RegistryAgent> {
|
||||
self.agents.iter().find(|agent| agent.id == id)
|
||||
}
|
||||
|
||||
pub fn is_fetching(&self) -> bool {
|
||||
self.is_fetching
|
||||
}
|
||||
|
||||
pub fn fetch_error(&self) -> Option<SharedString> {
|
||||
self.fetch_error.clone()
|
||||
}
|
||||
|
||||
pub fn refresh(&mut self, cx: &mut Context<Self>) {
|
||||
if self.pending_refresh.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
self.is_fetching = true;
|
||||
self.fetch_error = None;
|
||||
cx.notify();
|
||||
|
||||
let fs = self.fs.clone();
|
||||
let http_client = self.http_client.clone();
|
||||
|
||||
self.pending_refresh = Some(cx.spawn(async move |this, cx| {
|
||||
let result = match fetch_registry_index(http_client.clone()).await {
|
||||
Ok(data) => {
|
||||
build_registry_agents(fs.clone(), http_client, data.index, data.raw_body, true)
|
||||
.await
|
||||
}
|
||||
Err(error) => Err(error),
|
||||
};
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.pending_refresh = None;
|
||||
this.is_fetching = false;
|
||||
match result {
|
||||
Ok(agents) => {
|
||||
this.agents = agents;
|
||||
this.fetch_error = None;
|
||||
}
|
||||
Err(error) => {
|
||||
this.fetch_error = Some(SharedString::from(error.to_string()));
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
}));
|
||||
}
|
||||
|
||||
fn new(fs: Arc<dyn Fs>, http_client: Arc<dyn HttpClient>, cx: &mut Context<Self>) -> Self {
|
||||
let mut store = Self {
|
||||
fs: fs.clone(),
|
||||
http_client,
|
||||
agents: Vec::new(),
|
||||
is_fetching: false,
|
||||
fetch_error: None,
|
||||
pending_refresh: None,
|
||||
_poll_task: Task::ready(Ok(())),
|
||||
};
|
||||
|
||||
store.load_cached_registry(fs, store.http_client.clone(), cx);
|
||||
|
||||
store
|
||||
}
|
||||
|
||||
fn load_cached_registry(
|
||||
&mut self,
|
||||
fs: Arc<dyn Fs>,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
cx.spawn(async move |this, cx| -> Result<()> {
|
||||
let cache_path = registry_cache_path();
|
||||
if !fs.is_file(&cache_path).await {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let bytes = fs
|
||||
.load_bytes(&cache_path)
|
||||
.await
|
||||
.context("reading cached registry")?;
|
||||
let index: RegistryIndex =
|
||||
serde_json::from_slice(&bytes).context("parsing cached registry")?;
|
||||
|
||||
let agents = build_registry_agents(fs, http_client, index, bytes, false).await?;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
this.agents = agents;
|
||||
cx.notify();
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn start_polling(&mut self, cx: &mut Context<Self>) {
|
||||
self._poll_task = cx.spawn(async move |this, cx| -> Result<()> {
|
||||
loop {
|
||||
this.update(cx, |this, cx| this.refresh(cx))?;
|
||||
cx.background_executor()
|
||||
.timer(REGISTRY_REFRESH_INTERVAL)
|
||||
.await;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
struct RegistryFetchResult {
|
||||
index: RegistryIndex,
|
||||
raw_body: Vec<u8>,
|
||||
}
|
||||
|
||||
async fn fetch_registry_index(http_client: Arc<dyn HttpClient>) -> Result<RegistryFetchResult> {
|
||||
let mut response = http_client
|
||||
.get(REGISTRY_URL, AsyncBody::default(), true)
|
||||
.await
|
||||
.context("requesting ACP registry")?;
|
||||
|
||||
let mut body = Vec::new();
|
||||
response
|
||||
.body_mut()
|
||||
.read_to_end(&mut body)
|
||||
.await
|
||||
.context("reading ACP registry response")?;
|
||||
|
||||
if response.status().is_client_error() {
|
||||
let text = String::from_utf8_lossy(body.as_slice());
|
||||
bail!(
|
||||
"registry status error {}, response: {text:?}",
|
||||
response.status().as_u16()
|
||||
);
|
||||
}
|
||||
|
||||
let index: RegistryIndex = serde_json::from_slice(&body).context("parsing ACP registry")?;
|
||||
Ok(RegistryFetchResult {
|
||||
index,
|
||||
raw_body: body,
|
||||
})
|
||||
}
|
||||
|
||||
async fn build_registry_agents(
|
||||
fs: Arc<dyn Fs>,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
index: RegistryIndex,
|
||||
raw_body: Vec<u8>,
|
||||
update_cache: bool,
|
||||
) -> Result<Vec<RegistryAgent>> {
|
||||
let cache_dir = registry_cache_dir();
|
||||
fs.create_dir(&cache_dir).await?;
|
||||
|
||||
let cache_path = cache_dir.join("registry.json");
|
||||
if update_cache {
|
||||
fs.write(&cache_path, &raw_body).await?;
|
||||
}
|
||||
|
||||
let icons_dir = cache_dir.join("icons");
|
||||
if update_cache {
|
||||
fs.create_dir(&icons_dir).await?;
|
||||
}
|
||||
|
||||
let current_platform = current_platform_key();
|
||||
|
||||
let mut agents = Vec::new();
|
||||
for entry in index.agents {
|
||||
let Some(binary) = entry.distribution.binary.as_ref() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if binary.is_empty() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut targets = HashMap::default();
|
||||
for (platform, target) in binary.iter() {
|
||||
targets.insert(
|
||||
platform.clone(),
|
||||
RegistryTargetConfig {
|
||||
archive: target.archive.clone(),
|
||||
cmd: target.cmd.clone(),
|
||||
args: target.args.clone(),
|
||||
sha256: None,
|
||||
env: target.env.clone(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
let supports_current_platform = current_platform
|
||||
.as_ref()
|
||||
.is_some_and(|platform| targets.contains_key(*platform));
|
||||
|
||||
let icon_path = resolve_icon_path(
|
||||
&entry,
|
||||
&icons_dir,
|
||||
update_cache,
|
||||
fs.clone(),
|
||||
http_client.clone(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
agents.push(RegistryAgent {
|
||||
id: entry.id.into(),
|
||||
name: entry.name.into(),
|
||||
description: entry.description.into(),
|
||||
version: entry.version.into(),
|
||||
repository: entry.repository.map(Into::into),
|
||||
icon_path,
|
||||
targets,
|
||||
supports_current_platform,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(agents)
|
||||
}
|
||||
|
||||
async fn resolve_icon_path(
|
||||
entry: &RegistryEntry,
|
||||
icons_dir: &Path,
|
||||
update_cache: bool,
|
||||
fs: Arc<dyn Fs>,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
) -> Result<Option<SharedString>> {
|
||||
let icon_url = resolve_icon_url(entry);
|
||||
let Some(icon_url) = icon_url else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
let icon_path = icons_dir.join(format!("{}.svg", entry.id));
|
||||
if update_cache && !fs.is_file(&icon_path).await {
|
||||
if let Err(error) = download_icon(fs.clone(), http_client, &icon_url, entry).await {
|
||||
log::warn!(
|
||||
"Failed to download ACP registry icon for {}: {error:#}",
|
||||
entry.id
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if fs.is_file(&icon_path).await {
|
||||
Ok(Some(SharedString::from(
|
||||
icon_path.to_string_lossy().into_owned(),
|
||||
)))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
|
||||
async fn download_icon(
|
||||
fs: Arc<dyn Fs>,
|
||||
http_client: Arc<dyn HttpClient>,
|
||||
icon_url: &str,
|
||||
entry: &RegistryEntry,
|
||||
) -> Result<()> {
|
||||
let mut response = http_client
|
||||
.get(icon_url, AsyncBody::default(), true)
|
||||
.await
|
||||
.with_context(|| format!("requesting icon for {}", entry.id))?;
|
||||
|
||||
let mut body = Vec::new();
|
||||
response
|
||||
.body_mut()
|
||||
.read_to_end(&mut body)
|
||||
.await
|
||||
.with_context(|| format!("reading icon for {}", entry.id))?;
|
||||
|
||||
if response.status().is_client_error() {
|
||||
let text = String::from_utf8_lossy(body.as_slice());
|
||||
bail!(
|
||||
"icon status error {}, response: {text:?}",
|
||||
response.status().as_u16()
|
||||
);
|
||||
}
|
||||
|
||||
let icon_path = registry_cache_dir()
|
||||
.join("icons")
|
||||
.join(format!("{}.svg", entry.id));
|
||||
fs.write(&icon_path, &body).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn resolve_icon_url(entry: &RegistryEntry) -> Option<String> {
|
||||
let icon = entry.icon.as_ref()?;
|
||||
if icon.starts_with("https://") || icon.starts_with("http://") {
|
||||
return Some(icon.to_string());
|
||||
}
|
||||
|
||||
let relative_icon = icon.trim_start_matches("./");
|
||||
Some(format!(
|
||||
"https://raw.githubusercontent.com/agentclientprotocol/registry/main/{}/{relative_icon}",
|
||||
entry.id
|
||||
))
|
||||
}
|
||||
|
||||
fn current_platform_key() -> Option<&'static str> {
|
||||
let os = if cfg!(target_os = "macos") {
|
||||
"darwin"
|
||||
} else if cfg!(target_os = "linux") {
|
||||
"linux"
|
||||
} else if cfg!(target_os = "windows") {
|
||||
"windows"
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let arch = if cfg!(target_arch = "aarch64") {
|
||||
"aarch64"
|
||||
} else if cfg!(target_arch = "x86_64") {
|
||||
"x86_64"
|
||||
} else {
|
||||
return None;
|
||||
};
|
||||
|
||||
Some(match os {
|
||||
"darwin" => match arch {
|
||||
"aarch64" => "darwin-aarch64",
|
||||
"x86_64" => "darwin-x86_64",
|
||||
_ => return None,
|
||||
},
|
||||
"linux" => match arch {
|
||||
"aarch64" => "linux-aarch64",
|
||||
"x86_64" => "linux-x86_64",
|
||||
_ => return None,
|
||||
},
|
||||
"windows" => match arch {
|
||||
"aarch64" => "windows-aarch64",
|
||||
"x86_64" => "windows-x86_64",
|
||||
_ => return None,
|
||||
},
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
fn registry_cache_dir() -> PathBuf {
|
||||
paths::external_agents_dir().join("registry")
|
||||
}
|
||||
|
||||
fn registry_cache_path() -> PathBuf {
|
||||
registry_cache_dir().join("registry.json")
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct RegistryIndex {
|
||||
#[serde(rename = "version")]
|
||||
_version: String,
|
||||
agents: Vec<RegistryEntry>,
|
||||
#[serde(rename = "extensions")]
|
||||
_extensions: Vec<RegistryEntry>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct RegistryEntry {
|
||||
id: String,
|
||||
name: String,
|
||||
version: String,
|
||||
description: String,
|
||||
#[serde(default)]
|
||||
repository: Option<String>,
|
||||
#[serde(default)]
|
||||
icon: Option<String>,
|
||||
distribution: RegistryDistribution,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct RegistryDistribution {
|
||||
#[serde(default)]
|
||||
binary: Option<HashMap<String, RegistryBinaryTarget>>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct RegistryBinaryTarget {
|
||||
archive: String,
|
||||
cmd: String,
|
||||
#[serde(default)]
|
||||
args: Vec<String>,
|
||||
#[serde(default)]
|
||||
env: HashMap<String, String>,
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,3 +1,4 @@
|
||||
pub mod agent_registry_store;
|
||||
pub mod agent_server_store;
|
||||
pub mod buffer_store;
|
||||
mod color_extractor;
|
||||
@@ -43,7 +44,10 @@ use crate::{
|
||||
project_search::SearchResultsHandle,
|
||||
trusted_worktrees::{PathTrust, RemoteHostLocation, TrustedWorktrees},
|
||||
};
|
||||
pub use agent_server_store::{AgentServerStore, AgentServersUpdated, ExternalAgentServerName};
|
||||
pub use agent_registry_store::{AgentRegistryStore, RegistryAgent};
|
||||
pub use agent_server_store::{
|
||||
AgentServerStore, AgentServersUpdated, ExternalAgentServerName, ExternalAgentSource,
|
||||
};
|
||||
pub use git_store::{
|
||||
ConflictRegion, ConflictSet, ConflictSetSnapshot, ConflictSetUpdate,
|
||||
git_traversal::{ChildEntriesGitIter, GitEntry, GitEntryRef, GitTraversal},
|
||||
|
||||
@@ -27,7 +27,7 @@ log.workspace = true
|
||||
migrator.workspace = true
|
||||
paths.workspace = true
|
||||
release_channel.workspace = true
|
||||
rust-embed.workspace = true
|
||||
rust-embed = { workspace = true, features = ["debug-embed"] }
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
@@ -502,6 +502,41 @@ pub enum CustomAgentServerSettings {
|
||||
#[serde(default)]
|
||||
favorite_config_option_values: HashMap<String, Vec<String>>,
|
||||
},
|
||||
Registry {
|
||||
/// 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)]
|
||||
favorite_models: Vec<String>,
|
||||
/// Default values for session config options.
|
||||
///
|
||||
/// This is a map from config option ID to value ID.
|
||||
///
|
||||
/// Default: {}
|
||||
#[serde(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: {}
|
||||
#[serde(default)]
|
||||
favorite_config_option_values: HashMap<String, Vec<String>>,
|
||||
},
|
||||
}
|
||||
|
||||
#[with_fallible_options]
|
||||
|
||||
@@ -17,6 +17,7 @@ use crashes::InitCrashHandler;
|
||||
use db::kvp::{GLOBAL_KEY_VALUE_STORE, KEY_VALUE_STORE};
|
||||
use editor::Editor;
|
||||
use extension::ExtensionHostProxy;
|
||||
use feature_flags::{AcpBetaFeatureFlag, FeatureFlagAppExt as _};
|
||||
use fs::{Fs, RealFs};
|
||||
use futures::{StreamExt, channel::oneshot, future};
|
||||
use git::GitHostingProviderRegistry;
|
||||
@@ -617,6 +618,15 @@ fn main() {
|
||||
snippet_provider::init(cx);
|
||||
edit_prediction_registry::init(app_state.client.clone(), app_state.user_store.clone(), cx);
|
||||
let prompt_builder = PromptBuilder::load(app_state.fs.clone(), stdout_is_a_pty(), cx);
|
||||
if cx.has_flag::<AcpBetaFeatureFlag>() {
|
||||
project::AgentRegistryStore::init_global(cx);
|
||||
}
|
||||
cx.observe_flag::<AcpBetaFeatureFlag, _>(|is_enabled, cx| {
|
||||
if is_enabled {
|
||||
project::AgentRegistryStore::init_global(cx);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
agent_ui::init(
|
||||
app_state.fs.clone(),
|
||||
app_state.client.clone(),
|
||||
|
||||
@@ -25,7 +25,7 @@ use collections::VecDeque;
|
||||
use debugger_ui::debugger_panel::DebugPanel;
|
||||
use editor::{Editor, MultiBuffer};
|
||||
use extension_host::ExtensionStore;
|
||||
use feature_flags::{FeatureFlagAppExt, PanicFeatureFlag};
|
||||
use feature_flags::{FeatureFlagAppExt as _, PanicFeatureFlag};
|
||||
use fs::Fs;
|
||||
use futures::FutureExt as _;
|
||||
use futures::future::Either;
|
||||
@@ -4968,6 +4968,7 @@ mod tests {
|
||||
web_search::init(cx);
|
||||
web_search_providers::init(app_state.client.clone(), cx);
|
||||
let prompt_builder = PromptBuilder::load(app_state.fs.clone(), false, cx);
|
||||
project::AgentRegistryStore::init_global(cx);
|
||||
agent_ui::init(
|
||||
app_state.fs.clone(),
|
||||
app_state.client.clone(),
|
||||
|
||||
@@ -104,6 +104,12 @@ pub struct Extensions {
|
||||
pub id: Option<String>,
|
||||
}
|
||||
|
||||
/// Opens the external agent registry.
|
||||
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
|
||||
#[action(namespace = zed)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct AgentRegistry;
|
||||
|
||||
/// Decreases the font size in the editor buffer.
|
||||
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
|
||||
#[action(namespace = zed)]
|
||||
|
||||
Reference in new issue
Block a user