From 85126556b868b713cf14c10f8b4a6d861e42326e Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Fri, 30 Jan 2026 18:07:14 -0500 Subject: [PATCH] feat: make skills invokable as slash commands in the TUI - Add Skill.content() method to load skill template content from SKILL.md files - Modify Command.list() to include skills as invokable commands - Add 'skill' boolean property to Command.Info schema - Update autocomplete to show skills with (Skill) label in slash commands - Regenerate SDK to include skill property in Command type --- .../cmd/tui/component/prompt/autocomplete.tsx | 3 ++- packages/opencode/src/command/index.ts | 17 +++++++++++++++++ packages/opencode/src/skill/skill.ts | 7 +++++++ packages/sdk/js/src/v2/gen/types.gen.ts | 1 + 4 files changed, 27 insertions(+), 1 deletion(-) 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 718929d445..53ff795397 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 label = serverCommand.mcp ? " (MCP)" : serverCommand.skill ? " (Skill)" : "" results.push({ - display: "/" + serverCommand.name + (serverCommand.mcp ? " (MCP)" : ""), + display: "/" + serverCommand.name + label, 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 976f1cd51e..96276800cb 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" 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,21 @@ export namespace Command { } } + // Add skills as invokable commands + for (const skill of await Skill.all()) { + // Skip if a command with this name already exists + if (result[skill.name]) continue + result[skill.name] = { + name: skill.name, + description: skill.description, + skill: true, + get template() { + return Skill.content(skill.name).then((content) => content ?? "") + }, + hints: [], + } + } + return result }) diff --git a/packages/opencode/src/skill/skill.ts b/packages/opencode/src/skill/skill.ts index 5b300a9287..d5da85cfbe 100644 --- a/packages/opencode/src/skill/skill.ts +++ b/packages/opencode/src/skill/skill.ts @@ -153,4 +153,11 @@ export namespace Skill { export async function all() { return state().then((x) => Object.values(x)) } + + export async function content(name: string) { + const info = await get(name) + if (!info) return undefined + const md = await ConfigMarkdown.parse(info.location) + return md.content + } } diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index a8c61c4daa..ad8b18dfa0 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -2117,6 +2117,7 @@ export type Command = { agent?: string model?: string mcp?: boolean + skill?: boolean template: string subtask?: boolean hints: Array