Repository navigation
Description
Status: Mitigated on
devby reverting the OpenTUI 0.4.2 bump (#33842) —devis 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.jsonpluginarray) still load fine in 1.17.10. - Loading the identical plugin from a local
file://absolute path (outside anynode_modulesdir) intui.jsonstill 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 workingfile://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_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. - 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)
Activity
github-actions commentedon Jun 25, 2026
This issue might be a duplicate of an existing issue. Please check:
file://workaround works) — describes the samenode_modulespath /@opentuiruntime-rewrite root cause. Issue TUI plugins referenced by npm package spec silently fail to load in 1.17.10 (OpenTUI 0.4.2 dual-entry regression) #33884 adds detail about the 0.4.2 dual-entry (index.bun.jsvsindex.js) aspect, but the underlying problem and workaround are identical.lindeberg commentedon Jun 25, 2026
Update: already addressed on
devby the OpenTUI 0.4.2 revert (#33842, merged).devis back on@opentui/solid@0.3.4(single-entry), which is the version where npm-spec TUI plugins load correctly. So this regression is fixed for the next release; it only affects the shipped 1.17.10 build. Workaround for 1.17.10 users remains: reference the plugin by an absolutefile://path intui.json.Leaving this open as a guardrail for any future re-attempt of the 0.4.2 bump, because I verified the obvious one-line fix does not work and the real fix is non-trivial:
ensureRuntimePluginSupport({ rewrite: { nodeModulesBareSpecifiers: true } })(proposed in fix(tui): bridge node_modules @opentui imports so npm-spec TUI plugins load #33885). Result: plugin import still throwsExport named 'jsxDEV' not found ... jsx-runtime.d.ts, andhost RendererContext === plugin RendererContextis stillfalse. That flag only affects the core runtime-plugin's bare-import rewriting; it does not touch the path that breaks.@opentui/solid/scripts/solid-plugin.js), whose source filter hardcodes anode_modulesexclusion ((?!.*[/\\]node_modules[/\\])). npm-spec plugins live under~/.cache/opencode/.../node_modules/, so the transform never runs on them → Bun native JSX →@jsxImportSource @opentui/solidresolves to the plugin's bundledjsx-runtime.d.tsstub (0.4.x ships a realjsx-runtime.js, but the plugin pinned its own older@opentui/solid).node_modules: thejsxDEVthrow disappears (advances to unrelated missing-dep errors). This is exactly whyfile://plugins work and npm-spec plugins don't.Durable fix for a future 0.4.x bump (either/both):
@opentui/*instead of bundling their own — dedup/override@opentui/*(andsolid-js) to the host versions during the Arborist install inpackages/core/src/npm.ts. This fixes both the JSX-runtime stub and the duplicate-instance/version-skew.node_modulesexclusion so it still transforms files inside the opencode plugin cache tree.#33885 (the
nodeModulesBareSpecifiersPR) has been closed as ineffective + redundant.chore(deps): @opentui/*·solid-js를 dependencies로 선언
chore(deps): @opentui/*·solid-js를 dependencies로 선언
docs: opencode 1.17.10 npm-spec 로드 버그 안내 + file:// 워크어라운드 추가
docs: opencode 1.17.10 npm-spec 로드 버그 안내 + file:// 워크어라운드 추가
lindeberg commentedon Jul 9, 2026
Still a problem
nomaed commentedon Jul 18, 2026
Same thing happens for me.
With local path in
tui.jsonplugin works fine:{ "plugin": ["file:///Users/<user>/git/opencode-tui-usage/index.tsx"] }With npm plugin name in
tui.jsonit doesn't load properly and even when it loads, it doesn't work correctly.{ "plugin": ["opencode-tui-usage@v0.4.1"] }And it's not that it doesn't work at all, it's just kind of broken in all sort of ways:
MathiasMonstrey commentedon Aug 2, 2026
Still reproduces on 1.18.11, so the 0.4.x guardrail this issue was left open for has fired.
@opencode-ai/plugin@1.18.11declares@opentui/{core,solid,keymap}peers as>=0.4.5and pins0.4.5in devDependencies.My case isolates obstacle (b) (renderer-instance skew) cleanly from obstacle (a) (skipped Solid JSX transform), which I haven't seen reported separately, so adding the data point.
Why (a) doesn't apply here
The plugin is pre-bundled with tsup, so the JSX is already compiled to
_$createElement(...)calls indist/tui.jsand@opentui/solidis leftexternal. There is no@jsxImportSourcepragma and nojsx-runtimeimport at runtime, so thenode_modulesexclusion insolid-plugin.jsis irrelevant, and I never hit theExport named 'jsxDEV' not founderror. (b) alone is still fatal.Environment
git+ssh://spec against a self-hosted GitLab (not the npm registry), so this isn't registry-specific:"plugin": ["myplugin@git+ssh://git@gitlab.example.com:8086/group/sub/myplugin.git"]~/.cache/opencode/packages/<spec>/node_modules/:@opentui/solid 0.4.5,@opentui/core 0.4.5,solid-js 1.9.12, i.e. exactly the versions the host asks for. This is instance/export-condition skew, not version skew.Symptoms
The plugin loads and runs fine:
tui(api)executes,api.slots.register({ slots: { sidebar_content } })succeeds, and the host does invoke the slot function. Only rendering is broken.Wrapping the slot's return value in
<RendererContext.Provider value={api.renderer}>, usingRendererContextimported from the plugin's own@opentui/solid, silences the throw (api.rendereris present), but nothing renders.With the throw suppressed, Solid reactivity is dead across the boundary. A
createSignalcreated intui(api)and read inside<Show when={accessor()}>has its accessor evaluated exactly once, and never again aftersetStatus(...)fires from an async callback. Timestamps from the plugin's own log:That is consistent with the plugin's
solid-js/@opentui/solidowner+context globals being a different module instance from the host's: the host renders in its universe, the plugin's memo subscribes in its own, and the two never meet.Control
The identical
dist/tui.js, with the same dependency versions present in its ownnode_modules, renders and updates correctly when referenced by absolute path intui.json:{ "plugin": ["/home/me/.config/opencode/plugins/myplugin/dist/tui.js"] }So duplicate copies existing on disk isn't sufficient to break it. What matters is whether the plugin is loaded from under
node_modules. Same files, same versions, different load path, opposite outcome.Note for other plugin authors landing here
Unrelated to this bug, but it cost me a while: if your
package.jsondeclares abuildscript, installing from a git spec makes pacote treat the plugin as needing a "prepare" step (pacote/lib/git.js#prepareDirtriggers onpostinstall/build/preinstall/install/prepack/prepare). It then clones and spawns a fullnpm install, which fails under the bundled binary with the opaquegit dep preparation failed. Renaming the script to something off that list avoids it when you ship a committeddist/.gitslim commentedon Aug 24, 2026
Data point: still reproduces on v1.18.21 (current stable as of 2026-08-24), extending @MathiasMonstrey's 1.18.11 report — and it now silently breaks a popular third-party plugin for all of its npm/git-spec users.
Real-world impact
@slkiser/opencode-quotav4.8.2 (npm): its TUI sidebar panel is dead for every npm/git-spec install on 1.18.x. The server-side plugin works (slash commands, config), the TUI entry imports, the sidebar slot function is called and returns a component tree — and nothing paints. No[tui.plugin]output, no stderr, nothing. Users just see the panel silently disappear.Mechanics (obstacle (b), pre-bundled plugin — no JSX transform involved)
Same isolation as @MathiasMonstrey's tsup case: the plugin's
dist/tui.jsships with JSX pre-compiled andsolid-js/@opentui/solidleftexternal, so obstacle (a) is irrelevant — (b) alone is fatal.Temporarily injected
import.meta.resolveprobes from inside the plugin'stui.js(opencode 1.18.21, plugin installed under~/.cache/opencode/packages/<spec>/node_modules/<name>/):i.e. from a plugin file under
node_modules,solid-jsresolves to the server renderer (no browser/client export condition active in that import context) and@opentui/solidto the plugin-local copy — not the host's bridged runtime. The slot runs, the tree builds, and it renders through an inert server renderer: fully silent invisibility rather than a crash.Both probes resolve identically in a
#-tagged cache dir and a non-#dir, so the#in the install path isn't the discriminator —node_modules-ness is.Instrumented run in a live session: slot called ×5, sidebar state applied 224× across two accounts (data pipeline fully working), zero pixels rendered.
Workaround
Confirmed on 1.18.21: referencing the same plugin via a
file://dir outside anynode_modules(deps symlinked in) makes the sidebar paint stably — consistent with everything above.Suggestion
This guardrail has now fired in shipped stable (1.18.x), so obstacle (b) may deserve its own tracking: plugins whose files live under
node_modulescan't be bridged to host renderer instances through the current runtime-plugin hooks at all. A fix probably needs loader-level interception (e.g. loading TUI plugin entrypoints through a virtual module id / custom resolver instead of their on-disk path), rather than furthersolid-plugin.jsfilter tweaks.feat(ai): add opencode vcs status sidebar
pistonex commentedon Sep 14, 2026
Data point: still reproduces on the v2 line (v2.0.3, packaged CLI) via the frozen-reactivity path — a package TUI plugin renders its initial frame and never repaints (dual reactive graph), or fails to load with
Cannot find package '@opentui/solid'when the install has no local copies.The v2.0.3 resolution details and a production-validated workaround (host virtual
opentui:runtime-module:*imports, no JSX syntax — reference package@pistonex/opencode-go-usageon npm, 0.1.2 broken / 0.1.3 workaround) are posted on #39986.pistonex commentedon Sep 21, 2026
Follow-up: the workaround from my earlier comment here (full walkthrough on #39986) is now running in production as an installed npm package, and it just survived the opencode v2.0.11 theme API change (which broke every theme-aware TUI plugin —
Context.themewent from a function to aResolvedThemeobject with renamed tokens likebackground.surface.overlay→background.raised.base). Only token renames were needed; the virtual-import structure itself needed zero changes, which suggests it is a durable pattern across host API evolution, not a one-off hack.Reference:
@pistonex/opencode-go-usageon npm, installed viaopencode plugin add:opentui:runtime-module:*imports + hand-writtenjsx()/jsxs()(no JSX syntax) → loads, renders, updates reactively (data, hot language switch, auto-refresh)ResolvedThemetokens, back to fully workingAnyone currently broken by this (including the npm/git-spec users of
@slkiser/opencode-quotamentioned above) can ship today with the same pattern; details and the two candidate fix seams for the host are in the #39986 comment.schemaitat commentedon Oct 12, 2026
Data point on v2.0.27 (packaged Bun CLI, Linux): a GitHub-installed TUI plugin fails at load time with the
reactresolution error — i.e. obstacle (a) "the Solid JSX transform never runs", not just the frozen-reactivity symptom. A shipped plugin-side workaround below makes it load and stay reactive fromnode_modules.Environment
v2.0.27, installed viaopencode plugin add github:schemaitat/opencode-plugin-auto-classifier~/.cache/opencode/npm/git-<hash>/<ts>/node_modules/opencode-plugin-auto-classifier/exports["."]) loads fine; onlyexports["./tui"]fails, on every startup.Symptom
Confirmed mechanism
tui.tsx(JSX) as its./tuientry, and does not install@opentui/solid/solid-jslocally (declared as optional peers), so there is no local jsx-runtime to fall back to./** @jsxImportSource @opentui/solid */pragma it instead becomesCould not resolve: "@opentui/solid/jsx-dev-runtime"— confirming the config isn't the fix.tsconfig.json(withjsxImportSource) is not sufficient — it only moves the failure toCannot find package '@opentui/solid', since the transform-generated jsx-runtime import is emitted after the text-level runtime-module rewrite and never bridged.Workaround that ships and works
Same pattern @pistonex validated: make the entry a plain
.ts(no JSX) and import host singletons through the virtual ids, then build the tree withjsx()/jsxs()and per-expressiongetaccessors.exports["./tui"]→./tui.ts; keep@opentui/*andsolid-jsas optional peers so npm/git installs don't nest shadow copies.Shipped in
schemaitat/opencode-plugin-auto-classifier@0d3512a; verifiedopencode plugin updatere-resolves the commit andplugin listreports the new plugin id/version.Suggested seams (unchanged from this thread)
(a) Let the TSX transform emit the virtual
opentui:runtime-module:*id for the JSX-runtime import it generates, and/or (b) relax the Solid transform'snode_modulesexclusion (or enable specifier rewriting) for the plugin install tree. Until then, the virtual-id + no-JSX shape is the only path I found that both loads and stays reactive from an npm/git install.fix: run the TUI on the host's runtime, so an npm install renders and…
zemadeiran commentedon Oct 22, 2026
Reproduced a variant of this on opencode 2.1.2 (macOS arm64, Apple Terminal, zsh), with a git-installed TUI plugin.
Install:
opencode plugin add github:zemadeiran/opencode-file-tree-v2Every load fails at the same point:
The error is
react, notjsxDEV, which suggests the Solid JSX transform is skipped undernode_modulesand the JSX falls back to React's runtime, the same underlying cause as thenode_modulesexclusion described above.What I ruled out while testing:
solid-jsand@opentui/solidare present and resolved in the install, so missing packages are not the cause.peerDependenciestodependenciesdid not change the error.tsconfig.jsonwith"jsxImportSource": "@opentui/solid"did not change the error."plugins": ["/abs/path/to/plugin"]inopencode.json), so the code itself is fine.Because
opencode plugin addonly accepts npm or Git specs, the local-path workaround has to be set by hand. Happy to test any fix against this repro.zemadeiran commentedon Oct 22, 2026
Cross-post: New OpenCode V2 plugin sidebar file tree. Installed via and added to . Known issue: fails with (issue #33884). Repo: https://github.com/zemadeiran/opencode-file-tree-v2
opencodejr commentedon Oct 29, 2026
Thanks for the very thorough write-up and bisect, @lindeberg!
This report is about v1.17.10, and OpenCode v1 (0.x/1.x) only gets fixes for critical bugs such as data loss, security problems, or crashes or breakage with no workaround. You've documented a working workaround (an absolute
file://path intui.json), and you mention the OpenTUI bump was already reverted (#33842). So this is unlikely to get a further fix in v1.We recommend upgrading to OpenCode v2 (
@opencode/cli), where active development happens. It's a separate implementation with its own plugin loading.Related items, most closely related first:
node_modules@opentuiimports)sidebar_contentslot never renders (1.17.11), possibly the same root causeIf you think this is critical, or you can reproduce it on v2, please reply here with the details.
Load the TUI part through the host's virtual runtime modules
opencodejr commentedon Oct 30, 2026
Closing this as not planned. It affects only v1.17.10, and v1 gets fixes only for critical bugs. There's a working workaround (an absolute
file://path intui.json), and the OpenTUI bump was already reverted (#33842). Thanks again for the detailed root-cause analysis; it's useful background if this comes up again in #33885 or #34050.We recommend upgrading to OpenCode v2 (
@opencode/cli). If you can reproduce npm-spec TUI plugins failing to load on v2, please open a new issue with your v2 version and steps.