examples/aether-extension package in the Aether repository demonstrates how to combine Pi agent-level tools with Aether UI extensions in a single package. This walkthrough covers every part of the example: the package manifest, the Pi factory, the Aether factory, surface and page registration, storage, and the before_send event hook.
What this extension does
- Registers a Pi
/aether-examplecommand that notifies the agent when the extension is active - Adds a card surface above the composer with a live counter button and a prompt prefill action
- Adds a full-screen drawer page (“Extension dashboard”) with a persistent count card and an embedded WebView
- Bridges the WebView back to extension actions via
Aether.postMessage - Persists the counter value in extension storage across sessions
- Hooks into the
before_sendevent to strip a!rawprefix from outgoing messages
Package declaration
Thepackage.json lists the same index.ts entry point under both pi.extensions and aether.extensions, so a single file powers both the Pi agent side and the Aether UI side:
Full annotated code walkthrough
1. Imports and defineAetherExtension
ExtensionAPItypes thepiargument in the Pi factory (default export).defineAetherExtensionwraps the Aether factory and provides type-safe access to theaethercontext object.uiis the declarative UI builder used in everyrendercallback.
2. The Pi factory (default export): registering /aether-example
pi.extensions. You export the Pi factory as the default export. The handler does nothing more than fire an info notification — it is a minimal proof that the Pi and Aether sides of the package are live simultaneously.
3. The Aether factory (activateAether): counter action + prefill action
activateAether (a named export). Aether discovers it because the file is also listed under aether.extensions.
incrementreads the currentcountfromaether.storage(defaulting to0), increments it, writes it back, then shows an Android toast viaaether.host.invoke("app.notify", ...). Becausestorageis synchronous and persisted in~/.aether/app-extension-state.json, the value survives hot reloads and app restarts.prefillusesapp.setDraftInputto replace the composer text with a ready-made prompt. You can trigger this from any button in the UI by passing the action ID"prefill".
4. Surface registration at chat.composer.top
registerSurface attaches content to the chat.composer.top slot — directly above the text input. Key points:
order: 10— controls stacking order when multiple extensions register the same slot.rendercontext — receives live app state.storageholds your extension’s persisted key/value pairs;is_runningreflects whether the agent is currently generating.ui.card— wraps all content in a rounded card.radius: 22rounds the corners more aggressively to match the composer shape.ui.rowwitharrangement: "space-between"— stretches the label and status text to opposite ends.ui.rowwithwrap: true— the three buttons wrap onto a second line on narrow screens;rowSpacing: 8adds vertical breathing room between lines.ui.node("pageButton", { page: "dashboard", ... })— a special button type that navigates to the registered page with ID"dashboard". It opens the page in the conversation drawer.
5. Page registration with WebView and action bridge
registerPage creates a full-screen destination that Aether automatically lists in the conversation drawer. Points to note:
id: "dashboard"— this string must match thepagefield on thepageButtonnode registered in the surface above.rendercontext — receives the same app state object as surfaces.storage.countreads the persisted counter.ui.web— injects an arbitrary HTML document into a native AndroidWebView. JavaScript, DOM storage, and network access are all enabled inside the WebView.Aether.postMessage(JSON.stringify({ action, args }))— the only bridge from WebView JavaScript back to extension logic. When the user taps “Increment through JavaScript”, the WebView posts a message that Aether routes to the registeredincrementaction. You can pass arbitraryargsin the JSON object; the action handler receives them as its first argument.ui.columnwithscroll: true— makes the page body vertically scrollable when content overflows the screen.
6. The before_send event hook
aether.on("before_send", handler) fires immediately before the user’s message is submitted to the model. The handler receives the draft payload and may return a partial object to merge back in. Here, any message beginning with !raw has that prefix stripped before dispatch. Returning undefined (or nothing) leaves the payload unchanged.
Installation
- Zip the
examples/aether-extensiondirectory into a.ziparchive. - Open Aether on your Android device.
- Go to Settings → Extensions → Import.
- Select the zip file. Aether extracts it to
~/.aether/extensionsand hot-loads the Script side immediately.