(function () {
const esc = (s) => String(s ?? '').replace(/&/g, '&').replace(//g, '>');
const SEV_COLS = [
{ key: 'critical', label: 'Crítico', cls: 'ao-kanban-col--critical' },
{ key: 'high', label: 'Alto', cls: 'ao-kanban-col--high' },
{ key: 'warn', label: 'Aviso', cls: 'ao-kanban-col--warn' },
{ key: 'info', label: 'Info / OK', cls: 'ao-kanban-col--info' },
];
const AGENT_ICONS = {
A0: '🎯', A1: '💓', A2: '🛤', A3: '✉', A4: '🛡', A5: '👁',
A6: '🤖', A7: '🔧', sentinel: '📡', curator: '📚',
};
const AGENT_COLORS = {
A0: '#6366f1', A1: '#22c55e', A2: '#3b82f6', A3: '#06b6d4', A4: '#8b5cf6',
A5: '#ec4899', A6: '#a855f7', A7: '#ef4444', sentinel: '#f59e0b', curator: '#64748b',
};
const SUB_VIEWS = [
{ id: 'command', label: 'Squad Command', icon: '🎯' },
{ id: 'agents', label: 'Agent Management', icon: '👥' },
{ id: 'tasks', label: 'Task Board', icon: '📋' },
{ id: 'memory', label: 'Knowledge Graph', icon: '🧠' },
{ id: 'recurring', label: 'Recurring Tasks', icon: '🔄' },
{ id: 'activity', label: 'Activity Feed', icon: '📡' },
];
let state = {
selectedAgent: 'A6',
selectedIncidentId: null,
threadId: null,
mobileTab: 'board',
mainTab: 'missions',
subView: 'command',
chatModalOpen: false,
kbQuery: '',
lastActions: [],
pollTimer: null,
};
let cache = null;
let extraCache = { scenarios: null, activity: null, kbSources: null, kbHits: null };
let shellMounted = false;
let eventsBound = false;
let renderInFlight = false;
let pendingRender = null;
let lastThreadHtml = '';
let chatInFlight = false;
async function agentsApi(path, opts = {}) {
const h = authHeaders({ ...(opts.headers || {}) });
if (!(opts.body instanceof FormData) && !h['Content-Type']) h['Content-Type'] = 'application/json';
const isSlow = /\/chat$/.test(path) || path.includes('/chat');
const timeoutMs = isSlow ? 180000 : 60000;
const r = await fetchWithTimeout(`/api/v1/agents${path}`, { ...opts, headers: h }, timeoutMs);
if (r.status === 401) {
logout();
throw new Error('sessão expirada — faça login novamente');
}
if (!r.ok) throw new Error(`${r.status} ${(await r.text()).slice(0, 200)}`);
return r.json();
}
function escHtml(s) {
return esc(s);
}
function renderMarkdown(text) {
if (!text) return '';
const lines = String(text).split('\n');
const out = [];
let inList = false;
let inTable = false;
const flushList = () => {
if (inList) { out.push(''); inList = false; }
};
const flushTable = () => {
if (inTable) { out.push(''); inTable = false; }
};
for (let i = 0; i < lines.length; i++) {
const raw = lines[i];
const line = raw.trimEnd();
if (/^\|.+\|$/.test(line.trim())) {
flushList();
const cells = line.trim().slice(1, -1).split('|').map((c) => c.trim());
if (/^[-:\s|]+$/.test(line.replace(/\|/g, ''))) continue;
if (!inTable) {
out.push('
');
cells.forEach((c) => out.push(`| ${inlineMd(c)} | `));
out.push('
');
inTable = true;
} else {
out.push('');
cells.forEach((c) => out.push(`| ${inlineMd(c)} | `));
out.push('
');
}
continue;
}
flushTable();
if (/^---+$/.test(line.trim())) {
flushList();
out.push('
');
} else if (/^### (.+)/.test(line)) {
flushList();
out.push(`${inlineMd(line.replace(/^### /, ''))}
`);
} else if (/^## (.+)/.test(line)) {
flushList();
out.push(`${inlineMd(line.replace(/^## /, ''))}
`);
} else if (/^[-*] (.+)/.test(line)) {
if (!inList) { out.push(''); inList = true; }
out.push(`- ${inlineMd(line.replace(/^[-*] /, ''))}
`);
} else if (/^\d+\. (.+)/.test(line)) {
if (!inList) { out.push(''); inList = true; }
out.push(`- ${inlineMd(line.replace(/^\d+\. /, ''))}
`);
} else if (!line.trim()) {
flushList();
out.push('');
} else {
flushList();
out.push(`${inlineMd(line)}
`);
}
}
flushList();
flushTable();
return out.join('');
}
function inlineMd(s) {
return escHtml(s)
.replace(/`([^`]+)`/g, '$1')
.replace(/\*\*(.+?)\*\*/g, '$1');
}
function fmtMsgTime(iso) {
if (!iso) return 'agora';
try {
const d = new Date(iso);
const diff = Date.now() - d.getTime();
if (diff < 60000) return 'agora';
return d.toLocaleTimeString('pt-BR', { hour: '2-digit', minute: '2-digit' });
} catch {
return iso;
}
}
function chatErrorMessage(err) {
const msg = String(err?.message || err || '');
if (msg.includes('429') || msg.toLowerCase().includes('quota') || msg.toLowerCase().includes('balance')) {
return 'Conta KIMI sem saldo — recarregue em platform.moonshot.ai';
}
if (err?.name === 'AbortError' || msg.includes('aborted')) {
return 'Tempo esgotado — tente novamente ou verifique Groq/Ollama.';
}
return msg || 'Erro ao enviar mensagem';
}
function fmtAge(iso) {
if (!iso) return '—';
try {
const ms = Date.now() - new Date(iso).getTime();
const m = Math.floor(ms / 60000);
if (m < 1) return 'agora';
if (m < 60) return `há ${m} min`;
const h = Math.floor(m / 60);
if (h < 24) return `há ${h}h`;
return `há ${Math.floor(h / 24)}d`;
} catch {
return iso;
}
}
function liveDotClass(open) {
if ((open.critical || 0) > 0 || (open.high || 0) > 0) return 'ao-live-dot--alert';
if ((open.warn || 0) > 0) return 'ao-live-dot--warn';
return '';
}
function chatTargetLabel() {
if (state.selectedAgent === 'ALL') return 'A6 Copiloto';
const agents = cache?.roster?.agents || [];
const a = agents.find((x) => x.id === state.selectedAgent);
const icon = AGENT_ICONS[state.selectedAgent] || '🤖';
return a ? `${a.name} (${state.selectedAgent}) ${icon}` : `${state.selectedAgent} ${icon}`;
}
function chatAgentId() {
return state.selectedAgent === 'ALL' ? 'A6' : state.selectedAgent;
}
function msgBoxEl(root) {
if (!root) return null;
return root.querySelector(state.chatModalOpen ? '#ao-modal-thread-messages' : '#ao-thread-messages');
}
function chatInputEl(root) {
if (!root) return null;
return root.querySelector(state.chatModalOpen ? '#ao-modal-chat-input' : '#ao-chat-input');
}
function sendBtnEl(root) {
if (!root) return null;
return root.querySelector(state.chatModalOpen ? '#ao-modal-btn-send' : '#ao-btn-send');
}
function groupThreads(threads) {
const waiting = [];
const open = [];
threads.forEach((t) => {
if (t.status !== 'open') return;
if (t.pending_human > 0) waiting.push(t);
else open.push(t);
});
return { waiting, open };
}
function taskCardHtml(t) {
const active = state.threadId === t.id ? ' ao-task-card--active' : '';
const icon = AGENT_ICONS[t.primary_agent] || '🤖';
const badge = t.pending_human > 0 ? `aguarda você` : '';
return `
${icon}
${esc(t.subject)}
${esc(t.agent_name || t.primary_agent)} · ${fmtAge(t.updated_at)} · #${t.id}
${badge}
`;
}
function agentPickHtml(agents) {
const picks = agents.filter((a) => !['curator', 'sentinel'].includes(a.id)).slice(0, 8);
return picks.map((a) => {
const active = state.selectedAgent === a.id ? ' ao-agent-pick--active' : '';
const icon = AGENT_ICONS[a.id] || '🤖';
return ``;
}).join('');
}
function taskBoardHtml(data) {
const threads = data.threads?.threads || [];
const { waiting, open } = groupThreads(threads);
const agents = (data.roster?.agents || []).filter((a) => a.id !== 'curator');
const waitCards = waiting.map(taskCardHtml).join('')
|| '—
';
const openCards = open.slice(0, 12).map(taskCardHtml).join('')
|| 'Nenhuma conversa activa
';
return `
Aguardando você
${waitCards}
Conversas abertas
${openCards}
Nova conversa
Como conversar: escolha um agente, clique Abrir chat ou duplo-clique no Squad. O chat abre em modal grande.
${agentPickHtml(agents)}
`;
}
function deriveView(data) {
const list = data.incidents.incidents || [];
const openAgents = new Set(list.map((i) => i.primary_agent));
const counts = {};
list.forEach((i) => { counts[i.primary_agent] = (counts[i.primary_agent] || 0) + 1; });
const filtered = state.selectedAgent && state.selectedAgent !== 'ALL'
? list.filter((i) => i.primary_agent === state.selectedAgent)
: list;
const bySev = { critical: [], high: [], warn: [], info: [] };
filtered.forEach((inc) => {
const k = bySev[inc.severity] ? inc.severity : 'warn';
(bySev[k] || bySev.warn).push(inc);
});
const sel = list.find((i) => i.id === state.selectedIncidentId) || null;
return { list, openAgents, counts, filtered, bySev, sel };
}
function agentStatus(id, openAgents) {
if (openAgents.has(id)) return { cls: 'ao-squad-status--alert', label: 'alerta' };
if (state.selectedAgent === id) return { cls: 'ao-squad-status--active', label: 'activo' };
return { cls: 'ao-squad-status--standby', label: 'standby' };
}
function squadCardHtml(a, openAgents, counts) {
const selected = state.selectedAgent === a.id ? ' ao-squad-card--selected' : '';
const alert = openAgents.has(a.id) ? ' ao-squad-card--alert' : '';
const st = agentStatus(a.id, openAgents);
const count = counts[a.id] || 0;
const badge = count > 0 ? `${count}` : '';
const color = AGENT_COLORS[a.id] || '#64748b';
const icon = AGENT_ICONS[a.id] || '🤖';
return `
${badge}
${icon}
${esc(a.name)}
${esc(a.id)} · ${esc(a.codename)}
${st.label}
`;
}
function squadHtml(data, view) {
const squadAgents = (data.roster.agents || []).filter((a) => a.id !== 'curator');
const allCard = `
🌐
Todos
visão global
${view.list.length} abertos
`;
return allCard + squadAgents.map((a) => squadCardHtml(a, view.openAgents, view.counts)).join('');
}
function missionCardHtml(inc) {
const active = state.selectedIncidentId === inc.id ? ' ao-mission-card--active' : '';
const sevCls = ` ao-mission-card--${inc.severity || 'warn'}`;
const icon = AGENT_ICONS[inc.primary_agent] || '🤖';
return `
${icon} ${esc(inc.title)}
${esc(inc.agent_name)} · ${esc(inc.scenario_id)} · ${fmtAge(inc.last_seen_at)} · ${inc.occurrence_count || 1}×
${esc((inc.suggested_human_action || 'Investigar manualmente.').slice(0, 160))}
`;
}
function kanbanHtml(view) {
if (!view.filtered.length) {
return '✓
Ambiente saudável — nenhum incidente aberto.
';
}
return SEV_COLS.map((col) => {
const cards = (view.bySev[col.key] || []).map(missionCardHtml).join('')
|| '—
';
return `${col.label}
${cards}`;
}).join('');
}
function chipsHtml(overview, open) {
const tier = overview.tier === 't1' ? 'T1 LLM' : 'T0 Checks';
const provider = overview.provider || 'ollama';
const model = overview.model || '—';
let llmChip;
if (provider === 'groq') {
llmChip = `Groq · ${esc(model)}`;
} else if (provider === 'kimi') {
llmChip = `KIMI · ${esc(model)}`;
} else if (overview.ollama) {
llmChip = `Ollama · ${esc(model)}`;
} else {
llmChip = 'LLM offline';
}
const warnChip = (open.high || open.critical) ? ' ao-chip--warn' : '';
return `
${esc(tier)}
${llmChip}
Tick ${fmtAge(overview.last_tick_at)}
${overview.scenarios_ok || 0}/${overview.scenarios_total || 9} cenários OK
${open.critical || 0} crít · ${open.high || 0} alto · ${open.warn || 0} aviso`;
}
function timelineHtml(timeline) {
const ticks = timeline.ticks || [];
if (!ticks.length) return 'Sem ticks recentes.
';
return ticks.map((t) =>
`${esc(fmtAge(t.at))}${t.scenarios || '—'} cenários · ${t.findings || 0} findings
`
).join('');
}
function commsContextHtml(sel) {
if (!sel) return 'Seleccione um incidente no board ou fale com um agente.';
return `${esc(sel.title)}
${esc(sel.scenario_id)} · ${esc(sel.agent_name)}`;
}
function msgContext(m) {
if (m.context) return m.context;
if (m.context_json) {
try { return JSON.parse(m.context_json); } catch { return {}; }
}
return {};
}
function actionsHtml(actions) {
if (!actions?.length) return '';
const btns = actions.map((a) => {
if (a.type === 'run_scenario') {
const sev = a.severity || 'info';
return ``;
}
if (a.type === 'ack_incident') {
return ``;
}
return '';
}).join('');
return `Acções rápidas${btns}
`;
}
function threadBubble(m) {
const cls = m.from_type === 'human' ? 'ao-bubble-human' : 'ao-bubble-agent';
const ctx = msgContext(m);
const body = m.from_type === 'human'
? esc(m.body).replace(/\n/g, '
')
: renderMarkdown(m.body);
const model = ctx.model ? ` · ${esc(ctx.model)}` : '';
const actions = m.from_type === 'agent' ? actionsHtml(ctx.actions) : '';
return `
${esc(m.from_label || m.from_id)} · ${esc(fmtMsgTime(m.created_at))}${model}
${body}
${actions}
`;
}
function typingBubbleHtml(agentLabel) {
return `
${esc(agentLabel)} · a escrever...
`;
}
function subnavHtml() {
return SUB_VIEWS.map((v) =>
``
).join('');
}
function agentManagementHtml(data) {
const agents = (data.roster?.agents || []).filter((a) => a.id !== 'curator');
const cards = agents.map((a) => {
const icon = AGENT_ICONS[a.id] || '🤖';
const color = AGENT_COLORS[a.id] || '#64748b';
const reads = (a.reads || []).slice(0, 3).map((r) => `- ${esc(r)}
`).join('');
const actions = (a.actions || []).slice(0, 2).map((r) => `- ${esc(r)}
`).join('');
const scenarios = (a.scenarios || []).length
? a.scenarios.map((s) => `${esc(s)}`).join(' ')
: '—';
return `
${icon}
${esc(a.name)} ${esc(a.id)}
${esc(a.role)}
Aprovação: ${esc(a.approval)}
Cenários: ${scenarios}
`;
}).join('');
return `Agent Management
Roster A0–A7 + Vigia · clique Conversar para abrir chat em modal
${cards}
`;
}
function knowledgeGraphHtml() {
const sources = extraCache.kbSources?.sources || [];
const total = extraCache.kbSources?.total_chunks || 0;
const hits = extraCache.kbHits?.hits || [];
const sourceNodes = sources.map((s) => {
const short = String(s.source_path || '').split('/').slice(-2).join('/');
return `
📄 ${esc(short)}
${s.chunks} chunks · ${fmtAge(s.indexed_at)}
`;
}).join('') || 'Nenhum documento indexado — aguardar tick do Curador.
';
const hitCards = hits.map((h) =>
`${esc(h.source)}
${esc(h.snippet)}
`
).join('') || 'Pesquise specs, runbooks ou infra…
';
return `
Memory · Knowledge Graph
${total} chunks indexados · RAG alimenta Copiloto e agentes
Fontes indexadas
${sourceNodes}
`;
}
function recurringTasksHtml() {
const scenarios = extraCache.scenarios?.scenarios || [];
if (!scenarios.length) {
return 'Natural Language Recurring Tasks
A carregar cenários…
';
}
const rows = scenarios.map((s) => {
const statusCls = s.last_run_status === 'ok' ? 'ao-chip--ok' : (s.last_run_status === 'fail' ? 'ao-chip--warn' : '');
const enabled = s.enabled ? 'Activo' : 'Pausado';
return `
${esc(s.title || s.id)}
${esc(s.last_run_status || '—')}
${esc(s.id)}
${esc(s.schedule || 'cron')}
${enabled}
Severidade ${esc(s.severity_default || 'warn')}
Última execução: ${fmtAge(s.last_run_at)} · ${s.last_run_message ? esc(String(s.last_run_message).slice(0, 120)) : '—'}
`;
}).join('');
return `Natural Language Recurring Tasks
Cenários T0 vigilância 24/7 — linguagem natural descreve o check, worker executa no schedule
${rows}
`;
}
function activityFeedHtml() {
const events = extraCache.activity?.events || [];
if (!events.length) {
return 'Activity Feed
A carregar eventos…
';
}
const rows = events.map((e) => {
const payload = e.payload_json ? (() => { try { return JSON.parse(e.payload_json); } catch { return {}; } })() : {};
const agent = e.agent_id ? `${esc(e.agent_id)}` : '';
return `
${esc(fmtAge(e.ts))}
${esc(e.event_type)} ${agent}
${esc(e.message || '')}
${payload.model ? `
model: ${esc(payload.model)}
` : ''}
`;
}).join('');
return `Activity Feed
Últimos eventos agentic — ticks, chat, acks, runs
${rows}
`;
}
function patchHtml(node, html) {
if (!node || node.innerHTML === html) return false;
node.innerHTML = html;
return true;
}
function chatModalHtml() {
return `
Nova conversa — escreva abaixo.
`;
}
function mountShell(el) {
el.innerHTML = `
Agent Squad — clique filtra · duplo-clique ou 💬 abre chat modal
Task Board — conversas com agentes
${chatModalHtml()}
`;
shellMounted = true;
}
function updateSubViews(el) {
el.querySelectorAll('.ao-subnav-btn').forEach((btn) => {
btn.classList.toggle('active', btn.dataset.subView === state.subView);
});
el.querySelectorAll('.ao-subview').forEach((pane) => {
pane.classList.toggle('ao-subview--active', pane.dataset.subview === state.subView);
});
}
function updateMainTabs(el) {
el.querySelectorAll('[data-main-tab]').forEach((btn) => {
btn.classList.toggle('active', btn.dataset.mainTab === state.mainTab);
});
el.querySelectorAll('[data-main-pane]').forEach((pane) => {
pane.classList.toggle('ao-content-pane--active', pane.dataset.mainPane === state.mainTab);
});
}
function updateModalChrome(el, sel) {
const modal = el.querySelector('#ao-chat-modal');
if (!modal) return;
const agentId = chatAgentId();
const icon = AGENT_ICONS[agentId] || '🤖';
const iconEl = modal.querySelector('#ao-modal-icon');
const nameEl = modal.querySelector('#ao-modal-name');
const subEl = modal.querySelector('#ao-modal-sub');
const ctxEl = modal.querySelector('#ao-modal-context');
const labelEl = modal.querySelector('#ao-modal-chat-label');
if (iconEl) iconEl.textContent = icon;
if (nameEl) nameEl.textContent = chatTargetLabel();
if (subEl) subEl.textContent = state.threadId ? `Thread #${state.threadId}` : 'Nova conversa';
if (ctxEl) ctxEl.innerHTML = commsContextHtml(sel);
if (labelEl) labelEl.textContent = `Mensagem para ${chatTargetLabel()}`;
modal.classList.toggle('ao-modal--open', state.chatModalOpen);
modal.setAttribute('aria-hidden', state.chatModalOpen ? 'false' : 'true');
document.body.classList.toggle('ao-modal-open', state.chatModalOpen);
}
function updateCommsChrome(el, sel) {
const fab = el.querySelector('#ao-btn-open-chat');
const inline = el.querySelector('#ao-btn-open-chat-inline');
const label = `💬 Chat · ${chatAgentId()}`;
if (fab) fab.textContent = label;
if (inline) inline.textContent = `Abrir chat com ${chatAgentId()}`;
updateModalChrome(el, sel);
}
function updateMobileTabs(el) {
if (state.subView !== 'command') return;
el.querySelectorAll('.ao-mobile-tabs button').forEach((btn) => {
btn.classList.toggle('active', btn.dataset.aoTab === state.mobileTab);
});
el.querySelectorAll('[data-subview="command"] .ao-pane').forEach((pane) => {
const paneName = pane.dataset.aoPane;
const active = paneName === state.mobileTab
|| (state.mobileTab === 'tasks' && paneName === 'board');
pane.classList.toggle('ao-pane--active', active);
});
}
function patchSubViewPanes(el, data) {
patchHtml(el.querySelector('#ao-pane-agents'), agentManagementHtml(data));
patchHtml(el.querySelector('#ao-pane-tasks'), taskBoardHtml(data));
patchHtml(el.querySelector('#ao-pane-memory'), knowledgeGraphHtml());
patchHtml(el.querySelector('#ao-pane-recurring'), recurringTasksHtml());
patchHtml(el.querySelector('#ao-pane-activity'), activityFeedHtml());
}
function patchDashboard(el, data) {
const view = deriveView(data);
const open = data.overview.incidents_open || {};
const dot = el.querySelector('#ao-live-dot');
if (dot) dot.className = `ao-live-dot ${liveDotClass(open)}`.trim();
patchHtml(el.querySelector('#ao-stat-chips'), chipsHtml(data.overview, open));
patchHtml(el.querySelector('#ao-subnav'), subnavHtml());
patchHtml(el.querySelector('#ao-squad-grid'), squadHtml(data, view));
patchHtml(el.querySelector('#ao-kanban'), kanbanHtml(view));
patchHtml(el.querySelector('#ao-timeline'), timelineHtml(data.timeline));
patchHtml(el.querySelector('#ao-task-board'), taskBoardHtml(data));
const boardLabel = el.querySelector('#ao-board-label');
if (boardLabel) {
boardLabel.textContent = state.selectedAgent !== 'ALL'
? `Mission Board · filtro ${state.selectedAgent}`
: 'Mission Board';
}
patchSubViewPanes(el, data);
updateSubViews(el);
updateMainTabs(el);
updateCommsChrome(el, view.sel);
updateMobileTabs(el);
return view;
}
function openChatModal(root, agentId) {
if (agentId && agentId !== 'ALL') state.selectedAgent = agentId;
state.chatModalOpen = true;
const view = cache ? deriveView(cache) : { sel: null };
updateModalChrome(root, view.sel);
if (!state.threadId) {
const box = root.querySelector('#ao-modal-thread-messages');
if (box && !box.querySelector('.ao-bubble')) {
box.innerHTML = 'Nova conversa — escreva a primeira mensagem abaixo.
';
}
}
root.querySelector('#ao-modal-chat-input')?.focus();
}
function closeChatModal(root) {
state.chatModalOpen = false;
updateModalChrome(root, cache ? deriveView(cache).sel : null);
}
async function sendChatStream(root, path, body) {
const h = authHeaders({ 'Content-Type': 'application/json', Accept: 'text/event-stream' });
const r = await fetchWithTimeout(`/api/v1/agents${path}`, {
method: 'POST',
headers: h,
body: JSON.stringify(body),
}, 180000);
if (r.status === 401) {
logout();
throw new Error('sessão expirada — faça login novamente');
}
if (!r.ok) throw new Error(`${r.status} ${(await r.text()).slice(0, 200)}`);
if (!r.body) throw new Error('stream indisponível');
return r.body.getReader();
}
async function sendChatMessage(root) {
const input = chatInputEl(root);
const btn = sendBtnEl(root);
const box = msgBoxEl(root);
const q = (input?.value || '').trim();
if (!q || chatInFlight) return;
const target = chatAgentId();
chatInFlight = true;
if (btn) btn.disabled = true;
let streamText = '';
let typingEl = null;
if (box) {
box.querySelector('#ao-typing-bubble')?.remove();
box.querySelector('.ticket-meta')?.remove();
box.insertAdjacentHTML('beforeend', ``);
box.insertAdjacentHTML('beforeend', typingBubbleHtml(chatTargetLabel()));
typingEl = box.querySelector('#ao-typing-bubble');
box.scrollTop = box.scrollHeight;
}
try {
const path = state.threadId
? `/threads/${state.threadId}/chat/stream`
: '/chat/stream';
const reader = await sendChatStream(root, path, {
question: q,
include_findings: true,
target_agent: target,
});
const decoder = new TextDecoder();
let buf = '';
let doneEvt = null;
while (true) {
const { done, value } = await reader.read();
if (done) break;
buf += decoder.decode(value, { stream: true });
const parts = buf.split('\n\n');
buf = parts.pop() || '';
for (const part of parts) {
const line = part.trim();
if (!line.startsWith('data: ')) continue;
let evt;
try { evt = JSON.parse(line.slice(6)); } catch { continue; }
if (evt.type === 'start' && evt.thread_id) {
state.threadId = evt.thread_id;
updateCommsChrome(root, cache ? deriveView(cache).sel : null);
}
if (evt.type === 'token' && evt.text) {
streamText += evt.text;
const streamBody = typingEl?.querySelector('.ao-stream-body');
if (streamBody) {
streamBody.innerHTML = renderMarkdown(streamText);
box.scrollTop = box.scrollHeight;
}
}
if (evt.type === 'done') doneEvt = evt;
if (evt.type === 'error') throw new Error(evt.message || 'Erro no stream');
}
}
input.value = '';
lastThreadHtml = '';
if (doneEvt?.thread_id) state.threadId = doneEvt.thread_id;
if (doneEvt?.actions?.length) {
state.lastActions = doneEvt.actions;
if (typingEl) typingEl.insertAdjacentHTML('beforeend', actionsHtml(doneEvt.actions));
}
if (state.threadId) {
await loadThread(root, state.threadId);
} else if (typingEl && streamText) {
typingEl.id = '';
typingEl.classList.remove('ao-bubble--typing');
const meta = typingEl.querySelector('.ao-bubble-meta');
if (meta) meta.textContent = `${chatTargetLabel()} · agora${doneEvt?.model ? ` · ${doneEvt.model}` : ''}`;
}
try {
const threads = await agentsApi('/threads?limit=40');
if (cache) cache.threads = threads;
patchHtml(root.querySelector('#ao-pane-tasks'), cache ? taskBoardHtml(cache) : '');
patchHtml(root.querySelector('#ao-task-board'), cache ? taskBoardHtml(cache) : '');
} catch { /* ignore */ }
} catch (err) {
typingEl?.remove();
if (box) {
box.insertAdjacentHTML('beforeend', `${esc(chatErrorMessage(err))}
`);
box.scrollTop = box.scrollHeight;
}
} finally {
chatInFlight = false;
if (btn) btn.disabled = false;
input?.focus();
}
}
async function runScenarioAction(root, scenarioId, btn) {
if (btn) { btn.disabled = true; btn.dataset.prev = btn.textContent; btn.textContent = '⏳ A executar…'; }
const box = msgBoxEl(root);
try {
const res = await agentsApi(`/runs/${encodeURIComponent(scenarioId)}`, { method: 'POST' });
const ok = res.ok && res.status === 'ok';
const msg = ok
? `✅ **Check concluído** — \`${scenarioId}\` saudável (sem findings).`
: `⚠️ **Check concluído** — \`${scenarioId}\`: status \`${res.status}\`, ${res.findings_count || 0} finding(s). Run #${res.run_id || '?'}`;
if (box) {
box.insertAdjacentHTML('beforeend', `Sistema · agora
${renderMarkdown(msg)}
`);
box.scrollTop = box.scrollHeight;
}
await renderAgenticOps({ soft: true, force: true });
} catch (err) {
if (box) {
box.insertAdjacentHTML('beforeend', `${esc(chatErrorMessage(err))}
`);
}
} finally {
if (btn) {
btn.disabled = false;
btn.textContent = btn.dataset.prev || btn.textContent;
}
}
}
async function ackIncidentAction(root, incidentId, btn) {
if (btn) { btn.disabled = true; btn.textContent = '⏳…'; }
try {
await agentsApi(`/incidents/${incidentId}/ack`, { method: 'POST' });
const box = msgBoxEl(root);
if (box) {
box.insertAdjacentHTML('beforeend', `Sistema · agora
✓ Incidente #${incidentId} reconhecido (ack).
`);
box.scrollTop = box.scrollHeight;
}
await renderAgenticOps({ soft: true, force: true });
} catch (err) {
const box = msgBoxEl(root);
if (box) box.insertAdjacentHTML('beforeend', `${esc(chatErrorMessage(err))}
`);
} finally {
if (btn) btn.disabled = false;
}
}
function focusChat(root, agentId) {
if (agentId && agentId !== 'ALL') state.selectedAgent = agentId;
openChatModal(root, agentId);
}
async function openTaskThread(root, threadId, agentId) {
state.threadId = threadId;
if (agentId) state.selectedAgent = agentId;
lastThreadHtml = '';
openChatModal(root, agentId);
if (cache) patchDashboard(root, cache);
await loadThread(root, threadId);
updateCommsChrome(root, cache ? deriveView(cache).sel : null);
root.querySelector('#ao-modal-chat-input')?.focus();
}
function preserveInputs(el) {
return {
chat: chatInputEl(el)?.value || '',
modalChat: el.querySelector('#ao-modal-chat-input')?.value || '',
kb: el.querySelector('#ao-kb-search')?.value || state.kbQuery,
};
}
function restoreInputs(el, saved) {
const chat = chatInputEl(el);
if (chat && saved.chat) chat.value = saved.chat;
const modal = el.querySelector('#ao-modal-chat-input');
if (modal && saved.modalChat) modal.value = saved.modalChat;
const kb = el.querySelector('#ao-kb-search');
if (kb && saved.kb) kb.value = saved.kb;
}
async function loadThread(el, threadId, options = {}) {
const box = msgBoxEl(el);
if (!box || !threadId) return;
const silent = options.silent === true;
const prevScroll = box.scrollTop;
const atBottom = box.scrollHeight - box.scrollTop - box.clientHeight < 40;
if (!silent && state.threadId !== threadId) {
box.innerHTML = 'A carregar thread…
';
}
state.threadId = threadId;
const data = await agentsApi(`/threads/${threadId}/messages`);
const html = data.messages.map(threadBubble).join('') || 'Sem mensagens.
';
if (html === lastThreadHtml && state.threadId === threadId) {
if (atBottom) box.scrollTop = box.scrollHeight;
else box.scrollTop = prevScroll;
return;
}
lastThreadHtml = html;
box.innerHTML = html;
box.scrollTop = atBottom ? box.scrollHeight : prevScroll;
updateModalChrome(el, cache ? deriveView(cache).sel : null);
}
async function fetchDashboard() {
const [overview, incidents, roster, timeline, threads] = await Promise.all([
agentsApi('/overview'),
agentsApi('/incidents?status=open&limit=50'),
agentsApi('/roster'),
agentsApi('/timeline?limit=12'),
agentsApi('/threads?limit=40'),
]);
return { overview, incidents, roster, timeline, threads };
}
async function fetchExtraData() {
const [scenarios, activity, kbSources] = await Promise.all([
agentsApi('/scenarios'),
agentsApi('/action-log?limit=80'),
agentsApi('/kb/sources'),
]);
extraCache.scenarios = scenarios;
extraCache.activity = activity;
extraCache.kbSources = kbSources;
if (state.kbQuery.trim()) {
extraCache.kbHits = await agentsApi(`/kb/search?q=${encodeURIComponent(state.kbQuery.trim())}`);
}
}
async function runKbSearch(root) {
const input = root.querySelector('#ao-kb-search');
state.kbQuery = (input?.value || '').trim();
if (!state.kbQuery) return;
extraCache.kbHits = await agentsApi(`/kb/search?q=${encodeURIComponent(state.kbQuery)}`);
if (cache) patchHtml(root.querySelector('#ao-pane-memory'), knowledgeGraphHtml());
}
function bindEventsOnce(el) {
if (eventsBound) return;
eventsBound = true;
el.addEventListener('dblclick', (ev) => {
const root = document.getElementById('agentic-ops-content');
const agentNode = ev.target.closest('[data-agent-id]');
if (!root || !agentNode || agentNode.dataset.agentId === 'ALL') return;
if (ev.target.closest('.ao-squad-chat-btn')) return;
ev.preventDefault();
state.threadId = null;
lastThreadHtml = '';
focusChat(root, agentNode.dataset.agentId);
});
el.addEventListener('keydown', (ev) => {
const root = document.getElementById('agentic-ops-content');
if (!root || !root.contains(ev.target)) return;
if (ev.key === 'Enter' && !ev.shiftKey && (ev.target.id === 'ao-chat-input' || ev.target.id === 'ao-modal-chat-input')) {
ev.preventDefault();
sendChatMessage(root);
}
if (ev.key === 'Escape' && state.chatModalOpen) {
ev.preventDefault();
closeChatModal(root);
}
});
el.addEventListener('click', async (ev) => {
const root = document.getElementById('agentic-ops-content');
if (!root || !root.contains(ev.target)) return;
if (ev.target.closest('[data-close-modal]') || ev.target.closest('#ao-modal-close')) {
closeChatModal(root);
return;
}
const runBtn = ev.target.closest('[data-run-scenario]');
if (runBtn) {
ev.preventDefault();
await runScenarioAction(root, runBtn.dataset.runScenario, runBtn);
return;
}
const ackActionBtn = ev.target.closest('[data-ack-incident-id]');
if (ackActionBtn) {
ev.preventDefault();
await ackIncidentAction(root, parseInt(ackActionBtn.dataset.ackIncidentId, 10), ackActionBtn);
return;
}
const subBtn = ev.target.closest('[data-sub-view]');
if (subBtn && subBtn.classList.contains('ao-subnav-btn')) {
state.subView = subBtn.dataset.subView;
updateSubViews(root);
if (cache) patchSubViewPanes(root, cache);
return;
}
const openChatBtn = ev.target.closest('[data-open-chat], #ao-btn-open-chat, #ao-btn-open-chat-inline');
if (openChatBtn) {
ev.stopPropagation();
const agent = openChatBtn.dataset.openChat || chatAgentId();
state.threadId = openChatBtn.dataset.openChat ? null : state.threadId;
if (openChatBtn.dataset.openChat) lastThreadHtml = '';
focusChat(root, agent);
return;
}
const refreshBtn = ev.target.closest('#ao-btn-refresh');
if (refreshBtn) {
ev.preventDefault();
await renderAgenticOps({ force: true });
return;
}
const kbBtn = ev.target.closest('#ao-kb-search-btn');
if (kbBtn) {
await runKbSearch(root);
return;
}
const mainTabBtn = ev.target.closest('[data-main-tab]');
if (mainTabBtn) {
state.mainTab = mainTabBtn.dataset.mainTab;
updateMainTabs(root);
return;
}
const chatAgentBtn = ev.target.closest('[data-chat-agent]');
if (chatAgentBtn) {
state.selectedAgent = chatAgentBtn.dataset.chatAgent;
state.threadId = null;
lastThreadHtml = '';
if (cache) patchDashboard(root, cache);
focusChat(root, chatAgentBtn.dataset.chatAgent);
return;
}
const newChatBtn = ev.target.closest('#ao-btn-new-chat');
if (newChatBtn) {
state.threadId = null;
lastThreadHtml = '';
focusChat(root, chatAgentId());
return;
}
const taskCard = ev.target.closest('[data-thread-id]');
if (taskCard && taskCard.classList.contains('ao-task-card')) {
const tid = parseInt(taskCard.dataset.threadId, 10);
await openTaskThread(root, tid, taskCard.dataset.taskAgent);
return;
}
const agentNode = ev.target.closest('[data-agent-id]');
if (agentNode && !ev.target.closest('[data-ack-incident]') && !ev.target.closest('[data-open-chat]')) {
state.selectedAgent = agentNode.dataset.agentId;
if (cache) patchDashboard(root, cache);
return;
}
const ackBtn = ev.target.closest('[data-ack-incident]');
if (ackBtn) {
ev.stopPropagation();
const id = parseInt(ackBtn.dataset.ackIncident, 10);
await agentsApi(`/incidents/${id}/ack`, { method: 'POST' });
if (state.selectedIncidentId === id) {
state.selectedIncidentId = null;
state.threadId = null;
lastThreadHtml = '';
}
await renderAgenticOps({ soft: true, force: true });
return;
}
const incNode = ev.target.closest('[data-incident-id], [data-open-incident]');
if (incNode && !ev.target.closest('[data-ack-incident]')) {
const id = parseInt(incNode.dataset.incidentId || incNode.dataset.openIncident, 10);
state.selectedIncidentId = id;
if (cache) {
const view = patchDashboard(root, cache);
const tid = parseInt(root.querySelector(`[data-incident-id="${id}"]`)?.dataset.threadId, 10);
if (view.sel && tid) await openTaskThread(root, tid, view.sel.primary_agent);
else if (view.sel?.thread_id) await openTaskThread(root, view.sel.thread_id, view.sel.primary_agent);
}
return;
}
const tabBtn = ev.target.closest('.ao-mobile-tabs button');
if (tabBtn) {
state.mobileTab = tabBtn.dataset.aoTab;
if (tabBtn.dataset.aoTab === 'tasks') state.mainTab = 'tasks';
updateMobileTabs(root);
updateMainTabs(root);
return;
}
if (ev.target.closest('#ao-btn-send') || ev.target.closest('#ao-modal-btn-send')) {
await sendChatMessage(root);
}
});
}
function schedulePoll() {
if (state.pollTimer) clearInterval(state.pollTimer);
state.pollTimer = setInterval(() => {
if (document.hidden) return;
const view = document.getElementById('view-agentic-ops');
if (view && !view.hidden) renderAgenticOps({ poll: true });
}, 30000);
}
async function renderAgenticOps(options = {}) {
const el = document.getElementById('agentic-ops-content');
if (!el) return;
if (renderInFlight) {
if (options.poll) return;
pendingRender = { ...options, force: true };
return;
}
const soft = shellMounted && (options.poll || options.soft) && !options.force;
renderInFlight = true;
if (shellMounted && !el.querySelector('#ao-subnav')) {
shellMounted = false;
eventsBound = false;
}
if (!soft && !shellMounted) {
el.innerHTML = '';
}
if (!getToken()) {
shellMounted = false;
eventsBound = false;
el.innerHTML = 'Sessão não encontrada. Fazer login
';
renderInFlight = false;
return;
}
if (!soft && typeof ensureValidSession === 'function') {
const ok = await ensureValidSession();
if (!ok) {
shellMounted = false;
eventsBound = false;
el.innerHTML = 'Sessão expirada. Fazer login
';
renderInFlight = false;
return;
}
}
try {
const saved = soft ? preserveInputs(el) : null;
const data = await fetchDashboard();
cache = data;
try {
await fetchExtraData();
} catch {
/* extra panes degrade gracefully */
}
if (!shellMounted) {
mountShell(el);
bindEventsOnce(el);
}
const view = patchDashboard(el, data);
if (saved) restoreInputs(el, saved);
if (state.threadId && state.chatModalOpen) {
await loadThread(el, state.threadId, { silent: true });
} else if (state.threadId && !state.chatModalOpen && view.sel?.thread_id) {
await loadThread(el, view.sel.thread_id);
}
if (!state.pollTimer) schedulePoll();
} catch (err) {
if (!soft) el.innerHTML = `Erro: ${esc(err.message)}
`;
} finally {
renderInFlight = false;
if (pendingRender) {
const next = pendingRender;
pendingRender = null;
await renderAgenticOps(next);
}
}
}
document.addEventListener('keydown', (ev) => {
const view = document.getElementById('view-agentic-ops');
if (!view || view.hidden) return;
if (ev.key === 'r' && !ev.ctrlKey && !ev.metaKey) {
const t = ev.target;
if (t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA')) return;
ev.preventDefault();
renderAgenticOps({ force: true });
}
if (ev.key === 'Escape' && !state.chatModalOpen) {
state.selectedIncidentId = null;
state.threadId = null;
lastThreadHtml = '';
const el = document.getElementById('agentic-ops-content');
if (el && cache) patchDashboard(el, cache);
}
});
window.renderAgenticOps = renderAgenticOps;
})();