SDK API reference
definePlugin, context handles, host calls, error codes and runtime limits for CodeMux add-ons.
Everything below comes from @codemux/plugin-sdk (API 1.0.0). Your code runs in
a separate QuickJS process, not in the CodeMux window. It has no DOM, filesystem,
Node APIs, fetch or access to other add-ons. It can only do what the host calls
on this page allow and what your manifest grants.
definePlugin
import { definePlugin } from "@codemux/plugin-sdk";
export default definePlugin({
activate(ctx) {
// Register every declared contribution here.
},
deactivate() {
// Optional. CodeMux cleans up either way.
},
});src/index.tsx has to default-export the result. activate gets the
PluginContext (ctx) and can be async. By the time it returns, or its promise
resolves, it has to register a callback for every contribution in the manifest.
While activate runs, you can register callbacks, read settings and use storage.
Network requests, notifications, panels and composer calls are refused until it
finishes.
deactivate runs when the add-on stops. CodeMux removes registrations, views and
subscriptions itself, even if deactivate throws or never finishes.
The SDK also exports useState, useEffect, useMemo, useCallback, useRef
and useReducer from Preact.
Contributions
| Registration | Callback | Declared in |
|---|---|---|
ctx.commands.register(id, handler) | (context) => void | Promise<void> | contributes.commands |
ctx.composerActions.register(id, handler) | (context) => void | Promise<void> | contributes.composerActions |
ctx.panels.register(id, render) | ({ viewId, context }) => children | contributes.panels |
ctx.composerViews.register(id, render) | ({ viewId, context }) => children | contributes.composerViews |
Activation fails if you register an ID that isn't declared, register the same ID twice, or leave a declared ID unregistered.
Each register returns a disposer. Contributions are fixed while the add-on runs,
so disposing after activation only removes the callback. The command stays listed
and does nothing, and the view renders empty. Register the same ID again to
restore it.
Render functions return Preact children built from the
SDK components. Render a component that uses hooks, such as
(props) => <Main {...props} />, instead of calling hooks in the render function
itself.
Context handles and interactions
A ContextHandle is an opaque string that CodeMux checks on every call. It
identifies the add-on's current project and, for composer surfaces, the chat
composer it belongs to.
- Views get a handle as the
contextprop. - Commands and composer actions get one as their only argument.
- UI events such as
onPressandonChangeget one asevent.context.
Handles from commands, composer actions and UI events also carry a user
interaction. panels.open, composerViews.open, composer.appendText and
links.open need one. An interaction can be used once and expires after 10
seconds. Waiting on other host calls doesn't extend it. If it's missing, used or
expired, the call rejects with INTERACTION_REQUIRED.
Rules for handles:
- Pass the handle from the event that caused the action. Don't store handles for later.
- When the user switches projects, CodeMux unmounts every view and mounts it again
with a new handle, and old handles reject with
CONTEXT_STALE. Re-run effects whencontextchanges. - An add-on stops after 60 seconds with no open view. Keep anything that has to last in storage.
Host API
Every method returns a promise that rejects with a PluginError on
failure.
| Method | Returns | Needs |
|---|---|---|
workspace.current(context) | { id, name, rootName, location: "local" }, or null. Never a path | workspace.read |
workspace.subscribe(callback) | A disposer. callback(context | null) runs when the project changes. Without workspace.read it still runs, but always gets null | — |
git.summary(context) | { branch, ahead, behind, staged, unstaged, untracked, conflicts, paths, truncated }. branch is null on a detached HEAD, and paths has up to 500 relative paths | git.read |
panels.open(id, context) | Opens one of your panels | An interaction |
composerViews.open(id, context) | Opens one of your composer views above the composer bound to context | An interaction |
composer.appendText(context, text) | Adds up to 32 KiB to the end of the bound draft and keeps what's there. Resolves to the draft's revision number | composer.append and an interaction |
settings.get() | An object with every declared setting, including defaults | — |
settings.subscribe(callback) | A disposer. callback(settings) runs after the user changes a setting | — |
storage.get(scope, key) | The stored JSON value, or null | — |
storage.set(scope, key, value) | Resolves after the write is saved | — |
storage.delete(scope, key) | Resolves after the delete is saved | — |
http.fetch(context, request) | { status, headers, body } | A matching http entry |
links.open(url, context) | Opens an HTTPS URL in the user's browser | external.open and an interaction |
ui.notify(message) | Shows a short notification with your add-on's name (up to 500 bytes) | — |
appendText rejects with NO_COMPOSER when there's no chat composer, and from a
panel when CodeMux can't tell which composer is meant. A composer action always
targets its own composer. Appending never sends the message.
ui.notify allows three notifications a minute. After that it rejects with
RESOURCE_LIMIT, so catch it: ctx.ui.notify(text).catch(() => {}).
Storage
Each add-on has its own private JSON storage:
await ctx.storage.set({ scope: "global" }, "show-paths", true);
await ctx.storage.set({ scope: "workspace", context }, "last-issue", 42);
const value = await ctx.storage.get({ scope: "global" }, "show-paths");{ scope: "global" }is shared across projects.{ scope: "workspace", context }is separate for each project.- Keys are up to 128 ASCII characters. Values are JSON up to 64 KiB. Each add-on can store up to 5 MiB in total.
- Data stays through restarts and updates. Updates don't convert old data, so read it carefully. Uninstalling deletes it unless the user chooses to keep it.
HTTP
const response = await ctx.http.fetch(context, {
origin: "https://api.example.com",
path: "/v1/items?limit=20",
method: "GET",
headers: { Accept: "application/json" },
});
if (response.status === 200) {
const items = JSON.parse(response.body);
}originandmethodhave to match anhttpentry in your manifest. Other requests reject withNETWORK_DENIED.pathstarts with a single/and can include a query string.bodyis an optional string of up to 256 KiB.headerscan only setAcceptandContent-Type(up to 4 KiB each, no line breaks). Any other header rejects withNETWORK_DENIED.- Requests are sent with the user agent
CodeMux-Addon/1 <id>and ignore system proxy settings. - If the origin has a credential and the user entered one, CodeMux adds it as a
bearer token. If a token was saved but can't be read, for example because the
system keyring is locked, the call rejects with
CREDENTIAL_REQUIRED. - Redirects aren't followed. A redirect response rejects with
NETWORK_DENIED, and so does a connection failure. - Response bodies are UTF-8 text up to 512 KiB. A larger body rejects with
RESOURCE_LIMIT, and a body that isn't UTF-8 rejects withINVALID_MESSAGE. CodeMux doesn't ask for compressed responses, and a compressed response rejects withNETWORK_DENIED. - The only response headers returned are
content-type,retry-after,x-ratelimit-limit,x-ratelimit-remainingandx-ratelimit-reset. - An add-on can have four requests in flight and transfer 10 MiB a minute. Each request times out after 30 seconds.
An HTTP error status such as 404 or 500 still resolves. Check response.status.
Errors
Failed host calls reject with PluginError. Its code is stable, and its
message is readable text you can show to the user.
import { PluginError } from "@codemux/plugin-sdk";
try {
await ctx.composer.appendText(event.context, text);
} catch (error) {
if (error instanceof PluginError && error.code === "NO_COMPOSER") {
setError("Open a chat to add this to a draft.");
} else {
setError(error instanceof Error ? error.message : "Something went wrong");
}
}| Code | Meaning |
|---|---|
PERMISSION_DENIED | The manifest doesn't declare the permission or contribution this call needs |
CONTEXT_STALE | The project or composer for this handle changed or closed |
NO_WORKSPACE | No local project is open for this handle |
REMOTE_UNSUPPORTED | The project is a remote workspace. Calls that use the project only work on local projects |
NO_COMPOSER | No chat composer is available for this handle, or it's not clear which one is meant |
INTERACTION_REQUIRED | The call needs a user interaction, and it's missing, used or expired |
NOT_A_GIT_REPO | The project isn't a Git repository |
INCOMPATIBLE_API | This CodeMux version doesn't support the add-on's api range |
RESOURCE_LIMIT | A quota was used up, such as the request rate, notifications or storage |
TIMEOUT | The host didn't finish in time |
PLUGIN_STOPPED | The add-on is stopping. Pending calls are rejected |
INVALID_MESSAGE | The arguments are invalid, such as text over 32 KiB |
CREDENTIAL_REQUIRED | A saved credential can't be read, for example because the keyring is locked |
NETWORK_DENIED | The request, URL or origin isn't allowed, the server redirected, or the connection failed |
STORAGE_UNAVAILABLE | Storage can't be read or written right now |
Faults
Rejected host calls are expected, and they don't stop your add-on. If a command,
composer action, UI callback or subscription returns a promise that rejects with
a PluginError, CodeMux records a short diagnostic and the add-on keeps running.
Still, catch errors wherever the user needs to see them.
Anything else is a fault, and it stops the add-on:
- A synchronous throw from any callback or render function.
- A promise that rejects with something other than a
PluginError. - An unhandled rejection anywhere, such as in
useEffector a timer. - Invalid UI, such as an unknown prop or a value out of range.
- Going over one of the limits.
A stopped add-on stays off until the user chooses Retry in Settings → Add-ons. CodeMux never restarts it automatically.
If a click arrives for a callback that a newer render removed, or for a view that has closed, CodeMux ignores it and records a short diagnostic.
Limits
| Resource | Limit |
|---|---|
| Host requests | 16 in flight. 20 per second (bursts of 20) and 100 per minute. 15 s timeout, or 30 s for HTTP |
| Execution | 250 ms per callback (1 s during activation), and 1 s of JavaScript per rolling 5 s |
| Memory | 64 MiB heap and a 512 KiB stack |
| Timers | 128 active timers. Every timer waits at least 100 ms, so setTimeout(fn, 0) fires after 100 ms |
| Views | 4 open at once, each with up to 2,000 nodes, 32 levels deep, and 256 KiB of state |
| UI updates | 30 batches per second with up to 1,000 changes each |
| Console | 5 entries per second, up to 1,024 characters each. CodeMux records only their time, level and size, shown under Diagnostics on the add-on's page |
| Running add-ons | 8 at once across CodeMux. Idle ones stop first |
The SDK stays under these limits for you:
- Requests over the per-second rate wait their turn. Once the per-minute budget is
used up, requests reject with
RESOURCE_LIMITwithout reaching CodeMux. - UI updates are combined into about 16 batches per second.
- Console entries over the rate are dropped.
Fast typing won't stop an add-on, but don't make a host request on every keystroke. Save when a field loses focus, when the user clicks a button, or after a pause.