// panes/agent.js — MCP control plane + live events + semantic search with filters. import { registerPane } from '../app.js'; import { mcp, MCP_BASE, escapeHtml } from '../lib/api.js'; import { ok, err, fmtJSON, skeleton, copyToClipboard, historyStore } from '../lib/ui.js'; const searchHist = historyStore('cx_agent_search', 20); const kindFilter = new Set(); const TPL = `
Agent
CxAI MCP control plane · ${escapeHtml(MCP_BASE)}
checking…

Status

${skeleton(4)}

Semantic search

Run a query to see results.

Live events

timekinddetail
loading…
`; let timer = null; let lastEvents = []; let lastStatus = null; function setPill(state, text) { const p = document.getElementById('agent-pill'); const t = document.getElementById('agent-pill-text'); const navPill = document.getElementById('nav-agent-pill'); if (!p || !t) return; p.classList.remove('ok', 'err', 'warn', 'info'); p.classList.add(state); t.textContent = text; if (navPill) { navPill.textContent = state === 'ok' ? '●' : state === 'err' ? '!' : '·'; navPill.style.color = state === 'ok' ? 'var(--ok)' : state === 'err' ? 'var(--err)' : ''; } } function kpi(label, value, sub) { return `
${escapeHtml(label)}
${escapeHtml(value)}
${escapeHtml(sub || '')}
`; } async function refreshStatus(host) { try { const s = await mcp.status(); lastStatus = s; host.querySelector('#agent-status').textContent = fmtJSON(s); host.querySelector('#agent-status-ts').textContent = new Date().toLocaleTimeString(); setPill('ok', 'connected'); host.querySelector('#agent-kpis').innerHTML = [ kpi('Tools', String(s.tools_count ?? s.tools?.length ?? '—'), 'registered'), kpi('Events', String(s.events_count ?? s.events ?? lastEvents.length), 'since boot'), kpi('Index', String(s.index_size ?? s.embeddings ?? '—'), 'vectors'), ].join(''); } catch (e) { host.querySelector('#agent-status').textContent = `error: ${e.message}\n\nIs the MCP HTTP sidecar running on ${MCP_BASE}?`; setPill('err', 'unavailable'); } } function fmtEventTs(ts) { if (!ts) return ''; try { if (typeof ts === 'number') return new Date(ts * (ts < 1e12 ? 1000 : 1)).toLocaleTimeString(); return new Date(ts).toLocaleTimeString(); } catch { return String(ts); } } function renderKinds(host) { const counts = {}; lastEvents.forEach(e => { const k = e.kind || e.type || 'event'; counts[k] = (counts[k] || 0) + 1; }); const ks = Object.entries(counts).sort((a, b) => b[1] - a[1]); host.querySelector('#agent-kinds').innerHTML = ks.map(([k, n]) => `` ).join(''); host.querySelectorAll('#agent-kinds .chip').forEach(b => b.addEventListener('click', () => { const k = b.dataset.k; if (kindFilter.has(k)) kindFilter.delete(k); else kindFilter.add(k); renderKinds(host); renderEvents(host); })); } function renderEvents(host) { const q = host.querySelector('#agent-evfilter')?.value.trim().toLowerCase() || ''; const filtered = lastEvents .filter(ev => !kindFilter.size || kindFilter.has(ev.kind || ev.type || 'event')) .filter(ev => { if (!q) return true; const blob = (typeof ev.detail === 'string' ? ev.detail : JSON.stringify(ev.detail ?? ev.data ?? ev)).toLowerCase(); return blob.includes(q); }); const rows = filtered.slice().reverse().slice(0, 200).map(ev => ` ${escapeHtml(fmtEventTs(ev.ts ?? ev.timestamp ?? ev.time))} ${escapeHtml(ev.kind || ev.type || 'event')} ${escapeHtml(typeof ev.detail === 'string' ? ev.detail : JSON.stringify(ev.detail ?? ev.data ?? ev))} `).join(''); host.querySelector('#agent-events tbody').innerHTML = rows || 'no events match'; } async function refreshEvents(host) { try { lastEvents = (await mcp.events(200)) || []; renderKinds(host); renderEvents(host); } catch (e) { host.querySelector('#agent-events tbody').innerHTML = `events unavailable: ${escapeHtml(e.message)}`; } } function renderRecent(host) { const list = searchHist.list(); host.querySelector('#agent-recent').innerHTML = list.length ? list.slice(0, 8).map(h => ``).join('') : ''; host.querySelectorAll('#agent-recent .chip').forEach(b => b.addEventListener('click', () => { host.querySelector('#agent-q').value = b.dataset.q; host.querySelector('#agent-search-form').requestSubmit(); })); } async function search(host) { const q = host.querySelector('#agent-q').value.trim(); if (!q) return; const k = +host.querySelector('#agent-k').value || 10; host.querySelector('#agent-search-meta').textContent = 'searching…'; host.querySelector('#agent-hits').innerHTML = skeleton(3); try { const res = await mcp.call('search_semantic_tool', { query: q, k }); const hits = res?.hits || res?.result?.hits || res?.results || res || []; host.querySelector('#agent-search-meta').textContent = `${hits.length} hits`; host.querySelector('#agent-hits').innerHTML = hits.length ? hits.map(h => `
${escapeHtml(h.path || h.id || '?')}
${escapeHtml((h.snippet || h.text || '').slice(0, 280))}
${(h.distance ?? h.score ?? '').toString().slice(0, 6)}
`).join('') : '
no hits
'; searchHist.push({ q }); renderRecent(host); } catch (e) { host.querySelector('#agent-search-meta').textContent = 'error'; host.querySelector('#agent-hits').innerHTML = `
${escapeHtml(e.message)}
`; err(`search: ${e.message}`); } } registerPane('agent', { label: 'Agent', init(host) { host.innerHTML = TPL; host.querySelector('#agent-search-form').addEventListener('submit', (e) => { e.preventDefault(); search(host); }); host.querySelector('#agent-evfilter').addEventListener('input', () => renderEvents(host)); host.querySelector('#agent-clear-kinds').addEventListener('click', () => { kindFilter.clear(); renderKinds(host); renderEvents(host); }); host.querySelector('#agent-copy-status').addEventListener('click', () => copyToClipboard(fmtJSON(lastStatus || {}), 'status copied')); renderRecent(host); refreshStatus(host); refreshEvents(host); timer = setInterval(() => { if (!host.classList.contains('active')) return; if (!host.querySelector('#agent-tail')?.checked) return; refreshEvents(host); refreshStatus(host); }, 2000); }, refresh(host) { refreshStatus(host); refreshEvents(host); }, });