Last updated

Commands & AI context

Two small contribution types: commands that show up in the command palette, and AI context providers that teach Undra’s agents about your extension’s data.

Both follow the same loadable-extension shape you already know from the overview: one extension.js, a manifest with the right capability, and an activate(ctx) that registers contributions. If you have not built one yet, read the ctx API first. Neither of these contributions needs an item type, so they are the lightest things you can ship.

Two separate capabilities

Commands need commands.registry. Context providers need contextProviders.registry. Declare whichever you use in manifest.capabilities. They are independent, declare one or both.

Commands

A command is a named action that appears in Undra’s command palette. The user opens the palette, types your command’s title, hits enter, and your handler runs. That is the whole model.

The shape

Register commands with ctx.registerCommands([...]). Each command is a CommandContribution:

typescript
type CommandContribution = {
  id: string                                   // unique, namespace it with your publisher
  title: string                                // what the user sees in the palette
  category: string                             // groups related commands in the palette
  handler: (ctx: unknown) => void | Promise<void>  // runs when invoked; may be async
}

Field notes:

  • id is your stable handle. Namespace it (for example community.example.todo.archiveDone) so it never collides with another extension’s command.
  • title is the searchable label. Write it as an action: “Archive completed todos”, not “Todos”.
  • category is a grouping label shown alongside the title (for example “Todo”). Keep it short and reuse the same category across your commands.
  • handler does the work. Return a promise if it is async. The argument is the host-provided invocation context, treat it as opaque, your real entry point to the workspace is the ctx you captured in activate (closures work, see below).

A runnable example

This extension adds one palette command that creates a dated note. No item type, no editor, just an action.

extension.js
export const manifest = {
  id: 'community.example.dailynote',
  version: '0.1.0',
  displayName: 'Daily Note',
  publisher: 'example',
  capabilities: ['commands.registry'],
}

export function activate(ctx) {
  ctx.registerCommands([
    {
      id: 'community.example.dailynote.create',
      title: 'Create today\'s daily note',
      category: 'Daily Note',
      handler: async function () {
        const today = new Date().toISOString().slice(0, 10) // YYYY-MM-DD
        await ctx.workspace.create({
          type: 'note',
          title: 'Daily ' + today,
          content: '# ' + today + '\n\n',
        })
      },
    },
  ])

  return function deactivate() {}
}

Drop it at <workspace>/.undra/extensions/dailynote/extension.js, restart Undra, open the command palette, and type “daily”. Your command is there. Selecting it creates a fresh dated note.

The handler closes over activate’s ctx

The handler reaches the workspace through the ctx you captured in activate (the example calls ctx.workspace.create), not through the handler’s own argument. Capture anything you need (the workspace, the query API, your own state) in the closure. See the ctx API for workspace.create, workspace.update, and the read-only query surface.

AI context providers

A context provider is how your extension talks to Undra’s AI agents. When a chat or agent run is being prepared, Undra builds a prompt under a fixed token budget. Your provider gets called and can contribute two things into that budget: sticky facts (short, high-value lines the model should always know) and selected prompt lines (larger blocks, for example the body of a selected item). This is how you teach an agent about a custom item type or a piece of state it would otherwise have no idea exists.

Think of it as briefing the AI. If your extension stores data the model cannot see (a JSON body, an external sync, a computed status), a context provider is where you hand the model a one-line summary so its answers are grounded instead of guessed.

The shape

Register a provider with ctx.registry.registerContextProvider(manifest.id, contrib). The contribution is a ContextProviderContribution:

typescript
type ContextProviderContribution = {
  id: string
  order?: number      // lower runs first; default 100
  critical?: boolean  // if true, a failure surfaces instead of being silently isolated
  collect?: (ctx: unknown) => Promise<ContextProviderCollectResult | null | undefined>
  collectSync?: (ctx: unknown) => ContextProviderCollectResult | null | undefined
}

You provide collect, collectSync, or both:

  • collectSync runs on the fast prompt-preview path. Use it for cheap, immediate sticky facts (no awaiting, no I/O). If the host is just previewing what the prompt will look like, this is what it calls.
  • collect is the async path. Use it for heavier work: reading a selected item’s body, querying the workspace, hitting a cache. Return null (or undefined) when you have nothing to add, the host skips you cleanly.
  • order controls merge order across all providers (yours and Undra’s own). Lower numbers run first. Leave it at the default 100 unless you have a reason to sort earlier or later.
  • critical defaults to false, which means if your provider throws, Undra isolates the failure and drops your slice rather than breaking the whole prompt. Set it to true only when missing your context is worse than a visible error.

What you return is a ContextProviderCollectResult:

typescript
type ContextProviderCollectResult = {
  stickyFacts?: { tier: 'critical' | 'important' | 'supplemental'; text: string }[]
  selectedPromptLines?: string[]
}
  • stickyFacts are short lines the model keeps in view. Each has a tier. When the budget is tight, critical survives, important is next, and supplemental is dropped first. Spend critical sparingly, it is the most expensive real estate in the prompt.
  • selectedPromptLines are full prompt lines (for example the body of the item the user has selected). They are merged in registry order. Use them for bulk content, not for the always-on summary.

Sticky facts cost budget

Every sticky fact competes for a fixed prompt budget with everything else (other extensions, Undra’s own context, the conversation). Keep each line tight and factual. One good critical fact beats five vague supplemental ones. Do not dump whole records here, that is what selectedPromptLines is for.

A runnable example

This pairs with a hypothetical “todo” item type (see item types) and teaches the AI how many todos are open, so an agent asked “what is on my plate” answers from real data instead of hallucinating.

extension.js
export const manifest = {
  id: 'community.example.todocontext',
  version: '0.1.0',
  displayName: 'Todo AI Context',
  publisher: 'example',
  capabilities: ['contextProviders.registry'],
}

export function activate(ctx) {
  ctx.registry.registerContextProvider(manifest.id, {
    id: 'community.example.todocontext.openCount',
    order: 100,
    // Async because we read the workspace. Returns null when there is nothing useful.
    collect: async function () {
      // ctx.query is the read-only workspace query surface (see "The ctx API").
      const page = await ctx.query.queryMetadata({ itemType: 'todo', limit: 500 })
      const open = page.rows.filter(function (r) { return !r.dueDate || r.dueDate >= todayIso() })
      if (open.length === 0) return null
      return {
        stickyFacts: [
          {
            tier: 'important',
            text: 'The user has ' + open.length + ' open todo item(s) of type "todo". '
              + 'Use the todo items, not notes, when asked about tasks.',
          },
        ],
      }
    },
  })

  return function deactivate() {}
}

function todayIso() {
  return new Date().toISOString().slice(0, 10)
}

Drop it at <workspace>/.undra/extensions/todocontext/extension.js, restart Undra, then ask an agent about your tasks. Your sticky fact is now part of the prompt, so the model knows the count and which item type to trust.

Prefer collectSync for cheap facts

If your fact does not need any I/O (it is a constant, or something you already hold in a captured variable), put it in collectSync so it shows up on the fast preview path too. Provide collect only when you must await something. You can provide both, the host calls the right one for each path.

Where to go next

  • The ctx API, the workspace and query surfaces your handlers and providers call into.
  • Item types, give your data a real type so context providers have something concrete to describe.
  • Packaging & distribution, bundle and ship what you built here.