Skip to content

tui: slot render returning plain string crashes with "Orphan text error"; @opentui/* unimportable in plugins #42338

Closed as not planned

Description

@mirtalha1

Summary

TUI crashes with Orphan text error: "<string>" must have a <text> as a parent when a TUI plugin slot (sidebar.content) returns a plain string from render(). Plugins are effectively forced to return plain strings because importing @opentui/* inside a TUI plugin fails on next-17403 with OPENTUI_FORCE_WCWIDTH already registered with different configuration (host bug).

Environment

  • opencode version: 0.0.0-next-17403 (next channel)
  • OS: Linux 5.15.0-1081-oracle fix typo in [www/src/content/docs/docs/config.mdx] #87~20.04.1-Ubuntu SMP Mon Apr 28 12:53:11 UTC 2025 (aarch64 / arm64)
  • Terminal: VS Code 1.133.0 (TERM=xterm-256color, truecolor)
  • Shell: /bin/bash
  • Install/channel: next (via ~/.opencode/bin/opencode2)
  • Active plugins: usage-tracker.js (server), usage-tracker-tui.js (local TUI plugin at ~/.config/opencode/tui/usage-tracker-tui.js)

Reproduction

  1. Register a TUI plugin claiming the sidebar slot whose render returns a plain string:
import { Plugin } from "@opencode-ai/plugin/tui";

export default Plugin.define({
  id: "plain-string-slot",
  setup: (ctx) => {
    const claim = ctx.ui.slot({
      append: "sidebar.content",
      render: () => "Usage\nin 545.3k  out 125.8k\nre 129.7k  cache 139.8M/0\nhit 100%  cost $0.2697\ntotal 140.6M  steps 68",
    });
    return () => claim();
  },
});
  1. Open the TUI, navigate to a session view with the sidebar rendered.
  2. TUI crashes: Orphan text error: "Usage\nin 545.3k ..." must have a <text> as a parent: box-318 above text-node-554.

Expected Behavior

Either of:

  • A plain string returned from a slot render() is wrapped in a <text> node by the host (so strings are a valid slot payload), or
  • @opentui/* is importable from TUI plugin code so plugins can return proper element trees.

Neither path should crash the TUI. The crash takes down the whole terminal UI, not just the slot.

Actual Behavior

Full TUI crash with the orphan-text error. Stack trace:

Error: Orphan text error: "Usage
in 545.3k  out 125.8k
re 129.7k  cache 139.8M/0
hit 100%  cost $0.2697
total 140.6M  steps 68" must have a <text> as a parent: box-318 above text-node-554
    at w$ (../../node_modules/.bun/@[email protected]+b9027845aeff97b0/node_modules/@opentui/solid/index.bun.js:559:17)
    at N (../../node_modules/.bun/@[email protected]+b9027845aeff97b0/node_modules/@opentui/solid/index.bun.js:442:7)
    at P (../../node_modules/.bun/@[email protected]+b9027845aeff97b0/node_modules/@opentui/solid/index.bun.js:311:13)
    ...
    at children (../tui/src/routes/session/sidebar.tsx:66:7)
    ...
    at children (../tui/src/routes/session/index.tsx:1143:22)

Additional Context

  • The offending plugin was written to return a plain string only because importing @opentui/* in a TUI plugin dies with OPENTUI_FORCE_WCWIDTH already registered with different configuration — so on next-17403 there is no working way to render styled TUI content from a plugin slot.
  • Crash is deterministic: every render of the slot once the plugin has data. Sidebar is unrecoverable while the plugin is active.
  • The session being viewed at crash time had 68 steps; the sidebar showed live usage stats (input/output/reasoning/cache/cost).

Activity

  1. mirtalha1 commented on Aug 14, 2026

    @mirtalha1
    Author

    Recurred ~25 min after filing, same build (next-17403), same error signature on the no-data path:

    Error: Orphan text error: "Usage: no data yet — chat to track tokens" must have a <text> as a parent: box-3763 above text-node-9238
        at w$ (../../node_modules/.bun/@[email protected]+b9027845aeff97b0/node_modules/@opentui/solid/index.bun.js:559:17)
        ...
        at <anonymous> (../../tui/src/plugin/context.tsx:74:5)
    

    So the crash hits both code paths (empty state and populated state) — any render of the slot payload crashes the TUI, not just data-rich renders.

    Local workaround for the OPENTUI_FORCE_WCWIDTH already registered import blocker — confirmed working on next-17403: clear the host's env-registry singleton before importing @opentui/solid:

    const sym = Symbol.for("@opentui/core/singleton");
    if (globalThis[sym] && globalThis[sym]["env-registry"]) {
      delete globalThis[sym]["env-registry"];
    }
    solid = await import("@opentui/solid");

    With that, slot render() can return proper element trees built via createElement("box") / createElement("text") + setProp/insert (solid universal API re-exported by @opentui/solid), and the orphan-text crash disappears. If the import still fails, returning null from render() is tolerated by the host (renders nothing, no crash).

    Host-side fixes still warranted regardless:

    1. A plain string returned from a slot render() should be wrapped in a <text> node (or rejected with a per-slot error) instead of crashing the whole TUI.
    2. The @opentui/core singleton collision shouldn't be reachable from plugin module code — plugins loading a second copy of the bundled deps is a normal resolution outcome.
  2. github-actions commented on Oct 15, 2026

    @github-actions
    Contributor

    To stay organized issues are automatically closed after 60 days of no activity. If the issue is still relevant please open a new one.

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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions