Status: Mitigated on dev by reverting the OpenTUI 0.4.2 bump (#33842) — dev is back on 0.3.4 where npm-spec TUI plugins load. This issue documents the underlying loader problem (which the revert does not fix) so the eventual 0.4.2 re-bump does not reintroduce it. Only the shipped v1.17.10 is affected.
Summary
Between v1.17.9 and v1.17.10, TUI plugins referenced by their npm package spec in ~/.config/opencode/tui.json (e.g. {"plugin": ["@slkiser/opencode-quota"]}) silently stop loading. The sidebar panel never renders, the plugin's slash commands never register, and nothing useful is surfaced to the user.
Confirmed scope of the regression:
- Server-side plugins (same package, listed in
opencode.json plugin array) still load fine in 1.17.10.
- Loading the identical plugin from a local
file:// absolute path (outside any node_modules dir) in tui.json still works in 1.17.10.
- Only the npm-package-spec → TUI resolution path is broken.
Root cause
The regression bisects to the OpenTUI bump in #33610 (@opentui/* 0.3.4 → 0.4.2): 1.17.9 (OpenTUI 0.3.4) works, 1.17.10 (OpenTUI 0.4.2) does not, and the bump is the only change in range touching the npm-spec TUI load path. opencode's own TUI loader code (packages/opencode/src/plugin/tui/runtime.ts, loader.ts) is byte-unchanged across the range.
The underlying problem is that npm-spec TUI plugins load their own isolated copy of @opentui/solid from node_modules instead of the host's instance. opencode installs an npm TUI plugin into an isolated tree at ~/.cache/opencode/packages/<spec>/node_modules/<name>/ (packages/core/src/npm.ts, Arborist reify). The example plugin declares @opentui/solid as a direct dependency, so Arborist nests that copy inside the plugin's own node_modules — a different version/instance than the host opencode binary bundles. Under OpenTUI 0.4.2 this isolation produces two independent obstacles, verified on-machine:
(a) The Solid JSX transform never runs on the plugin (the proximate crash).
dist/tui.tsx declares /** @jsxImportSource @opentui/solid */. Normally OpenTUI's Solid Babel transform (bun-plugin-solid / solid-plugin.js, registered by ensureRuntimePluginSupport) rewrites JSX onto the bridged runtime so the literal @opentui/solid/jsx-runtime import never happens. But that transform's file filter contains a hardcoded node_modules exclusion:
const sourceFilter = input.resolvePath
? /^(?!.*[/\\]node_modules[/\\]).*\.[cm]?[jt]sx?(?:[?#].*)?$/
: /^(?!.*[/\\]node_modules[/\\]).*\.[cm]?[jt]sx(?:[?#].*)?$/;
npm-spec plugins live under node_modules, so the transform is skipped, Bun native-loads the JSX, and @opentui/solid/jsx-runtime resolves to the plugin's nested copy, whose ./jsx-runtime export points only at jsx-runtime.d.ts (a TypeScript stub — no JS, no jsxDEV). Result: a thrown Export named 'jsxDEV' not found in module '.../@opentui/solid/jsx-runtime.d.ts'.
(b) Renderer-instance skew (the deeper issue).
Even past the JSX crash, the plugin's @opentui/solid is a different physical module than the host's, so each has its own module-level RendererContext = createContext() singleton. useContext(RendererContext) / <Slot> in the plugin never find the host's slot registry → the panel never mounts and commands register on a dead renderer. 0.4.2 makes this worse via the new conditional bun/node exports (index.bun.js vs index.js are different physical files, each with their own RendererContext), so even matching versions can diverge by export condition.
Both obstacles are silent: the thrown import error is routed through the TUI runtime's error path, but opencode's TUI runs in the terminal alternate-screen buffer, so the console.error is painted over and never seen.
This explains all three facts: server plugins never touch the OpenTUI renderer (fine); file:// plugins live outside node_modules so the Solid transform runs and their @opentui imports are bridged to the host instance (fine); npm-spec plugins are excluded from the transform and resolve an isolated, mismatched copy (broken).
Reproduction / verification
Verified on macOS, opencode 1.17.10 (Homebrew), Bun 1.3.14, with @slkiser/opencode-quota:
# Host (Bun) resolves the bare specifier to the bun-conditioned entry:
bun import.meta.resolve('@opentui/solid') => .../@opentui/solid/index.bun.js
# Register the real 0.4.2 runtime plugin with opencode's default options, then import the
# plugin's dist/tui.tsx from node_modules:
plugin import THREW: Export named 'jsxDEV' not found in module '.../@opentui/solid/jsx-runtime.d.ts'
host RendererContext === plugin RendererContext ? false # separate instances
Cross-check: copying the identical plugin code to a path outside node_modules (mimicking file://) makes the jsxDEV/jsx-runtime throw disappear — confirming the node_modules location, not bare-specifier bridging, is the blocker.
A note on the obvious one-line lever: enabling nodeModulesBareSpecifiers: true on ensureRuntimePluginSupport was tested and is not sufficient — it only affects the core runtime-plugin's bare-import rewriting (runtime-plugin.js), not the Solid transform's node_modules exclusion, so the proximate jsxDEV throw remains and the instance skew is unaddressed.
Suggested fix directions
In rough order of how completely they address the root cause:
- Make npm-spec TUI plugins resolve the host's
@opentui/* instead of their own nested copy — dedupe/strip the plugin's bundled @opentui/* during install, or install so @opentui/* hoists to the host version. This mirrors the working file:// behavior and resolves both the version skew and (by making the resolved module the host's) the instance identity.
- Relax the Solid transform's hardcoded
node_modules exclusion for the plugin cache tree (upstream @opentui/solid solid-plugin.js) and add @opentui/solid/jsx-runtime to the bridge set — addresses the proximate crash, but on its own still leaves the version/instance skew.
- Independently: have the TUI plugin loader surface load/import errors to a persistent log (file), so this class of failure isn't completely silent.
Workaround
Reference the plugin by an absolute file:// path (outside any node_modules dir) in tui.json instead of the npm spec — that path runs through the Solid transform and is bridged to the host renderer, and works in 1.17.10.
Environment
- opencode 1.17.10 (Homebrew), Bun 1.3.14, macOS (arm64)
- Regression range:
v1.17.9..v1.17.10
- Culprit commit:
8cc79f7432982246e403da770b022d451ec7fba1 — deps: update OpenTUI to 0.4.2 (#33610)
Summary
Between v1.17.9 and v1.17.10, TUI plugins referenced by their npm package spec in
~/.config/opencode/tui.json(e.g.{"plugin": ["@slkiser/opencode-quota"]}) silently stop loading. The sidebar panel never renders, the plugin's slash commands never register, and nothing useful is surfaced to the user.Confirmed scope of the regression:
opencode.jsonpluginarray) still load fine in 1.17.10.file://absolute path (outside anynode_modulesdir) intui.jsonstill works in 1.17.10.Root cause
The regression bisects to the OpenTUI bump in #33610 (
@opentui/* 0.3.4 → 0.4.2): 1.17.9 (OpenTUI 0.3.4) works, 1.17.10 (OpenTUI 0.4.2) does not, and the bump is the only change in range touching the npm-spec TUI load path. opencode's own TUI loader code (packages/opencode/src/plugin/tui/runtime.ts,loader.ts) is byte-unchanged across the range.The underlying problem is that npm-spec TUI plugins load their own isolated copy of
@opentui/solidfromnode_modulesinstead of the host's instance. opencode installs an npm TUI plugin into an isolated tree at~/.cache/opencode/packages/<spec>/node_modules/<name>/(packages/core/src/npm.ts, Arboristreify). The example plugin declares@opentui/solidas a direct dependency, so Arborist nests that copy inside the plugin's ownnode_modules— a different version/instance than the host opencode binary bundles. Under OpenTUI 0.4.2 this isolation produces two independent obstacles, verified on-machine:(a) The Solid JSX transform never runs on the plugin (the proximate crash).
dist/tui.tsxdeclares/** @jsxImportSource @opentui/solid */. Normally OpenTUI's Solid Babel transform (bun-plugin-solid/solid-plugin.js, registered byensureRuntimePluginSupport) rewrites JSX onto the bridged runtime so the literal@opentui/solid/jsx-runtimeimport never happens. But that transform's file filter contains a hardcodednode_modulesexclusion:npm-spec plugins live under
node_modules, so the transform is skipped, Bun native-loads the JSX, and@opentui/solid/jsx-runtimeresolves to the plugin's nested copy, whose./jsx-runtimeexport points only atjsx-runtime.d.ts(a TypeScript stub — no JS, nojsxDEV). Result: a thrownExport named 'jsxDEV' not found in module '.../@opentui/solid/jsx-runtime.d.ts'.(b) Renderer-instance skew (the deeper issue).
Even past the JSX crash, the plugin's
@opentui/solidis a different physical module than the host's, so each has its own module-levelRendererContext = createContext()singleton.useContext(RendererContext)/<Slot>in the plugin never find the host's slot registry → the panel never mounts and commands register on a dead renderer. 0.4.2 makes this worse via the new conditionalbun/nodeexports (index.bun.jsvsindex.jsare different physical files, each with their ownRendererContext), so even matching versions can diverge by export condition.Both obstacles are silent: the thrown import error is routed through the TUI runtime's error path, but opencode's TUI runs in the terminal alternate-screen buffer, so the
console.erroris painted over and never seen.This explains all three facts: server plugins never touch the OpenTUI renderer (fine);
file://plugins live outsidenode_modulesso the Solid transform runs and their@opentuiimports are bridged to the host instance (fine); npm-spec plugins are excluded from the transform and resolve an isolated, mismatched copy (broken).Reproduction / verification
Verified on macOS, opencode 1.17.10 (Homebrew), Bun 1.3.14, with
@slkiser/opencode-quota:Cross-check: copying the identical plugin code to a path outside
node_modules(mimickingfile://) makes thejsxDEV/jsx-runtime throw disappear — confirming thenode_moduleslocation, not bare-specifier bridging, is the blocker.A note on the obvious one-line lever: enabling
nodeModulesBareSpecifiers: trueonensureRuntimePluginSupportwas tested and is not sufficient — it only affects the core runtime-plugin's bare-import rewriting (runtime-plugin.js), not the Solid transform'snode_modulesexclusion, so the proximatejsxDEVthrow remains and the instance skew is unaddressed.Suggested fix directions
In rough order of how completely they address the root cause:
@opentui/*instead of their own nested copy — dedupe/strip the plugin's bundled@opentui/*during install, or install so@opentui/*hoists to the host version. This mirrors the workingfile://behavior and resolves both the version skew and (by making the resolved module the host's) the instance identity.node_modulesexclusion for the plugin cache tree (upstream@opentui/solidsolid-plugin.js) and add@opentui/solid/jsx-runtimeto the bridge set — addresses the proximate crash, but on its own still leaves the version/instance skew.Workaround
Reference the plugin by an absolute
file://path (outside anynode_modulesdir) intui.jsoninstead of the npm spec — that path runs through the Solid transform and is bridged to the host renderer, and works in 1.17.10.Environment
v1.17.9..v1.17.108cc79f7432982246e403da770b022d451ec7fba1—deps: update OpenTUI to 0.4.2 (#33610)