cursorconnect 0.1.7 → 0.1.9
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.
- package/bridge-runtime/.env.example +7 -1
- package/bridge-runtime/connector-version.json +1 -1
- package/bridge-runtime/dist/agent-completion-push.d.ts +27 -22
- package/bridge-runtime/dist/agent-completion-push.js +242 -122
- package/bridge-runtime/dist/agent-completion-readiness.d.ts +19 -0
- package/bridge-runtime/dist/agent-completion-readiness.js +42 -0
- package/bridge-runtime/dist/chat-display-store.d.ts +32 -7
- package/bridge-runtime/dist/chat-display-store.js +99 -21
- package/bridge-runtime/dist/chat-display.d.ts +36 -0
- package/bridge-runtime/dist/chat-display.js +287 -24
- package/bridge-runtime/dist/chat-sync.d.ts +3 -1
- package/bridge-runtime/dist/chat-sync.js +20 -0
- package/bridge-runtime/dist/config.js +2 -0
- package/bridge-runtime/dist/connector-client-version.js +1 -1
- package/bridge-runtime/dist/debug-chats-page.d.ts +1 -1
- package/bridge-runtime/dist/debug-chats-page.js +148 -26
- package/bridge-runtime/dist/dom-transcript-store.d.ts +3 -1
- package/bridge-runtime/dist/dom-transcript-store.js +18 -3
- package/bridge-runtime/dist/extract-page.js +5 -4
- package/bridge-runtime/dist/index.js +9 -0
- package/bridge-runtime/dist/keep-awake.d.ts +5 -0
- package/bridge-runtime/dist/keep-awake.js +48 -0
- package/bridge-runtime/dist/lenta-capture.d.ts +46 -0
- package/bridge-runtime/dist/lenta-capture.js +146 -0
- package/bridge-runtime/dist/lenta-debug.d.ts +42 -0
- package/bridge-runtime/dist/lenta-debug.js +221 -0
- package/bridge-runtime/dist/lenta-delivery.d.ts +3 -0
- package/bridge-runtime/dist/lenta-delivery.js +10 -0
- package/bridge-runtime/dist/lenta-seq-journal.d.ts +48 -0
- package/bridge-runtime/dist/lenta-seq-journal.js +109 -0
- package/bridge-runtime/dist/message-filter.d.ts +5 -0
- package/bridge-runtime/dist/message-filter.js +4 -0
- package/bridge-runtime/dist/relay-upstream.d.ts +3 -0
- package/bridge-runtime/dist/relay-upstream.js +21 -0
- package/bridge-runtime/dist/relay.d.ts +47 -3
- package/bridge-runtime/dist/relay.js +667 -96
- package/bridge-runtime/dist/types.d.ts +13 -4
- package/dist/bridge-build.js +50 -0
- package/dist/index.js +9 -6
- package/dist/launch.js +5 -1
- package/dist/run-service.js +10 -4
- package/dist/startup-check.js +6 -0
- package/package.json +1 -1
- package/version-policy.json +2 -2
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isPassiveBackgroundShellState } from './message-filter.js';
|
|
1
2
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2
3
|
export function normalizeAgentTitle(title) {
|
|
3
4
|
return title
|
|
@@ -31,6 +32,25 @@ export function resolveCursorActiveComposerId(state) {
|
|
|
31
32
|
}
|
|
32
33
|
return activeTab?.composerId || activeTab?.id || state.activeComposerId;
|
|
33
34
|
}
|
|
35
|
+
/** Cursor is generating for this composer (active tab or sidebar spinner). */
|
|
36
|
+
export function isAgentGenerating(agentId, state) {
|
|
37
|
+
if (!agentId)
|
|
38
|
+
return false;
|
|
39
|
+
const activeId = resolveCursorActiveComposerId(state);
|
|
40
|
+
if (activeId === agentId && isPassiveBackgroundShellState(state))
|
|
41
|
+
return false;
|
|
42
|
+
if (activeId === agentId && state.agentWorking)
|
|
43
|
+
return true;
|
|
44
|
+
for (const tab of state.tabs) {
|
|
45
|
+
const id = tab.composerId ?? tab.id;
|
|
46
|
+
if (id !== agentId || !tab.isWorking)
|
|
47
|
+
continue;
|
|
48
|
+
if (activeId === agentId && isPassiveBackgroundShellState(state))
|
|
49
|
+
continue;
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
34
54
|
function isSyntheticRouteId(agentId) {
|
|
35
55
|
return /^sidebar-\d+$/.test(agentId) || agentId.startsWith('title:');
|
|
36
56
|
}
|
|
@@ -29,6 +29,8 @@ export function loadConfig() {
|
|
|
29
29
|
relayToken: process.env.RELAY_TOKEN?.trim() ?? '',
|
|
30
30
|
relayRoomId: identity?.roomId ?? relayRoomFromEnv,
|
|
31
31
|
pairingClientToken: identity?.clientToken,
|
|
32
|
+
keepAwakeEnabled: process.platform === 'darwin' && process.env.KEEP_AWAKE?.trim() !== '0',
|
|
33
|
+
relayKeepaliveMs: Math.max(0, parseInt(process.env.RELAY_KEEPALIVE_MS ?? '20000', 10) || 0),
|
|
32
34
|
};
|
|
33
35
|
}
|
|
34
36
|
export function loadSelectors() {
|
|
@@ -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,'&').replace(/</g,'<').replace(/>/g,'>');\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,'&').replace(/</g,'<').replace(/>/g,'>');\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="
|
|
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 = '
|
|
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
|
|
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*/
|
|
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 =
|
|
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
|
-
|
|
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 (
|
|
374
|
+
if (jsonlLiveStickTop) {
|
|
277
375
|
const panel = document.getElementById('panel');
|
|
278
|
-
panel.scrollTop =
|
|
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 === '
|
|
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
|
-
'
|
|
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
|
-
|
|
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 =
|
|
381
|
-
|
|
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
|
-
|
|
456
|
-
|
|
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() {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { ChatMessage } from './types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Append-only per-agent transcript built from DOM snapshots.
|
|
4
|
-
*
|
|
4
|
+
* DOM transcript for live overlay until JSONL covers the same turn.
|
|
5
5
|
*/
|
|
6
6
|
export declare class DomTranscriptStore {
|
|
7
7
|
private rowsByAgent;
|
|
@@ -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())
|
|
@@ -7,7 +7,7 @@ function transcriptKey(m) {
|
|
|
7
7
|
}
|
|
8
8
|
/**
|
|
9
9
|
* Append-only per-agent transcript built from DOM snapshots.
|
|
10
|
-
*
|
|
10
|
+
* DOM transcript for live overlay until JSONL covers the same turn.
|
|
11
11
|
*/
|
|
12
12
|
export class DomTranscriptStore {
|
|
13
13
|
rowsByAgent = new Map();
|
|
@@ -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) =>
|
|
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
|
-
|
|
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 (
|
|
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'))) {
|
|
@@ -8,8 +8,11 @@ import { WindowMonitor } from './window-monitor.js';
|
|
|
8
8
|
import { Relay } from './relay.js';
|
|
9
9
|
import { MessageDebugStore } from './message-debug-store.js';
|
|
10
10
|
import { connectorClientVersion } from './connector-client-version.js';
|
|
11
|
+
import { installKeepAwakeShutdown, startKeepAwake } from './keep-awake.js';
|
|
11
12
|
async function main() {
|
|
12
13
|
const config = loadConfig();
|
|
14
|
+
startKeepAwake(config.keepAwakeEnabled);
|
|
15
|
+
installKeepAwakeShutdown();
|
|
13
16
|
const selectors = loadSelectors();
|
|
14
17
|
const connectorVersion = connectorClientVersion();
|
|
15
18
|
console.log('=== CursorConnect Bridge ===');
|
|
@@ -18,6 +21,12 @@ async function main() {
|
|
|
18
21
|
console.log(`Server: http://${config.serverHost}:${config.serverPort}`);
|
|
19
22
|
if (config.relayUrl) {
|
|
20
23
|
console.log(`Relay upstream: ${config.relayUrl} (room=${config.relayRoomId})`);
|
|
24
|
+
if (config.relayKeepaliveMs > 0) {
|
|
25
|
+
console.log(`Relay keepalive: every ${config.relayKeepaliveMs}ms`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
if (config.keepAwakeEnabled) {
|
|
29
|
+
console.log('Keep-awake: on (macOS caffeinate)');
|
|
21
30
|
}
|
|
22
31
|
console.log(`Projects: ${config.cursorProjectsDir}`);
|
|
23
32
|
const stateManager = new StateManager(config.debounceMs);
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** macOS: `caffeinate -w <pid>` — Mac не уходит в сон, пока живёт bridge. */
|
|
2
|
+
export declare function startKeepAwake(enabled: boolean): void;
|
|
3
|
+
export declare function stopKeepAwake(): void;
|
|
4
|
+
export declare function isKeepAwakeActive(): boolean;
|
|
5
|
+
export declare function installKeepAwakeShutdown(): void;
|