ggbro Plugins

Client Plugins (Desktop)

Build sandboxed plugins for the ggbro desktop app. Custom packs show UI in Settings → Plugins. A small set of official host apps also open from the Apps (9-dot) grid. Local install only — no marketplace, no remote URL install.

Safety first. Plugins run in a dedicated Web Worker (no DOM) and talk to the host over a MessageChannel. They cannot read your auth token, call Node/Electron APIs, or touch the main React DOM. Only install packs you trust — treat them like local software.

Apps grid vs Settings panels

Two related surfaces — both work for custom packs that declare ui.panel.

Settings → Plugins (schema panels)Apps grid (9-dot)
Who Any pack with ui.panel that calls ggbro.ui.registerPanel Official host apps and enabled packs with ui.panel (your custom packs)
Examples Hello Sample, Client Lab controls, Pomodoro companion panel PDF Studio, Youtube, Discord Theme, Spotify, Hello, …
UI source Host draws your schema (inputs, buttons, selects) — no custom HTML/React in the sandbox Official apps: host React / webview. Custom packs: same schema Controls inside an Apps window
Custom packs? Yes — Controls under the pack card + Active plugin controls Yes — tile appears when the pack is enabled and has ui.panel. Use Open controls in Apps on the card
Rule of thumb. Building your own plugin? Call registerPanel, enable the pack, then open it from Apps or use Controls on Settings → Plugins. Inputs need Apply (or Enter) before Calculate sees new values.

Build your own plugin (step by step)

Authors ship their own packs — there is no built-in Calculator. Follow this checklist so Controls and Apps tiles actually appear.

Install layout (most common mistake). Put manifest.json and plugin.js in a dedicated folder (e.g. Documents\my-calculator\), not loose in Documents\ next to unrelated files. In the desktop app use Install Plugin → Folder and select that folder (or zip only those two files into a .ggbro-plugin).

1. Create two files in one folder

my-calculator/
  manifest.json
  plugin.js

2. manifest.json

{
  "id": "com.example.calculator",
  "name": "Calculator",
  "version": "1.0.0",
  "author": "You",
  "description": "Simple A + B calculator with history.",
  "main": "plugin.js",
  "permissions": ["ui.panel", "ui.toast", "storage"]
}

3. plugin.js — required panel rules

// plugin.js — register the panel FIRST, then load storage
(async function () {
  var numA = '0';
  var numB = '0';
  var operator = '+';
  var resultDisplay = '0';
  var history = [];

  async function renderUI() {
    var historyText = history.length
      ? history.slice(-5).reverse().join('\n')
      : 'No previous calculations.';

    await ggbro.ui.registerPanel({
      id: 'main',
      title: 'Quick Calculator',
      schema: [
        { type: 'section', id: 's_inputs', title: 'Inputs & Operation' },
        { type: 'input', id: 'num_a', label: 'First Number (A)', value: numA },
        {
          type: 'select',
          id: 'op',
          label: 'Operator',
          value: operator,
          options: [
            { value: '+', label: '+' },
            { value: '-', label: '-' },
            { value: '*', label: '*' },
            { value: '/', label: '/' }
          ]
        },
        { type: 'input', id: 'num_b', label: 'Second Number (B)', value: numB },
        { type: 'button', id: 'calc', label: 'Calculate' },
        { type: 'section', id: 's_result', title: 'Result' },
        { type: 'text', id: 'out', label: 'Output', value: resultDisplay },
        { type: 'section', id: 's_hist', title: 'History' },
        { type: 'textarea', id: 'hist', label: 'Last 5', value: historyText, rows: 4 },
        { type: 'button', id: 'clear', label: 'Clear History' }
      ]
    });
  }

  // Show Controls immediately
  await renderUI();

  try {
    var stored = await ggbro.storage.get('calc_history');
    if (stored && stored.value) {
      history = JSON.parse(stored.value);
      await renderUI();
    }
  } catch (e) {
    ggbro.log('storage', e && e.message);
  }

  await ggbro.toast({
    message: 'Calculator ready — open Settings → Plugins for controls.',
    type: 'success'
  });

  ggbro.onEvent(async function (event, payload) {
    if (event !== 'panelAction' || !payload) return;

    if (payload.fieldId === 'num_a') { numA = String(payload.value != null ? payload.value : ''); return; }
    if (payload.fieldId === 'num_b') { numB = String(payload.value != null ? payload.value : ''); return; }
    if (payload.fieldId === 'op') { operator = String(payload.value != null ? payload.value : '+'); return; }

    if (payload.fieldId === 'clear') {
      history = [];
      await ggbro.storage.clear('calc_history');
      await ggbro.toast({ message: 'History cleared', type: 'info' });
      await renderUI();
      return;
    }
    if (payload.fieldId !== 'calc') return;

    var a = parseFloat(numA);
    var b = parseFloat(numB);
    if (isNaN(a) || isNaN(b)) {
      resultDisplay = 'Error: Invalid Number';
      await ggbro.toast({ message: 'Enter valid numbers.', type: 'error' });
      await renderUI();
      return;
    }
    var res = 0;
    if (operator === '+') res = a + b;
    else if (operator === '-') res = a - b;
    else if (operator === '*') res = a * b;
    else if (operator === '/') {
      if (b === 0) {
        resultDisplay = 'Error: Division by 0';
        await ggbro.toast({ message: 'Cannot divide by zero.', type: 'error' });
        await renderUI();
        return;
      }
      res = a / b;
    }
    resultDisplay = String(res);
    history.push(a + ' ' + operator + ' ' + b + ' = ' + res);
    await ggbro.storage.set('calc_history', JSON.stringify(history));
    await ggbro.toast({ message: 'Result: ' + res, type: 'success' });
    await renderUI();
  });
})();

