From 281e58be7c28445023a5c52cd3b31dbc17cb8bb6 Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Wed, 17 Dec 2025 12:53:18 -0500 Subject: [PATCH 01/10] feat: add keybind support for custom slash commands Allow users to bind custom slash commands to keystrokes by adding entries to the keybinds config with keys starting with '/'. Changes: - Update Config.Keybinds schema to accept arbitrary keys using catchall - Add keyboard handler in TUI to match and execute command keybinds - Regenerate TypeScript SDK to reflect updated KeybindsConfig type When a command keybind is pressed, the prompt is set to the command text and immediately submitted, executing the custom command. Example usage in config.json: { "keybinds": { "/my-command": "ctrl+shift+m" } } Resolves: #47 --- packages/opencode/src/cli/cmd/tui/app.tsx | 50 ++++++++++++++++++++++- packages/opencode/src/config/config.ts | 2 +- packages/sdk/js/src/v2/gen/types.gen.ts | 1 + 3 files changed, 51 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index a1a8a5e80d1..80f01b91145 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -19,7 +19,7 @@ import { DialogHelp } from "./ui/dialog-help" import { CommandProvider, useCommandDialog } from "@tui/component/dialog-command" import { DialogAgent } from "@tui/component/dialog-agent" import { DialogSessionList } from "@tui/component/dialog-session-list" -import { KeybindProvider } from "@tui/context/keybind" +import { KeybindProvider, useKeybind } from "@tui/context/keybind" import { ThemeProvider, useTheme } from "@tui/context/theme" import { Home } from "@tui/routes/home" import { Session } from "@tui/routes/session" @@ -34,6 +34,7 @@ import { Provider } from "@/provider/provider" import { ArgsProvider, useArgs, type Args } from "./context/args" import open from "open" import { PromptRefProvider, usePromptRef } from "./context/prompt" +import { Keybind } from "@/util/keybind" async function getTerminalBackgroundColor(): Promise<"dark" | "light"> { // can't set raw mode if not a TTY @@ -167,6 +168,8 @@ function App() { const sync = useSync() const exit = useExit() const promptRef = usePromptRef() + const keybind = useKeybind() + const sdk = useSDK() createEffect(() => { console.log(JSON.stringify(route.data)) @@ -445,6 +448,51 @@ function App() { }, ]) + // Handle custom command keybinds + useKeyboard((evt) => { + if (command.suspended()) return + if (dialog.stack.length > 0) return + if (evt.defaultPrevented) return + + const keybinds = sync.data.config.keybinds ?? {} + for (const [key, value] of Object.entries(keybinds)) { + if (!key.startsWith("/")) continue + + const commandName = key.slice(1) + const commandKeybinds = Keybind.parse(value) + const parsed = keybind.parse(evt) + + for (const kb of commandKeybinds) { + if (Keybind.match(kb, parsed)) { + evt.preventDefault() + + // Find the command to verify it exists + const cmd = sync.data.command.find((c) => c.name === commandName) + if (!cmd) { + toast.show({ + variant: "error", + message: `Command not found: ${commandName}`, + duration: 3000, + }) + return + } + + // Set prompt to command and submit + const current = promptRef.current + if (current) { + current.set({ + input: `/${commandName}`, + parts: [], + }) + current.submit() + } + + return + } + } + } + }) + createEffect(() => { const currentModel = local.model.current() if (!currentModel) return diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 9086f70ce62..1a22811ba9d 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -560,7 +560,7 @@ export namespace Config { session_child_cycle_reverse: z.string().optional().default("left").describe("Previous child session"), terminal_suspend: z.string().optional().default("ctrl+z").describe("Suspend terminal"), }) - .strict() + .catchall(z.string()) .meta({ ref: "KeybindsConfig", }) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index 00f209c6d88..1bc704f0e43 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1110,6 +1110,7 @@ export type KeybindsConfig = { * Suspend terminal */ terminal_suspend?: string + [key: string]: string | undefined } export type AgentConfig = { From 326718da05f57bece3e22a062a7def1305f61f70 Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Wed, 17 Dec 2025 12:54:00 -0500 Subject: [PATCH 02/10] fix: handle undefined values in command keybind parsing Add null checks to prevent TypeScript errors when keybind values are undefined in the config. --- packages/opencode/src/cli/cmd/tui/app.tsx | 1 + packages/opencode/src/cli/cmd/tui/context/keybind.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 80f01b91145..7f5c1fd9dbd 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -457,6 +457,7 @@ function App() { const keybinds = sync.data.config.keybinds ?? {} for (const [key, value] of Object.entries(keybinds)) { if (!key.startsWith("/")) continue + if (!value) continue const commandName = key.slice(1) const commandKeybinds = Keybind.parse(value) diff --git a/packages/opencode/src/cli/cmd/tui/context/keybind.tsx b/packages/opencode/src/cli/cmd/tui/context/keybind.tsx index 4c82e594c3e..10766115a8c 100644 --- a/packages/opencode/src/cli/cmd/tui/context/keybind.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/keybind.tsx @@ -15,7 +15,7 @@ export const { use: useKeybind, provider: KeybindProvider } = createSimpleContex const keybinds = createMemo(() => { return pipe( sync.data.config.keybinds ?? {}, - mapValues((value) => Keybind.parse(value)), + mapValues((value) => (value ? Keybind.parse(value) : [])), ) }) const [store, setStore] = createStore({ From ac4ba7f1986f908f44a2d7e7a8a65345afc1969d Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Thu, 18 Dec 2025 08:16:11 -0500 Subject: [PATCH 03/10] feat: preserve prompt text as command arguments in keybind execution When a command keybind is triggered, preserve any existing text in the prompt input box and append it as arguments to the command. Example: If the prompt contains 'blah blah abrahadabra' and the user presses a key bound to /foo, the prompt becomes '/foo blah blah abrahadabra' before submission, passing the original text as arguments to the command. This makes command keybinds more flexible and allows users to quickly apply commands to text they've already typed. --- packages/opencode/src/cli/cmd/tui/app.tsx | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 7f5c1fd9dbd..db099a18102 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -478,12 +478,17 @@ function App() { return } - // Set prompt to command and submit + // Preserve existing prompt text as command arguments const current = promptRef.current if (current) { + const existingInput = current.current.input.trim() + const commandInput = existingInput + ? `/${commandName} ${existingInput}` + : `/${commandName}` + current.set({ - input: `/${commandName}`, - parts: [], + input: commandInput, + parts: current.current.parts, }) current.submit() } From 7a283f583b03042a99d49765d3e8bb32e43a81fa Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Sun, 21 Dec 2025 11:04:47 -0500 Subject: [PATCH 04/10] fix: remove unused sdk variable in app.tsx --- packages/opencode/src/cli/cmd/tui/app.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 650676be706..302b1b70445 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -177,7 +177,6 @@ function App() { const exit = useExit() const promptRef = usePromptRef() const keybind = useKeybind() - const sdk = useSDK() const [terminalTitleEnabled, setTerminalTitleEnabled] = createSignal(kv.get("terminal_title_enabled", true)) From 5a1d2d3fd8170166f0cebb0c634a3927af7e5aae Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Wed, 31 Dec 2025 22:22:43 -0500 Subject: [PATCH 05/10] Fix type compatibility for vercel provider after merge --- packages/opencode/src/provider/provider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 983a0827223..afd91ab35e0 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -59,7 +59,7 @@ export namespace Provider { "@ai-sdk/gateway": createGateway, "@ai-sdk/togetherai": createTogetherAI, "@ai-sdk/perplexity": createPerplexity, - "@ai-sdk/vercel": createVercel, + "@ai-sdk/vercel": createVercel as any, // @ts-ignore (TODO: kill this code so we dont have to maintain it) "@ai-sdk/github-copilot": createGitHubCopilotOpenAICompatible, } From 7fdf1212316c1617f7f972a8becdca53d2bf7337 Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Thu, 1 Jan 2026 18:15:45 -0500 Subject: [PATCH 06/10] revert a file --- packages/opencode/src/provider/provider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 495ac25b3cb..93d2104e25c 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -59,7 +59,7 @@ export namespace Provider { "@ai-sdk/gateway": createGateway, "@ai-sdk/togetherai": createTogetherAI, "@ai-sdk/perplexity": createPerplexity, - "@ai-sdk/vercel": createVercel as any, + "@ai-sdk/vercel": createVercel, // @ts-ignore (TODO: kill this code so we dont have to maintain it) "@ai-sdk/github-copilot": createGitHubCopilotOpenAICompatible, } From 9d4e527af63aa1f1617e3abaaf55be9e818f4499 Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Sun, 4 Jan 2026 12:08:15 -0500 Subject: [PATCH 07/10] Fix type compatibility with Vercel AI SDK provider --- packages/opencode/src/provider/provider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 9967edec5dd..9c207ed5e33 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -59,7 +59,7 @@ export namespace Provider { "@ai-sdk/gateway": createGateway, "@ai-sdk/togetherai": createTogetherAI, "@ai-sdk/perplexity": createPerplexity, - "@ai-sdk/vercel": createVercel, + "@ai-sdk/vercel": createVercel as any, // @ts-ignore (TODO: kill this code so we dont have to maintain it) "@ai-sdk/github-copilot": createGitHubCopilotOpenAICompatible, } From 12a6bbe2940443dea203af365ad9554a0bda80fe Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Sun, 4 Jan 2026 17:49:31 -0500 Subject: [PATCH 08/10] revert a file --- packages/opencode/src/provider/provider.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/provider/provider.ts b/packages/opencode/src/provider/provider.ts index 9c207ed5e33..9967edec5dd 100644 --- a/packages/opencode/src/provider/provider.ts +++ b/packages/opencode/src/provider/provider.ts @@ -59,7 +59,7 @@ export namespace Provider { "@ai-sdk/gateway": createGateway, "@ai-sdk/togetherai": createTogetherAI, "@ai-sdk/perplexity": createPerplexity, - "@ai-sdk/vercel": createVercel as any, + "@ai-sdk/vercel": createVercel, // @ts-ignore (TODO: kill this code so we dont have to maintain it) "@ai-sdk/github-copilot": createGitHubCopilotOpenAICompatible, } From da9cd02f01468a9dce69ca7fce3506ff10bd0a79 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Tue, 6 Jan 2026 12:21:04 -0600 Subject: [PATCH 09/10] fix: ensure 'name' isnt being sent in request body for custom agent --- packages/opencode/src/agent/agent.ts | 1 - packages/opencode/src/config/config.ts | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index c683727dfa3..6fc228795af 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -188,7 +188,6 @@ export namespace Agent { item.topP = value.top_p ?? item.topP item.mode = value.mode ?? item.mode item.color = value.color ?? item.color - item.name = value.options?.name ?? item.name item.steps = value.steps ?? item.steps item.options = mergeDeep(item.options, value.options ?? {}) item.permission = PermissionNext.merge(item.permission, PermissionNext.fromConfig(value.permission ?? {})) diff --git a/packages/opencode/src/config/config.ts b/packages/opencode/src/config/config.ts index 342ee56cf28..5a3ed05a362 100644 --- a/packages/opencode/src/config/config.ts +++ b/packages/opencode/src/config/config.ts @@ -483,6 +483,7 @@ export namespace Config { .catchall(z.any()) .transform((agent, ctx) => { const knownKeys = new Set([ + "name", "model", "prompt", "description", From ad6afc2d19ad84e657a25fb097fa34efdb617eda Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Tue, 6 Jan 2026 12:31:41 -0600 Subject: [PATCH 10/10] test: fix test --- packages/opencode/src/agent/agent.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/opencode/src/agent/agent.ts b/packages/opencode/src/agent/agent.ts index 6fc228795af..21859186659 100644 --- a/packages/opencode/src/agent/agent.ts +++ b/packages/opencode/src/agent/agent.ts @@ -188,6 +188,7 @@ export namespace Agent { item.topP = value.top_p ?? item.topP item.mode = value.mode ?? item.mode item.color = value.color ?? item.color + item.name = value.name ?? item.name item.steps = value.steps ?? item.steps item.options = mergeDeep(item.options, value.options ?? {}) item.permission = PermissionNext.merge(item.permission, PermissionNext.fromConfig(value.permission ?? {}))