# The JavaScript API A panel talks to Offsider with `offsider.js`, a small client Offsider serves to every panel. ## offsider.js Load the client, then wait for it to be ready: ```html ``` | Call | What it does | | --- | --- | | `offsider.ready()` | Resolves with `{ context, input }` once Offsider has said hello. `input` is `--input`. | | `offsider.on("result", fn)` | Called with the data from each `offsider send`. | | `offsider.run(name, args)` | Starts a declared command and returns a job with `events` and `cancel()`. | | `offsider.model(prompt)` | Generates text with Apple's on-device model when `foundationModels` is approved and available. | | `offsider.systemNotification(title, body)` | Posts a macOS notification when `notifications` is approved and macOS allows it. | | `offsider.storage` | `localStorage` for the panel, kept between openings. See [Storage](#storage). | | `offsider.on("storage", fn)` | Called with `{ action, key, oldValue, newValue, local }` whenever the storage changes. | | `offsider.on("context", fn)` | Called with the whole context whenever the appearance or system colors change. | | `offsider.on("teardown", fn)` | Called just before the window closes. | | `offsider.emit(name, data, note)` | Prints an `event` line. `note` is a sentence for people, such as "Ticked: Log in works". | | `offsider.message(text)` | Prints a `message` line. | | `offsider.modelContext(text, data)` | Prints a `context` line: what the app is showing, for Claude to know about. | | `offsider.call(name, args)` | Runs a tool from `panel.json`, and resolves with what it printed. | | `offsider.openLink(url)` | Opens an `http`, `https` or `mailto` link in the browser. | | `offsider.close()` | Closes the panel. | | `offsider.log(level, ...data)` | Prints a `log` line. `console.error`, `console.warn` and `console.info` already do. | ## Storage Web storage doesn't last in a panel: each window's WebKit data is thrown away when it closes, and the sandboxed page can't use `localStorage` at all. `offsider.storage` has the same interface instead: `getItem`, `setItem`, `removeItem`, `clear`, `key(index)`, `length`, and keys as properties. Offsider keeps it per panel folder in its Application Support folder, so it lasts between openings and is shared by every open panel from that folder. ```js await offsider.ready() const width = Number(offsider.storage.getItem("sidebarWidth")) || 210 offsider.storage.setItem("sidebarWidth", 240) offsider.on("storage", ({ action, key, oldValue, newValue, local }) => { // action: "added", "updated", "removed" or "cleared" (key null). // local: true when this page made the change, false when another panel from the folder did. }) ``` Values are strings, like `localStorage`; store JSON with `JSON.stringify`. Reads and writes are synchronous once `ready()` has resolved, and Offsider saves writes in order. Unlike the browser's `storage` event, `"storage"` also fires for the page's own changes. Writing past the 5 MB quota throws a `QuotaExceededError`. ## The context `offsider.context` looks like this: ```json { "theme": "dark", "colors": { "background": "rgba(40, 40, 40, 1.000)", "accent": "rgba(0, 122, 255, 1.000)" }, "locale": "en-AU", "timeZone": "Australia/Melbourne", "platform": "desktop", "displayMode": "inline", "containerDimensions": { "width": 400, "height": 700 }, "safeAreaInsets": { "top": 0, "right": 0, "bottom": 0, "left": 0 }, "panel": { "name": "review", "tools": ["prs"] } } ``` ### System colors `offsider.js` sets these semantic CSS variables on the panel page. Offsider reads the values from the host operating system and refreshes them when its appearance or color settings change. Panels can use the same names on other platforms without referring to AppKit. | CSS variable | Intended use | | --- | --- | | `--offsider-color-background` | Main window background | | `--offsider-color-surface` | A control or inset surface | | `--offsider-color-text` | Primary text | | `--offsider-color-text-muted` | Secondary text | | `--offsider-color-border` | Separators and borders | | `--offsider-color-accent` | User's accent color | | `--offsider-color-accent-text` | Text over the accent color | | `--offsider-color-selection` | Selected item background | | `--offsider-color-selection-text` | Selected item text | The same values are available in `offsider.context.colors` without the CSS prefix. Use browser fallbacks when previewing a page outside Offsider: ```css body { background: var(--offsider-color-background, Canvas); color: var(--offsider-color-text, CanvasText); } .selected { background: var(--offsider-color-selection, Highlight); color: var(--offsider-color-selection-text, HighlightText); } ``` The color variables are solid or translucent colors. The `vibrant` window material remains a separate native background effect. ## It's an MCP App Underneath, a panel talks to Offsider the way an [MCP App](https://modelcontextprotocol.io/extensions/apps/overview) talks to its host: JSON-RPC 2.0 over `postMessage`. `offsider.js` is a thin layer over it, and an app built with the MCP Apps SDK can use the supported methods below. This is a local MCP Apps-style UI host, not an MCP server or a claim of full SDK compatibility. | Method | Direction | Notes | | --- | --- | --- | | `ui/initialize` | app → Offsider | Returns `protocolVersion`, `hostInfo`, `hostCapabilities` and `hostContext`. | | `ui/notifications/initialized` | app → Offsider | Offsider then sends `tool-input` with the `--input`. | | `ui/notifications/tool-input` | Offsider → app | `{ arguments }` | | `ui/notifications/tool-result` | Offsider → app | Each `panel send`, as `{ content, structuredContent }`. | | `ui/notifications/host-context-changed` | Offsider → app | Only the fields that changed. | | `ui/resource-teardown` | Offsider → app | The window is closing. | | `tools/call` | app → Offsider | `{ name, arguments }`, answered with a `CallToolResult`. | | `ui/open-link` | app → Offsider | `{ url }` | | `ui/message` | app → Offsider | `{ role, content }` | | `ui/update-model-context` | app → Offsider | `{ content, structuredContent }` | | `ui/request-display-mode` | app → Offsider | Always `inline`. | | `notifications/message` | app → Offsider | Logging: `{ level, data }` | | `offsider/emit`, `offsider/close` | app → Offsider | Offsider's own additions. |