diff --git a/frontend/src/components/PluginsPanel.jsx b/frontend/src/components/PluginsPanel.jsx index 6f5dec6..59e65cd 100644 --- a/frontend/src/components/PluginsPanel.jsx +++ b/frontend/src/components/PluginsPanel.jsx @@ -11,10 +11,49 @@ const PLUGIN_ICONS = { jsonformat: BracesIcon, }; +// Helper badge renderer for compatibility tags (#597) +function CompatibilityBadge({ compatibility }) { + if (!compatibility) return null; + + const badges = Array.isArray(compatibility) + ? compatibility + : typeof compatibility === "object" + ? Object.values(compatibility) + : [compatibility]; + + return ( +
+ {badges.map((badge, idx) => { + const isLocal = String(badge).toLowerCase().includes("local") || String(badge).toLowerCase().includes("v1"); + const badgeStyle = isLocal + ? "bg-emerald-950/60 text-emerald-400 border-emerald-800/60" + : "bg-blue-950/60 text-blue-400 border-blue-800/60"; + + return ( + + {badge} + + ); + })} +
+ ); +} + export default function PluginsPanel({ sessionId, onClose }) { const [plugins, setPlugins] = useState([]); + const [searchQuery, setSearchQuery] = useState(""); const [selected, setSelected] = useState(null); - const [input, setInput] = useState(""); + + // Persistent input state initialized from localStorage (#596) + const [input, setInput] = useState(() => { + if (!sessionId) return ""; + return localStorage.getItem(`localmind_plugin_draft_${sessionId}`) || ""; + }); + const [output, setOutput] = useState(""); const [running, setRunning] = useState(false); const [loading, setLoading] = useState(false); @@ -22,10 +61,23 @@ export default function PluginsPanel({ sessionId, onClose }) { const [copied, setCopied] = useState(false); const [logs, setLogs] = useState([]); - // State for contextual menu and action toast feedback (#605) + // Persistent Pinned / Favorite Plugin IDs (#601) + const [pinnedIds, setPinnedIds] = useState(() => { + try { + const saved = localStorage.getItem(`plugins-panel-pinned:${sessionId}`); + return saved ? JSON.parse(saved) : []; + } catch (e) { + return []; + } + }); + + // State for contextual menu and action toast feedback (#602, #605) const [activeMenuId, setActiveMenuId] = useState(null); const [notification, setNotification] = useState(""); + // Changelog modal state (#603) + const [changelogPlugin, setChangelogPlugin] = useState(null); + // Persistence: View collapsed state (#592) const [isCollapsed, setIsCollapsed] = useState(() => { try { @@ -45,6 +97,15 @@ export default function PluginsPanel({ sessionId, onClose }) { } }); + // Sync pinned plugin state to localStorage (#601) + useEffect(() => { + try { + localStorage.setItem(`plugins-panel-pinned:${sessionId}`, JSON.stringify(pinnedIds)); + } catch (e) { + console.warn("localStorage write blocked:", e); + } + }, [pinnedIds, sessionId]); + // Sync collapsed state to localStorage (#592) useEffect(() => { try { @@ -96,7 +157,7 @@ export default function PluginsPanel({ sessionId, onClose }) { .finally(() => setLoading(false)); fetchLogs(); - }, [selectedPluginId]); + }, [selectedPluginId, sessionId]); function handleSelectPlugin(plugin) { setSelected(plugin); @@ -106,11 +167,37 @@ export default function PluginsPanel({ sessionId, onClose }) { setCopied(false); } - // Close contextual action menu on global click or Escape key + function togglePin(e, pluginId) { + e.stopPropagation(); + setPinnedIds((prev) => + prev.includes(pluginId) ? prev.filter((id) => id !== pluginId) : [...prev, pluginId] + ); + } + + // Re-sync input draft whenever sessionId changes (#596) + useEffect(() => { + if (!sessionId) return; + setInput(localStorage.getItem(`localmind_plugin_draft_${sessionId}`) || ""); + }, [sessionId]); + + // Persist plugin draft input to localStorage on edit (#596) + useEffect(() => { + if (!sessionId) return; + if (input) { + localStorage.setItem(`localmind_plugin_draft_${sessionId}`, input); + } else { + localStorage.removeItem(`localmind_plugin_draft_${sessionId}`); + } + }, [input, sessionId]); + + // Close menus and modals on outside click or Escape key (#603) useEffect(() => { const handleGlobalClick = () => setActiveMenuId(null); const handleKeyDown = (e) => { - if (e.key === "Escape") setActiveMenuId(null); + if (e.key === "Escape") { + setActiveMenuId(null); + setChangelogPlugin(null); + } }; window.addEventListener("click", handleGlobalClick); window.addEventListener("keydown", handleKeyDown); @@ -162,6 +249,12 @@ export default function PluginsPanel({ sessionId, onClose }) { } }; + function handleOpenChangelog(e, plugin) { + e.stopPropagation(); + setChangelogPlugin(plugin); + setActiveMenuId(null); + } + const toggleContextMenu = (e, pluginId) => { e.stopPropagation(); setActiveMenuId((prev) => (prev === pluginId ? null : pluginId)); @@ -178,6 +271,9 @@ export default function PluginsPanel({ sessionId, onClose }) { if (r.success) { setOutput(r.output); await fetchLogs(); + if (sessionId) { + localStorage.removeItem(`localmind_plugin_draft_${sessionId}`); + } } else { setError(r.error || "Plugin failed"); } @@ -188,6 +284,24 @@ export default function PluginsPanel({ sessionId, onClose }) { } } + // Sort plugins with pinned/favorites at the top (#601) + const sortedPlugins = [...plugins].sort((a, b) => { + const isAPinned = pinnedIds.includes(a.id); + const isBPinned = pinnedIds.includes(b.id); + if (isAPinned && !isBPinned) return -1; + if (!isAPinned && isBPinned) return 1; + return 0; + }); + + // Filter plugins in real-time by search query (#600) + const filteredPlugins = sortedPlugins.filter((p) => { + if (!searchQuery.trim()) return true; + const q = searchQuery.toLowerCase(); + const nameMatch = p.name?.toLowerCase().includes(q); + const descMatch = p.description?.toLowerCase().includes(q); + return nameMatch || descMatch; + }); + const handleCopy = async () => { if (!output) return; try { @@ -276,67 +390,111 @@ export default function PluginsPanel({ sessionId, onClose }) { )} - {/* Collapsible Panel Section (#592) */} + {/* Collapsible Panel Section */} {!isCollapsed && ( <> - {/* Plugin selector row with contextual dropdown menus (#605) */} -
- {plugins.map((p) => { - const Icon = PLUGIN_ICONS[p.icon] || PlugIcon; - const isMenuOpen = activeMenuId === p.id; - - return ( -
handleSelectPlugin(p)} - data-testid={`plugin-btn-${p.id}`} - className={`relative text-xs px-3.5 py-2 md:py-1.5 rounded-lg border transition font-medium flex items-center gap-1.5 cursor-pointer touch-manipulation select-none - ${selected?.id === p.id ? "border-purple-500 bg-purple-900/30 text-purple-300 shadow-sm shadow-purple-500/10" : "border-gray-700 text-gray-400 hover:bg-gray-800"}`} - > - - {p.name} - - {/* Context Menu Trigger Button */} - - - {/* Context Dropdown Menu (#605) */} - {isMenuOpen && ( -
e.stopPropagation()} - className="absolute left-0 top-full mt-1 w-36 bg-gray-950 border border-gray-800 rounded-lg shadow-xl z-50 py-1 text-xs text-gray-300 font-normal" + + {p.name} + + {p.compatibility && } + + {/* Pin/Favorite Toggle Button (#601) */} + - + + {/* Context Menu Trigger Button */} + + + {/* Context Dropdown Menu (#602, #603, #605) */} + {isMenuOpen && ( +
e.stopPropagation()} + className="absolute left-0 top-full mt-1 w-36 bg-gray-950 border border-gray-800 rounded-lg shadow-xl z-50 py-1 text-xs text-gray-300 font-normal" > - Export Config - -
- )} -
- ); - })} + + + +
+ )} +
+ ); + }) + ) : ( +

No matching plugins found.

+ )} {/* Plugin Input/Output Area OR Empty-State Guidance */} {selected ? (
-

{selected.description}

+
+

{selected.description}

+ {selected.compatibility && ( +
+ Compatibility: + +
+ )} +