// The assistant: a round button in the bottom right corner opens a chat. Each
// question goes with what the panel shows - the tabs, the active pane, the
// text of its terminal or its list of files - and the answer streams in.
// Commands the model suggests come as ```run``` blocks with «Выполнить»: a
// click sends the command to the active terminal, with the user's rights.
// Off until the user turns it on in the profile.
(function () {
'use strict';
const PLANE = '';
const STOP = '';
const esc = v => String(v).replace(/[&<>"]/g, c => ({'&': '&', '<': '<', '>': '>', '"': '"'}[c]));
const fab = document.createElement('button');
fab.type = 'button';
fab.className = 'ai-fab';
fab.hidden = true;
fab.title = 'ИИ-ассистент';
fab.setAttribute('aria-label', 'ИИ-ассистент');
fab.innerHTML = '';
const box = document.createElement('div');
box.className = 'ai-box';
box.hidden = true;
box.innerHTML =
'
Ассистент' +
'' +
'
' +
'
Спросите о том, что открыто в панели: ассистент видит вкладки и активный терминал или папку. Команды он предложит кнопкой «Выполнить».
' +
'';
document.body.append(fab, box);
const q = s => box.querySelector(s);
const list = q('.ai-list'), input = q('textarea'), send = q('.ai-send');
const talk = []; // {role, content}
let ac = null;
function setOn(on) { fab.hidden = !on; if (!on) box.hidden = true; }
fetch('/api/panel/ai', {cache: 'no-store', credentials: 'same-origin'}).then(r => r.json()).then(d => setOn(!!d.on)).catch(() => {});
document.addEventListener('lpra:ai', ev => setOn(ev.detail));
fab.onclick = () => { box.hidden = !box.hidden; if (!box.hidden) { where(); input.focus(); } };
q('.ai-close').onclick = () => { box.hidden = true; };
q('.ai-clear').onclick = () => {
if (ac) ac.abort();
talk.length = 0;
list.querySelectorAll('.ai-msg').forEach(m => m.remove());
q('.ai-hello').hidden = false;
};
// ---------- what the panel shows ----------
const W = () => window.Workspace;
function termText(p, lines) {
const b = p.term && p.term.buffer && p.term.buffer.active;
if (!b) return '';
const out = [];
for (let i = Math.max(0, b.length - lines); i < b.length; i++) {
const l = b.getLine(i);
if (l) out.push(l.translateToString(true));
}
while (out.length && !out[out.length - 1].trim()) out.pop();
return out.join('\n');
}
function paneLine(p) {
const kind = {term: 'терминал', ssh: 'SSH-терминал', files: 'файлы', sshfiles: 'файлы по SSH'}[p.kind] || p.kind;
return kind + ' · ' + (p.tooltip ? p.tooltip() : p.title()) + ' (машина ' + (p.agent ? p.agent.name : '?') + ')';
}
function context() {
const view = document.querySelector('main#app').dataset.view;
const out = ['Страница: ' + ({machines: 'Машины', terminal: 'WorkSpace', settings: 'Настройки'}[view] || view)];
const ws = W();
if (!ws) return out.join('\n');
const panes = ws.panes(), act = ws.activePane();
out.push('Панели (' + panes.length + '):');
panes.forEach(p => out.push((p === act ? '* ' : '- ') + paneLine(p)));
if (act && act.term) {
out.push('', 'Активный терминал, последние строки:', '```', termText(act, 120), '```');
} else if (act && act.entries) {
out.push('', 'Активная файловая панель: ' + act.cwd + (act.selected.size ? ' · выбрано: ' + [...act.selected].join(', ') : ''));
out.push(act.entries.slice(0, 200).map(e => (e.is_dir ? e.name + '/' : e.name + ' (' + e.size + ' B)')).join('\n'));
}
return out.join('\n');
}
function where() {
const act = W() && W().activePane();
q('.ai-where').textContent = act ? '· ' + (act.agent ? act.agent.name : '') : '';
}
// ---------- messages ----------
// render: text with ``` blocks; ```run``` ones get «Выполнить».
function render(el, text, done) {
const parts = text.split(/```/);
let html = '';
parts.forEach((part, i) => {
if (i % 2 === 0) { html += '
' + esc(part).replace(/\n/g, ' ') + '
'; return; }
const m = /^(\w*)\n?([\s\S]*)$/.exec(part);
const lang = m[1], code = m[2].replace(/\n$/, '');
const closed = i < parts.length - 1 || done;
html += '