diff --git a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx index 718929d445b..0d509d56efd 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx @@ -345,8 +345,9 @@ export function Autocomplete(props: { const results: AutocompleteOption[] = [...command.slashes()] for (const serverCommand of sync.data.command) { + const suffix = serverCommand.mcp ? " (MCP)" : serverCommand.skill ? " (Skill)" : "" results.push({ - display: "/" + serverCommand.name + (serverCommand.mcp ? " (MCP)" : ""), + display: "/" + serverCommand.name + suffix, description: serverCommand.description, onSelect: () => { const newText = "/" + serverCommand.name + " " diff --git a/packages/opencode/src/command/index.ts b/packages/opencode/src/command/index.ts index 976f1cd51e9..10995f072d3 100644 --- a/packages/opencode/src/command/index.ts +++ b/packages/opencode/src/command/index.ts @@ -6,6 +6,7 @@ import { Identifier } from "../id/id" import PROMPT_INITIALIZE from "./template/initialize.txt" import PROMPT_REVIEW from "./template/review.txt" import { MCP } from "../mcp" +import { Skill } from "../skill/skill" export namespace Command { export const Event = { @@ -27,6 +28,7 @@ export namespace Command { agent: z.string().optional(), model: z.string().optional(), mcp: z.boolean().optional(), + skill: z.boolean().optional(), // workaround for zod not supporting async functions natively so we use getters // https://zod.dev/v4/changelog?id=zfunction template: z.promise(z.string()).or(z.string()), @@ -118,6 +120,18 @@ export namespace Command { } } + for (const skill of await Skill.all()) { + result[skill.name] = { + name: skill.name, + skill: true, + description: skill.description, + get template() { + return Bun.file(skill.location).text() + }, + hints: ["$ARGUMENTS"], + } + } + return result }) diff --git a/packages/opencode/src/session/prompt.ts b/packages/opencode/src/session/prompt.ts index 9325583acf7..fc9d09d74f2 100644 --- a/packages/opencode/src/session/prompt.ts +++ b/packages/opencode/src/session/prompt.ts @@ -1605,6 +1605,22 @@ NOTE: At any point in time through this workflow you should feel free to ask the export async function command(input: CommandInput) { log.info("command", input) const command = await Command.get(input.command) + + if (command.skill) { + const skillPrompt = input.arguments + ? `Use the skill tool to load the "${input.command}" skill, then follow its instructions to handle: ${input.arguments}` + : `Use the skill tool to load the "${input.command}" skill and follow its instructions.` + + return prompt({ + sessionID: input.sessionID, + messageID: input.messageID, + model: input.model ? Provider.parseModel(input.model) : await lastModel(input.sessionID), + agent: input.agent ?? (await Agent.defaultAgent()), + parts: [{ type: "text", text: skillPrompt }], + variant: input.variant, + }) + } + const agentName = command.agent ?? input.agent ?? (await Agent.defaultAgent()) const raw = input.arguments.match(argsRegex) ?? [] diff --git a/packages/opencode/src/tool/skill.ts b/packages/opencode/src/tool/skill.ts index 386abdae745..345196e80b2 100644 --- a/packages/opencode/src/tool/skill.ts +++ b/packages/opencode/src/tool/skill.ts @@ -45,7 +45,7 @@ export const SkillTool = Tool.define("skill", async (ctx) => { const skill = await Skill.get(params.name) if (!skill) { - const available = await Skill.all().then((x) => Object.keys(x).join(", ")) + const available = await Skill.all().then((x) => x.map((s) => s.name).join(", ")) throw new Error(`Skill "${params.name}" not found. Available skills: ${available || "none"}`) } diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 8442889020f..221f381ccbf 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -2077,6 +2077,7 @@ export type Command = { agent?: string model?: string mcp?: boolean + skill?: boolean template: string subtask?: boolean hints: Array