From 773ee647d393b1c2fd04a296636e536677e08383 Mon Sep 17 00:00:00 2001 From: ori Date: Mon, 8 Jun 2026 18:00:15 +0300 Subject: [PATCH] fix: revert slot-based mode indicator, back to toast The host's JSX runtime doesn't resolve from git-installed plugins. Keeps the (insert) mode type fix and modeIndicator option (toast/none). --- AGENTS.md | 6 ++--- CHANGELOG.md | 14 ++++++++--- README.md | 9 ++++--- bunfig.toml | 2 -- package.json | 4 +-- src/{index.tsx => index.ts} | 50 +++++-------------------------------- src/version.ts | 2 +- test/preload.ts | 34 ------------------------- tsconfig.json | 3 --- 9 files changed, 27 insertions(+), 97 deletions(-) delete mode 100644 bunfig.toml rename src/{index.tsx => index.ts} (83%) delete mode 100644 test/preload.ts diff --git a/AGENTS.md b/AGENTS.md index f81233d..cc83cfe 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,10 +15,10 @@ vimcode is a TUI plugin for [OpenCode](https://opencode.ai). Before working on i **Gotchas we hit during development:** - TUI plugins go in `tui.json`, not `opencode.json`. The config field is `"plugin"`. -- The plugin `package.json` needs `exports: { "./tui": "./src/index.tsx" }` — the loader checks `./tui`, not `.`. +- The plugin `package.json` needs `exports: { "./tui": "./src/index.ts" }` — the loader checks `./tui`, not `.`. - `dispatchCommand()` from inside a `key:before` 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. -- **SolidJS imports work in distributed plugins** as of mid-2026. The plugin uses top-level `import { createSignal } from "solid-js"` and `@jsxImportSource @opentui/solid`. This requires `"type": "module"` in package.json and `solid-js`/`@opentui/solid`/`@opentui/core` as peer dependencies. Tests need a preload (`test/preload.ts`) to mock the JSX runtime since it's only available inside the OpenCode host. +- **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). ### Editor widget API @@ -54,7 +54,7 @@ This API surface makes text objects (`ciw`, `di"`), direct cursor manipulation, ``` src/ - index.tsx (291 lines) Plugin entry: intercept registration, action application, slot UI + index.ts (242 lines) Plugin entry: intercept registration, action application vim.ts (631 lines) Pure vim engine: state, handlers, command tables, types clipboard.ts (19 lines) writeClipboard() — cross-platform (pbcopy/xclip/xsel/wl-copy/clip.exe) version.ts (46 lines) Version constant, GitHub update check (cached daily) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e85d1e..d3a3c75 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,11 +8,16 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/). Version ## [Unreleased] -## [0.12.1] — 2026-06-08 +## [0.12.2] — 2026-06-08 -### Fixed +### Reverted + +- Persistent mode indicator removed. The SolidJS slot approach doesn't work from git-installed plugins — the host's JSX runtime can't be resolved from the package cache. Back to toast-only for now ([#3](https://github.com/oribarilan/vimcode/issues/3)). + +### Changed -- Plugin failed to load when installed via git URL. Peer dependencies (`solid-js`, `@opentui/solid`, `@opentui/core`) were getting installed into the plugin's `node_modules/`, and their `.d.ts` stubs shadowed the host's runtime modules. Removed peer deps — the host provides these at runtime. +- `modeIndicator` option now accepts `"toast"` (default) or `"none"`. The `"status"` value from v0.12.0 is gone until the host resolves JSX for external plugins. +- `Ctrl+O` one-shot normal now emits a proper `mode` action instead of a toast side-channel. The toast shows `(insert)` in lowercase, matching Vim convention. ## [0.12.0] — 2026-06-08 @@ -248,7 +253,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.12.1...HEAD +[Unreleased]: https://github.com/oribarilan/vimcode/compare/v0.12.2...HEAD +[0.12.2]: https://github.com/oribarilan/vimcode/compare/v0.12.1...v0.12.2 [0.12.1]: https://github.com/oribarilan/vimcode/compare/v0.12.0...v0.12.1 [0.12.0]: https://github.com/oribarilan/vimcode/compare/v0.11.0...v0.12.0 [0.11.0]: https://github.com/oribarilan/vimcode/compare/v0.10.0...v0.11.0 diff --git a/README.md b/README.md index 8dba079..9eaf811 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Add to your `tui.json` (or `.opencode/tui.json`): ```json { - "plugin": ["vimcode@git+https://github.com/oribarilan/vimcode.git#v0.12.1"] + "plugin": ["vimcode@git+https://github.com/oribarilan/vimcode.git#v0.12.2"] } ``` @@ -40,20 +40,20 @@ To pass options, use the tuple form in `tui.json`: ```json { - "plugin": [["vimcode@git+https://github.com/oribarilan/vimcode.git#v0.12.1", { "updateCheck": false }]] + "plugin": [["vimcode@git+https://github.com/oribarilan/vimcode.git#v0.12.2", { "updateCheck": false }]] } ``` | Option | Type | Default | Description | |--------|------|---------|-------------| | `updateCheck` | `boolean` | `true` | On startup, check GitHub for new versions (at most once per day). This is the only network request vimcode makes. Set to `false` to disable. | -| `modeIndicator` | `"status"` \| `"toast"` \| `"none"` | `"status"` | How to show the current mode. `"status"` shows a persistent label next to the prompt. `"toast"` flashes a brief notification on each switch (old behavior). `"none"` disables it, relying on cursor shape alone. | +| `modeIndicator` | `"toast"` \| `"none"` | `"toast"` | How to show the current mode. `"toast"` flashes a brief notification on each switch. `"none"` disables it, relying on cursor shape alone. | | `startMode` | `"insert"` \| `"normal"` | `"insert"` | Which mode to start in when OpenCode launches. | | `leader` | `string` | — | Set this to match your `"leader"` keybind in `tui.json`. If you use space as leader (common in vim), this keeps spaces working while you type and lets leader sequences like `space l` fire in normal mode. Supports modifiers too: `"C-x"`, `"S-a"`, `"M-x"`. | ## What it does -Adds normal/insert mode to OpenCode's prompt input. Escape enters normal mode, `i` goes back to insert. The current mode shows as a persistent label next to the prompt (configurable). +Adds normal/insert mode to OpenCode's prompt input. Escape enters normal mode, `i` goes back to insert. A brief toast shows the current mode on each switch (configurable). In insert mode, typing works normally. Enter adds a newline, Ctrl+Enter submits. The file picker and autocomplete keep working: Enter picks the selected item, Escape closes the picker without leaving insert. @@ -163,6 +163,7 @@ All normal-mode motions work for extending the selection: `h` `j` `k` `l` `w` `b - `V`, `Ctrl+v` - only character-wise visual mode (`v`) is supported, no line-wise or block - `ciw`, `di"`, etc. (text objects) - not yet implemented +- No persistent mode indicator - the toast fades after about a second. A slot-based indicator needs the host's JSX runtime, which doesn't resolve reliably from git-installed plugins ([#3](https://github.com/oribarilan/vimcode/issues/3)). Configurable key bindings are next once the core vim coverage stabilizes. diff --git a/bunfig.toml b/bunfig.toml deleted file mode 100644 index 786a377..0000000 --- a/bunfig.toml +++ /dev/null @@ -1,2 +0,0 @@ -[test] -preload = ["./test/preload.ts"] diff --git a/package.json b/package.json index 3b0bf8a..1ae62ba 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "vimcode", - "version": "0.12.1", + "version": "0.12.2", "description": "Vim keybindings for the OpenCode prompt", "author": "Ori Bar-ilan", "license": "MIT", @@ -16,7 +16,7 @@ "plugin" ], "exports": { - "./tui": "./src/index.tsx" + "./tui": "./src/index.ts" }, "files": [ "src/" diff --git a/src/index.tsx b/src/index.ts similarity index 83% rename from src/index.tsx rename to src/index.ts index bf4310d..38099f5 100644 --- a/src/index.tsx +++ b/src/index.ts @@ -1,7 +1,4 @@ -// @ts-nocheck -/** @jsxImportSource @opentui/solid */ import type { TuiPluginModule } from "@opencode-ai/plugin/tui"; -import { createSignal } from "solid-js"; import { writeClipboard } from "./clipboard"; import { checkForUpdate } from "./version"; import { @@ -11,7 +8,6 @@ import { handleInsertKey, handleNormalKey, handleVisualKey, - type Mode, matchesLeader, parseLeaderKey, translateKey, @@ -25,49 +21,15 @@ const plugin: TuiPluginModule = { state.mode = startMode; const leader = options?.leader ? parseLeaderKey(options.leader) : null; - // Resolve modeIndicator: "status" (default), "toast", or "none". + // Resolve modeIndicator: "toast" (default) or "none". // Backward compat: modeToast:false maps to "none", but only if // modeIndicator isn't explicitly set. - const modeIndicator: "status" | "toast" | "none" = - options?.modeIndicator === "status" || options?.modeIndicator === "toast" || options?.modeIndicator === "none" + const modeIndicator: "toast" | "none" = + options?.modeIndicator === "toast" || options?.modeIndicator === "none" ? options.modeIndicator : options?.modeToast === false ? "none" - : "status"; - - // Reactive signal for mode — drives the badge indicator. - const [mode, setMode] = createSignal(startMode); - - if (modeIndicator === "status") { - api.slots?.register?.({ - slots: { - // biome-ignore lint/suspicious/noExplicitAny: slot context type comes from host - session_prompt_right(ctx: any) { - const m = mode(); - const color = - m === "normal" - ? ctx.theme.current.warning - : m === "visual" - ? ctx.theme.current.primary - : ctx.theme.current.textMuted; - const label = m === "(insert)" ? m : m.toUpperCase(); - return {label}; - }, - // biome-ignore lint/suspicious/noExplicitAny: slot context type comes from host - home_prompt_right(ctx: any) { - const m = mode(); - const color = - m === "normal" - ? ctx.theme.current.warning - : m === "visual" - ? ctx.theme.current.primary - : ctx.theme.current.textMuted; - const label = m === "(insert)" ? m : m.toUpperCase(); - return {label}; - }, - }, - }); - } + : "toast"; // Track whether the previous key was the leader, so the follow-up // key also passes through to OpenCode's leader system. @@ -105,9 +67,9 @@ const plugin: TuiPluginModule = { setTimeout(() => api.keymap.dispatchCommand(action.cmd), 0); break; case "mode": - setMode(action.mode); if (modeIndicator === "toast") { - api.ui?.toast?.({ message: action.mode.toUpperCase(), variant: "info", duration: 800 }); + const label = action.mode === "(insert)" ? action.mode : action.mode.toUpperCase(); + api.ui?.toast?.({ message: label, variant: "info", duration: 800 }); } break; case "toast": diff --git a/src/version.ts b/src/version.ts index c658d62..a5781f1 100644 --- a/src/version.ts +++ b/src/version.ts @@ -1,5 +1,5 @@ // Keep in sync with package.json on each release. -export const VERSION = "0.12.1"; +export const VERSION = "0.12.2"; // GitHub API returns fresh content immediately; raw.githubusercontent.com // is CDN-cached for up to 5 minutes which delays update detection. diff --git a/test/preload.ts b/test/preload.ts deleted file mode 100644 index befc928..0000000 --- a/test/preload.ts +++ /dev/null @@ -1,34 +0,0 @@ -// Preload: mock the @opentui/solid JSX runtime for tests. -// The real runtime is injected by the OpenCode host at runtime. -import { mock } from "bun:test"; - -mock.module("@opentui/solid/jsx-runtime", () => ({ - jsx: (_type: string, props: Record) => ({ type: _type, props }), - jsxs: (_type: string, props: Record) => ({ type: _type, props }), - jsxDEV: (_type: string, props: Record) => ({ type: _type, props }), - Fragment: (props: { children?: unknown }) => props.children, -})); - -mock.module("@opentui/solid/jsx-dev-runtime", () => ({ - jsx: (_type: string, props: Record) => ({ type: _type, props }), - jsxs: (_type: string, props: Record) => ({ type: _type, props }), - jsxDEV: (_type: string, props: Record) => ({ type: _type, props }), - Fragment: (props: { children?: unknown }) => props.children, -})); - -mock.module("solid-js", () => ({ - createSignal: (init: T) => { - let value = init; - const getter = () => value; - const setter = (v: T | ((prev: T) => T)) => { - value = typeof v === "function" ? (v as (prev: T) => T)(value) : v; - }; - return [getter, setter] as const; - }, - createMemo: (fn: () => T) => fn, - createEffect: () => {}, - onCleanup: () => {}, - onMount: (fn: () => void) => fn(), - Show: (props: { when: unknown; children: unknown }) => props.children, - For: (props: { each: unknown[]; children: unknown }) => props.children, -})); diff --git a/tsconfig.json b/tsconfig.json index ea941d6..a69f561 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,9 +4,6 @@ "module": "ESNext", "moduleResolution": "bundler", - "jsx": "preserve", - "jsxImportSource": "@opentui/solid", - "strict": true, "esModuleInterop": true, "skipLibCheck": true,