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
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import { canonicalizeImagePaths } from './media-path.js';
|
|
1
2
|
import { cleanUserText, isAssistantReflectionText, isPassiveStatusChatLine, parseUserImagePaths, stripJsonlRedactionArtifacts, } from './message-filter.js';
|
|
2
3
|
function normalizeUserChatMessage(m) {
|
|
3
|
-
const images =
|
|
4
|
+
const images = canonicalizeImagePaths([
|
|
5
|
+
...(m.images ?? []),
|
|
6
|
+
...parseUserImagePaths(m.text ?? ''),
|
|
7
|
+
]);
|
|
4
8
|
const text = cleanUserText(m.text ?? '');
|
|
5
9
|
return {
|
|
6
10
|
...m,
|
|
@@ -18,7 +22,20 @@ export function compareUserText(text) {
|
|
|
18
22
|
return cleanUserText(text).replace(/\s+/g, ' ').trim();
|
|
19
23
|
}
|
|
20
24
|
function userImagePaths(m) {
|
|
21
|
-
return
|
|
25
|
+
return canonicalizeImagePaths([
|
|
26
|
+
...(m.images ?? []),
|
|
27
|
+
...parseUserImagePaths(m.text ?? ''),
|
|
28
|
+
]);
|
|
29
|
+
}
|
|
30
|
+
function userImageSetsEquivalent(a, b) {
|
|
31
|
+
const pa = userImagePaths(a);
|
|
32
|
+
const pb = userImagePaths(b);
|
|
33
|
+
if (!pa.length || !pb.length)
|
|
34
|
+
return false;
|
|
35
|
+
if (pa.length !== pb.length)
|
|
36
|
+
return false;
|
|
37
|
+
const setB = new Set(pb);
|
|
38
|
+
return pa.every((p) => setB.has(p));
|
|
22
39
|
}
|
|
23
40
|
export function userTextsEquivalent(ta, tb) {
|
|
24
41
|
const a = compareUserText(ta);
|
|
@@ -46,13 +63,10 @@ export function userMessagesEquivalent(a, b) {
|
|
|
46
63
|
const tb = compareUserText(b.text ?? '');
|
|
47
64
|
if (ta && tb && userTextsEquivalent(ta, tb))
|
|
48
65
|
return true;
|
|
66
|
+
if (userImageSetsEquivalent(a, b))
|
|
67
|
+
return true;
|
|
49
68
|
const pa = userImagePaths(a);
|
|
50
69
|
const pb = userImagePaths(b);
|
|
51
|
-
if (pa.length > 0 && pb.length > 0) {
|
|
52
|
-
const setB = new Set(pb);
|
|
53
|
-
if (pa.length === pb.length && pa.every((p) => setB.has(p)))
|
|
54
|
-
return true;
|
|
55
|
-
}
|
|
56
70
|
if (ta && tb && pa.length > 0 && pb.length > 0) {
|
|
57
71
|
const setB = new Set(pb);
|
|
58
72
|
if (pa.every((p) => setB.has(p)))
|
|
@@ -82,7 +96,12 @@ function pickPreferredUserMessage(a, b, preferB) {
|
|
|
82
96
|
const other = pick === na ? nb : na;
|
|
83
97
|
if ((pick.text?.length ?? 0) < (other.text?.length ?? 0))
|
|
84
98
|
pick = other;
|
|
85
|
-
const images =
|
|
99
|
+
const images = canonicalizeImagePaths([
|
|
100
|
+
...(pick.images ?? []),
|
|
101
|
+
...(other.images ?? []),
|
|
102
|
+
...parseUserImagePaths(pick.text ?? ''),
|
|
103
|
+
...parseUserImagePaths(other.text ?? ''),
|
|
104
|
+
]);
|
|
86
105
|
const flatPick = Math.min(a.flatIndex ?? Number.MAX_SAFE_INTEGER, b.flatIndex ?? Number.MAX_SAFE_INTEGER);
|
|
87
106
|
const id = !pick.id.startsWith('hist-')
|
|
88
107
|
? pick.id
|
|
@@ -96,6 +115,33 @@ function pickPreferredUserMessage(a, b, preferB) {
|
|
|
96
115
|
flatIndex: Number.isFinite(flatPick) ? flatPick : pick.flatIndex ?? other.flatIndex,
|
|
97
116
|
};
|
|
98
117
|
}
|
|
118
|
+
/** DOM textContent vs JSONL markdown — same turn, different surface form. */
|
|
119
|
+
function normalizeAssistantCompareText(text) {
|
|
120
|
+
return text
|
|
121
|
+
.replace(/^#+\s*/gm, '')
|
|
122
|
+
.replace(/\|/g, ' ')
|
|
123
|
+
.replace(/[-]{3,}/g, ' ')
|
|
124
|
+
.replace(/\s+/g, ' ')
|
|
125
|
+
.trim()
|
|
126
|
+
.toLowerCase();
|
|
127
|
+
}
|
|
128
|
+
function assistantTextsEquivalent(a, b) {
|
|
129
|
+
if (a.role !== 'assistant' || b.role !== 'assistant')
|
|
130
|
+
return false;
|
|
131
|
+
const ta = normalizeAssistantCompareText(a.text ?? '');
|
|
132
|
+
const tb = normalizeAssistantCompareText(b.text ?? '');
|
|
133
|
+
if (!ta || !tb)
|
|
134
|
+
return false;
|
|
135
|
+
if (ta === tb)
|
|
136
|
+
return true;
|
|
137
|
+
const short = ta.length <= tb.length ? ta : tb;
|
|
138
|
+
const long = ta.length <= tb.length ? tb : ta;
|
|
139
|
+
if (short.length < 20)
|
|
140
|
+
return false;
|
|
141
|
+
if (!long.includes(short))
|
|
142
|
+
return false;
|
|
143
|
+
return short.length / long.length >= 0.35;
|
|
144
|
+
}
|
|
99
145
|
function messageKey(m) {
|
|
100
146
|
const n = normalizeMessage(m);
|
|
101
147
|
if (n.role === 'user') {
|
|
@@ -104,17 +150,30 @@ function messageKey(m) {
|
|
|
104
150
|
const html = n.html?.trim() ? '1' : '0';
|
|
105
151
|
return `${n.role}:${n.text.trim().replace(/\s+/g, ' ')}:${(n.images ?? []).join('|')}:${html}`;
|
|
106
152
|
}
|
|
107
|
-
function messagesEquivalent(a, b) {
|
|
153
|
+
export function messagesEquivalent(a, b) {
|
|
108
154
|
if (a.role === 'user' && b.role === 'user')
|
|
109
155
|
return userMessagesEquivalent(a, b);
|
|
156
|
+
if (a.role === 'assistant' && b.role === 'assistant') {
|
|
157
|
+
return assistantTextsEquivalent(a, b) || messageKey(a) === messageKey(b);
|
|
158
|
+
}
|
|
110
159
|
return messageKey(a) === messageKey(b);
|
|
111
160
|
}
|
|
112
|
-
function
|
|
161
|
+
function mergeTimelineFields(prev, next, pick) {
|
|
162
|
+
const indices = [prev.flatIndex, next.flatIndex, pick.flatIndex].filter((n) => n != null && Number.isFinite(n));
|
|
163
|
+
const domSeqs = [prev.domSeq, next.domSeq, pick.domSeq].filter((n) => n != null && Number.isFinite(n));
|
|
164
|
+
return {
|
|
165
|
+
...pick,
|
|
166
|
+
flatIndex: indices.length ? Math.min(...indices) : pick.flatIndex,
|
|
167
|
+
domSeq: domSeqs.length ? Math.min(...domSeqs) : pick.domSeq,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
export function pickPreferredMessage(prev, next, preferNext) {
|
|
113
171
|
if (prev.role === 'user' && next.role === 'user') {
|
|
114
172
|
return pickPreferredUserMessage(prev, next, preferNext);
|
|
115
173
|
}
|
|
116
174
|
if (prev.role !== 'assistant' || next.role !== 'assistant') {
|
|
117
|
-
|
|
175
|
+
const pick = preferNext ? next : prev;
|
|
176
|
+
return mergeTimelineFields(prev, next, pick);
|
|
118
177
|
}
|
|
119
178
|
const score = (m) => {
|
|
120
179
|
let s = 0;
|
|
@@ -127,15 +186,20 @@ function pickPreferredMessage(prev, next, preferNext) {
|
|
|
127
186
|
};
|
|
128
187
|
const sp = score(prev);
|
|
129
188
|
const sn = score(next);
|
|
189
|
+
let pick;
|
|
130
190
|
if (sn > sp)
|
|
131
|
-
|
|
132
|
-
if (sp > sn)
|
|
133
|
-
|
|
134
|
-
|
|
191
|
+
pick = next;
|
|
192
|
+
else if (sp > sn)
|
|
193
|
+
pick = prev;
|
|
194
|
+
else
|
|
195
|
+
pick = preferNext ? next : prev;
|
|
196
|
+
return mergeTimelineFields(prev, next, pick);
|
|
135
197
|
}
|
|
136
198
|
function messageOrderKey(m) {
|
|
137
199
|
if (m.flatIndex != null && Number.isFinite(m.flatIndex))
|
|
138
200
|
return m.flatIndex;
|
|
201
|
+
if (m.domSeq != null && Number.isFinite(m.domSeq))
|
|
202
|
+
return m.domSeq;
|
|
139
203
|
const hist = /^hist-(\d+)/.exec(m.id);
|
|
140
204
|
if (hist)
|
|
141
205
|
return Number(hist[1]);
|
|
@@ -147,6 +211,10 @@ export function sortMessagesChronologically(messages) {
|
|
|
147
211
|
const kb = messageOrderKey(b);
|
|
148
212
|
if (ka !== kb)
|
|
149
213
|
return ka - kb;
|
|
214
|
+
const da = a.domSeq ?? 0;
|
|
215
|
+
const db = b.domSeq ?? 0;
|
|
216
|
+
if (da !== db)
|
|
217
|
+
return da - db;
|
|
150
218
|
return a.id.localeCompare(b.id);
|
|
151
219
|
});
|
|
152
220
|
}
|
|
@@ -168,6 +236,65 @@ function dedupeUserMessages(messages) {
|
|
|
168
236
|
}
|
|
169
237
|
return out;
|
|
170
238
|
}
|
|
239
|
+
function assistantTailMatchesLongBody(tail, body) {
|
|
240
|
+
const a = tail.trim();
|
|
241
|
+
const b = body.trim();
|
|
242
|
+
if (!a || !b || b.length <= a.length)
|
|
243
|
+
return false;
|
|
244
|
+
if (b.includes(a))
|
|
245
|
+
return true;
|
|
246
|
+
const words = (s) => s.toLowerCase().match(/[\p{L}\p{N}]+/gu) ?? [];
|
|
247
|
+
const wa = words(a);
|
|
248
|
+
const wb = words(b);
|
|
249
|
+
let shared = 0;
|
|
250
|
+
while (shared < wa.length && shared < wb.length && wa[shared] === wb[shared]) {
|
|
251
|
+
shared++;
|
|
252
|
+
}
|
|
253
|
+
return shared >= 3 && b.length > a.length * 1.15;
|
|
254
|
+
}
|
|
255
|
+
/** Cursor may render one assistant turn as A₁, user, A₂ (tail + body); restore A₂ before user. */
|
|
256
|
+
function fixAssistantFragmentSandwich(messages) {
|
|
257
|
+
const sorted = sortMessagesChronologically(normalizeAll(messages));
|
|
258
|
+
const out = [];
|
|
259
|
+
let i = 0;
|
|
260
|
+
while (i < sorted.length) {
|
|
261
|
+
if (i + 2 < sorted.length &&
|
|
262
|
+
sorted[i].role === 'assistant' &&
|
|
263
|
+
sorted[i + 1].role === 'user' &&
|
|
264
|
+
sorted[i + 2].role === 'assistant') {
|
|
265
|
+
const a1 = sorted[i];
|
|
266
|
+
const u = sorted[i + 1];
|
|
267
|
+
const a2 = sorted[i + 2];
|
|
268
|
+
const t1 = (a1.text ?? '').trim();
|
|
269
|
+
const t2 = (a2.text ?? '').trim();
|
|
270
|
+
if (assistantTailMatchesLongBody(t1, t2)) {
|
|
271
|
+
out.push(a2, u);
|
|
272
|
+
i += 3;
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
out.push(sorted[i]);
|
|
277
|
+
i++;
|
|
278
|
+
}
|
|
279
|
+
return out;
|
|
280
|
+
}
|
|
281
|
+
function assistantOneSupersedesOther(prev, next) {
|
|
282
|
+
if (assistantTextsEquivalent(prev, next))
|
|
283
|
+
return true;
|
|
284
|
+
const ta = (prev.text ?? '').trim();
|
|
285
|
+
const tb = (next.text ?? '').trim();
|
|
286
|
+
if (!ta || !tb)
|
|
287
|
+
return false;
|
|
288
|
+
if (assistantTailMatchesLongBody(ta, tb))
|
|
289
|
+
return true;
|
|
290
|
+
if (assistantTailMatchesLongBody(tb, ta))
|
|
291
|
+
return true;
|
|
292
|
+
const short = ta.length <= tb.length ? ta : tb;
|
|
293
|
+
const long = ta.length <= tb.length ? tb : ta;
|
|
294
|
+
if (short.length >= 40 && long.includes(short))
|
|
295
|
+
return true;
|
|
296
|
+
return false;
|
|
297
|
+
}
|
|
171
298
|
function collapseAssistantBursts(messages) {
|
|
172
299
|
const sorted = sortMessagesChronologically(normalizeAll(messages));
|
|
173
300
|
const out = [];
|
|
@@ -177,12 +304,17 @@ function collapseAssistantBursts(messages) {
|
|
|
177
304
|
return;
|
|
178
305
|
const visible = run.filter((m) => !isAssistantReflectionText(m.text ?? ''));
|
|
179
306
|
const pool = visible.length ? visible : run;
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
307
|
+
const merged = [];
|
|
308
|
+
for (const m of pool) {
|
|
309
|
+
const last = merged[merged.length - 1];
|
|
310
|
+
if (last && assistantOneSupersedesOther(last, m)) {
|
|
311
|
+
merged[merged.length - 1] = pickPreferredMessage(last, m, true);
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
merged.push(m);
|
|
315
|
+
}
|
|
185
316
|
}
|
|
317
|
+
out.push(...merged);
|
|
186
318
|
run = [];
|
|
187
319
|
};
|
|
188
320
|
for (const m of sorted) {
|
|
@@ -197,7 +329,7 @@ function collapseAssistantBursts(messages) {
|
|
|
197
329
|
return out;
|
|
198
330
|
}
|
|
199
331
|
export function prepareChatMessagesForDisplay(messages) {
|
|
200
|
-
return collapseAssistantBursts(dedupeUserMessages(messages));
|
|
332
|
+
return collapseAssistantBursts(dedupeUserMessages(fixAssistantFragmentSandwich(messages)));
|
|
201
333
|
}
|
|
202
334
|
function tailOverlap(history, live) {
|
|
203
335
|
const max = Math.min(history.length, live.length, 40);
|
|
@@ -210,6 +342,21 @@ function tailOverlap(history, live) {
|
|
|
210
342
|
}
|
|
211
343
|
return overlap;
|
|
212
344
|
}
|
|
345
|
+
function upsertLiveMessage(merged, m, live, overlap, preferNext) {
|
|
346
|
+
const tailWindow = Math.max(live.length - overlap, 0) + 6;
|
|
347
|
+
const searchFrom = Math.max(0, merged.length - tailWindow);
|
|
348
|
+
let dupIdx = -1;
|
|
349
|
+
for (let j = merged.length - 1; j >= searchFrom; j--) {
|
|
350
|
+
if (messagesEquivalent(merged[j], m)) {
|
|
351
|
+
dupIdx = j;
|
|
352
|
+
break;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (dupIdx >= 0)
|
|
356
|
+
merged[dupIdx] = pickPreferredMessage(merged[dupIdx], m, preferNext);
|
|
357
|
+
else
|
|
358
|
+
merged.push(m);
|
|
359
|
+
}
|
|
213
360
|
export function mergeHistoryTail(prev, incoming) {
|
|
214
361
|
if (!incoming.length)
|
|
215
362
|
return prepareChatMessagesForDisplay(prev);
|
|
@@ -228,11 +375,7 @@ export function mergeHistoryTail(prev, incoming) {
|
|
|
228
375
|
const prefix = prevN.slice(0, Math.max(0, prevN.length - overlap));
|
|
229
376
|
const merged = [...prefix];
|
|
230
377
|
for (const m of inc.slice(overlap)) {
|
|
231
|
-
|
|
232
|
-
if (dupIdx >= 0)
|
|
233
|
-
merged[dupIdx] = pickPreferredMessage(merged[dupIdx], m, true);
|
|
234
|
-
else
|
|
235
|
-
merged.push(m);
|
|
378
|
+
upsertLiveMessage(merged, m, inc, overlap, true);
|
|
236
379
|
}
|
|
237
380
|
return prepareChatMessagesForDisplay(merged);
|
|
238
381
|
}
|
|
@@ -246,12 +389,8 @@ export function mergeDomWithHistory(history, live) {
|
|
|
246
389
|
const overlap = tailOverlap(hist, collapsedLive);
|
|
247
390
|
const prefix = hist.slice(0, Math.max(0, hist.length - overlap));
|
|
248
391
|
const merged = [...prefix];
|
|
249
|
-
for (
|
|
250
|
-
|
|
251
|
-
if (dupIdx >= 0)
|
|
252
|
-
merged[dupIdx] = pickPreferredMessage(merged[dupIdx], m, true);
|
|
253
|
-
else
|
|
254
|
-
merged.push(m);
|
|
392
|
+
for (let i = overlap; i < collapsedLive.length; i++) {
|
|
393
|
+
upsertLiveMessage(merged, collapsedLive[i], collapsedLive, overlap, true);
|
|
255
394
|
}
|
|
256
395
|
return prepareChatMessagesForDisplay(merged);
|
|
257
396
|
}
|
|
@@ -268,8 +407,33 @@ export function historyRowsToChat(rows) {
|
|
|
268
407
|
return m.role === 'user' ? normalizeUserChatMessage(msg) : msg;
|
|
269
408
|
});
|
|
270
409
|
}
|
|
410
|
+
/** DOM sometimes attaches thumbnails to assistant rows; JSONL only has user images. */
|
|
411
|
+
function reassignAssistantImagesToUsers(messages) {
|
|
412
|
+
const orphan = [];
|
|
413
|
+
const stripped = messages.map((m) => {
|
|
414
|
+
if (m.role !== 'assistant' || !(m.images?.length ?? 0))
|
|
415
|
+
return m;
|
|
416
|
+
orphan.push(...m.images);
|
|
417
|
+
return { ...m, images: undefined };
|
|
418
|
+
});
|
|
419
|
+
if (!orphan.length)
|
|
420
|
+
return stripped;
|
|
421
|
+
const extra = canonicalizeImagePaths(orphan);
|
|
422
|
+
for (let i = stripped.length - 1; i >= 0; i--) {
|
|
423
|
+
const m = stripped[i];
|
|
424
|
+
if (m.role !== 'user')
|
|
425
|
+
continue;
|
|
426
|
+
const merged = canonicalizeImagePaths([...userImagePaths(m), ...extra]);
|
|
427
|
+
stripped[i] = normalizeUserChatMessage({
|
|
428
|
+
...m,
|
|
429
|
+
images: merged.length ? merged : undefined,
|
|
430
|
+
});
|
|
431
|
+
break;
|
|
432
|
+
}
|
|
433
|
+
return stripped;
|
|
434
|
+
}
|
|
271
435
|
export function filterClientDisplayList(messages) {
|
|
272
|
-
return messages.filter((m) => {
|
|
436
|
+
return reassignAssistantImagesToUsers(messages).filter((m) => {
|
|
273
437
|
const text = stripJsonlRedactionArtifacts(m.text ?? '');
|
|
274
438
|
if (!text && !(m.images?.length ?? 0))
|
|
275
439
|
return false;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default chat lenta: JSONL via `agent:messages` (see `ChatDisplayStore`).
|
|
3
|
+
* `CHAT_HISTORY_JSONL=1` — additionally socket `agent:history` / app `agentHistory` (legacy).
|
|
4
|
+
*/
|
|
5
|
+
export function isChatHistoryFromJsonl() {
|
|
6
|
+
return process.env.CHAT_HISTORY_JSONL === '1';
|
|
7
|
+
}
|
|
@@ -9,6 +9,8 @@ export declare class CommandExecutor {
|
|
|
9
9
|
private parseImagePaths;
|
|
10
10
|
private sendMessage;
|
|
11
11
|
private focusAgent;
|
|
12
|
+
/** Прокрутка к низу ленты — там смонтированы последние user/assistant узлы. */
|
|
13
|
+
scrollChatToBottom(): Promise<void>;
|
|
12
14
|
private switchTab;
|
|
13
15
|
private clickSelector;
|
|
14
16
|
private clickByLabel;
|
|
@@ -272,6 +272,50 @@ export class CommandExecutor {
|
|
|
272
272
|
: 'Agent tab not open in Cursor — open it once or start a new chat',
|
|
273
273
|
};
|
|
274
274
|
}
|
|
275
|
+
/** Прокрутка к низу ленты — там смонтированы последние user/assistant узлы. */
|
|
276
|
+
async scrollChatToBottom() {
|
|
277
|
+
const client = this.client;
|
|
278
|
+
if (!client)
|
|
279
|
+
return;
|
|
280
|
+
const containerSelectors = this.selectors.chatContainer.strategies;
|
|
281
|
+
try {
|
|
282
|
+
await client.evaluate(`
|
|
283
|
+
(() => {
|
|
284
|
+
const sels = ${JSON.stringify(containerSelectors)};
|
|
285
|
+
function findFirst(sels) {
|
|
286
|
+
for (const sel of sels) {
|
|
287
|
+
try {
|
|
288
|
+
const el = document.querySelector(sel);
|
|
289
|
+
if (el) return el;
|
|
290
|
+
} catch { /* skip */ }
|
|
291
|
+
}
|
|
292
|
+
return null;
|
|
293
|
+
}
|
|
294
|
+
const anchor = findFirst(sels);
|
|
295
|
+
if (!anchor) return false;
|
|
296
|
+
let scrollEl = anchor;
|
|
297
|
+
for (let i = 0; i < 14 && scrollEl; i++) {
|
|
298
|
+
const st = getComputedStyle(scrollEl);
|
|
299
|
+
const oy = st.overflowY;
|
|
300
|
+
if (
|
|
301
|
+
(oy === 'auto' || oy === 'scroll' || oy === 'overlay') &&
|
|
302
|
+
scrollEl.scrollHeight > scrollEl.clientHeight + 8
|
|
303
|
+
) {
|
|
304
|
+
break;
|
|
305
|
+
}
|
|
306
|
+
scrollEl = scrollEl.parentElement;
|
|
307
|
+
}
|
|
308
|
+
if (!scrollEl || scrollEl === document.body) return false;
|
|
309
|
+
scrollEl.scrollTop = scrollEl.scrollHeight;
|
|
310
|
+
scrollEl.dispatchEvent(new Event('scroll', { bubbles: true }));
|
|
311
|
+
return true;
|
|
312
|
+
})()
|
|
313
|
+
`);
|
|
314
|
+
}
|
|
315
|
+
catch {
|
|
316
|
+
/* non-fatal */
|
|
317
|
+
}
|
|
318
|
+
}
|
|
275
319
|
async switchTab(cmd) {
|
|
276
320
|
const client = this.client;
|
|
277
321
|
const title = cmd.tabTitle?.trim();
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export declare function isComposerUuid(id: string | undefined): boolean;
|
|
2
2
|
export declare function isSyntheticTabId(id: string | undefined): boolean;
|
|
3
3
|
export declare function normalizeComposerTitle(title: string): string;
|
|
4
|
+
export declare function titlesAlign(a: string, b: string): boolean;
|
|
4
5
|
/** Learns sidebar title → composer UUID while user switches chats in Cursor. */
|
|
5
6
|
export declare class ComposerTitleIndex {
|
|
6
7
|
private byTitle;
|
|
@@ -14,7 +14,7 @@ export function normalizeComposerTitle(title) {
|
|
|
14
14
|
.trim()
|
|
15
15
|
.toLowerCase();
|
|
16
16
|
}
|
|
17
|
-
function titlesAlign(a, b) {
|
|
17
|
+
export function titlesAlign(a, b) {
|
|
18
18
|
const na = normalizeComposerTitle(a);
|
|
19
19
|
const nb = normalizeComposerTitle(b);
|
|
20
20
|
if (!na || !nb)
|
|
@@ -33,12 +33,12 @@ export class ComposerTitleIndex {
|
|
|
33
33
|
return;
|
|
34
34
|
const tabT = activeTabTitle?.trim();
|
|
35
35
|
const headT = headerTitle?.trim();
|
|
36
|
-
if (
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
36
|
+
if (tabT) {
|
|
37
|
+
this.byTitle.set(normalizeComposerTitle(tabT), composerId);
|
|
38
|
+
}
|
|
39
|
+
if (tabT && headT && titlesAlign(tabT, headT)) {
|
|
40
|
+
this.byTitle.set(normalizeComposerTitle(headT), composerId);
|
|
41
|
+
}
|
|
42
42
|
}
|
|
43
43
|
lookup(title) {
|
|
44
44
|
if (!title?.trim())
|
|
@@ -0,0 +1,2 @@
|
|
|
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>";
|