From c9465a64be4a18d9ee1b71605b41d400582c3df7 Mon Sep 17 00:00:00 2001 From: simosmik Date: Thu, 5 Mar 2026 12:18:38 +0000 Subject: [PATCH] fix: hello world plugin --- examples/plugins/hello-world/index.js | 133 +++++++++++++++++++++ examples/plugins/hello-world/manifest.json | 4 +- src/contexts/PluginsContext.tsx | 2 +- 3 files changed, 136 insertions(+), 3 deletions(-) create mode 100644 examples/plugins/hello-world/index.js diff --git a/examples/plugins/hello-world/index.js b/examples/plugins/hello-world/index.js new file mode 100644 index 00000000..927c5dbd --- /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 c2553fcb..6438cda0 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 e433e38b..4da04fd9 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;