Browse API 1 documentation
Starter recipes
Copyable minimal examples for panes, composer utilities, and Roots.
These examples are intentionally small: plain HTML and JavaScript, no dependencies, no build step. Each directory can be loaded directly with Load Unpacked (Dev)… in a development build of Mango on macOS.
Use a reverse-DNS package identifier you control before distributing an extension. The identifiers below are examples only.
A visible channel Sprig
Choose this when the user needs a persistent custom interface attached to one conversation.
hello-pane/
├── manifest.json
└── index.html
manifest.json:
{
"grove": 1,
"id": "com.example.grove.hello-pane",
"name": "Hello Pane",
"version": "0.1.0",
"entry": "index.html",
"presentation": {
"surfaces": ["channelPane"],
"defaultSurface": "channelPane",
"defaultSize": { "width": 340, "height": 300 }
},
"capabilities": ["channel.read", "channel.send"],
"hostPermissions": []
}
index.html:
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Hello Pane</title>
<button id="hello">Say hello</button>
<ol id="messages"></ol>
<script>
const messages = document.querySelector('#messages')
grove.on('message', message => {
const item = document.createElement('li')
item.textContent = `${message.from}: ${message.text}`
messages.append(item)
})
document.querySelector('#hello').addEventListener('click', () => {
grove.send('Hello from Grove')
})
</script>
</html>
Start it from a channel. New live messages appear in the list, and the button sends through Mango to that bound channel.
A composer utility
Choose this when a temporary interface should return text to the message draft without sending automatically.
quick-insert/
├── manifest.json
└── index.html
manifest.json:
{
"grove": 1,
"id": "com.example.grove.quick-insert",
"name": "Quick Insert",
"version": "0.1.0",
"entry": "index.html",
"presentation": {
"surfaces": ["composer"],
"defaultSurface": "composer",
"defaultSize": { "width": 420, "height": 240 }
},
"contributions": {
"commands": [
{ "name": "greet", "summary": "Add a greeting to the draft" }
]
},
"capabilities": ["commands.register"],
"hostPermissions": []
}
index.html:
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Quick Insert</title>
<button id="insert">Insert greeting</button>
<button id="cancel">Cancel</button>
<script>
document.querySelector('#insert').addEventListener('click', () => {
grove.composer.complete('Hello from Grove')
})
document.querySelector('#cancel').addEventListener('click', () => {
grove.composer.close()
})
grove.on('command', invocation => {
if (invocation.command === 'greet') grove.composer.complete('Hello from Grove')
})
</script>
</html>
Open it from the composer’s + menu, or type /greet to launch it on demand. Insert greeting and the command both close the utility and place the text in the current draft for review.
A headless command Root
Choose this when Mango’s native command and card UI is enough and the extension needs no pane of its own.
hello-root/
├── manifest.json
└── index.html
manifest.json:
{
"grove": 1,
"id": "com.example.grove.hello-root",
"name": "Hello Root",
"version": "0.1.0",
"entry": "index.html",
"presentation": {
"surfaces": ["headless"],
"scope": "server"
},
"capabilities": ["commands.register"],
"hostPermissions": []
}
index.html:
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>Hello Root</title>
<script>
grove.commands.register([{
name: 'hello',
summary: 'Show a Grove greeting'
}])
grove.on('command', invocation => {
if (invocation.command !== 'hello') return
grove.card({
id: 'hello',
title: 'Hello from Grove',
body: `The command ran in ${invocation.target ?? 'Mango'}.`
})
})
</script>
</html>
Start the Root for a network, then type /hello in a conversation on that network. Mango renders the returned card above the composer.
Grow from the smallest example
- Add an optional
settings.htmlonly when the extension needs configuration. Settings pages receive no network access. - Add
hostPermissionsonly for the exact HTTPS or WSS origins the runtime contacts. - Add capabilities one feature at a time and test the consent flow after each change.
- Use
textContent, notinnerHTML, for IRC-controlled text. - Keep a Root’s work bounded and reconnect after lifecycle changes; it is client-side WebKit, not an always-on server.
Complete public examples
The maintained examples below are public source repositories. Links target the release tags reviewed with these API 1 docs. Visit the Mediaware EU repositories for newer releases, and use a release commit SHA when an immutable source reference is required.
| Example | What it demonstrates |
|---|---|
| hello-vue v0.1.0 | A visible Vue 3 and TypeScript Sprig. |
| giphy-composer v0.1.10 | A composer utility, manifest-declared commands, settings page, Keychain configuration, direct API requests, focus, and draft completion. |
| immich-composer v0.1.0 | A self-hosted service, brokered OAuth with PKCE, native non-CORS requests, Keychain sessions, binary previews, and composer uploads. |
| command-lab-root v0.1.0 | A compact Root for commands, native cards, IRC sends, and live message observation. |
| weatherbot v0.2.1 | Commands, cards, status badges, storage, external APIs, and channel events. |
| relay-formatter v0.1.2 | Inbound filtering and native channel-member menu actions. |
| fish-root v0.1.2 | Inbound and outbound filters with Keychain-backed legacy interoperability. |
| tus-uploader v0.2.2 | An application-scoped upload-provider Root, settings, bounded file reads, progress, and resumable TUS streaming. |
Use these after the minimal recipe works. Start with Package and manifest and Testing and distribution before shipping.