Skip to content

Commit 786ae0a

Browse files
authored
feat(app): add skill slash commands (#11369)
1 parent f73f88f commit 786ae0a

File tree

5 files changed

+59
-2
lines changed

5 files changed

+59
-2
lines changed

packages/app/src/components/prompt-input.tsx

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ interface SlashCommand {
111111
title: string
112112
description?: string
113113
keybind?: string
114-
type: "builtin" | "custom"
114+
type: "builtin" | "custom" | "skill"
115115
}
116116

117117
export const PromptInput: Component<PromptInputProps> = (props) => {
@@ -519,7 +519,15 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
519519
type: "custom" as const,
520520
}))
521521

522-
return [...custom, ...builtin]
522+
const skills = sync.data.skill.map((skill) => ({
523+
id: `skill.${skill.name}`,
524+
trigger: `skill:${skill.name}`,
525+
title: skill.name,
526+
description: skill.description,
527+
type: "skill" as const,
528+
}))
529+
530+
return [...skills, ...custom, ...builtin]
523531
})
524532

525533
const handleSlashSelect = (cmd: SlashCommand | undefined) => {
@@ -543,6 +551,25 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
543551
return
544552
}
545553

554+
if (cmd.type === "skill") {
555+
// Extract skill name from the id (skill.{name})
556+
const skillName = cmd.id.replace("skill.", "")
557+
const text = `Load the "${skillName}" skill and follow its instructions.`
558+
editorRef.innerHTML = ""
559+
editorRef.textContent = text
560+
prompt.set([{ type: "text", content: text, start: 0, end: text.length }], text.length)
561+
requestAnimationFrame(() => {
562+
editorRef.focus()
563+
const range = document.createRange()
564+
const sel = window.getSelection()
565+
range.selectNodeContents(editorRef)
566+
range.collapse(false)
567+
sel?.removeAllRanges()
568+
sel?.addRange(range)
569+
})
570+
return
571+
}
572+
546573
editorRef.innerHTML = ""
547574
prompt.set([{ type: "text", content: "", start: 0, end: 0 }], 0)
548575
command.trigger(cmd.id, "slash")
@@ -1706,6 +1733,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
17061733
{language.t("prompt.slash.badge.custom")}
17071734
</span>
17081735
</Show>
1736+
<Show when={cmd.type === "skill"}>
1737+
<span class="text-11-regular text-text-subtle px-1.5 py-0.5 bg-surface-base rounded">
1738+
{language.t("prompt.slash.badge.skill")}
1739+
</span>
1740+
</Show>
17091741
<Show when={command.keybind(cmd.id)}>
17101742
<span class="text-12-regular text-text-subtle">{command.keybind(cmd.id)}</span>
17111743
</Show>

packages/app/src/context/global-sync.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import {
1717
type VcsInfo,
1818
type PermissionRequest,
1919
type QuestionRequest,
20+
type AppSkillsResponse,
2021
createOpencodeClient,
2122
} from "@opencode-ai/sdk/v2/client"
2223
import { createStore, produce, reconcile, type SetStoreFunction, type Store } from "solid-js/store"
@@ -56,10 +57,13 @@ type ProjectMeta = {
5657
}
5758
}
5859

60+
export type Skill = AppSkillsResponse[number]
61+
5962
type State = {
6063
status: "loading" | "partial" | "complete"
6164
agent: Agent[]
6265
command: Command[]
66+
skill: Skill[]
6367
project: string
6468
projectMeta: ProjectMeta | undefined
6569
icon: string | undefined
@@ -388,6 +392,7 @@ function createGlobalSync() {
388392
status: "loading" as const,
389393
agent: [],
390394
command: [],
395+
skill: [],
391396
session: [],
392397
sessionTotal: 0,
393398
session_status: {},
@@ -528,6 +533,7 @@ function createGlobalSync() {
528533
Promise.all([
529534
sdk.path.get().then((x) => setStore("path", x.data!)),
530535
sdk.command.list().then((x) => setStore("command", x.data ?? [])),
536+
sdk.app.skills().then((x) => setStore("skill", x.data ?? [])),
531537
sdk.session.status().then((x) => setStore("session_status", x.data!)),
532538
loadSessions(directory),
533539
sdk.mcp.status().then((x) => setStore("mcp", x.data!)),

packages/app/src/i18n/en.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,7 @@ export const dict = {
216216
"prompt.popover.emptyCommands": "No matching commands",
217217
"prompt.dropzone.label": "Drop images or PDFs here",
218218
"prompt.slash.badge.custom": "custom",
219+
"prompt.slash.badge.skill": "skill",
219220
"prompt.context.active": "active",
220221
"prompt.context.includeActiveFile": "Include active file",
221222
"prompt.context.removeActiveFile": "Remove active file from context",

packages/opencode/src/cli/cmd/tui/component/prompt/autocomplete.tsx

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,20 @@ export function Autocomplete(props: {
359359
})
360360
}
361361

362+
for (const skill of sync.data.skill) {
363+
results.push({
364+
display: "/skill:" + skill.name,
365+
description: skill.description,
366+
onSelect: () => {
367+
const newText = `Load the "${skill.name}" skill and follow its instructions.`
368+
const cursor = props.input().logicalCursor
369+
props.input().deleteRange(0, 0, cursor.row, cursor.col)
370+
props.input().insertText(newText)
371+
props.input().cursorOffset = Bun.stringWidth(newText)
372+
},
373+
})
374+
}
375+
362376
results.sort((a, b) => a.display.localeCompare(b.display))
363377

364378
const max = firstBy(results, [(x) => x.display.length, "desc"])?.display.length

packages/opencode/src/cli/cmd/tui/context/sync.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import type {
1717
ProviderListResponse,
1818
ProviderAuthMethod,
1919
VcsInfo,
20+
AppSkillsResponse,
2021
} from "@opencode-ai/sdk/v2"
2122
import { createStore, produce, reconcile } from "solid-js/store"
2223
import { useSDK } from "@tui/context/sdk"
@@ -40,6 +41,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
4041
provider_auth: Record<string, ProviderAuthMethod[]>
4142
agent: Agent[]
4243
command: Command[]
44+
skill: AppSkillsResponse
4345
permission: {
4446
[sessionID: string]: PermissionRequest[]
4547
}
@@ -86,6 +88,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
8688
permission: {},
8789
question: {},
8890
command: [],
91+
skill: [],
8992
provider: [],
9093
provider_default: {},
9194
session: [],
@@ -385,6 +388,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
385388
Promise.all([
386389
...(args.continue ? [] : [sessionListPromise.then((sessions) => setStore("session", reconcile(sessions)))]),
387390
sdk.client.command.list().then((x) => setStore("command", reconcile(x.data ?? []))),
391+
sdk.client.app.skills().then((x) => setStore("skill", reconcile(x.data ?? []))),
388392
sdk.client.lsp.status().then((x) => setStore("lsp", reconcile(x.data!))),
389393
sdk.client.mcp.status().then((x) => setStore("mcp", reconcile(x.data!))),
390394
sdk.client.experimental.resource.list().then((x) => setStore("mcp_resource", reconcile(x.data ?? {}))),

0 commit comments

Comments
 (0)