- 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>
115 lines
5.1 KiB
Markdown
115 lines
5.1 KiB
Markdown
# 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
|
|
```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
|
|
```ts
|
|
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.
|