4. Package

# From inside my-calculator\ (PowerShell) — files must be at the zip root
Compress-Archive -Path manifest.json,plugin.js -DestinationPath my-plugin.ggbro-plugin.zip
Rename-Item my-plugin.ggbro-plugin.zip my-plugin.ggbro-plugin

Also accepted: leave it as .zip, or pick the folder in the install dialog.

5. Install & find the UI

  1. Desktop app → User Settings → PluginsInstall Plugin.
  2. Pick your .ggbro-plugin / .zip / folder. New packs install enabled.
  3. You should see:
    • A toast from your pack (if it uses ui.toast)
    • Controls on the pack card (and Active plugin controls near the top)
    • Open controls in Apps + an Apps (9-dot) tile when you declare ui.panel
  4. If Controls stay empty: update to the latest desktop app, then press Refresh. Check the red Runtime: line on the card.

Optional reference packs (Hello / Client Lab) are examples only — authors are expected to ship their own plugins. Clone hello/plugin.js if you want a starting point.

Install

  1. Open the ggbro desktop app (plugins are not available on web or mobile).
  2. User Settings → Plugins.
  3. Click Install & Enable on an official example (or Install / Update All Examples), or download a .ggbro-plugin pack and use Install Plugin.
  4. Official catalog installs turn the pack ON automatically.
  5. Custom / starter packs: use Controls on this Settings page, or the Apps grid tile / Open controls in Apps.
    Official host apps (PDF, YouTube, Discord Theme, Spotify, …): richer Apps windows plus companions.

Windows note: file and folder pickers are separate (Windows cannot filter .ggbro-plugin files in a combined dialog).

Re-installing the same id replaces the pack files and keeps the previous enabled/disabled state (catalog install re-enables).

Free official examples

Prefer one-click install from the desktop app: Settings → Plugins → Official examplesInstall & Enable. You can still download a single .ggbro-plugin file below and use Install Plugin. These packs are published on the ggbro site — still review permissions before leaving them enabled.

Catalog JSON: /developers/examples/plugins/index.json (used by the app for install + update checks). View pack / View source opens the .ggbro-plugin contents.

Two kinds of official examples
Productivity

PDF Studio

Advanced PDF viewer & editor — annotate, organize, compress, watermark, passwords, forms. Open from Apps or chat PDF attachments.

