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

@@ -38,7 +38,6 @@ export default function PluginTabContent({
selectedSession,
}: PluginTabContentProps) {
const containerRef = useRef<HTMLDivElement>(null);
const iframeRef = useRef<HTMLIFrameElement>(null);
const { isDarkMode } = useTheme();
const { plugins } = usePlugins();
@@ -49,8 +48,6 @@ export default function PluginTabContent({
const moduleRef = useRef<any>(null);
const plugin = plugins.find(p => p.name === pluginName);
// 'iframe' is the explicit legacy type; everything else (including 'module' and unset) uses module loading
const isIframe = plugin?.type === 'iframe';
// Keep contextRef current and notify the mounted plugin on every context change
useEffect(() => {
@@ -60,16 +57,10 @@ export default function PluginTabContent({
for (const cb of contextCallbacksRef.current) {
try { cb(ctx); } catch { /* plugin error — ignore */ }
}
}, [isDarkMode, selectedProject, selectedSession]);
// Also push to legacy iframe plugin
if (isIframe && iframeRef.current?.contentWindow) {
iframeRef.current.contentWindow.postMessage({ type: 'ccui:context', ...ctx }, '*');
}
}, [isDarkMode, selectedProject, selectedSession, isIframe]);
// ── Module plugin (default) ──────────────────────────────────────
useEffect(() => {
if (isIframe || !containerRef.current) return;
if (!containerRef.current) return;
let active = true;
const container = containerRef.current;
@@ -121,68 +112,7 @@ export default function PluginTabContent({
contextCallbacksRef.current.clear();
moduleRef.current = null;
};
}, [pluginName, isIframe, plugin?.entry]); // re-mount only when the plugin itself changes
// ── Legacy iframe plugin ─────────────────────────────────────────
useEffect(() => {
if (!isIframe) return;
const handleMessage = (event: MessageEvent) => {
if (event.source !== iframeRef.current?.contentWindow) return;
if (!event.data || typeof event.data !== 'object') return;
const { type } = event.data;
switch (type) {
case 'ccui:request-context':
iframeRef.current?.contentWindow?.postMessage(
{ type: 'ccui:context', ...contextRef.current },
'*',
);
break;
case 'ccui:rpc': {
const { requestId, method, path: rpcPath, body } = event.data;
if (!requestId || !rpcPath) break;
const cleanPath = String(rpcPath).replace(/^\//, '');
authenticatedFetch(`/api/plugins/${encodeURIComponent(pluginName)}/rpc/${cleanPath}`, {
method: method || 'GET',
...(body ? { body: JSON.stringify(body) } : {}),
})
.then(async (res) => {
const data = await res.json().catch(() => null);
iframeRef.current?.contentWindow?.postMessage(
{ type: 'ccui:rpc-response', requestId, status: res.status, data }, '*',
);
})
.catch((err) => {
iframeRef.current?.contentWindow?.postMessage(
{ type: 'ccui:rpc-response', requestId, status: 500, error: (err as Error).message }, '*',
);
});
break;
}
}
};
window.addEventListener('message', handleMessage);
return () => window.removeEventListener('message', handleMessage);
}, [isIframe, pluginName]);
if (isIframe) {
const src = `/api/plugins/${encodeURIComponent(pluginName)}/assets/${plugin?.entry ?? 'index.html'}`;
return (
<div className="h-full w-full overflow-hidden">
<iframe
ref={iframeRef}
src={src}
title={`Plugin: ${pluginName}`}
className="w-full h-full border-0"
sandbox="allow-scripts allow-forms allow-popups"
/>
</div>
);
}
}, [pluginName, plugin?.entry]); // re-mount only when the plugin itself changes
return <div ref={containerRef} className="h-full w-full overflow-auto" />;
}