Files
Bonchellon 37f5c504f7 Fix dead Unreal plugin actions, sync toast flood, Team Relay theming
- ue bridge: Launch / Build now read live refs. They captured startBuild /
  launchUE from the render where plugins loaded — before the async .uproject
  and .sln detection had resolved — so both saw empty paths forever. hasProject
  used the live ref, so the menu items showed up and then did nothing.
- Get Latest / Sync to revision: summarise `p4 sync` stdout (syncSummary)
  instead of dumping every depot path into the toast. flash() caps length and
  .toast caps height so no raw output can blow up the bubble again.
- Team Relay: use the real theme tokens — --txt / --add / --del, not
  --text / --ok / --err, which don't exist and silently fell back to a
  near-white hardcode, invisible on the light theme. Bumped to 1.0.1.
- Plugin dock panels get a status-bar button next to Terminal / Log; the tabs
  were otherwise only reachable from the Actions menu.
- PLUGINS.md documents the theme token names.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-18 16:50:14 +03:00

5.1 KiB

Exbyte Depot — Plugins

Plugins add features on top of the core app so the base stays lean and specialised work ships (and updates) independently.

What language?

JavaScript / TypeScript. A plugin is a plain ES module that exports activate(host). Author in TypeScript if you like, then compile to a single JS file (esbuild index.ts --bundle --format=esm --outfile=index.js). No build step is required for plain JS.

Why JS and not native? The app's Rust backend is compiled and sealed; the extensible surface is the webview. Plugins run in the app's webview and reach the app only through the host object — never invoke directly.

How plugins are loaded

The app reads the plugin's entry source from disk (Rust) and imports it as an ES module from a blob: URL — no eval, and it stays inside the CSP (script-src 'self' blob:). Disabled plugins are never imported.

Anatomy

my-plugin/
  plugin.json     # manifest
  index.js        # ES module exporting activate(host)

plugin.json

{
  "id": "my-plugin",                // unique; [A-Za-z0-9-_.]
  "name": "My Plugin",
  "version": "1.0.0",
  "author": "You",
  "description": "What it does.",
  "entry": "index.js",              // must stay inside the plugin folder
  "minAppVersion": "0.3.5",
  "permissions": ["p4:read", "notify"],
  "contributes": { "menu": true, "views": ["notes"], "fileContext": true, "submitHooks": true }
}

Permissions (declared, shown to the user)

permission grants
p4:read read-only p4: opened, dirs, describe, changeSizes, clientSpec on host.p4
p4:write mutating p4: add, edit, del, sync, reconcile on host.p4
notify native notifications via host.notify

Without a permission, the matching APIs are simply absent (or no-ops). host.storage, host.clipboard, host.flash, host.info, host.t, host.theme are always available.

The host SDK

host.version                       // Host SDK version, e.g. "1.0"
host.plugin                        // { id, name, dir }
host.has(perm)                     // boolean

host.p4.<method>(...)              // Promise — only the whitelisted set your perms allow
host.info()                        // { userName, clientName, clientRoot } | null
host.selectedFile()                // { depotFile } | null
host.refresh()                     // re-read the Changes view
host.theme()                       // current theme's CSS-var overrides

host.ui.addMenuItem(label, run)                 // Actions-menu action
host.ui.addFileContextItem(label, run(file))    // right-click a file
host.ui.addView({ id, title, render(el, host) })// a panel; render into a DOM element (return an optional cleanup fn)
host.ui.addSubmitHook(run(files) => {ok, message})  // block/allow a commit

host.flash(text, isError?)         // toast
host.notify(title, body)           // native notification (needs "notify")
host.clipboard.write(text)         // Promise
host.storage.get/set/remove(key)   // per-plugin localStorage namespace
host.t(key, params?)               // app i18n
host.log(...args)                  // console, tagged with the plugin id

Theme tokens

A plugin's DOM inherits the app's CSS variables — use them so the panel follows the active theme. The exact names (a wrong name silently falls back to your hardcoded default, which then breaks on the opposite theme):

--accent --accent-2 --accent-deep · --bg --panel --panel-2 --panel-3 --elevated --border · --txt --muted --faint · --add (success) --edit (warning) --del (danger) · --mono (font)

There is no --text, --ok, --err, or --sans.

Contribution points

  • Menu items and views appear in the Actions menu.
  • File context items appear when you right-click a file in Changes.
  • Submit hooks run before a commit is finalised; returning { ok:false, message } prompts the user to confirm or cancel. (This is exactly where a reference-integrity guard, size check, or lint would live.)

Import methods

  1. Install from folder — Actions → Plugins… → Install from folder… → pick a folder that contains plugin.json. It's copied into the app's plugins folder.
  2. Drop-in — Actions → Plugins… → Open plugins folder, drop my-plugin/ in there, then Reload.
  3. (Planned) Registry — a signed registry (exbyte-depot-plugins repo, minisign-signed bundles) for one-click install/updates, reusing the app's updater signing infra.

Try the example

plugins-examples/hello-exbyte/ demonstrates all four contribution points plus storage, clipboard, info and notifications. Install it from folder, then:

  • Actions → Say hello 👋 and Actions → Workspace Notes
  • right-click a file → Copy depot path (plugin)
  • try to commit a file named wip* → the submit guard warns.

Safety

Plugins run in-app on the main thread — install only ones you trust. The id is path-validated, the entry cannot escape the plugin folder, and each plugin's storage is namespaced. A plugin that throws is isolated and does not crash the app.