Files
galaxy/app/src/ai/skills/file_watchers/subscribers.rs
T

140 lines
4.9 KiB
Rust

use std::pin::Pin;
use futures::Future;
use ai::skills::{read_skills, ParsedSkill, SKILL_PROVIDER_DEFINITIONS};
use async_channel::Sender;
use galaxyui::ModelContext;
use repo_metadata::{repository::RepositorySubscriber, Repository, RepositoryUpdate};
/// Messages sent from [`RepositorySubscriber`]s to [`SkillManager`].
pub enum SkillRepositoryMessage {
/// Initial scan of a home skills directory (e.g., `~/.agents`).
HomeInitialScan { skills: Vec<ParsedSkill> },
/// Incremental file system updates from either a home provider directory or a project skills directory.
RepositoryUpdate { update: RepositoryUpdate },
/// File changes detected in a resolved symlink target directory.
SymlinkTargetUpdate { update: RepositoryUpdate },
}
/// A repository subscriber for project directories that forwards file change events to [`SkillManager`].
pub struct ProjectSkillSubscriber {
pub message_tx: Sender<SkillRepositoryMessage>,
}
impl RepositorySubscriber for ProjectSkillSubscriber {
fn on_scan(
&mut self,
_repository: &Repository,
_ctx: &mut ModelContext<Repository>,
) -> Pin<Box<dyn Future<Output = ()> + Send + 'static>> {
// Initial skill scanning is handled via RepositoryMetadataEvent::RepositoryUpdated,
// which fires AFTER the file tree is built. This subscriber is only used for
// incremental file change updates via on_files_updated.
Box::pin(async {})
}
fn on_files_updated(
&mut self,
_repository: &Repository,
update: &RepositoryUpdate,
_ctx: &mut ModelContext<Repository>,
) -> Pin<Box<dyn Future<Output = ()> + Send + 'static>> {
let tx = self.message_tx.clone();
let update = update.clone();
Box::pin(async move {
let _ = tx
.send(SkillRepositoryMessage::RepositoryUpdate { update })
.await;
})
}
}
/// A repository subscriber for resolved symlink target directories.
/// Forwards file change events as `SymlinkTargetUpdate` so the `SkillWatcher`
/// can map canonical paths back to their original symlink-based skill paths.
pub struct SymlinkSkillSubscriber {
pub message_tx: Sender<SkillRepositoryMessage>,
}
impl RepositorySubscriber for SymlinkSkillSubscriber {
fn on_scan(
&mut self,
_repository: &Repository,
_ctx: &mut ModelContext<Repository>,
) -> Pin<Box<dyn Future<Output = ()> + Send + 'static>> {
// Skills are already loaded at the symlink path; no initial scan needed.
Box::pin(async {})
}
fn on_files_updated(
&mut self,
_repository: &Repository,
update: &RepositoryUpdate,
_ctx: &mut ModelContext<Repository>,
) -> Pin<Box<dyn Future<Output = ()> + Send + 'static>> {
let tx = self.message_tx.clone();
let update = update.clone();
Box::pin(async move {
let _ = tx
.send(SkillRepositoryMessage::SymlinkTargetUpdate { update })
.await;
})
}
}
/// A repository subscriber for home skills directories that forwards file change events to [`SkillManager`].
pub struct HomeSkillSubscriber {
pub message_tx: Sender<SkillRepositoryMessage>,
}
impl RepositorySubscriber for HomeSkillSubscriber {
fn on_scan(
&mut self,
repository: &Repository,
_ctx: &mut ModelContext<Repository>,
) -> Pin<Box<dyn Future<Output = ()> + Send + 'static>> {
// For a home skills directory, this is the provider directory. E.g. ~/.agents
let repo_path = repository.root_dir().to_local_path_lossy();
let home_dir = dirs::home_dir();
let tx = self.message_tx.clone();
Box::pin(async move {
if let Some(home_dir) = home_dir {
let skills_path = SKILL_PROVIDER_DEFINITIONS.iter().find_map(|definition| {
let candidate_path = home_dir.join(definition.skills_path.clone());
if candidate_path.starts_with(&repo_path) {
Some(candidate_path)
} else {
None
}
});
if let Some(skills_path) = skills_path {
let skills = read_skills(&skills_path);
let _ = tx
.send(SkillRepositoryMessage::HomeInitialScan { skills })
.await;
}
}
})
}
fn on_files_updated(
&mut self,
_repository: &Repository,
update: &RepositoryUpdate,
_ctx: &mut ModelContext<Repository>,
) -> Pin<Box<dyn Future<Output = ()> + Send + 'static>> {
let tx = self.message_tx.clone();
let update = update.clone();
Box::pin(async move {
let _ = tx
.send(SkillRepositoryMessage::RepositoryUpdate { update })
.await;
})
}
}