Files
claudecodeui/examples/plugins/hello-world/index.html
simosmik 2588851746 feat(plugins): add backend subprocess support via RPC bridge
Extend the plugin system so plugins can optionally declare a
entry in their manifest. The host spawns a Node.js subprocess for that
script, assigns it a random local port, and exposes an RPC proxy route
() that the sandboxed iframe can call via
postMessage — avoiding the need for the iframe to hold auth tokens.

Changes:
- Add plugin process manager to spawn/stop server subprocesses
- Wire subprocess lifecycle into enable/disable and uninstall routes
- Add RPC proxy route on the host server
- Extend PluginsContext and PluginTabContent to handle ccui:rpc and
  ccui:rpc-response postMessage events
- Add hello-world server.js as a reference subprocess implementation
- Update manifest.json and README with server field documentation
2026-03-05 11:58:04 +00:00

115 lines
4.3 KiB
HTML

<!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; }
h2 { font-size: 1.1rem; margin-top: 24px; margin-bottom: 8px; opacity: 0.8; }
.context { font-size: 0.85rem; opacity: 0.7; margin-top: 16px; }
.context dt { font-weight: 600; margin-top: 8px; }
.context dd { margin-left: 12px; }
button {
margin-top: 12px;
padding: 8px 16px;
border: 1px solid currentColor;
border-radius: 6px;
background: transparent;
color: inherit;
cursor: pointer;
font-size: 0.85rem;
opacity: 0.8;
}
button:hover { opacity: 1; }
pre {
margin-top: 8px;
padding: 12px;
border-radius: 6px;
font-size: 0.8rem;
overflow-x: auto;
white-space: pre-wrap;
}
body.dark pre { background: #2a2a3e; }
body.light pre { background: #f0f0f4; }
</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>
<h2>Server RPC</h2>
<p style="font-size: 0.85rem; opacity: 0.7;">This calls the plugin's own Node.js server subprocess via the postMessage RPC bridge.</p>
<button id="btn-hello">Call GET /hello</button>
<button id="btn-echo">Call POST /echo</button>
<pre id="rpc-result">Click a button to make an RPC call...</pre>
<script>
// ── RPC helper ──────────────────────────────────────────────────
// Sends a request through the host's postMessage bridge, which
// proxies it to this plugin's server subprocess.
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,
}, '*');
});
}
// ── Context listener ────────────────────────────────────────────
window.addEventListener('message', (event) => {
if (!event.data || event.data.type !== 'ccui:context') return;
const { theme, project, session } = event.data;
document.body.className = theme || 'light';
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)';
});
// Request context on load
window.parent.postMessage({ type: 'ccui:request-context' }, '*');
// ── RPC demo buttons ────────────────────────────────────────────
document.getElementById('btn-hello').addEventListener('click', async () => {
const result = await callBackend('GET', '/hello');
document.getElementById('rpc-result').textContent = JSON.stringify(result, null, 2);
});
document.getElementById('btn-echo').addEventListener('click', async () => {
const result = await callBackend('POST', '/echo', { greeting: 'Hello from the iframe!' });
document.getElementById('rpc-result').textContent = JSON.stringify(result, null, 2);
});
</script>
</body>
</html>