cursorconnect 0.1.6 → 0.1.7
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 +10 -2
- package/bridge-runtime/connector-version.json +1 -1
- package/bridge-runtime/dist/agent-completion-push.d.ts +42 -0
- package/bridge-runtime/dist/agent-completion-push.js +220 -0
- package/bridge-runtime/dist/agent-title-match.d.ts +8 -7
- package/bridge-runtime/dist/agent-title-match.js +11 -1
- package/bridge-runtime/dist/chat-display-store.d.ts +21 -9
- package/bridge-runtime/dist/chat-display-store.js +94 -23
- package/bridge-runtime/dist/chat-display.d.ts +2 -0
- package/bridge-runtime/dist/chat-display.js +197 -33
- package/bridge-runtime/dist/chat-history-mode.d.ts +5 -0
- package/bridge-runtime/dist/chat-history-mode.js +7 -0
- package/bridge-runtime/dist/command-executor.d.ts +2 -0
- package/bridge-runtime/dist/command-executor.js +44 -0
- package/bridge-runtime/dist/composer-title-index.d.ts +1 -0
- package/bridge-runtime/dist/composer-title-index.js +7 -7
- package/bridge-runtime/dist/debug-chats-page.d.ts +2 -0
- package/bridge-runtime/dist/debug-chats-page.js +491 -0
- package/bridge-runtime/dist/dom-transcript-store.d.ts +17 -0
- package/bridge-runtime/dist/dom-transcript-store.js +76 -0
- package/bridge-runtime/dist/extract-page.js +56 -85
- package/bridge-runtime/dist/history-limit.d.ts +2 -0
- package/bridge-runtime/dist/history-limit.js +2 -0
- package/bridge-runtime/dist/history-request.d.ts +8 -0
- package/bridge-runtime/dist/history-request.js +7 -0
- package/bridge-runtime/dist/index.js +1 -0
- package/bridge-runtime/dist/jsonl-index.d.ts +21 -3
- package/bridge-runtime/dist/jsonl-index.js +237 -73
- package/bridge-runtime/dist/jsonl-live-debug.d.ts +24 -0
- package/bridge-runtime/dist/jsonl-live-debug.js +175 -0
- package/bridge-runtime/dist/media-path.d.ts +2 -0
- package/bridge-runtime/dist/media-path.js +17 -0
- package/bridge-runtime/dist/message-filter.d.ts +2 -0
- package/bridge-runtime/dist/message-filter.js +21 -5
- package/bridge-runtime/dist/pairing-code.d.ts +2 -0
- package/bridge-runtime/dist/pairing-code.js +9 -2
- package/bridge-runtime/dist/relay-upstream.d.ts +2 -1
- package/bridge-runtime/dist/relay-upstream.js +4 -1
- package/bridge-runtime/dist/relay.d.ts +21 -0
- package/bridge-runtime/dist/relay.js +332 -28
- package/bridge-runtime/dist/types.d.ts +21 -0
- package/bridge-runtime/selectors.json +4 -5
- package/dist/index.js +79 -20
- package/dist/launch.js +23 -5
- package/dist/macos-autostart.js +87 -0
- package/dist/pairing-code.js +12 -3
- package/dist/print-pairing.js +2 -0
- package/dist/run-service.js +31 -0
- package/dist/startup-check.js +165 -0
- package/package.json +1 -1
- package/version-policy.json +1 -1
|
@@ -0,0 +1,491 @@
|
|
|
1
|
+
/** Read-only browser UI: shows what bridge already holds (no writes). */
|
|
2
|
+
export const DEBUG_CHATS_PAGE_HTML = `<!DOCTYPE html>
|
|
3
|
+
<html lang="ru">
|
|
4
|
+
<head>
|
|
5
|
+
<meta charset="utf-8" />
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
7
|
+
<title>CursorConnect — bridge data</title>
|
|
8
|
+
<style>
|
|
9
|
+
:root { color-scheme: dark; font-family: system-ui, sans-serif; font-size: 14px; }
|
|
10
|
+
* { box-sizing: border-box; }
|
|
11
|
+
body { margin: 0; background: #111; color: #e8e8e8; }
|
|
12
|
+
header { padding: 10px 14px; border-bottom: 1px solid #333; display: flex; flex-wrap: wrap; gap: 8px; align-items: center; }
|
|
13
|
+
header h1 { font-size: 15px; margin: 0 12px 0 0; font-weight: 600; }
|
|
14
|
+
label { font-size: 12px; color: #aaa; }
|
|
15
|
+
input, select, button { background: #1c1c1c; border: 1px solid #444; color: #eee; border-radius: 6px; padding: 6px 8px; }
|
|
16
|
+
button { cursor: pointer; }
|
|
17
|
+
button:hover { border-color: #666; }
|
|
18
|
+
.layout { display: grid; grid-template-columns: 260px 1fr; min-height: calc(100vh - 52px); }
|
|
19
|
+
.agents { border-right: 1px solid #333; overflow: auto; max-height: calc(100vh - 52px); }
|
|
20
|
+
.agents button { display: block; width: 100%; text-align: left; border: none; border-bottom: 1px solid #2a2a2a; border-radius: 0; padding: 10px 12px; background: transparent; }
|
|
21
|
+
.agents button:hover { background: #1a1a1a; }
|
|
22
|
+
.agents button.active { background: #243044; }
|
|
23
|
+
.agents .meta { font-size: 11px; color: #888; margin-top: 2px; }
|
|
24
|
+
main { display: flex; flex-direction: column; min-width: 0; }
|
|
25
|
+
.tabs { display: flex; gap: 4px; padding: 8px 10px; border-bottom: 1px solid #333; flex-wrap: wrap; }
|
|
26
|
+
.tabs button { font-size: 12px; }
|
|
27
|
+
.tabs button.active { background: #2a3f5f; border-color: #5a7ab0; }
|
|
28
|
+
.status { padding: 6px 12px; font-size: 12px; color: #9ab; border-bottom: 1px solid #2a2a2a; }
|
|
29
|
+
.status.err { color: #f88; }
|
|
30
|
+
.chat { flex: 1; overflow: auto; padding: 12px; display: flex; flex-direction: column; gap: 10px; }
|
|
31
|
+
.bubble { max-width: 92%; padding: 8px 12px; border-radius: 10px; white-space: pre-wrap; word-break: break-word; }
|
|
32
|
+
.bubble.user { align-self: flex-end; background: #2d4a3e; }
|
|
33
|
+
.bubble.assistant { align-self: flex-start; background: #2a2a35; }
|
|
34
|
+
.bubble.system { align-self: center; background: #333; font-size: 12px; color: #bbb; }
|
|
35
|
+
.bubble .tag { font-size: 10px; color: #888; margin-bottom: 4px; font-family: ui-monospace, monospace; }
|
|
36
|
+
.raw { flex: 1; overflow: auto; margin: 0; padding: 12px; font-size: 11px; line-height: 1.4; background: #0a0a0a; color: #bdbdbd; }
|
|
37
|
+
.empty { color: #666; padding: 24px; text-align: center; }
|
|
38
|
+
.note { font-size: 12px; color: #9ab; padding: 8px 12px; border-bottom: 1px solid #2a2a2a; line-height: 1.45; }
|
|
39
|
+
table.cmp { width: 100%; border-collapse: collapse; font-size: 12px; }
|
|
40
|
+
table.cmp th, table.cmp td { border: 1px solid #333; padding: 6px 8px; vertical-align: top; }
|
|
41
|
+
table.cmp th { background: #1a1a1a; text-align: left; }
|
|
42
|
+
table.cmp tr.match td { background: #152515; }
|
|
43
|
+
table.cmp tr.miss td { background: #2a1818; }
|
|
44
|
+
table.cmp tr.domonly td { background: #1a1a2a; }
|
|
45
|
+
.jsonl-live-head { font-size: 12px; color: #9ab; margin-bottom: 10px; line-height: 1.45; }
|
|
46
|
+
.jsonl-row { border-left: 3px solid #444; padding: 8px 10px; margin-bottom: 6px; background: #161616; border-radius: 4px; }
|
|
47
|
+
.jsonl-row.new { animation: jsonl-flash 1.2s ease; border-left-color: #5a9fd4; }
|
|
48
|
+
.jsonl-row.in-lenta { border-left-color: #3d7a52; }
|
|
49
|
+
.jsonl-row.skip { border-left-color: #8a4040; }
|
|
50
|
+
.jsonl-row .meta { font-size: 10px; color: #888; font-family: ui-monospace, monospace; margin-bottom: 4px; }
|
|
51
|
+
.jsonl-row .body { white-space: pre-wrap; word-break: break-word; font-size: 13px; }
|
|
52
|
+
@keyframes jsonl-flash { from { background: #243044; } to { background: #161616; } }
|
|
53
|
+
@media (max-width: 720px) { .layout { grid-template-columns: 1fr; } .agents { max-height: 180px; } }
|
|
54
|
+
</style>
|
|
55
|
+
</head>
|
|
56
|
+
<body>
|
|
57
|
+
<header>
|
|
58
|
+
<h1>Bridge data (read-only)</h1>
|
|
59
|
+
<label>Token <input id="token" type="password" size="28" placeholder="если WEBAPP_PASSWORD" /></label>
|
|
60
|
+
<label>Refresh <input id="interval" type="number" min="0" max="60" value="2" style="width:48px" /> s</label>
|
|
61
|
+
<button type="button" id="btnRefresh">Обновить</button>
|
|
62
|
+
<span id="health" style="font-size:12px;color:#8a8"></span>
|
|
63
|
+
</header>
|
|
64
|
+
<div class="layout">
|
|
65
|
+
<nav class="agents" id="agents"><div class="empty">Загрузка списка…</div></nav>
|
|
66
|
+
<main>
|
|
67
|
+
<div class="tabs" id="tabs">
|
|
68
|
+
<button type="button" data-tab="history" class="active">JSONL · /api/agents/history</button>
|
|
69
|
+
<button type="button" data-tab="jsonl-live">JSONL live · файл</button>
|
|
70
|
+
<button type="button" data-tab="cache">JSONL cache</button>
|
|
71
|
+
<button type="button" data-tab="transcript">DOM transcript</button>
|
|
72
|
+
<button type="button" data-tab="dom">DOM · state.messages</button>
|
|
73
|
+
<button type="button" data-tab="compare">Сравнение JSONL ↔ DOM</button>
|
|
74
|
+
<button type="button" data-tab="debug">DOM extract debug</button>
|
|
75
|
+
<button type="button" data-tab="raw">JSON snapshot</button>
|
|
76
|
+
</div>
|
|
77
|
+
<div class="status" id="status">—</div>
|
|
78
|
+
<div class="note" id="note" hidden></div>
|
|
79
|
+
<div class="chat" id="panel"></div>
|
|
80
|
+
<pre class="raw" id="raw" hidden></pre>
|
|
81
|
+
</main>
|
|
82
|
+
</div>
|
|
83
|
+
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js"></script>
|
|
84
|
+
<script>
|
|
85
|
+
(function () {
|
|
86
|
+
const params = new URLSearchParams(location.search);
|
|
87
|
+
const roomId = params.get('roomId') || '';
|
|
88
|
+
const tokenEl = document.getElementById('token');
|
|
89
|
+
tokenEl.value = params.get('token') || localStorage.getItem('cc_debug_token') || '';
|
|
90
|
+
tokenEl.addEventListener('change', () => localStorage.setItem('cc_debug_token', tokenEl.value.trim()));
|
|
91
|
+
|
|
92
|
+
let tab = 'history';
|
|
93
|
+
let selectedId = params.get('agentId') || '';
|
|
94
|
+
let selectedTitle = '';
|
|
95
|
+
let snapshot = null;
|
|
96
|
+
let timer = null;
|
|
97
|
+
let jsonlLiveTimer = null;
|
|
98
|
+
let jsonlLiveLastLine = 0;
|
|
99
|
+
let jsonlLiveRows = [];
|
|
100
|
+
let jsonlLiveStickBottom = true;
|
|
101
|
+
let jsonlLiveFile = '';
|
|
102
|
+
let bridgeSocket = null;
|
|
103
|
+
let bridgeSubscribedId = '';
|
|
104
|
+
|
|
105
|
+
document.getElementById('tabs').addEventListener('click', (e) => {
|
|
106
|
+
const b = e.target.closest('button[data-tab]');
|
|
107
|
+
if (!b) return;
|
|
108
|
+
tab = b.dataset.tab;
|
|
109
|
+
document.querySelectorAll('#tabs button').forEach((x) => x.classList.toggle('active', x === b));
|
|
110
|
+
scheduleJsonlLive();
|
|
111
|
+
if (tab === 'jsonl-live') void resetJsonlLive();
|
|
112
|
+
renderPanel();
|
|
113
|
+
});
|
|
114
|
+
document.getElementById('btnRefresh').addEventListener('click', () => void loadAll());
|
|
115
|
+
document.getElementById('interval').addEventListener('change', schedule);
|
|
116
|
+
|
|
117
|
+
function token() { return tokenEl.value.trim(); }
|
|
118
|
+
function apiUrl(path) {
|
|
119
|
+
const u = new URL(path, location.origin);
|
|
120
|
+
if (roomId) u.searchParams.set('roomId', roomId);
|
|
121
|
+
const t = token();
|
|
122
|
+
if (t) u.searchParams.set('token', t);
|
|
123
|
+
return u.toString();
|
|
124
|
+
}
|
|
125
|
+
async function apiFetch(path) {
|
|
126
|
+
const headers = {};
|
|
127
|
+
const t = token();
|
|
128
|
+
if (t) headers.Authorization = 'Bearer ' + t;
|
|
129
|
+
return fetch(apiUrl(path), { headers });
|
|
130
|
+
}
|
|
131
|
+
function setStatus(msg, err) {
|
|
132
|
+
const el = document.getElementById('status');
|
|
133
|
+
el.textContent = msg;
|
|
134
|
+
el.className = 'status' + (err ? ' err' : '');
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function roleLabel(role) {
|
|
138
|
+
if (role === 'user') return 'Пользователь';
|
|
139
|
+
if (role === 'assistant') return 'Ассистент';
|
|
140
|
+
return role || 'system';
|
|
141
|
+
}
|
|
142
|
+
function normText(text, role) {
|
|
143
|
+
let t = String(text || '').replace(/\s+/g, ' ').trim().toLowerCase();
|
|
144
|
+
if (role === 'assistant') {
|
|
145
|
+
t = t.replace(/^#+\\s*/g, '').replace(/\\|/g, ' ').replace(/[-]{3,}/g, ' ');
|
|
146
|
+
}
|
|
147
|
+
return t.replace(/\s+/g, ' ').trim();
|
|
148
|
+
}
|
|
149
|
+
function textsMatch(a, b, role) {
|
|
150
|
+
const ta = normText(a, role);
|
|
151
|
+
const tb = normText(b, role);
|
|
152
|
+
if (!ta || !tb) return false;
|
|
153
|
+
if (ta === tb) return true;
|
|
154
|
+
const short = ta.length <= tb.length ? ta : tb;
|
|
155
|
+
const long = ta.length <= tb.length ? tb : ta;
|
|
156
|
+
return short.length >= 12 && long.includes(short) && short.length / long.length >= 0.35;
|
|
157
|
+
}
|
|
158
|
+
function renderBubbles(messages, source) {
|
|
159
|
+
if (!messages || !messages.length) return '<div class="empty">Нет сообщений</div>';
|
|
160
|
+
return messages.map((m) => {
|
|
161
|
+
const role = m.role || 'system';
|
|
162
|
+
const tag = roleLabel(role) + ' · ' + source + ' · ' + (m.id || '?') + (m.flatIndex != null ? ' · seq=' + m.flatIndex : '') + (m.html ? ' · html' : '');
|
|
163
|
+
const text = (m.text || '').trim() || (m.html ? '[html ' + m.html.length + ' chars — в Cursor с разметкой]' : '(пусто)');
|
|
164
|
+
return '<div class="bubble ' + role + '"><div class="tag">' + escapeHtml(tag) + '</div>' + escapeHtml(text) + '</div>';
|
|
165
|
+
}).join('');
|
|
166
|
+
}
|
|
167
|
+
function renderCompare(hist, dom) {
|
|
168
|
+
const rows = [];
|
|
169
|
+
const usedDom = new Set();
|
|
170
|
+
for (let i = 0; i < (hist || []).length; i++) {
|
|
171
|
+
const h = hist[i];
|
|
172
|
+
let domIdx = -1;
|
|
173
|
+
for (let j = 0; j < (dom || []).length; j++) {
|
|
174
|
+
if (usedDom.has(j)) continue;
|
|
175
|
+
if (h.role === dom[j].role && textsMatch(h.text, dom[j].text, h.role)) {
|
|
176
|
+
domIdx = j;
|
|
177
|
+
usedDom.add(j);
|
|
178
|
+
break;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
const cls = domIdx >= 0 ? 'match' : 'miss';
|
|
182
|
+
rows.push({ cls, n: i + 1, h, d: domIdx >= 0 ? dom[domIdx] : null });
|
|
183
|
+
}
|
|
184
|
+
for (let j = 0; j < (dom || []).length; j++) {
|
|
185
|
+
if (usedDom.has(j)) continue;
|
|
186
|
+
rows.push({ cls: 'domonly', n: '—', h: null, d: dom[j] });
|
|
187
|
+
}
|
|
188
|
+
if (!rows.length) return '<div class="empty">Нет данных</div>';
|
|
189
|
+
const tr = rows.map((r) => {
|
|
190
|
+
const hp = r.h ? escapeHtml((r.h.text || '').slice(0, 120)) : '—';
|
|
191
|
+
const dp = r.d ? escapeHtml((r.d.text || '').slice(0, 120)) : '—';
|
|
192
|
+
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>';
|
|
193
|
+
}).join('');
|
|
194
|
+
return '<table class="cmp"><thead><tr><th>#</th><th>JSONL</th><th>текст JSONL</th><th>DOM</th><th>текст DOM</th></tr></thead><tbody>' + tr + '</tbody></table>';
|
|
195
|
+
}
|
|
196
|
+
function escapeHtml(s) {
|
|
197
|
+
return String(s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>');
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function renderJsonlLive() {
|
|
201
|
+
if (!jsonlLiveRows.length) {
|
|
202
|
+
return '<div class="empty">Нет строк — выберите чат или дождитесь записи в JSONL</div>';
|
|
203
|
+
}
|
|
204
|
+
const head =
|
|
205
|
+
'<div class="jsonl-live-head">Файл: <code>' +
|
|
206
|
+
escapeHtml(jsonlLiveFile || '—') +
|
|
207
|
+
'</code> · строк в файле: <b>' +
|
|
208
|
+
jsonlLiveLastLine +
|
|
209
|
+
'</b> · показано: ' +
|
|
210
|
+
jsonlLiveRows.length +
|
|
211
|
+
' · опрос <b>300 ms</b> · bridge subscribe: <b>' +
|
|
212
|
+
escapeHtml(bridgeSubscribedId === selectedId ? 'on' : 'off') +
|
|
213
|
+
'</b></div>';
|
|
214
|
+
const body = jsonlLiveRows
|
|
215
|
+
.map((r) => {
|
|
216
|
+
const cls =
|
|
217
|
+
'jsonl-row' +
|
|
218
|
+
(r._new ? ' new' : '') +
|
|
219
|
+
(r.inLenta ? ' in-lenta' : ' skip');
|
|
220
|
+
const tools = r.tools?.length ? ' · tools: ' + r.tools.join(', ') : '';
|
|
221
|
+
const skip = r.inLenta ? '→ в ленту' : '→ skip: ' + (r.skipReason || '?');
|
|
222
|
+
return (
|
|
223
|
+
'<div class="' +
|
|
224
|
+
cls +
|
|
225
|
+
'" data-line="' +
|
|
226
|
+
r.lineNo +
|
|
227
|
+
'"><div class="meta">#' +
|
|
228
|
+
r.lineNo +
|
|
229
|
+
' · ' +
|
|
230
|
+
escapeHtml(r.role) +
|
|
231
|
+
tools +
|
|
232
|
+
' · ' +
|
|
233
|
+
escapeHtml(skip) +
|
|
234
|
+
(r._at ? ' · ' + r._at : '') +
|
|
235
|
+
'</div><div class="body">' +
|
|
236
|
+
escapeHtml(r.textPreview || '(пусто)') +
|
|
237
|
+
'</div></div>'
|
|
238
|
+
);
|
|
239
|
+
})
|
|
240
|
+
.join('');
|
|
241
|
+
return head + body;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
async function pollJsonlLive() {
|
|
245
|
+
if (!selectedId || tab !== 'jsonl-live') return;
|
|
246
|
+
try {
|
|
247
|
+
const q = new URLSearchParams();
|
|
248
|
+
q.set('agentId', selectedId);
|
|
249
|
+
if (selectedTitle) q.set('title', selectedTitle);
|
|
250
|
+
if (jsonlLiveLastLine > 0) q.set('afterLine', String(jsonlLiveLastLine));
|
|
251
|
+
else q.set('tail', '80');
|
|
252
|
+
const res = await apiFetch('/debug/jsonl-live?' + q.toString());
|
|
253
|
+
if (!res.ok) throw new Error('jsonl-live ' + res.status);
|
|
254
|
+
const data = await res.json();
|
|
255
|
+
jsonlLiveFile = data.filePath || '';
|
|
256
|
+
const at = new Date().toLocaleTimeString();
|
|
257
|
+
const incoming = (data.rows || []).map((r) => ({ ...r, _new: true, _at: at }));
|
|
258
|
+
if (jsonlLiveLastLine === 0) {
|
|
259
|
+
jsonlLiveRows = incoming;
|
|
260
|
+
} else if (incoming.length) {
|
|
261
|
+
const seen = new Set(jsonlLiveRows.map((x) => x.lineNo));
|
|
262
|
+
for (const r of incoming) {
|
|
263
|
+
if (!seen.has(r.lineNo)) jsonlLiveRows.push(r);
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (data.totalLines) jsonlLiveLastLine = data.totalLines;
|
|
267
|
+
else if (incoming.length) {
|
|
268
|
+
jsonlLiveLastLine = Math.max(jsonlLiveLastLine, incoming[incoming.length - 1].lineNo);
|
|
269
|
+
}
|
|
270
|
+
if (jsonlLiveRows.length > 600) jsonlLiveRows = jsonlLiveRows.slice(-500);
|
|
271
|
+
setTimeout(() => {
|
|
272
|
+
for (const r of jsonlLiveRows) r._new = false;
|
|
273
|
+
}, 1200);
|
|
274
|
+
if (tab === 'jsonl-live') {
|
|
275
|
+
renderPanel();
|
|
276
|
+
if (jsonlLiveStickBottom) {
|
|
277
|
+
const panel = document.getElementById('panel');
|
|
278
|
+
panel.scrollTop = panel.scrollHeight;
|
|
279
|
+
}
|
|
280
|
+
setStatus(
|
|
281
|
+
'JSONL live: ' +
|
|
282
|
+
jsonlLiveRows.length +
|
|
283
|
+
' shown · file lines ' +
|
|
284
|
+
(data.totalLines ?? '?') +
|
|
285
|
+
' · ' +
|
|
286
|
+
at
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
} catch (e) {
|
|
290
|
+
setStatus((e && e.message) || String(e), true);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async function resetJsonlLive() {
|
|
295
|
+
jsonlLiveLastLine = 0;
|
|
296
|
+
jsonlLiveRows = [];
|
|
297
|
+
jsonlLiveFile = '';
|
|
298
|
+
await pollJsonlLive();
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function ensureBridgeSubscribe() {
|
|
302
|
+
if (typeof io === 'undefined') return;
|
|
303
|
+
if (!bridgeSocket) {
|
|
304
|
+
const headers = {};
|
|
305
|
+
const t = token();
|
|
306
|
+
if (t) headers.Authorization = 'Bearer ' + t;
|
|
307
|
+
bridgeSocket = io(location.origin, { transports: ['websocket', 'polling'], auth: headers });
|
|
308
|
+
bridgeSocket.on('agent:messages', (p) => {
|
|
309
|
+
const n = p.historyMessages?.length ?? 0;
|
|
310
|
+
const tail = (p.historyMessages || [])[(n || 1) - 1];
|
|
311
|
+
const preview = (tail?.text || '').slice(0, 48);
|
|
312
|
+
setStatus(
|
|
313
|
+
'app-pipe agent:messages append=' +
|
|
314
|
+
!!p.append +
|
|
315
|
+
' n=' +
|
|
316
|
+
n +
|
|
317
|
+
' · ' +
|
|
318
|
+
preview
|
|
319
|
+
);
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
if (!selectedId || (bridgeSubscribedId === selectedId && tab === 'jsonl-live')) return;
|
|
323
|
+
bridgeSubscribedId = selectedId;
|
|
324
|
+
bridgeSocket.emit('agents:subscribe', {
|
|
325
|
+
agentId: selectedId,
|
|
326
|
+
title: selectedTitle || undefined,
|
|
327
|
+
focus: false,
|
|
328
|
+
});
|
|
329
|
+
setStatus('bridge subscribe ' + selectedId.slice(0, 8) + '… (как приложение)');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
function scheduleJsonlLive() {
|
|
333
|
+
if (jsonlLiveTimer) clearInterval(jsonlLiveTimer);
|
|
334
|
+
jsonlLiveTimer = null;
|
|
335
|
+
if (tab === 'jsonl-live') {
|
|
336
|
+
ensureBridgeSubscribe();
|
|
337
|
+
jsonlLiveTimer = setInterval(() => void pollJsonlLive(), 300);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function renderPanel() {
|
|
342
|
+
const panel = document.getElementById('panel');
|
|
343
|
+
const raw = document.getElementById('raw');
|
|
344
|
+
panel.hidden = tab === 'raw';
|
|
345
|
+
raw.hidden = tab !== 'raw';
|
|
346
|
+
if (tab === 'raw') {
|
|
347
|
+
raw.textContent = JSON.stringify({ snapshot, selectedId, tab }, null, 2);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const note = document.getElementById('note');
|
|
351
|
+
note.hidden = tab !== 'dom' && tab !== 'compare' && tab !== 'jsonl-live';
|
|
352
|
+
if (tab === 'jsonl-live') {
|
|
353
|
+
note.textContent =
|
|
354
|
+
'Файл .jsonl на диске (опрос 300 ms). «→ в ленту» = после filter в bridge. В приложение строка попадёт только если Connect открыл этот чат (ниже: bridge subscribe). Debug сам по себе app не кормит.';
|
|
355
|
+
} else if (tab === 'dom') {
|
|
356
|
+
note.textContent = 'DOM = смонтированные [data-flat-index] в Cursor (без JSONL). При открытии чата bridge прокручивает ленту вниз и poll. Пропуски = tool/thought строки.';
|
|
357
|
+
} else if (tab === 'compare') {
|
|
358
|
+
note.textContent =
|
|
359
|
+
'Зелёная = JSONL-лента (API) и DOM совпали. Красная = только в JSONL. Фиолетовая (domonly) = видно в DOM Cursor, слева «—» (нет в ленте API): схлопнули несколько ответов ассистента в один, или DOM ещё держит короткий статус («Ищу…», «Добавляю…»), а в API уже только финальный «Сделано…». Вкладка «JSONL cache» = то, что уходит в app.';
|
|
360
|
+
}
|
|
361
|
+
if (!selectedId && tab !== 'dom' && tab !== 'debug') {
|
|
362
|
+
panel.innerHTML = '<div class="empty">Выберите агента слева</div>';
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
if (tab === 'jsonl-live') {
|
|
366
|
+
panel.innerHTML = renderJsonlLive();
|
|
367
|
+
panel.onscroll = () => {
|
|
368
|
+
const nearBottom = panel.scrollHeight - panel.scrollTop - panel.clientHeight < 80;
|
|
369
|
+
jsonlLiveStickBottom = nearBottom;
|
|
370
|
+
};
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
panel.onscroll = null;
|
|
374
|
+
if (tab === 'history') {
|
|
375
|
+
panel.innerHTML = window.__historyHtml || '<div class="empty">Загрузка history…</div>';
|
|
376
|
+
} else if (tab === 'cache') {
|
|
377
|
+
const rows = snapshot?.displayCache?.[selectedId];
|
|
378
|
+
panel.innerHTML = renderBubbles(rows, 'jsonl-cache');
|
|
379
|
+
} else if (tab === 'transcript') {
|
|
380
|
+
const rows = snapshot?.domTranscript?.[selectedId];
|
|
381
|
+
panel.innerHTML = renderBubbles(rows, 'dom-transcript');
|
|
382
|
+
} else if (tab === 'dom') {
|
|
383
|
+
const st = snapshot?.cursor;
|
|
384
|
+
const active = st?.activeComposerId;
|
|
385
|
+
const hint = active ? 'activeComposerId=' + active : 'нет activeComposerId';
|
|
386
|
+
panel.innerHTML = '<div class="empty" style="text-align:left;margin-bottom:8px">' + escapeHtml(hint) + ' · ' + (st?.domMessageCount ?? 0) + ' msgs</div>' + renderBubbles(st?.domMessages, 'dom');
|
|
387
|
+
} else if (tab === 'compare') {
|
|
388
|
+
const hist = window.__historyData || [];
|
|
389
|
+
const dom =
|
|
390
|
+
(selectedId && snapshot?.domTranscript?.[selectedId]?.length
|
|
391
|
+
? snapshot.domTranscript[selectedId]
|
|
392
|
+
: null) ||
|
|
393
|
+
snapshot?.cursor?.domMessages ||
|
|
394
|
+
[];
|
|
395
|
+
const active = snapshot?.cursor?.activeComposerId;
|
|
396
|
+
const same = selectedId === active;
|
|
397
|
+
const head = '<div class="empty" style="text-align:left">agent=' + escapeHtml(selectedId.slice(0, 8)) + '… active=' + escapeHtml((active || '—').slice(0, 8)) + (same ? ' (совпадает)' : ' (другой чат в Cursor!)') + '</div>';
|
|
398
|
+
panel.innerHTML = head + renderCompare(hist, dom);
|
|
399
|
+
} else if (tab === 'debug') {
|
|
400
|
+
const d = snapshot?.domExtractDebug?.latest;
|
|
401
|
+
if (!d) { panel.innerHTML = '<div class="empty">Нет extract debug</div>'; return; }
|
|
402
|
+
panel.innerHTML = '<div class="bubble system"><div class="tag">extract debug</div>' + escapeHtml(JSON.stringify(d, null, 2)) + '</div>';
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function renderAgents(index) {
|
|
407
|
+
const nav = document.getElementById('agents');
|
|
408
|
+
const repos = index?.repos || [];
|
|
409
|
+
const items = [];
|
|
410
|
+
for (const repo of repos) {
|
|
411
|
+
for (const a of repo.agents || []) {
|
|
412
|
+
items.push({ id: a.id, title: a.title, repo: repo.name });
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
if (!items.length) {
|
|
416
|
+
nav.innerHTML = '<div class="empty">agents:index пуст</div>';
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
nav.innerHTML = items.map((a) => {
|
|
420
|
+
const cls = a.id === selectedId ? ' active' : '';
|
|
421
|
+
return '<button type="button" class="' + cls.trim() + '" data-id="' + escapeHtml(a.id) + '" data-title="' + escapeHtml(a.title || '') + '">' + escapeHtml(a.title || a.id) + '<div class="meta">' + escapeHtml(a.repo) + ' · ' + escapeHtml(a.id.slice(0, 8)) + '…</div></button>';
|
|
422
|
+
}).join('');
|
|
423
|
+
nav.querySelectorAll('button[data-id]').forEach((btn) => {
|
|
424
|
+
btn.addEventListener('click', () => {
|
|
425
|
+
selectedId = btn.dataset.id;
|
|
426
|
+
selectedTitle = btn.dataset.title || '';
|
|
427
|
+
nav.querySelectorAll('button').forEach((x) => x.classList.toggle('active', x.dataset.id === selectedId));
|
|
428
|
+
void loadHistory();
|
|
429
|
+
if (tab === 'jsonl-live') void resetJsonlLive();
|
|
430
|
+
renderPanel();
|
|
431
|
+
});
|
|
432
|
+
});
|
|
433
|
+
if (!selectedId && items[0]) {
|
|
434
|
+
selectedId = items[0].id;
|
|
435
|
+
selectedTitle = items[0].title || '';
|
|
436
|
+
nav.querySelector('button[data-id="' + selectedId + '"]')?.classList.add('active');
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
async function loadSnapshot() {
|
|
441
|
+
const res = await apiFetch('/debug/chat-snapshot');
|
|
442
|
+
if (!res.ok) throw new Error('chat-snapshot ' + res.status);
|
|
443
|
+
snapshot = await res.json();
|
|
444
|
+
const h = document.getElementById('health');
|
|
445
|
+
h.textContent = 'CDP ' + (snapshot.health?.cdp ? 'on' : 'off') + ' · updated ' + new Date(snapshot.at).toLocaleTimeString();
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
async function loadHistory() {
|
|
449
|
+
if (!selectedId) return;
|
|
450
|
+
const res = await apiFetch('/api/agents/history?agentId=' + encodeURIComponent(selectedId) + '&limit=120');
|
|
451
|
+
if (!res.ok) throw new Error('history ' + res.status);
|
|
452
|
+
const data = await res.json();
|
|
453
|
+
window.__historyData = data.messages || [];
|
|
454
|
+
window.__historyHtml = renderBubbles(window.__historyData, 'jsonl');
|
|
455
|
+
if (tab === 'history' || tab === 'compare') renderPanel();
|
|
456
|
+
setStatus('history: ' + (data.messages?.length ?? 0) + ' / total ' + (data.totalMessages ?? '?'));
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
async function loadIndex() {
|
|
460
|
+
const res = await apiFetch('/api/agents/index');
|
|
461
|
+
if (!res.ok) throw new Error('index ' + res.status);
|
|
462
|
+
return res.json();
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
async function loadAll() {
|
|
466
|
+
try {
|
|
467
|
+
setStatus('Загрузка…');
|
|
468
|
+
await loadSnapshot();
|
|
469
|
+
const index = await loadIndex();
|
|
470
|
+
renderAgents(index);
|
|
471
|
+
await loadHistory();
|
|
472
|
+
renderPanel();
|
|
473
|
+
setStatus('OK · ' + new Date().toLocaleTimeString());
|
|
474
|
+
} catch (e) {
|
|
475
|
+
setStatus((e && e.message) || String(e), true);
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
function schedule() {
|
|
480
|
+
if (timer) clearInterval(timer);
|
|
481
|
+
const sec = Number(document.getElementById('interval').value) || 0;
|
|
482
|
+
if (sec > 0) timer = setInterval(() => void loadAll(), sec * 1000);
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
schedule();
|
|
486
|
+
scheduleJsonlLive();
|
|
487
|
+
void loadAll();
|
|
488
|
+
})();
|
|
489
|
+
</script>
|
|
490
|
+
</body>
|
|
491
|
+
</html>`;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ChatMessage } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Append-only per-agent transcript built from DOM snapshots.
|
|
4
|
+
* Debug-only DOM transcript; not used for client chat lenta (JSONL-only).
|
|
5
|
+
*/
|
|
6
|
+
export declare class DomTranscriptStore {
|
|
7
|
+
private rowsByAgent;
|
|
8
|
+
has(agentId: string): boolean;
|
|
9
|
+
clear(agentId: string): void;
|
|
10
|
+
/** Replace transcript (e.g. opt-in JSONL path). Live chat uses ingest only. */
|
|
11
|
+
seed(agentId: string, rows: ChatMessage[]): void;
|
|
12
|
+
/** Merge latest DOM extract into transcript; returns sorted display list. */
|
|
13
|
+
ingest(agentId: string, snapshot: ChatMessage[]): ChatMessage[];
|
|
14
|
+
list(agentId: string): ChatMessage[];
|
|
15
|
+
/** Drop overlay rows once the same turn appears in JSONL. */
|
|
16
|
+
pruneCoveredBy(agentId: string, baseline: ChatMessage[]): void;
|
|
17
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { messagesEquivalent, pickPreferredMessage, sortMessagesChronologically, } from './chat-display.js';
|
|
2
|
+
/** Stable key for upsert across DOM polls (prefer extract id). */
|
|
3
|
+
function transcriptKey(m) {
|
|
4
|
+
if (m.id?.trim())
|
|
5
|
+
return m.id;
|
|
6
|
+
return `${m.role}:${m.flatIndex ?? 0}:${(m.text ?? '').slice(0, 80)}`;
|
|
7
|
+
}
|
|
8
|
+
/**
|
|
9
|
+
* Append-only per-agent transcript built from DOM snapshots.
|
|
10
|
+
* Debug-only DOM transcript; not used for client chat lenta (JSONL-only).
|
|
11
|
+
*/
|
|
12
|
+
export class DomTranscriptStore {
|
|
13
|
+
rowsByAgent = new Map();
|
|
14
|
+
has(agentId) {
|
|
15
|
+
return (this.rowsByAgent.get(agentId)?.size ?? 0) > 0;
|
|
16
|
+
}
|
|
17
|
+
clear(agentId) {
|
|
18
|
+
this.rowsByAgent.delete(agentId);
|
|
19
|
+
}
|
|
20
|
+
/** Replace transcript (e.g. opt-in JSONL path). Live chat uses ingest only. */
|
|
21
|
+
seed(agentId, rows) {
|
|
22
|
+
if (!rows.length)
|
|
23
|
+
return;
|
|
24
|
+
const map = new Map();
|
|
25
|
+
for (const m of rows) {
|
|
26
|
+
map.set(transcriptKey(m), m);
|
|
27
|
+
}
|
|
28
|
+
this.rowsByAgent.set(agentId, map);
|
|
29
|
+
}
|
|
30
|
+
/** Merge latest DOM extract into transcript; returns sorted display list. */
|
|
31
|
+
ingest(agentId, snapshot) {
|
|
32
|
+
if (!snapshot.length)
|
|
33
|
+
return this.list(agentId);
|
|
34
|
+
let map = this.rowsByAgent.get(agentId);
|
|
35
|
+
if (!map) {
|
|
36
|
+
map = new Map();
|
|
37
|
+
this.rowsByAgent.set(agentId, map);
|
|
38
|
+
}
|
|
39
|
+
for (const m of snapshot) {
|
|
40
|
+
let dupKey;
|
|
41
|
+
for (const [k, prev] of map) {
|
|
42
|
+
if (messagesEquivalent(prev, m)) {
|
|
43
|
+
dupKey = k;
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
const key = transcriptKey(m);
|
|
48
|
+
if (dupKey) {
|
|
49
|
+
const prev = map.get(dupKey);
|
|
50
|
+
map.delete(dupKey);
|
|
51
|
+
map.set(key, pickPreferredMessage(prev, m, true));
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
const prev = map.get(key);
|
|
55
|
+
map.set(key, prev ? pickPreferredMessage(prev, m, true) : m);
|
|
56
|
+
}
|
|
57
|
+
return this.list(agentId);
|
|
58
|
+
}
|
|
59
|
+
list(agentId) {
|
|
60
|
+
const map = this.rowsByAgent.get(agentId);
|
|
61
|
+
if (!map?.size)
|
|
62
|
+
return [];
|
|
63
|
+
return sortMessagesChronologically([...map.values()]);
|
|
64
|
+
}
|
|
65
|
+
/** Drop overlay rows once the same turn appears in JSONL. */
|
|
66
|
+
pruneCoveredBy(agentId, baseline) {
|
|
67
|
+
const map = this.rowsByAgent.get(agentId);
|
|
68
|
+
if (!map?.size || !baseline.length)
|
|
69
|
+
return;
|
|
70
|
+
for (const [key, row] of [...map.entries()]) {
|
|
71
|
+
if (baseline.some((b) => messagesEquivalent(b, row))) {
|
|
72
|
+
map.delete(key);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|