import { useEffect, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { backendClient } from '@/app/infra/http'; import { Button } from '@/components/ui/button'; import { toast } from 'sonner'; import { Skeleton } from '@/components/ui/skeleton'; import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter, } from '@/components/ui/dialog'; import { Checkbox } from '@/components/ui/checkbox'; import { CircleHelp, Plus, X, Server, Wrench, Sparkles } from 'lucide-react'; import { Badge } from '@/components/ui/badge'; import { Switch } from '@/components/ui/switch'; import { Label } from '@/components/ui/label'; import { Tooltip, TooltipContent, TooltipTrigger, } from '@/components/ui/tooltip'; import { Plugin } from '@/app/infra/entities/plugin'; import { MCPServer, Skill } from '@/app/infra/entities/api'; import PluginComponentList from '@/app/home/plugins/components/plugin-installed/PluginComponentList'; import { BoxUnavailableNotice } from '@/app/home/components/BoxUnavailableNotice'; import { useBoxStatus } from '@/app/infra/hooks/useBoxStatus'; function InfoTooltip({ label }: { label: string }) { return ( {label} ); } export default function PipelineExtension({ pipelineId, }: { pipelineId: string; }) { const { t } = useTranslation(); const { available: boxAvailable, hint: boxHint, reason: boxReason, } = useBoxStatus(); const [loading, setLoading] = useState(true); const [enableAllPlugins, setEnableAllPlugins] = useState(true); const [enableAllMCPServers, setEnableAllMCPServers] = useState(true); const [enableAllSkills, setEnableAllSkills] = useState(true); const [selectedPlugins, setSelectedPlugins] = useState([]); const [allPlugins, setAllPlugins] = useState([]); const [selectedMCPServers, setSelectedMCPServers] = useState([]); const [allMCPServers, setAllMCPServers] = useState([]); const [selectedSkills, setSelectedSkills] = useState([]); const [allSkills, setAllSkills] = useState([]); const [pluginDialogOpen, setPluginDialogOpen] = useState(false); const [mcpDialogOpen, setMcpDialogOpen] = useState(false); const [skillDialogOpen, setSkillDialogOpen] = useState(false); const [tempSelectedPluginIds, setTempSelectedPluginIds] = useState( [], ); const [tempSelectedMCPIds, setTempSelectedMCPIds] = useState([]); const [tempSelectedSkillIds, setTempSelectedSkillIds] = useState( [], ); useEffect(() => { loadExtensions(); }, [pipelineId]); const getPluginId = (plugin: Plugin): string => { const author = plugin.manifest.manifest.metadata.author; const name = plugin.manifest.manifest.metadata.name; return `${author}/${name}`; }; const loadExtensions = async () => { try { setLoading(true); const data = await backendClient.getPipelineExtensions(pipelineId); setEnableAllPlugins(data.enable_all_plugins ?? true); setEnableAllMCPServers(data.enable_all_mcp_servers ?? true); setEnableAllSkills(data.enable_all_skills ?? true); const boundPluginIds = new Set( data.bound_plugins.map((p) => `${p.author}/${p.name}`), ); const selected = data.available_plugins.filter((plugin) => boundPluginIds.has(getPluginId(plugin)), ); setSelectedPlugins(selected); setAllPlugins(data.available_plugins); // Load MCP servers const boundMCPServerIds = new Set(data.bound_mcp_servers || []); const selectedMCP = data.available_mcp_servers.filter((server) => boundMCPServerIds.has(server.uuid || ''), ); setSelectedMCPServers(selectedMCP); setAllMCPServers(data.available_mcp_servers); // Load Skills const boundSkillNames = new Set(data.bound_skills || []); const selectedSkill = (data.available_skills || []).filter((skill) => boundSkillNames.has(skill.name), ); setSelectedSkills(selectedSkill); setAllSkills(data.available_skills || []); } catch (error) { console.error('Failed to load extensions:', error); toast.error(t('pipelines.extensions.loadError')); } finally { setLoading(false); } }; const saveToBackend = async ( plugins: Plugin[], mcpServers: MCPServer[], skills: Skill[], newEnableAllPlugins?: boolean, newEnableAllMCPServers?: boolean, newEnableAllSkills?: boolean, ) => { try { const boundPluginsArray = plugins.map((plugin) => { const metadata = plugin.manifest.manifest.metadata; return { author: metadata.author || '', name: metadata.name, }; }); const boundMCPServerIds = mcpServers.map((server) => server.uuid || ''); const boundSkillIds = skills.map((skill) => skill.name); await backendClient.updatePipelineExtensions( pipelineId, boundPluginsArray, boundMCPServerIds, newEnableAllPlugins ?? enableAllPlugins, newEnableAllMCPServers ?? enableAllMCPServers, boundSkillIds, newEnableAllSkills ?? enableAllSkills, ); toast.success(t('pipelines.extensions.saveSuccess')); } catch (error) { console.error('Failed to save extensions:', error); toast.error(t('pipelines.extensions.saveError')); // Reload on error to restore correct state loadExtensions(); } }; const handleRemovePlugin = async (pluginId: string) => { const newPlugins = selectedPlugins.filter( (p) => getPluginId(p) !== pluginId, ); setSelectedPlugins(newPlugins); await saveToBackend(newPlugins, selectedMCPServers, selectedSkills); }; const handleRemoveMCPServer = async (serverUuid: string) => { const newServers = selectedMCPServers.filter((s) => s.uuid !== serverUuid); setSelectedMCPServers(newServers); await saveToBackend(selectedPlugins, newServers, selectedSkills); }; const handleRemoveSkill = async (skillName: string) => { const newSkills = selectedSkills.filter((s) => s.name !== skillName); setSelectedSkills(newSkills); await saveToBackend(selectedPlugins, selectedMCPServers, newSkills); }; const handleOpenPluginDialog = () => { setTempSelectedPluginIds(selectedPlugins.map((p) => getPluginId(p))); setPluginDialogOpen(true); }; const handleOpenMCPDialog = () => { setTempSelectedMCPIds(selectedMCPServers.map((s) => s.uuid || '')); setMcpDialogOpen(true); }; const handleOpenSkillDialog = () => { setTempSelectedSkillIds(selectedSkills.map((s) => s.name)); setSkillDialogOpen(true); }; const handleTogglePlugin = (pluginId: string) => { setTempSelectedPluginIds((prev) => prev.includes(pluginId) ? prev.filter((id) => id !== pluginId) : [...prev, pluginId], ); }; const handleToggleMCPServer = (serverUuid: string) => { setTempSelectedMCPIds((prev) => prev.includes(serverUuid) ? prev.filter((id) => id !== serverUuid) : [...prev, serverUuid], ); }; const handleToggleSkill = (skillName: string) => { setTempSelectedSkillIds((prev) => prev.includes(skillName) ? prev.filter((id) => id !== skillName) : [...prev, skillName], ); }; const handleToggleAllPlugins = () => { if (tempSelectedPluginIds.length === allPlugins.length) { setTempSelectedPluginIds([]); } else { setTempSelectedPluginIds(allPlugins.map((p) => getPluginId(p))); } }; const handleToggleAllMCPServers = () => { if (tempSelectedMCPIds.length === allMCPServers.length) { setTempSelectedMCPIds([]); } else { setTempSelectedMCPIds(allMCPServers.map((s) => s.uuid || '')); } }; const handleToggleAllSkills = () => { if (tempSelectedSkillIds.length === allSkills.length) { setTempSelectedSkillIds([]); } else { setTempSelectedSkillIds(allSkills.map((s) => s.name)); } }; const handleConfirmPluginSelection = async () => { const newSelected = allPlugins.filter((p) => tempSelectedPluginIds.includes(getPluginId(p)), ); setSelectedPlugins(newSelected); setPluginDialogOpen(false); await saveToBackend(newSelected, selectedMCPServers, selectedSkills); }; const handleConfirmMCPSelection = async () => { const newSelected = allMCPServers.filter((s) => tempSelectedMCPIds.includes(s.uuid || ''), ); setSelectedMCPServers(newSelected); setMcpDialogOpen(false); await saveToBackend(selectedPlugins, newSelected, selectedSkills); }; const handleConfirmSkillSelection = async () => { const newSelected = allSkills.filter((s) => tempSelectedSkillIds.includes(s.name), ); setSelectedSkills(newSelected); setSkillDialogOpen(false); await saveToBackend(selectedPlugins, selectedMCPServers, newSelected); }; const handleToggleEnableAllPlugins = async (checked: boolean) => { setEnableAllPlugins(checked); await saveToBackend( selectedPlugins, selectedMCPServers, selectedSkills, checked, undefined, undefined, ); }; const handleToggleEnableAllMCPServers = async (checked: boolean) => { setEnableAllMCPServers(checked); await saveToBackend( selectedPlugins, selectedMCPServers, selectedSkills, undefined, checked, undefined, ); }; const handleToggleEnableAllSkills = async (checked: boolean) => { setEnableAllSkills(checked); await saveToBackend( selectedPlugins, selectedMCPServers, selectedSkills, undefined, undefined, checked, ); }; if (loading) { return (
); } return (
{/* Plugins Section */}

{t('pipelines.extensions.pluginsTitle')}

{enableAllPlugins ? (

{t('pipelines.extensions.allPluginsEnabled')}

) : selectedPlugins.length === 0 ? (

{t('pipelines.extensions.noPluginsSelected')}

) : (
{selectedPlugins.map((plugin) => { const pluginId = getPluginId(plugin); const metadata = plugin.manifest.manifest.metadata; return (
{metadata.name}
{metadata.name}
{metadata.author} • v{metadata.version}
{ const componentKindCount: Record = {}; for (const component of plugin.components) { const kind = component.manifest.manifest.kind; if (componentKindCount[kind]) { componentKindCount[kind]++; } else { componentKindCount[kind] = 1; } } return componentKindCount; })()} showComponentName={true} showTitle={false} useBadge={true} t={t} />
{!plugin.enabled && ( {t('pipelines.extensions.disabled')} )}
); })}
)}
{/* MCP Servers Section */}

