Plugin system

Orbit has a small, operator-controlled plugin system. A deployment lists plugin scripts in config.json; each is loaded at startup and registers against a global Orbit object to hook events, read state, send IRC, theme the UI, and add UI in predefined slots.

Experimental. Orbit is a work in progress and this API may change between releases. Plugins are deployment-controlled — same trust level as the app itself. There is no user-uploaded plugin marketplace, by design.

Enabling plugins

Add script URLs to plugins in config.json:

{ "plugins": ["/app/plugins/orbit-demo.js"] }

They load in order, after the app boots. Host them anywhere the page can reach (same-origin recommended). For a third-party origin, pin the file with Subresource Integrity by giving an object instead of a URL:

{ "plugins": [{ "url": "https://cdn.example/x.js", "integrity": "sha384-…" }] }

(crossorigin defaults to anonymous when an integrity hash is set.)

A quick plugin

A plugin is a plain .js file that calls Orbit.plugin(). UI is authored with the html tagged template (runtime markup, no build step), or with orbit.h(...) (React.createElement).

Orbit.plugin('my-plugin', (orbit, log) => {
  log('loaded, Orbit v' + orbit.version);

  orbit.on('message', (m) => log(m.from, 'said', m.text, 'in', m.target));

  orbit.addUi('composer_button', () =>
    orbit.html`<button class="composer__emoji" title="Shrug"
      onClick=${() => orbit.irc.say('¯\\_(ツ)_/¯')}>🤷</button>`);
});

For anything substantial — real components with state — build a compiled plugin instead. See Compiled plugins.

The Orbit API

Global

Member Description
Orbit.version / Orbit.commit app version + git commit (build-time)
Orbit.apiVersion plugin API contract version (bumped on breaking changes) — guard with if (Orbit.apiVersion < N) …
Orbit.plugin(name, fn) register a plugin; fn(orbit, log)
Orbit.on/once/off/emit(event, …) the app event bus
Orbit.config() the resolved runtime config
Orbit.React / Orbit.ReactDOM / Orbit.jsxRuntime / Orbit.Fragment / Orbit.h / Orbit.html render primitives (externalization targets for compiled plugins)

Inside Orbit.plugin(name, (orbit) => …)

Member Description
orbit.on/once/off/emit event bus (see events below)
orbit.state.active() active buffer name
orbit.state.nick() / account() your nick / logged-in account
orbit.state.buffers() open buffer names
orbit.state.get() full store snapshot (read-only)
orbit.irc.send(line) send a raw IRC line
orbit.irc.msg(target, text) PRIVMSG a target
orbit.irc.say(text) send to the active buffer
orbit.irc.join(chan) / part(chan) join / part
orbit.irc.list() request the channel list
orbit.themes.current()/list()/set(id) read/set the theme
orbit.storage.get(key, def)/set(key, val) namespaced persistence
orbit.config() the resolved runtime config (branding, features, …)
orbit.i18n.language() current UI language code (e.g. es, pt-BR)
orbit.i18n.t(key, opts?) translate an app locale key (supports {{interpolation}})
orbit.i18n.pick(table) pick a string from a { lang: text } table by the current language
orbit.addUi(slot, render) add UI to a slot (returns a remover)
orbit.addSettingsSection({label, icon?, render}) add a whole Settings section
orbit.addMessageDecorator(m => …) inline UI after every message's text; m = {id, nick, text, raw, kind, ts, mine} (text is formatting-stripped, raw keeps mIRC codes)
orbit.addMessageAction(m => …) a button in every message's hover action toolbar (next to reply/react); same m
orbit.h / orbit.html render helpers
log(…) namespaced console logger

Events

ready, connected ({nick}), status (connection status string), buffer.active (buffer name), message ({from, target, text, self}), raw (the parsed IRC message).

UI slots

Slot Where
composer_button a button in the message composer toolbar
topbar_item an item in the channel topbar action row (next to search / notifications)
sidebar_item an item in the conversation sidebar header (next to the compose button)
footer_item a button in the app footer bar's actions (next to the away / settings buttons) — style it with the appbar__act class to match
settings_section a whole section in Settings (own nav entry + pane) — use orbit.addSettingsSection()

Two per-message hooks run for every rendered message: orbit.addMessageDecorator(m => …) appends inline UI after the text, and orbit.addMessageAction(m => …) adds a button to the hover action toolbar next to reply/react. Every contributed slot, action and decorator renders inside its own error boundary, so a crashing plugin renders nothing instead of taking down the app.

Localization (i18n)

The UI ships in 10 languages — keep your plugin in step so it isn't stuck in one. Two ways:

  • Self-contained — carry a { lang: text } table and let Orbit pick the current language:

js const LABEL = { en: 'Copy', fr: 'Copier', de: 'Kopieren' }; orbit.html`<button title=${orbit.i18n.pick(LABEL)}>⧉</button>`;

  • Interpolatedorbit.i18n.t('key', { name }) resolves an app locale key with {{placeholders}}, so word order is correct per language. The four bundled plugins (clock, copy, invite, games) use this.

Read the string at render time, not once at load: plugin UI re-renders when the language changes, so the text follows automatically. orbit.i18n.language() returns the current code if you need to branch.

orbit.i18n and orbit.config() were added in apiVersion 4 — guard with if (orbit.apiVersion >= 4) if your plugin must run on older builds.

Trust & security

Plugins are operator-controlled: a deployment lists them in config.json, so they run with the same trust as the app itself. There is no user-uploaded plugin mechanism. Orbit deliberately does not expose internal modules or runtime component replacement — that would couple plugins to internals that are still moving. The API above is the stable surface.