Skip to content

Widget API reference

Everything a sandboxed widget can use. For a first widget, start with Build a widget. The SDK’s v1.d.ts has the same API as TypeScript types.

A widget is a folder named for its id, holding widget.json and the widget’s files. At most 50 MB and 2,000 files, with no symbolic links.

{
"manifestVersion": 1,
"id": "acme.pr-dashboard",
"name": "PR Dashboard",
"version": "1.2.0",
"description": "Open pull requests across your repos",
"author": "Acme",
"homepage": "https://example.com/pr-dashboard",
"icon": "code-pull-request",
"defaultHue": 210,
"kind": "sandboxed",
"entry": "index.html",
"permissions": ["storage", "net:https://api.github.com"],
"contributes": {
"panes": [{ "name": "main", "label": "PRs", "icon": "code-pull-request", "entry": "index.html" }],
"commands": [{ "id": "refresh", "title": "Refresh pull requests", "icon": "rotate-right" }],
"statusItems": [{ "id": "count", "text": "PRs", "icon": "code-pull-request", "command": "refresh" }]
}
}
FieldRequiredMeaning
manifestVersionyesThe package format: 1.
idyes<publisher>.<name>: lowercase letters, digits and -, one dot, at most 64 characters. Must equal the folder name.
nameyesShown in the install prompt and Settings; at most 60 characters.
versionyesSemver, e.g. 1.2.0.
descriptionnoAt most 300 characters.
author, homepagenoShown in the prompt. homepage must be an https:// link.
iconnoA Font Awesome icon name.
defaultHuenoThe pane color’s hue, 0 to 359.
kindnosandboxed (the default) or trusted.
entrynoThe page a pane loads when it names none: index.html by default.
permissionsnoWhat it asks for (below). An unknown permission makes the package invalid.
contributes.panesyesAt least one. Each: name (lowercase letters, digits and -), label, and optionally icon, entry, and defaultMeta (a new pane’s initial state, keys written widget:<id>:<key>).
contributes.commandsnoUp to 20 entries in the command palette (see Commands and status bar items). Each: id (lowercase letters, digits and -), title (at most 60 characters), and optionally icon, pane (the pane it runs in, by name; the first one by default) and keywords (more words the palette’s search matches).
contributes.statusItemsnoUp to 4 items in the status bar. Each: id, text (at most 40 characters), and optionally icon, tooltip (at most 120 characters), command (one of the widget’s command ids, run on a click; without one, a click shows the widget’s first pane) and alignment (right, the default, or left).

Each pane is a view named ext:<id>/<name>, for example ext:acme.pr-dashboard/main, and an entry in the widget bar’s more menu.

PermissionLets the widget
storageKeep its own data on this computer
net:<origin>Make HTTP requests to that origin: net:https://api.github.com allows exactly that origin; net:https://*.example.com allows its subdomains (never the bare domain, never a lone *)
filesOpen files the user picks, and save files where the user chooses
clipboard:writeCopy text to the clipboard
panesOpen views other than its own
agents:readList the user’s agents and whether they’re working
agents:sendSend messages to the user’s agents

