// panes/inbox.js — sweep & route CxAI/_inbox via MCP tools with slider, history, presets.
import { registerPane } from '../app.js';
import { mcp, escapeHtml } from '../lib/api.js';
import { ok, err, fmtJSON, meterRow, historyStore, copyToClipboard } from '../lib/ui.js';
const sweepHist = historyStore('cx_inbox_sweeps', 15);
const routeHist = historyStore('cx_inbox_routes', 15);
const TPL = `
Inbox
Route artifacts through the CxAI inbox classifier.
Route a single file
Recent routes
Last sweep
no sweep yet
Route result
no route yet
Sweep history
`;
function kpi(label, value, sub) {
return `${escapeHtml(label)}
${escapeHtml(String(value))}
${escapeHtml(sub || '')}
`;
}
function updateThr(host) {
const v = +host.querySelector('#inbox-threshold').value;
host.querySelector('#inbox-thr-val').textContent = v.toFixed(2);
const state = v >= 0.8 ? 'ok' : v >= 0.5 ? 'warn' : 'err';
host.querySelector('#inbox-thr-meter').innerHTML = meterRow('confidence', v * 100, `${(v * 100).toFixed(0)}%`, state);
}
function renderHist(host) {
const sw = sweepHist.list();
host.querySelector('#inbox-sweep-history').innerHTML = sw.length
? sw.map((r, i) => ``).join('')
: 'no sweeps yet
';
host.querySelectorAll('#inbox-sweep-history [data-hi]').forEach(b => b.addEventListener('click', () => {
host.querySelector('#inbox-result').textContent = fmtJSON(sweepHist.list()[+b.dataset.hi]);
}));
const ro = routeHist.list();
host.querySelector('#inbox-route-history').innerHTML = ro.length
? ro.map((r, i) => ``).join('')
: 'no routes yet
';
host.querySelectorAll('#inbox-route-history [data-rhi]').forEach(b => b.addEventListener('click', () => {
const r = routeHist.list()[+b.dataset.rhi];
host.querySelector('#inbox-route-path').value = r.path;
host.querySelector('#inbox-route-result').textContent = fmtJSON(r);
}));
}
async function runSweep(host) {
const mode = host.querySelector('#inbox-mode').value;
const threshold = parseFloat(host.querySelector('#inbox-threshold').value);
const limit = +host.querySelector('#inbox-limit').value || 0;
const dry_run = host.querySelector('#inbox-dry').checked;
const verbose = host.querySelector('#inbox-verbose').checked;
host.querySelector('#inbox-sweep-meta').textContent = 'running…';
host.querySelector('#inbox-result').textContent = 'running…';
const args = { mode, threshold, dry_run };
if (limit > 0) args.limit = limit;
if (verbose) args.verbose = true;
try {
const r = await mcp.call('inbox_sweep_tool', args);
host.querySelector('#inbox-result').textContent = fmtJSON(r);
const total = (Array.isArray(r?.routed) ? r.routed.length : r?.routed) ?? r?.summary?.routed ?? 0;
host.querySelector('#inbox-sweep-meta').textContent = dry_run ? `dry-run · ${total} candidates` : `routed ${total}`;
const pill = document.getElementById('nav-inbox-pill');
if (pill && !dry_run) pill.textContent = String(total || '');
sweepHist.push({ mode, threshold, dry_run, routed: total, ok: true, response: r });
renderHist(host); updateKpis(host, r);
ok(`Sweep complete${dry_run ? ' (dry)' : ''}`);
} catch (e) {
host.querySelector('#inbox-result').textContent = fmtJSON(e.body ?? { error: e.message });
host.querySelector('#inbox-sweep-meta').textContent = `error ${e.status ?? ''}`;
sweepHist.push({ mode, threshold, dry_run, ok: false, error: e.message });
renderHist(host);
err(`sweep: ${e.message}`);
}
}
async function routeOne(host) {
const path = host.querySelector('#inbox-route-path').value.trim();
if (!path) return;
const dry_run = host.querySelector('#inbox-route-dry').checked;
host.querySelector('#inbox-route-meta').textContent = 'routing…';
host.querySelector('#inbox-route-result').textContent = 'routing…';
try {
const r = await mcp.call('route_file_tool', { path, dry_run });
host.querySelector('#inbox-route-result').textContent = fmtJSON(r);
const target = r?.target || r?.destination || 'ok';
host.querySelector('#inbox-route-meta').textContent = target;
routeHist.push({ path, dry_run, target, ok: true });
renderHist(host);
ok(`Routed ${path.split('/').pop()}`);
} catch (e) {
host.querySelector('#inbox-route-result').textContent = fmtJSON(e.body ?? { error: e.message });
host.querySelector('#inbox-route-meta').textContent = `error ${e.status ?? ''}`;
routeHist.push({ path, dry_run, ok: false, error: e.message });
renderHist(host);
err(`route: ${e.message}`);
}
}
function updateKpis(host, r) {
const routed = (Array.isArray(r?.routed) ? r.routed.length : r?.routed) ?? r?.summary?.routed ?? 0;
const skipped = (Array.isArray(r?.skipped) ? r.skipped.length : r?.skipped) ?? r?.summary?.skipped ?? 0;
const queued = r?.summary?.queued ?? r?.queued ?? '—';
host.querySelector('#inbox-kpis').innerHTML = [
kpi('Routed (last)', routed, 'files moved'),
kpi('Skipped (last)', skipped, 'low-confidence'),
kpi('Queue', queued, 'remaining'),
].join('');
}
registerPane('inbox', {
label: 'Inbox',
init(host) {
host.innerHTML = TPL;
updateThr(host); updateKpis(host, {});
host.querySelector('#inbox-threshold').addEventListener('input', () => updateThr(host));
host.querySelectorAll('[data-preset]').forEach(b => b.addEventListener('click', () => {
const v = { strict: 0.9, normal: 0.7, loose: 0.4 }[b.dataset.preset];
host.querySelector('#inbox-threshold').value = v; updateThr(host);
}));
host.querySelector('#inbox-sweep-form').addEventListener('submit', (e) => { e.preventDefault(); runSweep(host); });
host.querySelector('#inbox-route-form').addEventListener('submit', (e) => { e.preventDefault(); routeOne(host); });
host.querySelector('#inbox-refresh').addEventListener('click', () => runSweep(host));
host.querySelector('#inbox-copy').addEventListener('click', () => copyToClipboard(host.querySelector('#inbox-result').textContent));
renderHist(host);
},
});