Scope skill completions to /skill

Keep individual skills out of the top-level slash command menu so large
  skill collections do not crowd out built-in commands.

  Skills still complete after `/skill`, including both the full skill list
  after `/skill ` and prefix matches after `/skill <prefix>`.

(cherry picked from commit 57f8e3ad84dad9cf46290c0dc23e2b26504196df)
This commit is contained in:
reidliu41
2026-05-11 17:24:39 +08:00
committed by Hunter Bown
parent ca284d1fc0
commit 46ab1fdf62
+33 -34
View File
@@ -1986,23 +1986,20 @@ pub(crate) fn slash_completion_hints(
}
}
// Cached skills
let skill_prefix = completing_skill_arg.unwrap_or(prefix);
let prefix_lower = skill_prefix.to_ascii_lowercase();
for (skill_name, skill_desc) in cached_skills {
let skill_name_lower = skill_name.to_ascii_lowercase();
let command_prefix_matches = completing_skill_arg.is_none()
&& (prefix_lower.is_empty()
|| "skill".starts_with(&prefix_lower)
|| skill_name_lower.starts_with(&prefix_lower));
let skill_arg_matches =
completing_skill_arg.is_some() && skill_name_lower.starts_with(&prefix_lower);
if command_prefix_matches || skill_arg_matches {
entries.push(SlashMenuEntry {
name: format!("/skill {skill_name}"),
description: skill_desc.clone(),
is_skill: true,
});
// Cached skills are arguments to `/skill`, not top-level commands. Keep
// the top-level slash menu focused on commands and expand skills only
// after the user has selected the skill command.
let prefix_lower = completing_skill_arg.unwrap_or(prefix).to_ascii_lowercase();
if completing_skill_arg.is_some() {
for (skill_name, skill_desc) in cached_skills {
let skill_name_lower = skill_name.to_ascii_lowercase();
if skill_name_lower.starts_with(&prefix_lower) {
entries.push(SlashMenuEntry {
name: format!("/skill {skill_name}"),
description: skill_desc.clone(),
is_skill: true,
});
}
}
}
@@ -2373,39 +2370,41 @@ mod tests {
}
#[test]
fn slash_completion_hints_include_skills() {
fn slash_completion_hints_hide_skills_from_top_level_menu() {
let cached_skills = vec![
("search-files".to_string(), "Search files".to_string()),
("my-review".to_string(), "Review code".to_string()),
];
let hints = slash_completion_hints("/", 128, &cached_skills, Locale::En, None);
assert!(
hints
.iter()
.any(|hint| hint.name == "/skill search-files" && hint.is_skill)
);
assert!(
hints
.iter()
.any(|hint| hint.name == "/skill my-review" && hint.is_skill)
);
assert!(hints.iter().any(|hint| hint.name == "/skill"));
assert!(hints.iter().any(|hint| hint.name == "/skills"));
assert!(!hints.iter().any(|hint| hint.is_skill));
}
#[test]
fn slash_completion_hints_skills_match_prefix() {
fn slash_completion_hints_hide_skills_from_top_level_prefix() {
let cached_skills = vec![
("search-files".to_string(), "Search files".to_string()),
("my-review".to_string(), "Review code".to_string()),
];
let hints = slash_completion_hints("/se", 128, &cached_skills, Locale::En, None);
assert!(
hints
.iter()
.any(|hint| hint.name == "/skill search-files" && hint.is_skill)
);
assert!(!hints.iter().any(|hint| hint.name == "/skill search-files"));
assert!(!hints.iter().any(|hint| hint.name == "/skill my-review"));
}
#[test]
fn slash_completion_hints_complete_skill_argument_all() {
let cached_skills = vec![
("search-files".to_string(), "Search files".to_string()),
("my-review".to_string(), "Review code".to_string()),
];
let hints = slash_completion_hints("/skill ", 128, &cached_skills, Locale::En, None);
assert_eq!(hints.len(), 2);
assert!(hints.iter().any(|hint| hint.name == "/skill search-files"));
assert!(hints.iter().any(|hint| hint.name == "/skill my-review"));
assert!(hints.iter().all(|hint| hint.is_skill));
}
#[test]
fn slash_completion_hints_complete_skill_argument_prefix() {
let cached_skills = vec![