Core plugin slots

This page shows the Core host API for plugin slots.

Use Core slots when registered plugins must return BaseRenderable nodes for host-defined layout regions. The host keeps control of layout and slot types. Plugins render only through the context and data that the host supplies.

Start with Plugin slots for the shared registry, mode, ordering, and error model.

What Core adds#

Core adds these APIs to createSlotRegistry:

  • createCoreSlotRegistry: Creates a registry for BaseRenderable nodes.
  • registerCorePlugin: Registers the Core-specific CorePlugin interface.
  • SlotRenderable: Mounts a slot in the renderable tree.
  • resolveCoreSlot: Resolves entries without mounting them.

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

createCoreSlotRegistry accepts the same SlotRegistryOptions as createSlotRegistry.

Core hosts use @opentui/core/runtime-plugin-support when contributions come from Bun runtime-loaded modules. See Load plugins and modules at runtime for setup and module maps.

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 OpenTUI calls it when the registry unregisters the plugin or clears all plugins.
slots Partial<Record<SlotName, CoreSlotContribution>> yes Each value is a renderer function or a managed slot object.

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

Basic usage#

A SlotRenderable extends Renderable, so any renderable parent can own it. It resolves contributions and reconciles their output as child nodes.

Each contribution must return one synchronous BaseRenderable. A promise, the slot mount itself, or a node attached to another parent causes a reported render failure.

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. Calling it removes the plugin and calls its dispose hook.

Because SlotRenderable extends Renderable, it accepts standard layout options such as width, height, flexDirection, and padding.

SlotRenderable options#

Option Type Required Description
id string no Unique renderable identifier. OpenTUI generates one when omitted.
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 width, height, flexDirection, padding, and other standard layout props

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. SlotRenderable unsubscribes from the registry, calls onDeactivate on active managed slots, and then calls onDispose on all managed slots. It destroys host-owned nodes. It detaches plugin-owned nodes for the plugin to release in onDispose.

resolveCoreSlot#

Resolve slot entries without mounting them. Use this API to inspect or render contribution 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 contribution throws while it renders, the host can create placeholder nodes for that 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}`,
    })
  },
})

The registry reports contribution failures with source core and phase render. If the placeholder throws, the registry reports phase error_placeholder, and that contribution has no placeholder output. The host owns and destroys placeholder nodes.

Managed slot contributions#

A Core slot contribution can be a plain function or a managed slot object with node ownership hooks:

registerCorePlugin(registry, {
  id: "managed-plugin",
  slots: {
    statusbar: {
      render(_ctx, data) {
        return new TextRenderable(renderer, { id: "managed", content: "managed" })
      },
      onActivate(ctx) {
        // The contribution became active in this slot.
      },
      onDeactivate(ctx) {
        // The contribution is no longer active.
      },
      onDispose(ctx) {
        // Release the plugin-owned nodes 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 Runs when the contribution becomes active in the slot
onDeactivate (ctx) => void no Runs when the contribution is no longer active
onDispose (ctx) => void no Runs when the registry removes the plugin or the host destroys the slot

Node ownership#

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

For a managed contribution, the plugin owns the returned nodes. On deactivation, the host detaches them but does not destroy them. The plugin can retain and return those nodes if the contribution becomes active again. On disposal, the host calls onDispose. The plugin must then destroy or release its nodes.

Destroying SlotRenderable unsubscribes it from the registry. It calls onDeactivate for active managed contributions, then calls onDispose for all managed contributions. Host-owned failure placeholders and fallback nodes are also destroyed.

Example#

See the Core plugin slots example.