Skip to content

Core concepts

Twelve terms, in the order they build on each other. Each section says what the thing is and why you care. Read once now; the rest of the documentation assumes these words.

Why you care: it is the only unit of code the platform loads, so it is what you are always building.

An extension is a directory containing an extension.json manifest and subdirectories for whatever it ships: ui/ for a React surface, main/ for code that runs in the host process, agents/, skills/, commands/, hooks/, mcp/. Discovery is by convention — you create only the directories you use.

The id is the directory name: kebab-case, starting with a letter. There is no id field in the manifest.

An extension has up to two halves, both optional. The renderer half (ui/index.tsx) runs in the app window and exports a views object. The main half (main/activate.ts) runs in the host’s Node process and exports activate(ctx) / deactivate(). A pure-UI app ships only ui/; a headless tool pack ships only main/.

Why you care: it is the whole declaration — identity, surfaces, and access — and it is validated, so a typo fails loudly instead of quietly.

extension.json is checked against a closed schema at install and at activation. name and version are required; version is strict MAJOR.MINOR.PATCH with no pre-release tags. Unknown fields inside contributes.commands[], contributes.pages[], contributes.settings[], and contributes.toolMetadata[] are rejected rather than ignored.

Two version fields do different jobs. compat.pluginApi is the range of the plugin API you target: if the host’s version does not satisfy it, activation refuses with a message naming both versions. engines.wamp is the host-compatibility range shown to users in the catalog, and the marketplace requires it before it will accept an upload.

Why you care: “app” is not a different artifact — it is one field, so you can decide late.

Any page an extension contributes carries a presentation. The default, "docked", renders your page inside WAMP’s own chrome alongside the assistant. "app" gives your page the window: WAMP’s chrome is hidden and you draw your own with AppShell from the interface kit, leaving via AppShell.BackButton or pluginAPI.ui.exitAppMode().

{ "id": "tasks", "title": "Tasks", "context": "both", "presentation": "app" }

Everything else — manifest, permissions, build, distribution — is identical. An app is an extension that owns its window — nothing more.

Why you care: this is how your code becomes reachable; nothing you write shows up until it is contributed.

The contributes block declares what the extension adds to the host:

Contribution What it adds
pages[] A sidebar entry and the surface behind it. context is "global", "project", or "both".
views[] A component in a named slot: sidebar.primary, session.dock, statusbar.left, statusbar.right, chatInput.attachments, overlay.global. Each slot passes typed props.
commands[] A palette entry with an optional keybinding and scope. You declare it here and register the handler at activation.
agents[], skills Agent definitions and skills (below).
settings[] Typed per-extension user settings with defaults.
services[] A typed interface other extensions can require by id.
mcpServers[], agentRuntimes[] A tool-providing process, or a foreign agent that can answer a chat turn.

A dock tool is a session.dock view: a single panel bound to the active session’s working directory — a file tree, a preview, a browser. It receives the session id, the directory, and a requestFocus callback, and it must render a labeled state for “no project chosen yet” and “still loading”.

Your main half does not run until something wakes it. activationEvents declares what that is — onStartupFinished, onCommand:<id>, onView:<id>, onTool:<id>, onAgent:<id>, and a few more. There is no eager *: startup cost stays flat as the user installs more.

Why you care: half of them are enforced and half are consent signals, and knowing which is which prevents both a broken build and a false sense of safety.

permissions is an array in the manifest: database, network, cron, ai, terminal, filesystem, http-routes, notifications, process, auth.identity, mcp, and the structured { "auth.outbound": [origins] }.

Enforced permissions are withheld when undeclared — the capability is absent from the API your code receives, so ctx.api.cron is undefined and calling it throws. That covers cron, http-routes, notifications, process, auth.identity, auth.outbound, mcp, and the hosted cloud store.

Declarative permissions — ai, filesystem, database for local storage, network, terminal — are shown to the user at install and describe what the extension intends to touch, but the host does not withhold them at runtime today. Declare them honestly; do not treat a missing declaration as a barrier.

capabilities is a separate array for narrower opt-ins, such as webviews.navigate and webviews.executeScript.

Why you care: it is the entire surface between your code and the platform, and it comes in two flavors depending on which half you are in.

In the main half, activate(ctx) receives a context whose ctx.api carries the capability namespaces: ai, agents, data, cloud, cron, http, tools, webviews, secrets, notifications, services, commands, ipc, events, fs, process, pty, git, dialog, shell, tokens, storage, settings, workspace, sessions, auth, disposables. Anything that returns a disposable goes onto ctx.subscriptions and the host cleans it up for you:

ctx.subscriptions.push(
ctx.api.cron.schedule('daily-digest', '0 9 * * *', runDigest),
);

In the renderer half you import a single object, and every call is awaited:

import { pluginAPI } from '@wamp/plugin-api';

It is scoped to your extension — storage keys and tool names are namespaced for you. Its surface is deliberately narrower: ai, agents, skills, tasks, auth, data, cloud, fs, dialog, terminal, http, storage, shell, ui, navigation, workspace, tools, events, mcp, and a callable notify. The long-lived and security-sensitive namespaces — scheduling, secrets, child processes, HTTP routes, webviews, services, commands — exist only in main. To reach those from your UI, register a service both halves share, or register a tool in main and call it with pluginAPI.tools.call(name, input).

