feat(desktop): custom commands

This commit is contained in:
Adam 2025-12-15 07:05:50 -06:00
parent 5e37a902ce
commit ff6864a7ca
No known key found for this signature in database
GPG key ID: 9CB48779AF150E75
2 changed files with 69 additions and 9 deletions

View file

@ -61,6 +61,7 @@ interface SlashCommand {
title: string
description?: string
keybind?: string
type: "builtin" | "custom"
}
export const PromptInput: Component<PromptInputProps> = (props) => {
@ -208,8 +209,8 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
})
// Get slash commands from registered commands (only those with explicit slash trigger)
const slashCommands = createMemo<SlashCommand[]>(() =>
command.options
const slashCommands = createMemo<SlashCommand[]>(() => {
const builtin = command.options
.filter((opt) => !opt.disabled && !opt.id.startsWith("suggested.") && opt.slash)
.map((opt) => ({
id: opt.id,
@ -217,15 +218,46 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
title: opt.title,
description: opt.description,
keybind: opt.keybind,
})),
)
type: "builtin" as const,
}))
const custom = sync.data.command.map((cmd) => ({
id: `custom.${cmd.name}`,
trigger: cmd.name,
title: cmd.name,
description: cmd.description,
type: "custom" as const,
}))
return [...custom, ...builtin]
})
const handleSlashSelect = (cmd: SlashCommand | undefined) => {
if (!cmd) return
// Since slash commands only trigger from start, just clear the input
setStore("popover", null)
if (cmd.type === "custom") {
// For custom commands, insert the command text so user can add arguments
const text = `/${cmd.trigger} `
editorRef.innerHTML = ""
editorRef.textContent = text
prompt.set([{ type: "text", content: text, start: 0, end: text.length }], text.length)
// Set cursor at end
requestAnimationFrame(() => {
editorRef.focus()
const range = document.createRange()
const sel = window.getSelection()
range.selectNodeContents(editorRef)
range.collapse(false)
sel?.removeAllRanges()
sel?.addRange(range)
})
return
}
// For built-in commands, clear input and execute immediately
editorRef.innerHTML = ""
prompt.set([{ type: "text", content: "", start: 0, end: 0 }], 0)
setStore("popover", null)
command.trigger(cmd.id, "slash")
}
@ -571,6 +603,23 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
editorRef.innerHTML = ""
prompt.set([{ type: "text", content: "", start: 0, end: 0 }], 0)
// Check if this is a custom command
if (text.startsWith("/")) {
const [cmdName, ...args] = text.split(" ")
const commandName = cmdName.slice(1) // Remove leading "/"
const customCommand = sync.data.command.find((c) => c.name === commandName)
if (customCommand) {
sdk.client.session.command({
sessionID: existing.id,
command: commandName,
arguments: args.join(" "),
agent: local.agent.current()!.name,
model: `${local.model.current()!.provider.id}/${local.model.current()!.id}`,
})
return
}
}
sdk.client.session.prompt({
sessionID: existing.id,
agent: local.agent.current()!.name,
@ -641,9 +690,16 @@ export const PromptInput: Component<PromptInputProps> = (props) => {
<span class="text-14-regular text-text-weak truncate">{cmd.description}</span>
</Show>
</div>
<Show when={cmd.keybind}>
<span class="text-12-regular text-text-subtle shrink-0">{formatKeybind(cmd.keybind!)}</span>
</Show>
<div class="flex items-center gap-2 shrink-0">
<Show when={cmd.type === "custom"}>
<span class="text-11-regular text-text-subtle px-1.5 py-0.5 bg-surface-base rounded">
custom
</span>
</Show>
<Show when={cmd.keybind}>
<span class="text-12-regular text-text-subtle">{formatKeybind(cmd.keybind!)}</span>
</Show>
</div>
</button>
)}
</For>

View file

@ -13,6 +13,7 @@ import {
type SessionStatus,
type ProviderListResponse,
type ProviderAuthResponse,
type Command,
createOpencodeClient,
} from "@opencode-ai/sdk/v2/client"
import { createStore, produce, reconcile } from "solid-js/store"
@ -24,6 +25,7 @@ import { onMount } from "solid-js"
type State = {
ready: boolean
agent: Agent[]
command: Command[]
project: string
provider: ProviderListResponse
config: Config
@ -79,6 +81,7 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
path: { state: "", config: "", worktree: "", directory: "", home: "" },
ready: false,
agent: [],
command: [],
session: [],
session_status: {},
session_diff: {},
@ -118,6 +121,7 @@ export const { use: useGlobalSync, provider: GlobalSyncProvider } = createSimple
provider: () => sdk.provider.list().then((x) => setStore("provider", x.data!)),
path: () => sdk.path.get().then((x) => setStore("path", x.data!)),
agent: () => sdk.app.agents().then((x) => setStore("agent", x.data ?? [])),
command: () => sdk.command.list().then((x) => setStore("command", x.data ?? [])),
session: () => loadSessions(directory),
status: () => sdk.session.status().then((x) => setStore("session_status", x.data!)),
config: () => sdk.config.get().then((x) => setStore("config", x.data!)),