Просмотр исходного кода

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

Dax 3 недель назад
Родитель
Сommit
81ac41e089

+ 2 - 1
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.source === "mcp" ? ":mcp" : serverCommand.source === "skill" ? ":skill" : ""
       results.push({
-        display: "/" + serverCommand.name + (serverCommand.mcp ? " (MCP)" : ""),
+        display: "/" + serverCommand.name + label,
         description: serverCommand.description,
         onSelect: () => {
           const newText = "/" + serverCommand.name + " "

+ 18 - 2
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 = {
@@ -26,7 +27,7 @@ export namespace Command {
       description: z.string().optional(),
       agent: 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
       // https://zod.dev/v4/changelog?id=zfunction
       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())) {
       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
@@ -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
   })
 

+ 2 - 0
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<typeof Info>
 
@@ -74,6 +75,7 @@ export namespace Skill {
         name: parsed.data.name,
         description: parsed.data.description,
         location: match,
+        content: md.content,
       }
     }
 

+ 1 - 2
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

+ 2 - 1
packages/sdk/js/src/v2/gen/types.gen.ts

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