Files
claudecodeui/src/components/plugins/PluginTabContent.tsx
2026-03-05 11:27:45 +00:00

106 lines
3.3 KiB
TypeScript

import { useEffect, useRef } from 'react';
import { useTheme } from '../../contexts/ThemeContext';
import type { Project, ProjectSession } from '../../types/app';
type PluginTabContentProps = {
pluginName: string;
selectedProject: Project | null;
selectedSession: ProjectSession | null;
};
export default function PluginTabContent({
pluginName,
selectedProject,
selectedSession,
}: PluginTabContentProps) {
const iframeRef = useRef<HTMLIFrameElement>(null);
const { isDarkMode } = useTheme();
const iframeSrc = `/api/plugins/${encodeURIComponent(pluginName)}/assets/index.html`;
// Send context to iframe when it loads or when context changes.
// Use '*' as targetOrigin because the sandbox (without allow-same-origin) gives the
// iframe an opaque origin that cannot be matched with a specific origin string.
useEffect(() => {
const iframe = iframeRef.current;
if (!iframe) return;
const sendContext = () => {
iframe.contentWindow?.postMessage(
{
type: 'ccui:context',
theme: isDarkMode ? 'dark' : 'light',
project: selectedProject
? { name: selectedProject.name, path: selectedProject.fullPath || selectedProject.path }
: null,
session: selectedSession
? { id: selectedSession.id, title: selectedSession.title }
: null,
},
'*',
);
};
iframe.addEventListener('load', sendContext);
// Also send when context changes (iframe already loaded)
if (iframe.contentWindow) {
sendContext();
}
return () => {
iframe.removeEventListener('load', sendContext);
};
}, [isDarkMode, selectedProject, selectedSession]);
// Listen for messages from plugin iframe.
// We verify by event.source rather than event.origin because the sandboxed iframe
// (without allow-same-origin) has an opaque origin that shows up as "null".
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
// Only accept messages originating from our plugin iframe
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': {
// Plugin is requesting current context
iframeRef.current?.contentWindow?.postMessage(
{
type: 'ccui:context',
theme: isDarkMode ? 'dark' : 'light',
project: selectedProject
? { name: selectedProject.name, path: selectedProject.fullPath || selectedProject.path }
: null,
session: selectedSession
? { id: selectedSession.id, title: selectedSession.title }
: null,
},
'*',
);
break;
}
default:
break;
}
};
window.addEventListener('message', handleMessage);
return () => window.removeEventListener('message', handleMessage);
}, [isDarkMode, selectedProject, selectedSession]);
return (
<div className="h-full w-full overflow-hidden">
<iframe
ref={iframeRef}
src={iframeSrc}
title={`Plugin: ${pluginName}`}
className="w-full h-full border-0"
sandbox="allow-scripts allow-forms allow-popups"
/>
</div>
);
}