feat: show installed skills in UI (#7910)

Signed-off-by: Vincenzo Palazzo <vincenzopalazzodev@gmail.com>
Signed-off-by: Douwe Osinga <douwe@squareup.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Douwe Osinga <douwe@squareup.com>
This commit is contained in:
Vincenzo Palazzo
2026-03-26 21:57:12 +01:00
committed by GitHub
parent f6fdbbdd7e
commit 9feb3f3bca
8 changed files with 547 additions and 18 deletions
@@ -33,6 +33,10 @@ static COMMANDS: &[CommandDef] = &[
name: "clear",
description: "Clear the conversation history",
},
CommandDef {
name: "skills",
description: "List installed skills and other available sources",
},
];
pub fn list_commands() -> &'static [CommandDef] {
@@ -72,6 +76,7 @@ impl Agent {
"prompt" => self.handle_prompt_command(&params, session_id).await,
"compact" => self.handle_compact_command(session_id).await,
"clear" => self.handle_clear_command(session_id).await,
"skills" => self.handle_skills_command(session_id).await,
_ => {
self.handle_recipe_command(command, params_str, session_id)
.await
@@ -129,6 +134,49 @@ impl Agent {
)))
}
async fn handle_skills_command(&self, session_id: &str) -> Result<Option<Message>> {
use super::platform_extensions::summon::{list_installed_sources, SourceKind};
let working_dir = self
.config
.session_manager
.get_session(session_id, false)
.await
.ok()
.map(|s| s.working_dir);
let sources = list_installed_sources(working_dir.as_deref());
let skills: Vec<_> = sources
.iter()
.filter(|s| matches!(s.kind, SourceKind::Skill | SourceKind::BuiltinSkill))
.collect();
let mut output = String::new();
if skills.is_empty() {
output.push_str("No skills installed.\n\n");
output.push_str("Skills are loaded from SKILL.md files in:\n");
output.push_str(" - ~/.claude/skills/\n");
output.push_str(" - ~/.config/agents/skills/\n");
output.push_str(" - .goose/skills/ (in current directory)\n");
output.push_str(" - .claude/skills/ (in current directory)\n");
output.push_str(" - .agents/skills/ (in current directory)\n");
} else {
output.push_str(&format!("**Installed skills ({}):**\n\n", skills.len()));
for skill in &skills {
let kind_label = if skill.kind == SourceKind::BuiltinSkill {
" *(builtin)*"
} else {
""
};
output.push_str(&format!(
"- **{}**{}: {}\n",
skill.name, kind_label, skill.description
));
}
}
Ok(Some(Message::assistant().with_text(output)))
}
async fn handle_prompts_command(
&self,
params: &[&str],
@@ -390,6 +390,16 @@ fn scan_agents_from_dir(
}
}
/// Returns all discovered sources (skills, recipes, agents) from the given working directory.
/// If no directory is provided, falls back to `std::env::current_dir()`.
/// This is useful for listing what's available without needing a full SummonClient instance.
pub fn list_installed_sources(working_dir: Option<&Path>) -> Vec<Source> {
let dir = working_dir
.map(|p| p.to_path_buf())
.unwrap_or_else(|| std::env::current_dir().unwrap_or_default());
discover_filesystem_sources(&dir)
}
fn discover_filesystem_sources(working_dir: &Path) -> Vec<Source> {
let mut sources: Vec<Source> = Vec::new();
let mut seen: std::collections::HashSet<String> = std::collections::HashSet::new();