Skip to content

tui: npm package TUI plugins render once and never update (dual solid-js runtime from node_modules resolution) #48883

Closed as not planned

Description

@pistonex

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 via opencode plugin add)

Reproduction

  1. Author a TUI plugin package using the documented shape: Plugin.define from @opencode/plugin/tui, /** @jsxImportSource @opentui/solid */, a createResource fetcher, exports: { "./tui": ... }, and peerDependencies on solid-js / @opentui/solid as recommended by the CLI plugin publish docs.
  2. Publish to npm and install with opencode plugin add <pkg>.
  3. Restart the TUI and open a session. The plugin's sidebar slot renders its initial state (e.g. "loading…").
  4. 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 own solid-js copy 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 virtual opentui:runtime-module:* ids via ensureRuntimePluginSupport (see packages/tui/src/plugin/runtime-plugin-support.bun.ts on the v2.0.3 tag).
  • The rewrite loader only rewrites bare specifiers for files outside node_modules (nodeModulesBareSpecifiers: false by default). For files inside node_modules, only already-virtual runtime specifiers are rewritten (nodeModulesRuntimeSpecifiers: true).
  • The import of @opentui/solid/jsx-runtime / jsx-dev-runtime is 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-load node_modules imports without firing onResolve, 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 the node_modules resolution path.
  • The plugin install's lockfile shows solid-js/@opentui/solid arriving 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

  1. github-actions commented on Sep 14, 2026

    @github-actions
    Contributor

    This issue might be a duplicate of existing issues. Please check:

  2. pistonex commented on Sep 14, 2026

    @pistonex
    Author

    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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

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