Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 6 additions & 4 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,15 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Version

### Added

- `/vim` slash command to toggle vim mode on/off. State is persisted via `api.kv` so the preference survives restarts. When disabled, all keys pass through unmodified.
- `/vim` toggle command to disable/enable vim mode. Persisted across restarts.
- Startup toast when vim is disabled so users know why keybindings aren't active.

### Fixed

- `api.kv.set` call in `/vim` toggle now uses optional chaining (`api.kv?.set?.()`) to avoid crashing on OpenCode versions without `api.kv`.
- Disabled check moved before autocomplete handling so Escape/Enter are not intercepted when vim is disabled.
- Toggling vim off now resets to insert mode and clears pending operator/char/count state, preventing stale state on re-enable and fixing cursor style.
- Optional chaining on `api.kv.set` to avoid crashes on older OpenCode versions.
- Escape/Enter no longer intercepted when vim is disabled.
- Toggling vim off resets mode and clears pending state.
- `:vim` palette title uses `:` prefix, consistent with `:q`/`:quit`/`:wq`.

## [0.13.0] — 2026-06-09

Expand Down
51 changes: 17 additions & 34 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ const plugin: TuiPluginModule = {
// Load persisted disabled state
const persistedDisabled = (await api.kv?.get?.("vimcode.disabled")) as boolean | undefined;
state.disabled = persistedDisabled ?? false;
if (state.disabled) {
api.ui?.toast?.({ message: "Vim mode disabled (use /vim to re-enable)", variant: "info", duration: 3000 });
}

// Track whether the previous key was the leader, so the follow-up
// key also passes through to OpenCode's leader system.
Expand Down Expand Up @@ -200,44 +203,24 @@ const plugin: TuiPluginModule = {
// Register all commands via registerLayer (migrated from the deprecated
// api.command?.register API). Commands appear in the command palette and
// are accessible as slash commands.
const exitRun = async () => {
setTimeout(() => api.keymap.dispatchCommand("app.exit"), 0);
};
const exitCommands = ["q", "quit", "wq"].map((cmd) => ({
name: `vimcode.${cmd}`,
title: `:${cmd}`,
category: "Vim",
namespace: "palette",
desc: cmd === "wq" ? "Exit OpenCode (write and quit)" : "Exit OpenCode",
slashName: cmd,
run: exitRun,
}));
api.keymap.registerLayer?.({
commands: [
{
name: "vimcode.q",
title: ":q",
category: "Vim",
namespace: "palette",
desc: "Exit OpenCode",
slashName: "q",
run: async () => {
setTimeout(() => api.keymap.dispatchCommand("app.exit"), 0);
},
},
{
name: "vimcode.quit",
title: ":quit",
category: "Vim",
namespace: "palette",
desc: "Exit OpenCode",
slashName: "quit",
run: async () => {
setTimeout(() => api.keymap.dispatchCommand("app.exit"), 0);
},
},
{
name: "vimcode.wq",
title: ":wq",
category: "Vim",
namespace: "palette",
desc: "Exit OpenCode (write and quit)",
slashName: "wq",
run: async () => {
setTimeout(() => api.keymap.dispatchCommand("app.exit"), 0);
},
},
...exitCommands,
{
name: "vimcode.vim",
title: "/vim",
title: ":vim",
category: "Vim",
namespace: "palette",
desc: "Toggle vim mode on/off",
Expand Down
Loading