@@ -123,13 +123,28 @@ pub trait ExternalAgentServer {
cx: &mut AsyncApp,
) -> Task<Result<AgentServerCommand>>;
+ fn version(&self) -> Option<&SharedString> {
+ None
+ }
+
+ fn take_new_version_available_tx(&mut self) -> Option<watch::Sender<Option<String>>> {
+ None
+ }
+
+ fn set_new_version_available_tx(&mut self, _tx: watch::Sender<Option<String>>) {}
+
+ fn as_any(&self) -> &dyn Any;
fn as_any_mut(&mut self) -> &mut dyn Any;
}
-impl dyn ExternalAgentServer {
- fn downcast_mut<T: ExternalAgentServer + 'static>(&mut self) -> Option<&mut T> {
- self.as_any_mut().downcast_mut()
- }
+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 {
@@ -140,14 +155,7 @@ enum AgentServerStoreState {
downstream_client: Option<(u64, AnyProtoClient)>,
settings: Option<AllAgentServersSettings>,
http_client: Arc<dyn HttpClient>,
- extension_agents: Vec<(
- Arc<str>,
- String,
- HashMap<String, extension::TargetConfig>,
- HashMap<String, String>,
- Option<String>,
- Option<SharedString>,
- )>,
+ extension_agents: Vec<ExtensionAgentEntry>,
_subscriptions: Vec<Subscription>,
},
Remote {
@@ -221,14 +229,15 @@ impl AgentServerStore {
resolve_extension_icon_path(&extensions_dir, ext_id, icon)
});
- extension_agents.push((
- agent_name.clone(),
- ext_id.to_owned(),
- agent_entry.targets.clone(),
- agent_entry.env.clone(),
+ 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,
- Some(display_name),
- ));
+ display_name: Some(display_name),
+ version: Some(SharedString::from(manifest.version.clone())),
+ });
}
}
self.reregister_agents(cx);
@@ -287,6 +296,7 @@ impl AgentServerStore {
.iter()
.map(|(k, v)| (k.clone(), v.clone()))
.collect(),
+ version: Some(manifest.version.to_string()),
});
}
}
@@ -442,15 +452,28 @@ impl AgentServerStore {
})
.unwrap_or_default();
- self.external_agents.clear();
+ // Drain the existing versioned agents, extracting reconnect state
+ // from any active connection so we can preserve it or trigger a
+ // reconnect when the version changes.
+ let mut old_versioned_agents: HashMap<
+ AgentId,
+ (SharedString, watch::Sender<Option<String>>),
+ > = HashMap::default();
+ for (name, mut entry) in self.external_agents.drain() {
+ if let Some(version) = entry.server.version().cloned() {
+ if let Some(tx) = entry.server.take_new_version_available_tx() {
+ old_versioned_agents.insert(name, (version, tx));
+ }
+ }
+ }
// Insert extension agents before custom/registry so registry entries override extensions.
- for (agent_name, ext_id, targets, env, icon_path, display_name) in extension_agents.iter() {
- let name = AgentId(agent_name.clone().into());
- let mut env = env.clone();
+ 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(agent_name.as_ref())
+ .get(entry.agent_name.as_ref())
.and_then(|settings| match settings {
CustomAgentServerSettings::Extension { env, .. } => Some(env.clone()),
_ => None,
@@ -458,7 +481,8 @@ impl AgentServerStore {
{
env.extend(settings_env);
}
- let icon = icon_path
+ let icon = entry
+ .icon_path
.as_ref()
.map(|path| SharedString::from(path.clone()));
@@ -470,14 +494,16 @@ impl AgentServerStore {
http_client: http_client.clone(),
node_runtime: node_runtime.clone(),
project_environment: project_environment.clone(),
- extension_id: Arc::from(&**ext_id),
- targets: targets.clone(),
+ extension_id: Arc::from(&*entry.extension_id),
+ targets: entry.targets.clone(),
env,
- agent_id: agent_name.clone(),
+ agent_id: entry.agent_name.clone(),
+ version: entry.version.clone(),
+ new_version_available_tx: None,
}) as Box<dyn ExternalAgentServer>,
ExternalAgentSource::Extension,
icon,
- display_name.clone(),
+ entry.display_name.clone(),
),
);
}
@@ -527,8 +553,10 @@ impl AgentServerStore {
node_runtime: node_runtime.clone(),
project_environment: project_environment.clone(),
registry_id: Arc::from(name.as_str()),
+ version: agent.metadata.version.clone(),
targets: agent.targets.clone(),
env: env.clone(),
+ new_version_available_tx: None,
})
as Box<dyn ExternalAgentServer>,
ExternalAgentSource::Registry,
@@ -544,10 +572,12 @@ impl AgentServerStore {
Box::new(LocalRegistryNpxAgent {
node_runtime: node_runtime.clone(),
project_environment: project_environment.clone(),
+ version: agent.metadata.version.clone(),
package: agent.package.clone(),
args: agent.args.clone(),
distribution_env: agent.env.clone(),
settings_env: env.clone(),
+ new_version_available_tx: None,
})
as Box<dyn ExternalAgentServer>,
ExternalAgentSource::Registry,
@@ -562,6 +592,24 @@ impl AgentServerStore {
}
}
+ // For each rebuilt versioned agent, compare the version. If it
+ // changed, notify the active connection to reconnect. Otherwise,
+ // transfer the channel to the new entry so future updates can use it.
+ for (name, entry) in &mut self.external_agents {
+ let Some((old_version, mut tx)) = old_versioned_agents.remove(name) else {
+ continue;
+ };
+ let Some(new_version) = entry.server.version() else {
+ continue;
+ };
+
+ if new_version != &old_version {
+ tx.send(Some(new_version.to_string())).ok();
+ } else {
+ entry.server.set_new_version_available_tx(tx);
+ }
+ }
+
*old_settings = Some(new_settings);
if let Some((project_id, downstream_client)) = downstream_client {
@@ -798,9 +846,8 @@ impl AgentServerStore {
let mut metadata = HashMap::default();
for (name, mut entry) in previous_entries.drain() {
- if let Some(agent) = entry.server.downcast_mut::<RemoteExternalAgentServer>() {
- new_version_available_txs
- .insert(name.clone(), agent.new_version_available_tx.take());
+ if let Some(tx) = entry.server.take_new_version_available_tx() {
+ new_version_available_txs.insert(name.clone(), tx);
}
metadata.insert(name, (entry.icon, entry.display_name, entry.source));
@@ -831,9 +878,7 @@ impl AgentServerStore {
upstream_client: upstream_client.clone(),
worktree_store: worktree_store.clone(),
name: agent_id.clone(),
- new_version_available_tx: new_version_available_txs
- .remove(&agent_id)
- .flatten(),
+ new_version_available_tx: new_version_available_txs.remove(&agent_id),
};
(
agent_id,
@@ -867,25 +912,28 @@ impl AgentServerStore {
);
};
+ extension_agents.clear();
for ExternalExtensionAgent {
name,
icon_path,
extension_id,
targets,
env,
+ version,
} in envelope.payload.agents
{
- extension_agents.push((
- Arc::from(&*name),
+ extension_agents.push(ExtensionAgentEntry {
+ agent_name: Arc::from(&*name),
extension_id,
- targets
+ targets: targets
.into_iter()
.map(|(k, v)| (k, extension::TargetConfig::from_proto(v)))
.collect(),
- env.into_iter().collect(),
+ env: env.into_iter().collect(),
icon_path,
- None,
- ));
+ display_name: None,
+ version: version.map(SharedString::from),
+ });
}
this.reregister_agents(cx);
@@ -900,23 +948,21 @@ impl AgentServerStore {
mut cx: AsyncApp,
) -> Result<()> {
this.update(&mut cx, |this, _| {
- if let Some(agent) = this.external_agents.get_mut(&*envelope.payload.name)
- && let Some(agent) = agent.server.downcast_mut::<RemoteExternalAgentServer>()
- && let Some(new_version_available_tx) = &mut agent.new_version_available_tx
+ if let Some(entry) = this.external_agents.get_mut(&*envelope.payload.name)
+ && let Some(mut tx) = entry.server.take_new_version_available_tx()
{
- new_version_available_tx
- .send(Some(envelope.payload.version))
- .ok();
+ tx.send(Some(envelope.payload.version)).ok();
+ entry.server.set_new_version_available_tx(tx);
}
});
Ok(())
}
- pub fn get_extension_id_for_agent(&mut self, name: &AgentId) -> Option<Arc<str>> {
- self.external_agents.get_mut(name).and_then(|entry| {
+ 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_mut()
+ .as_any()
.downcast_ref::<LocalExtensionArchiveAgent>()
.map(|ext_agent| ext_agent.extension_id.clone())
})
@@ -932,6 +978,14 @@ struct RemoteExternalAgentServer {
}
impl ExternalAgentServer for RemoteExternalAgentServer {
+ 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(
&mut self,
extra_env: HashMap<String, String>,
@@ -982,6 +1036,10 @@ impl ExternalAgentServer for RemoteExternalAgentServer {
})
}
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
@@ -1039,6 +1097,45 @@ fn github_release_archive_from_url(archive_url: &str) -> Option<GithubReleaseArc
})
}
+fn sanitized_version_component(version: &str) -> String {
+ let sanitized = version
+ .chars()
+ .map(|character| match character {
+ 'a'..='z' | 'A'..='Z' | '0'..='9' | '.' | '_' | '-' => character,
+ _ => '-',
+ })
+ .collect::<String>();
+
+ if sanitized.is_empty() {
+ "unknown".to_string()
+ } else {
+ sanitized
+ }
+}
+
+fn versioned_archive_cache_dir(
+ base_dir: &Path,
+ version: Option<&str>,
+ archive_url: &str,
+) -> PathBuf {
+ let version = version.unwrap_or_default();
+ let sanitized_version = sanitized_version_component(version);
+
+ let mut version_hasher = Sha256::new();
+ version_hasher.update(version.as_bytes());
+ let version_hash = format!("{:x}", version_hasher.finalize());
+
+ let mut url_hasher = Sha256::new();
+ url_hasher.update(archive_url.as_bytes());
+ let url_hash = format!("{:x}", url_hasher.finalize());
+
+ base_dir.join(format!(
+ "v_{sanitized_version}_{}_{}",
+ &version_hash[..16],
+ &url_hash[..16],
+ ))
+}
+
pub struct LocalExtensionArchiveAgent {
pub fs: Arc<dyn Fs>,
pub http_client: Arc<dyn HttpClient>,
@@ -1048,15 +1145,30 @@ pub struct LocalExtensionArchiveAgent {
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(
&mut self,
extra_env: HashMap<String, String>,
- _new_version_available_tx: Option<watch::Sender<Option<String>>>,
+ new_version_available_tx: Option<watch::Sender<Option<String>>>,
cx: &mut AsyncApp,
) -> Task<Result<AgentServerCommand>> {
+ self.new_version_available_tx = new_version_available_tx;
let fs = self.fs.clone();
let http_client = self.http_client.clone();
let node_runtime = self.node_runtime.clone();
@@ -1065,6 +1177,7 @@ impl ExternalAgentServer for LocalExtensionArchiveAgent {
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
@@ -1120,13 +1233,11 @@ impl ExternalAgentServer for LocalExtensionArchiveAgent {
})?;
let archive_url = &target_config.archive;
-
- // Use URL as version identifier for caching
- // Hash the URL to get a stable directory name
- let mut hasher = Sha256::new();
- hasher.update(archive_url.as_bytes());
- let url_hash = format!("{:x}", hasher.finalize());
- let version_dir = dir.join(format!("v_{}", url_hash));
+ 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
@@ -1213,6 +1324,10 @@ impl ExternalAgentServer for LocalExtensionArchiveAgent {
})
}
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
@@ -1224,17 +1339,32 @@ struct LocalRegistryArchiveAgent {
node_runtime: NodeRuntime,
project_environment: Entity<ProjectEnvironment>,
registry_id: Arc<str>,
+ version: SharedString,
targets: HashMap<String, RegistryTargetConfig>,
env: HashMap<String, String>,
+ new_version_available_tx: Option<watch::Sender<Option<String>>>,
}
impl ExternalAgentServer for LocalRegistryArchiveAgent {
+ fn version(&self) -> Option<&SharedString> {
+ Some(&self.version)
+ }
+
+ 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(
&mut self,
extra_env: HashMap<String, String>,
- _new_version_available_tx: Option<watch::Sender<Option<String>>>,
+ new_version_available_tx: Option<watch::Sender<Option<String>>>,
cx: &mut AsyncApp,
) -> Task<Result<AgentServerCommand>> {
+ self.new_version_available_tx = new_version_available_tx;
let fs = self.fs.clone();
let http_client = self.http_client.clone();
let node_runtime = self.node_runtime.clone();
@@ -1242,6 +1372,7 @@ impl ExternalAgentServer for LocalRegistryArchiveAgent {
let registry_id = self.registry_id.clone();
let targets = self.targets.clone();
let settings_env = self.env.clone();
+ let version = self.version.clone();
cx.spawn(async move |cx| {
let mut env = project_environment
@@ -1296,11 +1427,8 @@ impl ExternalAgentServer for LocalRegistryArchiveAgent {
env.extend(settings_env);
let archive_url = &target_config.archive;
-
- let mut hasher = Sha256::new();
- hasher.update(archive_url.as_bytes());
- let url_hash = format!("{:x}", hasher.finalize());
- let version_dir = dir.join(format!("v_{}", url_hash));
+ let version_dir =
+ versioned_archive_cache_dir(&dir, Some(version.as_ref()), archive_url);
if !fs.is_dir(&version_dir).await {
let sha256 = if let Some(provided_sha) = &target_config.sha256 {
@@ -1377,6 +1505,10 @@ impl ExternalAgentServer for LocalRegistryArchiveAgent {
})
}
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
@@ -1385,19 +1517,34 @@ impl ExternalAgentServer for LocalRegistryArchiveAgent {
struct LocalRegistryNpxAgent {
node_runtime: NodeRuntime,
project_environment: Entity<ProjectEnvironment>,
+ version: SharedString,
package: SharedString,
args: Vec<String>,
distribution_env: HashMap<String, String>,
settings_env: HashMap<String, String>,
+ new_version_available_tx: Option<watch::Sender<Option<String>>>,
}
impl ExternalAgentServer for LocalRegistryNpxAgent {
+ fn version(&self) -> Option<&SharedString> {
+ Some(&self.version)
+ }
+
+ 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(
&mut self,
extra_env: HashMap<String, String>,
- _new_version_available_tx: Option<watch::Sender<Option<String>>>,
+ new_version_available_tx: Option<watch::Sender<Option<String>>>,
cx: &mut AsyncApp,
) -> Task<Result<AgentServerCommand>> {
+ self.new_version_available_tx = new_version_available_tx;
let node_runtime = self.node_runtime.clone();
let project_environment = self.project_environment.downgrade();
let package = self.package.clone();
@@ -1442,6 +1589,10 @@ impl ExternalAgentServer for LocalRegistryNpxAgent {
})
}
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
@@ -1479,6 +1630,10 @@ impl ExternalAgentServer for LocalCustomAgent {
})
}
+ fn as_any(&self) -> &dyn Any {
+ self
+ }
+
fn as_any_mut(&mut self) -> &mut dyn Any {
self
}
@@ -1687,9 +1842,168 @@ impl CustomAgentServerSettings {
}
}
+impl From<settings::CustomAgentServerSettings> for CustomAgentServerSettings {
+ fn from(value: settings::CustomAgentServerSettings) -> Self {
+ match value {
+ settings::CustomAgentServerSettings::Custom {
+ path,
+ args,
+ env,
+ default_mode,
+ default_model,
+ favorite_models,
+ default_config_options,
+ favorite_config_option_values,
+ } => CustomAgentServerSettings::Custom {
+ command: AgentServerCommand {
+ path: PathBuf::from(shellexpand::tilde(&path.to_string_lossy()).as_ref()),
+ args,
+ env: Some(env),
+ },
+ default_mode,
+ default_model,
+ favorite_models,
+ default_config_options,
+ 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 {
+ env,
+ default_mode,
+ default_model,
+ default_config_options,
+ favorite_models,
+ favorite_config_option_values,
+ } => CustomAgentServerSettings::Registry {
+ env,
+ default_mode,
+ default_model,
+ default_config_options,
+ favorite_models,
+ favorite_config_option_values,
+ },
+ }
+ }
+}
+
+impl settings::Settings for AllAgentServersSettings {
+ fn from_settings(content: &settings::SettingsContent) -> Self {
+ let agent_settings = content.agent_servers.clone().unwrap();
+ Self(
+ agent_settings
+ .0
+ .into_iter()
+ .map(|(k, v)| (k, v.into()))
+ .collect(),
+ )
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
+ use crate::agent_registry_store::{
+ AgentRegistryStore, RegistryAgent, RegistryAgentMetadata, RegistryNpxAgent,
+ };
+ use crate::worktree_store::{WorktreeIdCounter, WorktreeStore};
+ use gpui::{AppContext as _, TestAppContext};
+ use node_runtime::NodeRuntime;
+ use settings::Settings as _;
+
+ fn make_npx_agent(id: &str, version: &str) -> RegistryAgent {
+ let id = SharedString::from(id.to_string());
+ RegistryAgent::Npx(RegistryNpxAgent {
+ metadata: RegistryAgentMetadata {
+ id: AgentId::new(id.clone()),
+ name: id.clone(),
+ description: SharedString::from(""),
+ version: SharedString::from(version.to_string()),
+ repository: None,
+ website: None,
+ icon_path: None,
+ },
+ package: id,
+ args: Vec::new(),
+ env: HashMap::default(),
+ })
+ }
+
+ fn init_test_settings(cx: &mut TestAppContext) {
+ cx.update(|cx| {
+ let settings_store = SettingsStore::test(cx);
+ cx.set_global(settings_store);
+ });
+ }
+
+ fn init_registry(
+ cx: &mut TestAppContext,
+ agents: Vec<RegistryAgent>,
+ ) -> gpui::Entity<AgentRegistryStore> {
+ cx.update(|cx| AgentRegistryStore::init_test_global(cx, agents))
+ }
+
+ fn set_registry_settings(cx: &mut TestAppContext, agent_names: &[&str]) {
+ cx.update(|cx| {
+ AllAgentServersSettings::override_global(
+ AllAgentServersSettings(
+ agent_names
+ .iter()
+ .map(|name| {
+ (
+ name.to_string(),
+ settings::CustomAgentServerSettings::Registry {
+ env: HashMap::default(),
+ default_mode: None,
+ default_model: None,
+ favorite_models: Vec::new(),
+ default_config_options: HashMap::default(),
+ favorite_config_option_values: HashMap::default(),
+ }
+ .into(),
+ )
+ })
+ .collect(),
+ ),
+ cx,
+ );
+ });
+ }
+
+ fn create_agent_server_store(cx: &mut TestAppContext) -> gpui::Entity<AgentServerStore> {
+ cx.update(|cx| {
+ let fs: Arc<dyn Fs> = fs::FakeFs::new(cx.background_executor().clone());
+ 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 http_client = http_client::FakeHttpClient::with_404_response();
+
+ cx.new(|cx| {
+ AgentServerStore::local(
+ NodeRuntime::unavailable(),
+ fs,
+ project_environment,
+ http_client,
+ cx,
+ )
+ })
+ })
+ }
#[test]
fn detects_supported_archive_suffixes() {
@@ -1755,78 +2069,182 @@ mod tests {
assert_eq!(
error,
- Some("unsupported archive type in URL: https://example.com/agent.tar.xz".to_string())
+ Some("unsupported archive type in URL: https://example.com/agent.tar.xz".to_string()),
);
}
-}
-impl From<settings::CustomAgentServerSettings> for CustomAgentServerSettings {
- fn from(value: settings::CustomAgentServerSettings) -> Self {
- match value {
- settings::CustomAgentServerSettings::Custom {
- path,
- args,
- env,
- default_mode,
- default_model,
- favorite_models,
- default_config_options,
- favorite_config_option_values,
- } => CustomAgentServerSettings::Custom {
- command: AgentServerCommand {
- path: PathBuf::from(shellexpand::tilde(&path.to_string_lossy()).as_ref()),
- args,
- env: Some(env),
- },
- default_mode,
- default_model,
- favorite_models,
- default_config_options,
- 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 {
- env,
- default_mode,
- default_model,
- default_config_options,
- favorite_models,
- favorite_config_option_values,
- } => CustomAgentServerSettings::Registry {
- env,
- default_mode,
- default_model,
- default_config_options,
- favorite_models,
- favorite_config_option_values,
- },
- }
+ #[test]
+ fn versioned_archive_cache_dir_includes_version_before_url_hash() {
+ let slash_version_dir = versioned_archive_cache_dir(
+ Path::new("/tmp/agents"),
+ Some("release/2.3.5"),
+ "https://example.com/agent.zip",
+ );
+ let colon_version_dir = versioned_archive_cache_dir(
+ Path::new("/tmp/agents"),
+ Some("release:2.3.5"),
+ "https://example.com/agent.zip",
+ );
+ let file_name = slash_version_dir
+ .file_name()
+ .and_then(|name| name.to_str())
+ .expect("cache directory should have a file name");
+
+ assert!(file_name.starts_with("v_release-2.3.5_"));
+ assert_ne!(slash_version_dir, colon_version_dir);
}
-}
-impl settings::Settings for AllAgentServersSettings {
- fn from_settings(content: &settings::SettingsContent) -> Self {
- let agent_settings = content.agent_servers.clone().unwrap();
- Self(
- agent_settings
- .0
- .into_iter()
- .map(|(k, v)| (k, v.into()))
- .collect(),
- )
+ #[gpui::test]
+ fn test_version_change_sends_notification(cx: &mut TestAppContext) {
+ init_test_settings(cx);
+ let registry = init_registry(cx, vec![make_npx_agent("test-agent", "1.0.0")]);
+ set_registry_settings(cx, &["test-agent"]);
+ let store = create_agent_server_store(cx);
+
+ // Verify the agent was registered with version 1.0.0.
+ store.read_with(cx, |store, _| {
+ let entry = store
+ .external_agents
+ .get(&AgentId::new("test-agent"))
+ .expect("agent should be registered");
+ assert_eq!(
+ entry.server.version().map(|v| v.to_string()),
+ Some("1.0.0".to_string())
+ );
+ });
+
+ // Set up a watch channel and store the tx on the agent.
+ let (tx, mut rx) = watch::channel::<Option<String>>(None);
+ store.update(cx, |store, _| {
+ let entry = store
+ .external_agents
+ .get_mut(&AgentId::new("test-agent"))
+ .expect("agent should be registered");
+ entry.server.set_new_version_available_tx(tx);
+ });
+
+ // Update the registry to version 2.0.0.
+ registry.update(cx, |store, cx| {
+ store.set_agents(vec![make_npx_agent("test-agent", "2.0.0")], cx);
+ });
+ cx.run_until_parked();
+
+ // The watch channel should have received the new version.
+ assert_eq!(rx.borrow().as_deref(), Some("2.0.0"));
+ }
+
+ #[gpui::test]
+ fn test_same_version_preserves_tx(cx: &mut TestAppContext) {
+ init_test_settings(cx);
+ let registry = init_registry(cx, vec![make_npx_agent("test-agent", "1.0.0")]);
+ set_registry_settings(cx, &["test-agent"]);
+ let store = create_agent_server_store(cx);
+
+ let (tx, mut rx) = watch::channel::<Option<String>>(None);
+ store.update(cx, |store, _| {
+ let entry = store
+ .external_agents
+ .get_mut(&AgentId::new("test-agent"))
+ .expect("agent should be registered");
+ entry.server.set_new_version_available_tx(tx);
+ });
+
+ // "Refresh" the registry with the same version.
+ registry.update(cx, |store, cx| {
+ store.set_agents(vec![make_npx_agent("test-agent", "1.0.0")], cx);
+ });
+ cx.run_until_parked();
+
+ // No notification should have been sent.
+ assert_eq!(rx.borrow().as_deref(), None);
+
+ // The tx should have been transferred to the rebuilt agent entry.
+ store.update(cx, |store, _| {
+ let entry = store
+ .external_agents
+ .get_mut(&AgentId::new("test-agent"))
+ .expect("agent should be registered");
+ assert!(
+ entry.server.take_new_version_available_tx().is_some(),
+ "tx should have been transferred to the rebuilt agent"
+ );
+ });
+ }
+
+ #[gpui::test]
+ fn test_no_tx_stored_does_not_panic_on_version_change(cx: &mut TestAppContext) {
+ init_test_settings(cx);
+ let registry = init_registry(cx, vec![make_npx_agent("test-agent", "1.0.0")]);
+ set_registry_settings(cx, &["test-agent"]);
+ let _store = create_agent_server_store(cx);
+
+ // Update the registry without having stored any tx — should not panic.
+ registry.update(cx, |store, cx| {
+ store.set_agents(vec![make_npx_agent("test-agent", "2.0.0")], cx);
+ });
+ cx.run_until_parked();
+ }
+
+ #[gpui::test]
+ fn test_multiple_agents_independent_notifications(cx: &mut TestAppContext) {
+ init_test_settings(cx);
+ let registry = init_registry(
+ cx,
+ vec![
+ make_npx_agent("agent-a", "1.0.0"),
+ make_npx_agent("agent-b", "3.0.0"),
+ ],
+ );
+ set_registry_settings(cx, &["agent-a", "agent-b"]);
+ let store = create_agent_server_store(cx);
+
+ let (tx_a, mut rx_a) = watch::channel::<Option<String>>(None);
+ let (tx_b, mut rx_b) = watch::channel::<Option<String>>(None);
+ store.update(cx, |store, _| {
+ store
+ .external_agents
+ .get_mut(&AgentId::new("agent-a"))
+ .expect("agent-a should be registered")
+ .server
+ .set_new_version_available_tx(tx_a);
+ store
+ .external_agents
+ .get_mut(&AgentId::new("agent-b"))
+ .expect("agent-b should be registered")
+ .server
+ .set_new_version_available_tx(tx_b);
+ });
+
+ // Update only agent-a to a new version; agent-b stays the same.
+ registry.update(cx, |store, cx| {
+ store.set_agents(
+ vec![
+ make_npx_agent("agent-a", "2.0.0"),
+ make_npx_agent("agent-b", "3.0.0"),
+ ],
+ cx,
+ );
+ });
+ cx.run_until_parked();
+
+ // agent-a should have received a notification.
+ assert_eq!(rx_a.borrow().as_deref(), Some("2.0.0"));
+
+ // agent-b should NOT have received a notification.
+ assert_eq!(rx_b.borrow().as_deref(), None);
+
+ // agent-b's tx should have been transferred.
+ store.update(cx, |store, _| {
+ assert!(
+ store
+ .external_agents
+ .get_mut(&AgentId::new("agent-b"))
+ .expect("agent-b should be registered")
+ .server
+ .take_new_version_available_tx()
+ .is_some(),
+ "agent-b tx should have been transferred"
+ );
+ });
}
}