// ========================================== // Chat Surface — ToolsPopup Component // ========================================== // Scrollable popup anchored to a toolbar icon. // Category-based tool toggles with persistence to localStorage. const html = window.html; const { useState, useEffect, useRef, useCallback, useMemo } = window.hooks; const STORAGE_KEY = 'sw-disabled-tools'; /** * Load tools list from API and organize by category. * @returns {{ categories: Array, disabled: Set, toggle: Function, getDisabled: Function }} */ export function useTools() { const [tools, setTools] = useState([]); const [disabled, setDisabled] = useState(_loadDisabled); useEffect(() => { // Load available tools from API if (window.sw?.api?.tools?.list) { window.sw.api.tools.list().then(resp => { setTools(resp || []); }).catch(() => {}); } }, []); const categories = useMemo(() => { const map = {}; for (const t of tools) { const cat = t.category || 'Other'; if (!map[cat]) map[cat] = []; map[cat].push(t); } return Object.entries(map).map(([name, items]) => ({ name, items })); }, [tools]); const toggle = useCallback((toolName) => { setDisabled(prev => { const next = new Set(prev); if (next.has(toolName)) next.delete(toolName); else next.add(toolName); _saveDisabled(next); return next; }); }, []); const toggleCategory = useCallback((catName) => { setDisabled(prev => { const cat = categories.find(c => c.name === catName); if (!cat) return prev; const next = new Set(prev); const allDisabled = cat.items.every(t => next.has(t.name)); for (const t of cat.items) { if (allDisabled) next.delete(t.name); else next.add(t.name); } _saveDisabled(next); return next; }); }, [categories]); const getDisabled = useCallback(() => [...disabled], [disabled]); return { categories, disabled, toggle, toggleCategory, getDisabled }; } /** * @param {{ * open: boolean, * onClose: () => void, * categories: Array<{name: string, items: Array<{name: string, description?: string}>}>, * disabled: Set, * onToggle: (name: string) => void, * onToggleCategory: (catName: string) => void, * }} props */ export function ToolsPopup({ open, onClose, categories, disabled, onToggle, onToggleCategory }) { const panelRef = useRef(null); const [expandedCats, setExpandedCats] = useState({}); // Close on Escape or outside click useEffect(() => { if (!open) return; function _onKey(e) { if (e.key === 'Escape') { e.stopPropagation(); onClose(); } } function _onClick(e) { if (panelRef.current && !panelRef.current.contains(e.target)) onClose(); } document.addEventListener('keydown', _onKey, true); setTimeout(() => document.addEventListener('click', _onClick), 0); return () => { document.removeEventListener('keydown', _onKey, true); document.removeEventListener('click', _onClick); }; }, [open, onClose]); const _toggleCat = useCallback((name) => { setExpandedCats(prev => ({ ...prev, [name]: !prev[name] })); }, []); if (!open) return null; const disabledCount = disabled.size; return html` `; } function _loadDisabled() { try { const arr = JSON.parse(localStorage.getItem(STORAGE_KEY)); return new Set(arr || []); } catch (_) { return new Set(); } } function _saveDisabled(set) { try { localStorage.setItem(STORAGE_KEY, JSON.stringify([...set])); } catch (_) {} }