cursorconnect 0.1.8 → 0.1.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/bridge-runtime/.env.example +2 -0
  2. package/bridge-runtime/connector-version.json +1 -1
  3. package/bridge-runtime/dist/agent-completion-push.d.ts +18 -6
  4. package/bridge-runtime/dist/agent-completion-push.js +186 -41
  5. package/bridge-runtime/dist/agent-completion-readiness.d.ts +19 -0
  6. package/bridge-runtime/dist/agent-completion-readiness.js +42 -0
  7. package/bridge-runtime/dist/chat-display-store.d.ts +32 -7
  8. package/bridge-runtime/dist/chat-display-store.js +96 -21
  9. package/bridge-runtime/dist/chat-display.d.ts +36 -0
  10. package/bridge-runtime/dist/chat-display.js +287 -24
  11. package/bridge-runtime/dist/chat-sync.d.ts +3 -1
  12. package/bridge-runtime/dist/chat-sync.js +20 -0
  13. package/bridge-runtime/dist/debug-chats-page.d.ts +1 -1
  14. package/bridge-runtime/dist/debug-chats-page.js +148 -26
  15. package/bridge-runtime/dist/dom-transcript-store.d.ts +2 -0
  16. package/bridge-runtime/dist/dom-transcript-store.js +17 -2
  17. package/bridge-runtime/dist/extract-page.js +5 -4
  18. package/bridge-runtime/dist/lenta-capture.d.ts +46 -0
  19. package/bridge-runtime/dist/lenta-capture.js +146 -0
  20. package/bridge-runtime/dist/lenta-debug.d.ts +42 -0
  21. package/bridge-runtime/dist/lenta-debug.js +221 -0
  22. package/bridge-runtime/dist/lenta-delivery.d.ts +3 -0
  23. package/bridge-runtime/dist/lenta-delivery.js +10 -0
  24. package/bridge-runtime/dist/lenta-seq-journal.d.ts +48 -0
  25. package/bridge-runtime/dist/lenta-seq-journal.js +109 -0
  26. package/bridge-runtime/dist/message-filter.d.ts +5 -0
  27. package/bridge-runtime/dist/message-filter.js +4 -0
  28. package/bridge-runtime/dist/relay.d.ts +37 -3
  29. package/bridge-runtime/dist/relay.js +557 -51
  30. package/bridge-runtime/dist/types.d.ts +9 -4
  31. package/dist/bridge-build.js +50 -0
  32. package/dist/index.js +9 -6
  33. package/dist/launch.js +5 -1
  34. package/dist/run-service.js +10 -4
  35. package/dist/startup-check.js +6 -0
  36. package/package.json +1 -1
  37. package/version-policy.json +1 -1
@@ -1,2 +1,2 @@
1
1
  /** Read-only browser UI: shows what bridge already holds (no writes). */
