From 5d3a51e29ebb2596e17afb96366e8b05301dcf44 Mon Sep 17 00:00:00 2001 From: Duducoco <69681789+Duducoco@users.noreply.github.com> Date: Fri, 8 May 2026 01:00:55 +0800 Subject: [PATCH] fix(skills): show all skills in slash menu (#1083) Aligns the slash-menu cache with `discover_in_workspace`, so global `~/.deepseek/skills` and `~/.claude/skills` skills appear alongside project-local `.agents/skills` in the slash menu, `/skills` listing, and `/skill ` lookups. Also strips surrounding YAML quotes from `SKILL.md` frontmatter values, so a skill declared as `name: "hud"` registers as `hud` and matches prefix lookup. Closes #1068. Thanks to @AlphaGogoo / @Duducoco for the fix and the test. --- crates/tui/src/skills/mod.rs | 12 ++++++++++- crates/tui/src/tui/app.rs | 42 ++++++++++++++++++++++++++++++++---- 2 files changed, 49 insertions(+), 5 deletions(-) diff --git a/crates/tui/src/skills/mod.rs b/crates/tui/src/skills/mod.rs index 72532572..ff69fc4b 100644 --- a/crates/tui/src/skills/mod.rs +++ b/crates/tui/src/skills/mod.rs @@ -248,7 +248,17 @@ impl SkillRegistry { continue; } if let Some((key, value)) = line.split_once(':') { - metadata.insert(key.trim().to_ascii_lowercase(), value.trim().to_string()); + let value = value.trim(); + let unquoted = if (value.starts_with('"') + && value.ends_with('"') + && value.len() >= 2) + || (value.starts_with('\'') && value.ends_with('\'') && value.len() >= 2) + { + &value[1..value.len() - 1] + } else { + value + }; + metadata.insert(key.trim().to_ascii_lowercase(), unquoted.to_string()); } } diff --git a/crates/tui/src/tui/app.rs b/crates/tui/src/tui/app.rs index bc6204c3..86f82c3e 100644 --- a/crates/tui/src/tui/app.rs +++ b/crates/tui/src/tui/app.rs @@ -1246,7 +1246,7 @@ impl App { } else { global_skills_dir }; - let cached_skills = Self::discover_cached_skills(&skills_dir); + let cached_skills = Self::discover_cached_skills(&workspace); let input_history = crate::composer_history::load_history(); let (initial_input_text, initial_input_cursor) = match initial_input { @@ -1440,8 +1440,8 @@ impl App { } } - fn discover_cached_skills(skills_dir: &std::path::Path) -> Vec<(String, String)> { - crate::skills::SkillRegistry::discover(skills_dir) + fn discover_cached_skills(workspace: &std::path::Path) -> Vec<(String, String)> { + crate::skills::discover_in_workspace(workspace) .list() .iter() .map(|s| (s.name.clone(), s.description.clone())) @@ -1449,7 +1449,7 @@ impl App { } pub fn refresh_skill_cache(&mut self) { - self.cached_skills = Self::discover_cached_skills(&self.skills_dir); + self.cached_skills = Self::discover_cached_skills(&self.workspace); } pub fn submit_api_key(&mut self) -> Result { @@ -3918,6 +3918,40 @@ mod tests { })); } + #[test] + fn cached_skills_merges_across_candidate_directories() { + let tmp = tempfile::TempDir::new().expect("tempdir"); + let workspace = tmp.path().join("workspace"); + + // Higher-precedence directory contains a stale empty dir for `foo` + // (no SKILL.md). This used to shadow the real definition further + // down the candidate list when the cache only scanned a single dir. + std::fs::create_dir_all(workspace.join(".agents").join("skills").join("foo")) + .expect("stale empty dir"); + + // Lower-precedence directory has the real skill. + let real_dir = workspace.join(".claude").join("skills").join("foo"); + std::fs::create_dir_all(&real_dir).expect("real skill dir"); + std::fs::write( + real_dir.join("SKILL.md"), + "---\nname: foo\ndescription: Real foo skill\n---\nbody\n", + ) + .expect("skill file"); + + let mut options = test_options(false); + options.workspace = workspace.clone(); + options.skills_dir = tmp.path().join("global-skills"); + let app = App::new(options, &Config::default()); + + assert!( + app.cached_skills + .iter() + .any(|(name, description)| name == "foo" && description == "Real foo skill"), + "cached_skills should fall through to lower-precedence dir when higher-precedence one has an empty stub: {:?}", + app.cached_skills, + ); + } + #[test] fn submit_input_consolidates_oversized_input_into_paste_file() { let tmp = tempfile::TempDir::new().expect("tempdir");