feat(skill): render skills as slash commands in available items list

Skills now appear as <command> items with / prefix (e.g., /review-work)
instead of <skill> items, making them discoverable alongside regular
slash commands in the skill tool description.
This commit is contained in:
YeonGyu-Kim
2026-03-12 18:53:44 +09:00
parent 50638cf783
commit a400adae97
2 changed files with 24 additions and 20 deletions
+18 -15
View File
@@ -384,7 +384,7 @@ describe("skill tool - ordering and priority", () => {
} }
} }
it("lists skills before commands in available_items", () => { it("shows skills as command items with slash prefix in available_items", () => {
//#given: mix of skills and commands //#given: mix of skills and commands
const skills = [ const skills = [
createMockSkillWithScope("builtin-skill", "builtin"), createMockSkillWithScope("builtin-skill", "builtin"),
@@ -398,16 +398,17 @@ describe("skill tool - ordering and priority", () => {
//#when: creating tool with both //#when: creating tool with both
const tool = createSkillTool({ skills, commands }) const tool = createSkillTool({ skills, commands })
//#then: skills should appear before commands //#then: skills should appear as <command> items with / prefix, listed before regular commands
const desc = tool.description const desc = tool.description
const skillIndex = desc.indexOf("<skill>") expect(desc).toContain("<name>/builtin-skill</name>")
const commandIndex = desc.indexOf("<command>") expect(desc).toContain("<name>/project-skill</name>")
expect(skillIndex).toBeGreaterThan(0) expect(desc).not.toContain("<skill>")
expect(commandIndex).toBeGreaterThan(0) const skillCmdIndex = desc.indexOf("/project-skill")
expect(skillIndex).toBeLessThan(commandIndex) const regularCmdIndex = desc.indexOf("/project-cmd")
expect(skillCmdIndex).toBeLessThan(regularCmdIndex)
}) })
it("sorts skills by priority: project > user > opencode > builtin", () => { it("sorts skill-commands by priority: project > user > opencode > builtin", () => {
//#given: skills in random order //#given: skills in random order
const skills = [ const skills = [
createMockSkillWithScope("builtin-skill", "builtin"), createMockSkillWithScope("builtin-skill", "builtin"),
@@ -421,10 +422,10 @@ describe("skill tool - ordering and priority", () => {
//#then: should be sorted by priority //#then: should be sorted by priority
const desc = tool.description const desc = tool.description
const projectIndex = desc.indexOf("project-skill") const projectIndex = desc.indexOf("/project-skill")
const userIndex = desc.indexOf("user-skill") const userIndex = desc.indexOf("/user-skill")
const opencodeIndex = desc.indexOf("opencode-skill") const opencodeIndex = desc.indexOf("/opencode-skill")
const builtinIndex = desc.indexOf("builtin-skill") const builtinIndex = desc.indexOf("/builtin-skill")
expect(projectIndex).toBeLessThan(userIndex) expect(projectIndex).toBeLessThan(userIndex)
expect(userIndex).toBeLessThan(opencodeIndex) expect(userIndex).toBeLessThan(opencodeIndex)
@@ -468,7 +469,7 @@ describe("skill tool - ordering and priority", () => {
expect(tool.description).toContain("Skills listed before commands") expect(tool.description).toContain("Skills listed before commands")
}) })
it("uses <available_items> wrapper with unified format", () => { it("uses <available_items> wrapper with unified command format", () => {
//#given: mix of skills and commands //#given: mix of skills and commands
const skills = [createMockSkillWithScope("test-skill", "project")] const skills = [createMockSkillWithScope("test-skill", "project")]
const commands = [createMockCommand("test-cmd", "project")] const commands = [createMockCommand("test-cmd", "project")]
@@ -476,10 +477,12 @@ describe("skill tool - ordering and priority", () => {
//#when: creating tool //#when: creating tool
const tool = createSkillTool({ skills, commands }) const tool = createSkillTool({ skills, commands })
//#then: should use unified wrapper //#then: should use unified wrapper with all items as commands
expect(tool.description).toContain("<available_items>") expect(tool.description).toContain("<available_items>")
expect(tool.description).toContain("</available_items>") expect(tool.description).toContain("</available_items>")
expect(tool.description).toContain("<skill>") expect(tool.description).not.toContain("<skill>")
expect(tool.description).toContain("<command>") expect(tool.description).toContain("<command>")
expect(tool.description).toContain("/test-skill")
expect(tool.description).toContain("/test-cmd")
}) })
}) })
+6 -5
View File
@@ -45,23 +45,24 @@ function formatCombinedDescription(skills: SkillInfo[], commands: CommandInfo[])
const allItems: string[] = [] const allItems: string[] = []
// Sort and add skills first (skills before commands) // Skills rendered as command items (skills are also slash-invocable)
if (skills.length > 0) { if (skills.length > 0) {
const sortedSkills = [...skills].sort((a, b) => { const sortedSkills = [...skills].sort((a, b) => {
const priorityA = scopePriority[a.scope] || 0 const priorityA = scopePriority[a.scope] || 0
const priorityB = scopePriority[b.scope] || 0 const priorityB = scopePriority[b.scope] || 0
return priorityB - priorityA // Higher priority first return priorityB - priorityA
}) })
sortedSkills.forEach(skill => { sortedSkills.forEach(skill => {
const parts = [ const parts = [
" <skill>", " <command>",
` <name>${skill.name}</name>`, ` <name>/${skill.name}</name>`,
` <description>${skill.description}</description>`, ` <description>${skill.description}</description>`,
` <scope>${skill.scope}</scope>`,
] ]
if (skill.compatibility) { if (skill.compatibility) {
parts.push(` <compatibility>${skill.compatibility}</compatibility>`) parts.push(` <compatibility>${skill.compatibility}</compatibility>`)
} }
parts.push(" </skill>") parts.push(" </command>")
allItems.push(parts.join("\n")) allItems.push(parts.join("\n"))
}) })
} }