com.ggbro.plugin.pdf-studio · v1 · host app + pack companion
Media

Youtube Search

Search YouTube with thumbnails, duration, and paging. Play or hand off to the downloader from results.

com.ggbro.plugin.youtube-search · v1 · host app + pack companion
Media

YouTube Downloader

Download videos or entire PL… playlists. Quality presets, pause/resume, progress in Downloads.

com.ggbro.plugin.youtube-downloader · v1 · host app + pack companion
Theme

Discord Theme

Modern Discord-style layout — left server rail, blurple chrome, Appearance-friendly tokens.

com.ggbro.plugin.discord-theme · v1 · host app + pack companion
Music

Spotify

Opens Spotify Web inside ggbro (Apps → Spotify). Log in and play in an in-app panel — not just a link.

com.ggbro.plugin.spotify · v3 · host webview + pack companion
Starter

Hello Sample

Starter with a Settings panel — greet button, sound cue, visit counter, and live theme readout.

com.ggbro.sample.hello · v3 · ui.panel, storage, ui.sound, …
Advanced

Client Lab

Kitchen-sink demo: panel buttons, storage stats, sounds, and local message decorations.

com.ggbro.sample.client-lab · v3.1 · ui.panel, app.context, messages.*, …
Productivity

Pomodoro Focus

Focus timer with a control panel — start/pause/skip, duration presets, sounds, and cycle stats.

com.ggbro.plugin.pomodoro · v3 · ui.panel, ui.sound, storage
Theme

Theme Pulse

Advanced appearance studio — colors, radius, button styles, density, chat width & alignment, plus pulse / random look.

com.ggbro.plugin.theme-pulse · v4.0.4 · ui.panel, theme.read, theme.write, …
Wellness

Session Coach

Wellness panel — tip button, check-in interval, optional auto nudges, and tip history.

com.ggbro.plugin.session-coach · v3 · ui.panel, app.context, storage
Welcome

Startup Greeting

Welcome panel — replay greeting, vibe style picker, optional sound, remembers last hello.

com.ggbro.plugin.startup-greeting · v3 · ui.panel, app.info, storage

Catalog JSON: /developers/examples/plugins/index.json. Individual source files also live under each slug folder.

Install in 30 seconds. Download → Settings → Plugins → Install Plugin → pick the .ggbro-plugin file → enable if needed → use Controls on that page (or Apps for official host apps).

Pack format

Each plugin is a flat directory (or zip of those files). Nested folders are not kept — only declared basenames are installed.

{
  "id": "com.example.hello",
  "name": "Hello Plugin",
  "version": "1.0.0",
  "author": "Example",
  "description": "Shows a toast on load",
  "main": "plugin.js",
  "styles": "styles.css",
  "permissions": ["ui.toast", "theme.read", "app.info"]
}
FieldRequiredRules
idyesReverse-domain, lowercase: com.example.name (letters, digits, ., _, -). Installed folder is named exactly this id.
nameyes1–80 characters (shown in Settings and toast labels)
versionyes1–32 characters (display only)
authornoMax 80 characters
descriptionnoMax 280 characters
mainyesBasename only, e.g. plugin.js (safe chars, .js)
stylesnoBasename only, e.g. styles.css
permissionsyes*Array of allowlisted permissions (duplicates ignored). Empty array is allowed if you only use ggbro.log.

*Use [] if you need no host capabilities beyond logging.

Optional README.md may be copied for your own notes; it is never executed.

Only these files are installed: manifest.json, main, optional styles, optional README. Extra .js/.css in the zip are discarded.

Zip packaging

# Example (PowerShell) — from the folder that contains the files
Compress-Archive -Path manifest.json,plugin.js,styles.css,README.md -DestinationPath hello.ggbro-plugin.zip
Rename-Item hello.ggbro-plugin.zip hello.ggbro-plugin

Wrong: zipping a parent folder so the archive contains my-plugin/manifest.json only. Prefer selecting the files themselves, or use Install Plugin → pick the folder.

Permissions

