feat(core): add native skill activation
This commit is contained in:
parent
524ee8fc03
commit
23adaaaeab
26 changed files with 814 additions and 76 deletions
|
|
@ -2,26 +2,32 @@ import { TextAttributes } from "@opentui/core"
|
|||
import { DialogSelect, type DialogSelectOption } from "../ui/dialog-select"
|
||||
import { createResource, createMemo, createSignal } from "solid-js"
|
||||
import { useDialog } from "../ui/dialog"
|
||||
import { useSDK } from "../context/sdk"
|
||||
import { useTheme } from "../context/theme"
|
||||
import { errorMessage } from "../util/error"
|
||||
import { useData } from "../context/data"
|
||||
import type { LocationRef } from "@opencode-ai/sdk/v2"
|
||||
|
||||
export type DialogSkillProps = {
|
||||
location?: LocationRef
|
||||
onSelect: (skill: string) => void
|
||||
}
|
||||
|
||||
export function DialogSkill(props: DialogSkillProps) {
|
||||
const dialog = useDialog()
|
||||
const sdk = useSDK()
|
||||
const data = useData()
|
||||
const { theme } = useTheme()
|
||||
dialog.setSize("large")
|
||||
|
||||
const [loadError, setLoadError] = createSignal<unknown>()
|
||||
|
||||
const [skills] = createResource(() =>
|
||||
sdk.client.app
|
||||
.skills({}, { throwOnError: true })
|
||||
.then((result) => result.data ?? [])
|
||||
Promise.resolve()
|
||||
.then(async () => {
|
||||
const current = data.location.skill.list(props.location)
|
||||
if (current) return current
|
||||
await data.location.skill.refresh(props.location)
|
||||
return data.location.skill.list(props.location) ?? []
|
||||
})
|
||||
// Catch so the rejected resource never reaches the memo below: reading
|
||||
// skills() in an errored state re-throws and tears down the dialog.
|
||||
.catch((error) => {
|
||||
|
|
|
|||
|
|
@ -443,12 +443,12 @@ export function Autocomplete(props: {
|
|||
|
||||
const commands = createMemo((): AutocompleteOption[] => {
|
||||
const results: AutocompleteOption[] = [...slashes()]
|
||||
const commandNames = new Set<string>()
|
||||
|
||||
for (const serverCommand of sync.data.command) {
|
||||
if (serverCommand.source === "skill") continue
|
||||
const label = serverCommand.source === "mcp" ? ":mcp" : ""
|
||||
for (const serverCommand of data.location.command.list(location()) ?? []) {
|
||||
commandNames.add(serverCommand.name)
|
||||
results.push({
|
||||
display: "/" + serverCommand.name + label,
|
||||
display: "/" + serverCommand.name,
|
||||
description: serverCommand.description,
|
||||
onSelect: () => {
|
||||
const newText = "/" + serverCommand.name + " "
|
||||
|
|
@ -460,6 +460,22 @@ export function Autocomplete(props: {
|
|||
})
|
||||
}
|
||||
|
||||
for (const skill of data.location.skill
|
||||
.list(location())
|
||||
?.filter((skill) => skill.slash === true && !commandNames.has(skill.name)) ?? []) {
|
||||
results.push({
|
||||
display: "/" + skill.name,
|
||||
description: skill.description,
|
||||
onSelect: () => {
|
||||
const newText = "/" + skill.name + " "
|
||||
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))
|
||||
|
||||
const max = firstBy(results, [(x) => x.display.length, "desc"])?.display.length
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ import { usePromptWorkspace } from "./workspace"
|
|||
import { usePromptMove } from "./move"
|
||||
import { readLocalAttachment } from "./local-attachment"
|
||||
import { useData } from "../../context/data"
|
||||
import { useLocation } from "../../context/location"
|
||||
|
||||
export type PromptProps = {
|
||||
sessionID?: string
|
||||
|
|
@ -154,6 +155,7 @@ export function Prompt(props: PromptProps) {
|
|||
const project = useProject()
|
||||
const sync = useSync()
|
||||
const data = useData()
|
||||
const currentLocation = useLocation()
|
||||
const tuiConfig = useTuiConfig()
|
||||
const dialog = useDialog()
|
||||
const toast = useToast()
|
||||
|
|
@ -533,6 +535,7 @@ export function Prompt(props: PromptProps) {
|
|||
run: () => {
|
||||
dialog.replace(() => (
|
||||
<DialogSkill
|
||||
location={currentLocation()}
|
||||
onSelect={(skill) => {
|
||||
input.setText(`/${skill} `)
|
||||
setStore("prompt", {
|
||||
|
|
@ -1088,7 +1091,9 @@ export function Prompt(props: PromptProps) {
|
|||
setStore("mode", "normal")
|
||||
} else if (
|
||||
inputText.startsWith("/") &&
|
||||
sync.data.command.some((x) => x.name === inputText.split("\n")[0].split(" ")[0].slice(1))
|
||||
(data.location.command.list(currentLocation()) ?? []).some(
|
||||
(command) => command.name === inputText.split("\n")[0].split(" ")[0].slice(1),
|
||||
)
|
||||
) {
|
||||
move.startSubmit()
|
||||
// Parse command from first line, preserve multi-line content in arguments
|
||||
|
|
@ -1107,6 +1112,17 @@ export function Prompt(props: PromptProps) {
|
|||
variant,
|
||||
parts: nonTextParts.filter((x) => x.type === "file"),
|
||||
})
|
||||
} else if (
|
||||
inputText.startsWith("/") &&
|
||||
(data.location.skill.list(currentLocation()) ?? []).some(
|
||||
(skill) => skill.slash === true && skill.name === inputText.split("\n")[0].split(" ")[0].slice(1),
|
||||
)
|
||||
) {
|
||||
move.startSubmit()
|
||||
void sdk.api.session.skill({
|
||||
sessionID,
|
||||
skill: inputText.split("\n")[0].split(" ")[0].slice(1),
|
||||
})
|
||||
} else {
|
||||
move.startSubmit()
|
||||
if (!session) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue