diff --git a/packages/opencode/src/cli/cmd/tui/app.tsx b/packages/opencode/src/cli/cmd/tui/app.tsx index ea742f699708..6074a2bdbd6d 100644 --- a/packages/opencode/src/cli/cmd/tui/app.tsx +++ b/packages/opencode/src/cli/cmd/tui/app.tsx @@ -14,6 +14,8 @@ import { batch, Show, on, + onCleanup, + untrack, } from "solid-js" import { win32DisableProcessedInput, win32InstallCtrlCGuard } from "./win32" import { Flag } from "@opencode-ai/core/flag/flag" @@ -58,6 +60,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" import { TuiConfigProvider, useTuiConfig } from "./context/tui-config" import { TuiConfig } from "@/cli/cmd/tui/config/tui" import { createTuiApi } from "@/cli/cmd/tui/plugin/api" @@ -781,6 +784,70 @@ function App(props: { onSnapshot?: () => Promise }) { }, ]) + // Handle custom command keybinds + useKeyboard((evt) => { + if (command.suspended()) return + if (dialog.stack.length > 0) return + if (evt.defaultPrevented) return + + const keybinds = tuiConfig.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) + 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 + } + + // 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: commandInput, + parts: current.current.parts, + }) + current.submit() + } + + return + } + } + } + }) + + createEffect(() => { + const currentModel = local.model.current() + if (!currentModel) return + if (currentModel.providerID === "openrouter" && !kv.get("openrouter_warning", false)) { + untrack(() => { + DialogAlert.show( + dialog, + "Warning", + "While openrouter is a convenient way to access LLMs your request will often be routed to subpar providers that do not work well in our testing.\n\nFor reliable access to models check out OpenCode Zen\nhttps://opencode.ai/zen", + ).then(() => kv.set("openrouter_warning", true)) + }) + } + }) event.on(TuiEvent.CommandExecute.type, (evt) => { command.trigger(evt.properties.command) }) diff --git a/packages/opencode/src/cli/cmd/tui/config/tui-schema.ts b/packages/opencode/src/cli/cmd/tui/config/tui-schema.ts index ed79e8e52418..746a7fdcfc81 100644 --- a/packages/opencode/src/cli/cmd/tui/config/tui-schema.ts +++ b/packages/opencode/src/cli/cmd/tui/config/tui-schema.ts @@ -9,7 +9,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"), diff --git a/packages/opencode/src/cli/cmd/tui/context/keybind.tsx b/packages/opencode/src/cli/cmd/tui/context/keybind.tsx index 2c1ab245a50c..88e40e3ae891 100644 --- a/packages/opencode/src/cli/cmd/tui/context/keybind.tsx +++ b/packages/opencode/src/cli/cmd/tui/context/keybind.tsx @@ -17,7 +17,7 @@ export const { use: useKeybind, provider: KeybindProvider } = createSimpleContex const keybinds = createMemo>(() => { return pipe( (config.keybinds ?? {}) as Record, - mapValues((value) => Keybind.parse(value)), + mapValues((value) => (value ? Keybind.parse(value) : [])), ) }) const [store, setStore] = createStore({ 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