Plugin API reference
Look up the exact signature of anything an extension can call, the permission it needs, and whether the shipped type declarations know about it. For what each namespace is for, with worked examples, read The plugin API first — this page is the lookup layer that guide defers to.
How to read this
Section titled “How to read this”Members are grouped by where the code runs:
ctx.api.*— the main-process half, reached through thectxpassed toactivate(ctx). Declared asPluginAPIin@wamp/extension-sdk.pluginAPI.*— the window half, imported from@wamp/plugin-api.
Every table carries an In types column. No means the member exists at
runtime but is absent from the published declarations, so calling it is a
compile error until you cast around it. Treat a No as unsupported: undeclared
means unversioned, and it can change without notice.
The Permission noted on a namespace is the manifest permission that must be
declared for the namespace to exist. Without it the member is undefined — not
a thrown error — so guard or declare. See Permissions.
Four permission strings are not runtime gates: ai, filesystem, network,
and terminal are disclosure labels shown to the user, and the host does not
withhold those capabilities today. Read this column as a statement about which
namespaces are present, not as a security boundary. Permissions constrain host
APIs only; installed Node and UI code remains trusted code.
ctx.api.db, the old raw-SQLite handle, no longer exists. Raw SQL survives in
exactly two places: the migrations array of a typed-data schema, and
data.raw(sql, params).
The context object
Section titled “The context object”ctx carries three members besides api.
| Member | In types | Description |
|---|---|---|
ctx.pluginId: string |
Yes | This extension’s id — its directory name |
ctx.pluginPath: string |
Yes | Absolute path to the extension’s own directory |
ctx.log |
Yes | info / warn / error / debug, each (message, ...args) |
ctx.api.ai
Section titled “ctx.api.ai”Permission: none.
| Member | In types | Description |
|---|---|---|
complete(input, options?): Promise<string> |
Yes | One-shot completion; resolves to the assistant text |
stream(input, callbacks, options?): Promise<void> |
Yes | The same completion, streamed through callbacks.onChunk |
generateObject<T>(input, schema, options?): Promise<T> |
Yes | Completion constrained to a JSON schema |
completeWithUsage(input, options?) |
Yes | complete plus { text, usage } |
abort(requestId): void |
Yes | Cancels an in-flight stream by the id onRequestStart gave |
input is either a prompt string or a { role, content }[] array.
options is ExtensionAIOptions: model? (defaults to the user’s selected
model), system?, maxTokens? (defaults to 4096), temperature?.
callbacks for stream:
| Callback | In types | Description |
|---|---|---|
onChunk(text) |
Yes | Fires per streamed text fragment |
onEnd(usage, stopReason?) |
Yes | stopReason: 'max_tokens' means the answer was cut off, not finished |
onError(error: Error) |
Yes | Terminal failure |
onRequestStart(requestId) |
Yes | The id abort takes |
Cancelling a main-process stream takes both members: keep the id
onRequestStart hands over, then call abort(id). In the window,
pluginAPI.ai.stream returns a cancel function instead.
There is no multi-turn session primitive. A chat-shaped UI keeps its own
message history (component state, pluginAPI.storage, or typed data) and
calls complete/stream once per turn with that history as input.
ctx.api.agents
Section titled “ctx.api.agents”Permission: none.
| Member | In types | Description |
|---|---|---|
delegate(agentType, task, options?) |
Yes | Runs an agent to completion and resolves with its result |
getAvailable(): string[] |
Yes | Agent ids currently registered |
options: context?, todos?, skills?, modelOverride? (a tier name —
fast, balanced, powerful — or a full model id). The result is
{ text, toolsUsed, stopReason, usage, costUsd }; costUsd is 0 for unpriced
models. The window’s delegate additionally accepts outputSchema and returns
object / schemaError, which the main-process declaration does not carry.
ctx.api.tools
Section titled “ctx.api.tools”Permission: none.
| Member | In types | Description |
|---|---|---|
register(tool): Disposable |
Yes | Registers a tool the assistant can call |
tool fields: name, description, inputSchema (JSON Schema object),
execute(input, ctx), plus optional category, group, display, and policy.
execute returns a string or an array of content blocks
({ type: 'text', text } / { type: 'image', data, mediaType, dims? }).
Tools sharing a group appear to the model as one grouped operation. Policy
supports readOnly, mutates, parallelSafe, untrustedSource, egress,
longRunning, compactable, and persistenceQuota; read-only tools run
concurrently unless parallelSafe is false. Declare egress: 'url' when the
call sends a request to the host its url argument names, or egress: 'any'
when it can reach hosts it picks while running. Outside Full access the user
then allows each host (or, for 'any', the tool) once per chat, and an
unattended run is refused.
Tool names must match ^[a-zA-Z0-9_-]+$. A name containing a dot throws inside
activate(), which faults the whole extension — so write notes_search, never
notes.search. Schemas, display metadata, and policy flags are covered in
Contributing tools.
Inside a tool’s execute
Section titled “Inside a tool’s execute”The second argument to execute is typed unknown. The SDK publishes one
narrow shape for it, ToolContext, and the runtime object has more.
| Member | In types | Description |
|---|---|---|
ctx.services?.client?.request(method, params?, opts?) |
Yes | Reverse-RPC to the connected client; absent on a desktop host, which is the client |
ctx.sendToolUse(name, input, status, result?, meta?) |
No | Pushes an interim tool-use card to the UI; status is running, success, or error |
ctx.api.data
Section titled “ctx.api.data”Permission: none. Typed data is stored in the engine profile; remote renderer queries cross the engine transport.
| Member | In types | Description |
|---|---|---|
defineSchema(schema): Promise<DataAPI<S>> |
Yes | Creates or migrates the tables and returns the typed query surface |
The returned object has one property per table plus raw:
| Member | In types | Description |
|---|---|---|
findMany(args?): Promise<Row[]> |
Yes | where, orderBy, limit, offset |
findUnique(args): Promise<Row | null> |
Yes | Single row by where |
create(args): Promise<Row> |
Yes | Inserts args.data and returns the stored row |
update(args): Promise<{ changes }> |
Yes | Applies args.data to rows matching args.where |
delete(args): Promise<{ changes }> |
Yes | Deletes rows matching args.where |
count(args?): Promise<number> |
Yes | Row count |
raw<R>(sql, params?): Promise<R[]> |
Yes | Raw SQL escape hatch; rows come back untyped |
The predicate builder q (eq, ne, in, notIn, like, isNull,
isNotNull, now, and the comparison operators) is imported from
@wamp/extension-sdk. Schema shape, column types, and migrations are in
Typed data.
ctx.api.storage, secrets, settings
Section titled “ctx.api.storage, secrets, settings”Permission: none for all three.
| Member | In types | Description |
|---|---|---|
storage.get<T>(key, defaultValue?): Promise<T | undefined> |
Yes | Per-extension key/value read |
storage.set<T>(key, value): Promise<void> |
Yes | Write |
storage.delete(key): Promise<void> |
Yes | Remove one key |
storage.keys(): Promise<string[]> |
Yes | Every key this extension has stored |
secrets.set(key, value): Promise<void> |
Yes | Encrypted write |
secrets.get(key): Promise<string | null> |
Yes | Encrypted read |
secrets.delete(key): Promise<void> |
Yes | Remove |
secrets.has(key): Promise<boolean> |
Yes | Existence check without decrypting |
settings.get<T>(key): Promise<T | undefined> |
Yes | The user’s override, falling back to the contributes.settings default |
settings.onDidChange<T>(key, callback): Disposable |
Yes | Observe writes to one setting in the current host process; dispose the returned subscription during deactivation |
settings.set<T>(key, value): Promise<void> |
Yes | Persists under settings:<key> in this extension’s storage |
ctx.api.commands, services
Section titled “ctx.api.commands, services”Permission: none for both.
| Member | In types | Description |
|---|---|---|
commands.register(cmd): Disposable |
Yes | cmd carries the handler inside the descriptor |
commands.execute(id, args?): Promise<unknown> |
Yes | Invokes any registered command |
commands.getAll(): Command[] |
Yes | Every registered command |
services.register<T>(id, impl): Disposable |
Yes | Publishes a service under id |
services.get<T>(id): T | undefined |
Yes | Lookup that tolerates absence |
services.require<T>(id): T |
Yes | Lookup that throws when absent |
services.onRegister<T>(id, cb): Disposable |
Yes | Fires when a service appears |
services.onUnregister(id, cb): Disposable |
Yes | Fires when it goes away |
A Command is { id, title, keybinding?, category?, scope? }, where scope is
focused-view, focused-slot, global, or an app-specific string. Registering
with no handler in either position throws.
ctx.api.ipc, events
Section titled “ctx.api.ipc, events”Permission: none for both.
| Member | In types | Description |
|---|---|---|
ipc.handle(channel, handler): () => void |
Yes | Answers pluginAPI.ipc.invoke from your UI half |
ipc.broadcast(channel, payload): void |
Yes | Pushes to every window; throws synchronously if the channel’s prefix is not a declared ipcNamespaces entry |
events.emit(event, ...args): void |
Yes | Deprecated; use ipc.broadcast for UI notifications |
ipc.handle is generic in its argument tuple, so a typed handler such as
(_e, req: SpawnRequest) => … infers without a cast.
ctx.api.fs
Section titled “ctx.api.fs”Permission: none. These call Node directly with the path you pass, so they are not restricted to the workspace.
| Member | In types | Description |
|---|---|---|
readFile(path): Promise<string> |
Yes | UTF-8 read |
readFileBuffer(path): Promise<Buffer> |
Yes | Binary read |
writeFile(path, content): Promise<void> |
Yes | UTF-8 write |
exists(path): Promise<boolean> |
Yes | Access check |
readDir(path): Promise<string[]> |
Yes | Entry names |
mkdir(path): Promise<void> |
Yes | Recursive create |
copyFile(src, dest): Promise<void> |
Yes | Copy |
remove(path): Promise<void> |
Yes | Delete |
stat(path) |
Yes | { size, isDirectory, isFile, mtime } |
getDataPath(subdir?): string |
Yes | This extension’s own data directory, created if missing |
join, dirname, basename, extname |
Yes | Path helpers, synchronous |
ctx.api.dialog, notifications, shell
Section titled “ctx.api.dialog, notifications, shell”| Member | Permission | In types | Description |
|---|---|---|---|
dialog.openFile(options?): Promise<string[] | null> |
none | Yes | Native picker; options.directory picks folders, options.multiSelections allows several |
dialog.saveFile(options?): Promise<string | null> |
none | Yes | Save picker with title, defaultPath, filters |
notifications.show({ title, body, silent? }): void |
notifications |
Yes | System notification; works with no window open |
shell.onWillOpenExternal(handler): Disposable |
none | Yes | Claims a URL open before the OS browser sees it |
An interceptor returns true to claim the URL. Handlers run in registration
order across extensions, the first true wins, and a throw is treated as
fall-through. Only user-initiated opens travel through this surface.
ctx.api.webviews
Section titled “ctx.api.webviews”Permission: none, but navigation control, script injection, and request
interception each need a matching entry in the manifest’s capabilities.
| Member | In types | Description |
|---|---|---|
create(options): WebviewHandle |
Yes | options: id, html?, url?, enableScripts?, partition?, surface?, webRequestRewrite? |
enableScripts defaults to true; pass false to disable page JavaScript.
Native views always retain their page while hidden.
WebviewHandle:
| Member | In types | Description |
|---|---|---|
id |
Yes | Readonly string |
webview.asWebviewUri(localPath): string |
Yes | Owner-bound URL for a file inside this extension |
webview.cspSource |
Yes | CSP source authorizing URLs returned by asWebviewUri |
loadURL(url): Promise<void> |
Yes | Navigate; resolves on load |
goBack(), goForward(), reload(opts?) |
Yes | History and refresh |
executeScript(source, options?): Promise<unknown> |
Yes | options.world is isolated or main |
onWillNavigate(handler): Disposable |
Yes | Return allow, deny, or { redirect } |
onResourceRequest(handler): Disposable |
Yes | Return { cancel?, redirect?, modifyHeaders? } |
getState<T>(), setState<T>(state) |
Yes | Per-webview state slot |
setSurface(surface): void |
Yes | Moves between visible and headless without reloading |
reveal(): void |
Yes | setSurface('visible') plus raise-to-front |
setBounds(rect): void |
Yes | Window pixels, not CSS pixels — multiply by devicePixelRatio |
openDevTools(opts?): void |
Yes | mode: bottom, right, undocked, detach |
dispose(): void |
Yes | Destroys the webview |
ctx.api.http
Section titled “ctx.api.http”| Member | Permission | In types | Description |
|---|---|---|---|
http.route(method, path, handler): registration |
http-routes |
Yes | Callable disposer with ready: Promise<void>; method is GET, POST, PUT, DELETE, or PATCH |
http.baseUrl(): string |
http-routes |
Yes | Authoritative after the first registration’s ready resolves |
A route handler receives { method, path, params, query, body, headers } and
returns { status?, body?, headers? }. Await registration.ready before
publishing the URL or completing activation. Scheduled agent work is covered in
Scheduled work.
ctx.api.process, pty, git
Section titled “ctx.api.process, pty, git”All three are gated on the single process permission — every git operation
is a git process spawn, so it grants nothing process does not already imply.
| Member | In types | Description |
|---|---|---|
process.spawn(command, args, options?): PluginChildProcess |
Yes | options: cwd, env, stdio |
process.killAll(): void |
Yes | Kills every child this extension started |
pty.spawn(options): PtyHandle |
Yes | options: command, args?, cwd, env?, cols?, rows? (defaults 80×24) |
pty.get(id): PtyHandle | undefined |
Yes | undefined once the session has exited |
pty.list(): PtyHandle[] |
Yes | Live sessions this extension owns |
git.isRepo(cwd): Promise<boolean> |
Yes | Whether cwd is inside a repository |
git.getRepoRoot(cwd): Promise<string> |
Yes | Repository root for cwd |
git.worktree.list(cwd): Promise<GitWorktree[]> |
Yes | Every worktree of the repository |
git.worktree.diff(ref, mainRepoCwd?): Promise<GitFileDiff[]> |
Yes | Per-file status and line counts |
PluginChildProcess exposes pid, stdin, stdout, stderr,
kill(signal?), and on('exit' | 'error', handler).
PtyHandle exposes id, pid, write(data), resize(cols, rows),
kill(signal?), and the disposable-returning onData(cb) and onExit(cb).
Reach for pty whenever a human or a terminal UI is on the other end;
process gives line-buffered pipes with no resize.
GitWorktree is { path, branch, head, isMain, lockReason? }. GitFileDiff is
{ path, status, oldPath?, additions, deletions, patch?, binary? }, where
patch is populated on request rather than by list operations.
ctx.api.auth
Section titled “ctx.api.auth”Permission: auth.identity. Outbound origins are declared separately as
{ "auth.outbound": [origin, …] }.
| Member | In types | Description |
|---|---|---|
getSession(): Promise<AuthSession | null> |
Yes | Cached identity; null when signed out. Never round-trips |
fetch(url, init?): Promise<AuthFetchResult> |
Yes | Signed call to your own backend, forwarded through the main process |
onChange(cb): () => void |
Yes | cb(session, reason), where reason is logged-in, logged-out, token-refreshed, or session-expired |
AuthSession is { user: { id, email, name? }, extensionId, audience, childTokenExpiresAt }. Your extension never sees WAMP’s own token; fetch
attaches a child token whose audience is ext:<extensionId> and returns
{ status, data, headers }. It throws when the URL’s origin is not in the
auth.outbound allowlist (loopback origins are auto-allowed in a dev build) or
when nobody is signed in. See
Sign in with WAMP.
ctx.api.tokens and disposables
Section titled “ctx.api.tokens and disposables”| Member | Permission | In types | Description |
|---|---|---|---|
tokens.onUsage(handler): () => void |
none | Yes | handler({ input_tokens, output_tokens, model?, conversationId? }) |
disposables.add(d): void |
none | Yes | Accepts a Disposable or a plain function |
disposables.disposed: boolean |
none | Yes | Readonly; true after the host tears the extension down |
Use ToolContext.workspacePath for tools and
ExtensionIpcCallEvent.workspacePath for UI requests. Each identifies the
project that invoked the operation.
ctx.api.sessions, clientAffordances
Section titled “ctx.api.sessions, clientAffordances”These depend on services supplied by the host rather than permissions. Desktop supplies both; a headless core without those host services supplies neither. Guard their presence instead of inferring it from local versus remote mode.
| Member | In types | Description |
|---|---|---|
sessions.getActive(): ActiveSession | null |
Yes | The session on screen; null on a fresh launch |
sessions.getKnownIds(): readonly string[] | null |
Yes | Every session that exists. null means the catalog has not loaded — which is not “no sessions” |
sessions.onDidChange(cb): Disposable |
Yes | Fires when the active session or the known-id set changes |
clientAffordances.register(config): Disposable |
Yes | Claims a wamp.client.<namespace>.* namespace |
ActiveSession is { id, name, context }, where context is absent until the
user picks a project. There is deliberately no session-deleted event: deletes
have an undo window and can happen while the app is closed, so reconcile
against getKnownIds() instead of listening.
ClientAffordanceConfig is { namespace, methods, rateLimit: { burst, perMinute }, singleFlight?, handle(submethod, params) }. Registering a
namespace another extension already claimed throws.
The window surface
Section titled “The window surface”pluginAPI is a closed interface: every member is required, so a typo is a
compile error rather than a runtime undefined. Sensitive calls with declared
permission boundaries are re-checked outside the window. pluginAPI has no MCP
namespace; MCP server configuration lives in Settings.
Skill package mutations share the host’s
trusted renderer realm; they are not a per-extension authorization boundary.
The engine still requires a validated preview plus its exact digest and applies
the same trust, containment, and atomic-install policy to every caller.
pluginAPI.notify, ai, agents, skills, tasks
Section titled “pluginAPI.notify, ai, agents, skills, tasks”| Member | In types | Description |
|---|---|---|
notify(message, type?): void |
Yes | In-app toast; type is info, success, or error |
notify.success(message), .error(message), .info(message) |
Yes | The same, spelled per level |
ai.complete(input, options?): Promise<string> |
Yes | One-shot completion |
ai.stream(input, callbacks, options?): () => void |
Yes | Streams, and returns a cancel function |
ai.generateObject<T>(input, schema, options?): Promise<T> |
Yes | Schema-constrained completion |
ai.listModels(): Promise<ListModelsResult> |
Yes | { models, aliases } — the surface a model picker needs |
agents.delegate(agentType, task, options?): Promise<DelegationResult> |
Yes | options adds outputSchema; the result adds object and schemaError |
agents.list(): Promise<string[]> |
Yes | Agent ids available for delegation |
agents.listEntries(): Promise<AgentEntry[]> |
Yes | Full catalog with scope, triggers, tools, and skills |
agents.save(payload): Promise<{ success, error? }> |
Yes | Creates or overwrites a user- or workspace-scope agent |
agents.delete(agentId): Promise<{ success, error? }> |
Yes | Deletes a user- or workspace-scope agent |
agents.listLoadErrors(): Promise<AgentLoadErrorEntry[]> |
Yes | Agent files that currently fail to parse |
agents.listScheduled(): Promise<ScheduledAgentEntry[]> |
Yes | Cron-scheduled agents with next fire time |
agents.onRegistryChanged(cb): () => void |
Yes | Fires on any registry change |
skills.list(): Promise<SkillEntry[]> |
Yes | Every installed skill |
skills.getDetails(qualifiedId): Promise<SkillDetails | null> |
Yes | Exact manifest, body, and resource metadata, loaded on demand |
skills.listLoadErrors(): Promise<SkillLoadError[]> |
Yes | Invalid packages excluded from the usable catalog |
skills.prepareInstall(registryId, skillId): Promise<PrepareInstallResult> |
Yes | Read-only validation and full package preview with exact digest |
skills.install(registryId, skillId, expectedDigest): Promise<SkillInstallResult> |
Yes | Installs only the exact reviewed digest |
skills.uninstall(registryId, skillId): Promise<SkillInstallResult> |
Yes | Removes a registry-installed skill |
skills.checkUpdates(skills): Promise<…> |
Yes | Compares installed hashes against the registries |
skills.listRegistries(): Promise<SkillRegistryEntry[]> |
Yes | Configured registries |
skills.setRegistryEnabled(id, enabled): Promise<void> |
Yes | Persists the effective registry layer |
skills.searchRegistries(query, maxResults?): Promise<SkillSearchResult> |
Yes | { hits, errors } |
skills.onChanged(callback): () => void |
Yes | Install, uninstall, extension, and watched-file changes |
tasks.enqueue(input): Promise<{ runId }> |
Yes | runId is null when the requested agentId is not registered |
tasks.list(runId): Promise<PluginTask[]> |
Yes | Tasks for a run, in creation order |
tasks.get(runId, id): Promise<PluginTask | null> |
Yes | One task |
tasks.subscribe(runId, callbacks): () => void |
Yes | onCreated, onUpdated, onDeleted. Call the returned function or listeners leak |
Neither listModels, skills, nor agents.listEntries has a main-process
equivalent, so build pickers in the window. Agents and
skills covers delegation and the registries.
pluginAPI.data, storage
Section titled “pluginAPI.data, storage”| Member | In types | Description |
|---|---|---|
data.defineSchema(schema): DataAPI<S> |
Yes | Not awaited here; awaited in main |
storage.get<T>(key): Promise<T | null> |
Yes | Per-extension read. Returns null, where main returns undefined |
storage.set(key, value): Promise<void> |
Yes | Write |
The window’s storage has no delete or keys — do those from the main half.
pluginAPI.fs, dialog, http
Section titled “pluginAPI.fs, dialog, http”| Member | In types | Description |
|---|---|---|
fs.read(path): Promise<string> |
Yes | Named readFile in main |
fs.write(path, content): Promise<void> |
Yes | Named writeFile in main |
fs.listDir(path): Promise<DirEntry[]> |
Yes | { name, path, isDirectory } per entry |
fs.exists(path): Promise<boolean> |
Yes | Existence check |
fs.stat(path): Promise<FileStat | null> |
Yes | { type, size, mtimeMs, ctimeMs }, or null when absent or outside the workspace |
fs.mkdir(path): Promise<void> |
Yes | Create |
fs.delete(path): Promise<void> |
Yes | Named remove in main |
fs.move(sourcePath, destPath): Promise<void> |
Yes | No main-process equivalent |
dialog.openFile(options?): Promise<string[]> |
Yes | options: filters, multiple |
dialog.openFolder(): Promise<string | null> |
Yes | No main-process equivalent |
dialog.saveFile(options?): Promise<string | null> |
Yes | options: title, defaultPath, filters |
http.fetch(url, options?): Promise<FetchResult> |
Yes | Routed through the main process, so CORS does not apply. Returns { status, data, headers } |
The window’s fs goes through the host’s file service and its path rules; the
main-process fs does not. It also has no path helpers — join, dirname,
basename, extname, and getDataPath exist only on ctx.api.fs.
Host availability
Section titled “Host availability”Every pluginAPI namespace is present. An operation the host cannot serve rejects with an
error whose code is host_unsupported; navigation.go and navigation.back throw it
synchronously. Unavailable subscriptions return a no-op unsubscribe. A failed fs.exists or
storage.get call rejects instead of reporting false or null.
| Capability | Desktop workspace | Desktop Cloud-session realm | Cloud web |
|---|---|---|---|
notify, workspace.path, navigation params, ui.exitAppMode |
Available | Available; workspace.path is null |
Available |
navigation.go, navigation.back |
Available | host_unsupported |
Available |
fs.read, tools.call, ipc.invoke / ipc.subscribe |
Available | Available | Available |
Other fs operations, workspace.pluginsPath, ui.showExtension |
Available | host_unsupported |
host_unsupported |
shell.openExternal |
Available | Available | host_unsupported |
ai, agents, skills, auth.getSession / auth.fetch, http, storage, dialog |
Available | host_unsupported |
host_unsupported |
auth.resourceFetch |
Available when the viewer has a resource binding | host_unsupported |
host_unsupported |
pluginAPI.ui, navigation, shell, workspace, tools, events, auth
Section titled “pluginAPI.ui, navigation, shell, workspace, tools, events, auth”| Member | In types | Description |
|---|---|---|
ui.showExtension(pluginId): Promise<void> |
Yes | Surfaces another extension’s primary view |
ui.exitAppMode(): void |
Yes | Host-specific: closes a Cloud panel; does not close or navigate a Desktop app window |
navigation.go(pageId, params?): void |
Yes | Unknown ids no-op with a console warning |
navigation.back(): void |
Yes | Desktop restores the previous page and its params; Cloud closes the panel |
navigation.params(): Record<string, unknown> |
Yes | Params of the current page or panel |
shell.openExternal(url): Promise<void> |
Yes | Opens in the OS browser; a rejected scheme throws |
workspace.path(): string | null |
Yes | Workspace root, or null |
workspace.pluginsPath(): Promise<string> |
Yes | Where extensions are installed |
tools.call(toolName, input?): Promise<unknown> |
Yes | Invokes one of this extension’s own registered tools |
events.on(event, callback): () => void |
Yes | event is file.changed or ai.toolUse |
auth.getSession(): Promise<AuthSession | null> |
Yes | Same shape as the main-process member |
auth.fetch(url, init?): Promise<AuthFetchResult> |
Yes, Desktop only | For portable extensions, call the server half over pluginAPI.ipc and use ctx.api.auth.fetch there |
auth.resourceFetch({ audience, path }, { signal }?): Promise<ResourceFetchResponse> |
Yes | GET of a connected resource as the viewer, sent by WAMP Desktop; { status, headers, body }, never the token. Requires the audience in auth.resource; other hosts reject with ResourceFetchError host_unsupported. See Read your service from extension UI |
auth.onChange(cb): () => void |
Yes | Session lifecycle events |
Never call window.alert, prompt, or confirm — they are blocked. Use
<Dialog> from @wamp/ui; see Interface kit.
pluginAPI.ipc
Section titled “pluginAPI.ipc”| Member | In types | Description |
|---|---|---|
ipc.invoke<T>(channel, ...args): Promise<T> |
Yes | Request to your Node half; works over Desktop IPC or the Cloud engine connection |
ipc.subscribe(channel, handler): () => void |
Yes | Listen to a broadcast; call the returned function on cleanup |
Declare the channel prefix in contributes.ipcNamespaces. A host without an
IPC binding rejects invocation explicitly. Subscribe before requesting initial
state when events can arrive during that request.
The window’s activate host
Section titled “The window’s activate host”A UI bundle may export activate(host: ExtensionUIHost) to register renderer-side
contributions. Import ExtensionUIHost and its dock, chat and tool types from
@wamp/extension-sdk. This is how a bundle reaches tool-card rendering, chat
blocks, new-chat destinations and dock tabs.
| Member | In types | Description |
|---|---|---|
host.ownerId: string |
Yes | The extension’s own id |
host.surface: 'workspace' | 'cloud-session' |
Yes | Which shell mounted the host |
host.tools.registerCardRenderer(toolName, component) |
Yes | Draws the chat card for one tool’s results with your own React component. Keyed by the tool’s name |
host.chat.registerBlockKind(kind) |
Yes | Registers a chat-input block kind with a chip and a serializer |
host.chat.insertBlock(block) |
Yes | Inserts a block into the active chat draft |
host.chat.setSessionDestinations(viewId, provider) |
Yes | Publishes a creator for the active project’s new-chat destination picker |
host.chat.clearSessionDestinations(viewId) |
Yes | Removes that view’s destination creator |
host.dock.setTabs(viewId, provider) |
Yes | Publishes a session.dock view’s tabs into the dock’s tab row |
host.dock.clearTabs(viewId): void |
Yes | Removes the provider’s published tabs |
host.dock.present(request) |
Yes | Presents a resource through a view that advertises and validates it |
host.dock.listViews() |
Yes | Lists registered views and the resource kinds they accept |
host.dock.canPresent(sessionId) |
Yes | Whether the host can present a resource for that session |
registerCardRenderer and registerBlockKind return disposables. The tab and
session-destination setters and clearers return void. The host drops all of
these registrations on unload, including after an activation throw. Return
{ dispose() } from activate for any other extension-owned resources; the
same cleanup runs on a Cloud session switch.
Every host passes ownerId, surface and the whole of dock. Desktop’s
workspace adds chat and tools; the Cloud web host and the desktop’s own
Cloud session realm pass neither, and activate lazily when a panel or its
workbench resources are requested. Guard host-specific namespaces before use —
a host publishes a capability group whole or not at all, so host.chat && … is
the check, and activate runs bare, where a missing member is a TypeError
that costs the extension its panel.
surface is how a view avoids drawing a second copy of something the shell
already draws: a cloud-session shell reviews that session’s changes and pull
requests itself, so Git Workbench publishes only its history graph there.
The Cloud host reports an activation throw but still attempts to mount the view;
do not mistake a visible panel for successful provider registration.
- The plugin API — the same surfaces, organized by task.
- Permissions — declaring them, and writing for absence.
- Manifest reference — every field, including
contributesandcapabilities. - Troubleshooting — what a specific error means.