// panes/agent.js — absorbs the standalone CxAI dashboard.
// MCP status + live events feed (2s poll) + semantic search.
import { registerPane } from '../app.js';
import { mcp, MCP_BASE, escapeHtml } from '../lib/api.js';
import { ok, err, fmtJSON, skeleton } from '../lib/ui.js';
const TPL = `
Agent
CxAI MCP control plane · ${escapeHtml(MCP_BASE)}
checking…
Semantic search
Run a query to see results.
`;
let timer = 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)' : '';
}
}
async function refreshStatus(host) {
try {
const s = await mcp.status();
host.querySelector('#agent-status').textContent = fmtJSON(s);
host.querySelector('#agent-status-ts').textContent = new Date().toLocaleTimeString();
setPill('ok', 'connected');
} 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); }
}
async function refreshEvents(host) {
try {
const arr = await mcp.events(150);
const rows = (arr || []).slice().reverse().slice(0, 100).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 yet |
';
} catch (e) {
host.querySelector('#agent-events tbody').innerHTML =
`| events unavailable: ${escapeHtml(e.message)} |
`;
}
}
async function search(host) {
const q = host.querySelector('#agent-q').value.trim();
if (!q) return;
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: 10 });
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
';
} 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);
});
refreshStatus(host);
refreshEvents(host);
const tick = () => {
if (timer) clearInterval(timer);
timer = setInterval(() => {
if (!host.classList.contains('active')) return;
if (!host.querySelector('#agent-tail')?.checked) return;
refreshEvents(host);
refreshStatus(host);
}, 2000);
};
tick();
},
refresh(host) { refreshStatus(host); refreshEvents(host); },
});