2
- export declare const DEBUG_CHATS_PAGE_HTML = "<!DOCTYPE html>\n<html lang=\"ru\">\n<head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>CursorConnect \u2014 bridge data</title>\n <style>\n :root { color-scheme: dark; font-family: system-ui, sans-serif; font-size: 14px; }\n * { box-sizing: border-box; }\n body { margin: 0; background: #111; color: #e8e8e8; }\n header { padding: 10px 14px; border-bottom: 1px solid #333; display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }\n header h1 { font-size: 15px; margin: 0 12px 0 0; font-weight: 600; }\n label { font-size: 12px; color: #aaa; }\n input, select, button { background: #1c1c1c; border: 1px solid #444; color: #eee; border-radius: 6px; padding: 6px 8px; }\n button { cursor: pointer; }\n button:hover { border-color: #666; }\n .layout { display: grid; grid-template-columns: 260px 1fr; min-height: calc(100vh - 52px); }\n .agents { border-right: 1px solid #333; overflow: auto; max-height: calc(100vh - 52px); }\n .agents button { display: block; width: 100%; text-align: left; border: none; border-bottom: 1px solid #2a2a2a; border-radius: 0; padding: 10px 12px; background: transparent; }\n .agents button:hover { background: #1a1a1a; }\n .agents button.active { background: #243044; }\n .agents .meta { font-size: 11px; color: #888; margin-top: 2px; }\n main { display: flex; flex-direction: column; min-width: 0; }\n .tabs { display: flex; gap: 4px; padding: 8px 10px; border-bottom: 1px solid #333; flex-wrap: wrap; }\n .tabs button { font-size: 12px; }\n .tabs button.active { background: #2a3f5f; border-color: #5a7ab0; }\n .status { padding: 6px 12px; font-size: 12px; color: #9ab; border-bottom: 1px solid #2a2a2a; }\n .status.err { color: #f88; }\n .chat { flex: 1; overflow: auto; padding: 12px; display: flex; flex-direction: column; gap: 10px; }\n .bubble { max-width: 92%; padding: 8px 12px; border-radius: 10px; white-space: pre-wrap; word-break: break-word; }\n .bubble.user { align-self: flex-end; background: #2d4a3e; }\n .bubble.assistant { align-self: flex-start; background: #2a2a35; }\n .bubble.system { align-self: center; background: #333; font-size: 12px; color: #bbb; }\n .bubble .tag { font-size: 10px; color: #888; margin-bottom: 4px; font-family: ui-monospace, monospace; }\n .raw { flex: 1; overflow: auto; margin: 0; padding: 12px; font-size: 11px; line-height: 1.4; background: #0a0a0a; color: #bdbdbd; }\n .empty { color: #666; padding: 24px; text-align: center; }\n .note { font-size: 12px; color: #9ab; padding: 8px 12px; border-bottom: 1px solid #2a2a2a; line-height: 1.45; }\n table.cmp { width: 100%; border-collapse: collapse; font-size: 12px; }\n table.cmp th, table.cmp td { border: 1px solid #333; padding: 6px 8px; vertical-align: top; }\n table.cmp th { background: #1a1a1a; text-align: left; }\n table.cmp tr.match td { background: #152515; }\n table.cmp tr.miss td { background: #2a1818; }\n table.cmp tr.domonly td { background: #1a1a2a; }\n .jsonl-live-head { font-size: 12px; color: #9ab; margin-bottom: 10px; line-height: 1.45; }\n .jsonl-row { border-left: 3px solid #444; padding: 8px 10px; margin-bottom: 6px; background: #161616; border-radius: 4px; }\n .jsonl-row.new { animation: jsonl-flash 1.2s ease; border-left-color: #5a9fd4; }\n .jsonl-row.in-lenta { border-left-color: #3d7a52; }\n .jsonl-row.skip { border-left-color: #8a4040; }\n .jsonl-row .meta { font-size: 10px; color: #888; font-family: ui-monospace, monospace; margin-bottom: 4px; }\n .jsonl-row .body { white-space: pre-wrap; word-break: break-word; font-size: 13px; }\n @keyframes jsonl-flash { from { background: #243044; } to { background: #161616; } }\n @media (max-width: 720px) { .layout { grid-template-columns: 1fr; } .agents { max-height: 180px; } }\n </style>\n</head>\n<body>\n <header>\n <h1>Bridge data (read-only)</h1>\n <label>Token <input id=\"token\" type=\"password\" size=\"28\" placeholder=\"\u0435\u0441\u043B\u0438 WEBAPP_PASSWORD\" /></label>\n <label>Refresh <input id=\"interval\" type=\"number\" min=\"0\" max=\"60\" value=\"2\" style=\"width:48px\" /> s</label>\n <button type=\"button\" id=\"btnRefresh\">\u041E\u0431\u043D\u043E\u0432\u0438\u0442\u044C</button>\n <span id=\"health\" style=\"font-size:12px;color:#8a8\"></span>\n </header>\n <div class=\"layout\">\n <nav class=\"agents\" id=\"agents\"><div class=\"empty\">\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u0441\u043F\u0438\u0441\u043A\u0430\u2026</div></nav>\n <main>\n <div class=\"tabs\" id=\"tabs\">\n <button type=\"button\" data-tab=\"history\" class=\"active\">JSONL \u00B7 /api/agents/history</button>\n <button type=\"button\" data-tab=\"jsonl-live\">JSONL live \u00B7 \u0444\u0430\u0439\u043B</button>\n <button type=\"button\" data-tab=\"cache\">JSONL cache</button>\n <button type=\"button\" data-tab=\"transcript\">DOM transcript</button>\n <button type=\"button\" data-tab=\"dom\">DOM \u00B7 state.messages</button>\n <button type=\"button\" data-tab=\"compare\">\u0421\u0440\u0430\u0432\u043D\u0435\u043D\u0438\u0435 JSONL \u2194 DOM</button>\n <button type=\"button\" data-tab=\"debug\">DOM extract debug</button>\n <button type=\"button\" data-tab=\"raw\">JSON snapshot</button>\n </div>\n <div class=\"status\" id=\"status\">\u2014</div>\n <div class=\"note\" id=\"note\" hidden></div>\n <div class=\"chat\" id=\"panel\"></div>\n <pre class=\"raw\" id=\"raw\" hidden></pre>\n </main>\n </div>\n <script src=\"https://cdn.socket.io/4.7.5/socket.io.min.js\"></script>\n <script>\n(function () {\n const params = new URLSearchParams(location.search);\n const roomId = params.get('roomId') || '';\n const tokenEl = document.getElementById('token');\n tokenEl.value = params.get('token') || localStorage.getItem('cc_debug_token') || '';\n tokenEl.addEventListener('change', () => localStorage.setItem('cc_debug_token', tokenEl.value.trim()));\n\n let tab = 'history';\n let selectedId = params.get('agentId') || '';\n let selectedTitle = '';\n let snapshot = null;\n let timer = null;\n let jsonlLiveTimer = null;\n let jsonlLiveLastLine = 0;\n let jsonlLiveRows = [];\n let jsonlLiveStickBottom = true;\n let jsonlLiveFile = '';\n let bridgeSocket = null;\n let bridgeSubscribedId = '';\n\n document.getElementById('tabs').addEventListener('click', (e) => {\n const b = e.target.closest('button[data-tab]');\n if (!b) return;\n tab = b.dataset.tab;\n document.querySelectorAll('#tabs button').forEach((x) => x.classList.toggle('active', x === b));\n scheduleJsonlLive();\n if (tab === 'jsonl-live') void resetJsonlLive();\n renderPanel();\n });\n document.getElementById('btnRefresh').addEventListener('click', () => void loadAll());\n document.getElementById('interval').addEventListener('change', schedule);\n\n function token() { return tokenEl.value.trim(); }\n function apiUrl(path) {\n const u = new URL(path, location.origin);\n if (roomId) u.searchParams.set('roomId', roomId);\n const t = token();\n if (t) u.searchParams.set('token', t);\n return u.toString();\n }\n async function apiFetch(path) {\n const headers = {};\n const t = token();\n if (t) headers.Authorization = 'Bearer ' + t;\n return fetch(apiUrl(path), { headers });\n }\n function setStatus(msg, err) {\n const el = document.getElementById('status');\n el.textContent = msg;\n el.className = 'status' + (err ? ' err' : '');\n }\n\n function roleLabel(role) {\n if (role === 'user') return '\u041F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044C';\n if (role === 'assistant') return '\u0410\u0441\u0441\u0438\u0441\u0442\u0435\u043D\u0442';\n return role || 'system';\n }\n function normText(text, role) {\n let t = String(text || '').replace(/s+/g, ' ').trim().toLowerCase();\n if (role === 'assistant') {\n t = t.replace(/^#+\\s*/g, '').replace(/\\|/g, ' ').replace(/[-]{3,}/g, ' ');\n }\n return t.replace(/s+/g, ' ').trim();\n }\n function textsMatch(a, b, role) {\n const ta = normText(a, role);\n const tb = normText(b, role);\n if (!ta || !tb) return false;\n if (ta === tb) return true;\n const short = ta.length <= tb.length ? ta : tb;\n const long = ta.length <= tb.length ? tb : ta;\n return short.length >= 12 && long.includes(short) && short.length / long.length >= 0.35;\n }\n function renderBubbles(messages, source) {\n if (!messages || !messages.length) return '<div class=\"empty\">\u041D\u0435\u0442 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0439</div>';\n return messages.map((m) => {\n const role = m.role || 'system';\n const tag = roleLabel(role) + ' \u00B7 ' + source + ' \u00B7 ' + (m.id || '?') + (m.flatIndex != null ? ' \u00B7 seq=' + m.flatIndex : '') + (m.html ? ' \u00B7 html' : '');\n const text = (m.text || '').trim() || (m.html ? '[html ' + m.html.length + ' chars \u2014 \u0432 Cursor \u0441 \u0440\u0430\u0437\u043C\u0435\u0442\u043A\u043E\u0439]' : '(\u043F\u0443\u0441\u0442\u043E)');\n return '<div class=\"bubble ' + role + '\"><div class=\"tag\">' + escapeHtml(tag) + '</div>' + escapeHtml(text) + '</div>';\n }).join('');\n }\n function renderCompare(hist, dom) {\n const rows = [];\n const usedDom = new Set();\n for (let i = 0; i < (hist || []).length; i++) {\n const h = hist[i];\n let domIdx = -1;\n for (let j = 0; j < (dom || []).length; j++) {\n if (usedDom.has(j)) continue;\n if (h.role === dom[j].role && textsMatch(h.text, dom[j].text, h.role)) {\n domIdx = j;\n usedDom.add(j);\n break;\n }\n }\n const cls = domIdx >= 0 ? 'match' : 'miss';\n rows.push({ cls, n: i + 1, h, d: domIdx >= 0 ? dom[domIdx] : null });\n }\n for (let j = 0; j < (dom || []).length; j++) {\n if (usedDom.has(j)) continue;\n rows.push({ cls: 'domonly', n: '\u2014', h: null, d: dom[j] });\n }\n if (!rows.length) return '<div class=\"empty\">\u041D\u0435\u0442 \u0434\u0430\u043D\u043D\u044B\u0445</div>';\n const tr = rows.map((r) => {\n const hp = r.h ? escapeHtml((r.h.text || '').slice(0, 120)) : '\u2014';\n const dp = r.d ? escapeHtml((r.d.text || '').slice(0, 120)) : '\u2014';\n 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>';\n }).join('');\n return '<table class=\"cmp\"><thead><tr><th>#</th><th>JSONL</th><th>\u0442\u0435\u043A\u0441\u0442 JSONL</th><th>DOM</th><th>\u0442\u0435\u043A\u0441\u0442 DOM</th></tr></thead><tbody>' + tr + '</tbody></table>';\n }\n function escapeHtml(s) {\n return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');\n }\n\n function renderJsonlLive() {\n if (!jsonlLiveRows.length) {\n return '<div class=\"empty\">\u041D\u0435\u0442 \u0441\u0442\u0440\u043E\u043A \u2014 \u0432\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0447\u0430\u0442 \u0438\u043B\u0438 \u0434\u043E\u0436\u0434\u0438\u0442\u0435\u0441\u044C \u0437\u0430\u043F\u0438\u0441\u0438 \u0432 JSONL</div>';\n }\n const head =\n '<div class=\"jsonl-live-head\">\u0424\u0430\u0439\u043B: <code>' +\n escapeHtml(jsonlLiveFile || '\u2014') +\n '</code> \u00B7 \u0441\u0442\u0440\u043E\u043A \u0432 \u0444\u0430\u0439\u043B\u0435: <b>' +\n jsonlLiveLastLine +\n '</b> \u00B7 \u043F\u043E\u043A\u0430\u0437\u0430\u043D\u043E: ' +\n jsonlLiveRows.length +\n ' \u00B7 \u043E\u043F\u0440\u043E\u0441 <b>300 ms</b> \u00B7 bridge subscribe: <b>' +\n escapeHtml(bridgeSubscribedId === selectedId ? 'on' : 'off') +\n '</b></div>';\n const body = jsonlLiveRows\n .map((r) => {\n const cls =\n 'jsonl-row' +\n (r._new ? ' new' : '') +\n (r.inLenta ? ' in-lenta' : ' skip');\n const tools = r.tools?.length ? ' \u00B7 tools: ' + r.tools.join(', ') : '';\n const skip = r.inLenta ? '\u2192 \u0432 \u043B\u0435\u043D\u0442\u0443' : '\u2192 skip: ' + (r.skipReason || '?');\n return (\n '<div class=\"' +\n cls +\n '\" data-line=\"' +\n r.lineNo +\n '\"><div class=\"meta\">#' +\n r.lineNo +\n ' \u00B7 ' +\n escapeHtml(r.role) +\n tools +\n ' \u00B7 ' +\n escapeHtml(skip) +\n (r._at ? ' \u00B7 ' + r._at : '') +\n '</div><div class=\"body\">' +\n escapeHtml(r.textPreview || '(\u043F\u0443\u0441\u0442\u043E)') +\n '</div></div>'\n );\n })\n .join('');\n return head + body;\n }\n\n async function pollJsonlLive() {\n if (!selectedId || tab !== 'jsonl-live') return;\n try {\n const q = new URLSearchParams();\n q.set('agentId', selectedId);\n if (selectedTitle) q.set('title', selectedTitle);\n if (jsonlLiveLastLine > 0) q.set('afterLine', String(jsonlLiveLastLine));\n else q.set('tail', '80');\n const res = await apiFetch('/debug/jsonl-live?' + q.toString());\n if (!res.ok) throw new Error('jsonl-live ' + res.status);\n const data = await res.json();\n jsonlLiveFile = data.filePath || '';\n const at = new Date().toLocaleTimeString();\n const incoming = (data.rows || []).map((r) => ({ ...r, _new: true, _at: at }));\n if (jsonlLiveLastLine === 0) {\n jsonlLiveRows = incoming;\n } else if (incoming.length) {\n const seen = new Set(jsonlLiveRows.map((x) => x.lineNo));\n for (const r of incoming) {\n if (!seen.has(r.lineNo)) jsonlLiveRows.push(r);\n }\n }\n if (data.totalLines) jsonlLiveLastLine = data.totalLines;\n else if (incoming.length) {\n jsonlLiveLastLine = Math.max(jsonlLiveLastLine, incoming[incoming.length - 1].lineNo);\n }\n if (jsonlLiveRows.length > 600) jsonlLiveRows = jsonlLiveRows.slice(-500);\n setTimeout(() => {\n for (const r of jsonlLiveRows) r._new = false;\n }, 1200);\n if (tab === 'jsonl-live') {\n renderPanel();\n if (jsonlLiveStickBottom) {\n const panel = document.getElementById('panel');\n panel.scrollTop = panel.scrollHeight;\n }\n setStatus(\n 'JSONL live: ' +\n jsonlLiveRows.length +\n ' shown \u00B7 file lines ' +\n (data.totalLines ?? '?') +\n ' \u00B7 ' +\n at\n );\n }\n } catch (e) {\n setStatus((e && e.message) || String(e), true);\n }\n }\n\n async function resetJsonlLive() {\n jsonlLiveLastLine = 0;\n jsonlLiveRows = [];\n jsonlLiveFile = '';\n await pollJsonlLive();\n }\n\n function ensureBridgeSubscribe() {\n if (typeof io === 'undefined') return;\n if (!bridgeSocket) {\n const headers = {};\n const t = token();\n if (t) headers.Authorization = 'Bearer ' + t;\n bridgeSocket = io(location.origin, { transports: ['websocket', 'polling'], auth: headers });\n bridgeSocket.on('agent:messages', (p) => {\n const n = p.historyMessages?.length ?? 0;\n const tail = (p.historyMessages || [])[(n || 1) - 1];\n const preview = (tail?.text || '').slice(0, 48);\n setStatus(\n 'app-pipe agent:messages append=' +\n !!p.append +\n ' n=' +\n n +\n ' \u00B7 ' +\n preview\n );\n });\n }\n if (!selectedId || (bridgeSubscribedId === selectedId && tab === 'jsonl-live')) return;\n bridgeSubscribedId = selectedId;\n bridgeSocket.emit('agents:subscribe', {\n agentId: selectedId,\n title: selectedTitle || undefined,\n focus: false,\n });\n setStatus('bridge subscribe ' + selectedId.slice(0, 8) + '\u2026 (\u043A\u0430\u043A \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0435)');\n }\n\n function scheduleJsonlLive() {\n if (jsonlLiveTimer) clearInterval(jsonlLiveTimer);\n jsonlLiveTimer = null;\n if (tab === 'jsonl-live') {\n ensureBridgeSubscribe();\n jsonlLiveTimer = setInterval(() => void pollJsonlLive(), 300);\n }\n }\n\n function renderPanel() {\n const panel = document.getElementById('panel');\n const raw = document.getElementById('raw');\n panel.hidden = tab === 'raw';\n raw.hidden = tab !== 'raw';\n if (tab === 'raw') {\n raw.textContent = JSON.stringify({ snapshot, selectedId, tab }, null, 2);\n return;\n }\n const note = document.getElementById('note');\n note.hidden = tab !== 'dom' && tab !== 'compare' && tab !== 'jsonl-live';\n if (tab === 'jsonl-live') {\n note.textContent =\n '\u0424\u0430\u0439\u043B .jsonl \u043D\u0430 \u0434\u0438\u0441\u043A\u0435 (\u043E\u043F\u0440\u043E\u0441 300 ms). \u00AB\u2192 \u0432 \u043B\u0435\u043D\u0442\u0443\u00BB = \u043F\u043E\u0441\u043B\u0435 filter \u0432 bridge. \u0412 \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0435 \u0441\u0442\u0440\u043E\u043A\u0430 \u043F\u043E\u043F\u0430\u0434\u0451\u0442 \u0442\u043E\u043B\u044C\u043A\u043E \u0435\u0441\u043B\u0438 Connect \u043E\u0442\u043A\u0440\u044B\u043B \u044D\u0442\u043E\u0442 \u0447\u0430\u0442 (\u043D\u0438\u0436\u0435: bridge subscribe). Debug \u0441\u0430\u043C \u043F\u043E \u0441\u0435\u0431\u0435 app \u043D\u0435 \u043A\u043E\u0440\u043C\u0438\u0442.';\n } else if (tab === 'dom') {\n note.textContent = 'DOM = \u0441\u043C\u043E\u043D\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u044B\u0435 [data-flat-index] \u0432 Cursor (\u0431\u0435\u0437 JSONL). \u041F\u0440\u0438 \u043E\u0442\u043A\u0440\u044B\u0442\u0438\u0438 \u0447\u0430\u0442\u0430 bridge \u043F\u0440\u043E\u043A\u0440\u0443\u0447\u0438\u0432\u0430\u0435\u0442 \u043B\u0435\u043D\u0442\u0443 \u0432\u043D\u0438\u0437 \u0438 poll. \u041F\u0440\u043E\u043F\u0443\u0441\u043A\u0438 = tool/thought \u0441\u0442\u0440\u043E\u043A\u0438.';\n } else if (tab === 'compare') {\n note.textContent =\n '\u0417\u0435\u043B\u0451\u043D\u0430\u044F = JSONL-\u043B\u0435\u043D\u0442\u0430 (API) \u0438 DOM \u0441\u043E\u0432\u043F\u0430\u043B\u0438. \u041A\u0440\u0430\u0441\u043D\u0430\u044F = \u0442\u043E\u043B\u044C\u043A\u043E \u0432 JSONL. \u0424\u0438\u043E\u043B\u0435\u0442\u043E\u0432\u0430\u044F (domonly) = \u0432\u0438\u0434\u043D\u043E \u0432 DOM Cursor, \u0441\u043B\u0435\u0432\u0430 \u00AB\u2014\u00BB (\u043D\u0435\u0442 \u0432 \u043B\u0435\u043D\u0442\u0435 API): \u0441\u0445\u043B\u043E\u043F\u043D\u0443\u043B\u0438 \u043D\u0435\u0441\u043A\u043E\u043B\u044C\u043A\u043E \u043E\u0442\u0432\u0435\u0442\u043E\u0432 \u0430\u0441\u0441\u0438\u0441\u0442\u0435\u043D\u0442\u0430 \u0432 \u043E\u0434\u0438\u043D, \u0438\u043B\u0438 DOM \u0435\u0449\u0451 \u0434\u0435\u0440\u0436\u0438\u0442 \u043A\u043E\u0440\u043E\u0442\u043A\u0438\u0439 \u0441\u0442\u0430\u0442\u0443\u0441 (\u00AB\u0418\u0449\u0443\u2026\u00BB, \u00AB\u0414\u043E\u0431\u0430\u0432\u043B\u044F\u044E\u2026\u00BB), \u0430 \u0432 API \u0443\u0436\u0435 \u0442\u043E\u043B\u044C\u043A\u043E \u0444\u0438\u043D\u0430\u043B\u044C\u043D\u044B\u0439 \u00AB\u0421\u0434\u0435\u043B\u0430\u043D\u043E\u2026\u00BB. \u0412\u043A\u043B\u0430\u0434\u043A\u0430 \u00ABJSONL cache\u00BB = \u0442\u043E, \u0447\u0442\u043E \u0443\u0445\u043E\u0434\u0438\u0442 \u0432 app.';\n }\n if (!selectedId && tab !== 'dom' && tab !== 'debug') {\n panel.innerHTML = '<div class=\"empty\">\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0430\u0433\u0435\u043D\u0442\u0430 \u0441\u043B\u0435\u0432\u0430</div>';\n return;\n }\n if (tab === 'jsonl-live') {\n panel.innerHTML = renderJsonlLive();\n panel.onscroll = () => {\n const nearBottom = panel.scrollHeight - panel.scrollTop - panel.clientHeight < 80;\n jsonlLiveStickBottom = nearBottom;\n };\n return;\n }\n panel.onscroll = null;\n if (tab === 'history') {\n panel.innerHTML = window.__historyHtml || '<div class=\"empty\">\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430 history\u2026</div>';\n } else if (tab === 'cache') {\n const rows = snapshot?.displayCache?.[selectedId];\n panel.innerHTML = renderBubbles(rows, 'jsonl-cache');\n } else if (tab === 'transcript') {\n const rows = snapshot?.domTranscript?.[selectedId];\n panel.innerHTML = renderBubbles(rows, 'dom-transcript');\n } else if (tab === 'dom') {\n const st = snapshot?.cursor;\n const active = st?.activeComposerId;\n const hint = active ? 'activeComposerId=' + active : '\u043D\u0435\u0442 activeComposerId';\n panel.innerHTML = '<div class=\"empty\" style=\"text-align:left;margin-bottom:8px\">' + escapeHtml(hint) + ' \u00B7 ' + (st?.domMessageCount ?? 0) + ' msgs</div>' + renderBubbles(st?.domMessages, 'dom');\n } else if (tab === 'compare') {\n const hist = window.__historyData || [];\n const dom =\n (selectedId && snapshot?.domTranscript?.[selectedId]?.length\n ? snapshot.domTranscript[selectedId]\n : null) ||\n snapshot?.cursor?.domMessages ||\n [];\n const active = snapshot?.cursor?.activeComposerId;\n const same = selectedId === active;\n const head = '<div class=\"empty\" style=\"text-align:left\">agent=' + escapeHtml(selectedId.slice(0, 8)) + '\u2026 active=' + escapeHtml((active || '\u2014').slice(0, 8)) + (same ? ' (\u0441\u043E\u0432\u043F\u0430\u0434\u0430\u0435\u0442)' : ' (\u0434\u0440\u0443\u0433\u043E\u0439 \u0447\u0430\u0442 \u0432 Cursor!)') + '</div>';\n panel.innerHTML = head + renderCompare(hist, dom);\n } else if (tab === 'debug') {\n const d = snapshot?.domExtractDebug?.latest;\n if (!d) { panel.innerHTML = '<div class=\"empty\">\u041D\u0435\u0442 extract debug</div>'; return; }\n panel.innerHTML = '<div class=\"bubble system\"><div class=\"tag\">extract debug</div>' + escapeHtml(JSON.stringify(d, null, 2)) + '</div>';\n }\n }\n\n function renderAgents(index) {\n const nav = document.getElementById('agents');\n const repos = index?.repos || [];\n const items = [];\n for (const repo of repos) {\n for (const a of repo.agents || []) {\n items.push({ id: a.id, title: a.title, repo: repo.name });\n }\n }\n if (!items.length) {\n nav.innerHTML = '<div class=\"empty\">agents:index \u043F\u0443\u0441\u0442</div>';\n return;\n }\n nav.innerHTML = items.map((a) => {\n const cls = a.id === selectedId ? ' active' : '';\n 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) + ' \u00B7 ' + escapeHtml(a.id.slice(0, 8)) + '\u2026</div></button>';\n }).join('');\n nav.querySelectorAll('button[data-id]').forEach((btn) => {\n btn.addEventListener('click', () => {\n selectedId = btn.dataset.id;\n selectedTitle = btn.dataset.title || '';\n nav.querySelectorAll('button').forEach((x) => x.classList.toggle('active', x.dataset.id === selectedId));\n void loadHistory();\n if (tab === 'jsonl-live') void resetJsonlLive();\n renderPanel();\n });\n });\n if (!selectedId && items[0]) {\n selectedId = items[0].id;\n selectedTitle = items[0].title || '';\n nav.querySelector('button[data-id=\"' + selectedId + '\"]')?.classList.add('active');\n }\n }\n\n async function loadSnapshot() {\n const res = await apiFetch('/debug/chat-snapshot');\n if (!res.ok) throw new Error('chat-snapshot ' + res.status);\n snapshot = await res.json();\n const h = document.getElementById('health');\n h.textContent = 'CDP ' + (snapshot.health?.cdp ? 'on' : 'off') + ' \u00B7 updated ' + new Date(snapshot.at).toLocaleTimeString();\n }\n\n async function loadHistory() {\n if (!selectedId) return;\n const res = await apiFetch('/api/agents/history?agentId=' + encodeURIComponent(selectedId) + '&limit=120');\n if (!res.ok) throw new Error('history ' + res.status);\n const data = await res.json();\n window.__historyData = data.messages || [];\n window.__historyHtml = renderBubbles(window.__historyData, 'jsonl');\n if (tab === 'history' || tab === 'compare') renderPanel();\n setStatus('history: ' + (data.messages?.length ?? 0) + ' / total ' + (data.totalMessages ?? '?'));\n }\n\n async function loadIndex() {\n const res = await apiFetch('/api/agents/index');\n if (!res.ok) throw new Error('index ' + res.status);\n return res.json();\n }\n\n async function loadAll() {\n try {\n setStatus('\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430\u2026');\n await loadSnapshot();\n const index = await loadIndex();\n renderAgents(index);\n await loadHistory();\n renderPanel();\n setStatus('OK \u00B7 ' + new Date().toLocaleTimeString());\n } catch (e) {\n setStatus((e && e.message) || String(e), true);\n }\n }\n\n function schedule() {\n if (timer) clearInterval(timer);\n const sec = Number(document.getElementById('interval').value) || 0;\n if (sec > 0) timer = setInterval(() => void loadAll(), sec * 1000);\n }\n\n schedule();\n scheduleJsonlLive();\n void loadAll();\n})();\n </script>\n</body>\n</html>";
2
+ export declare const DEBUG_CHATS_PAGE_HTML = "<!DOCTYPE html>\n<html lang=\"ru\">\n<head>\n <meta charset=\"utf-8\" />\n <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\" />\n <title>CursorConnect \u2014 bridge data</title>\n <style>\n :root { color-scheme: dark; font-family: system-ui, sans-serif; font-size: 14px; }\n * { box-sizing: border-box; }\n body { margin: 0; background: #111; color: #e8e8e8; }\n header { padding: 10px 14px; border-bottom: 1px solid #333; display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }\n header h1 { font-size: 15px; margin: 0 12px 0 0; font-weight: 600; }\n label { font-size: 12px; color: #aaa; }\n input, select, button { background: #1c1c1c; border: 1px solid #444; color: #eee; border-radius: 6px; padding: 6px 8px; }\n button { cursor: pointer; }\n button:hover { border-color: #666; }\n .layout { display: grid; grid-template-columns: 260px 1fr; min-height: calc(100vh - 52px); }\n .agents { border-right: 1px solid #333; overflow: auto; max-height: calc(100vh - 52px); }\n .agents button { display: block; width: 100%; text-align: left; border: none; border-bottom: 1px solid #2a2a2a; border-radius: 0; padding: 10px 12px; background: transparent; }\n .agents button:hover { background: #1a1a1a; }\n .agents button.active { background: #243044; }\n .agents .meta { font-size: 11px; color: #888; margin-top: 2px; }\n main { display: flex; flex-direction: column; min-width: 0; }\n .tabs { display: flex; gap: 4px; padding: 8px 10px; border-bottom: 1px solid #333; flex-wrap: wrap; }\n .tabs button { font-size: 12px; }\n .tabs button.active { background: #2a3f5f; border-color: #5a7ab0; }\n .status { padding: 6px 12px; font-size: 12px; color: #9ab; border-bottom: 1px solid #2a2a2a; }\n .status.err { color: #f88; }\n .chat { flex: 1; overflow: auto; padding: 12px; display: flex; flex-direction: column; gap: 10px; }\n .bubble { max-width: 92%; padding: 8px 12px; border-radius: 10px; white-space: pre-wrap; word-break: break-word; }\n .bubble.user { align-self: flex-end; background: #2d4a3e; }\n .bubble.assistant { align-self: flex-start; background: #2a2a35; }\n .bubble.system { align-self: center; background: #333; font-size: 12px; color: #bbb; }\n .bubble .tag { font-size: 10px; color: #888; margin-bottom: 4px; font-family: ui-monospace, monospace; }\n .bubble.segment-jsonl { border: 1px solid #2a4a38; }\n .bubble.segment-dom { border: 1px solid #4a3a5a; box-shadow: 0 0 0 1px #3a2a4a inset; }\n .lenta-head { font-size: 12px; color: #9ab; margin-bottom: 10px; line-height: 1.45; }\n .lenta-divider { align-self: center; font-size: 11px; color: #6a8a9a; padding: 4px 0; letter-spacing: 0.02em; }\n .raw { flex: 1; overflow: auto; margin: 0; padding: 12px; font-size: 11px; line-height: 1.4; background: #0a0a0a; color: #bdbdbd; }\n .empty { color: #666; padding: 24px; text-align: center; }\n .note { font-size: 12px; color: #9ab; padding: 8px 12px; border-bottom: 1px solid #2a2a2a; line-height: 1.45; }\n table.cmp { width: 100%; border-collapse: collapse; font-size: 12px; }\n table.cmp th, table.cmp td { border: 1px solid #333; padding: 6px 8px; vertical-align: top; }\n table.cmp th { background: #1a1a1a; text-align: left; }\n table.cmp tr.match td { background: #152515; }\n table.cmp tr.miss td { background: #2a1818; }\n table.cmp tr.domonly td { background: #1a1a2a; }\n .jsonl-live-head { font-size: 12px; color: #9ab; margin-bottom: 10px; line-height: 1.45; }\n .jsonl-row { border-left: 3px solid #444; padding: 8px 10px; margin-bottom: 6px; background: #161616; border-radius: 4px; }\n .jsonl-row.new { animation: jsonl-flash 1.2s ease; border-left-color: #5a9fd4; }\n .jsonl-row.in-lenta { border-left-color: #3d7a52; }\n .jsonl-row.skip { border-left-color: #8a4040; }\n .jsonl-row .meta { font-size: 10px; color: #888; font-family: ui-monospace, monospace; margin-bottom: 4px; }\n .jsonl-row .body { white-space: pre-wrap; word-break: break-word; font-size: 13px; }\n @keyframes jsonl-flash { from { background: #243044; } to { background: #161616; } }\n @media (max-width: 720px) { .layout { grid-template-columns: 1fr; } .agents { max-height: 180px; } }\n </style>\n</head>\n<body>\n <header>\n <h1>Bridge data (read-only)</h1>\n <label>Token <input id=\"token\" type=\"password\" size=\"28\" placeholder=\"\u0435\u0441\u043B\u0438 WEBAPP_PASSWORD\" /></label>\n <label>Refresh <input id=\"interval\" type=\"number\" min=\"0\" max=\"60\" value=\"2\" style=\"width:48px\" /> s</label>\n <button type=\"button\" id=\"btnRefresh\">\u041E\u0431\u043D\u043E\u0432\u0438\u0442\u044C</button>\n <span id=\"health\" style=\"font-size:12px;color:#8a8\"></span>\n </header>\n <div class=\"layout\">\n <nav class=\"agents\" id=\"agents\"><div class=\"empty\">\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430 \u0441\u043F\u0438\u0441\u043A\u0430\u2026</div></nav>\n <main>\n <div class=\"tabs\" id=\"tabs\">\n <button type=\"button\" data-tab=\"lenta\" class=\"active\">\u041B\u0435\u043D\u0442\u0430 \u00B7 JSONL+DOM</button>\n <button type=\"button\" data-tab=\"history\">JSONL \u00B7 /api/agents/history</button>\n <button type=\"button\" data-tab=\"jsonl-live\">JSONL live \u00B7 \u0444\u0430\u0439\u043B</button>\n <button type=\"button\" data-tab=\"cache\">JSONL cache</button>\n <button type=\"button\" data-tab=\"transcript\">DOM transcript</button>\n <button type=\"button\" data-tab=\"dom\">DOM \u00B7 state.messages</button>\n <button type=\"button\" data-tab=\"compare\">\u0421\u0440\u0430\u0432\u043D\u0435\u043D\u0438\u0435 JSONL \u2194 DOM</button>\n <button type=\"button\" data-tab=\"debug\">DOM extract debug</button>\n <button type=\"button\" data-tab=\"raw\">JSON snapshot</button>\n </div>\n <div class=\"status\" id=\"status\">\u2014</div>\n <div class=\"note\" id=\"note\" hidden></div>\n <div class=\"chat\" id=\"panel\"></div>\n <pre class=\"raw\" id=\"raw\" hidden></pre>\n </main>\n </div>\n <script src=\"https://cdn.socket.io/4.7.5/socket.io.min.js\"></script>\n <script>\n(function () {\n const params = new URLSearchParams(location.search);\n const roomId = params.get('roomId') || '';\n const tokenEl = document.getElementById('token');\n tokenEl.value = params.get('token') || localStorage.getItem('cc_debug_token') || '';\n tokenEl.addEventListener('change', () => localStorage.setItem('cc_debug_token', tokenEl.value.trim()));\n\n let tab = 'lenta';\n let selectedId = params.get('agentId') || '';\n let selectedTitle = '';\n let snapshot = null;\n let timer = null;\n let jsonlLiveTimer = null;\n let jsonlLiveLastLine = 0;\n let jsonlLiveRows = [];\n let jsonlLiveStickTop = true;\n let jsonlLiveFile = '';\n let bridgeSocket = null;\n let bridgeSubscribedId = '';\n\n document.getElementById('tabs').addEventListener('click', (e) => {\n const b = e.target.closest('button[data-tab]');\n if (!b) return;\n tab = b.dataset.tab;\n document.querySelectorAll('#tabs button').forEach((x) => x.classList.toggle('active', x === b));\n scheduleJsonlLive();\n if (tab === 'jsonl-live') void resetJsonlLive();\n renderPanel();\n });\n document.getElementById('btnRefresh').addEventListener('click', () => void loadAll());\n document.getElementById('interval').addEventListener('change', schedule);\n\n function token() { return tokenEl.value.trim(); }\n function apiUrl(path) {\n const u = new URL(path, location.origin);\n if (roomId) u.searchParams.set('roomId', roomId);\n const t = token();\n if (t) u.searchParams.set('token', t);\n return u.toString();\n }\n async function apiFetch(path) {\n const headers = {};\n const t = token();\n if (t) headers.Authorization = 'Bearer ' + t;\n return fetch(apiUrl(path), { headers });\n }\n function setStatus(msg, err) {\n const el = document.getElementById('status');\n el.textContent = msg;\n el.className = 'status' + (err ? ' err' : '');\n }\n\n function roleLabel(role) {\n if (role === 'user') return '\u041F\u043E\u043B\u044C\u0437\u043E\u0432\u0430\u0442\u0435\u043B\u044C';\n if (role === 'assistant') return '\u0410\u0441\u0441\u0438\u0441\u0442\u0435\u043D\u0442';\n return role || 'system';\n }\n function normText(text, role) {\n let t = String(text || '').replace(/s+/g, ' ').trim().toLowerCase();\n if (role === 'assistant') {\n t = t.replace(/^#+\\s*/gm, '').replace(/\\|/g, ' ').replace(/[-]{3,}/g, ' ');\n }\n return t.replace(/s+/g, ' ').trim();\n }\n /** DOM textContent \u0447\u0430\u0441\u0442\u043E \u0440\u0432\u0451\u0442 URL: \u00ABhttp:// 127. 0. 0.1\u00BB vs JSONL \u00ABhttp://127.0.0.1\u00BB. */\n function compactMatchText(text, role) {\n return normText(text, role).replace(/s/g, '');\n }\n function textsMatch(a, b, role) {\n const ta = normText(a, role);\n const tb = normText(b, role);\n if (!ta || !tb) return false;\n if (ta === tb) return true;\n const ca = compactMatchText(a, role);\n const cb = compactMatchText(b, role);\n if (ca && cb && ca === cb) return true;\n const shortC = ca.length <= cb.length ? ca : cb;\n const longC = ca.length <= cb.length ? cb : ca;\n if (shortC.length >= 12 && longC.includes(shortC) && shortC.length / longC.length >= 0.35) {\n return true;\n }\n const short = ta.length <= tb.length ? ta : tb;\n const long = ta.length <= tb.length ? tb : ta;\n return short.length >= 12 && long.includes(short) && short.length / long.length >= 0.35;\n }\n function renderBubbles(messages, source) {\n if (!messages || !messages.length) return '<div class=\"empty\">\u041D\u0435\u0442 \u0441\u043E\u043E\u0431\u0449\u0435\u043D\u0438\u0439</div>';\n return [...messages].reverse().map((m) => {\n const role = m.role || 'system';\n const tag =\n roleLabel(role) +\n ' \u00B7 ' +\n source +\n ' \u00B7 ' +\n (m.id || '?') +\n (m.flatIndex != null ? ' \u00B7 flat=' + m.flatIndex : '') +\n (m.html ? ' \u00B7 html' : '');\n const text = (m.text || '').trim() || (m.html ? '[html ' + m.html.length + ' chars \u2014 \u0432 Cursor \u0441 \u0440\u0430\u0437\u043C\u0435\u0442\u043A\u043E\u0439]' : '(\u043F\u0443\u0441\u0442\u043E)');\n return '<div class=\"bubble ' + role + '\"><div class=\"tag\">' + escapeHtml(tag) + '</div>' + escapeHtml(text) + '</div>';\n }).join('');\n }\n function resolveLentaForAgent() {\n const snap = snapshot?.lentaByAgent?.[selectedId];\n const api = window.__lentaData;\n const messages =\n (snap?.messages?.length ? snap.messages : null) ||\n (api?.messages?.length ? api.messages : null) ||\n snapshot?.displayMessages?.[selectedId] ||\n [];\n const jsonlHistory = snap?.jsonlHistory ?? api?.jsonlHistory ?? [];\n const domOverlay = snap?.domOverlay ?? api?.domOverlay ?? [];\n const source = snap?.source ?? api?.source ?? (domOverlay.length ? 'hybrid' : 'jsonl');\n const jsonlRowCount = snap?.jsonlRowCount ?? api?.jsonlRowCount ?? jsonlHistory.length;\n const totalMessages = api?.totalMessages ?? messages.length;\n const payloadSeq = api?.payloadSeq ?? snapshot?.lentaPayloadSeq?.[selectedId];\n return { messages, jsonlHistory, domOverlay, source, jsonlRowCount, totalMessages, payloadSeq };\n }\n function renderLentaFeed(lenta) {\n const messages = lenta.messages || [];\n if (!messages.length) {\n return '<div class=\"empty\">\u041D\u0435\u0442 \u043B\u0435\u043D\u0442\u044B \u2014 \u043E\u0442\u043A\u0440\u043E\u0439\u0442\u0435 \u0447\u0430\u0442 \u0432 Cursor \u0438\u043B\u0438 \u0434\u043E\u0436\u0434\u0438\u0442\u0435\u0441\u044C JSONL</div>';\n }\n const jsonlLen = (lenta.jsonlHistory || []).length;\n const domN = (lenta.domOverlay || []).length;\n const head =\n '<div class=\"lenta-head\">\u041A\u0430\u043A \u0432 app (<code>agent:messages</code>): <b>' +\n messages.length +\n '</b> \u043F\u0443\u0437\u044B\u0440\u0435\u0439 \u00B7 JSONL <b>' +\n jsonlLen +\n '</b> \u00B7 DOM live <b>' +\n domN +\n '</b> \u00B7 source=<b>' +\n escapeHtml(lenta.source || '?') +\n '</b>' +\n (lenta.totalMessages > messages.length\n ? ' \u00B7 \u0432 \u0444\u0430\u0439\u043B\u0435 \u0432\u0441\u0435\u0433\u043E ' + lenta.totalMessages\n : '') +\n (lenta.payloadSeq != null ? ' \u00B7 <b>emitSeq=' + lenta.payloadSeq + '</b>' : '') +\n ' \u00B7 \u043D\u043E\u0432\u044B\u0435 \u0441\u0432\u0435\u0440\u0445\u0443 \u00B7 <span style=\"opacity:0.85\">flat=\u043A\u043B\u044E\u0447 \u043F\u043E\u0440\u044F\u0434\u043A\u0430 \u0441\u0442\u0440\u043E\u043A\u0438, \u043D\u0435 emitSeq</span></div>';\n const parts = [];\n for (let i = messages.length - 1; i >= 0; i--) {\n const m = messages[i];\n const isDom = i >= jsonlLen;\n if (i === jsonlLen - 1 && domN > 0 && jsonlLen > 0) {\n parts.push('<div class=\"lenta-divider\">\u2500\u2500 JSONL archive \u2500\u2500</div>');\n }\n const role = m.role || 'system';\n const seg = isDom ? 'DOM live' : 'JSONL';\n const tag =\n roleLabel(role) +\n ' \u00B7 ' +\n seg +\n ' \u00B7 ' +\n (m.id || '?') +\n (m.flatIndex != null ? ' \u00B7 flat=' + m.flatIndex : '') +\n (m.html ? ' \u00B7 html' : '');\n const text =\n (m.text || '').trim() ||\n (m.html ? '[html ' + m.html.length + ' chars \u2014 \u0432 Cursor \u0441 \u0440\u0430\u0437\u043C\u0435\u0442\u043A\u043E\u0439]' : '(\u043F\u0443\u0441\u0442\u043E)');\n parts.push(\n '<div class=\"bubble ' +\n role +\n ' segment-' +\n (isDom ? 'dom' : 'jsonl') +\n '\"><div class=\"tag\">' +\n escapeHtml(tag) +\n '</div>' +\n escapeHtml(text) +\n '</div>'\n );\n }\n return head + parts.join('');\n }\n function renderCompare(hist, dom) {\n const rows = [];\n const usedDom = new Set();\n for (let i = 0; i < (hist || []).length; i++) {\n const h = hist[i];\n let domIdx = -1;\n // DOM viewport = \u0432\u0435\u0441\u044C \u0441\u043C\u043E\u043D\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u044B\u0439 \u0447\u0430\u0442 (tool-\u0441\u0442\u0440\u043E\u043A\u0438, \u0441\u0442\u0430\u0442\u0443\u0441\u044B); \u0441\u043E\u0432\u043F\u0430\u0434\u0435\u043D\u0438\u0435 \u0447\u0430\u0441\u0442\u043E \u0432 \u0445\u0432\u043E\u0441\u0442\u0435.\n for (let j = (dom || []).length - 1; j >= 0; j--) {\n if (usedDom.has(j)) continue;\n if (h.role === dom[j].role && textsMatch(h.text, dom[j].text, h.role)) {\n domIdx = j;\n usedDom.add(j);\n break;\n }\n }\n const cls = domIdx >= 0 ? 'match' : 'miss';\n rows.push({ cls, n: i + 1, h, d: domIdx >= 0 ? dom[domIdx] : null });\n }\n for (let j = 0; j < (dom || []).length; j++) {\n if (usedDom.has(j)) continue;\n rows.push({ cls: 'domonly', n: '\u2014', h: null, d: dom[j] });\n }\n if (!rows.length) return '<div class=\"empty\">\u041D\u0435\u0442 \u0434\u0430\u043D\u043D\u044B\u0445</div>';\n const tr = rows.map((r) => {\n const hp = r.h ? escapeHtml((r.h.text || '').slice(0, 120)) : '\u2014';\n const dp = r.d ? escapeHtml((r.d.text || '').slice(0, 120)) : '\u2014';\n 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>';\n }).join('');\n return '<table class=\"cmp\"><thead><tr><th>#</th><th>JSONL</th><th>\u0442\u0435\u043A\u0441\u0442 JSONL</th><th>DOM</th><th>\u0442\u0435\u043A\u0441\u0442 DOM</th></tr></thead><tbody>' + tr + '</tbody></table>';\n }\n function escapeHtml(s) {\n return String(s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');\n }\n\n function renderJsonlLive() {\n if (!jsonlLiveRows.length) {\n return '<div class=\"empty\">\u041D\u0435\u0442 \u0441\u0442\u0440\u043E\u043A \u2014 \u0432\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0447\u0430\u0442 \u0438\u043B\u0438 \u0434\u043E\u0436\u0434\u0438\u0442\u0435\u0441\u044C \u0437\u0430\u043F\u0438\u0441\u0438 \u0432 JSONL</div>';\n }\n const head =\n '<div class=\"jsonl-live-head\">\u0424\u0430\u0439\u043B: <code>' +\n escapeHtml(jsonlLiveFile || '\u2014') +\n '</code> \u00B7 \u0441\u0442\u0440\u043E\u043A \u0432 \u0444\u0430\u0439\u043B\u0435: <b>' +\n jsonlLiveLastLine +\n '</b> \u00B7 \u043F\u043E\u043A\u0430\u0437\u0430\u043D\u043E: ' +\n jsonlLiveRows.length +\n ' \u00B7 \u043E\u043F\u0440\u043E\u0441 <b>300 ms</b> \u00B7 bridge subscribe: <b>' +\n escapeHtml(bridgeSubscribedId === selectedId ? 'on' : 'off') +\n '</b></div>';\n const body = [...jsonlLiveRows]\n .reverse()\n .map((r) => {\n const cls =\n 'jsonl-row' +\n (r._new ? ' new' : '') +\n (r.inLenta ? ' in-lenta' : ' skip');\n const tools = r.tools?.length ? ' \u00B7 tools: ' + r.tools.join(', ') : '';\n const skip = r.inLenta ? '\u2192 \u0432 \u043B\u0435\u043D\u0442\u0443' : '\u2192 skip: ' + (r.skipReason || '?');\n return (\n '<div class=\"' +\n cls +\n '\" data-line=\"' +\n r.lineNo +\n '\"><div class=\"meta\">#' +\n r.lineNo +\n ' \u00B7 ' +\n escapeHtml(r.role) +\n tools +\n ' \u00B7 ' +\n escapeHtml(skip) +\n (r._at ? ' \u00B7 ' + r._at : '') +\n '</div><div class=\"body\">' +\n escapeHtml(r.textPreview || '(\u043F\u0443\u0441\u0442\u043E)') +\n '</div></div>'\n );\n })\n .join('');\n return head + body;\n }\n\n async function pollJsonlLive() {\n if (!selectedId || tab !== 'jsonl-live') return;\n try {\n const q = new URLSearchParams();\n q.set('agentId', selectedId);\n if (selectedTitle) q.set('title', selectedTitle);\n if (jsonlLiveLastLine > 0) q.set('afterLine', String(jsonlLiveLastLine));\n else q.set('tail', '80');\n const res = await apiFetch('/debug/jsonl-live?' + q.toString());\n if (!res.ok) throw new Error('jsonl-live ' + res.status);\n const data = await res.json();\n jsonlLiveFile = data.filePath || '';\n const at = new Date().toLocaleTimeString();\n const incoming = (data.rows || []).map((r) => ({ ...r, _new: true, _at: at }));\n if (jsonlLiveLastLine === 0) {\n jsonlLiveRows = incoming;\n } else if (incoming.length) {\n const seen = new Set(jsonlLiveRows.map((x) => x.lineNo));\n for (const r of incoming) {\n if (!seen.has(r.lineNo)) jsonlLiveRows.push(r);\n }\n }\n if (data.totalLines) jsonlLiveLastLine = data.totalLines;\n else if (incoming.length) {\n jsonlLiveLastLine = Math.max(jsonlLiveLastLine, incoming[incoming.length - 1].lineNo);\n }\n if (jsonlLiveRows.length > 600) jsonlLiveRows = jsonlLiveRows.slice(-500);\n setTimeout(() => {\n for (const r of jsonlLiveRows) r._new = false;\n }, 1200);\n if (tab === 'jsonl-live') {\n renderPanel();\n if (jsonlLiveStickTop) {\n const panel = document.getElementById('panel');\n panel.scrollTop = 0;\n }\n setStatus(\n 'JSONL live: ' +\n jsonlLiveRows.length +\n ' shown \u00B7 file lines ' +\n (data.totalLines ?? '?') +\n ' \u00B7 ' +\n at\n );\n }\n } catch (e) {\n setStatus((e && e.message) || String(e), true);\n }\n }\n\n async function resetJsonlLive() {\n jsonlLiveLastLine = 0;\n jsonlLiveRows = [];\n jsonlLiveFile = '';\n await pollJsonlLive();\n }\n\n function ensureBridgeSubscribe() {\n if (typeof io === 'undefined') return;\n if (!bridgeSocket) {\n const headers = {};\n const t = token();\n if (t) headers.Authorization = 'Bearer ' + t;\n bridgeSocket = io(location.origin, { transports: ['websocket', 'polling'], auth: headers });\n bridgeSocket.on('agent:messages', (p) => {\n const n = p.historyMessages?.length ?? 0;\n const tail = (p.historyMessages || [])[(n || 1) - 1];\n const preview = (tail?.text || '').slice(0, 48);\n setStatus(\n 'app-pipe agent:messages append=' +\n !!p.append +\n ' n=' +\n n +\n ' \u00B7 ' +\n preview\n );\n });\n }\n if (!selectedId || (bridgeSubscribedId === selectedId && tab === 'jsonl-live')) return;\n bridgeSubscribedId = selectedId;\n bridgeSocket.emit('agents:subscribe', {\n agentId: selectedId,\n title: selectedTitle || undefined,\n focus: false,\n });\n setStatus('bridge subscribe ' + selectedId.slice(0, 8) + '\u2026 (\u043A\u0430\u043A \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0435)');\n }\n\n function scheduleJsonlLive() {\n if (jsonlLiveTimer) clearInterval(jsonlLiveTimer);\n jsonlLiveTimer = null;\n if (tab === 'jsonl-live') {\n ensureBridgeSubscribe();\n jsonlLiveTimer = setInterval(() => void pollJsonlLive(), 300);\n }\n }\n\n function renderPanel() {\n const panel = document.getElementById('panel');\n const raw = document.getElementById('raw');\n panel.hidden = tab === 'raw';\n raw.hidden = tab !== 'raw';\n if (tab === 'raw') {\n raw.textContent = JSON.stringify({ snapshot, selectedId, tab }, null, 2);\n return;\n }\n const note = document.getElementById('note');\n note.hidden = tab !== 'lenta' && tab !== 'dom' && tab !== 'compare' && tab !== 'jsonl-live';\n if (tab === 'lenta') {\n note.textContent =\n '\u0413\u043E\u0442\u043E\u0432\u0430\u044F \u043B\u0435\u043D\u0442\u0430: [...JSONL archive, ...DOM overlay] \u2014 \u0442\u043E\u0442 \u0436\u0435 compose, \u0447\u0442\u043E \u0443\u0445\u043E\u0434\u0438\u0442 \u0432 \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0435. \u0417\u0435\u043B\u0451\u043D\u0430\u044F \u0440\u0430\u043C\u043A\u0430 = JSONL, \u0444\u0438\u043E\u043B\u0435\u0442\u043E\u0432\u0430\u044F = DOM (\u0435\u0449\u0451 \u043D\u0435 \u0432 .jsonl). \u041E\u0431\u043D\u043E\u0432\u043B\u044F\u0435\u0442\u0441\u044F \u043F\u043E \u0442\u0430\u0439\u043C\u0435\u0440\u0443 \u0438 \u043F\u043E\u0441\u043B\u0435 \u00AB\u041E\u0431\u043D\u043E\u0432\u0438\u0442\u044C\u00BB.';\n } else if (tab === 'jsonl-live') {\n note.textContent =\n '\u0424\u0430\u0439\u043B .jsonl \u043D\u0430 \u0434\u0438\u0441\u043A\u0435 (\u043E\u043F\u0440\u043E\u0441 300 ms). \u00AB\u2192 \u0432 \u043B\u0435\u043D\u0442\u0443\u00BB = \u043F\u043E\u0441\u043B\u0435 filter \u0432 bridge. \u0412 \u043F\u0440\u0438\u043B\u043E\u0436\u0435\u043D\u0438\u0435 \u0441\u0442\u0440\u043E\u043A\u0430 \u043F\u043E\u043F\u0430\u0434\u0451\u0442 \u0442\u043E\u043B\u044C\u043A\u043E \u0435\u0441\u043B\u0438 Connect \u043E\u0442\u043A\u0440\u044B\u043B \u044D\u0442\u043E\u0442 \u0447\u0430\u0442 (\u043D\u0438\u0436\u0435: bridge subscribe). Debug \u0441\u0430\u043C \u043F\u043E \u0441\u0435\u0431\u0435 app \u043D\u0435 \u043A\u043E\u0440\u043C\u0438\u0442.';\n } else if (tab === 'dom') {\n note.textContent = 'DOM = \u0441\u043C\u043E\u043D\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u043D\u044B\u0435 [data-flat-index] \u0432 Cursor (\u0431\u0435\u0437 JSONL). \u041F\u0440\u0438 \u043E\u0442\u043A\u0440\u044B\u0442\u0438\u0438 \u0447\u0430\u0442\u0430 bridge \u043F\u0440\u043E\u043A\u0440\u0443\u0447\u0438\u0432\u0430\u0435\u0442 \u043B\u0435\u043D\u0442\u0443 \u0432\u043D\u0438\u0437 \u0438 poll. \u041F\u0440\u043E\u043F\u0443\u0441\u043A\u0438 = tool/thought \u0441\u0442\u0440\u043E\u043A\u0438.';\n } else if (tab === 'compare') {\n note.textContent =\n 'JSONL = archive \u0438\u0437 \u0444\u0430\u0439\u043B\u0430; DOM = \u0432\u0435\u0441\u044C viewport Cursor (\u0432\u043A\u043B\u044E\u0447\u0430\u044F tool/\u0441\u0442\u0430\u0442\u0443\u0441\u044B). \u0421\u043E\u043F\u043E\u0441\u0442\u0430\u0432\u043B\u0435\u043D\u0438\u0435: \u0441 \u0445\u0432\u043E\u0441\u0442\u0430 DOM + \u0441\u0440\u0430\u0432\u043D\u0435\u043D\u0438\u0435 \u0442\u0435\u043A\u0441\u0442\u0430 \u0431\u0435\u0437 \u043F\u0440\u043E\u0431\u0435\u043B\u043E\u0432 (URL \u0438\u0437 textContent). \u0417\u0435\u043B\u0451\u043D\u0430\u044F = \u043F\u0430\u0440\u0430 \u043D\u0430\u0439\u0434\u0435\u043D\u0430. \u041A\u0440\u0430\u0441\u043D\u0430\u044F = \u0442\u043E\u043B\u044C\u043A\u043E JSONL. \u0424\u0438\u043E\u043B\u0435\u0442\u043E\u0432\u0430\u044F = \u0442\u043E\u043B\u044C\u043A\u043E DOM (\u043D\u0435\u0442 \u043F\u0430\u0440\u044B \u0432 archive \u0438\u043B\u0438 \u0434\u0440\u0443\u0433\u0430\u044F \u043F\u043E\u0432\u0435\u0440\u0445\u043D\u043E\u0441\u0442\u044C \u0442\u0435\u043A\u0441\u0442\u0430).';\n }\n if (!selectedId && tab !== 'dom' && tab !== 'debug') {\n panel.innerHTML = '<div class=\"empty\">\u0412\u044B\u0431\u0435\u0440\u0438\u0442\u0435 \u0430\u0433\u0435\u043D\u0442\u0430 \u0441\u043B\u0435\u0432\u0430</div>';\n return;\n }\n if (tab === 'lenta') {\n panel.innerHTML = renderLentaFeed(resolveLentaForAgent());\n panel.onscroll = null;\n return;\n }\n if (tab === 'jsonl-live') {\n panel.innerHTML = renderJsonlLive();\n panel.onscroll = () => {\n jsonlLiveStickTop = panel.scrollTop < 80;\n };\n return;\n }\n panel.onscroll = null;\n if (tab === 'history') {\n panel.innerHTML = window.__historyHtml || '<div class=\"empty\">\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430 history\u2026</div>';\n } else if (tab === 'cache') {\n const rows = snapshot?.displayCache?.[selectedId];\n panel.innerHTML = renderBubbles(rows, 'jsonl-cache');\n } else if (tab === 'transcript') {\n const rows =\n snapshot?.domOverlay?.[selectedId] ||\n snapshot?.lentaByAgent?.[selectedId]?.domOverlay ||\n snapshot?.domTranscript?.[selectedId];\n panel.innerHTML = renderBubbles(rows, 'dom-overlay');\n } else if (tab === 'dom') {\n const st = snapshot?.cursor;\n const active = st?.activeComposerId;\n const hint = active ? 'activeComposerId=' + active : '\u043D\u0435\u0442 activeComposerId';\n panel.innerHTML = '<div class=\"empty\" style=\"text-align:left;margin-bottom:8px\">' + escapeHtml(hint) + ' \u00B7 ' + (st?.domMessageCount ?? 0) + ' msgs</div>' + renderBubbles(st?.domMessages, 'dom');\n } else if (tab === 'compare') {\n const hist = window.__historyData || [];\n const dom = snapshot?.cursor?.domMessages || [];\n const active = snapshot?.cursor?.activeComposerId;\n const same = selectedId === active;\n const head = '<div class=\"empty\" style=\"text-align:left\">agent=' + escapeHtml(selectedId.slice(0, 8)) + '\u2026 active=' + escapeHtml((active || '\u2014').slice(0, 8)) + (same ? ' (\u0441\u043E\u0432\u043F\u0430\u0434\u0430\u0435\u0442)' : ' (\u0434\u0440\u0443\u0433\u043E\u0439 \u0447\u0430\u0442 \u0432 Cursor!)') + '</div>';\n panel.innerHTML = head + renderCompare(hist, dom);\n } else if (tab === 'debug') {\n const d = snapshot?.domExtractDebug?.latest;\n if (!d) { panel.innerHTML = '<div class=\"empty\">\u041D\u0435\u0442 extract debug</div>'; return; }\n panel.innerHTML = '<div class=\"bubble system\"><div class=\"tag\">extract debug</div>' + escapeHtml(JSON.stringify(d, null, 2)) + '</div>';\n }\n }\n\n function renderAgents(index) {\n const nav = document.getElementById('agents');\n const repos = index?.repos || [];\n const items = [];\n for (const repo of repos) {\n for (const a of repo.agents || []) {\n items.push({ id: a.id, title: a.title, repo: repo.name });\n }\n }\n if (!items.length) {\n nav.innerHTML = '<div class=\"empty\">agents:index \u043F\u0443\u0441\u0442</div>';\n return;\n }\n nav.innerHTML = items.map((a) => {\n const cls = a.id === selectedId ? ' active' : '';\n 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) + ' \u00B7 ' + escapeHtml(a.id.slice(0, 8)) + '\u2026</div></button>';\n }).join('');\n nav.querySelectorAll('button[data-id]').forEach((btn) => {\n btn.addEventListener('click', () => {\n selectedId = btn.dataset.id;\n selectedTitle = btn.dataset.title || '';\n nav.querySelectorAll('button').forEach((x) => x.classList.toggle('active', x.dataset.id === selectedId));\n void loadHistory();\n if (tab === 'jsonl-live') void resetJsonlLive();\n renderPanel();\n });\n });\n if (!selectedId && items[0]) {\n selectedId = items[0].id;\n selectedTitle = items[0].title || '';\n nav.querySelector('button[data-id=\"' + selectedId + '\"]')?.classList.add('active');\n }\n }\n\n async function loadSnapshot() {\n const res = await apiFetch('/debug/chat-snapshot');\n if (!res.ok) throw new Error('chat-snapshot ' + res.status);\n snapshot = await res.json();\n const h = document.getElementById('health');\n h.textContent = 'CDP ' + (snapshot.health?.cdp ? 'on' : 'off') + ' \u00B7 updated ' + new Date(snapshot.at).toLocaleTimeString();\n }\n\n async function loadHistory() {\n if (!selectedId) return;\n const res = await apiFetch('/api/agents/history?agentId=' + encodeURIComponent(selectedId) + '&limit=120');\n if (!res.ok) throw new Error('history ' + res.status);\n const data = await res.json();\n window.__historyData = data.historyMessages || data.messages || [];\n window.__historyHtml = renderBubbles(window.__historyData, 'jsonl');\n window.__lentaData = {\n messages: data.messages || [],\n jsonlHistory: data.historyMessages || [],\n domOverlay: data.liveMessages || [],\n source: data.source,\n jsonlRowCount: (data.historyMessages || []).length,\n totalMessages: data.totalMessages,\n payloadSeq: data.seq,\n };\n if (tab === 'lenta' || tab === 'history' || tab === 'compare') renderPanel();\n const l = window.__lentaData;\n setStatus(\n 'history: jsonl ' +\n (l.jsonlHistory?.length ?? 0) +\n ' + dom ' +\n (l.domOverlay?.length ?? 0) +\n ' \u2192 lenta ' +\n (l.messages?.length ?? 0) +\n ' / total ' +\n (l.totalMessages ?? '?')\n );\n }\n\n async function loadIndex() {\n const res = await apiFetch('/api/agents/index');\n if (!res.ok) throw new Error('index ' + res.status);\n return res.json();\n }\n\n async function loadAll() {\n try {\n setStatus('\u0417\u0430\u0433\u0440\u0443\u0437\u043A\u0430\u2026');\n await loadSnapshot();\n const index = await loadIndex();\n renderAgents(index);\n await loadHistory();\n renderPanel();\n setStatus('OK \u00B7 ' + new Date().toLocaleTimeString());\n } catch (e) {\n setStatus((e && e.message) || String(e), true);\n }\n }\n\n function schedule() {\n if (timer) clearInterval(timer);\n const sec = Number(document.getElementById('interval').value) || 0;\n if (sec > 0) timer = setInterval(() => void loadAll(), sec * 1000);\n }\n\n schedule();\n scheduleJsonlLive();\n void loadAll();\n})();\n </script>\n</body>\n</html>";
@@ -33,6 +33,10 @@ export const DEBUG_CHATS_PAGE_HTML = `<!DOCTYPE html>
33
33
  .bubble.assistant { align-self: flex-start; background: #2a2a35; }
34
34
  .bubble.system { align-self: center; background: #333; font-size: 12px; color: #bbb; }
35
35
  .bubble .tag { font-size: 10px; color: #888; margin-bottom: 4px; font-family: ui-monospace, monospace; }
36
+ .bubble.segment-jsonl { border: 1px solid #2a4a38; }
37
+ .bubble.segment-dom { border: 1px solid #4a3a5a; box-shadow: 0 0 0 1px #3a2a4a inset; }
38
+ .lenta-head { font-size: 12px; color: #9ab; margin-bottom: 10px; line-height: 1.45; }
39
+ .lenta-divider { align-self: center; font-size: 11px; color: #6a8a9a; padding: 4px 0; letter-spacing: 0.02em; }
36
40
  .raw { flex: 1; overflow: auto; margin: 0; padding: 12px; font-size: 11px; line-height: 1.4; background: #0a0a0a; color: #bdbdbd; }
37
41
  .empty { color: #666; padding: 24px; text-align: center; }
38
42
  .note { font-size: 12px; color: #9ab; padding: 8px 12px; border-bottom: 1px solid #2a2a2a; line-height: 1.45; }
@@ -65,7 +69,8 @@ export const DEBUG_CHATS_PAGE_HTML = `<!DOCTYPE html>
65
69
  <nav class="agents" id="agents"><div class="empty">Загрузка списка…</div></nav>
