From ff6864a7ca3772e6f2702d585c6bb64a40bd6cce Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Mon, 15 Dec 2025 07:05:50 -0600 Subject: [PATCH] feat(desktop): custom commands --- .../desktop/src/components/prompt-input.tsx | 74 ++++++++++++++++--- packages/desktop/src/context/global-sync.tsx | 4 + 2 files changed, 69 insertions(+), 9 deletions(-) diff --git a/packages/desktop/src/components/prompt-input.tsx b/packages/desktop/src/components/prompt-input.tsx index 51c4e24d2..87f91104c 100644 --- a/packages/desktop/src/components/prompt-input.tsx +++ b/packages/desktop/src/components/prompt-input.tsx @@ -61,6 +61,7 @@ interface SlashCommand { title: string description?: string keybind?: string + type: "builtin" | "custom" } export const PromptInput: Component = (props) => { @@ -208,8 +209,8 @@ export const PromptInput: Component = (props) => { }) // Get slash commands from registered commands (only those with explicit slash trigger) - const slashCommands = createMemo(() => - command.options + const slashCommands = createMemo(() => { + 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 = (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 = (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 = (props) => { {cmd.description} - - {formatKeybind(cmd.keybind!)} - +
+ + + custom + + + + {formatKeybind(cmd.keybind!)} + +
)} diff --git a/packages/desktop/src/context/global-sync.tsx b/packages/desktop/src/context/global-sync.tsx index 8151a2c6f..b90dde34f 100644 --- a/packages/desktop/src/context/global-sync.tsx +++ b/packages/desktop/src/context/global-sync.tsx @@ -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!)),