Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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 + " "
Expand Down
20 changes: 18 additions & 2 deletions packages/opencode/src/command/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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()),
Expand Down Expand Up @@ -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
Expand All @@ -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: [],
Copy link

Copilot AI Jan 31, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Skills are added with an empty hints array, but hints should be computed from the skill template content to support argument placeholders like $1, $2, or $ARGUMENTS. Other command types (default commands at lines 70, 79, config commands at line 93, and MCP prompts at line 119) all compute hints from their templates using the hints() function. Without proper hints, skill templates that use argument placeholders won't provide autocomplete suggestions for those arguments in the TUI. Consider computing hints after loading the skill content, or making hints a getter that computes them lazily from the template.

Copilot uses AI. Check for mistakes.
}
}

return result
})

Expand Down
2 changes: 2 additions & 0 deletions packages/opencode/src/skill/skill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>

Expand Down Expand Up @@ -74,6 +75,7 @@ export namespace Skill {
name: parsed.data.name,
description: parsed.data.description,
location: match,
content: md.content,
}
}

Expand Down
3 changes: 1 addition & 2 deletions packages/opencode/src/tool/skill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) => {
Expand Down Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion packages/sdk/js/src/v2/gen/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -4913,6 +4913,7 @@ export type AppSkillsResponses = {
name: string
description: string
location: string
content: string
}>
}

Expand Down
Loading