From bf5168d14c61a7260bf8fd085f94e87cd069814e Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Fri, 30 Jan 2026 20:45:30 -0500 Subject: [PATCH 1/3] feat: make skills invokable as slash commands in the TUI --- .../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 718929d445b..53ff795397b 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 976f1cd51e9..96276800cbe 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 5b300a9287b..d5da85cfbe5 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 a8c61c4daae..ad8b18dfa03 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 From 0142445c5a09ed7bac3289024a411458b3244272 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Fri, 30 Jan 2026 23:55:24 -0500 Subject: [PATCH 2/3] tui: show command source prefix in autocomplete for MCP and Skill commands --- packages/opencode/script/build.ts | 1 + .../src/cli/cmd/tui/component/prompt/autocomplete.tsx | 2 +- packages/opencode/src/command/index.ts | 9 ++++----- packages/opencode/src/skill/skill.ts | 9 ++------- packages/opencode/src/tool/skill.ts | 3 +-- packages/sdk/js/src/v2/gen/types.gen.ts | 4 ++-- 6 files changed, 11 insertions(+), 17 deletions(-) diff --git a/packages/opencode/script/build.ts b/packages/opencode/script/build.ts index f0b3fa828a7..6d781e88ba4 100755 --- a/packages/opencode/script/build.ts +++ b/packages/opencode/script/build.ts @@ -142,6 +142,7 @@ for (const item of targets) { tsconfig: "./tsconfig.json", plugins: [solidPlugin], sourcemap: "external", + bytecode: true, compile: { autoloadBunfig: false, autoloadDotenv: false, 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 53ff795397b..bd000e2ab07 100644 --- a/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx +++ b/packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx @@ -345,7 +345,7 @@ export function Autocomplete(props: { const results: AutocompleteOption[] = [...command.slashes()] for (const serverCommand of sync.data.command) { - const label = serverCommand.mcp ? " (MCP)" : serverCommand.skill ? " (Skill)" : "" + const label = serverCommand.source === "mcp" ? ":mcp" : serverCommand.source === "skill" ? ":skill" : "" results.push({ display: "/" + serverCommand.name + label, description: serverCommand.description, diff --git a/packages/opencode/src/command/index.ts b/packages/opencode/src/command/index.ts index 96276800cbe..14dbeb6794e 100644 --- a/packages/opencode/src/command/index.ts +++ b/packages/opencode/src/command/index.ts @@ -27,8 +27,7 @@ export namespace Command { description: z.string().optional(), agent: z.string().optional(), model: z.string().optional(), - mcp: z.boolean().optional(), - skill: z.boolean().optional(), + source: z.enum(["command", "mcp", "skill"]).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()), @@ -96,7 +95,7 @@ export namespace Command { for (const [name, prompt] of Object.entries(await MCP.prompts())) { result[name] = { name, - mcp: true, + source: "mcp", description: prompt.description, get template() { // since a getter can't be async we need to manually return a promise here @@ -127,9 +126,9 @@ export namespace Command { result[skill.name] = { name: skill.name, description: skill.description, - skill: true, + source: "skill", get template() { - return Skill.content(skill.name).then((content) => content ?? "") + return skill.content }, hints: [], } diff --git a/packages/opencode/src/skill/skill.ts b/packages/opencode/src/skill/skill.ts index d5da85cfbe5..6e05d013ae5 100644 --- a/packages/opencode/src/skill/skill.ts +++ b/packages/opencode/src/skill/skill.ts @@ -18,6 +18,7 @@ export namespace Skill { name: z.string(), description: z.string(), location: z.string(), + content: z.string(), }) export type Info = z.infer @@ -74,6 +75,7 @@ export namespace Skill { name: parsed.data.name, description: parsed.data.description, location: match, + content: md.content, } } @@ -153,11 +155,4 @@ 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/opencode/src/tool/skill.ts b/packages/opencode/src/tool/skill.ts index 76d9fd535e7..8f285d5999a 100644 --- a/packages/opencode/src/tool/skill.ts +++ b/packages/opencode/src/tool/skill.ts @@ -2,7 +2,6 @@ import path from "path" import z from "zod" import { Tool } from "./tool" import { Skill } from "../skill" -import { ConfigMarkdown } from "../config/markdown" import { PermissionNext } from "../permission/next" export const SkillTool = Tool.define("skill", async (ctx) => { @@ -62,7 +61,7 @@ export const SkillTool = Tool.define("skill", async (ctx) => { always: [params.name], metadata: {}, }) - const content = (await ConfigMarkdown.parse(skill.location)).content + const content = skill.content const dir = path.dirname(skill.location) // Format output similar to plugin pattern diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index ad8b18dfa03..cb2f586775a 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -2116,8 +2116,7 @@ export type Command = { description?: string agent?: string model?: string - mcp?: boolean - skill?: boolean + source?: "command" | "mcp" | "skill" template: string subtask?: boolean hints: Array @@ -4914,6 +4913,7 @@ export type AppSkillsResponses = { name: string description: string location: string + content: string }> } From 2487ddd137dcd0484f495d94c12728bc2eb9f6e3 Mon Sep 17 00:00:00 2001 From: Dax Raad Date: Sat, 31 Jan 2026 00:41:29 -0500 Subject: [PATCH 3/3] fix --- packages/opencode/script/build.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/opencode/script/build.ts b/packages/opencode/script/build.ts index 6d781e88ba4..f0b3fa828a7 100755 --- a/packages/opencode/script/build.ts +++ b/packages/opencode/script/build.ts @@ -142,7 +142,6 @@ for (const item of targets) { tsconfig: "./tsconfig.json", plugins: [solidPlugin], sourcemap: "external", - bytecode: true, compile: { autoloadBunfig: false, autoloadDotenv: false,