// panes/tools.js — MCP tool browser & invoker with schema-driven forms.
import { registerPane } from '../app.js';
import { mcp, escapeHtml } from '../lib/api.js';
import { fmtJSON, ok, err, copyToClipboard, inputFromSchema, collectSchemaForm, historyStore } from '../lib/ui.js';
const callHist = historyStore('cx_tool_calls', 30);
const FAV_KEY = 'cx_tool_favs';
const favs = new Set(JSON.parse(localStorage.getItem(FAV_KEY) || '[]'));
function persistFavs() { localStorage.setItem(FAV_KEY, JSON.stringify([...favs])); }
const TPL = `
Tools
Discover and invoke MCP tools.
Select a tool
Choose a tool on the left.
Result
—
Recent calls
`;
let allTools = [];
let current = null;
let mode = 'form';
let group = 'all';
function passes(t, q) {
const blob = (t.name + ' ' + (t.description || '')).toLowerCase();
if (q && !blob.includes(q)) return false;
if (group === 'fav' && !favs.has(t.name)) return false;
if (group === 'recent') {
const recents = new Set(callHist.list().map(r => r.tool));
if (!recents.has(t.name)) return false;
}
return true;
}
function renderList(host) {
const q = host.querySelector('#tl-search').value.trim().toLowerCase();
const items = allTools.filter(t => passes(t, q));
host.querySelector('#tl-list').innerHTML = items.length
? items.map(t => ``).join('')
: 'no tools match
';
host.querySelectorAll('[data-tool]').forEach(b => b.addEventListener('click', () => select(host, b.dataset.tool)));
}
function renderForm(host) {
if (!current) return;
const f = host.querySelector('#tl-form');
const schema = current.inputSchema || current.input_schema || {};
const props = schema.properties || {};
const names = Object.keys(props);
if (!names.length) {
f.innerHTML = 'no input schema — call with empty args or use JSON
';
return;
}
f.innerHTML = names.map(n => inputFromSchema(n, props[n])).join('');
}
function renderHist(host) {
const list = callHist.list().filter(r => r.tool === current?.name);
host.querySelector('#tl-hist').innerHTML = list.length
? list.map((r, i) => ``).join('')
: 'no recent calls
';
host.querySelectorAll('#tl-hist [data-hi]').forEach(b => b.addEventListener('click', () => {
const r = list[+b.dataset.hi];
if (r) { host.querySelector('#tl-json').value = fmtJSON(r.args); switchMode(host, 'json'); }
}));
}
function select(host, name) {
current = allTools.find(t => t.name === name);
if (!current) return;
host.querySelector('#tl-name').textContent = current.name;
host.querySelector('#tl-desc').textContent = current.description || '—';
host.querySelector('#tl-body').hidden = false;
const favBtn = host.querySelector('#tl-fav');
favBtn.hidden = false;
favBtn.textContent = favs.has(name) ? '★ unfavorite' : '☆ favorite';
host.querySelector('#tl-json').value = '{}';
renderForm(host); renderHist(host);
}
function switchMode(host, m) {
mode = m;
host.querySelectorAll('#tl-mode button').forEach(b => b.classList.toggle('active', b.dataset.m === m));
host.querySelector('#tl-form').hidden = m !== 'form';
host.querySelector('#tl-json').hidden = m !== 'json';
}
function gatherArgs(host) {
if (mode === 'form') return collectSchemaForm(host.querySelector('#tl-form'));
const t = host.querySelector('#tl-json').value.trim();
if (!t) return {};
return JSON.parse(t);
}
async function load(host) {
try {
const j = await mcp.tools();
allTools = (j.tools || []).slice().sort((a, b) => a.name.localeCompare(b.name));
renderList(host);
} catch (e) {
host.querySelector('#tl-list').innerHTML = `${escapeHtml(e.message)}
`;
}
}
registerPane('tools', {
label: 'Tools',
init(host) {
host.innerHTML = TPL;
host.querySelector('#tl-refresh').addEventListener('click', () => load(host));
host.querySelector('#tl-search').addEventListener('input', () => renderList(host));
host.querySelectorAll('#tl-seg button').forEach(b => b.addEventListener('click', () => {
host.querySelectorAll('#tl-seg button').forEach(x => x.classList.toggle('active', x === b));
group = b.dataset.g; renderList(host);
}));
host.querySelectorAll('#tl-mode button').forEach(b => b.addEventListener('click', () => switchMode(host, b.dataset.m)));
host.querySelector('#tl-fav').addEventListener('click', () => {
if (!current) return;
if (favs.has(current.name)) favs.delete(current.name); else favs.add(current.name);
persistFavs();
host.querySelector('#tl-fav').textContent = favs.has(current.name) ? '★ unfavorite' : '☆ favorite';
renderList(host);
});
host.querySelector('#tl-copy-args').addEventListener('click', () => {
try { copyToClipboard(fmtJSON(gatherArgs(host)), 'args copied'); } catch (e) { err(e.message); }
});
host.querySelector('#tl-reset').addEventListener('click', () => {
if (current) select(host, current.name);
});
host.querySelector('#tl-call').addEventListener('click', async () => {
if (!current) return;
let args; try { args = gatherArgs(host); } catch (e) { return err('args: ' + e.message); }
const t0 = performance.now();
const pre = host.querySelector('#tl-result');
pre.textContent = 'calling…';
try {
const j = await mcp.call(current.name, args);
const dt = (performance.now() - t0).toFixed(0);
host.querySelector('#tl-meta').textContent = `${dt} ms`;
pre.textContent = fmtJSON(j);
callHist.push({ t: Date.now(), tool: current.name, args });
renderHist(host);
ok('called ' + current.name);
} catch (e) {
pre.textContent = 'error: ' + e.message;
err(e.message);
}
});
load(host);
},
});