Core slots

This page shows the core host API for plugin slots.

Use slots when you want external modules to render BaseRenderable UI in host-defined layout regions without forking the app. The host keeps control of layout and slot typing; plugins only render through the APIs you expose.

If you have not read the shared model yet, start with Plugin Slots.

What core adds

On top of createSlotRegistry, core provides:

  • createCoreSlotRegistry — create a registry typed for BaseRenderable nodes
  • registerCorePlugin — register a plugin using the core-specific CorePlugin interface
  • SlotRenderable — a Renderable that mounts a slot into the renderable tree
  • resolveCoreSlot — resolve slot entries without mounting
  • @opentui/core/runtime-plugin-support / createRuntimePlugin — runtime module support for external plugin/module loading in Bun

Core slot renderers receive both the host context and slot data: (ctx, data) => BaseRenderable.

createCoreSlotRegistry accepts the same SlotRegistryOptions as createSlotRegistry.

Runtime-loaded external plugins

If your app loads plugin modules from disk at runtime, import this once in your app entry:

import "@opentui/core/runtime-plugin-support"

This installs Bun runtime support for @opentui/core plus default core runtime entrypoints (@opentui/core/testing). Add first-party package runtime modules explicitly when a plugin imports them.

If you need additional host-resolved modules, configure runtime support before the side-effect import is loaded:

const { ensureRuntimePluginSupport } = await import("@opentui/core/runtime-plugin-support/configure")

ensureRuntimePluginSupport({
  additional: {
    "my-runtime-module": { marker: "ok" },
  },
})

For plugins that import @opentui/three, install @opentui/three in the host app and pass its runtime module map:

import { runtimeModules as threeRuntimeModules } from "@opentui/three/runtime-modules"
import { ensureRuntimePluginSupport } from "@opentui/core/runtime-plugin-support/configure"

ensureRuntimePluginSupport({
  additional: threeRuntimeModules,
})

The side-effect import and configurable entrypoint install the same runtime plugin. Late additions throw with a clear error instead of being ignored.

CorePlugin interface

Field Type Required Description
id string yes Unique identifier. Duplicate ids throw.
order number no Sort priority (ascending). Defaults to 0.
setup (ctx, renderer) => void no Called once at registration time. If it throws, the plugin is not registered.
dispose () => void no Called when the plugin is unregistered or the registry is cleared.
slots Partial<Record<SlotName, CoreSlotContribution>> yes Each value is a renderer function or a managed slot object.

A CoreSlotContribution is either a plain renderer (ctx, data) => BaseRenderable or a CoreManagedSlot with lifecycle hooks.

Basic usage

A SlotRenderable extends Renderable, so it can be added to any parent like a BoxRenderable. It resolves plugins from the registry, manages their lifecycle, and reconciles their output as its children.

import {
  BoxRenderable,
  createCliRenderer,
  createCoreSlotRegistry,
  registerCorePlugin,
  SlotRenderable,
  TextRenderable,
} from "@opentui/core"

type Slots = "statusbar"
type SlotData = { label: string }
const context = { appName: "core-app", version: "1.0.0" }

const renderer = await createCliRenderer()

const registry = createCoreSlotRegistry<Slots, typeof context, SlotData>(renderer, context)

const unregister = registerCorePlugin(registry, {
  id: "clock-plugin",
  order: 0,
  slots: {
    statusbar(_ctx, data) {
      return new TextRenderable(renderer, {
        id: "clock-status",
        content: `clock: ${data.label}`,
      })
    },
  },
})

const slot = new SlotRenderable(renderer, {
  id: "statusbar-slot",
  registry,
  name: "statusbar",
  data: { label: "ok" },
  mode: "append",
  width: "100%",
  height: 3,
  flexDirection: "row",
  fallback: () =>
    new TextRenderable(renderer, {
      id: "statusbar-fallback",
      content: "fallback",
    }),
})

renderer.root.add(slot)

// later
slot.mode = "replace"
slot.data = { label: "updated" }
slot.refresh()
slot.destroy()

