From 281e58be7c28445023a5c52cd3b31dbc17cb8bb6 Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Wed, 17 Dec 2025 12:53:18 -0500 Subject: [PATCH 01/16] 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 a1a8a5e80d16..80f01b91145a 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 9086f70ce622..1a22811ba9d0 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 00f209c6d883..1bc704f0e43a 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/16] 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 80f01b91145a..7f5c1fd9dbd3 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 4c82e594c3e5..10766115a8c4 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/16] 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 7f5c1fd9dbd3..db099a181023 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/16] 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 650676be706b..302b1b704456 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/16] 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 983a08272237..afd91ab35e0d 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/16] 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 495ac25b3cbb..93d2104e25c0 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/16] 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 9967edec5dd8..9c207ed5e338 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/16] 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 9c207ed5e338..9967edec5dd8 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/16] 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 c683727dfa33..6fc228795af8 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 342ee56cf284..5a3ed05a3629 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/16] 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 6fc228795af8..218591866598 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 ?? {})) From 1752f11dd4258ed2c4f49d7515ba2d52f4dd12ad Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Wed, 21 Jan 2026 21:56:41 -0500 Subject: [PATCH 11/16] fix: update permission config test to match new key order from dev merge --- packages/opencode/test/config/config.test.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index decd18446c19..521158d36f24 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -1078,11 +1078,14 @@ test("permission config preserves key order", async () => { fn: async () => { const config = await Config.get() expect(Object.keys(config.permission!)).toEqual([ + "doom_loop", + "external_directory", + "read", + "webfetch", + "bash", "*", "edit", "write", - "external_directory", - "read", "todowrite", "todoread", "thoughts_*", From f6d36f40c12db92b5ca0e76fcecd57c94b48efcf Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Mon, 26 Jan 2026 15:15:34 -0500 Subject: [PATCH 12/16] fix: revert file --- packages/opencode/test/config/config.test.ts | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/packages/opencode/test/config/config.test.ts b/packages/opencode/test/config/config.test.ts index 521158d36f24..decd18446c19 100644 --- a/packages/opencode/test/config/config.test.ts +++ b/packages/opencode/test/config/config.test.ts @@ -1078,14 +1078,11 @@ test("permission config preserves key order", async () => { fn: async () => { const config = await Config.get() expect(Object.keys(config.permission!)).toEqual([ - "doom_loop", - "external_directory", - "read", - "webfetch", - "bash", "*", "edit", "write", + "external_directory", + "read", "todowrite", "todoread", "thoughts_*", From d994ae307318e8b13ef0d8a5a79c33d7a1ecfd02 Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Fri, 27 Feb 2026 10:42:50 -0500 Subject: [PATCH 13/16] fix: allow custom keybinds in tui.json by using catchall instead of strict --- packages/opencode/src/cli/cmd/tui/app.tsx | 5 +++-- packages/opencode/src/config/tui-schema.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 5d3975f973db..c946f324387a 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -39,7 +39,7 @@ import open from "open" import { writeHeapSnapshot } from "v8" import { PromptRefProvider, usePromptRef } from "./context/prompt" import { Keybind } from "@/util/keybind" -import { TuiConfigProvider } from "./context/tui-config" +import { TuiConfigProvider, useTuiConfig } from "./context/tui-config" import { TuiConfig } from "@/config/tui" async function getTerminalBackgroundColor(): Promise<"dark" | "light"> { @@ -217,6 +217,7 @@ function App() { const exit = useExit() const promptRef = usePromptRef() const keybind = useKeybind() + const tuiConfig = useTuiConfig() useKeyboard((evt) => { if (!Flag.OPENCODE_EXPERIMENTAL_DISABLE_COPY_ON_SELECT) return @@ -667,7 +668,7 @@ function App() { if (dialog.stack.length > 0) return if (evt.defaultPrevented) return - const keybinds = sync.data.config.keybinds ?? {} + const keybinds = tuiConfig.keybinds ?? {} for (const [key, value] of Object.entries(keybinds)) { if (!key.startsWith("/")) continue if (!value) continue diff --git a/packages/opencode/src/config/tui-schema.ts b/packages/opencode/src/config/tui-schema.ts index f9068e3f01d3..1ea7cbd86215 100644 --- a/packages/opencode/src/config/tui-schema.ts +++ b/packages/opencode/src/config/tui-schema.ts @@ -8,7 +8,7 @@ const KeybindOverride = z z.ZodOptional >, ) - .strict() + .catchall(z.string().optional()) export const TuiOptions = z.object({ scroll_speed: z.number().min(0.001).optional().describe("TUI scroll speed"), From 4821a953f66204462b2ff929fe5132452b07fb26 Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Fri, 17 Apr 2026 10:00:09 -0400 Subject: [PATCH 14/16] fix sdk generation noise for keybind commands --- packages/sdk/js/src/v2/gen/types.gen.ts | 383 ------------------------ 1 file changed, 383 deletions(-) diff --git a/packages/sdk/js/src/v2/gen/types.gen.ts b/packages/sdk/js/src/v2/gen/types.gen.ts index a7dd6f93e521..795c2f264adc 100644 --- a/packages/sdk/js/src/v2/gen/types.gen.ts +++ b/packages/sdk/js/src/v2/gen/types.gen.ts @@ -1165,389 +1165,6 @@ export type GlobalEvent = { | SyncEventSessionDeleted } -/** - * Custom keybind configurations - */ -export type KeybindsConfig = { - /** - * Leader key for keybind combinations - */ - leader?: string - /** - * Exit the application - */ - app_exit?: string - /** - * Open external editor - */ - editor_open?: string - /** - * List available themes - */ - theme_list?: string - /** - * Toggle sidebar - */ - sidebar_toggle?: string - /** - * Toggle session scrollbar - */ - scrollbar_toggle?: string - /** - * Toggle username visibility - */ - username_toggle?: string - /** - * View status - */ - status_view?: string - /** - * Export session to editor - */ - session_export?: string - /** - * Create a new session - */ - session_new?: string - /** - * List all sessions - */ - session_list?: string - /** - * Show session timeline - */ - session_timeline?: string - /** - * Fork session from message - */ - session_fork?: string - /** - * Rename session - */ - session_rename?: string - /** - * Delete session - */ - session_delete?: string - /** - * Delete stash entry - */ - stash_delete?: string - /** - * Open provider list from model dialog - */ - model_provider_list?: string - /** - * Toggle model favorite status - */ - model_favorite_toggle?: string - /** - * Share current session - */ - session_share?: string - /** - * Unshare current session - */ - session_unshare?: string - /** - * Interrupt current session - */ - session_interrupt?: string - /** - * Compact the session - */ - session_compact?: string - /** - * Scroll messages up by one page - */ - messages_page_up?: string - /** - * Scroll messages down by one page - */ - messages_page_down?: string - /** - * Scroll messages up by one line - */ - messages_line_up?: string - /** - * Scroll messages down by one line - */ - messages_line_down?: string - /** - * Scroll messages up by half page - */ - messages_half_page_up?: string - /** - * Scroll messages down by half page - */ - messages_half_page_down?: string - /** - * Navigate to first message - */ - messages_first?: string - /** - * Navigate to last message - */ - messages_last?: string - /** - * Navigate to next message - */ - messages_next?: string - /** - * Navigate to previous message - */ - messages_previous?: string - /** - * Navigate to last user message - */ - messages_last_user?: string - /** - * Copy message - */ - messages_copy?: string - /** - * Undo message - */ - messages_undo?: string - /** - * Redo message - */ - messages_redo?: string - /** - * Toggle code block concealment in messages - */ - messages_toggle_conceal?: string - /** - * Toggle tool details visibility - */ - tool_details?: string - /** - * List available models - */ - model_list?: string - /** - * Next recently used model - */ - model_cycle_recent?: string - /** - * Previous recently used model - */ - model_cycle_recent_reverse?: string - /** - * Next favorite model - */ - model_cycle_favorite?: string - /** - * Previous favorite model - */ - model_cycle_favorite_reverse?: string - /** - * List available commands - */ - command_list?: string - /** - * List agents - */ - agent_list?: string - /** - * Next agent - */ - agent_cycle?: string - /** - * Previous agent - */ - agent_cycle_reverse?: string - /** - * Cycle model variants - */ - variant_cycle?: string - /** - * Clear input field - */ - input_clear?: string - /** - * Paste from clipboard - */ - input_paste?: string - /** - * Submit input - */ - input_submit?: string - /** - * Insert newline in input - */ - input_newline?: string - /** - * Move cursor left in input - */ - input_move_left?: string - /** - * Move cursor right in input - */ - input_move_right?: string - /** - * Move cursor up in input - */ - input_move_up?: string - /** - * Move cursor down in input - */ - input_move_down?: string - /** - * Select left in input - */ - input_select_left?: string - /** - * Select right in input - */ - input_select_right?: string - /** - * Select up in input - */ - input_select_up?: string - /** - * Select down in input - */ - input_select_down?: string - /** - * Move to start of line in input - */ - input_line_home?: string - /** - * Move to end of line in input - */ - input_line_end?: string - /** - * Select to start of line in input - */ - input_select_line_home?: string - /** - * Select to end of line in input - */ - input_select_line_end?: string - /** - * Move to start of visual line in input - */ - input_visual_line_home?: string - /** - * Move to end of visual line in input - */ - input_visual_line_end?: string - /** - * Select to start of visual line in input - */ - input_select_visual_line_home?: string - /** - * Select to end of visual line in input - */ - input_select_visual_line_end?: string - /** - * Move to start of buffer in input - */ - input_buffer_home?: string - /** - * Move to end of buffer in input - */ - input_buffer_end?: string - /** - * Select to start of buffer in input - */ - input_select_buffer_home?: string - /** - * Select to end of buffer in input - */ - input_select_buffer_end?: string - /** - * Delete line in input - */ - input_delete_line?: string - /** - * Delete to end of line in input - */ - input_delete_to_line_end?: string - /** - * Delete to start of line in input - */ - input_delete_to_line_start?: string - /** - * Backspace in input - */ - input_backspace?: string - /** - * Delete character in input - */ - input_delete?: string - /** - * Undo in input - */ - input_undo?: string - /** - * Redo in input - */ - input_redo?: string - /** - * Move word forward in input - */ - input_word_forward?: string - /** - * Move word backward in input - */ - input_word_backward?: string - /** - * Select word forward in input - */ - input_select_word_forward?: string - /** - * Select word backward in input - */ - input_select_word_backward?: string - /** - * Delete word forward in input - */ - input_delete_word_forward?: string - /** - * Delete word backward in input - */ - input_delete_word_backward?: string - /** - * Previous history item - */ - history_previous?: string - /** - * Next history item - */ - history_next?: string - /** - * Next child session - */ - session_child_cycle?: string - /** - * Previous child session - */ - session_child_cycle_reverse?: string - /** - * Go to parent session - */ - session_parent?: string - /** - * Suspend terminal - */ - terminal_suspend?: string - /** - * Toggle terminal title - */ - terminal_title_toggle?: string - /** - * Toggle tips on home screen - */ - tips_toggle?: string - /** - * Toggle thinking blocks visibility - */ - display_thinking?: string - [key: string]: string | undefined -} - /** * Log level */ From d8d1a06367f5a8dea2103cbadad5d9efd8289360 Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Thu, 23 Apr 2026 19:21:12 -0400 Subject: [PATCH 15/16] fix: import Keybind from @/util instead of @/util/keybind to use namespace export --- packages/opencode/src/cli/cmd/tui/app.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index 88ff315317f8..69913c404d2b 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -58,7 +58,7 @@ import { Provider } from "@/provider" import { ArgsProvider, useArgs, type Args } from "./context/args" import open from "open" import { PromptRefProvider, usePromptRef } from "./context/prompt" -import { Keybind } from "@/util/keybind" +import { Keybind } from "@/util" import { TuiConfigProvider, useTuiConfig } from "./context/tui-config" import { TuiConfig } from "@/cli/cmd/tui/config/tui" import { createTuiApi, TuiPluginRuntime, type RouteMap } from "./plugin" From d81ad745fc57c98b5b9e9143d1c11bf988b018c3 Mon Sep 17 00:00:00 2001 From: Ariane Emory Date: Mon, 4 May 2026 05:56:39 -0400 Subject: [PATCH 16/16] fix: allow custom command keybinds in keybinds schema The Effect Schema migration lost the .catchall(z.string()) that allowed arbitrary string keys like /note-dir for custom command keybinds. Change Schema.Struct to Schema.StructWithRest with a Record catchall to restore custom command keybind support. --- packages/opencode/src/config/keybinds.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/opencode/src/config/keybinds.ts b/packages/opencode/src/config/keybinds.ts index a84fc0b37d58..e14645946bed 100644 --- a/packages/opencode/src/config/keybinds.ts +++ b/packages/opencode/src/config/keybinds.ts @@ -14,7 +14,7 @@ const keybind = (value: string, description: string) => // cannot consume ctrl+z on native Windows terminals (no POSIX suspend). const inputUndoDefault = process.platform === "win32" ? "ctrl+z,ctrl+-,super+z" : "ctrl+-,super+z" -const KeybindsSchema = Schema.Struct({ +const KeybindsSchema = Schema.StructWithRest(Schema.Struct({ leader: keybind("ctrl+x", "Leader key for keybind combinations"), app_exit: keybind("ctrl+c,ctrl+d,q", "Exit the application"), editor_open: keybind("e", "Open external editor"), @@ -115,7 +115,9 @@ const KeybindsSchema = Schema.Struct({ tips_toggle: keybind("h", "Toggle tips on home screen"), plugin_manager: keybind("none", "Open plugin manager dialog"), display_thinking: keybind("none", "Toggle thinking blocks visibility"), -}).annotate({ identifier: "KeybindsConfig" }) + }), + [Schema.Record(Schema.String, Schema.String)] +).annotate({ identifier: "KeybindsConfig" }) export type Keybinds = Schema.Schema.Type