diff --git a/src/tools/skill/tools.test.ts b/src/tools/skill/tools.test.ts index 852560980..ee042274d 100644 --- a/src/tools/skill/tools.test.ts +++ b/src/tools/skill/tools.test.ts @@ -57,6 +57,45 @@ const mockContext = { abort: new AbortController().signal, } +describe("skill tool - synchronous description", () => { + it("includes available_skills immediately when skills are pre-provided", () => { + // #given + const loadedSkills = [createMockSkill("test-skill")] + + // #when + const tool = createSkillTool({ skills: loadedSkills }) + + // #then + expect(tool.description).toContain("") + expect(tool.description).toContain("test-skill") + }) + + it("includes all pre-provided skills in available_skills immediately", () => { + // #given + const loadedSkills = [ + createMockSkill("playwright"), + createMockSkill("frontend-ui-ux"), + createMockSkill("git-master"), + ] + + // #when + const tool = createSkillTool({ skills: loadedSkills }) + + // #then + expect(tool.description).toContain("playwright") + expect(tool.description).toContain("frontend-ui-ux") + expect(tool.description).toContain("git-master") + }) + + it("shows no-skills message immediately when empty skills are pre-provided", () => { + // #given / #when + const tool = createSkillTool({ skills: [] }) + + // #then + expect(tool.description).toContain("No skills are currently available") + }) +}) + describe("skill tool - agent restriction", () => { it("allows skill without agent restriction to any agent", async () => { // #given diff --git a/src/tools/skill/tools.ts b/src/tools/skill/tools.ts index 1c4db6999..3a2eca1f3 100644 --- a/src/tools/skill/tools.ts +++ b/src/tools/skill/tools.ts @@ -147,7 +147,14 @@ export function createSkillTool(options: SkillLoadOptions = {}): ToolDefinition return cachedDescription } - getDescription() + if (options.skills) { + const skillInfos = options.skills.map(loadedSkillToInfo) + cachedDescription = skillInfos.length === 0 + ? TOOL_DESCRIPTION_NO_SKILLS + : TOOL_DESCRIPTION_PREFIX + formatSkillsXml(skillInfos) + } else { + getDescription() + } return tool({ get description() { diff --git a/src/tools/slashcommand/tools.test.ts b/src/tools/slashcommand/tools.test.ts new file mode 100644 index 000000000..256a087da --- /dev/null +++ b/src/tools/slashcommand/tools.test.ts @@ -0,0 +1,76 @@ +import { describe, it, expect } from "bun:test" +import { createSlashcommandTool } from "./tools" +import type { CommandInfo } from "./types" +import type { LoadedSkill } from "../../features/opencode-skill-loader" + +function createMockCommand(name: string, description = ""): CommandInfo { + return { + name, + metadata: { + name, + description: description || `Test command ${name}`, + }, + scope: "builtin", + } +} + +function createMockSkill(name: string, description = ""): LoadedSkill { + return { + name, + path: `/test/skills/${name}/SKILL.md`, + resolvedPath: `/test/skills/${name}`, + definition: { + name, + description: description || `Test skill ${name}`, + template: "Test template", + }, + scope: "opencode-project", + } +} + +describe("slashcommand tool - synchronous description", () => { + it("includes available_skills immediately when commands and skills are pre-provided", () => { + // #given + const commands = [createMockCommand("commit", "Create a git commit")] + const skills = [createMockSkill("playwright", "Browser automation via Playwright MCP")] + + // #when + const tool = createSlashcommandTool({ commands, skills }) + + // #then + expect(tool.description).toContain("") + expect(tool.description).toContain("commit") + expect(tool.description).toContain("playwright") + }) + + it("includes all pre-provided commands and skills in description immediately", () => { + // #given + const commands = [ + createMockCommand("commit", "Git commit"), + createMockCommand("plan", "Create plan"), + ] + const skills = [ + createMockSkill("playwright", "Browser automation"), + createMockSkill("frontend-ui-ux", "Frontend design"), + createMockSkill("git-master", "Git operations"), + ] + + // #when + const tool = createSlashcommandTool({ commands, skills }) + + // #then + expect(tool.description).toContain("commit") + expect(tool.description).toContain("plan") + expect(tool.description).toContain("playwright") + expect(tool.description).toContain("frontend-ui-ux") + expect(tool.description).toContain("git-master") + }) + + it("shows prefix-only description when both commands and skills are empty", () => { + // #given / #when + const tool = createSlashcommandTool({ commands: [], skills: [] }) + + // #then - even with no items, description should be built synchronously (not just prefix) + expect(tool.description).toContain("Load a skill") + }) +}) diff --git a/src/tools/slashcommand/tools.ts b/src/tools/slashcommand/tools.ts index 6719fa6ac..b45695b7e 100644 --- a/src/tools/slashcommand/tools.ts +++ b/src/tools/slashcommand/tools.ts @@ -210,8 +210,12 @@ export function createSlashcommandTool(options: SlashcommandToolOptions = {}): T return cachedDescription } - // Pre-warm the cache immediately - buildDescription() + if (options.commands !== undefined && options.skills !== undefined) { + const allItems = [...options.commands, ...options.skills.map(skillToCommandInfo)] + cachedDescription = buildDescriptionFromItems(allItems) + } else { + buildDescription() + } return tool({ get description() {