diff --git a/examples/plugins/hello-world/index.js b/examples/plugins/hello-world/index.js
new file mode 100644
index 0000000..927c5db
--- /dev/null
+++ b/examples/plugins/hello-world/index.js
@@ -0,0 +1,133 @@
+/**
+ * Hello World plugin — module entry point.
+ *
+ * The host calls mount(container, api) when the plugin tab is activated and
+ * unmount(container) when it is torn down.
+ *
+ * api shape:
+ * api.context — current PluginContext snapshot
+ * api.onContextChange(cb) → unsubscribe — called whenever theme/project/session changes
+ * api.rpc(method, path, body?) → Promise — proxied to this plugin's server subprocess
+ */
+
+export function mount(container, api) {
+ // ── Build DOM ──────────────────────────────────────────────────────────────
+ container.innerHTML = `
+
+
Hello from a plugin!
+
+ This tab is rendered by a plain ES module — no iframe.
+
+
+
Context
+
+ - Theme
+ - —
+ - Project
+ - —
+ - Session
+ - —
+
+
+
Server RPC
+
+ Calls this plugin's Node.js server subprocess via the RPC bridge.
+
+
+
+
+
+
Click a button to make an RPC call…
+
+ `;
+
+ // ── Theme helper ──────────────────────────────────────────────────────────
+ function applyTheme(theme) {
+ const root = container.querySelector('#hw-root');
+ if (!root) return;
+ const isDark = theme === 'dark';
+ root.style.background = isDark ? '#1a1a2e' : '#ffffff';
+ root.style.color = isDark ? '#e0e0e0' : '#1a1a2e';
+ const pre = container.querySelector('#hw-result');
+ if (pre) pre.style.background = isDark ? '#2a2a3e' : '#f0f0f4';
+ }
+
+ // ── Render context values ─────────────────────────────────────────────────
+ function renderContext(ctx) {
+ const t = container.querySelector('#hw-theme');
+ const p = container.querySelector('#hw-project');
+ const s = container.querySelector('#hw-session');
+ if (t) t.textContent = ctx.theme || '—';
+ if (p) p.textContent = ctx.project ? ctx.project.name : '(none)';
+ if (s) s.textContent = ctx.session ? (ctx.session.title || ctx.session.id) : '(none)';
+ applyTheme(ctx.theme);
+ }
+
+ // Apply initial context
+ renderContext(api.context);
+
+ // Subscribe to future changes
+ const unsubscribe = api.onContextChange(renderContext);
+
+ // ── Button styles ─────────────────────────────────────────────────────────
+ container.querySelectorAll('button').forEach((btn) => {
+ Object.assign(btn.style, {
+ padding: '8px 16px',
+ border: '1px solid currentColor',
+ borderRadius: '6px',
+ background: 'transparent',
+ color: 'inherit',
+ cursor: 'pointer',
+ fontSize: '0.85rem',
+ opacity: '0.8',
+ });
+ btn.addEventListener('mouseenter', () => { btn.style.opacity = '1'; });
+ btn.addEventListener('mouseleave', () => { btn.style.opacity = '0.8'; });
+ });
+
+ // ── RPC buttons ───────────────────────────────────────────────────────────
+ const resultEl = container.querySelector('#hw-result');
+
+ container.querySelector('#hw-btn-hello').addEventListener('click', async () => {
+ resultEl.textContent = 'Loading…';
+ try {
+ const data = await api.rpc('GET', '/hello');
+ resultEl.textContent = JSON.stringify(data, null, 2);
+ } catch (err) {
+ resultEl.textContent = `Error: ${err.message}`;
+ }
+ });
+
+ container.querySelector('#hw-btn-echo').addEventListener('click', async () => {
+ resultEl.textContent = 'Loading…';
+ try {
+ const data = await api.rpc('POST', '/echo', { greeting: 'Hello from the plugin module!' });
+ resultEl.textContent = JSON.stringify(data, null, 2);
+ } catch (err) {
+ resultEl.textContent = `Error: ${err.message}`;
+ }
+ });
+
+ // Store unsubscribe so unmount can clean up
+ container._hwUnsubscribe = unsubscribe;
+}
+
+export function unmount(container) {
+ if (typeof container._hwUnsubscribe === 'function') {
+ container._hwUnsubscribe();
+ delete container._hwUnsubscribe;
+ }
+ container.innerHTML = '';
+}
diff --git a/examples/plugins/hello-world/manifest.json b/examples/plugins/hello-world/manifest.json
index c2553fc..6438cda 100644
--- a/examples/plugins/hello-world/manifest.json
+++ b/examples/plugins/hello-world/manifest.json
@@ -5,9 +5,9 @@
"description": "A minimal example plugin that demonstrates the plugin API.",
"author": "Claude Code UI",
"icon": "Puzzle",
- "type": "iframe",
+ "type": "module",
"slot": "tab",
- "entry": "index.html",
+ "entry": "index.js",
"server": "server.js",
"permissions": []
}
diff --git a/src/contexts/PluginsContext.tsx b/src/contexts/PluginsContext.tsx
index e433e38..4da04fd 100644
--- a/src/contexts/PluginsContext.tsx
+++ b/src/contexts/PluginsContext.tsx
@@ -9,7 +9,7 @@ export type Plugin = {
description: string;
author: string;
icon: string;
- type: 'iframe' | 'react';
+ type: 'iframe' | 'react' | 'module';
slot: 'tab';
entry: string;
server: string | null;