Skip to content

fix(tui): import Solid control-flow builtins from solid-js, not @opentui/solid - #282

Merged
ualtinok merged 1 commit into
cortexkit:masterfrom
iceteaSA:fix/tui-solid-builtins-upstream
Aug 7, 2026
Merged

fix(tui): import Solid control-flow builtins from solid-js, not @opentui/solid#282
ualtinok merged 1 commit into
cortexkit:masterfrom
iceteaSA:fix/tui-solid-builtins-upstream

Conversation

@iceteaSA

@iceteaSA iceteaSA commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Symptom

The sidebar and every /ctx-* command disappear. No crash, no stderr, nothing in ~/.local/share/opencode/log. The server plugin is healthy the whole time — transforms run, RPC answers, the DB is fine. Only the TUI surface is gone.

I hit this on a daily driver after an OpenCode upgrade. It reproduces on clean master.

Cause

scripts/build-tui.ts passes the Solid transform moduleName: runtimeModuleId("@opentui/solid"). The transform routes every import it emits through that single module — renderer helpers and control-flow builtins alike.

@opentui/solid only re-exports its own renderer helpers. The builtins live in solid-js. Measured against @opentui/solid 0.4.5:

builtin @opentui/solid solid-js
For
Show
Index
Switch
Match
ErrorBoundary
Suspense
SuspenseList
Portal
Dynamic

@opentui/solid exposes 48 names and none of the eight control-flow builtins. Portal and Dynamic genuinely do belong to it.

The <For> at src/tui/slots/sidebar-content.tsx:997 therefore compiles to:

import { For as _$For } from "opentui:runtime-module:%40opentui%2Fsolid";

which throws at plugin load:

Export named 'For' not found in module 'opentui:runtime-module:%40opentui%2Fsolid'

OpenCode swallows errors thrown while loading a TUI plugin, so the plugin is dropped with no diagnostic. That silence is why the symptom looks like "the sidebar broke" rather than "the plugin failed to load".

This is latent on master — the committed src/tui-compiled/slots/sidebar-content.tsx carries the bad import today. It only becomes fatal once the host resolves that virtual module strictly.

Fix

Redirect, after the transform, any specifier the OpenTUI runtime does not export to the solid-js runtime module.

The split is read from the real export sets at build time (Object.keys(await import(...))) rather than hardcoded, so it keeps working when OpenTUI changes what it re-exports — a hardcoded list would just relocate the bug to the next version bump.

A name exported by neither module now fails the build, instead of shipping a bundle that breaks the TUI silently.

The emitted imports are one specifier per statement, so the rewrite is a narrow line-level match rather than a general import parser.

Verification

  • Reproduced on clean master: committed bundle has the bad For import.
  • After the fix all 24 opentui:runtime-module:* specifiers in the rebuilt bundle resolve against their target module's real exports.
  • build:tui is reproducible — rebuilding leaves no drift.
  • Confirmed live: sidebar and /ctx-* commands return.

Gates on this branch (clean master base): plugin 3532 pass / 0 fail, typecheck 0 across all three packages, check:tui-compiled PASS.

Regression test

src/tui/tui-compiled-runtime-imports.test.ts resolves every compiled specifier against the actual module export sets, and separately asserts no solid-js-only builtin is imported from @opentui/solid.

Red-checked both ways: 2/2 fail with the pre-fix build script, 2/2 pass after.

Note on debugging this class

Worth knowing for anyone who hits a silent TUI drop: instrumenting the tui factory is not enough, because an empty log cannot distinguish "module never imported" from "factory never called". src/tui/entry.mjs runs before any TypeScript module loads and is the only place an import-time failure is observable. It also needs a synchronous writer — src/shared/logger.ts buffers and flushes on process exit, which is exactly the path a hard load failure skips.

Happy to split the test into its own commit or adjust the redirect's placement if you'd prefer it inside compileTsx.


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Summary by cubic

Fixes the TUI by routing Solid control‑flow builtins to solid-js instead of @opentui/solid. Restores the sidebar and /ctx-* commands, and prevents silent plugin drops.

  • Bug Fixes
    • Reads export sets from @opentui/solid and solid-js at build time and rewrites single‑specifier opentui:runtime-module:* imports to solid-js when needed; fails the build if a name exists in neither module.
    • Recompiled sidebar so <For> imports from solid-js.
    • Adds shared TUI_RUNTIME_SPECIFIERS and runtimeModuleId; guard tests validate all compiled imports, reject unknown runtime module IDs, ensure modules are non‑empty and the list has no duplicates, and assert control‑flow builtins never import from @opentui/solid.

Written for commit 15371df. Summary will update on new commits.

Review in cubic

Greptile Summary

The PR corrects generated TUI imports by routing Solid-only control-flow builtins to the host-provided solid-js runtime while retaining OpenTUI renderer helpers in @opentui/solid.

  • Centralizes allowed TUI runtime specifiers and virtual-module ID generation.
  • Resolves actual module export sets during TUI compilation and rejects unresolved generated imports.
  • Regenerates the sidebar bundle with For imported from solid-js.
  • Adds regression coverage validating every compiled runtime import against its target module.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