{t('pipelines.extensions.mcpServersTitle')}

{enableAllMCPServers ? (

{t('pipelines.extensions.allMCPServersEnabled')}

) : selectedMCPServers.length === 0 ? (

{t('pipelines.extensions.noMCPServersSelected')}

) : (
{selectedMCPServers.map((server) => (
{server.name}
{server.mode}
{server.runtime_info && server.runtime_info.status === 'connected' && ( {t('pipelines.extensions.toolCount', { count: server.runtime_info.tool_count || 0, })} )}
{!server.enable && ( {t('pipelines.extensions.disabled')} )}
))}
)}
{/* Skills Section */}

{t('pipelines.extensions.skillsTitle')}

{!boxAvailable && ( )}
{enableAllSkills ? (

{t('pipelines.extensions.allSkillsEnabled')}

) : selectedSkills.length === 0 ? (

{t('pipelines.extensions.noSkillsSelected')}

) : (
{selectedSkills.map((skill) => (
{skill.display_name || skill.name}
{skill.description}
))}
)}
{/* Plugin Selection Dialog */} {t('pipelines.extensions.selectPlugins')} {allPlugins.length > 0 && (
0 } onCheckedChange={handleToggleAllPlugins} /> {t('pipelines.extensions.selectAll')}
)}
{allPlugins.length === 0 ? (

{t('pipelines.extensions.noPluginsInstalled')}

) : ( allPlugins.map((plugin) => { const pluginId = getPluginId(plugin); const metadata = plugin.manifest.manifest.metadata; const isSelected = tempSelectedPluginIds.includes(pluginId); return (
handleTogglePlugin(pluginId)} > {metadata.name}
{metadata.name}
{metadata.author} • v{metadata.version}
{ const componentKindCount: Record = {}; for (const component of plugin.components) { const kind = component.manifest.manifest.kind; if (componentKindCount[kind]) { componentKindCount[kind]++; } else { componentKindCount[kind] = 1; } } return componentKindCount; })()} showComponentName={true} showTitle={false} useBadge={true} t={t} />
{!plugin.enabled && ( {t('pipelines.extensions.disabled')} )}
); }) )}
{/* MCP Server Selection Dialog */} {t('pipelines.extensions.selectMCPServers')} {allMCPServers.length > 0 && (
0 } onCheckedChange={handleToggleAllMCPServers} /> {t('pipelines.extensions.selectAll')}
)}
{allMCPServers.length === 0 ? (

{t('pipelines.extensions.noMCPServersConfigured')}

) : ( allMCPServers.map((server) => { const isSelected = tempSelectedMCPIds.includes( server.uuid || '', ); return (
handleToggleMCPServer(server.uuid || '')} >
{server.name}
{server.mode}
{server.runtime_info && server.runtime_info.status === 'connected' && ( {t('pipelines.extensions.toolCount', { count: server.runtime_info.tool_count || 0, })} )}
{!server.enable && ( {t('pipelines.extensions.disabled')} )}
); }) )}
{/* Skill Selection Dialog */} {t('pipelines.extensions.selectSkills')} {allSkills.length > 0 && (
0 } onCheckedChange={handleToggleAllSkills} /> {t('pipelines.extensions.selectAll')}
)}
{allSkills.length === 0 ? (

{t('pipelines.extensions.noSkillsAvailable')}

) : ( allSkills.map((skill) => { const isSelected = tempSelectedSkillIds.includes(skill.name); return (
handleToggleSkill(skill.name)} >
{skill.display_name || skill.name}
{skill.description}
); }) )}
); }