Skip to content

TUI plugins referenced by npm package spec silently fail to load in 1.17.10 (OpenTUI 0.4.2 dual-entry regression) #33884

Description

@lindeberg

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:

  1. 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.
  2. 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.
  3. 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: 8cc79f7432982246e403da770b022d451ec7fba1deps: update OpenTUI to 0.4.2 (#33610)

Metadata

Metadata

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions