docs(plugins): update README for ES module frontend

Replace iframe-based plugin documentation with ES module approach.
Update architecture diagram, file structure, and manifest.json
examples to reflect the switch from index.html/iframe to
index.js/module with mount/unmount exports and api.rpc() helper.
This commit is contained in:
simosmik
2026-03-05 13:07:53 +00:00
parent c9465a64be
commit 23d59ec716
2 changed files with 67 additions and 141 deletions

View File

@@ -1,10 +1,10 @@
# Hello World Plugin
A minimal example showing how to build a plugin for Claude Code UI, covering both frontend (iframe) and backend (server subprocess) capabilities.
A minimal example showing how to build a plugin for Claude Code UI, covering both frontend (ES module) and backend (server subprocess) capabilities.
## How plugins work
A plugin's UI runs client-side inside a sandboxed iframe. Plugins can optionally declare a `server` entry in their manifest — a Node.js script that the host runs as a subprocess. The iframe communicates with its server through a postMessage RPC bridge (the host proxies the calls because the sandboxed iframe has no auth token).
A plugin's UI is a plain ES module loaded directly into the host app — no iframe. Plugins can optionally declare a `server` entry in their manifest — a Node.js script that the host runs as a subprocess. The module calls the server through an `api.rpc()` helper (the host proxies the calls using its own auth token).
```
┌─────────────────────────────────────────────────────────┐
@@ -30,10 +30,11 @@ A plugin's UI runs client-side inside a sandboxed iframe. Plugins can optionally
┌───────────▼─────────────────────────▼───────────────────┐
│ Frontend (browser) │
│ │
│ Plugin iframe ◄──postMessage──► Host app
(sandboxed) ccui:context
ccui:request-context
ccui:rpc / ccui:rpc-response
│ Plugin module (index.js)
import(url) → mount(container, api)
api.context — theme / project / session
api.onContextChange — subscribe to changes
│ api.rpc(method, path, body) → Promise │
└─────────────────────────────────────────────────────────┘
```
@@ -42,12 +43,12 @@ A plugin's UI runs client-side inside a sandboxed iframe. Plugins can optionally
```
my-plugin/
manifest.json # Required — plugin metadata
index.html # Frontend entry point (rendered in iframe)
index.js # Frontend entry point (ES module, mount/unmount exports)
server.js # Optional — backend entry point (runs as subprocess)
package.json # Optional — npm dependencies for server.js
```
All files in the plugin directory are accessible via `/api/plugins/:name/assets/`. Use relative paths in your HTML (e.g., `<link href="styles.css">`).
All files in the plugin directory are accessible via `/api/plugins/:name/assets/`.
## manifest.json
@@ -59,11 +60,11 @@ All files in the plugin directory are accessible via `/api/plugins/:name/assets/
"description": "Short description shown in settings.",
"author": "Your Name",
"icon": "Puzzle", // Lucide icon name (see available icons below)
"type": "iframe", // Rendering method — only "iframe" is supported today
"type": "module", // "module" (default) or "iframe" (legacy)
"slot": "tab", // Where the plugin appears — only "tab" is supported today
"entry": "index.html", // Frontend entry file, relative to plugin directory
"entry": "index.js", // Frontend entry file, relative to plugin directory
"server": "server.js", // Optional — backend entry file, runs as Node.js subprocess
"permissions": [] // Reserved for future use
"permissions": [] // Reserved for future use
}
```
@@ -79,6 +80,56 @@ All files in the plugin directory are accessible via `/api/plugins/:name/assets/
---
## Frontend — Module API
The host dynamically imports your entry file and calls `mount(container, api)`. When the plugin tab is closed or the plugin is disabled, `unmount(container)` is called.
```js
// index.js
export function mount(container, api) {
// api.context — current snapshot: { theme, project, session }
// api.onContextChange(cb) — subscribe, returns an unsubscribe function
// api.rpc(method, path, body?) — call the plugin's server subprocess
container.innerHTML = '<p>Hello!</p>';
const unsub = api.onContextChange((ctx) => {
container.style.background = ctx.theme === 'dark' ? '#111' : '#fff';
});
container._cleanup = unsub;
}
export function unmount(container) {
if (typeof container._cleanup === 'function') container._cleanup();
container.innerHTML = '';
}
```
### Context object
```js
api.context // always up to date
// {
// theme: "dark" | "light",
// project: { name: string, path: string } | null,
// session: { id: string, title: string } | null,
// }
```
### RPC helper
```js
// Calls /api/plugins/:name/rpc/hello via the host's authenticated fetch
const data = await api.rpc('GET', '/hello');
// With a JSON body
const result = await api.rpc('POST', '/echo', { greeting: 'hi' });
```
---
## Backend — Server subprocess
Plugins that need to make authenticated API calls, use npm packages, or run Node.js logic can declare a `"server"` entry in their manifest. The host manages the full lifecycle:
@@ -140,70 +191,15 @@ server.listen(0, '127.0.0.1', () => {
---
## Frontend — Context API
The host app sends a `ccui:context` message to the iframe whenever the theme, project, or session changes:
```js
window.addEventListener('message', (event) => {
if (event.data?.type !== 'ccui:context') return;
const { theme, project, session } = event.data;
// theme — "dark" | "light"
// project — { name, path } | null
// session — { id, title } | null
});
```
To request the current context on load:
```js
window.parent.postMessage({ type: 'ccui:request-context' }, '*');
```
## Frontend — RPC bridge
The sandboxed iframe cannot make authenticated API calls directly. Instead, it sends RPC requests via `postMessage`, and the host proxies them to the plugin's server subprocess:
```js
function callBackend(method, path, body) {
return new Promise((resolve) => {
const requestId = Math.random().toString(36).slice(2);
function handler(event) {
if (event.data?.type === 'ccui:rpc-response' && event.data.requestId === requestId) {
window.removeEventListener('message', handler);
resolve(event.data);
}
}
window.addEventListener('message', handler);
window.parent.postMessage({
type: 'ccui:rpc',
requestId,
method,
path,
body: body || undefined,
}, '*');
});
}
// Usage
const result = await callBackend('GET', '/hello');
// result = { type: 'ccui:rpc-response', requestId, status: 200, data: { message: 'Hello!' } }
```
The host receives `ccui:rpc`, makes an authenticated fetch to `/api/plugins/:name/rpc/*`, and returns the response as `ccui:rpc-response`.
## Frontend — Mobile
On desktop, each enabled plugin gets its own tab in the tab bar. On mobile, plugins are grouped under a single "More" button in the bottom navigation to save space.
## Security
### Frontend sandbox
### Frontend isolation
Plugins run in a sandboxed iframe with `allow-scripts allow-forms allow-popups`. They **cannot** access the host app's cookies, localStorage, or auth tokens (`allow-same-origin` is intentionally omitted).
Plugin modules run in the same JS context as the host app but have no access to auth tokens or internal state — only the `api` object passed to `mount`. They cannot make authenticated API calls directly; all server communication goes through `api.rpc()`, which the host proxies.
### Server subprocess isolation