Skip to main content

nexus_core/app/
skills_popup.rs

1use super::App;
2use tokio::sync::mpsc;
3
4impl App {
5    /// Re-read discovered skills from disk (after an install/remove, an
6    /// external Agent Skills change, or a Ctrl+E hand-edit of `SKILL.md`).
7    /// The view re-clamps its cursor after calling this.
8    pub fn reload_skills(&mut self) {
9        let skills =
10            crate::skills::load_skills_from_dirs(&crate::skills::app_skill_roots(&self.space.root));
11        if skills != self.skills {
12            self.skills = skills;
13            self.bump_cache_epoch();
14        }
15    }
16
17    /// Whether the selected skill belongs to Nexus's writable skill root.
18    /// Skills discovered from Agent Skills roots are intentionally read-only
19    /// to the remove action; editing them remains possible through `$EDITOR`.
20    pub fn skill_is_app_managed(&self, skill: &crate::skills::Skill) -> bool {
21        crate::skills::is_app_managed(skill, &self.space.root)
22    }
23
24    /// Domain half of `/skills` install: parse the typed `owner/repo/path`
25    /// (or `owner/repo`) and kick off the background GitHub fetch. Same
26    /// bg-task shape as memory extraction. The view owns the edit buffer and
27    /// mode; `Ok(())` means the task started (or the spec was invalid — the
28    /// message is pushed as a status line).
29    pub fn start_skill_install(&mut self, spec: &str) {
30        let spec = spec.trim().to_string();
31        let Some((owner, repo, path)) = crate::skills::parse_gh_shorthand(&spec) else {
32            self.push_status(format!("expected owner/repo/path, got: {spec}"));
33            return;
34        };
35        let dest = crate::skills::skills_dir(&self.space.root);
36        let (tx, rx) = mpsc::unbounded_channel();
37        self.skills_rx = Some(rx);
38        self.push_status(format!("installing {spec}…"));
39        tokio::spawn(async move {
40            let client = reqwest::Client::new();
41            let result = crate::skills::install_from_github(&client, &owner, &repo, &path, &dest)
42                .await
43                .map_err(|e| e.to_string());
44            let _ = tx.send(result);
45        });
46    }
47
48    pub fn on_skill_install_result(&mut self, result: Option<Result<String, String>>) {
49        self.skills_rx = None;
50        match result {
51            Some(Ok(name)) => {
52                self.reload_skills();
53                self.push_status(format!("installed skill: {name}"));
54            }
55            Some(Err(e)) => self.push_status(format!("skill install failed: {e}")),
56            None => {}
57        }
58    }
59}