245 lines
7.1 KiB
JavaScript
245 lines
7.1 KiB
JavaScript
|
|
(function () {
|
||
|
|
const buffers = {};
|
||
|
|
let liveEs = null;
|
||
|
|
|
||
|
|
function esc(s) {
|
||
|
|
const d = document.createElement('div');
|
||
|
|
d.textContent = s || '';
|
||
|
|
return d.innerHTML;
|
||
|
|
}
|
||
|
|
|
||
|
|
function nowIso() {
|
||
|
|
return new Date().toISOString();
|
||
|
|
}
|
||
|
|
|
||
|
|
function lineClass(type) {
|
||
|
|
if (!type) return 'line-action';
|
||
|
|
if (type === 'command') return 'line-command';
|
||
|
|
if (type === 'output') return 'line-output';
|
||
|
|
if (String(type).indexOf('handoff') >= 0) return 'line-handoff';
|
||
|
|
if (type === 'error') return 'line-error';
|
||
|
|
if (String(type).indexOf('monitor') >= 0) return 'line-handoff';
|
||
|
|
if (String(type).indexOf('browse') >= 0) return 'line-action';
|
||
|
|
return 'line-action';
|
||
|
|
}
|
||
|
|
|
||
|
|
function formatLine(entry) {
|
||
|
|
const t = (entry.at || '').substring(11, 19) || (entry.at || '').substring(11, 16) || '--:--';
|
||
|
|
const msg = entry.message || entry.title || '';
|
||
|
|
const detail = entry.detail ? ' — ' + String(entry.detail).slice(0, 200) : '';
|
||
|
|
return (
|
||
|
|
'<div class="term-line ' +
|
||
|
|
lineClass(entry.type) +
|
||
|
|
'"><span class="term-ts">' +
|
||
|
|
esc(t) +
|
||
|
|
'</span><span class="term-msg">' +
|
||
|
|
esc(msg + detail) +
|
||
|
|
'</span></div>'
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
function render(key) {
|
||
|
|
const els = document.querySelectorAll('[data-term-key="' + key + '"]');
|
||
|
|
if (!els.length) return;
|
||
|
|
const lines = buffers[key] || [];
|
||
|
|
const html =
|
||
|
|
!lines.length
|
||
|
|
? '<div class="term-line term-idle"><span class="term-msg">Typ <code>help</code> voor commando\'s · Enter om uit te voeren</span></div>'
|
||
|
|
: lines.slice(-80).map(formatLine).join('');
|
||
|
|
els.forEach(function (el) {
|
||
|
|
el.innerHTML = html;
|
||
|
|
el.scrollTop = el.scrollHeight;
|
||
|
|
const win = el.closest('.agent-term-window');
|
||
|
|
if (win && lines.length) win.classList.add('is-live');
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function pushLine(key, entry, skipDup) {
|
||
|
|
if (!key || !entry) return;
|
||
|
|
key = String(key).toLowerCase();
|
||
|
|
if (!buffers[key]) buffers[key] = [];
|
||
|
|
if (skipDup && entry._id) {
|
||
|
|
const seen = buffers[key].some(function (l) {
|
||
|
|
return l._id === entry._id;
|
||
|
|
});
|
||
|
|
if (seen) return;
|
||
|
|
}
|
||
|
|
if (!entry.at) entry.at = nowIso();
|
||
|
|
buffers[key].push(entry);
|
||
|
|
if (buffers[key].length > 120) buffers[key].shift();
|
||
|
|
render(key);
|
||
|
|
}
|
||
|
|
|
||
|
|
function setBusy(key, busy) {
|
||
|
|
document.querySelectorAll('[data-term-input="' + key + '"]').forEach(function (input) {
|
||
|
|
input.disabled = !!busy;
|
||
|
|
input.classList.toggle('is-busy', !!busy);
|
||
|
|
});
|
||
|
|
document.querySelectorAll('[data-term-key="' + key + '"]').forEach(function (body) {
|
||
|
|
const win = body.closest('.agent-term-window');
|
||
|
|
if (win) win.classList.toggle('is-running', !!busy);
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
async function runCommand(key, cmd) {
|
||
|
|
key = String(key || '').toLowerCase();
|
||
|
|
cmd = (cmd || '').trim();
|
||
|
|
if (!cmd) return;
|
||
|
|
if (cmd.toLowerCase() === 'clear') {
|
||
|
|
buffers[key] = [];
|
||
|
|
render(key);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
pushLine(key, { type: 'command', message: '$ ' + cmd, at: nowIso() });
|
||
|
|
setBusy(key, true);
|
||
|
|
try {
|
||
|
|
const r = await fetch('/api/agents/souls/' + encodeURIComponent(key) + '/terminal/command', {
|
||
|
|
method: 'POST',
|
||
|
|
headers: { 'Content-Type': 'application/json' },
|
||
|
|
body: JSON.stringify({ command: cmd }),
|
||
|
|
});
|
||
|
|
const data = await r.json().catch(function () {
|
||
|
|
return {};
|
||
|
|
});
|
||
|
|
if (!r.ok) {
|
||
|
|
const err =
|
||
|
|
(typeof data.detail === 'string' && data.detail) ||
|
||
|
|
(Array.isArray(data.detail) && data.detail[0]?.msg) ||
|
||
|
|
'Commando mislukt';
|
||
|
|
pushLine(key, { type: 'error', message: err, at: nowIso() });
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
(data.lines || []).forEach(function (line) {
|
||
|
|
pushLine(key, {
|
||
|
|
type: line.type || 'output',
|
||
|
|
message: line.message || '',
|
||
|
|
detail: line.detail || '',
|
||
|
|
at: nowIso(),
|
||
|
|
});
|
||
|
|
});
|
||
|
|
} catch (e) {
|
||
|
|
pushLine(key, { type: 'error', message: e.message || 'Netwerkfout', at: nowIso() });
|
||
|
|
} finally {
|
||
|
|
setBusy(key, false);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
function bindInputs() {
|
||
|
|
document.querySelectorAll('[data-term-input]').forEach(function (input) {
|
||
|
|
if (input._termBound) return;
|
||
|
|
input._termBound = true;
|
||
|
|
const key = input.getAttribute('data-term-input');
|
||
|
|
input.addEventListener('keydown', function (e) {
|
||
|
|
if (e.key === 'Enter') {
|
||
|
|
e.preventDefault();
|
||
|
|
e.stopPropagation();
|
||
|
|
const val = input.value;
|
||
|
|
input.value = '';
|
||
|
|
runCommand(key, val);
|
||
|
|
}
|
||
|
|
});
|
||
|
|
input.addEventListener('click', function (e) {
|
||
|
|
e.stopPropagation();
|
||
|
|
});
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
async function loadHistory(key) {
|
||
|
|
try {
|
||
|
|
const r = await fetch('/api/agents/souls/' + encodeURIComponent(key) + '/events?limit=30');
|
||
|
|
if (!r.ok) return;
|
||
|
|
const data = await r.json();
|
||
|
|
buffers[key] = (data.items || []).map(function (ev) {
|
||
|
|
const meta = ev.metadata || {};
|
||
|
|
let type = 'action';
|
||
|
|
const et = ev.event_type || '';
|
||
|
|
if (et.indexOf('handoff') >= 0) type = 'handoff_out';
|
||
|
|
if (meta.target_agent) type = 'handoff_out';
|
||
|
|
if (meta.source_agent) type = 'handoff_in';
|
||
|
|
if (et.indexOf('monitor') >= 0) type = 'monitor';
|
||
|
|
if (et.indexOf('browse') >= 0) type = 'browse';
|
||
|
|
if (et === 'terminal_in') type = 'command';
|
||
|
|
if (et === 'terminal_out' || et === 'terminal_note') type = 'output';
|
||
|
|
return {
|
||
|
|
_id: ev.id,
|
||
|
|
type: type,
|
||
|
|
message: ev.title || ev.event_type,
|
||
|
|
detail: ev.body,
|
||
|
|
at: ev.created_at,
|
||
|
|
};
|
||
|
|
});
|
||
|
|
render(key);
|
||
|
|
} catch (e) {}
|
||
|
|
}
|
||
|
|
|
||
|
|
function handleLivePayload(d) {
|
||
|
|
if (!d || d.type === 'connected' || d.type === 'error') return;
|
||
|
|
const key = (d.agent || '').toLowerCase();
|
||
|
|
if (!key) return;
|
||
|
|
pushLine(
|
||
|
|
key,
|
||
|
|
{
|
||
|
|
_id: d.id,
|
||
|
|
type: d.type,
|
||
|
|
message: d.message,
|
||
|
|
detail: d.detail,
|
||
|
|
at: d.at,
|
||
|
|
},
|
||
|
|
true
|
||
|
|
);
|
||
|
|
document.querySelectorAll('[data-term-key="' + key + '"]').forEach(function (el) {
|
||
|
|
const win = el.closest('.agent-term-window');
|
||
|
|
if (win) win.classList.add('is-live');
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
function startLiveStream() {
|
||
|
|
if (liveEs || typeof EventSource === 'undefined') return;
|
||
|
|
liveEs = new EventSource('/api/agents/live/stream');
|
||
|
|
liveEs.onmessage = function (ev) {
|
||
|
|
try {
|
||
|
|
handleLivePayload(JSON.parse(ev.data));
|
||
|
|
} catch (e) {}
|
||
|
|
};
|
||
|
|
liveEs.onerror = function () {
|
||
|
|
/* EventSource auto-reconnects */
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function stopLiveStream() {
|
||
|
|
if (liveEs) {
|
||
|
|
try {
|
||
|
|
liveEs.close();
|
||
|
|
} catch (e) {}
|
||
|
|
liveEs = null;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
async function mountAll() {
|
||
|
|
const nodes = document.querySelectorAll('[data-term-key]');
|
||
|
|
const keys = [];
|
||
|
|
nodes.forEach(function (n) {
|
||
|
|
const k = n.getAttribute('data-term-key');
|
||
|
|
if (k && keys.indexOf(k) < 0) keys.push(k);
|
||
|
|
});
|
||
|
|
await Promise.all(keys.map(loadHistory));
|
||
|
|
bindInputs();
|
||
|
|
startLiveStream();
|
||
|
|
}
|
||
|
|
|
||
|
|
function stopAll() {
|
||
|
|
stopLiveStream();
|
||
|
|
}
|
||
|
|
|
||
|
|
window.AgentsTerminals = {
|
||
|
|
mountAll,
|
||
|
|
stopAll,
|
||
|
|
pushLine,
|
||
|
|
render,
|
||
|
|
runCommand,
|
||
|
|
bindInputs,
|
||
|
|
startLiveStream,
|
||
|
|
stopLiveStream,
|
||
|
|
};
|
||
|
|
})();
|