Declare every host API you call (except ggbro.log). Missing permission → the call rejects. Enabling a pack that requests theme.write, ui.webview, net.fetch, or messages.* asks for a one-time confirm.

PermissionAPIPurpose
app.infoggbro.getAppInfo()App version + platform
app.contextggbro.getContext() / context eventsRead-only navigation context (view, server/channel/dm ids — no message bodies)
theme.readggbro.getTheme() / onThemeChangeRead theme / chrome snapshot; listen for changes
theme.writeggbro.setTheme(patch)Apply allowlisted Appearance settings (colors, shape, layout). Sensitive — confirm on enable.
ui.toastggbro.toast({ message, type? })Show an in-app toast
ui.navigateggbro.navigate({ type, ... })In-app navigation (see below)
ui.openUrlggbro.openUrl({ url }) · panel link / button openUrlOpen https:// or spotify: via the OS / browser
ui.webviewHost Plugin AppOfficial apps may open an allowlisted in-app webview (e.g. Spotify Web). Sensitive — confirm on enable.
storageggbro.storage.get/set/clearDurable local KV under userData/plugin-data/<id>/ (256 KB cap; survives pack reinstall)
ui.soundggbro.playSound({ id })Play a built-in sound id (no arbitrary files)
ui.panelggbro.ui.registerPanelShow schema controls in Settings → Plugins. Official host apps also use this permission for Apps windows.
messages.read_displayggbro.onMessages(cb)Redacted visible messages for the active view
messages.decorateggbro.decorateMessages([...])Local-only prefix/suffix/highlights (never sent to the server)
net.fetchggbro.fetch({ url, … })Host-proxied HTTPS to user allowlisted hosts only

Host API

Your main script runs inside the sandbox with a frozen window.ggbro object. Talk to the host only through these methods (all async methods return Promises).

ggbro.getAppInfo()

Requires app.info.

{ "version": "2.8.x", "platform": "desktop" }

ggbro.getTheme()

Requires theme.read. Returns the live Appearance + chrome snapshot (string values).

{
  "accent": "#5865f2",
  "fontSize": "14px",
  "accentPreset": "blurple",
  "bgPreset": "classic",
  "radius": "soft",
  "density": "compact",
  "buttonStyle": "soft",
  "switchStyle": "filled",
  "chatWidth": "full",
  "msgAlign": "start",
  "radiusBtn": "10px",
  "chatMaxWidth": "720px",
  "chromePad": "10px"
}

ggbro.setTheme(patch)

Requires theme.write. Applies an allowlisted partial of Appearance settings (same store as User Settings → Appearance). Unknown keys are ignored; enums are clamped. Rate limit: 12 writes / 30s.

await ggbro.setTheme({
  accentPreset: 'cyan',
  radius: 'round',
  buttonStyle: 'outline',
  switchStyle: 'soft',
  density: 'comfy',
  chatWidth: 'wide',
  msgAlign: 'center',
  fontSize: 15
});

Allowed patch fields:

ggbro.onThemeChange(callback)

Requires theme.read for events to be delivered. Returns an unsubscribe function. Max 8 listeners per plugin.

Theme & chrome (what plugins can safely change)

Safe surface. Plugins may restyle allowlisted chrome — not the whole React tree.

Not allowed: arbitrary DOM, hiding UI with *, url() backgrounds, overlays, reading tokens, or sending messages as the user.

ggbro.storage

Requires storage. Keys: [a-zA-Z0-9._-]{1,64}. Values are strings (max 16 KB each). Total JSON file max 256 KB.

Durable: data lives under userData/plugin-data/<id>/ — reinstalling or updating the pack does not wipe storage.

await ggbro.storage.set('count', '1');
const { value } = await ggbro.storage.get('count'); // string | null
await ggbro.storage.clear('count'); // or clear() for all

ggbro.playSound({ id })

Requires ui.sound. Allowlisted ids only: notification, voiceJoin, voiceLeave, mute, unmute, deafen, undeafen, friendRequest, boing, airBurst.

ggbro.getContext()

Requires app.context. Returns the current navigation snapshot:

