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
11 changes: 6 additions & 5 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ vimcode is a TUI plugin for [OpenCode](https://opencode.ai). Before working on i
- **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/JSX still does not work in cache-installed plugins.** Last reproduced on 2026-09-08 with OpenCode 1.18.21 using an npm-source tarball. A plain `.ts` entry and its TUI hook loaded, but importing a `.tsx` module with `/** @jsxImportSource @opentui/solid */` failed with `Cannot find module '@opentui/solid/jsx-dev-runtime'`. The Solid transform excludes files under `node_modules`; the runtime prescan therefore cannot see the JSX-generated import before Bun resolves it. OpenCode 1.18.25 has identical relevant runtime code and also pins OpenTUI 0.4.5. OpenTUI 0.5.9 retains the exclusion. Until upstream changes this path, avoid JSX and `solid-js` imports in distributed plugins. Use `api.ui.toast()` for mode feedback instead of slot indicators. See [#3](https://github.com/oribarilan/vimcode/issues/3).
- **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).
- **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 host UI packages dev-only. Optional peers of `@opencode-ai/plugin` are not installed by a clean Bun install; declare test dependencies explicitly. `@opentui/keymap` is pinned in devDependencies for headless keymap tests.
- **Test distributed plugin behavior through the package cache.** `dev-tui.json` uses `"plugin": ["."]`, which loads from the working tree and does not reproduce cache-only module resolution failures. Use an npm-source tarball spec such as `name@file:/absolute/path/package.tgz` or the real `git+https://...#ref` install form, and clear only that package's cache entry before retesting.

### Editor widget API
Expand Down Expand Up @@ -58,9 +58,9 @@ This API surface makes text objects (`ciw`, `di"`), direct cursor manipulation,

```
src/
index.ts (474 lines) Dual v1 tui/v2 setup entry: intercept registration, action application
index.ts (477 lines) Dual v1 tui/v2 setup entry: intercept registration, action application
editor.ts (28 lines) Host-coordinate horizontal selection bounds, preserving native anchor
v2.ts (243 lines) Experimental v2 TUI facade (host input, commands, state, events)
v2.ts (246 lines) Experimental v2 TUI facade (host input, commands, state, events)
vim/ Pure vim engine (thin barrel re-exports the public surface):
index.ts (7 lines) Barrel — public surface only. No export *, no internals.
types.ts (57 lines) Action union, VimState, Mode, Operator, Pending, Range, KeyEvent, HandlerResult, PromptAccess
Expand Down Expand Up @@ -88,7 +88,8 @@ test/
textobject.test.ts (64) resolveTextObject dispatch seam
integration.test.ts (662) Full pipeline: one-shot normal, plugin init, undo snapshots, version sync, prompt overlay tracking
editor.test.ts (118) Host-coordinate boundaries and opaque selection-color forwarding
v2.test.ts (369) Experimental v2 facade contract and lifecycle tests
v2.test.ts (428) Experimental v2 facade contract and lifecycle tests
child-session-navigation.test.ts (267) #79: isolated intercept and real OpenTUI keymap navigation regressions
leader.test.ts (125 lines) Unit tests for leader key matching functions
compat/ Optional real-host Python driver and test-only TUI fixture (not packaged)
```
Expand Down Expand Up @@ -168,7 +169,7 @@ just compat-unit # Pure checks for the optional host harness
just compat v1 /absolute/opencode 1.18.33 /canonical/empty/output # Isolated installed-artifact check
```

The `dev-tui.json` config is picked up only by `just dev`. Running `opencode` normally in this directory does not load the plugin. `just dev2` runs `scripts/dev2.ts` with the tested v2 package (or an explicit binary), loads local source through the root `tui.ts` shim, and isolates on-disk settings/history under `.dev2/`. The shim/launcher are not distributed; package installs still resolve `exports["./tui"]`. Visual mode captures the focused editor on entry and re-anchors at the new editor's cursor on the next eligible key after a prompt switch; overlay keys do not change ownership. Horizontal normalization probes `editBuffer.getTextRange()` in host display-cell coordinates and uses lower `editorView` selection calls to retain the renderer's native anchor. The v2 disabled setting is per activation: external storage reconciliation applies on reload, while local `/vim` updates both controller and form guard immediately. `just compat-unit` runs in CI; live-host checks remain optional.
The `dev-tui.json` config is picked up only by `just dev`. Running `opencode` normally in this directory does not load the plugin. `just dev2` runs `scripts/dev2.ts` with the tested v2 package (or an explicit binary), loads local source through the root `tui.ts` shim, and isolates on-disk settings/history under `.dev2/`. The shim/launcher are not distributed; package installs still resolve `exports["./tui"]`. Both dev commands retain arrow navigation and add `ctrl+x j` to open children/picker; v1 uses `h`/`l` to cycle and `k` to return, while v2 uses `h`/`k` and `j`/`l` in the Composer. Visual mode captures the focused editor on entry and re-anchors at the new editor's cursor on the next eligible key after a prompt switch; overlay keys do not change ownership. Horizontal normalization probes `editBuffer.getTextRange()` in host display-cell coordinates and uses lower `editorView` selection calls to retain the renderer's native anchor. The v2 disabled setting is per activation: external storage reconciliation applies on reload, while local `/vim` updates both controller and form guard immediately. `just compat-unit` runs in CI; live-host checks remain optional.

The experimental v2 adapter uses a raw renderer key listener because v2's public keymap does not expose intercepts; see `docs/opencode-v2-poc.md` for configuration and `docs/opencode-v2-strategy.md` for tested alternatives and remaining compatibility gaps.

Expand Down
15 changes: 14 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,18 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Version

## [Unreleased]

## [0.19.1] — 2026-10-04

### Changed

- Added Vim-style subagent-navigation aliases to `just dev` and `just dev2` alongside the arrow bindings for manual checks.

### Fixed

- Remapped navigation keys now reach OpenCode in read-only child-session views without changing the Vim mode ([#79](https://github.com/oribarilan/vimcode/issues/79)).
- Corrected v2 root-session metadata so root prompts continue receiving Vim commands.
- Declared the OpenTUI keymap dev dependency so navigation regression tests run from clean installs.

## [0.19.0] — 2026-10-02

### Added
Expand Down Expand Up @@ -369,7 +381,8 @@ First release. Modal editing for the OpenCode prompt.

> `g` fires immediately as buffer-home instead of waiting for `gg`. The `yy` line tracker drifts on clicks and arrow keys. Visual mode and text objects aren't feasible without cursor position access.

[Unreleased]: https://github.com/oribarilan/vimcode/compare/v0.19.0...HEAD
[Unreleased]: https://github.com/oribarilan/vimcode/compare/v0.19.1...HEAD
[0.19.1]: https://github.com/oribarilan/vimcode/compare/v0.19.0...v0.19.1
[0.19.0]: https://github.com/oribarilan/vimcode/compare/v0.18.1...v0.19.0
[0.18.1]: https://github.com/oribarilan/vimcode/compare/v0.18.0...v0.18.1
[0.18.0]: https://github.com/oribarilan/vimcode/compare/v0.17.1...v0.18.0
Expand Down
8 changes: 4 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ Follow [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Add your change

## Host support policy

OpenCode v2 support is rolling out gradually and is experimental in vimcode v0.19.0. Future maintenance will move to v2 only; v1 compatibility remains in this release. The recommended v1 pin stays at `v0.18.1`. Do not automatically advance that legacy recommendation when preparing a v2-focused release.
OpenCode v2 support is rolling out gradually and remains experimental in vimcode v0.19.1. Future maintenance will move to v2 only; v1 compatibility remains in this release. The recommended v1 pin stays at `v0.18.1`. Do not automatically advance that legacy recommendation when preparing a v2-focused release.

## Release process

Expand All @@ -83,19 +83,19 @@ Releases are manual.

Both host versions load the same `./tui` package entry via a Git URL. Pin a tag or commit so upgrades use a new cache entry.

On OpenCode **v1**, use `tui.json`. The recommended legacy pin is `v0.18.1`; `v0.19.0` still includes v1 compatibility for users who opt in:
On OpenCode **v1**, use `tui.json`. The recommended legacy pin is `v0.18.1`; `v0.19.1` includes v1 compatibility and the subagent-navigation fix for users who opt in:

```json
{ "plugin": ["vimcode@git+https://github.com/oribarilan/vimcode.git#v0.18.1"] }
```

On OpenCode **v2**, use `cli.json` and pin `v0.19.0`. v2 support is experimental; the v1-only `v0.18.1` release does not work on v2:
On OpenCode **v2**, use `cli.json` and pin `v0.19.1`. v2 support is experimental; the v1-only `v0.18.1` release does not work on v2:

```json
{
"plugins": [
{
"package": "vimcode@git+https://github.com/oribarilan/vimcode.git#v0.19.0"
"package": "vimcode@git+https://github.com/oribarilan/vimcode.git#v0.19.1"
}
]
}
Expand Down
18 changes: 11 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ OpenCode v2 support is rolling out gradually, starting with experimental support

| OpenCode version | Recommended vimcode pin | Status |
|------------------|-------------------------|--------|
| v1 | `v0.18.1` | Previous v1-only release. `v0.19.0` also includes v1 compatibility during the transition. |
| v2 | `v0.19.0` | Experimental; validated on OpenCode 2.0.15 on macOS. |
| v1 | `v0.18.1` | Previous v1-only release. Opt into `v0.19.1` for the subagent-navigation fix and shared v1/v2 updates. |
| v2 | `v0.19.1` | Experimental; validated on OpenCode 2.0.15 on macOS. |

V1 users can stay on an older pinned release instead of following the v2 rollout. We have not set a date for removing v1 support.

Expand All @@ -38,7 +38,7 @@ Use the instructions for your OpenCode major version. Both versions use the same

### OpenCode v1

We recommend pinning `v0.18.1` for existing v1 users who want the previous v1-only behavior. To opt into the shared fixes in `v0.19.0`, change the Git ref to `v0.19.0`; revert to the older pin if you encounter a regression.
We recommend pinning `v0.18.1` for existing v1 users who want the previous v1-only behavior. To get the subagent-navigation fix and shared updates in `v0.19.1`, change the Git ref to `v0.19.1`; revert to the older pin if you encounter a regression.

Add to your `tui.json` (or `.opencode/tui.json`):

Expand All @@ -54,13 +54,13 @@ By default, you'll see a toast when a newer vimcode version is available. If you

### OpenCode v2 (experimental)

Pin `v0.19.0` in your global `cli.json`. The older `v0.18.1` release does **not** support v2.
Pin `v0.19.1` in your global `cli.json`. The older `v0.18.1` release does **not** support v2.

```json
{
"plugins": [
{
"package": "vimcode@git+https://github.com/oribarilan/vimcode.git#v0.19.0"
"package": "vimcode@git+https://github.com/oribarilan/vimcode.git#v0.19.1"
}
]
}
Expand Down Expand Up @@ -88,7 +88,7 @@ Put `options` alongside `package` in `cli.json`:
{
"plugins": [
{
"package": "vimcode@git+https://github.com/oribarilan/vimcode.git#v0.19.0",
"package": "vimcode@git+https://github.com/oribarilan/vimcode.git#v0.19.1",
"options": { "updateCheck": false }
}
]
Expand Down Expand Up @@ -116,6 +116,10 @@ In normal mode, keys are vim commands. Unrecognized keys get swallowed so you do

When OpenCode shows its own UI (command palette, `/sessions`, the `@` file picker, question prompts, permission prompts) vimcode steps aside. All keys pass through to the overlay until it closes.

### Subagent navigation

Read-only child-session views pass keys to OpenCode without changing your Vim mode. On v2, the Composer also owns its navigation keys. Returning to the parent prompt preserves the mode you were using.

### Escape behavior

First Escape in insert mode switches to normal - it won't trigger OpenCode's double-escape interrupt. So canceling a running response from insert mode takes 3 escapes: one for normal, two more for the interrupt.
Expand Down Expand Up @@ -145,7 +149,7 @@ On **v2**, set both the host keybind and the plugin option in `cli.json`:
"keybinds": { "leader": "space" },
"plugins": [
{
"package": "vimcode@git+https://github.com/oribarilan/vimcode.git#v0.19.0",
"package": "vimcode@git+https://github.com/oribarilan/vimcode.git#v0.19.1",
"options": { "experimentalV2Leader": "space" }
}
]
Expand Down
6 changes: 5 additions & 1 deletion dev-tui.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
{
"plugin": ["."],
"keybinds": {
"leader": "ctrl+x"
"leader": "ctrl+x",
"session_child_first": "<leader>down,<leader>j",
"session_parent": "up,k",
"session_child_cycle": "right,l",
"session_child_cycle_reverse": "left,h"
}
}
16 changes: 13 additions & 3 deletions docs/opencode-v2-poc.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
# Experimental OpenCode v2 POC

These notes record the v2 proof of concept and compatibility evidence. v2 support is experimental in vimcode v0.19.0; this is not a full-parity or all-versions support declaration. The same `./tui` entry has v1 `tui(api, options)` and v2 `setup(context)` callbacks. v1 still uses its existing API; v2 adapts the host UI, storage, commands and events to the same Vim controller and engine.
These notes record the v2 proof of concept and compatibility evidence. v2 support is experimental in vimcode v0.19.1; this is not a full-parity or all-versions support declaration. The same `./tui` entry has v1 `tui(api, options)` and v2 `setup(context)` callbacks. v1 still uses its existing API; v2 adapts the host UI, storage, commands and events to the same Vim controller and engine.

## Local development

Run `just dev2` from this worktree. It uses npm's package runner (`npx`) to download/cache the tested `@opencode/cli@2.0.15` release, then launches a standalone v2 instance with the local plugin. It does not replace your installed `opencode` or change `just dev`.

Both `just dev` (v1) and `just dev2` (v2) include subagent-navigation aliases for manual checks, alongside the native arrow bindings. `ctrl+x j` opens the child view/picker. On v1, `h`/`l` cycles children and `k` returns to the parent. On v2, `h`/`k` moves up, `j`/`l` moves down, Enter selects a child, and Escape closes the Composer. At the first picker row, `h`/`k` also closes it. No config editing or separate test launcher is needed.

Settings, credentials, cache and history are stored separately under the ignored `.dev2/` directory. Existing provider environment variables and project config still apply; otherwise connect a provider inside the dev instance. The launcher clears inherited OpenCode config overrides and fixes the dev leader to `ctrl+x`.

To use an existing v2 binary instead:
Expand All @@ -21,15 +23,15 @@ The root `tui.ts` re-exports `src/index.ts` for v2's local-directory loader. Thi

The v2 public `keymap` has no key intercept or configured-leader lookup. The POC uses `context.renderer.keyInput.prependListener("keypress", ...)` to intercept prompt keys **before** the host keymap. It calls both `preventDefault()` and `stopPropagation()` on consumed keys. This raw OpenTUI ordering is not a documented OpenCode plugin guarantee; do not rely on it as production compatibility without testing against installed packages and future host releases.

The [README](../README.md#opencode-v2-experimental) shows the release Git install pinned to `v0.19.0`. The earlier Git experiment below used commit `d8050f765b6c2c4e7fdc700d8345c1c5752644cb`. Use the global `cli.json` on v2, not v1's `tui.json`.
The [README](../README.md#opencode-v2-experimental) shows the release Git install pinned to `v0.19.1`. The earlier Git experiment below used commit `d8050f765b6c2c4e7fdc700d8345c1c5752644cb`. Use the global `cli.json` on v2, not v1's `tui.json`.

To test local changes instead, create a fresh artifact directory and build a tarball with `npm pack --ignore-scripts --pack-destination /absolute/path/to/artifacts`. Configure that artifact in `cli.json`:

```json
{
"plugins": [
{
"package": "vimcode@file:/absolute/path/to/artifacts/vimcode-0.19.0.tgz",
"package": "vimcode@file:/absolute/path/to/artifacts/vimcode-0.19.1.tgz",
"options": { "updateCheck": false, "experimentalV2Leader": "ctrl+x" }
}
]
Expand Down Expand Up @@ -58,6 +60,14 @@ The same tarball was exercised in real macOS tmux sessions on OpenCode **2.0.15*

Earlier parent receipts are `/private/tmp/vimcode-v2-hardening/review-fixed-v1/receipt.json` and `review-fixed-v2/receipt.json`. Both verify artifact SHA-256 `8e43ee3812ecf2baf325fcc97b4e73d6e49ea116ed719fb2e7e12dbab218f7f0`. Earlier exploratory evidence remains under `/tmp/vimcode-v2-poc-runtime/` and `/tmp/vimcode-v2-alternatives/`.

## Issue #79 follow-up (2026-10-04)

The child-session passthrough guard now lives once in the shared controller. The v2 facade distinguishes root sessions from children before supplying `parentID`; otherwise the shared guard would incorrectly bypass root editing. The Vim engine is unchanged. Composer passthrough already existed and is now covered explicitly for normal, visual and insert modes.

Fresh installed-artifact runs passed cold and warm loading on v1.18.33 (39 checks) and v2.0.15 (48 checks), with the same inherited tab-offset and snapshot-redo known gaps and no failures. Receipts: `/private/tmp/vimcode-79-dual-v1-live/receipt.json` and `/private/tmp/vimcode-79-dual-v2-live/receipt.json`.

A separate real v2 navigation smoke passed nine checks using synthetic child sessions, remapped Composer keys, and exact route/editor snapshots. It verified `h`/`l` picker movement, `k` returning to the parent without `i`, and normal editing afterward. This used v2's `composer.subagent.up` (`h`, `k`), `composer.subagent.down` (`l`) and `composer.subagent.select` (`return`), not v1's sibling-cycle config names. The temporary fixture/driver is outside the checkout; receipt: `/private/tmp/vimcode-79-dual-v2-navigation-corrected/receipt.json`.

## Before claiming support

Follow-up live tests covered root/child forms and permission rejection. They reproduced a host space-leader bug inside textual forms, including without vimcode installed. The v2 adapter now protects printable leader characters in focused forms, and the tested multiword root/child answers submitted correctly. The guard does not interpret other form keys as Vim commands and stays inactive when vimcode is disabled.
Expand Down
Loading
Loading