Add /skills command (#8600)
Signed-off-by: Lucas Alvares Gomes <lucasagomes@gmail.com> Co-authored-by: Lifei Zhou <lifei@squareup.com>
This commit is contained in:
committed by
GitHub
parent
501dde5570
commit
bdf5c43f3d
@@ -121,6 +121,31 @@ impl GooseCompleter {
|
|||||||
Ok((line.len(), vec![]))
|
Ok((line.len(), vec![]))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Complete skill names for the /skills command
|
||||||
|
fn complete_skill_names(&self, line: &str) -> Result<(usize, Vec<Pair>)> {
|
||||||
|
use goose::agents::platform_extensions::skills::list_installed_skills;
|
||||||
|
|
||||||
|
let cwd = std::env::current_dir().unwrap_or_default();
|
||||||
|
let skills = list_installed_skills(Some(&cwd));
|
||||||
|
let skill_names: Vec<String> = skills.iter().map(|s| s.name.clone()).collect();
|
||||||
|
|
||||||
|
// Complete the last letter being typed (e.g. "/skills coding in<tab>")
|
||||||
|
let last = line.rsplit_once(' ').map_or("", |(_, w)| w);
|
||||||
|
let pos = line.len() - last.len();
|
||||||
|
|
||||||
|
let partial = last.to_lowercase();
|
||||||
|
let candidates: Vec<Pair> = skill_names
|
||||||
|
.iter()
|
||||||
|
.filter(|name| name.to_lowercase().starts_with(&partial))
|
||||||
|
.map(|name| Pair {
|
||||||
|
display: name.clone(),
|
||||||
|
replacement: format!("{} ", name),
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok((pos, candidates))
|
||||||
|
}
|
||||||
|
|
||||||
/// Complete slash commands
|
/// Complete slash commands
|
||||||
fn complete_slash_commands(&self, line: &str) -> Result<(usize, Vec<Pair>)> {
|
fn complete_slash_commands(&self, line: &str) -> Result<(usize, Vec<Pair>)> {
|
||||||
// Define available slash commands
|
// Define available slash commands
|
||||||
@@ -136,6 +161,7 @@ impl GooseCompleter {
|
|||||||
"/prompt",
|
"/prompt",
|
||||||
"/mode",
|
"/mode",
|
||||||
"/recipe",
|
"/recipe",
|
||||||
|
"/skills",
|
||||||
];
|
];
|
||||||
|
|
||||||
// Find commands that match the prefix
|
// Find commands that match the prefix
|
||||||
@@ -374,6 +400,10 @@ impl Completer for GooseCompleter {
|
|||||||
return self.complete_mode_flags(line);
|
return self.complete_mode_flags(line);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if line.starts_with("/skills ") {
|
||||||
|
return self.complete_skill_names(line);
|
||||||
|
}
|
||||||
|
|
||||||
return Ok((pos, vec![]));
|
return Ok((pos, vec![]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ pub enum InputResult {
|
|||||||
Compact,
|
Compact,
|
||||||
ToggleFullToolOutput,
|
ToggleFullToolOutput,
|
||||||
Edit(Option<String>),
|
Edit(Option<String>),
|
||||||
|
ListSkills,
|
||||||
|
LoadSkills(Vec<String>),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -204,6 +206,7 @@ fn handle_slash_command(input: &str) -> Option<InputResult> {
|
|||||||
const CMD_SUMMARIZE_DEPRECATED: &str = "/summarize";
|
const CMD_SUMMARIZE_DEPRECATED: &str = "/summarize";
|
||||||
const CMD_EDIT: &str = "/edit";
|
const CMD_EDIT: &str = "/edit";
|
||||||
const CMD_EDIT_WITH_SPACE: &str = "/edit ";
|
const CMD_EDIT_WITH_SPACE: &str = "/edit ";
|
||||||
|
const CMD_SKILLS: &str = "/skills";
|
||||||
|
|
||||||
match input {
|
match input {
|
||||||
"/exit" | "/quit" => Some(InputResult::Exit),
|
"/exit" | "/quit" => Some(InputResult::Exit),
|
||||||
@@ -267,6 +270,16 @@ fn handle_slash_command(input: &str) -> Option<InputResult> {
|
|||||||
s if s == CMD_CLEAR => Some(InputResult::Clear),
|
s if s == CMD_CLEAR => Some(InputResult::Clear),
|
||||||
s if s.starts_with(CMD_RECIPE) => parse_recipe_command(s),
|
s if s.starts_with(CMD_RECIPE) => parse_recipe_command(s),
|
||||||
s if s == CMD_COMPACT => Some(InputResult::Compact),
|
s if s == CMD_COMPACT => Some(InputResult::Compact),
|
||||||
|
// Match "/skills" exactly or "/skills " with args - avoids matching e.g. "/skillsextra"
|
||||||
|
s if s == CMD_SKILLS || s.starts_with(&format!("{CMD_SKILLS} ")) => {
|
||||||
|
let args = s.get(CMD_SKILLS.len()..).unwrap_or("").trim();
|
||||||
|
if args.is_empty() {
|
||||||
|
Some(InputResult::ListSkills)
|
||||||
|
} else {
|
||||||
|
let names: Vec<String> = args.split_whitespace().map(String::from).collect();
|
||||||
|
Some(InputResult::LoadSkills(names))
|
||||||
|
}
|
||||||
|
}
|
||||||
s if s == CMD_SUMMARIZE_DEPRECATED => {
|
s if s == CMD_SUMMARIZE_DEPRECATED => {
|
||||||
println!("{}", console::style("⚠️ Note: /summarize has been renamed to /compact and will be removed in a future release.").yellow());
|
println!("{}", console::style("⚠️ Note: /summarize has been renamed to /compact and will be removed in a future release.").yellow());
|
||||||
Some(InputResult::Compact)
|
Some(InputResult::Compact)
|
||||||
@@ -417,6 +430,7 @@ fn print_help() {
|
|||||||
/compact - Compact the current conversation to reduce context length while preserving key information.
|
/compact - Compact the current conversation to reduce context length while preserving key information.
|
||||||
/edit [text] - Open your prompt editor to compose a message. Optionally pre-fill with text.
|
/edit [text] - Open your prompt editor to compose a message. Optionally pre-fill with text.
|
||||||
Uses $GOOSE_PROMPT_EDITOR, $VISUAL, or $EDITOR (in that order).
|
Uses $GOOSE_PROMPT_EDITOR, $VISUAL, or $EDITOR (in that order).
|
||||||
|
/skills - List available skills or enable skills by name (usage: /skills [<name>...])
|
||||||
/? or /help - Display this help message
|
/? or /help - Display this help message
|
||||||
/clear - Clears the current chat history
|
/clear - Clears the current chat history
|
||||||
|
|
||||||
@@ -747,4 +761,48 @@ mod tests {
|
|||||||
// Test /editfoo is not a valid command
|
// Test /editfoo is not a valid command
|
||||||
assert!(handle_slash_command("/editfoo").is_none());
|
assert!(handle_slash_command("/editfoo").is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_skill_command() {
|
||||||
|
// Test with a single skill name
|
||||||
|
let Some(InputResult::LoadSkills(names)) = handle_slash_command("/skills coding") else {
|
||||||
|
panic!(
|
||||||
|
"Expected LoadSkills, got {:?}",
|
||||||
|
handle_slash_command("/skills coding")
|
||||||
|
);
|
||||||
|
};
|
||||||
|
assert_eq!(names, vec!["coding"]);
|
||||||
|
|
||||||
|
// Test with multiple skill names
|
||||||
|
let Some(InputResult::LoadSkills(names)) = handle_slash_command("/skills coding insight")
|
||||||
|
else {
|
||||||
|
panic!(
|
||||||
|
"Expected LoadSkills, got {:?}",
|
||||||
|
handle_slash_command("/skills coding insight")
|
||||||
|
);
|
||||||
|
};
|
||||||
|
assert_eq!(names, vec!["coding", "insight"]);
|
||||||
|
|
||||||
|
// Test with extra whitespace
|
||||||
|
let Some(InputResult::LoadSkills(names)) = handle_slash_command("/skills my-skill ")
|
||||||
|
else {
|
||||||
|
panic!(
|
||||||
|
"Expected LoadSkills, got {:?}",
|
||||||
|
handle_slash_command("/skills my-skill ")
|
||||||
|
);
|
||||||
|
};
|
||||||
|
assert_eq!(names, vec!["my-skill"]);
|
||||||
|
|
||||||
|
// Test with no name: ListSkills
|
||||||
|
assert!(matches!(
|
||||||
|
handle_slash_command("/skills"),
|
||||||
|
Some(InputResult::ListSkills)
|
||||||
|
));
|
||||||
|
|
||||||
|
// Test with only whitespace after /skills: ListSkills
|
||||||
|
assert!(matches!(
|
||||||
|
handle_slash_command("/skills "),
|
||||||
|
Some(InputResult::ListSkills)
|
||||||
|
));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -666,6 +666,14 @@ impl CliSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
InputResult::LoadSkills(names) => {
|
||||||
|
history.save(editor);
|
||||||
|
self.handle_load_skills(&names).await?;
|
||||||
|
}
|
||||||
|
InputResult::ListSkills => {
|
||||||
|
history.save(editor);
|
||||||
|
self.handle_list_skills().await?;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
@@ -875,6 +883,55 @@ impl CliSession {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn handle_load_skills(&mut self, names: &[String]) -> Result<()> {
|
||||||
|
// NOTE: We don't validate the skill names here because the load_skill tool will
|
||||||
|
// handle that and provide feedback to the user if any skill names are invalid.
|
||||||
|
let message = format!(
|
||||||
|
"Use the load_skill tool to load the following skills: {}.",
|
||||||
|
names
|
||||||
|
.iter()
|
||||||
|
.map(|n| format!("\"{}\"", n))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(", ")
|
||||||
|
);
|
||||||
|
self.push_message(Message::user().with_text(&message));
|
||||||
|
output::show_thinking();
|
||||||
|
let result = self
|
||||||
|
.process_agent_response(true, CancellationToken::default())
|
||||||
|
.await;
|
||||||
|
output::hide_thinking();
|
||||||
|
result?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn handle_list_skills(&mut self) -> Result<()> {
|
||||||
|
use comfy_table::{presets, Cell, ContentArrangement, Table};
|
||||||
|
use goose::agents::platform_extensions::skills::list_installed_skills;
|
||||||
|
let cwd = std::env::current_dir().unwrap_or_default();
|
||||||
|
let skills = list_installed_skills(Some(&cwd));
|
||||||
|
|
||||||
|
if skills.is_empty() {
|
||||||
|
println!("{}", console::style("No skills available.").yellow());
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut table = Table::new();
|
||||||
|
table.set_content_arrangement(ContentArrangement::Dynamic);
|
||||||
|
table.load_preset(presets::ASCII_FULL);
|
||||||
|
table.set_header(vec!["Skill", "Description"]);
|
||||||
|
|
||||||
|
let mut sorted_skills = skills;
|
||||||
|
sorted_skills.sort_by(|a, b| a.name.cmp(&b.name));
|
||||||
|
|
||||||
|
for skill in &sorted_skills {
|
||||||
|
table.add_row(vec![Cell::new(&skill.name), Cell::new(&skill.description)]);
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("{table}");
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn handle_compact(&mut self) -> Result<()> {
|
async fn handle_compact(&mut self) -> Result<()> {
|
||||||
let prompt = "Are you sure you want to compact this conversation? This will condense the message history.";
|
let prompt = "Are you sure you want to compact this conversation? This will condense the message history.";
|
||||||
let should_summarize = match cliclack::confirm(prompt).initial_value(true).interact() {
|
let should_summarize = match cliclack::confirm(prompt).initial_value(true).interact() {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ When a session starts, goose adds any skills that it discovers to its instructio
|
|||||||
- "Follow the new-service skill to set up the auth service"
|
- "Follow the new-service skill to set up the auth service"
|
||||||
- "Apply the deployment skill"
|
- "Apply the deployment skill"
|
||||||
|
|
||||||
You can also ask goose what skills are available.
|
You can also ask goose what skills are available, or use the CLI `/skills` command to list available skills and load one or more by name (e.g. `/skills code-review edge-case-finder`).
|
||||||
|
|
||||||
:::info Claude Compatibility
|
:::info Claude Compatibility
|
||||||
goose skills are compatible with Claude Desktop and other [agents that support Agent Skills](https://agentskills.io/home#adoption).
|
goose skills are compatible with Claude Desktop and other [agents that support Agent Skills](https://agentskills.io/home#adoption).
|
||||||
|
|||||||
Reference in New Issue
Block a user