mirror of
https://github.com/block/goose.git
synced 2026-04-28 03:29:36 +00:00
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:
parent
501dde5570
commit
bdf5c43f3d
4 changed files with 146 additions and 1 deletions
|
|
@ -121,6 +121,31 @@ impl GooseCompleter {
|
|||
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
|
||||
fn complete_slash_commands(&self, line: &str) -> Result<(usize, Vec<Pair>)> {
|
||||
// Define available slash commands
|
||||
|
|
@ -136,6 +161,7 @@ impl GooseCompleter {
|
|||
"/prompt",
|
||||
"/mode",
|
||||
"/recipe",
|
||||
"/skills",
|
||||
];
|
||||
|
||||
// Find commands that match the prefix
|
||||
|
|
@ -374,6 +400,10 @@ impl Completer for GooseCompleter {
|
|||
return self.complete_mode_flags(line);
|
||||
}
|
||||
|
||||
if line.starts_with("/skills ") {
|
||||
return self.complete_skill_names(line);
|
||||
}
|
||||
|
||||
return Ok((pos, vec![]));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -27,6 +27,8 @@ pub enum InputResult {
|
|||
Compact,
|
||||
ToggleFullToolOutput,
|
||||
Edit(Option<String>),
|
||||
ListSkills,
|
||||
LoadSkills(Vec<String>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
|
@ -204,6 +206,7 @@ fn handle_slash_command(input: &str) -> Option<InputResult> {
|
|||
const CMD_SUMMARIZE_DEPRECATED: &str = "/summarize";
|
||||
const CMD_EDIT: &str = "/edit";
|
||||
const CMD_EDIT_WITH_SPACE: &str = "/edit ";
|
||||
const CMD_SKILLS: &str = "/skills";
|
||||
|
||||
match input {
|
||||
"/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.starts_with(CMD_RECIPE) => parse_recipe_command(s),
|
||||
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 => {
|
||||
println!("{}", console::style("⚠️ Note: /summarize has been renamed to /compact and will be removed in a future release.").yellow());
|
||||
Some(InputResult::Compact)
|
||||
|
|
@ -417,6 +430,7 @@ fn print_help() {
|
|||
/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.
|
||||
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
|
||||
/clear - Clears the current chat history
|
||||
|
||||
|
|
@ -747,4 +761,48 @@ mod tests {
|
|||
// Test /editfoo is not a valid command
|
||||
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(())
|
||||
}
|
||||
|
|
@ -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<()> {
|
||||
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() {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
- "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
|
||||
goose skills are compatible with Claude Desktop and other [agents that support Agent Skills](https://agentskills.io/home#adoption).
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue