feat: Support listing and selecting skills via $ or /skills (#7506)

List/Select skills with $-mention or /skills
This commit is contained in:
xl-openai
2025-12-03 15:12:46 -08:00
committed by GitHub
parent 231ff19ca2
commit 9a50a04400
11 changed files with 505 additions and 97 deletions

View File

@@ -31,8 +31,11 @@ pub(crate) struct CommandPopup {
}
impl CommandPopup {
pub(crate) fn new(mut prompts: Vec<CustomPrompt>) -> Self {
let builtins = built_in_slash_commands();
pub(crate) fn new(mut prompts: Vec<CustomPrompt>, skills_enabled: bool) -> Self {
let builtins: Vec<(&'static str, SlashCommand)> = built_in_slash_commands()
.into_iter()
.filter(|(_, cmd)| skills_enabled || *cmd != SlashCommand::Skills)
.collect();
// Exclude prompts that collide with builtin command names and sort by name.
let exclude: HashSet<String> = builtins.iter().map(|(n, _)| (*n).to_string()).collect();
prompts.retain(|p| !exclude.contains(&p.name));
@@ -232,7 +235,7 @@ mod tests {
#[test]
fn filter_includes_init_when_typing_prefix() {
let mut popup = CommandPopup::new(Vec::new());
let mut popup = CommandPopup::new(Vec::new(), false);
// Simulate the composer line starting with '/in' so the popup filters
// matching commands by prefix.
popup.on_composer_text_change("/in".to_string());
@@ -252,7 +255,7 @@ mod tests {
#[test]
fn selecting_init_by_exact_match() {
let mut popup = CommandPopup::new(Vec::new());
let mut popup = CommandPopup::new(Vec::new(), false);
popup.on_composer_text_change("/init".to_string());
// When an exact match exists, the selected command should be that
@@ -267,7 +270,7 @@ mod tests {
#[test]
fn model_is_first_suggestion_for_mo() {
let mut popup = CommandPopup::new(Vec::new());
let mut popup = CommandPopup::new(Vec::new(), false);
popup.on_composer_text_change("/mo".to_string());
let matches = popup.filtered_items();
match matches.first() {
@@ -297,7 +300,7 @@ mod tests {
argument_hint: None,
},
];
let popup = CommandPopup::new(prompts);
let popup = CommandPopup::new(prompts, false);
let items = popup.filtered_items();
let mut prompt_names: Vec<String> = items
.into_iter()
@@ -313,13 +316,16 @@ mod tests {
#[test]
fn prompt_name_collision_with_builtin_is_ignored() {
// Create a prompt named like a builtin (e.g. "init").
let popup = CommandPopup::new(vec![CustomPrompt {
name: "init".to_string(),
path: "/tmp/init.md".to_string().into(),
content: "should be ignored".to_string(),
description: None,
argument_hint: None,
}]);
let popup = CommandPopup::new(
vec![CustomPrompt {
name: "init".to_string(),
path: "/tmp/init.md".to_string().into(),
content: "should be ignored".to_string(),
description: None,
argument_hint: None,
}],
false,
);
let items = popup.filtered_items();
let has_collision_prompt = items.into_iter().any(|it| match it {
CommandItem::UserPrompt(i) => popup.prompt(i).is_some_and(|p| p.name == "init"),
@@ -333,13 +339,16 @@ mod tests {
#[test]
fn prompt_description_uses_frontmatter_metadata() {
let popup = CommandPopup::new(vec![CustomPrompt {
name: "draftpr".to_string(),
path: "/tmp/draftpr.md".to_string().into(),
content: "body".to_string(),
description: Some("Create feature branch, commit and open draft PR.".to_string()),
argument_hint: None,
}]);
let popup = CommandPopup::new(
vec![CustomPrompt {
name: "draftpr".to_string(),
path: "/tmp/draftpr.md".to_string().into(),
content: "body".to_string(),
description: Some("Create feature branch, commit and open draft PR.".to_string()),
argument_hint: None,
}],
false,
);
let rows = popup.rows_from_matches(vec![(CommandItem::UserPrompt(0), None, 0)]);
let description = rows.first().and_then(|row| row.description.as_deref());
assert_eq!(
@@ -350,13 +359,16 @@ mod tests {
#[test]
fn prompt_description_falls_back_when_missing() {
let popup = CommandPopup::new(vec![CustomPrompt {
name: "foo".to_string(),
path: "/tmp/foo.md".to_string().into(),
content: "body".to_string(),
description: None,
argument_hint: None,
}]);
let popup = CommandPopup::new(
vec![CustomPrompt {
name: "foo".to_string(),
path: "/tmp/foo.md".to_string().into(),
content: "body".to_string(),
description: None,
argument_hint: None,
}],
false,
);
let rows = popup.rows_from_matches(vec![(CommandItem::UserPrompt(0), None, 0)]);
let description = rows.first().and_then(|row| row.description.as_deref());
assert_eq!(description, Some("send saved prompt"));