66
70
  <main>
67
71
  <div class="tabs" id="tabs">
68
- <button type="button" data-tab="history" class="active">JSONL · /api/agents/history</button>
72
+ <button type="button" data-tab="lenta" class="active">Лента · JSONL+DOM</button>
73
+ <button type="button" data-tab="history">JSONL · /api/agents/history</button>
69
74
  <button type="button" data-tab="jsonl-live">JSONL live · файл</button>
70
75
  <button type="button" data-tab="cache">JSONL cache</button>
71
76
  <button type="button" data-tab="transcript">DOM transcript</button>
@@ -89,7 +94,7 @@ export const DEBUG_CHATS_PAGE_HTML = `<!DOCTYPE html>
89
94
  tokenEl.value = params.get('token') || localStorage.getItem('cc_debug_token') || '';
90
95
  tokenEl.addEventListener('change', () => localStorage.setItem('cc_debug_token', tokenEl.value.trim()));
91
96
 
92
- let tab = 'history';
97
+ let tab = 'lenta';
93
98
  let selectedId = params.get('agentId') || '';
94
99
  let selectedTitle = '';
95
100
  let snapshot = null;
@@ -97,7 +102,7 @@ export const DEBUG_CHATS_PAGE_HTML = `<!DOCTYPE html>
97
102
  let jsonlLiveTimer = null;
98
103
  let jsonlLiveLastLine = 0;
99
104
  let jsonlLiveRows = [];
100
- let jsonlLiveStickBottom = true;
105
+ let jsonlLiveStickTop = true;
101
106
  let jsonlLiveFile = '';
102
107
  let bridgeSocket = null;
103
108
  let bridgeSubscribedId = '';
@@ -142,35 +147,127 @@ export const DEBUG_CHATS_PAGE_HTML = `<!DOCTYPE html>
142
147
  function normText(text, role) {
143
148
  let t = String(text || '').replace(/\s+/g, ' ').trim().toLowerCase();
144
149
  if (role === 'assistant') {
145
- t = t.replace(/^#+\\s*/g, '').replace(/\\|/g, ' ').replace(/[-]{3,}/g, ' ');
150
+ t = t.replace(/^#+\\s*/gm, '').replace(/\\|/g, ' ').replace(/[-]{3,}/g, ' ');
146
151
  }
147
152
  return t.replace(/\s+/g, ' ').trim();
148
153
  }
154
+ /** DOM textContent часто рвёт URL: «http:// 127. 0. 0.1» vs JSONL «http://127.0.0.1». */
155
+ function compactMatchText(text, role) {
156
+ return normText(text, role).replace(/\s/g, '');
157
+ }
149
158
  function textsMatch(a, b, role) {
150
159
  const ta = normText(a, role);
151
160
  const tb = normText(b, role);
152
161
  if (!ta || !tb) return false;
153
162
  if (ta === tb) return true;
163
+ const ca = compactMatchText(a, role);
164
+ const cb = compactMatchText(b, role);
165
+ if (ca && cb && ca === cb) return true;
166
+ const shortC = ca.length <= cb.length ? ca : cb;
167
+ const longC = ca.length <= cb.length ? cb : ca;
168
+ if (shortC.length >= 12 && longC.includes(shortC) && shortC.length / longC.length >= 0.35) {
169
+ return true;
170
+ }
154
171
  const short = ta.length <= tb.length ? ta : tb;
155
172
  const long = ta.length <= tb.length ? tb : ta;
156
173
  return short.length >= 12 && long.includes(short) && short.length / long.length >= 0.35;
157
174
  }
158
175
  function renderBubbles(messages, source) {
159
176
  if (!messages || !messages.length) return '<div class="empty">Нет сообщений</div>';
160
- return messages.map((m) => {
177
+ return [...messages].reverse().map((m) => {
161
178
  const role = m.role || 'system';
162
- const tag = roleLabel(role) + ' · ' + source + ' · ' + (m.id || '?') + (m.flatIndex != null ? ' · seq=' + m.flatIndex : '') + (m.html ? ' · html' : '');
179
+ const tag =
180
+ roleLabel(role) +
181
+ ' · ' +
182
+ source +
183
+ ' · ' +
184
+ (m.id || '?') +
185
+ (m.flatIndex != null ? ' · flat=' + m.flatIndex : '') +
186
+ (m.html ? ' · html' : '');
163
187
  const text = (m.text || '').trim() || (m.html ? '[html ' + m.html.length + ' chars — в Cursor с разметкой]' : '(пусто)');
164
188
  return '<div class="bubble ' + role + '"><div class="tag">' + escapeHtml(tag) + '</div>' + escapeHtml(text) + '</div>';
165
189
  }).join('');
166
190
  }
191
+ function resolveLentaForAgent() {
192
+ const snap = snapshot?.lentaByAgent?.[selectedId];
193
+ const api = window.__lentaData;
194
+ const messages =
195
+ (snap?.messages?.length ? snap.messages : null) ||
196
+ (api?.messages?.length ? api.messages : null) ||
197
+ snapshot?.displayMessages?.[selectedId] ||
198
+ [];
199
+ const jsonlHistory = snap?.jsonlHistory ?? api?.jsonlHistory ?? [];
200
+ const domOverlay = snap?.domOverlay ?? api?.domOverlay ?? [];
201
+ const source = snap?.source ?? api?.source ?? (domOverlay.length ? 'hybrid' : 'jsonl');
202
+ const jsonlRowCount = snap?.jsonlRowCount ?? api?.jsonlRowCount ?? jsonlHistory.length;
203
+ const totalMessages = api?.totalMessages ?? messages.length;
204
+ const payloadSeq = api?.payloadSeq ?? snapshot?.lentaPayloadSeq?.[selectedId];
205
+ return { messages, jsonlHistory, domOverlay, source, jsonlRowCount, totalMessages, payloadSeq };
206
+ }
207
+ function renderLentaFeed(lenta) {
208
+ const messages = lenta.messages || [];
209
+ if (!messages.length) {
210
+ return '<div class="empty">Нет ленты — откройте чат в Cursor или дождитесь JSONL</div>';
211
+ }
212
+ const jsonlLen = (lenta.jsonlHistory || []).length;
213
+ const domN = (lenta.domOverlay || []).length;
214
+ const head =
215
+ '<div class="lenta-head">Как в app (<code>agent:messages</code>): <b>' +
216
+ messages.length +
217
+ '</b> пузырей · JSONL <b>' +
218
+ jsonlLen +
219
+ '</b> · DOM live <b>' +
220
+ domN +
221
+ '</b> · source=<b>' +
222
+ escapeHtml(lenta.source || '?') +
223
+ '</b>' +
224
+ (lenta.totalMessages > messages.length
225
+ ? ' · в файле всего ' + lenta.totalMessages
226
+ : '') +
227
+ (lenta.payloadSeq != null ? ' · <b>emitSeq=' + lenta.payloadSeq + '</b>' : '') +
228
+ ' · новые сверху · <span style="opacity:0.85">flat=ключ порядка строки, не emitSeq</span></div>';
229
+ const parts = [];
230
+ for (let i = messages.length - 1; i >= 0; i--) {
231
+ const m = messages[i];
232
+ const isDom = i >= jsonlLen;
233
+ if (i === jsonlLen - 1 && domN > 0 && jsonlLen > 0) {
234
+ parts.push('<div class="lenta-divider">── JSONL archive ──</div>');
235
+ }
236
+ const role = m.role || 'system';
237
+ const seg = isDom ? 'DOM live' : 'JSONL';
238
+ const tag =
239
+ roleLabel(role) +
240
+ ' · ' +
241
+ seg +
242
+ ' · ' +
243
+ (m.id || '?') +
244
+ (m.flatIndex != null ? ' · flat=' + m.flatIndex : '') +
245
+ (m.html ? ' · html' : '');
246
+ const text =
247
+ (m.text || '').trim() ||
248
+ (m.html ? '[html ' + m.html.length + ' chars — в Cursor с разметкой]' : '(пусто)');
249
+ parts.push(
250
+ '<div class="bubble ' +
251
+ role +
252
+ ' segment-' +
253
+ (isDom ? 'dom' : 'jsonl') +
254
+ '"><div class="tag">' +
255
+ escapeHtml(tag) +
256
+ '</div>' +
257
+ escapeHtml(text) +
258
+ '</div>'
259
+ );
260
+ }
261
+ return head + parts.join('');
262
+ }
167
263
  function renderCompare(hist, dom) {
168
264
  const rows = [];
169
265
  const usedDom = new Set();
170
266
  for (let i = 0; i < (hist || []).length; i++) {
171
267
  const h = hist[i];
172
268
  let domIdx = -1;
173
- for (let j = 0; j < (dom || []).length; j++) {
269
+ // DOM viewport = весь смонтированный чат (tool-строки, статусы); совпадение часто в хвосте.
270
+ for (let j = (dom || []).length - 1; j >= 0; j--) {
174
271
  if (usedDom.has(j)) continue;
175
272
  if (h.role === dom[j].role && textsMatch(h.text, dom[j].text, h.role)) {
176
273
  domIdx = j;
@@ -211,7 +308,8 @@ export const DEBUG_CHATS_PAGE_HTML = `<!DOCTYPE html>
211
308
  ' · опрос <b>300 ms</b> · bridge subscribe: <b>' +
