Skip to main content
CODEMUXManual

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

RegistrationCallbackDeclared 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 }) => childrencontributes.panels
ctx.composerViews.register(id, render)({ viewId, context }) => childrencontributes.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 context prop.
  • Commands and composer actions get one as their only argument.
  • UI events such as onPress and onChange get one as event.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 when context changes.
  • 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.

MethodReturnsNeeds
workspace.current(context){ id, name, rootName, location: "local" }, or null. Never a pathworkspace.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 pathsgit.read
panels.open(id, context)Opens one of your panelsAn interaction
composerViews.open(id, context)Opens one of your composer views above the composer bound to contextAn 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 numbercomposer.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 browserexternal.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);
}
  • origin and method have to match an http entry in your manifest. Other requests reject with NETWORK_DENIED.
  • path starts with a single / and can include a query string.
  • body is an optional string of up to 256 KiB.
  • headers can only set Accept and Content-Type (up to 4 KiB each, no line breaks). Any other header rejects with NETWORK_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 with INVALID_MESSAGE. CodeMux doesn't ask for compressed responses, and a compressed response rejects with NETWORK_DENIED.
  • The only response headers returned are content-type, retry-after, x-ratelimit-limit, x-ratelimit-remaining and x-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");
  }
}
CodeMeaning
PERMISSION_DENIEDThe manifest doesn't declare the permission or contribution this call needs
CONTEXT_STALEThe project or composer for this handle changed or closed
NO_WORKSPACENo local project is open for this handle
REMOTE_UNSUPPORTEDThe project is a remote workspace. Calls that use the project only work on local projects
NO_COMPOSERNo chat composer is available for this handle, or it's not clear which one is meant
INTERACTION_REQUIREDThe call needs a user interaction, and it's missing, used or expired
NOT_A_GIT_REPOThe project isn't a Git repository
INCOMPATIBLE_APIThis CodeMux version doesn't support the add-on's api range
RESOURCE_LIMITA quota was used up, such as the request rate, notifications or storage
TIMEOUTThe host didn't finish in time
PLUGIN_STOPPEDThe add-on is stopping. Pending calls are rejected
INVALID_MESSAGEThe arguments are invalid, such as text over 32 KiB
CREDENTIAL_REQUIREDA saved credential can't be read, for example because the keyring is locked
NETWORK_DENIEDThe request, URL or origin isn't allowed, the server redirected, or the connection failed
STORAGE_UNAVAILABLEStorage 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 useEffect or 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

ResourceLimit
Host requests16 in flight. 20 per second (bursts of 20) and 100 per minute. 15 s timeout, or 30 s for HTTP
Execution250 ms per callback (1 s during activation), and 1 s of JavaScript per rolling 5 s
Memory64 MiB heap and a 512 KiB stack
Timers128 active timers. Every timer waits at least 100 ms, so setTimeout(fn, 0) fires after 100 ms
Views4 open at once, each with up to 2,000 nodes, 32 levels deep, and 256 KiB of state
UI updates30 batches per second with up to 1,000 changes each
Console5 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-ons8 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_LIMIT without 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.