diff options
| author | Adam <[email protected]> | 2025-12-15 07:05:50 -0600 |
|---|---|---|
| committer | Adam <[email protected]> | 2025-12-15 10:20:19 -0600 |
| commit | ff6864a7ca3772e6f2702d585c6bb64a40bd6cce (patch) | |
| tree | 9b4dfebee7db7d5dfd4098065d4fa4c99018d414 /packages/desktop/src | |
| parent | 5e37a902ce0ad209cedb0a85e997f1964064424a (diff) | |
| download | opencode-ff6864a7ca3772e6f2702d585c6bb64a40bd6cce.tar.gz opencode-ff6864a7ca3772e6f2702d585c6bb64a40bd6cce.zip | |
feat(desktop): custom commands
Diffstat (limited to 'packages/desktop/src')
| -rw-r--r-- | packages/desktop/src/components/prompt-input.tsx | 74 | ||||
| -rw-r--r-- | 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<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> 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!)), |
