Changeset 0.13.1 (#65)

This commit is contained in:
2026-02-25 23:56:27 +00:00
parent 8292a6efa8
commit 113b98ace4
24 changed files with 1565 additions and 24 deletions

222
src/js/tools-toggle.js Normal file
View File

@@ -0,0 +1,222 @@
// ── 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: '🔍' },
notes: { label: 'Notes', icon: '📝' },
utilities: { label: 'Utilities', 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.data || [];
_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.data || [];
_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 stateClass = state === 'all' ? 'enabled' : state === 'partial' ? 'partial' : '';
const chevron = tools.length > 1
? `<span class="tools-popup-expand ${expanded ? 'open' : ''}" data-expand="${cat}"></span>`
: '';
html += `<div class="tools-popup-category ${stateClass}" data-category="${cat}">
<span class="tools-popup-check"></span>
<span class="tools-popup-label">${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 ${on ? 'enabled' : ''}" data-tool="${t.name}">
<span class="tools-popup-check"></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();
// Expand/collapse chevron
const expander = e.target.closest('.tools-popup-expand');
if (expander) {
_toggleExpand(expander.dataset.expand);
return;
}
// Individual tool toggle
const toolRow = e.target.closest('.tools-popup-tool');
if (toolRow) {
_toggleTool(toolRow.dataset.tool);
return;
}
// Category toggle
const catRow = e.target.closest('.tools-popup-category');
if (catRow) _toggleCategory(catRow.dataset.category);
});
// Close on outside click
document.addEventListener('click', () => popup.classList.remove('open'));
}
return { init, getDisabledTools, refresh };
})();