A private or local address (loopback, your network, link-local) is reachable only through a permission that names that address itself, port included: an IP address (net:http://127.0.0.1:8188) or localhost (net:http://localhost:3000). A public name that resolves to a private address is refused, and so is a wildcard.

<link rel="stylesheet" href="/agentmux/widget-sdk/am-widget.css" />
<script type="module">
import { connect } from "/agentmux/widget-sdk/v1.js";
const am = await connect();
</script>

connect({ applyTheme = true, timeoutMs = 10000 }) performs the handshake and resolves to the client. It applies the app’s theme as CSS variables on the document and keeps them current. am-widget.css styles plain HTML with them. It rejects if the page isn’t running inside AgentMux, or if AgentMux doesn’t connect in time.

am.info holds what the handshake returned:

Field
widget{ id, version, pane }: this widget, and which of its panes this is
agentmux{ version, host }
permissionsThe permissions the user granted
themeThe current theme (below)
metaThis pane’s state
visibility, focusedWhether the pane is shown, and whether it has focus

Every method returns a promise. A refused call rejects with an AgentMuxError (see Errors). am.call(method, params) calls any method by name, including ones this SDK version has no wrapper for.

MethodPermission
am.meta.get()This pane’s own state, an object
am.meta.set(patch)Merges patch into it; a null value deletes a key. At most 64 KB per pane. It’s kept with the pane: it survives restarts and moves with the pane, and each pane of the widget has its own
am.ui.setTitle(text, icon?)The pane’s tab title (at most 200 characters)
am.ui.setHeaderActions(actions)Up to 4 buttons in the pane’s header, { id, icon, title }; a click is the action event
am.ui.setContextMenu(items)Up to 20 items in the pane’s menu, { id, label, disabled? } or { separator: true }; a click is the action event
am.ui.toast(text, kind?)A notification under the widget’s name; kind is info, success, warning or error
am.ui.openUrl(url)Opens an http/https link in a browser pane next to the widget
am.ui.setStatusItem(id, look?)Changes how one of the widget’s status bar items looks while this pane is open: { text, icon, tooltip, tone, hidden }, where tone is info, success, warning or error. Fields left out show the widget.json look; no look puts it back. An id widget.json doesn’t declare is not_found
am.theme.get()The current theme
am.panes.open(view, meta?, split?)panes, except for the widget’s own viewsOpens a pane; split is right (default), down or tab

Needs storage. Per widget (shared by all its panes, in every window), not per pane. Values are anything JSON can hold.

Method
am.storage.get(key)The value, or null
am.storage.set(key, value)Keys up to 256 bytes, values up to 1 MB, at most 5 MB per widget
am.storage.delete(key)
am.storage.list(prefix?)The keys, sorted, optionally only those starting with prefix

Uninstalling a widget deletes its storage. A change from any pane sends every pane of the widget the storage event.

am.net.fetch(url, { method, headers, body, timeoutMs }) needs a net: permission for the URL’s origin. AgentMux makes the request, not the widget’s page. That means no cookies, no browser session, and no proxy.

  • Every redirect must also be to a granted origin (at most 5 redirects). Authorization and Cookie headers aren’t sent on to another origin.
  • The connection’s own headers (Host, Content-Length, Connection, Transfer-Encoding, Proxy-* and the like) can’t be set.
  • body is a string, a Uint8Array or an ArrayBuffer.
  • Request and response bodies are limited to 10 MB each. The timeout defaults to 30 seconds, at most 120.

The result has status, statusText, headers, url (after redirects), ok, and text(), json() and bytes().

The widget’s page itself can’t make network requests: fetch and the like are blocked in it.

MethodPermission
am.files.pick({ accept?, multiple? })filesOpens the file dialog. Resolves to the chosen files’ { name, type, size, dataBase64, bytes() }, never their paths, up to 25 MB in all. Rejects cancelled if the user cancels. Call it from a click (in the widget or on one of its header actions): a dialog doesn’t open otherwise
am.files.save(name, data, type?)filesOpens the save dialog with data (a string or bytes, up to 25 MB). Resolves false if the user cancels. Like pick, call it from a click
am.clipboard.writeText(text)clipboard:writeUp to 1 MB of text
MethodPermission
am.agents.list()agents:readThe agents running on this computer: { id, name, state }, state being working, idle or stopped
am.agents.send(agent, text)agents:sendDelivers text to that agent, marked as sent by the widget (widget:<id>) and unverified, so the agent treats it like any message whose sender it can’t check. At most 8 KB, 10 messages a minute. Resolves to the message id

am.on(event, callback) returns a function that stops listening.

EventParamsWhen
visibility{ state }: active, dormant or windowHiddenThe pane is shown, hidden behind another tab, or its window is minimized
focus{ focused }The pane gains or loses focus
themethe themeThe user changes the theme or the pane’s color
meta{ meta }The pane’s state changed from outside the widget (another window, an undo)
action{ id, source }: header or menuThe user clicked one of its header actions or menu items
command{ id, source }: palette or statusThe user ran one of its commands from the palette, or clicked a status item that runs one
storage{ keys }The widget’s storage changed, from any of its panes in any window
dispose{}The pane is closing or the widget reloading; the connection closes a second later

A hidden pane keeps running, so pause timers and polling while it’s dormant:

import { useVisibility } from "/agentmux/widget-sdk/v1.js";
useVisibility(am, { onActive: startPolling, onDormant: stopPolling });

A widget can add entries to the command palette and items to the status bar, declared in widget.json (above). They need no permission, and they’re there only while the widget is installed and turned on. An older AgentMux ignores them.

A command shows in the palette under Widgets as “PR Dashboard: Refresh pull requests”. Running it focuses the widget’s pane in the current tab, or opens one, and sends that pane the command event:

am.on("command", ({ id }) => {
if (id === "refresh") refresh();
});

A pane opened for the command gets it right after it connects, even if the widget subscribes a moment later. Commands run only when you click them: they can’t be bound to keys, and agents can’t run them.

A status bar item shows its widget.json text and icon until one of the widget’s panes changes it, and again once that pane closes. Its tooltip always starts with the widget’s name, so it can’t pass for one of AgentMux’s own items:

await am.ui.setStatusItem("count", { text: `${prs.length} PRs`, tone: prs.length > 20 ? "warning" : undefined });

A widget runs only while one of its panes is open, so an item it updates shows live values only then.

{ mode, vars }: mode is dark or light. vars are CSS custom properties, set on the document by connect():

--am-bg, --am-fg, --am-muted, --am-accent, --am-border, --am-error, --am-warning, --am-success, --am-font, --am-font-mono, --am-radius, and the pane’s hue as --am-pane-hue.

A refused call rejects with an AgentMuxError, which has a code, a name, a message and sometimes data:

CodeNameWhen
1001permission_deniedThe widget wasn’t granted what the method needs; data.permission names it
1002limit_exceededA size, count or rate limit; data.limit names it
1003not_foundAn unknown agent, view or key
1004network_errornet.fetch failed before a response: DNS, refused, timeout, a redirect it may not follow
1005unavailableAgentMux can’t do it here, or the widget changed or was turned off (reload it)
1006cancelledThe user cancelled a dialog
1007not_readyA call before the handshake
1099internalAgentMux failed; message says how
-32601, -32602method_not_found, invalid_paramsAn unknown method, or wrong parameters

The protocol is version 1. Within it, AgentMux only adds: new methods, new optional parameters, new fields in results, new events. A widget should ignore fields it doesn’t know. am.info.agentmux.version says which AgentMux it runs in, and an unknown method rejects with method_not_found.