Why you care: it is the storage most apps want, and it is the one thing both halves of your extension can read with the same types.

You declare a schema once — conventionally shared/schema.ts — and hand it to defineSchema from either half. Main and renderer get the same typed handle; the renderer’s is a proxy over the process boundary.

const schema = {
version: 1,
tables: {
books: {
id: { type: 'text', primary: true },
title: { type: 'text', notNull: true },
status: { type: 'enum', values: ['want', 'reading', 'done'] as const },
added: { type: 'datetime', defaultNow: true },
},
},
} as const;

Column kinds are text, integer, real, boolean, datetime, json, and enum. Queries take predicates (gte, in, like, isNull, …) with bare values as shorthand for equality. Editing the schema later is safe in the direction that matters: a new column is added to the existing table automatically, while a change that would destroy stored rows fails loudly instead. Under it is a per-extension SQLite database, and data.raw(sql, params) is the escape hatch for a query the predicate DSL cannot express.

Typed data is local to the machine. An app whose users sign in and expect their records on a second device uses the hosted document store instead (cloud.mine and cloud.shared), which needs the database permission.

Why you care: a tool is how the AI does something in your app rather than only talking about it.

A tool is a named function with a JSON-Schema input, registered at activation:

ctx.subscriptions.push(
ctx.api.tools.register({
name: 'reading_list_add_book',
description: 'Add a book to the reading list.',
inputSchema: { type: 'object', properties: { title: { type: 'string' } }, required: ['title'] },
execute: async ({ title }) => data.books.create({ data: { id: `b-${Date.now()}`, title } }),
}),
);

Names match [a-zA-Z0-9_-] and must not contain dots — major model providers reject those, so an invalid name fails activation immediately. Once registered, the tool is in the host’s registry: any agent whose definition allows it can call it, including a foreign coding agent selected in the composer, which is lent WAMP’s registry over MCP minus the file and terminal categories. Your own UI narrows a conversation to your tools with tools: { include: ['reading_list_*'] }.

toolMetadata — an icon, a label, a category, and whether the tool is read-only or long-running — controls how a call renders in the transcript.

Why you care: an agent is a configured way of working that your app can invoke; a skill is knowledge loaded only when it is relevant.

An agent definition is a markdown file — agents/<id>.md — with YAML frontmatter (name, description, tools, optionally a model and budget) and the system prompt as the body. Definitions live in layers with fixed precedence (builtin, then extension, then user, then workspace); a higher layer shadows a lower one rather than replacing it, so uninstalling an extension reveals the built-in agent underneath.

A skill is also a markdown file with frontmatter, but it is not an actor: it is instructional content an agent loads on demand when the task matches the skill’s description. Ship skills for the knowledge your app needs the assistant to have without paying for it in every conversation.

Your code can hand work to an agent and get a result back:

const { text } = await ctx.api.agents.delegate('explorer', 'Summarize this quarter');

Call ctx.api.agents.getAvailable() — or pluginAPI.agents.list() — before naming an agent: the set is dynamic.

Why you care: these are the two units you will see in every log, budget, and piece of UI about work in progress.

A run is one execution of an agent loop. It has an id, a status (running, completed, failed, cancelled, crashed), a stop reason, a budget snapshot, accumulated token usage, and a trigger source that records where it came from: chat, cron, webhook, delegation, one-shot, external, or goal. Delegation makes child runs, so a piece of work is a tree rather than a line. Runs are persisted, which is why one can be recovered after a crash.

A session is a multi-turn conversation. In the shell, a chat session is always bound to a directory — there is no floating conversation. In your code, a session is an object you own: created with a conversationId, optionally narrowed to your tools and extended with your own system prompt, emitting streaming events, and persisting its history wherever you tell it to.

const { messages, send, isStreaming } = useAISession({
conversationId: 'reading-list.main',
tools: { include: ['reading_list_*'] },
});

The relationship: sending into a session starts a run.

Why you care: this is how an extension stops being something users install and becomes something they download.

A product is a brand file — product.json — that turns the platform into a named binary: application name, icon, window, which extensions are bundled, the user-data directory, the update feed, and optionally a rootPageId so the app boots straight into your page instead of the standard shell. The user-data directory is named after the product, which is why two products on one machine never share state.

Nothing in your extension changes to be shipped this way. A product is configuration around the same artifact, and the same extension can also be published to the marketplace.

Why you care: it decides who signs in to your product, and it is one line rather than an architecture.

A product declares accounts.mode:

Mode Who signs in Notes
wamp A WAMP account. The default. Takes no other keys.
none Nobody — the app opens straight into itself. With an appSlug, each installation gets a stable anonymous identity and an AI credential; without one, the product is purely local and has no hosted AI.
own Your users, who never see a WAMP login. Requires an appSlug. signUp: false makes it invite-only.

Your extension code does not branch on the mode. The shell owns sign-in, and what reaches your app is a session either way — which is what lets the same app ship all three ways unchanged.