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" }
  ]
}
FieldTypeRequiredDescription
schemaVersionnumberYesAlways 1
idstringYesUnique identifier (kebab-case recommended)
namestringYesDisplay name shown in UI
versionstringYesSemver version
entrystringYesRelative path to JS entry file
iconstringYesRelative path to SVG icon file
linksarrayNoOptional quick links shown on detail page
linesarrayYesOutput shape used for loading skeletons

Validation rules:

  • entry must be relative (not absolute)
  • entry must exist within the plugin directory
  • id must match globalThis.__openusage_plugin.id
  • icon must be relative and point to an SVG file (use fill="currentColor" for theme compatibility)
  • links[].url (if provided) must be an http:// or https:// URL
FieldTypeRequiredDescription
labelstringYesLink text shown in the provider detail quick-actions row
urlstringYesExternal 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

FieldTypeRequiredDescription
typestringYesOne of: text, progress, badge, barChart
labelstringYesStatic label shown in the UI for this line
scopestringYes"overview" or "detail" - where line appears
primaryOrdernumberNoLower number = higher priority; orders this progress line among the tray-icon candidates (see below)
periodstringNo"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 (primaryOrder is ignored on other types)
  • Lower primaryOrder wins; 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 text
  • resetsAt: optional ISO timestamp (UI shows "Resets in ..." automatically)
  • periodDurationMs: optional period length in milliseconds (enables pace indicator when combined with resetsAt)

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

ConditionResult
Plugin throws a stringError badge with that string
Plugin throws non-stringError badge with a generic fallback message
Promise rejectsError badge
Promise never resolvesError badge (timeout)
Invalid line typeError badge
Missing lines arrayError badge
Invalid progress valuesError 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.pluginDataDir for plugin-specific state/config
  • Keep probes fast (users wait on refresh)
  • Validate API responses before accessing nested fields

See Also