packages/plugin/scripts/build-tui.ts Adds export-aware post-transform routing so Solid-only builtins resolve from the correct host runtime module.
packages/plugin/src/shared/tui-runtime-specifiers.ts Centralizes the runtime specifier allowlist and virtual-module ID construction for build and test consumers.
packages/plugin/src/tui-compiled/slots/sidebar-content.tsx Updates the generated sidebar bundle so the transformed For builtin imports from solid-js.
packages/plugin/src/tui/tui-compiled-runtime-imports.test.ts Validates compiled runtime imports against real module export sets and guards against future builtin misrouting.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
    A[TSX source] --> B[Solid transform]
    B --> C{Generated name exported by OpenTUI Solid?}
    C -->|Yes| D[Keep @opentui/solid runtime import]
    C -->|No| E{Exported by solid-js?}
    E -->|Yes| F[Redirect to solid-js runtime import]
    E -->|No| G[Fail TUI build]
    D --> H[Compiled TUI]
    F --> H
    H --> I[Regression test validates runtime exports]
Loading

Reviews (5): Last reviewed commit: "fix(tui): import Solid control-flow buil..." | Re-trigger Greptile

@iceteaSA
iceteaSA force-pushed the fix/tui-solid-builtins-upstream branch 2 times, most recently from 10ac5a2 to aeb6c19 Compare August 7, 2026 07:41

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/plugin/src/tui/tui-compiled-runtime-imports.test.ts Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All reported issues were addressed across 3 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread packages/plugin/src/tui/tui-compiled-runtime-imports.test.ts Outdated
…tui/solid

The sidebar and every /ctx-* command disappear, with no crash and nothing on
stderr. Reproduces on clean master.

The Solid transform routes ALL emitted imports through the single `moduleName`
build-tui.ts passes it (`@opentui/solid`) — renderer helpers and control-flow
builtins alike. `@opentui/solid` re-exports only its own renderer helpers; `For`,
`Show`, `Index`, `Switch`, `Match`, `ErrorBoundary`, `Suspense` and
`SuspenseList` live in solid-js. The `<For>` at sidebar-content.tsx:997
therefore compiled to

    import { For as _$For } from "opentui:runtime-module:%40opentui%2Fsolid";

and the host threw while loading the plugin:

    Export named 'For' not found in module 'opentui:runtime-module:@opentui/solid'

OpenCode swallows errors thrown while loading a TUI plugin, so the plugin was
dropped with no diagnostic. This is latent on master today — the committed
tui-compiled bundle carries the bad import; it turns fatal once the host
resolves that virtual module strictly.

Fix: after the transform, redirect any specifier the OpenTUI runtime does not
export to the solid-js runtime module. The split is read from the real export
sets at build time rather than hardcoded, so it keeps working when OpenTUI
changes what it re-exports — a hardcoded list would relocate the bug to the next
version bump. A name exported by neither module now fails the build instead of
shipping a bundle that breaks the TUI silently.

Verified against the live host: @opentui/solid 0.4.5 exposes 48 names and none
of the eight builtins; solid-js exports all eight. Portal and Dynamic really do
belong to @opentui/solid and are left alone. All 24 runtime specifiers in the
rebuilt bundle resolve, build:tui is reproducible, and the sidebar returns.

The specifier list moved to src/shared/tui-runtime-specifiers.ts so the build
and the guard test read one source and cannot drift apart. It sits in
src/shared/ rather than src/tui/ because build-tui.ts copies every file under
src/tui/ into the shipped bundle, and this is build/test tooling.

Regression test (src/tui/tui-compiled-runtime-imports.test.ts) resolves every
compiled specifier against the actual export sets of all 8 rewritten modules,
fails on any runtime module id outside that set, and separately asserts no
solid-js-only builtin is imported from @opentui/solid. Red-checked: 2/3 fail
against a bundle built with the pre-fix script; an injected import from an
unknown runtime module fails with the module named; duplicating an entry in the
specifier list and stubbing a module to an empty export set each fail their own
assertion.

Gates: plugin 3534/0, typecheck 0 across 3 packages, lint clean,
check:tui-compiled PASS.
@iceteaSA
iceteaSA force-pushed the fix/tui-solid-builtins-upstream branch from ffbc472 to 15371df Compare August 7, 2026 08:14
@ualtinok
ualtinok merged commit 3a38ed9 into cortexkit:master Aug 7, 2026
14 checks passed
@alfonso-magic-context

Copy link
Copy Markdown
Collaborator

Merged in 3a38ed9 and shipped in v0.34.1 — thank you for an exceptional report and fix.

We hit the same broken import in CI within the hour (a new sidebar slot used <For> unimported, and the packaged-install smoke caught the compiled path breaking), but our hotfix only fixed the instance. Your PR is the class fix: reading the real export sets at build time is exactly right, and failing the build on names neither module exports closes the silent-drop hole for good. The red-checked regression test and the entry.mjs debugging note were both kept — the latter is going into the TUI docs.

The merge carries your commit as authored; the compiled bundle was regenerated over the combined result (your routing + the now-explicit For import in the raw source).

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants