/** Read-only browser UI: shows what bridge already holds (no writes). */
export const DEBUG_CHATS_PAGE_HTML = `<!DOCTYPE html>
<html lang="ru">
<head>
  <meta charset="utf-8" />
  <meta name="viewport" content="width=device-width, initial-scale=1" />
  <title>Cursor Connect — bridge data</title>
  <style>
    :root { color-scheme: dark; font-family: system-ui, sans-serif; font-size: 14px; }
    * { box-sizing: border-box; }
    body { margin: 0; background: #111; color: #e8e8e8; }
    header { padding: 10px 14px; border-bottom: 1px solid #333; display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
    header h1 { font-size: 15px; margin: 0 12px 0 0; font-weight: 600; }
    label { font-size: 12px; color: #aaa; }
    input, select, button { background: #1c1c1c; border: 1px solid #444; color: #eee; border-radius: 6px; padding: 6px 8px; }
    button { cursor: pointer; }
    button:hover { border-color: #666; }
    .layout { display: grid; grid-template-columns: 260px 1fr; min-height: calc(100vh - 52px); }
    .agents { border-right: 1px solid #333; overflow: auto; max-height: calc(100vh - 52px); }
    .agents button { display: block; width: 100%; text-align: left; border: none; border-bottom: 1px solid #2a2a2a; border-radius: 0; padding: 10px 12px; background: transparent; }
    .agents button:hover { background: #1a1a1a; }
    .agents button.active { background: #243044; }
    .agents .meta { font-size: 11px; color: #888; margin-top: 2px; }
    main { display: flex; flex-direction: column; min-width: 0; }
    .tabs { display: flex; gap: 4px; padding: 8px 10px; border-bottom: 1px solid #333; flex-wrap: wrap; }
    .tabs button { font-size: 12px; }
    .tabs button.active { background: #2a3f5f; border-color: #5a7ab0; }
    .status { padding: 6px 12px; font-size: 12px; color: #9ab; border-bottom: 1px solid #2a2a2a; }
    .status.err { color: #f88; }
    .chat { flex: 1; overflow: auto; padding: 12px; display: flex; flex-direction: column; gap: 10px; }
    .bubble { max-width: 92%; padding: 8px 12px; border-radius: 10px; white-space: pre-wrap; word-break: break-word; }
    .bubble.user { align-self: flex-end; background: #2d4a3e; }
    .bubble.assistant { align-self: flex-start; background: #2a2a35; }
    .bubble.system { align-self: center; background: #333; font-size: 12px; color: #bbb; }
    .bubble .tag { font-size: 10px; color: #888; margin-bottom: 4px; font-family: ui-monospace, monospace; }
    .bubble.segment-jsonl { border: 1px solid #2a4a38; }
    .bubble.segment-dom { border: 1px solid #4a3a5a; box-shadow: 0 0 0 1px #3a2a4a inset; }
    .lenta-head { font-size: 12px; color: #9ab; margin-bottom: 10px; line-height: 1.45; }
    .lenta-divider { align-self: center; font-size: 11px; color: #6a8a9a; padding: 4px 0; letter-spacing: 0.02em; }
    .raw { flex: 1; overflow: auto; margin: 0; padding: 12px; font-size: 11px; line-height: 1.4; background: #0a0a0a; color: #bdbdbd; }
    .empty { color: #666; padding: 24px; text-align: center; }
    .note { font-size: 12px; color: #9ab; padding: 8px 12px; border-bottom: 1px solid #2a2a2a; line-height: 1.45; }
    table.cmp { width: 100%; border-collapse: collapse; font-size: 12px; }
    table.cmp th, table.cmp td { border: 1px solid #333; padding: 6px 8px; vertical-align: top; }
    table.cmp th { background: #1a1a1a; text-align: left; }
    table.cmp tr.match td { background: #152515; }
    table.cmp tr.miss td { background: #2a1818; }
    table.cmp tr.domonly td { background: #1a1a2a; }
    .jsonl-live-head { font-size: 12px; color: #9ab; margin-bottom: 10px; line-height: 1.45; }
    .jsonl-row { border-left: 3px solid #444; padding: 8px 10px; margin-bottom: 6px; background: #161616; border-radius: 4px; }
    .jsonl-row.new { animation: jsonl-flash 1.2s ease; border-left-color: #5a9fd4; }
    .jsonl-row.in-lenta { border-left-color: #3d7a52; }
    .jsonl-row.skip { border-left-color: #8a4040; }
    .jsonl-row .meta { font-size: 10px; color: #888; font-family: ui-monospace, monospace; margin-bottom: 4px; }
    .jsonl-row .body { white-space: pre-wrap; word-break: break-word; font-size: 13px; }
    @keyframes jsonl-flash { from { background: #243044; } to { background: #161616; } }
    @media (max-width: 720px) { .layout { grid-template-columns: 1fr; } .agents { max-height: 180px; } }
  </style>
</head>
<body>
  <header>
    <h1>Bridge data (read-only)</h1>
    <label>Token <input id="token" type="password" size="28" placeholder="если WEBAPP_PASSWORD" /></label>
    <label>Refresh <input id="interval" type="number" min="0" max="60" value="2" style="width:48px" /> s</label>
    <button type="button" id="btnRefresh">Обновить</button>
    <span id="health" style="font-size:12px;color:#8a8"></span>
  </header>
  <div class="layout">
    <nav class="agents" id="agents"><div class="empty">Загрузка списка…</div></nav>
    <main>
      <div class="tabs" id="tabs">
        <button type="button" data-tab="lenta" class="active">Лента · JSONL+DOM</button>
        <button type="button" data-tab="history">JSONL · /api/agents/history</button>
        <button type="button" data-tab="jsonl-live">JSONL live · файл</button>
        <button type="button" data-tab="cache">JSONL cache</button>
        <button type="button" data-tab="transcript">DOM transcript</button>
        <button type="button" data-tab="dom">DOM · state.messages</button>
        <button type="button" data-tab="compare">Сравнение JSONL ↔ DOM</button>
        <button type="button" data-tab="debug">DOM extract debug</button>
        <button type="button" data-tab="raw">JSON snapshot</button>
      </div>
      <div class="status" id="status">—</div>
      <div class="note" id="note" hidden></div>
      <div class="chat" id="panel"></div>
      <pre class="raw" id="raw" hidden></pre>
    </main>
  </div>
  <script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
  <script>
(function () {
  const params = new URLSearchParams(location.search);
  const roomId = params.get('roomId') || '';
  const tokenEl = document.getElementById('token');
  tokenEl.value = params.get('token') || localStorage.getItem('cc_debug_token') || '';
  tokenEl.addEventListener('change', () => localStorage.setItem('cc_debug_token', tokenEl.value.trim()));

  let tab = 'lenta';
  let selectedId = params.get('agentId') || '';
  let selectedTitle = '';
  let snapshot = null;
  let timer = null;
  let jsonlLiveTimer = null;
  let jsonlLiveLastLine = 0;
  let jsonlLiveRows = [];
  let jsonlLiveStickTop = true;
  let jsonlLiveFile = '';
  let bridgeSocket = null;
  let bridgeSubscribedId = '';

  document.getElementById('tabs').addEventListener('click', (e) => {
    const b = e.target.closest('button[data-tab]');
    if (!b) return;
    tab = b.dataset.tab;
    document.querySelectorAll('#tabs button').forEach((x) => x.classList.toggle('active', x === b));
    scheduleJsonlLive();
    if (tab === 'jsonl-live') void resetJsonlLive();
    renderPanel();
  });
  document.getElementById('btnRefresh').addEventListener('click', () => void loadAll());
  document.getElementById('interval').addEventListener('change', schedule);

  function token() { return tokenEl.value.trim(); }
  function apiUrl(path) {
    const u = new URL(path, location.origin);
    if (roomId) u.searchParams.set('roomId', roomId);
    const t = token();
    if (t) u.searchParams.set('token', t);
    return u.toString();
  }
  async function apiFetch(path) {
    const headers = {};
    const t = token();
    if (t) headers.Authorization = 'Bearer ' + t;
    return fetch(apiUrl(path), { headers });
  }
  function setStatus(msg, err) {
    const el = document.getElementById('status');
    el.textContent = msg;
    el.className = 'status' + (err ? ' err' : '');
  }

  function roleLabel(role) {
    if (role === 'user') return 'Пользователь';
    if (role === 'assistant') return 'Ассистент';
    return role || 'system';
  }
  function normText(text, role) {
    let t = String(text || '').replace(/\s+/g, ' ').trim().toLowerCase();
    if (role === 'assistant') {
      t = t.replace(/^#+\\s*/gm, '').replace(/\\|/g, ' ').replace(/[-]{3,}/g, ' ');
    }
    return t.replace(/\s+/g, ' ').trim();
  }
  /** DOM textContent часто рвёт URL: «http:// 127. 0. 0.1» vs JSONL «http://127.0.0.1». */
  function compactMatchText(text, role) {
    return normText(text, role).replace(/\s/g, '');
  }
  function textsMatch(a, b, role) {
    const ta = normText(a, role);
    const tb = normText(b, role);
    if (!ta || !tb) return false;
    if (ta === tb) return true;
    const ca = compactMatchText(a, role);
    const cb = compactMatchText(b, role);
    if (ca && cb && ca === cb) return true;
    const shortC = ca.length <= cb.length ? ca : cb;
    const longC = ca.length <= cb.length ? cb : ca;
    if (shortC.length >= 12 && longC.includes(shortC) && shortC.length / longC.length >= 0.35) {
      return true;
    }
    const short = ta.length <= tb.length ? ta : tb;
    const long = ta.length <= tb.length ? tb : ta;
    return short.length >= 12 && long.includes(short) && short.length / long.length >= 0.35;
  }
  function renderBubbles(messages, source) {
    if (!messages || !messages.length) return '<div class="empty">Нет сообщений</div>';
    return [...messages].reverse().map((m) => {
      const role = m.role || 'system';
      const tag =
        roleLabel(role) +
        ' · ' +
        source +
        ' · ' +
        (m.id || '?') +
        (m.flatIndex != null ? ' · flat=' + m.flatIndex : '') +
        (m.html ? ' · html' : '');
      const text = (m.text || '').trim() || (m.html ? '[html ' + m.html.length + ' chars — в Cursor с разметкой]' : '(пусто)');
      return '<div class="bubble ' + role + '"><div class="tag">' + escapeHtml(tag) + '</div>' + escapeHtml(text) + '</div>';
    }).join('');
  }
  function resolveLentaForAgent() {
    const snap = snapshot?.lentaByAgent?.[selectedId];
    const api = window.__lentaData;
    const messages =
      (snap?.messages?.length ? snap.messages : null) ||
      (api?.messages?.length ? api.messages : null) ||
      snapshot?.displayMessages?.[selectedId] ||
      [];
    const jsonlHistory = snap?.jsonlHistory ?? api?.jsonlHistory ?? [];
    const domOverlay = snap?.domOverlay ?? api?.domOverlay ?? [];
    const source = snap?.source ?? api?.source ?? (domOverlay.length ? 'hybrid' : 'jsonl');
    const jsonlRowCount = snap?.jsonlRowCount ?? api?.jsonlRowCount ?? jsonlHistory.length;
    const totalMessages = api?.totalMessages ?? messages.length;
    const payloadSeq = api?.payloadSeq ?? snapshot?.lentaPayloadSeq?.[selectedId];
    return { messages, jsonlHistory, domOverlay, source, jsonlRowCount, totalMessages, payloadSeq };
  }
  function renderLentaFeed(lenta) {
    const messages = lenta.messages || [];
    if (!messages.length) {
      return '<div class="empty">Нет ленты — откройте чат в Cursor или дождитесь JSONL</div>';
    }
    const jsonlLen = (lenta.jsonlHistory || []).length;
    const domN = (lenta.domOverlay || []).length;
    const head =
      '<div class="lenta-head">Как в app (<code>agent:messages</code>): <b>' +
      messages.length +
      '</b> пузырей · JSONL <b>' +
      jsonlLen +
      '</b> · DOM live <b>' +
      domN +
      '</b> · source=<b>' +
      escapeHtml(lenta.source || '?') +
      '</b>' +
      (lenta.totalMessages > messages.length
        ? ' · в файле всего ' + lenta.totalMessages
        : '') +
      (lenta.payloadSeq != null ? ' · <b>emitSeq=' + lenta.payloadSeq + '</b>' : '') +
      ' · новые сверху · <span style="opacity:0.85">flat=ключ порядка строки, не emitSeq</span></div>';
    const parts = [];
    for (let i = messages.length - 1; i >= 0; i--) {
      const m = messages[i];
      const isDom = i >= jsonlLen;
      if (i === jsonlLen - 1 && domN > 0 && jsonlLen > 0) {
        parts.push('<div class="lenta-divider">── JSONL archive ──</div>');
      }
      const role = m.role || 'system';
      const seg = isDom ? 'DOM live' : 'JSONL';
      const tag =
        roleLabel(role) +
        ' · ' +
        seg +
        ' · ' +
        (m.id || '?') +
        (m.flatIndex != null ? ' · flat=' + m.flatIndex : '') +
        (m.html ? ' · html' : '');
      const text =
        (m.text || '').trim() ||
        (m.html ? '[html ' + m.html.length + ' chars — в Cursor с разметкой]' : '(пусто)');
      parts.push(
        '<div class="bubble ' +
          role +
          ' segment-' +
          (isDom ? 'dom' : 'jsonl') +
          '"><div class="tag">' +
          escapeHtml(tag) +
          '</div>' +
          escapeHtml(text) +
          '</div>'
      );
    }
    return head + parts.join('');
  }
  function renderCompare(hist, dom) {
    const rows = [];
    const usedDom = new Set();
    for (let i = 0; i < (hist || []).length; i++) {
      const h = hist[i];
      let domIdx = -1;
      // DOM viewport = весь смонтированный чат (tool-строки, статусы); совпадение часто в хвосте.
      for (let j = (dom || []).length - 1; j >= 0; j--) {
        if (usedDom.has(j)) continue;
        if (h.role === dom[j].role && textsMatch(h.text, dom[j].text, h.role)) {
          domIdx = j;
          usedDom.add(j);
          break;
        }
      }
      const cls = domIdx >= 0 ? 'match' : 'miss';
      rows.push({ cls, n: i + 1, h, d: domIdx >= 0 ? dom[domIdx] : null });
    }
    for (let j = 0; j < (dom || []).length; j++) {
      if (usedDom.has(j)) continue;
      rows.push({ cls: 'domonly', n: '—', h: null, d: dom[j] });
    }
    if (!rows.length) return '<div class="empty">Нет данных</div>';
    const tr = rows.map((r) => {
      const hp = r.h ? escapeHtml((r.h.text || '').slice(0, 120)) : '—';
      const dp = r.d ? escapeHtml((r.d.text || '').slice(0, 120)) : '—';
      return '<tr class="' + r.cls + '"><td>' + r.n + '</td><td>' + (r.h ? r.h.role : '') + '</td><td>' + hp + '</td><td>' + (r.d ? r.d.role : '') + '</td><td>' + dp + '</td></tr>';
    }).join('');
    return '<table class="cmp"><thead><tr><th>#</th><th>JSONL</th><th>текст JSONL</th><th>DOM</th><th>текст DOM</th></tr></thead><tbody>' + tr + '</tbody></table>';
  }
  function escapeHtml(s) {
    return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
  }

  function renderJsonlLive() {
    if (!jsonlLiveRows.length) {
      return '<div class="empty">Нет строк — выберите чат или дождитесь записи в JSONL</div>';
    }
    const head =
      '<div class="jsonl-live-head">Файл: <code>' +
      escapeHtml(jsonlLiveFile || '—') +
      '</code> · строк в файле: <b>' +
      jsonlLiveLastLine +
      '</b> · показано: ' +
      jsonlLiveRows.length +
      ' · опрос <b>300 ms</b> · bridge subscribe: <b>' +
      escapeHtml(bridgeSubscribedId === selectedId ? 'on' : 'off') +
      '</b></div>';
    const body = [...jsonlLiveRows]
      .reverse()
      .map((r) => {
        const cls =
          'jsonl-row' +
          (r._new ? ' new' : '') +
          (r.inLenta ? ' in-lenta' : ' skip');
        const tools = r.tools?.length ? ' · tools: ' + r.tools.join(', ') : '';
        const skip = r.inLenta ? '→ в ленту' : '→ skip: ' + (r.skipReason || '?');
        return (
          '<div class="' +
          cls +
          '" data-line="' +
          r.lineNo +
          '"><div class="meta">#' +
          r.lineNo +
          ' · ' +
          escapeHtml(r.role) +
          tools +
          ' · ' +
          escapeHtml(skip) +
          (r._at ? ' · ' + r._at : '') +
          '</div><div class="body">' +
          escapeHtml(r.textPreview || '(пусто)') +
          '</div></div>'
        );
      })
      .join('');
    return head + body;
  }

  async function pollJsonlLive() {
    if (!selectedId || tab !== 'jsonl-live') return;
    try {
      const q = new URLSearchParams();
      q.set('agentId', selectedId);
      if (selectedTitle) q.set('title', selectedTitle);
      if (jsonlLiveLastLine > 0) q.set('afterLine', String(jsonlLiveLastLine));
      else q.set('tail', '80');
      const res = await apiFetch('/debug/jsonl-live?' + q.toString());
      if (!res.ok) throw new Error('jsonl-live ' + res.status);
      const data = await res.json();
      jsonlLiveFile = data.filePath || '';
      const at = new Date().toLocaleTimeString();
      const incoming = (data.rows || []).map((r) => ({ ...r, _new: true, _at: at }));
      if (jsonlLiveLastLine === 0) {
        jsonlLiveRows = incoming;
      } else if (incoming.length) {
        const seen = new Set(jsonlLiveRows.map((x) => x.lineNo));
        for (const r of incoming) {
          if (!seen.has(r.lineNo)) jsonlLiveRows.push(r);
        }
      }
      if (data.totalLines) jsonlLiveLastLine = data.totalLines;
      else if (incoming.length) {
        jsonlLiveLastLine = Math.max(jsonlLiveLastLine, incoming[incoming.length - 1].lineNo);
      }
      if (jsonlLiveRows.length > 600) jsonlLiveRows = jsonlLiveRows.slice(-500);
      setTimeout(() => {
        for (const r of jsonlLiveRows) r._new = false;
      }, 1200);
      if (tab === 'jsonl-live') {
        renderPanel();
        if (jsonlLiveStickTop) {
          const panel = document.getElementById('panel');
          panel.scrollTop = 0;
        }
        setStatus(
          'JSONL live: ' +
            jsonlLiveRows.length +
            ' shown · file lines ' +
            (data.totalLines ?? '?') +
            ' · ' +
            at
        );
      }
    } catch (e) {
      setStatus((e && e.message) || String(e), true);
    }
  }

  async function resetJsonlLive() {
    jsonlLiveLastLine = 0;
    jsonlLiveRows = [];
    jsonlLiveFile = '';
    await pollJsonlLive();
  }

  function ensureBridgeSubscribe() {
    if (typeof io === 'undefined') return;
    if (!bridgeSocket) {
      const headers = {};
      const t = token();
      if (t) headers.Authorization = 'Bearer ' + t;
      bridgeSocket = io(location.origin, { transports: ['websocket', 'polling'], auth: headers });
      bridgeSocket.on('agent:messages', (p) => {
        const n = p.historyMessages?.length ?? 0;
        const tail = (p.historyMessages || [])[(n || 1) - 1];
        const preview = (tail?.text || '').slice(0, 48);
        setStatus(
          'app-pipe agent:messages append=' +
            !!p.append +
            ' n=' +
            n +
            ' · ' +
            preview
        );
      });
    }
    if (!selectedId || (bridgeSubscribedId === selectedId && tab === 'jsonl-live')) return;
    bridgeSubscribedId = selectedId;
    bridgeSocket.emit('agents:subscribe', {
      agentId: selectedId,
      title: selectedTitle || undefined,
      focus: false,
    });
    setStatus('bridge subscribe ' + selectedId.slice(0, 8) + '… (как приложение)');
  }

  function scheduleJsonlLive() {
    if (jsonlLiveTimer) clearInterval(jsonlLiveTimer);
    jsonlLiveTimer = null;
    if (tab === 'jsonl-live') {
      ensureBridgeSubscribe();
      jsonlLiveTimer = setInterval(() => void pollJsonlLive(), 300);
    }
  }

  function renderPanel() {
    const panel = document.getElementById('panel');
    const raw = document.getElementById('raw');
    panel.hidden = tab === 'raw';
    raw.hidden = tab !== 'raw';
    if (tab === 'raw') {
      raw.textContent = JSON.stringify({ snapshot, selectedId, tab }, null, 2);
      return;
    }
    const note = document.getElementById('note');
    note.hidden = tab !== 'lenta' && tab !== 'dom' && tab !== 'compare' && tab !== 'jsonl-live';
    if (tab === 'lenta') {
      note.textContent =
        'Готовая лента: [...JSONL archive, ...DOM overlay] — тот же compose, что уходит в приложение. Зелёная рамка = JSONL, фиолетовая = DOM (ещё не в .jsonl). Обновляется по таймеру и после «Обновить».';
    } else if (tab === 'jsonl-live') {
      note.textContent =
        'Файл .jsonl на диске (опрос 300 ms). «→ в ленту» = после filter в bridge. В приложение строка попадёт только если Connect открыл этот чат (ниже: bridge subscribe). Debug сам по себе app не кормит.';
    } else if (tab === 'dom') {
      note.textContent = 'DOM = смонтированные [data-flat-index] в Cursor (без JSONL). При открытии чата bridge прокручивает ленту вниз и poll. Пропуски = tool/thought строки.';
    } else if (tab === 'compare') {
      note.textContent =
        'JSONL = archive из файла; DOM = весь viewport Cursor (включая tool/статусы). Сопоставление: с хвоста DOM + сравнение текста без пробелов (URL из textContent). Зелёная = пара найдена. Красная = только JSONL. Фиолетовая = только DOM (нет пары в archive или другая поверхность текста).';
    }
    if (!selectedId && tab !== 'dom' && tab !== 'debug') {
      panel.innerHTML = '<div class="empty">Выберите агента слева</div>';
      return;
    }
    if (tab === 'lenta') {
      panel.innerHTML = renderLentaFeed(resolveLentaForAgent());
      panel.onscroll = null;
      return;
    }
    if (tab === 'jsonl-live') {
      panel.innerHTML = renderJsonlLive();
      panel.onscroll = () => {
        jsonlLiveStickTop = panel.scrollTop < 80;
      };
      return;
    }
    panel.onscroll = null;
    if (tab === 'history') {
      panel.innerHTML = window.__historyHtml || '<div class="empty">Загрузка history…</div>';
    } else if (tab === 'cache') {
      const rows = snapshot?.displayCache?.[selectedId];
      panel.innerHTML = renderBubbles(rows, 'jsonl-cache');
    } else if (tab === 'transcript') {
      const rows =
        snapshot?.domOverlay?.[selectedId] ||
        snapshot?.lentaByAgent?.[selectedId]?.domOverlay ||
        snapshot?.domTranscript?.[selectedId];
      panel.innerHTML = renderBubbles(rows, 'dom-overlay');
    } else if (tab === 'dom') {
      const st = snapshot?.cursor;
      const active = st?.activeComposerId;
      const hint = active ? 'activeComposerId=' + active : 'нет activeComposerId';
      panel.innerHTML = '<div class="empty" style="text-align:left;margin-bottom:8px">' + escapeHtml(hint) + ' · ' + (st?.domMessageCount ?? 0) + ' msgs</div>' + renderBubbles(st?.domMessages, 'dom');
    } else if (tab === 'compare') {
      const hist = window.__historyData || [];
      const dom = snapshot?.cursor?.domMessages || [];
      const active = snapshot?.cursor?.activeComposerId;
      const same = selectedId === active;
      const head = '<div class="empty" style="text-align:left">agent=' + escapeHtml(selectedId.slice(0, 8)) + '… active=' + escapeHtml((active || '—').slice(0, 8)) + (same ? ' (совпадает)' : ' (другой чат в Cursor!)') + '</div>';
      panel.innerHTML = head + renderCompare(hist, dom);
    } else if (tab === 'debug') {
      const d = snapshot?.domExtractDebug?.latest;
      if (!d) { panel.innerHTML = '<div class="empty">Нет extract debug</div>'; return; }
      panel.innerHTML = '<div class="bubble system"><div class="tag">extract debug</div>' + escapeHtml(JSON.stringify(d, null, 2)) + '</div>';
    }
  }

  function renderAgents(index) {
    const nav = document.getElementById('agents');
    const repos = index?.repos || [];
    const items = [];
    for (const repo of repos) {
      for (const a of repo.agents || []) {
        items.push({ id: a.id, title: a.title, repo: repo.name });
      }
    }
    if (!items.length) {
      nav.innerHTML = '<div class="empty">agents:index пуст</div>';
      return;
    }
    nav.innerHTML = items.map((a) => {
      const cls = a.id === selectedId ? ' active' : '';
      return '<button type="button" class="' + cls.trim() + '" data-id="' + escapeHtml(a.id) + '" data-title="' + escapeHtml(a.title || '') + '">' + escapeHtml(a.title || a.id) + '<div class="meta">' + escapeHtml(a.repo) + ' · ' + escapeHtml(a.id.slice(0, 8)) + '…</div></button>';
    }).join('');
    nav.querySelectorAll('button[data-id]').forEach((btn) => {
      btn.addEventListener('click', () => {
        selectedId = btn.dataset.id;
        selectedTitle = btn.dataset.title || '';
        nav.querySelectorAll('button').forEach((x) => x.classList.toggle('active', x.dataset.id === selectedId));
        void loadHistory();
        if (tab === 'jsonl-live') void resetJsonlLive();
        renderPanel();
      });
    });
    if (!selectedId && items[0]) {
      selectedId = items[0].id;
      selectedTitle = items[0].title || '';
      nav.querySelector('button[data-id="' + selectedId + '"]')?.classList.add('active');
    }
  }

  async function loadSnapshot() {
    const res = await apiFetch('/debug/chat-snapshot');
    if (!res.ok) throw new Error('chat-snapshot ' + res.status);
    snapshot = await res.json();
    const h = document.getElementById('health');
    h.textContent = 'CDP ' + (snapshot.health?.cdp ? 'on' : 'off') + ' · updated ' + new Date(snapshot.at).toLocaleTimeString();
  }

  async function loadHistory() {
    if (!selectedId) return;
    const res = await apiFetch('/api/agents/history?agentId=' + encodeURIComponent(selectedId) + '&limit=120');
    if (!res.ok) throw new Error('history ' + res.status);
    const data = await res.json();
    window.__historyData = data.historyMessages || data.messages || [];
    window.__historyHtml = renderBubbles(window.__historyData, 'jsonl');
    window.__lentaData = {
      messages: data.messages || [],
      jsonlHistory: data.historyMessages || [],
      domOverlay: data.liveMessages || [],
      source: data.source,
      jsonlRowCount: (data.historyMessages || []).length,
      totalMessages: data.totalMessages,
      payloadSeq: data.seq,
    };
    if (tab === 'lenta' || tab === 'history' || tab === 'compare') renderPanel();
    const l = window.__lentaData;
    setStatus(
      'history: jsonl ' +
        (l.jsonlHistory?.length ?? 0) +
        ' + dom ' +
        (l.domOverlay?.length ?? 0) +
        ' → lenta ' +
        (l.messages?.length ?? 0) +
        ' / total ' +
        (l.totalMessages ?? '?')
    );
  }

  async function loadIndex() {
    const res = await apiFetch('/api/agents/index');
    if (!res.ok) throw new Error('index ' + res.status);
    return res.json();
  }

  async function loadAll() {
    try {
      setStatus('Загрузка…');
      await loadSnapshot();
      const index = await loadIndex();
      renderAgents(index);
      await loadHistory();
      renderPanel();
      setStatus('OK · ' + new Date().toLocaleTimeString());
    } catch (e) {
      setStatus((e && e.message) || String(e), true);
    }
  }

  function schedule() {
    if (timer) clearInterval(timer);
    const sec = Number(document.getElementById('interval').value) || 0;
    if (sec > 0) timer = setInterval(() => void loadAll(), sec * 1000);
  }

  schedule();
  scheduleJsonlLive();
  void loadAll();
})();
  </script>
</body>
</html>`;
