Repository navigation
Description
Summary
TUI plugins published as npm packages (installed via opencode plugin add) load and paint their initial frame, but plugin-driven reactive updates never reach the screen — the widget stays frozen forever. Root cause: inside the plugin install's node_modules, Bun resolves bare specifiers (solid-js, @opentui/solid, and the JSX-runtime import generated by the tsx transform) against the package's own copies instead of the host's runtime modules, creating a second Solid runtime whose signals the host renderer never observes. No package.json configuration works: with the peer deps from the publish docs the UI freezes; without them the plugin fails to load.
Environment
- opencode version: 2.0.3
- OS: Linux 6.12.107+deb13-amd64 (x64)
- Terminal: TERM=xterm-256color, COLORTERM=truecolor
- Shell: /bin/bash
- Install/channel: latest (v2.0.3)
- Active plugins:
@pistonex/opencode-go-usage(npm package TUI plugin, installed viaopencode plugin add)
Reproduction
- Author a TUI plugin package using the documented shape:
Plugin.definefrom@opencode/plugin/tui,/** @jsxImportSource @opentui/solid */, acreateResourcefetcher,exports: { "./tui": ... }, andpeerDependenciesonsolid-js/@opentui/solidas recommended by the CLI plugin publish docs. - Publish to npm and install with
opencode plugin add <pkg>. - Restart the TUI and open a session. The plugin's sidebar slot renders its initial state (e.g. "loading…").
- Wait for the async fetcher. (Verified with a non-reactive debug trace surfaced through a host dialog: the fetch settles with status 200 in ~500 ms and the resource resolves
ok=true.)
Reference package: @pistonex/opencode-go-usage on npm — 0.1.2 exhibits the bug, 0.1.3 carries the workaround.
Expected Behavior
The widget re-renders with the fetched data. The plugin's signals/resources should run on the same Solid instance the host renderer uses — as they do when the exact same file is loaded as a local plugin under ~/.config/opencode/plugins/<name>/tui.tsx (outside node_modules), which works perfectly.
Actual Behavior
Two failure modes depending on the package's declared dependencies:
- With
solid-js/@opentui/*as peer or regular dependencies (npm auto-installs non-optional peers into~/.cache/opencode/npm/<pkg>@latest/<id>/node_modules): the plugin loads, the initial render works, but plugin reactivity is dead. The resource resolves on the plugin's ownsolid-jscopy while the host renders with its own instance → the UI stays frozen on the initial frame ("loading…") forever. - Without them: the plugin fails to load at all:
plugin operation failed ... stage=load error="Cannot find package '@opentui/solid' imported from .../node_modules/<pkg>/src/tui.tsx"
Root cause analysis (from @opentui/core/runtime-plugin.js)
- The TUI registers host runtime modules (
solid-js,@opentui/solid, both JSX runtimes,@opencode/plugin/tui) as virtualopentui:runtime-module:*ids viaensureRuntimePluginSupport(seepackages/tui/src/plugin/runtime-plugin-support.bun.tson the v2.0.3 tag). - The rewrite loader only rewrites bare specifiers for files outside
node_modules(nodeModulesBareSpecifiers: falseby default). For files insidenode_modules, only already-virtual runtime specifiers are rewritten (nodeModulesRuntimeSpecifiers: true). - The import of
@opentui/solid/jsx-runtime/jsx-dev-runtimeis generated by Bun's tsx transform after the textual rewrite pass, so it is never rewritten even when the plugin's own source-level imports are handled. Since Bun can native-loadnode_modulesimports without firingonResolve, the JSX runtime resolves to the package's local copy when present (dual runtime) or fails to resolve when absent.
So for a package plugin: import ... from "solid-js" gets rewritten to the host module, but the JSX runtime import (generated) resolves to the local copy — the component tree ends up split across two Solid runtimes and nothing updates.
Workaround (published and working)
Import the host modules through their virtual ids and avoid JSX syntax (explicit jsx()/jsxs() calls with per-expression getters, the same output shape the Solid JSX compiler generates):
import { createMemo, createResource, createSignal, onCleanup, Show } from "opentui:runtime-module:solid-js"
import { Plugin, usePlugin } from "opentui:runtime-module:%40opencode%2Fplugin%2Ftui"
import { jsx, jsxs } from "opentui:runtime-module:%40opentui%2Fsolid%2Fjsx-runtime"This works end-to-end (data loads, hot language switching, auto-refresh, collapse/expand — see https://github.com/pistonex/opencode-go-usage), but it is an undocumented, fragile contract that every published TUI plugin would need to replicate. The current publish docs recommend plain peerDependencies, which leads straight into the frozen-UI failure mode.
Suggested directions
- Enable bare-specifier rewriting (or a scoped opt-in) for plugin install directories, so package plugins resolve host runtime modules the same way local plugins do; or
- Have the tsx transform emit the virtual runtime-module id for the JSX runtime import; or
- At minimum, document the virtual-module import pattern for package plugin authors.
Additional Context
- Debug instrumentation confirmed the fetch resolves (status 200, payload parsed, resource resolved
ok=true) while the UI stays on the initial frame — network/fetch is not a factor. - The same plugin file works flawlessly when loaded from the config plugins directory (outside
node_modules) — the divergence is exclusively thenode_modulesresolution path. - The plugin install's lockfile shows
solid-js/@opentui/solidarriving via npm's auto-installed non-optional peers (npm v7+ behavior), i.e. exactly what the publish docs recommend. - A survey of npm found no previously published TUI sidebar-widget package plugins (the only similar plugin is a v1-style server plugin without Solid), so this path appears largely unexercised so far.
Activity
github-actions commentedon Sep 14, 2026
This issue might be a duplicate of existing issues. Please check:
@opentui/solidcopy fromnode_modules, the Solid transform's hardcodednode_modulesexclusion prevents proper rewriting, and the dual runtime instance breaks reactivity. That issue contains additional detail on the JSX transform path and renderer-instance skew.node_modulespath vs. afile://path outsidenode_modules.pistonex commentedon Sep 14, 2026
Closing as duplicate — this is the v2.0.3 manifestation of the loader problem tracked in #33884 and the design issue #39986 (which describes this exact dual-reactive-graph failure).
The production-validated workaround (host virtual
opentui:runtime-module:*imports, no JSX syntax) and the additional v2.0.3 resolution details from this report have been cross-posted to #39986, and a v2.0.3 data point to #33884.fix: ship the TUI entry compiled with host runtime ids