registerCorePlugin returns an unregister function (() => void). Calling it removes the plugin and invokes its dispose hook.

Because SlotRenderable extends Renderable, it accepts all standard layout options (width, height, flexDirection, padding, etc.) alongside the slot-specific options.

SlotRenderable options

Option Type Required Description
id string yes Unique renderable identifier (inherited from RenderableOptions)
registry CoreSlotRegistry yes The registry to read plugins from
name slot name yes Which slot to mount
data object no Slot data passed to plugin renderers as the second argument
mode SlotMode no "append" (default), "replace", or "single_winner". See slot modes.
fallback BaseRenderable | BaseRenderable[] | () => ... no Fallback nodes or a factory that creates them
pluginFailurePlaceholder (failure, ctx) => BaseRenderable | BaseRenderable[] | undefined no Creates placeholder UI when a plugin throws
…layout options RenderableOptions no Standard layout props: width, height, flexDirection, padding, etc.

Instance API

Member Description
mode Getter/setter. Changing the mode automatically refreshes the slot.
refresh() Re-resolve plugins and reconcile mounted children.
destroy() Inherits from Renderable. In addition to the standard cleanup, SlotRenderable unsubscribes from the registry, calls onDeactivate on active managed slots, then calls onDispose on all managed slots. Host-owned nodes (plain function contributions) are destroyed; plugin-owned nodes (managed slot contributions) are detached but left for the plugin to clean up in onDispose.

resolveCoreSlot

Resolve slot entries without mounting them. Useful when you need to inspect or render plugin output manually.

import { createCliRenderer, createCoreSlotRegistry, resolveCoreSlot } from "@opentui/core"

const renderer = await createCliRenderer()
const registry = createCoreSlotRegistry<"statusbar">(renderer, {
  appName: "core-app",
  version: "1.0.0",
})

const entries = resolveCoreSlot(registry, "statusbar")
// Array<{ id: string, renderer: (ctx, data) => BaseRenderable }>

Plugin failure placeholders

If a plugin throws during slot render, you can provide host-controlled fallback UI for that plugin failure:

const slot = new SlotRenderable(renderer, {
  registry,
  name: "statusbar",
  fallback: () => new TextRenderable(renderer, { id: "fallback", content: "fallback" }),
  pluginFailurePlaceholder(failure, ctx) {
    return new TextRenderable(renderer, {
      id: `error-${failure.pluginId}`,
      content: `plugin error: ${failure.pluginId}`,
    })
  },
})

Managed slot contributions

A core slot contribution can be a plain function or a managed slot object with lifecycle hooks:

registerCorePlugin(registry, {
  id: "managed-plugin",
  slots: {
    statusbar: {
      render(_ctx, data) {
        return new TextRenderable(renderer, { id: "managed", content: "managed" })
      },
      onActivate(ctx) {
        // plugin became active in this slot (visible)
      },
      onDeactivate(ctx) {
        // plugin is no longer active (e.g. mode changed to single_winner and this plugin lost)
      },
      onDispose(ctx) {
        // plugin is removed/disposed for this slot
      },
    },
  },
})

CoreManagedSlot interface

Field Type Required Description
render (ctx, data) => BaseRenderable yes Creates the renderable node for this slot
onActivate (ctx) => void no Called when the plugin becomes active (visible) in the slot
onDeactivate (ctx) => void no Called when the plugin is no longer active (e.g. lost single_winner)
onDispose (ctx) => void no Called when the plugin is removed or the slot is destroyed

Node ownership

When a slot contribution is a plain function, the host owns the returned nodes. On deactivation or disposal, the host detaches and destroys them.

When a slot contribution is a managed slot object, the plugin owns the nodes. On deactivation the host detaches them but does not destroy them — the plugin can reuse them if it becomes active again. On disposal, onDispose is called so the plugin can clean up.

Observability

registry.onPluginError((event) => {
  console.error(event.pluginId, event.phase, event.source, event.error.message)
})

Example: packages/examples/src/core-plugin-slots-demo.ts