| name | cate-extension |
| description | Build, test, and publish a Cate extension, a web panel (optionally backed by a local server) that runs on Cate's canvas. Use when the user wants to create or scaffold a Cate extension, add a panel to Cate, work with the extension manifest, cateApi scopes, or the window.cate host API, or submit an extension to the cate-extensions catalog. |
| user-invocable | true |
Authoring Cate Extensions
A Cate extension adds panels to Cate's infinite canvas by shipping a web
frontend, optionally plus a local server process. Panels render in
isolated webviews and talk to Cate only through the injected window.cate
bridge, gated by manifest-declared scopes.
Two shapes:
- Frontend-only (default): static web assets. Cate serves them and injects
the
cate bridge. No process, port, token, or lifecycle. Best for viewers,
editors, formatters, dashboards over cate.storage.
- Server-backed: also ships a local server for full OS access (filesystem,
processes, network). Cate spawns one server per extension per workspace;
every panel webview of that extension connects to it (n:1).
Official extensions live in the catalog repo
github.com/0-AI-UG/cate-extensions (one folder per extension under
extensions/<id>/). The Cate repo carries an in-tree mirror at
cate-extensions/ for offline dev and tests. When working inside either repo,
read a shipped extension as a live reference: cate.mermaid (frontend-only),
cate.usage (server-backed), cate.frontendkit / cate.kitchensink
(dev-only reference apps, not published).
Creating a new extension: where to start
Two starting points; decide before scaffolding:
-
Clone the catalog repo and scaffold at extensions/<id>/ when the
extension might be published later, should use the shared UI kit, or is more
than a small one-off. The kit ships only by file copy inside that repo
(there is no npm package), and starting there gives you ./build.sh
validation and the PR flow with no later migration.
git clone git@github.com:0-AI-UG/cate-extensions.git
-
Scaffold inline in the current workspace (any folder holding a
manifest.json) for small, private, workspace-local tools. No kit: theme by
hand from cate.theme.get(). If it outgrows this, move the folder into a
catalog-repo clone and adopt the kit then.
Either way, the day-to-day loop is sideloading (see Local development loop);
the catalog repo additionally supports the local file:// catalog flow.
Project anatomy
Minimal frontend-only extension (build tool optional; plain static files work):
extensions/acme.example/
manifest.json # required, see below
README.md # first line becomes the catalog description fallback
package.json # only if it needs a build: must expose "build" script
index.html # or src/ + vite build -> dist/index.html
src/
main.ts
_kit/ # synced copy of the shared UI kit (never edit; see Kit)
cate-host.d.ts # typings for window.cate
Packaging rule (build.sh in the catalog repo): if the extension directory
contains a package.json with a build script, CI runs npm install +
npm run build; if a dist/ exists after that, the published artifact ships
only manifest.json + dist/, otherwise the whole folder. manifest.json
is always at the artifact root.
Manifest (manifest.json)
{
"id": "acme.example",
"name": "Example",
"version": "1.0.0",
"description": "One-line catalog description.",
"frontend": "dist/index.html",
"panels": [
{ "id": "main", "label": "Example", "icon": "<svg …>…</svg>",
"defaultSize": { "width": 600, "height": 400 } }
],
"server": { "command": "node dist/server.js", "readyPath": "/health", "portEnv": "PORT" },
"cateApi": ["storage", "theme"]
}
| Field | Rules |
|---|
id | Required. Must match ^[A-Za-z0-9][A-Za-z0-9._-]*$ (it becomes a filesystem path). Convention: publisher.name, e.g. cate.mermaid. Invalid id rejects the whole manifest. |
name | Display name; falls back to id. |
version | SemVer-ish, must match ^[A-Za-z0-9][A-Za-z0-9.+_-]*$ or it is silently dropped (treated as 0.0.0). The artifact is <id>-<version>.tgz; bump it for every published change. |
panels | Required, non-empty. Every panel needs non-empty id and label or the whole manifest is rejected. icon is an inline SVG string. defaultSize needs both numbers. |
frontend | Entry HTML for frontend-only extensions. Ignored when server is present (the server serves its own frontend). |
server | Optional; makes the extension server-backed. command required; readyPath defaults to /health, portEnv to PORT. |
cateApi | Scopes the extension uses (see next section). |
description | Optional; wins over the README first line in the catalog. |
dev | true excludes the extension from the published catalog (still built; for reference apps). |
Scopes (cateApi)
Host-enforced, default-deny: any cate.* call outside the declared scopes
returns { error: 'scope-denied' }. A bare namespace grants its sub-scopes
(editor grants editor.read + editor.write). Declare the minimum; scopes
are shown to the user as the extension's permissions.
| Scope | Unlocks |
|---|
| (none) | cate.version, cate.panel.id, cate.panel.setTitle |
workspace.read | cate.workspace.get() |
theme | cate.theme.get() |
ui | cate.ui.notify() |
editor.read | cate.editor.* except openFile |
editor.write | cate.editor.openFile() |
storage | cate.storage.* |
canvas | cate.canvas.createPanel() |
files.drop | cate.files.onDrop() |
agent | cate.agent.* (plus first-use user consent per app session; one run at a time per extension, concurrent runs get { error: 'agent-busy' }) |
Host API (window.cate)
The complete surface today. Canonical typings: src/shared/cate-host-api.d.ts
in the Cate repo, mirrored as kit/cate-host.d.ts in the catalog repo and
synced into each extension's src/_kit/. Trust the .d.ts over any prose docs.
cate.version(): Promise<number>
cate.panel.id: string
cate.panel.setTitle(title: string): Promise<void>
cate.workspace.get(): Promise<{ rootPath, branch, worktree }>
cate.theme.get(): Promise<{ id, type: 'dark'|'light', app, terminal }>
cate.editor.openFile(path, { line?, column? })
cate.canvas.createPanel(type, {
position?: { x, y },
url?, filePath?,
extensionId?, extensionPanelId? })
cate.ui.notify(message, level?: 'info'|'warn'|'error')
cate.files.onDrop(cb): () => void
cate.storage.get/set/delete/keys
cate.storage.panel.get/set
cate.storage.onChange(cb): () => void
cate.agent.open({ resume? }) => { sessionId } | { error }
cate.agent.send(sessionId, prompt) => { text, message } | { error }
cate.agent.dispose(sessionId)
cate.agent.run(prompt) => { text, message } | { error }
cate.agent.cancel()
Agent turns are long-lived (minutes); they resolve on the agent's terminal
agent_end. Do not wrap them in short timeouts.
cate.storage persists as hand-editable JSON under
<project>/.cate/extensions/<extensionId>/. Frontend and server share the same
store, so it is the supported channel for cross-panel and panel-to-server
state. JSON-serializable values only; anything large or binary belongs in a
server-backed extension's own filesystem.
UI kit and theming
The catalog repo ships a shared kit at kit/ so extensions look native to
Cate:
cate-kit.css: design tokens (--cate-*) + component classes (cate-*) for
app shell, buttons, inputs, cards, banners, drawer, empty state, spinner.
theme.ts: initTheme() / applyTheme(), maps cate.theme.get() onto the
tokens (declare the theme scope).
service-connection.ts: ServiceConnection, a state-machine widget
(idle / provisioning / connecting / needs-connection / ready / error) that
gates the panel behind a connection card. Use it for extensions wrapping a
bring-your-own external service.
server/http.ts: Node HTTP scaffolding for server-backed extensions.
api-client.ts: proxyBasePath() / apiFetch() for panel-to-server calls
through Cate's proxy (the webview never holds the token; fetch relative
paths and the proxy injects the bearer token).
There is no monorepo: the kit is copied into consumers at src/_kit/ by
node scripts/sync-kit.mjs, and the copies are committed. To adopt it, add
your extension id to KIT_CONSUMERS (and SERVER_CONSUMERS if server-backed)
in scripts/sync-kit.mjs, run the sync, and never edit src/_kit/ directly
(CI runs sync-kit.mjs --check and fails on stale copies).
Server-backed contract
Only relevant when the manifest has server. Cate injects env on spawn:
PORT: free port to listen on. HOST=127.0.0.1: the server must bind
this, never 0.0.0.0 (a wider bind exposes it on the network and defeats
the token gate).
CATE_TOKEN: shared secret; require it on every panel connection.
CATE_API: token-gated local HTTP/WS endpoint for server-side reverse-API
calls and event streams.
WORKSPACE_ROOT: the workspace the server belongs to.
Lifecycle: lazy spawn on first panel open per (extensionId, workspace); Cate
probes readyPath before loading the webview (timeout/exit shows captured
stderr + Restart). Many panels share the one server: route state and events by
cate.panel.id, treat panel open/close as join/leave, and survive panel
remounts (dock moves) without dropping state. When the last panel closes there
is a ~30s grace window, then SIGTERM/SIGKILL. Crashes auto-restart with backoff
(2 attempts per 60s), then require a manual restart.
Local development loop
Two ways to run an in-progress extension, both from Settings -> Extensions:
- Sideload a folder (fastest): "Add local folder…" pointing at the
extension directory (the one containing
manifest.json; build first if it
needs dist/). On a local workspace the folder is served in place, so
frontend edits only need a rebuild + panel reload. On a remote workspace it
is re-uploaded on every re-provision.
- Local catalog: in a checkout of the catalog repo run
./build.sh (with
CATALOG_BASE_URL unset it writes dist/catalog/index.json with file://
artifact URLs), then add the absolute path to that index.json as a
catalog source. Local catalog entries always re-provision on panel open, so
edits land without version bumps.
Write tests where logic allows (vitest is the convention; see cate.mermaid's
src/*.test.ts), and give the extension a typecheck script against the kit
typings.
Publishing to the catalog
The trust boundary is PR review; merging to main publishes automatically.
- Fork/clone
github.com/0-AI-UG/cate-extensions and add
extensions/<your-id>/ (move the folder in, if it was scaffolded inline in
a workspace) with manifest.json and a README.md whose first line is a
good one-line description (used by the catalog when the manifest has no
description).
- If using the kit, add the id to the consumer lists in
scripts/sync-kit.mjs and commit the synced src/_kit/.
- Verify locally:
./build.sh must succeed end to end (it builds every
extension, tars artifacts, and generates the index).
- Open a PR. CI runs
./build.sh to validate. Expect the review to be a
security review: servers run unsandboxed on user machines.
- On merge, CI rebuilds with
CATALOG_BASE_URL pointing at the rolling
catalog GitHub Release and uploads index.json plus every
<id>-<version>.tgz as release assets. Users get it from the default
catalog source https://github.com/0-AI-UG/cate-extensions/releases/download/catalog/index.json.
For updates: bump version in both manifest.json and package.json, since
the artifact name embeds it and installed copies are keyed by it.
Pre-submit checklist
id matches ^[A-Za-z0-9][A-Za-z0-9._-]*$; panels non-empty, each with
id + label.
cateApi is minimal; no bare namespace when one sub-scope suffices.
- Frontend degrades gracefully when a call returns
{ error: 'scope-denied' }
or undefined (older hosts); gate features on cate.version().
- Theme scope declared and
initTheme() wired, so the panel matches light and
dark themes.
- Server (if any): binds
HOST, honors PORT, rejects connections without
CATE_TOKEN, routes per-panel state by cate.panel.id, and tolerates
panels joining/leaving without restarting.
files.drop users also handle native webview drop events as a fallback
for windows where the host overlay is not active.
./build.sh passes; artifact contains manifest.json at the root.
- README first line reads well as a catalog description.