/** * PluginsPanel — search, filter, sort and browse the plugin catalog. */ import { useEffect, useMemo } from 'react'; import { Badge } from '@renderer/components/ui/badge'; import { Button } from '@renderer/components/ui/button'; import { Checkbox } from '@renderer/components/ui/checkbox'; import { Label } from '@renderer/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@renderer/components/ui/select'; import { useStore } from '@renderer/store'; import { getCliProviderExtensionCapability } from '@shared/utils/providerExtensionCapabilities'; import { inferCapabilities, normalizeCategory } from '@shared/utils/extensionNormalizers'; import { ArrowUpDown, Filter, Puzzle, Search } from 'lucide-react'; import { useShallow } from 'zustand/react/shallow'; import { SearchInput } from '../common/SearchInput'; import { CapabilityChips } from './CapabilityChips'; import { CategoryChips } from './CategoryChips'; import { PluginCard } from './PluginCard'; import { PluginDetailDialog } from './PluginDetailDialog'; import type { EnrichedPlugin, PluginCapability, PluginFilters, PluginSortField, } from '@shared/types/extensions'; interface PluginsPanelProps { projectPath: string | null; pluginFilters: PluginFilters; pluginSort: { field: PluginSortField; order: 'asc' | 'desc' }; selectedPluginId: string | null; updatePluginSearch: (search: string) => void; toggleCategory: (category: string) => void; toggleCapability: (capability: PluginCapability) => void; toggleInstalledOnly: () => void; setSelectedPluginId: (id: string | null) => void; clearFilters: () => void; hasActiveFilters: boolean; setPluginSort: (sort: { field: PluginSortField; order: 'asc' | 'desc' }) => void; } const SORT_OPTIONS: { value: string; label: string }[] = [ { value: 'popularity:desc', label: 'Popular' }, { value: 'name:asc', label: 'Name A-Z' }, { value: 'name:desc', label: 'Name Z-A' }, { value: 'category:asc', label: 'Category' }, ]; /** Pure function: filter + sort the catalog */ function selectFilteredPlugins( catalog: EnrichedPlugin[], filters: PluginFilters, sort: { field: PluginSortField; order: 'asc' | 'desc' } ): EnrichedPlugin[] { let result = catalog; // Search if (filters.search) { const q = filters.search.toLowerCase(); result = result.filter( (p) => p.name.toLowerCase().includes(q) || p.description.toLowerCase().includes(q) || p.pluginId.toLowerCase().includes(q) ); } // Categories if (filters.categories.length > 0) { result = result.filter((p) => filters.categories.includes(normalizeCategory(p.category))); } // Capabilities if (filters.capabilities.length > 0) { result = result.filter((p) => { const caps = inferCapabilities(p); return filters.capabilities.some((fc) => caps.includes(fc)); }); } // Installed only if (filters.installedOnly) { result = result.filter((p) => p.isInstalled); } // Sort const direction = sort.order === 'asc' ? 1 : -1; result = [...result].sort((a, b) => { switch (sort.field) { case 'popularity': return (a.installCount - b.installCount) * direction; case 'name': return a.name.localeCompare(b.name) * direction; case 'category': return a.category.localeCompare(b.category) * direction; default: return 0; } }); return result; } export const PluginsPanel = ({ projectPath, pluginFilters, pluginSort, selectedPluginId, updatePluginSearch, toggleCategory, toggleCapability, toggleInstalledOnly, setSelectedPluginId, clearFilters, hasActiveFilters, setPluginSort, }: PluginsPanelProps): React.JSX.Element => { const { catalog, loading, error, cliStatus } = useStore( useShallow((s) => ({ catalog: s.pluginCatalog, loading: s.pluginCatalogLoading, error: s.pluginCatalogError, cliStatus: s.cliStatus, })) ); const filtered = useMemo( () => selectFilteredPlugins(catalog, pluginFilters, pluginSort), [catalog, pluginFilters, pluginSort] ); const selectedPlugin = useMemo( () => selectedPluginId ? (catalog.find((p) => p.pluginId === selectedPluginId) ?? null) : null, [catalog, selectedPluginId] ); useEffect(() => { if (selectedPluginId && !loading && !selectedPlugin) { setSelectedPluginId(null); } }, [loading, selectedPlugin, selectedPluginId, setSelectedPluginId]); useEffect(() => { if (error && selectedPluginId) { setSelectedPluginId(null); } }, [error, selectedPluginId, setSelectedPluginId]); const sortValue = `${pluginSort.field}:${pluginSort.order}`; const activeFilterCount = pluginFilters.categories.length + pluginFilters.capabilities.length + (pluginFilters.installedOnly ? 1 : 0) + (pluginFilters.search ? 1 : 0); const totalCategoryCount = useMemo( () => new Set(catalog.map((plugin) => normalizeCategory(plugin.category))).size, [catalog] ); const totalCapabilityCount = useMemo(() => { const counts = new Set(); for (const plugin of catalog) { for (const capability of inferCapabilities(plugin)) { counts.add(capability); } } return counts.size; }, [catalog]); return (
{cliStatus?.flavor === 'agent_teams_orchestrator' && (() => { const codexProvider = cliStatus.providers.find( (provider) => provider.providerId === 'codex' ); if (!codexProvider) return null; const capability = getCliProviderExtensionCapability(codexProvider, 'plugins'); if (capability.status === 'supported') { return null; } return (
Plugins currently apply to Anthropic sessions in the multimodel runtime. {capability.reason ? ` ${capability.reason}` : ''}
); })()} {/* Search + Sort + Installed only row */}
{/* Filters */}

Browse by fit

{activeFilterCount} active

Narrow the catalog by category, capability, or installed state.

{catalog.length} plugins {totalCategoryCount} categories {totalCapabilityCount} capabilities
{hasActiveFilters && ( )}
Categories {pluginFilters.categories.length} selected
Capabilities {pluginFilters.capabilities.length} selected
{/* Result count */} {!loading && !error && filtered.length > 0 && (

Showing {filtered.length} of {catalog.length} plugin{catalog.length !== 1 ? 's' : ''}

{hasActiveFilters && (

Results update instantly as you refine filters.

)}
)} {/* Content */} {loading && (
{Array.from({ length: 6 }, (_, i) => (
))}
)} {error && (
{error}
)} {!loading && !error && filtered.length === 0 && (
{hasActiveFilters ? ( ) : ( )}

{hasActiveFilters ? 'No plugins match your filters' : 'No plugins available'}

{hasActiveFilters ? 'Try adjusting your search or filter criteria' : 'Check back later for new plugins'}

{hasActiveFilters && ( )}
)} {!loading && !error && filtered.length > 0 && (
{filtered.map((plugin, index) => ( ))}
)} {/* Detail dialog */} setSelectedPluginId(null)} projectPath={projectPath} />
); };