Repository navigation
Description
Problem
The packaged Bun CLI does not reliably apply OpenTUI's runtime Solid transform to external TUI plugin TSX. A plugin can either fail while resolving react/jsx-dev-runtime or load project-local solid-js / @opentui/solid copies. The latter renders its initial frame, but signal updates do not repaint because the plugin and host use separate reactive graphs. Imperative renderable mutation works because it bypasses Solid reactivity.
This is specific to dynamically loaded external plugin source in the packaged executable. The same plugin works in source-mode Bun.
Reproduction
- Run the packaged CLI with an external
.opencode/plugins/tui/reactive.tsxplugin. - Give the plugin a project-local
solid-jsand@opentui/solid, as documented for local plugin dependencies. - Register a slot that renders a signal and updates it from a timer.
- Observe either a
react/jsx-dev-runtimeresolution failure or an initial frame that never repaints.
The reproduction must also cover the supported index.ts -> view.tsx layout, not only a .tsx entrypoint.
Rejected approach
PR #39983 attempted to precompile external JSX with Bun.build and map selected Solid/OpenTUI imports to the host runtime. We closed it because runtime bundling changed plugin module semantics and still missed supported shapes:
.tsand.jsentrypoints importing TSX bypassed preparation.- Bundling relocated
import.meta.url, breaking plugin-relative assets, workers, WASM, and dynamic imports. - OpenTUI's transform excludes JSX under
node_modules, so npm TSX could still fail. - Package-prefix externalization could leave unsupported
solid-js/*imports unresolved or load another runtime. - Cache identity used only entrypoint mtime despite embedding the full dependency graph.
- The persistent generated-bundle cache was unbounded and shared across processes.
- Tests invoked the preparation helper directly rather than exercising the packaged executable.
Do not revive graph-wide runtime bundling without resolving those compatibility constraints.
Plan
- Keep a minimal packaged-CLI regression fixture that proves a signal update visibly repaints.
- Cover both a direct
.tsxentrypoint and a.tsentrypoint importing.tsx. - Run the fixture against Bun 1.4/canary, where related runtime
onLoadfixes have landed. - If the newer Bun runtime fixes the reproduction, upgrade when a suitable release is available.
- Otherwise reduce it to an OpenTUI reproduction and add an OpenTUI-owned external-module loading API or upstream fix. OpenTUI should own its runtime module registry and transform contract.
- Keep OpenCode loading plugins as modules. Do not bundle arbitrary third-party dependency graphs into the OpenCode state directory.
Acceptance criteria
- Reactive external TSX visibly repaints in the packaged CLI.
.ts -> .tsxplugin graphs work.- Plugin and host share the required Solid/OpenTUI singleton modules.
- Ordinary dependencies retain normal module identity and resolution behavior.
- Plugin-relative assets and
import.meta.urlretain their original meaning. - The regression runs against the packaged artifact, not only source-mode Bun.
Activity
fix(tui): restore live label updates on opencode 1.18, add inactivity…
Remount the widget instead of relying on reactive signal updates
pistonex commentedon Sep 14, 2026
Data point from the v2 line + a workaround validated in production on v2.0.3 (originally reported in #48883, closed as duplicate of this issue / #33884).
Reproduces on v2.0.3 with a package TUI plugin (
opencode plugin add, install under~/.cache/opencode/npm/...): the widget paints its initial frame and then never repaints, even though the async fetcher resolves — verified with a non-reactive trace surfaced through a host dialog (fetch settles 200 in ~500 ms, resource resolvesok=true, UI frozen). Without localsolid-js/@opentuicopies in the install, the plugin instead fails to load entirely:Additional detail on the resolution split (reading
@opentui/core/runtime-plugin.jsagainst observed v2.0.3 behavior):import ... from "solid-js","@opencode/plugin/tui") do get rewritten to host runtime modules even insidenode_modules— the prescan path (nodeModulesRuntimeSpecifiers: true) installs exact-path rewrite loaders for node_modules files whose source imports match the runtime map.@opentui/solid/jsx-runtime/jsx-dev-runtimeimport generated by the tsx transform is emitted after the textual rewrite, so it is never rewritten. Bun then resolves it from the node_modules importer against the package's local copy (dual runtime), or fails when no copy exists.solid-js, while the component tree runs on the package's local@opentui/solid— the initial frame renders, nothing ever repaints.Workaround — works today with no host changes. Publish the plugin importing 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 emits, so no auto-generated bare JSX-runtime import exists):Notes:
opentui:runtime-module:*ids resolve throughbuild.moduleregistrations, which work regardless of the importer's location — the one path that reliably lands on host instances from insidenode_modules.jsx-runtime, notjsx-dev-runtime: the latter only re-exportsFragmentandjsxDEV, the former exportsjsxandjsxsas well.solid-js/@opentui/*peers as optional (peerDependenciesMeta) keeps npm from auto-installing shadow copies into plugin installs; with virtual imports the local copies are irrelevant anyway.Live reference:
@pistonex/opencode-go-usageon npm — 0.1.2 exhibits the frozen-UI failure, 0.1.3 carries the workaround. Installed as a package, data loads, hot language switching, 5-minute auto-refresh and collapse/expand all work.For the eventual fix, the two candidate seams this analysis points at are: (a) letting the tsx transform emit the virtual runtime-module id for the JSX runtime import, or (b) enabling bare-specifier rewriting for plugin install directories — both would make the documented author shape (plain JSX + peerDependencies) work as published.
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.dskanakoglou commentedon Oct 16, 2026
Hello, I am a researcher from DTU Denmark. I am using opencode for a workshop and ran into this with
github:-installed TUI plugins (reported as #56110, which I'm closing as a duplicate). It reproduces on 2.0.29 and 2.0.31: a plugin in the npm cache renders once and never repaints, while the same folder loaded by path works.Following step 5 of the plan, I have an OpenTUI-side fix ready: an
includeoption on the Solid transform (and onensureRuntimePluginSupport), so a host can name directories whose.jsx/.tsxfiles are compiled even undernode_modules. OpenCode would then pass its npm cache directory. It doesn't bundle, so modules load normally andimport.meta.urlkeeps its meaning. It has a unit test and a reactivity test, and the OpenTUI Solid suite passes.I also tested a ~20-line OpenCode-only version of the same idea, which reuses OpenTUI's loader for
<cache>/npm/**/*.tsx. In a locally built packaged binary, a reactive.tsxentry and a.ts→.tsxentry both repaint, and so does opencode-sidebar-plus installed viagithub:. An unpatched build from the same commit stays frozen. What I haven't done yet is a regression test that runs against the packaged artifact.Would you prefer a PR to OpenTUI, or the OpenCode-only change as a stopgap?
dskanakoglou commentedon Oct 17, 2026
Still reproduces on 2.0.32. Happy to open either PR (OpenTUI
includeoption or the OpenCode-side stopgap), whichever you prefer.dskanakoglou commentedon Oct 22, 2026
Opened the OpenTUI side as anomalyco/opentui#1741 (an
includeoption on the Solid transform). Once it's released, OpenCode would pass its npm cache directory toensureRuntimePluginSupport.rizal72 commentedon Oct 25, 2026
Same failure on v2.1.4, from a published plugin rather than a local one — and it is the shape the publish docs recommend today.
@rizal72/oc-coffee@0.1.0shipsexports: { "./tui": "./tui.tsx" }with the@opentui/*andsolid-jspeers, exactly asbuild/plugins/cli→ "Publish and load" shows. Installed from npm:Reproduced minimally in a clean
HOMEwith one probe plugin whose path containsnode_modules; the same directory moved outsidenode_modulesloads.A workaround that works today, and is probably worth documenting until this is fixed: ship the TUI entry compiled.
babel-preset-solid(moduleName: "@opentui/solid",generate: "universal") plusexports["./tui"] = "./tui.js"loads cleanly on 2.1.4, with nopeerDependenciesneeded — the host resolves@opentui/solid,solid-jsand@opencode/plugin/tuifrom its own registry. No bundling is involved, so the reasons #39983 was closed do not apply.@rizal72/oc-coffee@0.1.1ships that shape and loads.pistonex commentedon Oct 25, 2026
Confirming from the other side of @rizal72's report: a published npm TUI plugin still works on v2.1.4 with the virtual-import pattern, so the "published package" shape is not the blocker — the JSX transform path is.
Environment: opencode v2.1.4, Linux x64,
opencode plugin add @pistonex/opencode-go-usage. Plugin listed as0.2.0/ source@pistonex/opencode-go-usage, and the plugin log shows clean reconciliations with zero load/setup errors after the update.What it does (no build step):
exports["./tui"]points at a.tsxfile that contains no JSX syntax — the tree is built withjsx()/jsxs()and per-expressiongetaccessors.opentui:runtime-module:solid-js,...%40opencode%2Fplugin%2Ftui,...%40opentui%2Fsolid%2Fjsx-runtime).@opentui/*/solid-jsstay optional peers, so npm installs no shadow copies.Durability so far: the same shape survived the v2.0.11 theme API change (function →
ResolvedThemeobject) with only token renames — no structural change. On 2.1.4,ResolvedThemeandcredential.listare unchanged, and the plugin also moved its API key source tocontext.client.credential.list()(the legacyaccount.jsongoes stale on rotation), which confirms the host client in the plugin context is the right seam for that too.On the two workarounds: @rizal72's precompiled-JS approach is a good option when authors want to keep JSX; the virtual-import approach needs no toolchain but requires hand-written
jsx()calls. Either way, once theincludeoption from anomalyco/opentui#1741 lands, both can be dropped in favor of plain TSX + peers as documented.fix(tui): ship compiled TUI entry for packaged-CLI installs
opencodejr commentedon Oct 28, 2026
I reproduced this on
@opencode/cli@2.2.0(packaged binary, latest release), and it still happens on the latestdev(1c49e51) run from source.The failure depends on whether the plugin's path contains
node_modules:.opencode/plugins/<name>/work. Both atui.tsxentry and atui.tsentry importingview.tsxrender and repaint (the timer-driven counter keeps advancing), with project-localsolid-js@1.9.17and@opentui/solid@0.7.0installed..opencode/node_modules/…. That is how npm/github-installed plugins are laid out. Both fail with:.ts→.tsxplugin, the error points atview.tsxinstead. The TUI shows "2 plugins failed".This matches the
reactresolution symptom. I didn't see the "first frame renders but never repaints" variant here, because the transform fails before it gets that far. The script below sets up both layouts, and the recording and screenshots show the counters advancing and the plugin error dialog.recording.mp4
Reproduction script (drive.ts)
Run it with opencode-drive:
opencode-drive run ./drive.ts. It drivesopencodefrom yourPATH; setOC_DEVto the path of an OpenCode checkout to run that from source instead.fix(tui): load published TUI via precompiled entry
fix: ship the TUI entry compiled with host runtime ids
build: ship precompiled dist/tui.js for github: installs
fix: ship a compiled TUI so github: installs render
build!: ship a precompiled TUI entry and rename to opencode-usage
tuzelko-familiar commentedon Nov 7, 2026
A setup that works for us on v2.2.5, in case it helps another author:
tui.ts(exports["./tui"] = "./tui.ts"), no JSX syntax and no TUI build step.jsx()calls with getters for every reactive expression:@opentui/core,@opentui/solid, andsolid-jsare optional peers (peerDependenciesMeta), so npm does not install shadow copies.Rpcimport, so the TUI part has no runtime@opencode/pluginimport.With that, an async
pendingload and livecontext.data.on("rpc.…")updates both re-render from the installed package. Shipped in@tuzelko/opencode-delayed-message@0.2.7; source: https://github.com/TuzelKO/opencode-delayed-message/blob/main/tui.ts