summaryrefslogtreecommitdiffhomepage
path: root/packages/desktop/src
diff options
context:
space:
mode:
authorAdam <[email protected]>2025-12-15 07:05:50 -0600
committerAdam <[email protected]>2025-12-15 10:20:19 -0600
commitff6864a7ca3772e6f2702d585c6bb64a40bd6cce (patch)
tree9b4dfebee7db7d5dfd4098065d4fa4c99018d414 /packages/desktop/src
parent5e37a902ce0ad209cedb0a85e997f1964064424a (diff)
downloadopencode-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.tsx74
-rw-r--r--packages/desktop/src/context/global-sync.tsx4
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!)),