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/3d, @opentui/core/testing).

If you need additional host-resolved modules, register your own plugin:

import { plugin } from "bun"
import { createRuntimePlugin } from "@opentui/core/runtime-plugin"

plugin(
  createRuntimePlugin({
    additional: {
      "my-runtime-module": async () => (await import("./runtime-module")) as Record<string, unknown>,
    },
  }),
)

CorePlugin interface

FieldTypeRequiredDescription
idstringyesUnique identifier. Duplicate ids throw.
ordernumbernoSort priority (ascending). Defaults to 0.
setup(ctx, renderer) => voidnoCalled once at registration time. If it throws, the plugin is not registered.
dispose() => voidnoCalled when the plugin is unregistered or the registry is cleared.
slotsPartial<Record<SlotName, CoreSlotContribution>>yesEach 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

OptionTypeRequiredDescription
idstringyesUnique renderable identifier (inherited from RenderableOptions)
registryCoreSlotRegistryyesThe registry to read plugins from
nameslot nameyesWhich slot to mount
dataobjectnoSlot data passed to plugin renderers as the second argument
modeSlotModeno"append" (default), "replace", or "single_winner". See slot modes.
fallbackBaseRenderable | BaseRenderable[] | () => ...noFallback nodes or a factory that creates them
pluginFailurePlaceholder(failure, ctx) => BaseRenderable | BaseRenderable[] | undefinednoCreates placeholder UI when a plugin throws
…layout optionsRenderableOptionsnoStandard layout props: width, height, flexDirection, padding, etc.

Instance API

MemberDescription
modeGetter/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 { resolveCoreSlot } from "@opentui/core"

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

FieldTypeRequiredDescription
render(ctx, data) => BaseRenderableyesCreates the renderable node for this slot
onActivate(ctx) => voidnoCalled when the plugin becomes active (visible) in the slot
onDeactivate(ctx) => voidnoCalled when the plugin is no longer active (e.g. lost single_winner)
onDispose(ctx) => voidnoCalled 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/core/src/examples/core-plugin-slots-demo.ts