Plugin structure, manifest format, output schema, and lifecycle.
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)
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>/.
{
"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
| 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) |
Plugins must declare their output shape in plugin.json. This enables the UI to render
loading skeletons instantly while probes execute asynchronously.
| 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
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" }
]
}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" }
]
}Plugins must register themselves on the global object:
globalThis.__openusage_plugin = {
id: "my-provider", // Must match manifest.id
probe: function(ctx) { ... }
}probe(ctx) must return (or resolve to):
{ lines: MetricLine[] }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)
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" })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 …"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",
})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" })| 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).
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 }),
],
}
},
}
})()- 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
- Host API Reference - Full documentation of
ctx.host.*APIs