feat: make skills invokable as slash commands in the TUI (#11390)

This commit is contained in:
Dax
2026-01-31 00:41:55 -05:00
committed by GitHub
parent c0e71c4261
commit 81ac41e089
5 changed files with 25 additions and 6 deletions

View File

@@ -345,8 +345,9 @@ export function Autocomplete(props: {
const results: AutocompleteOption[] = [...command.slashes()] const results: AutocompleteOption[] = [...command.slashes()]
for (const serverCommand of sync.data.command) { for (const serverCommand of sync.data.command) {
const label = serverCommand.source === "mcp" ? ":mcp" : serverCommand.source === "skill" ? ":skill" : ""
results.push({ results.push({
display: "/" + serverCommand.name + (serverCommand.mcp ? " (MCP)" : ""), display: "/" + serverCommand.name + label,
description: serverCommand.description, description: serverCommand.description,
onSelect: () => { onSelect: () => {
const newText = "/" + serverCommand.name + " " const newText = "/" + serverCommand.name + " "

View File

@@ -6,6 +6,7 @@ import { Identifier } from "../id/id"
import PROMPT_INITIALIZE from "./template/initialize.txt" import PROMPT_INITIALIZE from "./template/initialize.txt"
import PROMPT_REVIEW from "./template/review.txt" import PROMPT_REVIEW from "./template/review.txt"
import { MCP } from "../mcp" import { MCP } from "../mcp"
import { Skill } from "../skill"
export namespace Command { export namespace Command {
export const Event = { export const Event = {
@@ -26,7 +27,7 @@ export namespace Command {
description: z.string().optional(), description: z.string().optional(),
agent: z.string().optional(), agent: z.string().optional(),
model: z.string().optional(), model: z.string().optional(),
mcp: z.boolean().optional(), source: z.enum(["command", "mcp", "skill"]).optional(),
// workaround for zod not supporting async functions natively so we use getters // workaround for zod not supporting async functions natively so we use getters
// https://zod.dev/v4/changelog?id=zfunction // https://zod.dev/v4/changelog?id=zfunction
template: z.promise(z.string()).or(z.string()), template: z.promise(z.string()).or(z.string()),
@@ -94,7 +95,7 @@ export namespace Command {
for (const [name, prompt] of Object.entries(await MCP.prompts())) { for (const [name, prompt] of Object.entries(await MCP.prompts())) {
result[name] = { result[name] = {
name, name,
mcp: true, source: "mcp",
description: prompt.description, description: prompt.description,
get template() { get template() {
// since a getter can't be async we need to manually return a promise here // since a getter can't be async we need to manually return a promise here
@@ -118,6 +119,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,
source: "skill",
get template() {
return skill.content
},
hints: [],
}
}
return result return result
}) })

View File

@@ -18,6 +18,7 @@ export namespace Skill {
name: z.string(), name: z.string(),
description: z.string(), description: z.string(),
location: z.string(), location: z.string(),
content: z.string(),
}) })
export type Info = z.infer<typeof Info> export type Info = z.infer<typeof Info>
@@ -74,6 +75,7 @@ export namespace Skill {
name: parsed.data.name, name: parsed.data.name,
description: parsed.data.description, description: parsed.data.description,
location: match, location: match,
content: md.content,
} }
} }

View File

@@ -2,7 +2,6 @@ import path from "path"
import z from "zod" import z from "zod"
import { Tool } from "./tool" import { Tool } from "./tool"
import { Skill } from "../skill" import { Skill } from "../skill"
import { ConfigMarkdown } from "../config/markdown"
import { PermissionNext } from "../permission/next" import { PermissionNext } from "../permission/next"
export const SkillTool = Tool.define("skill", async (ctx) => { export const SkillTool = Tool.define("skill", async (ctx) => {
@@ -62,7 +61,7 @@ export const SkillTool = Tool.define("skill", async (ctx) => {
always: [params.name], always: [params.name],
metadata: {}, metadata: {},
}) })
const content = (await ConfigMarkdown.parse(skill.location)).content const content = skill.content
const dir = path.dirname(skill.location) const dir = path.dirname(skill.location)
// Format output similar to plugin pattern // Format output similar to plugin pattern

View File

@@ -2116,7 +2116,7 @@ export type Command = {
description?: string description?: string
agent?: string agent?: string
model?: string model?: string
mcp?: boolean source?: "command" | "mcp" | "skill"
template: string template: string
subtask?: boolean subtask?: boolean
hints: Array<string> hints: Array<string>
@@ -4913,6 +4913,7 @@ export type AppSkillsResponses = {
name: string name: string
description: string description: string
location: string location: string
content: string
}> }>
} }