Browse API 1 documentation
GroveAPI 1preview

Surfaces and scopes

Choose a visible Sprig, composer utility, or headless Root.

Mango 3.1+View Markdown

A Grove instance has two separate properties:

  • its surface determines where or whether it is visible;
  • its scope determines which application, channel, query, or IRC-session context Mango may expose.

Changing presentation does not widen scope. Mango checks the bound scope again for every event and bridge call.

Channel and window Sprigs

A normal visible Sprig declares one or both of these surfaces:

json
"presentation": {
  "surfaces": ["channelPane", "window"],
  "defaultSurface": "channelPane",
  "minSize": { "width": 280, "height": 200 },
  "defaultSize": { "width": 420, "height": 500 }
}

channelPane is the default when presentation.surfaces is absent or empty. Starting it from a channel binds the instance to that channel. Channel capabilities apply only to that bound target.

window changes where the same web content appears. It does not grant server-wide data. The instance retains the scope selected when it was started.

Sizing values are suggestions. Mango clamps them to the available native space and may adapt the presentation on iPhone or iPad.

Network tab Sprigs

A visible extension that owns one interface for an entire IRC connection declares an exclusive tab surface and server scope:

json
"presentation": {
  "surfaces": ["tab"],
  "defaultSurface": "tab",
  "scope": "server"
}

Mango runs at most one instance of that package for the IRC session and opens it as a full conversation-area tab. Starting it again selects the existing tab instead of creating a second bot or dashboard. Different server-tab packages may coexist on the same network.

With channel.read, the instance receives live messages from every confirmed joined channel, and the retained channels event reports the current confirmed roster. It does not receive CHATHISTORY replay, raw IRC traffic, unjoined channels, or permission to send. Sending to a named joined channel additionally requires network.send, and Mango validates membership again for every call.

Composer utilities

A composer utility is transient and application-scoped:

json
"presentation": {
  "surfaces": ["composer"],
  "defaultSurface": "composer",
  "minSize": { "width": 320, "height": 220 },
  "defaultSize": { "width": 520, "height": 500 }
}

composer must be the only declared surface. Mango opens the utility after an explicit action from the message composer’s + menu, or after the user invokes a slash command the utility declared under contributions.commands. A declared command launches the same transient surface on demand; Mango does not keep a hidden WebView running. It has:

  • grove.info.scope.kind === 'application';
  • an empty grove.info.networkID;
  • no channel, query, or server event stream;
  • no implicit IRC send target;
  • no autostart.

The utility can return bounded text to the draft with grove.composer.complete(text). With the separately granted uploads.use capability, it can instead hand one bounded Uint8Array or ArrayBuffer to Mango’s configured uploader through grove.composer.upload(data, { filename }). Both operations close the utility and leave the resulting text or uploaded URL in the composer for review; neither sends a message. grove.composer.close() closes without changing the draft.

See giphy-composer v0.1.10 for a focused picker with on-demand commands that returns a URL and immich-composer v0.1.0 for a composer that can return either a service link or an uploaded private copy.

For search-driven utilities, call grove.composer.requestFocus?.('field-id') after rendering the input. On iOS this hands an already-focused message composer to the utility without opening a previously hidden keyboard. Use touch input text of at least 16px to avoid focus zoom. Closing restores focus when the utility was opened from the focused draft.

Headless Roots

A Root declares headless and no other surface:

json
"presentation": {
  "surfaces": ["headless"]
}

The Root has no pane or window. It reaches the user through granted integrations such as registered slash commands, native cards, status badges, member-menu actions, message filters, or upload providers.

Without an explicit presentation.scope, API 1 binds the Root to the channel or query from which it was started. This is useful when the Root’s state and actions should affect only one conversation.

An application-scoped Root declares:

json
"presentation": {
  "surfaces": ["headless"],
  "scope": "application"
}

Mango treats this as one logical Root in management, configuration, and lifecycle, while executing one passive mirrored runtime in each top-level IRC session. It is the preferred scope for an upload destination or another integration whose configuration should be available on every network. Starting or stopping it in one Grove Manager serially starts or stops every mirror, and its autostart choice is stored once for the application rather than once per network.

Every mirror has grove.info.scope.kind === 'application', an empty grove.info.networkID, and no IRC message, command, filter, presence, or implicit send route. Host requests are routed through the mirror owned by the IRC session that initiated the request; registrations from another session’s mirror do not replace that handler. Code must therefore register the same contributions idempotently in every mirror and must not assume JavaScript globals are singleton application state. Use application-wide storage for shared service configuration and keep each runtime passive except while handling an explicit host request such as an upload.

tus-uploader v0.2.2 is a complete application-scoped Root whose mirrored runtimes register one shared upload destination.

A server-scoped Root declares:

json
"presentation": {
  "surfaces": ["headless"],
  "scope": "server"
}

This creates one instance for one IRC session rather than one instance per buffer. Server scope is appropriate for a connection-wide slash-command service, a filter intentionally applied across that session, an upload destination that deliberately has separate configuration per network, or native member actions that open isolated service pages beside the selected channel. A visible server-tab Sprig uses the same singleton rule. Server scope does not automatically grant server.events, channel reads, sends, or raw IRC access; capabilities still determine which events and calls are available.

With ui.web-pane, a server Root may open or replace a packaged HTML pane only beside a named confirmed channel. It should do so from a host-attributed native workflow such as a member action. That page is not the Root: it receives no Grove bridge or IRC context and closes with its owner.

The runtime scope object

Every runtime can inspect its host-owned identity:

js
const { id, api, networkID, scope } = grove.info

scope.kind is application, channel, query, or server. scope.target is the channel or query name when one exists and otherwise null. networkID is an opaque stable identifier for the owning IRC session and is empty for application-scoped Roots, composer utilities, and settings pages.

Do not infer authority from these values. They are context for behavior and storage keys; Mango independently enforces the granted capabilities and scope.

Lifecycle expectations

Visible and headless instances run in WebKit while Mango keeps them mounted. A channel- or server-scoped Root may autostart when its configured IRC network connects; an application-scoped Root has one application-wide autostart choice and is mirrored as top-level sessions register. Both remain client-side:

  • it stops when Mango stops;
  • it may be suspended with the application;
  • WebKit timers are not a durable scheduler;
  • sockets should reconnect after lifecycle transitions;
  • persistent storage does not make the runtime always-on.

Listen for lifecycle events when the extension maintains time-sensitive state:

js
grove.on('lifecycle', ({ state }) => {
  if (state === 'foreground' || state === 'pulse') reconnectIfNeeded()
  if (state === 'background') persistPendingState()
})