# Building an Undra extension An extension is a single file that adds new functionality to Undra, a new kind of item with its own editor, a command, or a canvas widget, with no rebuild. This guide is written so an AI agent or a person can build a working extension from it alone. :::tip Building one with an AI? You only need **The format** and **The ctx API** below. Write one `extension.js`, drop it in the extensions folder, restart Undra. That's it. ::: ## Quick start {#quickstart} 1. Write one `extension.js` following [the format](#format) (copy the [example](#example) and change it). 2. Put it at `/.undra/extensions//extension.js`. 3. Restart (or reload) Undra. Your extension is live, its type appears in the explorer's right-click **New** menu, opens in your editor, and saves. ## The format {#format} An extension is a **pure ES module** named `extension.js` with two exports: a `manifest` and an `activate(ctx)` function. ```javascript export const manifest = { id: 'publisher.name', // dot-namespaced, at least 2 segments version: '0.1.0', displayName: 'My Extension', publisher: 'yourname', // lowercase capabilities: ['itemTypes.registry'], } export function activate(ctx) { // register your contributions here using ctx (see "The ctx API") return function deactivate() {} // optional cleanup, called on unload/reload } ``` ### Hard rules :::warn No `import` statements. The module is loaded by URL and can't resolve `react` or any package. **Everything comes through `ctx`**, React is `ctx.runtime`. This is the #1 mistake; don't import anything. ::: - Build UI with `ctx.runtime.createElement` (aliased `h` below). No JSX. - Style with inline styles + Undra's [CSS variables](#theme) so it matches the theme. ## Where it loads {#where} Put your extension in the workspace's hidden extensions folder, one folder per extension: ``` /.undra/extensions//extension.js ``` Undra scans this folder on startup and loads everything it finds. Drop the file, restart, and it's live. (Two other paths you usually don't need: the **dev workbench** search-paths in Settings → Extensions, with hot-reload while you iterate; and the signed **community catalog** for published extensions.) ## The ctx API {#ctx} `activate(ctx)` receives everything through `ctx`: | ctx.… | what it does | |-------|--------------| | `runtime` | React, `createElement`, `Fragment`, the common hooks (`useState`, `useEffect`, `useRef`, `useMemo`, …), and the full `React` object | | `registry.registerItemType(manifestId, def)` | claim a new item type (file + route + template) | | `registerItemTabPresentations([…])` | the type's icon + title | | `registerItemTabRenderers([…])` | the type's editor component | | `registerCommands([…])` | a command (appears in the palette) | | `registerCanvasWidgets([…])` | a widget for the canvas | | `workspace.getDocument(itemId)` | read an item's body → `{ id, title, content }` | | `workspace.update(itemId, patch)` | save an item → `{ title?, content? }` | | `workspace.create(opts)` | create an item → `{ type, title?, folderPath?, content? }` | | `exportApi(api)` / `getExtensionApi(id)` | publish / consume another extension's API (declare it in `manifest.dependencies`) | ## Item types {#item-types} The most common contribution. One call claims a first-class type, its own file extension, route, icon, and editor. Undra persists the file; **you own the body** (a string, usually JSON). ```javascript ctx.registry.registerItemType(manifest.id, { id: 'book', // the TYPE, one lowercase token, no dots label: 'Book', pluralLabel: 'Books', dockIconClassSuffix: 'book', fileExtension: '.ubook', // '.u' + the type id, by convention routePrefix: '/books', emptyBodyTemplateKind: 'json', // 'json' (you own the body) or 'markdown' }) ``` Read the body with `ctx.workspace.getDocument(itemId).content`; save it with `ctx.workspace.update(itemId, { content })`. For structured types, store a JSON string. ## Capabilities {#capabilities} Declare in `manifest.capabilities` what your extension contributes: - `itemTypes.registry`, register a new item type (most common). - `commands.registry`, register commands. - `contextProviders.registry`, register AI context providers. - `canvasWidgets.registry`, register canvas widgets. ## Full example, a Book type {#example} A first-class **Book** item: author, rating, and notes, with its own editor. Save it at `/.undra/extensions/book/extension.js`, restart, then right-click a folder → **New → Book**. ```javascript export const manifest = { id: 'community.example.book', version: '0.1.0', displayName: 'Book', publisher: 'example', capabilities: ['itemTypes.registry'], } export function activate(ctx) { const { createElement: h, useState, useEffect } = ctx.runtime const TYPE = 'book' ctx.registry.registerItemType(manifest.id, { id: TYPE, label: 'Book', pluralLabel: 'Books', dockIconClassSuffix: TYPE, fileExtension: '.u' + TYPE, routePrefix: '/' + TYPE + 's', emptyBodyTemplateKind: 'json', }) function Icon(p) { return h('span', { style: { fontSize: (p && p.size ? p.size : 14) + 'px' } }, '📘') } ctx.registerItemTabPresentations([{ id: TYPE, title: 'Book', icon: Icon, dockIconClassSuffix: TYPE }]) function Editor(props) { const itemId = props.itemId const [data, setData] = useState(null) useEffect(function () { let cancelled = false ctx.workspace.getDocument(itemId).then(function (doc) { if (cancelled) return var v = { author: '', rating: 0, notes: '' } try { Object.assign(v, JSON.parse(doc.content || '{}')) } catch (e) {} setData(v) }).catch(function () { if (!cancelled) setData({ author: '', rating: 0, notes: '' }) }) return function () { cancelled = true } }, [itemId]) if (!data) return h('div', { style: { padding: '24px', color: 'var(--u-text2,#8c969f)' } }, 'Loading...') var save = function (next) { setData(next); ctx.workspace.update(itemId, { content: JSON.stringify(next) }) } var field = { width: '100%', boxSizing: 'border-box', padding: '9px 12px', borderRadius: '9px', font: 'inherit', fontSize: '13.5px', border: '1px solid var(--u-border0,rgba(255,255,255,0.12))', background: 'var(--u-bgPanel,#16191c)', color: 'var(--u-text0,#f2f4f5)', outline: 'none', marginBottom: '10px' } return h('div', { style: { height: '100%', overflow: 'auto', padding: '32px', background: 'var(--u-bgEditor,#111315)', color: 'var(--u-text0,#f2f4f5)' } }, h('div', { style: { maxWidth: '520px' } }, h('div', { style: { fontSize: '18px', fontWeight: 650, marginBottom: '14px' } }, '📘 Book'), h('label', { style: { fontSize: '12px', color: 'var(--u-text2,#8c969f)' } }, 'Author'), h('input', { value: data.author, onChange: function (e) { save(Object.assign({}, data, { author: e.target.value })) }, style: field }), h('label', { style: { fontSize: '12px', color: 'var(--u-text2,#8c969f)' } }, 'Rating (0-5)'), h('input', { type: 'number', min: 0, max: 5, value: data.rating, onChange: function (e) { save(Object.assign({}, data, { rating: Number(e.target.value) })) }, style: field }), h('label', { style: { fontSize: '12px', color: 'var(--u-text2,#8c969f)' } }, 'Notes'), h('textarea', { value: data.notes, rows: 8, onChange: function (e) { save(Object.assign({}, data, { notes: e.target.value })) }, style: Object.assign({}, field, { resize: 'vertical' }) }), ), ) } ctx.registerItemTabRenderers([ { id: TYPE, render: function (p) { return (p.tab && p.tab.kind === 'item' && p.tab.itemType === TYPE) ? h(Editor, { itemId: p.tab.itemId }) : null } }, ]) } ``` ## Scaling up, powerful extensions {#scaling} Everything above hand-writes a single file, ideal for adding a type, a widget, or a command. For something larger (many files, TypeScript, npm libraries), write it like normal software and **bundle it into one `extension.js`** with a build step. The loader only needs one file with no runtime imports, and a bundler produces exactly that, every module and library inlined. This is the standard plugin model: ship a built bundle. There is no practical size limit, a large, library-heavy extension is completely fine. :::warn Use Undra's React, not your own. Take `createElement` and the hooks from `ctx.runtime`, never bundle `react` yourself (two copies break hooks). `ctx.runtime` gives you `createElement`, `Fragment`, the common hooks (`useState`, `useEffect`, `useRef`, `useMemo`, `useCallback`, …) and the full `React`. Bundle every *other* library freely. ::: ### Project layout ``` my-extension/ src/ index.ts // exports manifest + activate runtime.ts // holds Undra's React, filled at activate BookEditor.tsx // components, npm libraries, anything build.mjs // esbuild -> extension.js package.json ``` ### Share Undra's React across files Keep the runtime in one small module that `activate` fills, so every file can use it: ```javascript // Filled once at activate; import { R } from './runtime' anywhere. export const R = {} // R.h, R.Fragment, R.useState, R.useEffect, ... ``` ```javascript import { R } from './runtime' import { BookEditor } from './BookEditor' export const manifest = { id: 'community.example.book', version: '0.1.0', displayName: 'Book', publisher: 'example', capabilities: ['itemTypes.registry'], } export function activate(ctx) { Object.assign(R, { h: ctx.runtime.createElement, Fragment: ctx.runtime.Fragment, useState: ctx.runtime.useState, useEffect: ctx.runtime.useEffect, useRef: ctx.runtime.useRef, }) ctx.registry.registerItemType(manifest.id, { id: 'book', label: 'Book', pluralLabel: 'Books', dockIconClassSuffix: 'book', fileExtension: '.ubook', routePrefix: '/books', emptyBodyTemplateKind: 'json', }) ctx.registerItemTabRenderers([ { id: 'book', render: (p) => (p.tab && p.tab.itemType === 'book') ? R.h(BookEditor, { ctx, itemId: p.tab.itemId }) : null }, ]) } ``` Components `import { R } from './runtime'` and call `R.h(...)` / `R.useState(...)`. Prefer JSX? Point your bundler's JSX factory at `R.h` (below) and write markup as usual. ### The build ```javascript import { build } from 'esbuild' await build({ entryPoints: ['src/index.ts'], outfile: 'extension.js', bundle: true, // inline every import into one file format: 'esm', // an ES module, as the loader expects target: 'es2020', jsxFactory: 'R.h', // only if you use JSX jsxFragment: 'R.Fragment', }) ``` Run `node build.mjs` (wire it to `npm run build`), then drop the produced `extension.js` in `/.undra/extensions//` like any other. That one file can be tens of thousands of lines and pull in whatever libraries you need. ## Theme variables {#theme} Use these in inline styles so your extension matches light and dark themes. Always pass a fallback, e.g. `var(--u-accent, #5b8def)`. - **Text**, `--u-text0` (primary), `--u-text1`, `--u-text2` (muted) - **Surfaces**, `--u-bgEditor`, `--u-bgPanel`, `--u-bg0`, `--u-bgHover` - **Borders**, `--u-border0`, `--u-border1` - **Accent**, `--u-accent` (+ `--u-accentSubtle`), `--u-danger`, `--u-warning` ## Testing {#testing} 1. Save your `extension.js` in the folder above. 2. Restart or reload Undra. 3. Open **Settings → Extensions**, your extension should be active, or it shows an error with the reason if the code threw. 4. For item types: right-click a folder → **New → Your Type**, edit it, restart, and confirm it reloads with your data intact. :::tip Common mistakes Most errors come from one of three things: an `import` statement (not allowed, use `ctx`), a `manifest.id` that isn't dot-namespaced, or a `TYPE` with a dot in it (it must be a single token). ::: ## Trust & sharing {#trust} A loadable extension runs with **full access**, it can read and write files and reach the network. Only run extensions you trust. Published, signed extensions go through the community catalog and show a **Verified** badge; anything you drop in the folder yourself or sideload is **Unverified**, fine for your own, and worth a careful look for anyone else's. ## Where to go next {#next} This page is the quick start. The rest of the section goes deeper: - **[The ctx API](/docs/extensions/ctx-api/)**, a reference for every member of `ctx`, with signatures. - **[Item types](/docs/extensions/item-types/)**, the full first-class item-type contract and editor pattern. - **[Canvas widgets](/docs/extensions/canvas-widgets/)**, build a widget that lives on the canvas. - **[Commands & AI context](/docs/extensions/commands-and-context/)**, palette commands and feeding context to agents. - **[Packaging & distribution](/docs/extensions/packaging/)**, dev-mode live reload, signing, sideloading, and dependencies.