Skip to content

Extension authoring

OpenCode Superapp uses a model familiar to VS Code Extension authors: one installable package, an optional main entrypoint, activation events, contribution points, a public API, and a separate Extension Host. The formats and APIs are adapted to Superapp and are not compatible with VS Code Extensions or .vsix packages.

There is no released CLI or marketplace yet. Create a folder and install it from Extensions > Install > Install from folder.

my-extension/
├── package.json
└── icon.svg
{
"$schema": "https://opencode-superapp.local/schemas/extension-package.schema.json",
"name": "my-extension",
"publisher": "example.author",
"version": "1.0.0",
"displayName": "My Extension",
"description": "Explain what this Extension adds.",
"engines": { "opencodeSuperapp": ">=0.1.0" },
"icon": "icon.svg",
"contributes": {}
}

Superapp derives the ID as example.author.my-extension. Do not add source, trust, installed state, enabled state, permissions, requirements, or private service IDs to the manifest. Package paths must be relative and stay inside the folder; symlinks are rejected.

Optional fields include main, activationEvents, readme, license, repository, author, homepage, keywords, os, and cpu. Superapp does not install npm dependencies or execute package lifecycle scripts.

{
"contributes": {
"skills": [{ "path": "skills/review/SKILL.md" }]
}
}

The file must be a complete Skill with name and description frontmatter. Use Skills > Import Skill instead when one loose Skill does not need package identity, versioning, or distribution.

A remote MCP server uses HTTPS:

{
"contributes": {
"mcpServers": [{
"id": "example-tools",
"name": "Example Tools",
"description": "Tools from the Example service.",
"runtime": { "kind": "remote", "url": "https://mcp.example.com" }
}]
}
}

Package-local MCP implementations use either:

{ "kind": "javascript", "entrypoint": "mcp/index.mjs" }

or a package-relative executable command:

{ "kind": "executable", "command": ["bin/example-server", "--stdio"] }

OpenCode owns connection, authentication, tools, and live status. Local packages cannot request Superapp’s private integrated services.

Declare the command and executable entrypoint:

{
"main": "src/extension.ts",
"activationEvents": ["onCommand:example.author.my-extension.hello"],
"contributes": {
"commands": [{
"command": "example.author.my-extension.hello",
"title": "Say Hello"
}]
}
}

Register the declared ID from code:

import { commands } from "@opencode-superapp/api";
import type { ExtensionContext } from "@opencode-superapp/api";
export function activate(context: ExtensionContext) {
context.subscriptions.push(
commands.registerCommand("example.author.my-extension.hello", () => "Hello")
);
}

activate(context) is required when main is present; deactivate() is optional. The host validates runtime registrations against package.json.

A composer action places an already declared command:

{
"contributes": {
"commands": [{ "command": "example.author.my-extension.rewrite", "title": "Rewrite selection" }],
"menus": { "composer": [{ "command": "example.author.my-extension.rewrite" }] }
}
}

The API offers limited composer.insertText() and composer.replaceSelection() helpers. It does not expose unrestricted draft or application state.

Declare a webview and activate when it opens:

{
"main": "src/extension.mjs",
"activationEvents": ["onView:notes"],
"contributes": {
"views": {
"rightPanel": [{ "id": "notes", "name": "Notes", "type": "webview" }]
}
}
}

Register a provider:

import { window } from "@opencode-superapp/api";
export function activate(context) {
context.subscriptions.push(window.registerWebviewViewProvider("notes", {
resolveWebviewView(view) {
view.webview.options = { enableScripts: true };
view.webview.html = `<!doctype html><textarea class="scratchpad"></textarea>`;
view.webview.onDidReceiveMessage(message => context.log.info("View message", { message }));
}
}));
}

Webviews run in sandboxed iframes without direct Tauri or parent-DOM access. Use acquireSuperappApi().postMessage(value) inside the webview and view.webview.onDidReceiveMessage() in Extension code. Use view.webview.postMessage() for the reverse direction. Package assets must be converted with asWebviewUri(). Persistent view state is not supported in v1.

Supported activation events are onCommand:<id>, onView:<id>, onStartupFinished, and *. Command and view events are inferred when omitted. Events for unsupported APIs, including onLanguage, are rejected.

Imported executable code runs with your operating-system user privileges in a separate crash-isolation process. It has no public access to private native services or arbitrary Tauri commands. Users see it as Unverified local extension.

The installed folder is a snapshot. Increase the semantic version before installing an update; the same or an older version is rejected. An .ocx file is simply a ZIP of the folder with package.json at its root, or within exactly one top-level directory. Do not include node_modules, caches, secrets, or unrelated project files.