feat(app): add skill slash commands (#11369)
This commit is contained in:
@@ -111,7 +111,7 @@ interface SlashCommand {
|
|||||||
title: string
|
title: string
|
||||||
description?: string
|
description?: string
|
||||||
keybind?: string
|
keybind?: string
|
||||||
type: "builtin" | "custom"
|
type: "builtin" | "custom" | "skill"
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PromptInput: Component<PromptInputProps> = (props) => {
|
export const PromptInput: Component<PromptInputProps> = (props) => {
|
||||||
@@ -519,7 +519,15 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
type: "custom" as const,
|
type: "custom" as const,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
return [...custom, ...builtin]
|
const skills = sync.data.skill.map((skill) => ({
|
||||||
|
id: `skill.${skill.name}`,
|
||||||
|
trigger: `skill:${skill.name}`,
|
||||||
|
title: skill.name,
|
||||||
|
description: skill.description,
|
||||||
|
type: "skill" as const,
|
||||||
|
}))
|
||||||
|
|
||||||
|
return [...skills, ...custom, ...builtin]
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleSlashSelect = (cmd: SlashCommand | undefined) => {
|
const handleSlashSelect = (cmd: SlashCommand | undefined) => {
|
||||||
@@ -543,6 +551,25 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (cmd.type === "skill") {
|
||||||
|
// Extract skill name from the id (skill.{name})
|
||||||
|
const skillName = cmd.id.replace("skill.", "")
|
||||||
|
const text = `Load the "${skillName}" skill and follow its instructions.`
|
||||||
|
editorRef.innerHTML = ""
|
||||||
|
editorRef.textContent = text
|
||||||
|
prompt.set([{ type: "text", content: text, start: 0, end: text.length }], text.length)
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
editorRef.focus()
|
||||||
|
const range = document.createRange()
|
||||||
|
const sel = window.getSelection()
|
||||||
|
range.selectNodeContents(editorRef)
|
||||||
|
range.collapse(false)
|
||||||
|
sel?.removeAllRanges()
|
||||||
|
sel?.addRange(range)
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
editorRef.innerHTML = ""
|
editorRef.innerHTML = ""
|
||||||
prompt.set([{ type: "text", content: "", start: 0, end: 0 }], 0)
|
prompt.set([{ type: "text", content: "", start: 0, end: 0 }], 0)
|
||||||
command.trigger(cmd.id, "slash")
|
command.trigger(cmd.id, "slash")
|
||||||
@@ -1706,6 +1733,11 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
|
|||||||
{language.t("prompt.slash.badge.custom")}
|
{language.t("prompt.slash.badge.custom")}
|
||||||
</span>
|
</span>
|
||||||
</Show>
|
</Show>
|
||||||
|
<Show when={cmd.type === "skill"}>
|
||||||
|
<span class="text-11-regular text-text-subtle px-1.5 py-0.5 bg-surface-base rounded">
|
||||||
|
{language.t("prompt.slash.badge.skill")}
|
||||||
|
</span>
|
||||||
|
</Show>
|
||||||
<Show when={command.keybind(cmd.id)}>
|
<Show when={command.keybind(cmd.id)}>
|
||||||
<span class="text-12-regular text-text-subtle">{command.keybind(cmd.id)}</span>
|
<span class="text-12-regular text-text-subtle">{command.keybind(cmd.id)}</span>
|
||||||
</Show>
|
</Show>
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import {
|
|||||||
type VcsInfo,
|
type VcsInfo,
|
||||||
type PermissionRequest,
|
type PermissionRequest,
|
||||||
type QuestionRequest,
|
type QuestionRequest,
|
||||||
|
type AppSkillsResponse,
|
||||||
createOpencodeClient,
|
createOpencodeClient,
|
||||||
} from "@opencode-ai/sdk/v2/client"
|
} from "@opencode-ai/sdk/v2/client"
|
||||||
import { createStore, produce, reconcile, type SetStoreFunction, type Store } from "solid-js/store"
|
import { createStore, produce, reconcile, type SetStoreFunction, type Store } from "solid-js/store"
|
||||||
@@ -56,10 +57,13 @@ type ProjectMeta = {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Skill = AppSkillsResponse[number]
|
||||||
|
|
||||||
type State = {
|
type State = {
|
||||||
status: "loading" | "partial" | "complete"
|
status: "loading" | "partial" | "complete"
|
||||||
agent: Agent[]
|
agent: Agent[]
|
||||||
command: Command[]
|
command: Command[]
|
||||||
|
skill: Skill[]
|
||||||
project: string
|
project: string
|
||||||
projectMeta: ProjectMeta | undefined
|
projectMeta: ProjectMeta | undefined
|
||||||
icon: string | undefined
|
icon: string | undefined
|
||||||
@@ -388,6 +392,7 @@ function createGlobalSync() {
|
|||||||
status: "loading" as const,
|
status: "loading" as const,
|
||||||
agent: [],
|
agent: [],
|
||||||
command: [],
|
command: [],
|
||||||
|
skill: [],
|
||||||
session: [],
|
session: [],
|
||||||
sessionTotal: 0,
|
sessionTotal: 0,
|
||||||
session_status: {},
|
session_status: {},
|
||||||
@@ -528,6 +533,7 @@ function createGlobalSync() {
|
|||||||
Promise.all([
|
Promise.all([
|
||||||
sdk.path.get().then((x) => setStore("path", x.data!)),
|
sdk.path.get().then((x) => setStore("path", x.data!)),
|
||||||
sdk.command.list().then((x) => setStore("command", x.data ?? [])),
|
sdk.command.list().then((x) => setStore("command", x.data ?? [])),
|
||||||
|
sdk.app.skills().then((x) => setStore("skill", x.data ?? [])),
|
||||||
sdk.session.status().then((x) => setStore("session_status", x.data!)),
|
sdk.session.status().then((x) => setStore("session_status", x.data!)),
|
||||||
loadSessions(directory),
|
loadSessions(directory),
|
||||||
sdk.mcp.status().then((x) => setStore("mcp", x.data!)),
|
sdk.mcp.status().then((x) => setStore("mcp", x.data!)),
|
||||||
|
|||||||
@@ -216,6 +216,7 @@ export const dict = {
|
|||||||
"prompt.popover.emptyCommands": "No matching commands",
|
"prompt.popover.emptyCommands": "No matching commands",
|
||||||
"prompt.dropzone.label": "Drop images or PDFs here",
|
"prompt.dropzone.label": "Drop images or PDFs here",
|
||||||
"prompt.slash.badge.custom": "custom",
|
"prompt.slash.badge.custom": "custom",
|
||||||
|
"prompt.slash.badge.skill": "skill",
|
||||||
"prompt.context.active": "active",
|
"prompt.context.active": "active",
|
||||||
"prompt.context.includeActiveFile": "Include active file",
|
"prompt.context.includeActiveFile": "Include active file",
|
||||||
"prompt.context.removeActiveFile": "Remove active file from context",
|
"prompt.context.removeActiveFile": "Remove active file from context",
|
||||||
|
|||||||
@@ -359,6 +359,20 @@ export function Autocomplete(props: {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const skill of sync.data.skill) {
|
||||||
|
results.push({
|
||||||
|
display: "/skill:" + skill.name,
|
||||||
|
description: skill.description,
|
||||||
|
onSelect: () => {
|
||||||
|
const newText = `Load the "${skill.name}" skill and follow its instructions.`
|
||||||
|
const cursor = props.input().logicalCursor
|
||||||
|
props.input().deleteRange(0, 0, cursor.row, cursor.col)
|
||||||
|
props.input().insertText(newText)
|
||||||
|
props.input().cursorOffset = Bun.stringWidth(newText)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
results.sort((a, b) => a.display.localeCompare(b.display))
|
results.sort((a, b) => a.display.localeCompare(b.display))
|
||||||
|
|
||||||
const max = firstBy(results, [(x) => x.display.length, "desc"])?.display.length
|
const max = firstBy(results, [(x) => x.display.length, "desc"])?.display.length
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ import type {
|
|||||||
ProviderListResponse,
|
ProviderListResponse,
|
||||||
ProviderAuthMethod,
|
ProviderAuthMethod,
|
||||||
VcsInfo,
|
VcsInfo,
|
||||||
|
AppSkillsResponse,
|
||||||
} from "@opencode-ai/sdk/v2"
|
} from "@opencode-ai/sdk/v2"
|
||||||
import { createStore, produce, reconcile } from "solid-js/store"
|
import { createStore, produce, reconcile } from "solid-js/store"
|
||||||
import { useSDK } from "@tui/context/sdk"
|
import { useSDK } from "@tui/context/sdk"
|
||||||
@@ -40,6 +41,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
provider_auth: Record<string, ProviderAuthMethod[]>
|
provider_auth: Record<string, ProviderAuthMethod[]>
|
||||||
agent: Agent[]
|
agent: Agent[]
|
||||||
command: Command[]
|
command: Command[]
|
||||||
|
skill: AppSkillsResponse
|
||||||
permission: {
|
permission: {
|
||||||
[sessionID: string]: PermissionRequest[]
|
[sessionID: string]: PermissionRequest[]
|
||||||
}
|
}
|
||||||
@@ -86,6 +88,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
permission: {},
|
permission: {},
|
||||||
question: {},
|
question: {},
|
||||||
command: [],
|
command: [],
|
||||||
|
skill: [],
|
||||||
provider: [],
|
provider: [],
|
||||||
provider_default: {},
|
provider_default: {},
|
||||||
session: [],
|
session: [],
|
||||||
@@ -385,6 +388,7 @@ export const { use: useSync, provider: SyncProvider } = createSimpleContext({
|
|||||||
Promise.all([
|
Promise.all([
|
||||||
...(args.continue ? [] : [sessionListPromise.then((sessions) => setStore("session", reconcile(sessions)))]),
|
...(args.continue ? [] : [sessionListPromise.then((sessions) => setStore("session", reconcile(sessions)))]),
|
||||||
sdk.client.command.list().then((x) => setStore("command", reconcile(x.data ?? []))),
|
sdk.client.command.list().then((x) => setStore("command", reconcile(x.data ?? []))),
|
||||||
|
sdk.client.app.skills().then((x) => setStore("skill", reconcile(x.data ?? []))),
|
||||||
sdk.client.lsp.status().then((x) => setStore("lsp", reconcile(x.data!))),
|
sdk.client.lsp.status().then((x) => setStore("lsp", reconcile(x.data!))),
|
||||||
sdk.client.mcp.status().then((x) => setStore("mcp", reconcile(x.data!))),
|
sdk.client.mcp.status().then((x) => setStore("mcp", reconcile(x.data!))),
|
||||||
sdk.client.experimental.resource.list().then((x) => setStore("mcp_resource", reconcile(x.data ?? {}))),
|
sdk.client.experimental.resource.list().then((x) => setStore("mcp_resource", reconcile(x.data ?? {}))),
|
||||||
|
|||||||
Reference in New Issue
Block a user