{
  view: "channel" | "dm" | "group-dm" | "friends" | "unknown",
  serverId: string | null,
  channelId: string | null,
  dmUserId: string | null,
  groupId: string | null
}

Also delivered as ggbro.onEvent with event === "context" whenever the user navigates. IDs are sanitized; no message content is included.

ggbro.ui.registerPanel({ id, title, schema })

Requires ui.panel. The desktop host draws the panel in Settings → Plugins (summary under Active plugin controls, and again under that pack’s card as Controls). Call again with the same panel id to live-update labels/values. Listen with ggbro.onEvent((event, payload) => …) when event === "panelAction".

Panel payload:

{
  panelId: string,   // your panel id, e.g. "main"
  fieldId: string,   // the field's id — NOT the label
  value?: unknown    // string | boolean | number | true (buttons)
}
Sanitizer rules (fields silently dropped if invalid)

Field types (each row also needs id):

typeKey propspanelAction value
sectiontitle
textlabel, value
noticetext, tone? info|success|warning|error
inputlabel, value?, placeholder?, password?string (Apply / Enter)
textarealabel, value?, rows?string (Apply)
togglelabel, value?boolean
selectlabel, options: [{value,label}], value?string
sliderlabel, min?, max?, step?, value?number (on release)
colorlabel, value? (#rgb / #rrggbb)hex string
progresslabel, value?, max?display only
buttonlabel, openUrl? (needs ui.openUrl)true
linklabel, href (https; needs ui.openUrl)

See the full walkthrough under Build your first plugin, or peek at hello/plugin.js for a reference panel.

ggbro.onMessages(callback) / ggbro.decorateMessages(patches)

Requires messages.read_display / messages.decorate. Payloads are redacted (id, authorName, content, isOwn). Patches: prefixText, suffixText, highlightRanges. Display-only — never written back to the API.

ggbro.fetch({ url, method?, headers?, body? })

Requires net.fetch. The worker has no network; the host proxies HTTPS GET/POST/HEAD to hosts you allowlist in Settings → Plugins. No GGBRO session cookies or Authorization. ggbro.app / localhost blocked.

ggbro.openUrl({ url })

Requires ui.openUrl. Opens an external link through the host (OS / browser). Allowed: https://… and spotify: URIs. Rate limit: 6 / 10s.

ggbro.toast({ message, type? })

Requires ui.toast.

ggbro.navigate(target)

Requires ui.navigate. See Navigate payloads. Rate limit: 8 navigations / 30 seconds per plugin. IDs/codes must match [a-zA-Z0-9._-]{1,128}.

ggbro.log(...args)

Always available. Writes to the desktop DevTools console as [plugin:<id>]. Max ~500 chars. No permission required.

Full example

// plugin.js
(async function () {
  const info = await ggbro.getAppInfo();
  ggbro.log('running on', info.platform, info.version);

  await ggbro.toast({ message: 'Plugin loaded', type: 'success' });

  const theme = await ggbro.getTheme();
  ggbro.log('accent', theme.accent, theme.radius, theme.buttonStyle, theme.switchStyle);

  // Optional — requires theme.write (sensitive confirm on enable)
  // await ggbro.setTheme({ radius: 'round', buttonStyle: 'outline', switchStyle: 'soft', chatWidth: 'wide' });

  const unsub = ggbro.onThemeChange(function (t) {
    ggbro.log('theme changed', t.accent, t.density);
  });

  // Optional — requires ui.navigate
  // await ggbro.navigate({ type: 'channel', channelId: '...' });

  // unsub(); // when you no longer need theme events
})();

Only these shapes are accepted. Unknown fields are dropped; invalid payloads reject.

typeRequired fieldsEffect
channelchannelIdJump to a channel (if you have access)
messagechannelId, messageIdOpen channel and jump to message
dmtargetUserIdOpen DM with user
dm-groupgroupIdOpen group DM
invitecodeOpen invite flow
playlistcodeOpen YouTube playlist share code
await ggbro.navigate({ type: 'channel', channelId: 'aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee' });
await ggbro.navigate({ type: 'message', channelId: '...', messageId: '...' });
await ggbro.navigate({ type: 'dm', targetUserId: '...' });
await ggbro.navigate({ type: 'invite', code: 'AbCdEf' });
await ggbro.navigate({ type: 'playlist', code: 'ABC123' });

CSS

Optional styles.css is injected into the host document after a strict sanitizer.

/* Tokens */
:root {
  --gg-radius-btn: 14px;
  --gg-chat-max-width: 840px;
}

/* Safe chrome hooks */
[data-gg-chrome="composer"] {
  padding-top: 12px;
  padding-bottom: 16px;
}
.gg-btn-primary {
  border-radius: 999px;
}

/* Stripped — will not affect the UI */
* { display: none }
.sidebar { position: fixed; background: url(https://evil.example) }

Prefer ggbro.setTheme() for Appearance presets; use CSS for fine-tuning chrome hooks only.

Limits

LimitValue
Installed plugins50
Enabled plugins (running)25
Pack size2 MB
Per-file size (main/styles)512 KB
Zip entries32
Toast rate5 / 10s per plugin
Navigate rate8 / 30s per plugin
Theme write rate limit12 / 30s per plugin
Theme listeners8 per plugin
Panels per plugin8
Fields per panel schema48
Host request timeout10 seconds

Security model

Residual risk. Enabling a plugin with ui.toast / ui.navigate lets it notify you or request in-app navigation. Editing files under the plugins folder (Open Folder) and pressing Refresh reloads that code — only edit packs you trust.

Managing plugins (Settings)

How plugin updates work. When we ship a new example version, bump version in the pack manifest.json and in index.json, then republish the .ggbro-plugin file. Users click Check for Updates (or open Settings → Plugins) and press the green Update button on that pack — no need to re-download manually.

Checking for updates

  1. Open User Settings → Plugins.
  2. Click Check for Updates — the app reloads index.json and compares each catalog version to your installed pack.
  3. Rows with a newer catalog version show Update available and a green Update to v… button.
  4. Click Update (or Install / Update All Examples) — the pack is re-downloaded from the official catalog, replaced on disk, and enabled.

Tip for authors: always bump the semver-style version string (e.g. 3.0.03.1.0) in both the pack manifest and the catalog entry, or clients will not show Update.

Troubleshooting

SymptomCheck
No tile in the Apps (9-dot) gridPack must be enabled and declare ui.panel. Update to the latest desktop app. Press Refresh in Settings → Plugins.
No “Open controls in Apps” on the cardNeeds ui.panel in the manifest. Official host apps show “Open … in Apps” instead.
Plugin enabled but empty ControlsUpdate to 2.8.168+ (packs now run in a Web Worker — older iframe hosts timed out). Press Refresh. Call registerPanel before slow storage. Every field needs id. Check the red Runtime: line / DevTools [plugin:…].
Installed from Documents but nothing runsUse a dedicated folder with only manifest.json + plugin.js (e.g. Documents\my-calculator\). Reinstall from that folder, then Refresh.
Buttons / inputs do nothingListen for payload.fieldId, not payload.label. Ensure ui.panel is in the manifest.
Select shows no optionsUse options: [{ value, label }, …] — string arrays are ignored.
Install fails: invalid idUse reverse-domain like com.example.hello
Install fails: missing mainmain basename must exist next to manifest.json (zip root / folder root)
Plugin listed with errorFolder name must equal id; fix or reinstall
API rejects "Missing permission"Add the permission to manifest.json and reinstall / refresh
Toast rate limitWait 10s; reduce toast spam
CSS does nothingOnly root custom properties and allowlisted chrome/button selectors survive sanitization
No toast / no logsEnsure plugin is enabled; open DevTools Console for [plugin:…] lines
Edited files not appliedClick Refresh in Settings → Plugins
Can't enableDisable another plugin (25 enabled max)
Looking for UI on web / mobilePlugins are desktop-only

Out of scope

Also building bots? See the Bot API documentation and free examples in the Developer Portal.