Feat: initial plugin system (Frontend only)

This commit is contained in:
simosmik
2026-03-05 11:27:45 +00:00
parent f4615dfca3
commit 6517ec4ecd
19 changed files with 1262 additions and 53 deletions

View File

@@ -0,0 +1,127 @@
# Hello World Plugin
A minimal example showing how to build a plugin for Claude Code UI.
## How plugins work
A plugin's UI runs client-side inside a sandboxed iframe. The backend handles plugin lifecycle (install, update, uninstall) and serves the plugin's files as static assets.
```
┌─────────────────────────────────────────────────┐
│ Backend (server) │
│ │
│ Lifecycle (spawns child processes): │
│ git clone / git pull Install & update │
│ npm install Dependency setup │
│ │
│ Runtime: │
│ GET /api/plugins List plugins │
│ GET /api/plugins/:name/assets/* Serve files │
│ PUT /api/plugins/:name/enable Toggle on/off │
│ DELETE /api/plugins/:name Uninstall │
└──────────────────────┬──────────────────────────┘
│ serves static files
┌──────────────────────▼──────────────────────────┐
│ Frontend (browser) │
│ │
│ Plugin iframe ◄──postMessage──► Host app │
│ (sandboxed) ccui:context │
│ ccui:request-context │
└─────────────────────────────────────────────────┘
```
## Plugin structure
A plugin is a directory with at minimum two files:
```
my-plugin/
manifest.json # Required — plugin metadata
index.html # Entry point (referenced by manifest.entry)
styles.css # Optional — any static assets alongside entry
app.js # Optional — JS loaded by your HTML
```
All files in the plugin directory are accessible via `/api/plugins/:name/assets/`. Use relative paths in your HTML to reference them (e.g., `<link href="styles.css">`, `<script src="app.js">`).
## manifest.json
```jsonc
{
"name": "hello-world", // Unique id — alphanumeric, hyphens, underscores only
"displayName": "Hello World", // Shown in the UI
"version": "1.0.0",
"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
"slot": "tab", // Where the plugin appears — only "tab" is supported today
"entry": "index.html", // Path to the entry file, relative to the plugin directory
"permissions": [] // Reserved for future use
}
```
### Available icons
`Puzzle` (default), `Box`, `Database`, `Globe`, `Terminal`, `Wrench`, `Zap`, `BarChart3`, `Folder`, `MessageSquare`, `GitBranch`
## Installation
**Manual:** Copy your plugin folder into `~/.claude-code-ui/plugins/`.
**From git:** In Settings > Plugins, paste a git URL and click Install. The repo is cloned into the plugins directory.
## Backend details
The backend manages the full plugin lifecycle. During **install and update**, it spawns child processes on the server:
- **Install** — runs `git clone --depth 1` to clone the repo, validates `manifest.json`, then runs `npm install --production --ignore-scripts` if a `package.json` exists. Install scripts are blocked (`--ignore-scripts`) to prevent arbitrary code execution.
- **Update** — runs `git pull --ff-only` in the plugin directory, re-validates the manifest, re-runs `npm install --production --ignore-scripts` if needed.
- **Uninstall** — deletes the plugin directory recursively from disk.
At **runtime** (after install), the backend:
- **Discovery** — scans `~/.claude-code-ui/plugins/` for directories containing a valid `manifest.json`
- **Asset serving** — serves any file inside a plugin directory at `/api/plugins/:name/assets/*` with correct MIME types. Path traversal outside the plugin directory is blocked.
- **Config** — per-plugin enabled/disabled state stored in `~/.claude-code-ui/plugins.json`
Plugins **cannot** register custom server routes, middleware, or execute their own backend code. The server only serves their files statically. If your plugin needs external data, fetch it from the iframe directly (subject to CORS).
## 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 — 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
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). Any static assets (CSS, JS, images) can be placed alongside the entry file and referenced with relative paths.
npm `postinstall` scripts are blocked during installation (`--ignore-scripts`), so plugins should ship pre-built assets.
## Try it
```bash
cp -r examples/plugins/hello-world ~/.claude-code-ui/plugins/
```
Then open Settings > Plugins — "Hello World" should appear. Enable it and its tab will show up.

View File

@@ -0,0 +1,52 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Hello World Plugin</title>
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
padding: 24px;
transition: background-color 0.2s, color 0.2s;
}
body.dark { background: #1a1a2e; color: #e0e0e0; }
body.light { background: #ffffff; color: #1a1a2e; }
h1 { font-size: 1.4rem; margin-bottom: 8px; }
.context { font-size: 0.85rem; opacity: 0.7; margin-top: 16px; }
.context dt { font-weight: 600; margin-top: 8px; }
.context dd { margin-left: 12px; }
</style>
</head>
<body class="light">
<h1>Hello from a plugin!</h1>
<p>This tab is rendered inside a sandboxed iframe.</p>
<dl class="context" id="ctx">
<dt>Theme</dt><dd id="ctx-theme"></dd>
<dt>Project</dt><dd id="ctx-project"></dd>
<dt>Session</dt><dd id="ctx-session"></dd>
</dl>
<script>
// Listen for context updates from the host app.
window.addEventListener('message', (event) => {
if (!event.data || event.data.type !== 'ccui:context') return;
const { theme, project, session } = event.data;
// Apply theme
document.body.className = theme || 'light';
// Display context
document.getElementById('ctx-theme').textContent = theme || '—';
document.getElementById('ctx-project').textContent = project ? project.name : '(none)';
document.getElementById('ctx-session').textContent = session ? session.title || session.id : '(none)';
});
// Ask the host for the current context on load.
window.parent.postMessage({ type: 'ccui:request-context' }, '*');
</script>
</body>
</html>

View File

@@ -0,0 +1,12 @@
{
"name": "hello-world",
"displayName": "Hello World",
"version": "1.0.0",
"description": "A minimal example plugin that demonstrates the plugin API.",
"author": "Claude Code UI",
"icon": "Puzzle",
"type": "iframe",
"slot": "tab",
"entry": "index.html",
"permissions": []
}