// 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.

Sweep

idle

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); }, });