diff --git a/AGENTS.md b/AGENTS.md index 8feebb7..154f416 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,7 +19,7 @@ vimcode is a TUI plugin for [OpenCode](https://opencode.ai). Before working on i - **`key:before` is NOT a valid intercept type.** The keymap only supports `"key"`, `"key:after"`, and `"raw"`. Passing `"key:before"` silently registers a raw terminal sequence handler that crashes on key events. - `dispatchCommand()` from inside a `key` intercept doesn't work for cursor movement. Wrap in `setTimeout(..., 0)` to break out of the intercept stack. - `registerLayer` with `activeWhen` using SolidJS signals requires `reactiveMatcherFromSignal` from `@opentui/keymap/solid`. Plain `() => signal()` doesn't trigger re-evaluation. We chose intercepts instead of layers to avoid this. -- **Leader key is handled entirely within the keymap's `dispatchLayers()`.** There is no separate `useKeyboard` handler for it. `registerTimedLeader` registers a token; `dispatchLayers()` matches it; `getPendingSequence()` exposes the state. Calling `ctx.consume()` in a `key` intercept sets `event.propagationStopped`, which the keymap checks after each intercept — if set, it skips `dispatchLayers()` entirely. This is the correct way to suppress the leader in insert mode. +- **Leader key is handled entirely within the keymap's `dispatchLayers()`.** There is no separate `useKeyboard` handler for it. `registerTimedLeader` registers a token; `dispatchLayers()` matches it; `getPendingSequence()` exposes the state. Calling `ctx.consume()` in a `key` intercept sets `event.propagationStopped`, which the keymap checks after each intercept — if set, it skips `dispatchLayers()` entirely. In insert mode, printable leaders are consumed and inserted as text; non-printable leaders (ctrl+x, etc.) are not consumed, so they fall through to `dispatchLayers()` and trigger OpenCode's leader bindings. - **`api.tuiConfig.keybinds`** gives access to OpenCode's resolved keybind config. `api.tuiConfig.keybinds.get("leader")?.[0]?.key` returns the configured leader key. Used by `resolveLeader()` to auto-detect the leader without requiring a separate plugin option. - **SolidJS imports do NOT work in git-installed plugins.** The host's `ensureRuntimePluginSupport` intercepts `solid-js` and `@opentui/solid` imports, but `@opentui/solid/jsx-dev-runtime` (generated by the JSX transform) doesn't resolve from the package cache. Declaring them as peer deps also fails — Bun installs local `.d.ts` stubs that shadow the host's runtime modules. Until OpenCode fixes this, avoid JSX and `solid-js` imports in distributed plugins. Use `api.ui.toast()` for mode feedback instead of slot indicators. - **Do NOT add `solid-js`, `@opentui/solid`, or `@opentui/core` as dependencies or peerDependencies.** If they're in `package.json`, Bun installs them into the plugin's `node_modules/`, and the local `.d.ts` stubs shadow the host's runtime module intercepts. The host provides these at runtime via `ensureRuntimePluginSupport`. Keep them only in `devDependencies` (via `@opencode-ai/plugin` which pulls them in for type-checking). diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c86535..ceb6b02 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Version ### Fixed +- Non-printable leader keys (e.g. `ctrl+x`) now work in insert mode instead of being swallowed ([#42](https://github.com/oribarilan/vimcode/issues/42)). - Escape from insert mode now moves cursor left, matching vim. - Clipboard now uses `wl-copy` on Wayland instead of `xclip`, which doesn't work there. diff --git a/README.md b/README.md index 017ded5..cb4ca57 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ vimcode reads OpenCode's leader key from your `tui.json` keybinds and handles it In **normal and visual mode**, the leader key and the follow-up key pass straight through to OpenCode, so leader shortcuts (`c` for copy, etc.) work as expected. -In **insert mode**, vimcode intercepts the leader key to prevent the leader menu from popping up while you type. If the leader is a printable key like space, the character still gets inserted normally. +In **insert mode**, printable leaders (like space) insert their character. Non-printable leaders (like `ctrl+x`) pass through to OpenCode, so leader shortcuts work from any mode. This allows, for example, to use the popular vim-style `space` leader, set it in your `tui.json`: diff --git a/src/index.ts b/src/index.ts index b5c6806..ff0cedd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -312,10 +312,12 @@ const plugin: TuiPluginModule = { : handleNormalKey(state, key, ctx.event, prompt); if (handlerMode === "normal") finishOneShotIfComplete(state, result); - // In insert mode, if the handler didn't consume the key, check - // if it's a leader key. Swallow it to prevent the leader menu - // from popping up while typing. This runs after handleInsertKey - // so explicit handlers (escape, return, tab, ctrl+o) take priority. + // In insert mode, intercept printable leaders (space, "a") so + // they insert their character instead of triggering the leader + // menu mid-typing. Non-printable leaders (ctrl+x, alt+m) fall + // through to dispatchLayers() so app-level shortcuts work + // without switching modes. Runs after handleInsertKey so + // explicit handlers (escape, return, tab, ctrl+o) take priority. // Don't mutate `result` — it may be the shared PASS constant. let consume = result.consume; let actions = result.actions; @@ -323,8 +325,10 @@ const plugin: TuiPluginModule = { const matched = findMatchingLeader(ctx.event, leaderKeys); if (matched) { const ch = leaderChar(matched); - actions = ch ? [{ type: "insertText" as const, text: ch }] : []; - consume = true; + if (ch) { + actions = [{ type: "insertText" as const, text: ch }]; + consume = true; + } } }