245 lines
8.5 KiB
JavaScript
245 lines
8.5 KiB
JavaScript
// ── tools-toggle.js ─────────────────────────
|
||
// Tools toggle popup: category-based enable/disable of server-side tools.
|
||
// Categories expand to show individual tool toggles.
|
||
// State persisted in localStorage, sent as disabled_tools[] in completion requests.
|
||
|
||
'use strict';
|
||
|
||
const ToolsToggle = (() => {
|
||
const STORAGE_KEY = 'cs-disabled-tools';
|
||
|
||
// Category display config
|
||
const CATEGORY_META = {
|
||
search: { label: 'Web Search', icon: '🔍' },
|
||
knowledge: { label: 'Knowledge Bases', icon: '📚' },
|
||
notes: { label: 'Notes', icon: '📝' },
|
||
utilities: { label: 'Utilities', icon: '🧮' },
|
||
workspace: { label: 'Workspace', icon: '🗂️' },
|
||
context: { label: 'Context', icon: '📎' },
|
||
memory: { label: 'Memory', icon: '🧠' },
|
||
browser: { label: 'Extensions', icon: '🧩' },
|
||
};
|
||
|
||
// Cached tool manifest from GET /api/v1/tools
|
||
let _tools = []; // [{name, display_name, description, category}]
|
||
let _categories = {}; // {category: [{name, displayName}]}
|
||
let _disabled = new Set();
|
||
let _expanded = new Set(); // which categories are expanded
|
||
|
||
// ── Public API ──────────────────────────
|
||
|
||
/** Initialize: load disabled set from storage, fetch tool manifest. */
|
||
async function init() {
|
||
_loadDisabled();
|
||
try {
|
||
const resp = await API.getTools();
|
||
_tools = resp.tools || [];
|
||
_buildCategories();
|
||
_render();
|
||
_show();
|
||
} catch (e) {
|
||
console.warn('Tools toggle: failed to load tools', e);
|
||
}
|
||
_wireEvents();
|
||
}
|
||
|
||
/** Returns array of disabled tool names for the completion request. */
|
||
function getDisabledTools() {
|
||
return [..._disabled];
|
||
}
|
||
|
||
/** Refresh tool list (e.g. after browser extension connects). */
|
||
async function refresh() {
|
||
try {
|
||
const resp = await API.getTools();
|
||
_tools = resp.tools || [];
|
||
_buildCategories();
|
||
_render();
|
||
} catch (e) { /* silent */ }
|
||
}
|
||
|
||
// ── Internal ────────────────────────────
|
||
|
||
function _loadDisabled() {
|
||
try {
|
||
const raw = localStorage.getItem(STORAGE_KEY);
|
||
_disabled = raw ? new Set(JSON.parse(raw)) : new Set();
|
||
} catch { _disabled = new Set(); }
|
||
}
|
||
|
||
function _saveDisabled() {
|
||
localStorage.setItem(STORAGE_KEY, JSON.stringify([..._disabled]));
|
||
_updateBtnState();
|
||
}
|
||
|
||
function _buildCategories() {
|
||
_categories = {};
|
||
for (const t of _tools) {
|
||
const cat = t.category || 'other';
|
||
if (!_categories[cat]) _categories[cat] = [];
|
||
_categories[cat].push({
|
||
name: t.name,
|
||
displayName: t.display_name || t.name,
|
||
});
|
||
}
|
||
}
|
||
|
||
/** Returns 'all' | 'none' | 'partial' for a category's enabled state. */
|
||
function _categoryState(cat) {
|
||
const tools = _categories[cat] || [];
|
||
if (tools.length === 0) return 'none';
|
||
const enabledCount = tools.filter(t => !_disabled.has(t.name)).length;
|
||
if (enabledCount === tools.length) return 'all';
|
||
if (enabledCount === 0) return 'none';
|
||
return 'partial';
|
||
}
|
||
|
||
function _toggleCategory(cat) {
|
||
const tools = _categories[cat] || [];
|
||
const state = _categoryState(cat);
|
||
if (state === 'all') {
|
||
// All on → all off
|
||
tools.forEach(t => _disabled.add(t.name));
|
||
} else {
|
||
// Partial or none → all on
|
||
tools.forEach(t => _disabled.delete(t.name));
|
||
}
|
||
_saveDisabled();
|
||
_render();
|
||
}
|
||
|
||
function _toggleTool(name) {
|
||
if (_disabled.has(name)) {
|
||
_disabled.delete(name);
|
||
} else {
|
||
_disabled.add(name);
|
||
}
|
||
_saveDisabled();
|
||
_render();
|
||
}
|
||
|
||
function _toggleExpand(cat) {
|
||
if (_expanded.has(cat)) {
|
||
_expanded.delete(cat);
|
||
} else {
|
||
_expanded.add(cat);
|
||
}
|
||
_render();
|
||
}
|
||
|
||
function _show() {
|
||
const wrap = document.querySelector('.tools-toggle-wrap');
|
||
if (wrap && _tools.length > 0) {
|
||
wrap.style.display = '';
|
||
_updateBtnState();
|
||
}
|
||
}
|
||
|
||
function _updateBtnState() {
|
||
const btn = document.getElementById('toolsToggleBtn');
|
||
if (!btn) return;
|
||
btn.classList.toggle('has-disabled', _disabled.size > 0);
|
||
const count = _disabled.size;
|
||
btn.title = count > 0 ? `Tools (${count} disabled)` : 'Tools';
|
||
}
|
||
|
||
function _render() {
|
||
const popup = document.getElementById('toolsPopup');
|
||
if (!popup) return;
|
||
|
||
const cats = Object.keys(_categories);
|
||
const order = Object.keys(CATEGORY_META);
|
||
cats.sort((a, b) => {
|
||
const ai = order.indexOf(a), bi = order.indexOf(b);
|
||
return (ai === -1 ? 999 : ai) - (bi === -1 ? 999 : bi);
|
||
});
|
||
|
||
let html = '';
|
||
for (const cat of cats) {
|
||
const meta = CATEGORY_META[cat] || { label: cat, icon: '🔧' };
|
||
const state = _categoryState(cat);
|
||
const tools = _categories[cat];
|
||
const expanded = _expanded.has(cat);
|
||
const switchClass = state === 'all' ? 'on' : state === 'partial' ? 'on partial' : '';
|
||
const chevron = tools.length > 1
|
||
? `<span class="tools-popup-expand ${expanded ? 'open' : ''}" data-expand="${cat}">›</span>`
|
||
: '';
|
||
const labelExpand = tools.length > 1 ? ` data-expand="${cat}"` : '';
|
||
|
||
html += `<div class="tools-popup-category" data-category="${cat}">
|
||
<span class="tools-popup-switch ${switchClass}" data-switch="${cat}"><span class="tools-popup-switch-knob"></span></span>
|
||
<span class="tools-popup-label"${labelExpand}>${meta.icon} ${meta.label}</span>
|
||
${chevron}
|
||
</div>`;
|
||
|
||
// Expanded children
|
||
if (expanded && tools.length > 1) {
|
||
for (const t of tools) {
|
||
const on = !_disabled.has(t.name);
|
||
html += `<div class="tools-popup-tool" data-tool="${t.name}">
|
||
<span class="tools-popup-switch ${on ? 'on' : ''}" data-tool-switch="${t.name}"><span class="tools-popup-switch-knob"></span></span>
|
||
<span class="tools-popup-label">${t.displayName}</span>
|
||
</div>`;
|
||
}
|
||
}
|
||
}
|
||
|
||
popup.innerHTML = html;
|
||
}
|
||
|
||
function _wireEvents() {
|
||
const btn = document.getElementById('toolsToggleBtn');
|
||
const popup = document.getElementById('toolsPopup');
|
||
if (!btn || !popup) return;
|
||
|
||
btn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
popup.classList.toggle('open');
|
||
});
|
||
|
||
popup.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
|
||
// Toggle switch on category row
|
||
const catSwitch = e.target.closest('[data-switch]');
|
||
if (catSwitch) {
|
||
_toggleCategory(catSwitch.dataset.switch);
|
||
return;
|
||
}
|
||
|
||
// Toggle switch on individual tool row
|
||
const toolSwitch = e.target.closest('[data-tool-switch]');
|
||
if (toolSwitch) {
|
||
_toggleTool(toolSwitch.dataset.toolSwitch);
|
||
return;
|
||
}
|
||
|
||
// Expand/collapse: chevron or label click on multi-tool categories
|
||
const expander = e.target.closest('[data-expand]');
|
||
if (expander) {
|
||
_toggleExpand(expander.dataset.expand);
|
||
return;
|
||
}
|
||
|
||
// Individual tool label (no expand, just toggle)
|
||
const toolRow = e.target.closest('.tools-popup-tool');
|
||
if (toolRow) {
|
||
_toggleTool(toolRow.dataset.tool);
|
||
return;
|
||
}
|
||
|
||
// Single-tool category label click (no chevron) — toggle
|
||
const catRow = e.target.closest('.tools-popup-category');
|
||
if (catRow) {
|
||
_toggleCategory(catRow.dataset.category);
|
||
return;
|
||
}
|
||
});
|
||
|
||
// Close on outside click
|
||
document.addEventListener('click', () => popup.classList.remove('open'));
|
||
}
|
||
|
||
return { init, getDisabledTools, refresh };
|
||
})();
|