Plugin Schema
June 6, 2026 · View on GitHub
Plugin structure, manifest format, output schema, and lifecycle.
Architecture Overview
Auto-update timer fires (or app loads)
|
Tauri command `run_plugin_probes(pluginIds?)`
|
For each enabled plugin:
-> Create fresh QuickJS sandbox
-> Inject host APIs (`ctx.host.*`)
-> Evaluate plugin.js
-> Call `probe(ctx)`
-> Parse returned `{ lines: MetricLine[] }`
|
Return `PluginOutput[]` to frontend
|
UI renders via ProviderCard component
Key points:
- Each probe runs in isolated QuickJS runtime (no shared state between plugins or calls)
- Plugins are synchronous or Promise-based (unresolved promises timeout)
- Auto-update timer - runs on app load and on configurable interval (5/15/30/60 min)
Plugin Directory Layout
plugins/<id>/
plugin.json <- manifest (required)
plugin.js <- entry script (required)
icon.svg <- plugin icon (required)
Bundled plugins live under src-tauri/resources/bundled_plugins/<id>/.
Manifest Schema (plugin.json)
{
"schemaVersion": 1,
"id": "my-provider",
"name": "My Provider",
"version": "0.0.1",
"entry": "plugin.js",
"icon": "icon.svg",
"links": [{ "label": "Status", "url": "https://status.example.com" }],
"lines": [
{ "type": "badge", "label": "Plan", "scope": "overview" },
{ "type": "progress", "label": "Usage", "scope": "overview", "primaryOrder": 1 },
{ "type": "text", "label": "Details", "scope": "detail" }
]
}
| Field | Type | Required | Description |
|---|---|---|---|
schemaVersion | number | Yes | Always 1 |
id | string | Yes | Unique identifier (kebab-case recommended) |
name | string | Yes | Display name shown in UI |
version | string | Yes | Semver version |
entry | string | Yes | Relative path to JS entry file |
icon | string | Yes | Relative path to SVG icon file |
links | array | No | Optional quick links shown on detail page |
lines | array | Yes | Output shape used for loading skeletons |
Validation rules:
entrymust be relative (not absolute)entrymust exist within the plugin directoryidmust matchglobalThis.__openusage_plugin.idiconmust be relative and point to an SVG file (usefill="currentColor"for theme compatibility)links[].url(if provided) must be anhttp://orhttps://URL
Links Array (Optional)
| Field | Type | Required | Description |
|---|---|---|---|
label | string | Yes | Link text shown in the provider detail quick-actions row |
url | string | Yes | External destination opened in the browser (http/https only) |
Output Shape Declaration
Plugins must declare their output shape in plugin.json. This enables the UI to render
loading skeletons instantly while probes execute asynchronously.
Lines Array
| Field | Type | Required | Description |
|---|---|---|---|
type | string | Yes | One of: text, progress, badge, barChart |
label | string | Yes | Static label shown in the UI for this line |
scope | string | Yes | "overview" or "detail" - where line appears |
primaryOrder | number | No | Lower number = higher priority; orders this progress line among the tray-icon candidates (see below) |
period | string | No | "weekly" marks this line as the provider's weekly metric (see below) |
"overview"- shown on both Overview tab and plugin detail pages"detail"- shown only on plugin detail pages
Primary Progress (Tray Icon)
Progress lines opt into the system tray icon by setting primaryOrder (a number). Lines are sorted by primaryOrder into an ordered list of candidates, and the tray shows the first candidate that has runtime data — falling back to the next when an earlier one is absent. This lets a provider prefer a short-window metric but degrade gracefully when it isn't reported.
Rules:
- Only
type: "progress"lines are candidates (primaryOrderis ignored on other types) - Lower
primaryOrderwins; the frontend walks the ordered list and uses the first one present in live data - Up to 4 enabled plugins are shown in the tray (in plugin order)
- If no data is available yet, the bar shows as a track without fill
Example:
{
"lines": [
{ "type": "badge", "label": "Plan", "scope": "overview" },
{ "type": "progress", "label": "Plan usage", "scope": "overview", "primaryOrder": 1 },
{ "type": "progress", "label": "Overage", "scope": "overview", "primaryOrder": 2 },
{ "type": "text", "label": "Resets", "scope": "detail" }
]
}
Weekly Metric (Menubar)
A provider can mark one progress line with "period": "weekly". When the user sets the menubar metric to Weekly (Settings → Menubar Icon), the tray icon and tooltip show this line instead of the provider's primary metric.
It is an override of the primary metric, not a standalone mode: the provider must still define a primary (primaryOrder) line — a provider with only a weekly line will not appear in the menubar. Providers without a weekly line keep showing their primary. period only recognizes "weekly" (other values are ignored), and only the first "period": "weekly" line is used.
{
"lines": [
{ "type": "progress", "label": "Session", "scope": "overview", "primaryOrder": 1 },
{ "type": "progress", "label": "Weekly", "scope": "overview", "period": "weekly" }
]
}
Entry Point Structure
Plugins must register themselves on the global object:
globalThis.__openusage_plugin = {
id: "my-provider", // Must match manifest.id
probe: function(ctx) { ... }
}
Output Schema
probe(ctx) must return (or resolve to):
{ lines: MetricLine[] }
Line Types
type MetricLine =
| { type: "text"; label: string; value: string; color?: string; subtitle?: string }
| {
type: "progress";
label: string;
used: number;
limit: number;
format:
| { kind: "percent" }
| { kind: "dollars" }
| { kind: "count"; suffix: string };
resetsAt?: string; // ISO timestamp
periodDurationMs?: number; // period length in ms for pace tracking
color?: string;
}
| { type: "badge"; label: string; text: string; color?: string; subtitle?: string }
| {
type: "barChart";
label: string;
points: Array<{ label: string; value: number; valueLabel?: string }>;
note?: string;
color?: string;
}
color: optional hex string (e.g.#22c55e)subtitle: optional text displayed below the line in smaller muted textresetsAt: optional ISO timestamp (UI shows "Resets in ..." automatically)periodDurationMs: optional period length in milliseconds (enables pace indicator when combined withresetsAt)
Text Line
Simple label/value pair.
ctx.line.text({ label: "Account", value: "user@example.com" })
ctx.line.text({ label: "Status", value: "Active", color: "#22c55e", subtitle: "Since Jan 2024" })
Progress Line
Shows a progress bar with optional formatting.
ctx.line.progress({ label: "Usage", used: 42, limit: 100, format: { kind: "percent" } })
// Renders (depending on user settings): "42%" or "58% left"
ctx.line.progress({ label: "Spend", used: 12.34, limit: 100, format: { kind: "dollars" } })
// Renders: "\$12.34" or "\$87.66 left"
ctx.line.progress({
label: "Session",
used: 75,
limit: 100,
format: { kind: "percent" },
resetsAt: ctx.util.toIso("2026-02-01T00:00:00Z"),
})
// UI will show: "Resets in …"
Bar Chart Line
Shows a compact vertical bar chart for small history snapshots.
ctx.line.barChart({
label: "Usage Trend",
points: [
{ label: "Feb 1", value: 1200, valueLabel: "1.2K tokens" },
{ label: "Feb 2", value: 2400, valueLabel: "2.4K tokens" },
],
note: "Estimated from local logs",
})
Badge Line
Status indicator with colored border.
ctx.line.badge({ label: "Plan", text: "Pro", color: "#000000" })
ctx.line.badge({ label: "Status", text: "Connected", color: "#22c55e", subtitle: "Last sync 5m ago" })
Error Handling
| Condition | Result |
|---|---|
| Plugin throws a string | Error badge with that string |
| Plugin throws non-string | Error badge with a generic fallback message |
| Promise rejects | Error badge |
| Promise never resolves | Error badge (timeout) |
| Invalid line type | Error badge |
Missing lines array | Error badge |
| Invalid progress values | Error badge (line-specific validation error) |
Prefer throwing short, actionable strings (not Error objects).
Minimal Example
A complete, working plugin that fetches data and displays all three line types.
plugin.json:
{
"schemaVersion": 1,
"id": "minimal",
"name": "Minimal Example",
"version": "0.0.1",
"entry": "plugin.js",
"icon": "icon.svg",
"lines": [
{ "type": "badge", "label": "Status", "scope": "overview" },
{ "type": "progress", "label": "Usage", "scope": "overview", "primaryOrder": 1 },
{ "type": "text", "label": "Fetched at", "scope": "detail" }
]
}
plugin.js:
(function () {
globalThis.__openusage_plugin = {
id: "minimal",
probe: function (ctx) {
let resp
try {
resp = ctx.host.http.request({
method: "GET",
url: "https://httpbin.org/json",
timeoutMs: 5000,
})
} catch (e) {
throw "Request failed. Check your connection."
}
if (resp.status !== 200) {
throw "Request failed (HTTP " + resp.status + "). Try again later."
}
let data
try {
data = JSON.parse(resp.bodyText)
} catch {
throw "Invalid JSON. Try again later."
}
return {
lines: [
ctx.line.badge({ label: "Status", text: "Connected", color: "#22c55e" }),
ctx.line.progress({
label: "Usage",
used: 42,
limit: 100,
format: { kind: "percent" },
resetsAt: ctx.util.toIso("2026-02-01T00:00:00Z"),
}),
ctx.line.text({ label: "Fetched at", value: ctx.nowIso }),
],
}
},
}
})()
Best Practices
- Wrap all host API calls in try/catch
- Throw short, user-friendly strings (not raw exception objects)
- Use
ctx.app.pluginDataDirfor plugin-specific state/config - Keep probes fast (users wait on refresh)
- Validate API responses before accessing nested fields
See Also
- Host API Reference - Full documentation of
ctx.host.*APIs