orbit/docs/PLUGIN-SDK.md
Jean b0b7812bf9 sandbox: add orbit.command + orbit.shortcut; harden ui.claim
Full sandbox re-audit: the isolation/capability model is sound (opaque origin,
fail-closed gate, irc-raw split, 'raw' withheld, namespaced storage, cloneable
event payloads). The gap was that sandboxed plugins couldn't register the slash
commands / shortcuts the registry already supports in-page.

- orbit.command(name, run, help): the host registers via the registry and relays
  (args, rest) to the guest; the plugin's action still goes through gated verbs.
- orbit.shortcut(combo, run): modifier REQUIRED — the shortcut loop fires while
  typing, so a bare-key combo would keylog and eat keystrokes; the host rejects it.
- Registration hooks are disposed on each handshake so the adoption reload can't
  duplicate them.
- ui.claim now validates the slot against the known set (bogus slot -> footer).

Verified live: /hello relays to the guest and posts to the channel. Template +
docs updated.
2026-07-07 18:32:44 +00:00

4.1 KiB

Plugin SDK

A sandboxed plugin runs in an isolated iframe and reaches the app only through the orbit API. The SDK gives you high-level building blocks so you never touch the rough edges of the sandbox (theming, frame sizing, positioning, animations) — those are solved once, in the framework.

Register a plugin:

Orbit.plugin('my-plugin', function (orbit, log) {
  // ...
});

Load it via config.json:

{ "url": "/app/plugins/third/my-plugin.js", "sandbox": true, "permissions": ["irc"] }

See public/plugins/third/orbit-hello.js for a complete ~15-line starter, and orbit-radio.js for a real one.

orbit.panel(opts) — a bar tab that opens a themed panel

The flagship helper. It gives you a nav trigger that matches the native tabs and a floating panel that opens upward, fully in view, with no flash — regardless of theme or how tall your content is. You only fill the panel body.

var p = orbit.panel({
  slot: 'nav_item',       // where the trigger lives (nav bar)
  icon: '📻',             // emoji, or an SVG string, or a DOM node
  label: 'Radio',         // tab label (keep it short/fixed)
  title: 'Radio',         // panel header title (optional; adds a close button)
  width: 264,             // panel width in px (default 280)
  render: function (body, panel) {
    body.appendChild(orbit.el.row('FIP', 'Éclectique'));   // fill the panel
  },
  onToggle: function (open) {},   // optional
});

Returns a controller:

  • p.open() / p.close() / p.toggle() / p.isOpen()
  • p.active(bool) — tint the tab with the accent colour (e.g. "playing" / "unread")
  • p.body() — the panel content element

What the SDK handles for you: matching the native tab look, the async frame resize, growing upward out of the bar, never flashing a half-drawn panel, staying transparent (no grey/black box), the open animation, and theming.

orbit.el — themed atoms

Pre-styled elements that already match light/dark:

  • orbit.el.button(label, onClick) — an accent button.
  • orbit.el.row(title, sub?, onClick?) — a list row (title + optional subtitle).
  • orbit.el.slider({ min, max, step, value, onInput }) — a themed range input.

Style anything else with the theme CSS variables, which are mirrored into the frame: --bg, --ink, --accent, --muted, --border. Text defaults to --ink, so you rarely need to set colours yourself.

orbit.ui(slot, build) — low-level slot

The primitive panel is built on. Use it only for custom widgets that aren't a panel (e.g. a topbar clock). build(root) fills root; the frame is sized to it. Slots: nav_item, topbar_item, footer_item, sidebar_item, composer_button, settings_section.

The rest of the API

  • orbit.state.nick() / .account() / .active() / .buffers() — read-only, no permission.
  • orbit.server.network() / .isupport() / .hasCap(name) / .caps() — read-only.
  • orbit.storage.get(key, fallback) / .set(key, value) — needs storage; per-plugin, persisted.
  • orbit.irc.say(text) / .msg(to, text) / .send(line) / .join(chan) / .part(chan) — needs irc.
  • orbit.notify(title, body) — needs notify; rate-limited.
  • orbit.on(event, fn) — subscribe to app events; returns an unsubscribe fn. Events:
    • message{ from, target, text, self } on each incoming PRIVMSG (no permission).
    • connected{ nick }, buffer.active → the active buffer name, status → connection status.
  • orbit.command(name, run, help?) — register a slash command. Typing /name a b calls run(["a","b"], "a b"). Returns an unregister fn. Built-in commands win.
  • orbit.shortcut(combo, run) — register a keyboard shortcut, e.g. "mod+j" or "alt+shift+r" (mod = ⌘/Ctrl). A modifier is required — bare keys are refused so a plugin can't swallow your typing. Built-in shortcuts win.
  • log(...) — console log (shown when plugin debug is on).

Permissions

Grant the least you need in config.json: irc, storage, notify, or none (explicitly nothing). Read-only state/server need no permission. Anything not granted is denied at the bridge.