# The ctx API A reference for every surface a loadable extension's `activate(ctx)` receives: the React runtime, the registration calls, and the read/write workspace APIs. When Undra loads your `extension.js`, it calls `activate(ctx)` and hands you a single `ctx` object. Everything your extension can do flows through it: there are no imports, no globals, no module resolution. This page documents each member of `ctx` with its exact signature and a tiny runnable example. For the format and lifecycle of an extension, start with the [Overview](/docs/extensions/). :::warn The golden rule Never `import` anything (not even `react`). The module is loaded by URL and cannot resolve packages. Take React from `ctx.runtime`, and bundle every other library inline. Importing `react` yourself loads a second copy and breaks hooks. ::: ## The surface at a glance {#surface} | `ctx.…` | Signature | What it does | |---------|-----------|--------------| | `runtime` | `{ createElement, Fragment, useState, useEffect, useRef, useMemo, useCallback, … }` | Undra's React. Build all UI from this. | | `registry.registerItemType` | `(manifestId, def) => () => void` | Claim a first-class item type (file + route + template). See [Item types](/docs/extensions/item-types/). | | `registerItemTabPresentations` | `(contribs[]) => void` | The type's icon and title. | | `registerItemTabRenderers` | `(contribs[]) => void` | The type's editor component. | | `registerCommands` | `(contribs[]) => void` | Commands for the palette. See [Commands & AI context](/docs/extensions/commands-and-context/). | | `registerCanvasWidgets` | `(contribs[]) => void` | Canvas widgets. See [Canvas widgets](/docs/extensions/canvas-widgets/). | | `workspace.getDocument` | `(itemId) => Promise<{ id, title, content }>` | Read an item's body. | | `workspace.update` | `(itemId, { title?, content? }) => Promise` | Save an item. | | `workspace.create` | `({ type, title?, folderPath?, content? }) => Promise` | Create an item. | | `query` | `{ queryMetadata, searchKeyword, getChangesSince }` | Advanced read-only workspace introspection. | | `exportApi` / `getExtensionApi` | `(api) => void` / `(id) => T \| undefined` | Publish / consume another extension's API. See [Packaging & distribution](/docs/extensions/packaging/). | Each call requires the matching capability in `manifest.capabilities` (`itemTypes.registry`, `commands.registry`, `canvasWidgets.registry`, `contextProviders.registry`). `workspace`, `query`, `exportApi`, and `getExtensionApi` need no capability; they are always present on the desktop host. ## ctx.runtime {#runtime} Undra's own React. It carries `createElement`, `Fragment`, the common hooks (`useState`, `useEffect`, `useRef`, `useMemo`, `useCallback`), and the full `React` object for anything else you need. Destructure what you use at the top of `activate` and alias `createElement` to `h`. ```javascript export function activate(ctx) { const { createElement: h, useState } = ctx.runtime function Counter() { const [n, setN] = useState(0) return h('button', { onClick: () => setN(n + 1) }, 'Clicked ' + n) } // ...register Counter through one of the contribution calls below } ``` :::warn One React, always Undra's If you split your code across files (a bundled extension), do not `import 'react'` in any of them. Fill a small shared module from `ctx.runtime` at activation and import that instead. See [Packaging & distribution](/docs/extensions/packaging/) for the pattern. ::: ## ctx.registry.registerItemType {#register-item-type} Claims a new first-class item type: its own file extension, route, icon, and editor. Returns a disposer you can ignore (Undra tears it down on reload). ```javascript ctx.registry.registerItemType(manifest.id, { id: 'book', // the TYPE: one lowercase token, no dots label: 'Book', pluralLabel: 'Books', // optional, defaults to `${label}s` dockIconClassSuffix: 'book', // optional explorer/dock presentation suffix fileExtension: '.ubook', // '.u' + the type id, by convention routePrefix: '/books', emptyBodyTemplateKind: 'json', // 'json' (body starts as {}) or 'markdown' (empty string) }) ``` Provide `fileExtension`, `routePrefix`, and `emptyBodyTemplateKind` together to register a full first-class type the kernel can create, open, and persist like a built-in. The body is a string you own (usually a JSON blob). This is only the identity call; the icon, title, and editor come from the two `registerItemTab*` calls below. Requires capability `itemTypes.registry`. For the full lifecycle (templates, persistence, the body contract), see [Item types](/docs/extensions/item-types/). ## ctx.registerItemTabPresentations {#item-tab-presentations} Gives the type its icon and title in tabs, the explorer, and the dock. One contribution per type, keyed by the type id. ```javascript function Icon(p) { return ctx.runtime.createElement( 'span', { style: { fontSize: (p && p.size ? p.size : 14) + 'px' } }, '📘', ) } ctx.registerItemTabPresentations([ { id: 'book', title: 'Book', icon: Icon, dockIconClassSuffix: 'book' }, ]) ``` Each contribution is `{ id, title, icon, dockIconClassSuffix? }`. `id` is the type id, `icon` is a component that receives a `size` prop, and `dockIconClassSuffix` is optional. ## ctx.registerItemTabRenderers {#item-tab-renderers} Supplies the editor component for your type. Each contribution is `{ id, render }`, where `render(p)` is called for every open tab and must return your element only when the tab is one of yours. The render contract: inspect `p.tab` and return your editor only when it matches. `p.tab.kind` is `'item'` for item tabs, `p.tab.itemType` is the type id, and `p.tab.itemId` is the id you pass to the workspace APIs. Return `null` for anything that is not yours. ```javascript const TYPE = 'book' const { createElement: h } = ctx.runtime ctx.registerItemTabRenderers([ { id: TYPE, render: function (p) { if (p.tab && p.tab.kind === 'item' && p.tab.itemType === TYPE) { return h(BookEditor, { itemId: p.tab.itemId }) } return null }, }, ]) ``` Your editor component then loads and saves its body with `ctx.workspace.getDocument` and `ctx.workspace.update` (below). A complete editor lives in [Item types](/docs/extensions/item-types/). ## ctx.registerCommands {#register-commands} Registers commands that appear in the command palette. Each contribution is `{ id, title, category, handler }`; `handler` is called with a host context object when the user runs the command. ```javascript ctx.registerCommands([ { id: 'community.example.book.new', title: 'New Book', category: 'Book', handler: async () => { await ctx.workspace.create({ type: 'book', title: 'Untitled Book' }) }, }, ]) ``` Requires capability `commands.registry`. For palette behavior, categories, and AI context providers (the `contextProviders.registry` capability), see [Commands & AI context](/docs/extensions/commands-and-context/). ## ctx.registerCanvasWidgets {#register-canvas-widgets} Registers a renderer for a canvas widget, keyed by `widgetKind`. The canvas owns node creation and mutation; your component renders the node's `data` and writes changes back through `setData`. ```javascript ctx.registerCanvasWidgets([ { widgetKind: 'community.example.counter', title: 'Counter', defaultData: { count: 0 }, defaultSize: { width: 220, height: 120 }, component: function (props) { // props: { data, setData, width, height } const { createElement: h } = ctx.runtime const count = Number(props.data.count || 0) return h( 'button', { onClick: () => props.setData({ count: count + 1 }) }, 'Count: ' + count, ) }, }, ]) ``` Each contribution is `{ widgetKind, title, icon?, defaultData?, defaultSize?, component }`. The `component` receives `{ data, setData, width, height }`: `data` is the node's persisted object, `setData(next)` writes it back (the renderer never touches the canvas document), and `width`/`height` are the node's current pixel size. Requires capability `canvasWidgets.registry`. See [Canvas widgets](/docs/extensions/canvas-widgets/) for the full widget model. ## ctx.workspace {#workspace} The sealed read/write API for item bodies. Your editor uses it to load and persist its content through a contract instead of reaching into renderer internals. The `content` field is the raw body string; your extension owns its meaning. ```ts ctx.workspace.getDocument(itemId: string) => Promise<{ id: string; title: string; content: string }> ctx.workspace.update(itemId: string, patch: { title?: string; content?: string }) => Promise ctx.workspace.create(opts: { type: string title?: string folderPath?: string | null content?: string }) => Promise ``` A load-then-save round trip inside an editor: ```javascript const { createElement: h, useState, useEffect } = ctx.runtime function BookEditor(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 let v = { author: '', notes: '' } try { Object.assign(v, JSON.parse(doc.content || '{}')) } catch (e) {} setData(v) }) return function () { cancelled = true } }, [itemId]) if (!data) return h('div', null, 'Loading...') const save = function (next) { setData(next) ctx.workspace.update(itemId, { content: JSON.stringify(next) }) } return h('input', { value: data.author, onChange: function (e) { save(Object.assign({}, data, { author: e.target.value })) }, }) } ``` Notes: - `getDocument` returns `{ id, title, content }`. The `content` is exactly the string you last wrote (the empty-body template on a fresh item). - `update` takes a partial patch. Pass only `content` to save the body, only `title` to rename, or both. - `create` needs at least `type` (your registered type id). `folderPath` of `null` or omitted creates at a default location; pass a workspace-relative folder path to target a folder. - `update` and `create` resolve to an opaque value; treat them as fire-and-forget writes (await for ordering, ignore the result). ## ctx.query {#query} A read-only introspection API for the wider workspace, separate from `ctx.workspace` (which is scoped to single-item bodies). Use it to list items by metadata, run a keyword search, or follow a change feed. Every method is read-only; it cannot mutate anything. ```ts ctx.query.version // '1' ctx.query.queryMetadata(request: { limit?: number offset?: number folderPath?: string itemType?: string location?: 'live' | 'trash' }) => Promise<{ limit: number offset: number total: number rows: Array<{ id: string relPath: string type: string format?: string | null title: string folderPath: string tags: string[] dueDate?: string | null createdAt: string updatedAt: string location: string deletedAt?: string | null originalPath?: string | null metadataRev: number }> }> ctx.query.searchKeyword(request: { query: string; limit?: number }) => Promise<{ query: string hits: Array<{ itemId: string; score: number }> }> ctx.query.getChangesSince(seq: number, options?: { limit?: number }) => Promise<{ fromSeq: number latestSeq: number events: Array<{ seq: number kind: string itemId?: string | null metadataRev?: number | null createdAtMs: number payload: Record }> hasGap: boolean }> ``` List the titles of every item of your type in a folder: ```javascript const page = await ctx.query.queryMetadata({ itemType: 'book', folderPath: 'Library', limit: 50, }) const titles = page.rows.map(function (r) { return r.title }) // page.total tells you whether to fetch another page with offset ``` Keyword-search the workspace, then load the top hit's body: ```javascript const res = await ctx.query.searchKeyword({ query: 'tolkien', limit: 5 }) if (res.hits.length) { const doc = await ctx.workspace.getDocument(res.hits[0].itemId) // doc.content is the matched item's body } ``` Follow the change feed (poll incrementally, watch `hasGap`): ```javascript let cursor = 0 async function poll() { const res = await ctx.query.getChangesSince(cursor) if (res.hasGap) { // you fell behind the retained window; re-read from queryMetadata instead } for (const ev of res.events) { // ev.kind, ev.itemId, ev.metadataRev, ev.payload } cursor = res.latestSeq } ``` :::note Paging and cursors `queryMetadata` returns `{ limit, offset, total, rows }`: page by advancing `offset` until you have seen `total` rows. `getChangesSince` returns `latestSeq` as your next cursor and `hasGap: true` when the requested `seq` is older than the retained event window (re-snapshot with `queryMetadata` when that happens). ::: `ctx.query` may be absent on hosts that do not provide it, so null-check `ctx.query` before use if you want to be defensive. On the desktop host it is present. ## ctx.exportApi and ctx.getExtensionApi {#api-bridge} The inter-extension bridge. An extension publishes a public API with `exportApi`, and a dependent consumes it with `getExtensionApi`. ```ts ctx.exportApi(api: unknown) => void ctx.getExtensionApi(id: string) => T | undefined ``` ```javascript // In the provider extension's activate: ctx.exportApi({ greet: (name) => 'Hello, ' + name }) // In a dependent extension's activate (declare the dep in manifest.dependencies): const provider = ctx.getExtensionApi('community.example.greeter') if (provider) { // null-check: a dependency may activate and export nothing console.log(provider.greet('world')) } ``` To consume another extension you must declare it in `manifest.dependencies` (a bare id string means "any version, required"; an object adds a semver `version` range and/or `optional: true`). Undra activates dependencies before dependents, so a required dependency has already exported by the time your `activate` runs. A satisfied dependency still gates activation only, not API presence, so always null-check the result. See [Packaging & distribution](/docs/extensions/packaging/) for declaring dependencies and shipping a multi-extension bundle. ## Where to go next - [Item types](/docs/extensions/item-types/): the full lifecycle behind `registerItemType` and the tab renderer contract. - [Canvas widgets](/docs/extensions/canvas-widgets/): the complete widget model behind `registerCanvasWidgets`. - [Commands & AI context](/docs/extensions/commands-and-context/): commands and the `contextProviders.registry` capability.