212
309
  escapeHtml(bridgeSubscribedId === selectedId ? 'on' : 'off') +
213
310
  '</b></div>';
214
- const body = jsonlLiveRows
311
+ const body = [...jsonlLiveRows]
312
+ .reverse()
215
313
  .map((r) => {
216
314
  const cls =
217
315
  'jsonl-row' +
@@ -273,9 +371,9 @@ export const DEBUG_CHATS_PAGE_HTML = `<!DOCTYPE html>
273
371
  }, 1200);
274
372
  if (tab === 'jsonl-live') {
275
373
  renderPanel();
276
- if (jsonlLiveStickBottom) {
374
+ if (jsonlLiveStickTop) {
277
375
  const panel = document.getElementById('panel');
278
- panel.scrollTop = panel.scrollHeight;
376
+ panel.scrollTop = 0;
279
377
  }
280
378
  setStatus(
281
379
  'JSONL live: ' +
@@ -348,25 +446,32 @@ export const DEBUG_CHATS_PAGE_HTML = `<!DOCTYPE html>
348
446
  return;
349
447
  }
350
448
  const note = document.getElementById('note');
351
- note.hidden = tab !== 'dom' && tab !== 'compare' && tab !== 'jsonl-live';
352
- if (tab === 'jsonl-live') {
449
+ note.hidden = tab !== 'lenta' && tab !== 'dom' && tab !== 'compare' && tab !== 'jsonl-live';
450
+ if (tab === 'lenta') {
451
+ note.textContent =
452
+ 'Готовая лента: [...JSONL archive, ...DOM overlay] — тот же compose, что уходит в приложение. Зелёная рамка = JSONL, фиолетовая = DOM (ещё не в .jsonl). Обновляется по таймеру и после «Обновить».';
453
+ } else if (tab === 'jsonl-live') {
353
454
  note.textContent =
354
455
  'Файл .jsonl на диске (опрос 300 ms). «→ в ленту» = после filter в bridge. В приложение строка попадёт только если Connect открыл этот чат (ниже: bridge subscribe). Debug сам по себе app не кормит.';
355
456
  } else if (tab === 'dom') {
356
457
  note.textContent = 'DOM = смонтированные [data-flat-index] в Cursor (без JSONL). При открытии чата bridge прокручивает ленту вниз и poll. Пропуски = tool/thought строки.';
357
458
  } else if (tab === 'compare') {
358
459
  note.textContent =
359
- 'Зелёная = JSONL-лента (API) и DOM совпали. Красная = только в JSONL. Фиолетовая (domonly) = видно в DOM Cursor, слева «—» (нет в ленте API): схлопнули несколько ответов ассистента в один, или DOM ещё держит короткий статус («Ищу…», «Добавляю…»), а в API уже только финальный «Сделано…». Вкладка «JSONL cache» = то, что уходит в app.';
460
+ 'JSONL = archive из файла; DOM = весь viewport Cursor (включая tool/статусы). Сопоставление: с хвоста DOM + сравнение текста без пробелов (URL из textContent). Зелёная = пара найдена. Красная = только JSONL. Фиолетовая = только DOM (нет пары в archive или другая поверхность текста).';
360
461
  }
361
462
  if (!selectedId && tab !== 'dom' && tab !== 'debug') {
362
463
  panel.innerHTML = '<div class="empty">Выберите агента слева</div>';
363
464
  return;
364
465
  }
466
+ if (tab === 'lenta') {
467
+ panel.innerHTML = renderLentaFeed(resolveLentaForAgent());
468
+ panel.onscroll = null;
469
+ return;
470
+ }
365
471
  if (tab === 'jsonl-live') {
366
472
  panel.innerHTML = renderJsonlLive();
367
473
  panel.onscroll = () => {
368
- const nearBottom = panel.scrollHeight - panel.scrollTop - panel.clientHeight < 80;
369
- jsonlLiveStickBottom = nearBottom;
474
+ jsonlLiveStickTop = panel.scrollTop < 80;
370
475
  };
371
476
  return;
372
477
  }
@@ -377,8 +482,11 @@ export const DEBUG_CHATS_PAGE_HTML = `<!DOCTYPE html>
377
482
  const rows = snapshot?.displayCache?.[selectedId];
378
483
  panel.innerHTML = renderBubbles(rows, 'jsonl-cache');
379
484
  } else if (tab === 'transcript') {
380
- const rows = snapshot?.domTranscript?.[selectedId];
381
- panel.innerHTML = renderBubbles(rows, 'dom-transcript');
485
+ const rows =
486
+ snapshot?.domOverlay?.[selectedId] ||
487
+ snapshot?.lentaByAgent?.[selectedId]?.domOverlay ||
488
+ snapshot?.domTranscript?.[selectedId];
489
+ panel.innerHTML = renderBubbles(rows, 'dom-overlay');
382
490
  } else if (tab === 'dom') {
383
491
  const st = snapshot?.cursor;
384
492
  const active = st?.activeComposerId;
@@ -386,12 +494,7 @@ export const DEBUG_CHATS_PAGE_HTML = `<!DOCTYPE html>
386
494
  panel.innerHTML = '<div class="empty" style="text-align:left;margin-bottom:8px">' + escapeHtml(hint) + ' · ' + (st?.domMessageCount ?? 0) + ' msgs</div>' + renderBubbles(st?.domMessages, 'dom');
387
495
  } else if (tab === 'compare') {
388
496
  const hist = window.__historyData || [];
389
- const dom =
390
- (selectedId && snapshot?.domTranscript?.[selectedId]?.length
391
- ? snapshot.domTranscript[selectedId]
392
- : null) ||
393
- snapshot?.cursor?.domMessages ||
394
- [];
497
+ const dom = snapshot?.cursor?.domMessages || [];
395
498
  const active = snapshot?.cursor?.activeComposerId;
396
499
  const same = selectedId === active;
397
500
  const head = '<div class="empty" style="text-align:left">agent=' + escapeHtml(selectedId.slice(0, 8)) + '… active=' + escapeHtml((active || '—').slice(0, 8)) + (same ? ' (совпадает)' : ' (другой чат в Cursor!)') + '</div>';
@@ -450,10 +553,29 @@ export const DEBUG_CHATS_PAGE_HTML = `<!DOCTYPE html>
450
553
  const res = await apiFetch('/api/agents/history?agentId=' + encodeURIComponent(selectedId) + '&limit=120');
451
554
  if (!res.ok) throw new Error('history ' + res.status);
452
555
  const data = await res.json();
453
- window.__historyData = data.messages || [];
556
+ window.__historyData = data.historyMessages || data.messages || [];
454
557
  window.__historyHtml = renderBubbles(window.__historyData, 'jsonl');
455
- if (tab === 'history' || tab === 'compare') renderPanel();
456
- setStatus('history: ' + (data.messages?.length ?? 0) + ' / total ' + (data.totalMessages ?? '?'));
558
+ window.__lentaData = {
559
+ messages: data.messages || [],
560
+ jsonlHistory: data.historyMessages || [],
561
+ domOverlay: data.liveMessages || [],
562
+ source: data.source,
563
+ jsonlRowCount: (data.historyMessages || []).length,
564
+ totalMessages: data.totalMessages,
565
+ payloadSeq: data.seq,
566
+ };
567
+ if (tab === 'lenta' || tab === 'history' || tab === 'compare') renderPanel();
568
+ const l = window.__lentaData;
569
+ setStatus(
570
+ 'history: jsonl ' +
571
+ (l.jsonlHistory?.length ?? 0) +
572
+ ' + dom ' +
573
+ (l.domOverlay?.length ?? 0) +
574
+ ' → lenta ' +
575
+ (l.messages?.length ?? 0) +
576
+ ' / total ' +
577
+ (l.totalMessages ?? '?')
578
+ );
457
579
  }
458
580
 
459
581
  async function loadIndex() {
@@ -14,4 +14,6 @@ export declare class DomTranscriptStore {
14
14
  list(agentId: string): ChatMessage[];
15
15
  /** Drop overlay rows once the same turn appears in JSONL. */
16
16
  pruneCoveredBy(agentId: string, baseline: ChatMessage[]): void;
17
+ /** Keep transcript aligned with current CDP viewport (drop scrolled-away bubbles). */
18
+ reconcileToViewport(agentId: string, snapshot: ChatMessage[]): void;
17
19
  }
@@ -1,4 +1,4 @@
1
- import { messagesEquivalent, pickPreferredMessage, sortMessagesChronologically, } from './chat-display.js';
1
+ import { archiveCoversOverlay, messagesEquivalent, pickPreferredMessage, sortMessagesChronologically, } from './chat-display.js';
2
2
  /** Stable key for upsert across DOM polls (prefer extract id). */
3
3
  function transcriptKey(m) {
4
4
  if (m.id?.trim())
@@ -68,9 +68,24 @@ export class DomTranscriptStore {
68
68
  if (!map?.size || !baseline.length)
69
69
  return;
70
70
  for (const [key, row] of [...map.entries()]) {
71
- if (baseline.some((b) => messagesEquivalent(b, row))) {
71
+ if (baseline.some((b) => archiveCoversOverlay(b, row))) {
72
72
  map.delete(key);
73
73
  }
74
74
  }
75
75
  }
76
+ /** Keep transcript aligned with current CDP viewport (drop scrolled-away bubbles). */
77
+ reconcileToViewport(agentId, snapshot) {
78
+ const map = this.rowsByAgent.get(agentId);
79
+ if (!map?.size)
80
+ return;
81
+ if (!snapshot.length) {
82
+ this.rowsByAgent.delete(agentId);
83
+ return;
84
+ }
85
+ const visibleKeys = new Set(snapshot.map((m) => transcriptKey(m)));
86
+ for (const key of [...map.keys()]) {
87
+ if (!visibleKeys.has(key))
88
+ map.delete(key);
89
+ }
90
+ }
76
91
  }
@@ -1083,9 +1083,10 @@ export function extractionFunction(containerSelectors, tabSelectors, inputSelect
1083
1083
  const backgroundStatusText = pendingApprovals.length === 0 && !pendingQuestionnaire
1084
1084
  ? findBackgroundWorkStatusText(container)
1085
1085
  : undefined;
1086
+ const hasActiveGeneration = detectAgentWorking(container);
1086
1087
  const agentWorking = pendingApprovals.length === 0 &&
1087
1088
  !pendingQuestionnaire &&
1088
- (detectAgentWorking(container) || !!backgroundStatusText);
1089
+ hasActiveGeneration;
1089
1090
  let agentStatus = document.querySelector('span.auxiliary-bar-chat-title')?.textContent?.trim() ||
1090
1091
  undefined;
1091
1092
  let agentStatusMessage;
@@ -1093,12 +1094,12 @@ export function extractionFunction(containerSelectors, tabSelectors, inputSelect
1093
1094
  agentStatus = 'waiting_questionnaire';
1094
1095
  else if (pendingApprovals.length > 0)
1095
1096
  agentStatus = 'waiting_approval';
1096
- else if (agentWorking && backgroundStatusText) {
1097
+ else if (hasActiveGeneration)
1098
+ agentStatus = 'working';
1099
+ else if (backgroundStatusText) {
1097
1100
  agentStatus = 'background_shell';
1098
1101
  agentStatusMessage = backgroundStatusText;
1099
1102
  }
1100
- else if (agentWorking)
1101
- agentStatus = 'working';
1102
1103
  let contextPercent;
1103
1104
  let terminalCount;
1104
1105
  for (const el of Array.from(document.querySelectorAll('button, span, div'))) {
@@ -0,0 +1,46 @@
1
+ import type { ChatMessage, HistoryMessage } from './types.js';
2
+ export type LentaCaptureReason = 'jsonl_file' | 'dom_ingest' | 'emit_agent_messages' | 'manual';
3
+ export interface LentaCaptureConfig {
4
+ enabled: boolean;
5
+ agentId: string;
6
+ title?: string;
7
+ }
8
+ export interface LentaCapturePayload {
9
+ reason: LentaCaptureReason;
10
+ source?: 'dom' | 'jsonl' | 'hybrid';
11
+ emitSeq?: number;
12
+ seqHeld?: boolean;
13
+ transition?: string;
14
+ jsonlRowCount?: number;
15
+ historyMessages: ChatMessage[];
16
+ liveMessages: ChatMessage[];
17
+ messages: ChatMessage[];
18
+ domRaw?: ChatMessage[];
19
+ domViewport?: ChatMessage[];
20
+ jsonlRawRows?: HistoryMessage[];
21
+ jsonlFilePath?: string;
22
+ activeComposerId?: string;
23
+ agentWorking?: boolean;
24
+ }
25
+ export declare function captureConfigPath(): string;
26
+ export declare function readCaptureConfig(): LentaCaptureConfig | null;
27
+ export declare function writeCaptureConfig(cfg: LentaCaptureConfig): void;
28
+ export declare function clearCaptureConfig(): void;
29
+ export declare function captureSessionDir(agentId: string): string;
30
+ export declare class LentaCaptureSession {
31
+ private readonly agentId;
32
+ private step;
33
+ private lastSig;
34
+ constructor(agentId: string);
35
+ shouldCapture(targetAgentId: string): boolean;
36
+ record(payload: LentaCapturePayload): string | null;
37
+ }
38
+ export declare function getLentaCaptureSession(agentId: string): LentaCaptureSession | null;
39
+ export declare function captureLentaIfEnabled(agentId: string, payload: LentaCapturePayload): void;
40
+ export declare function resolveAgentJsonlPath(projectsDir: string, agentId: string, opts: {
41
+ title?: string;
42
+ composerIdByTitle?: Record<string, string>;
43
+ activeComposerId?: string;
44
+ activeTabTitle?: string;
45
+ }): string | undefined;
46
+ export declare function listCaptureSteps(agentId: string): string[];