cursorconnect 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +5 -4
  2. package/bridge-runtime/dist/agent-title-match.js +16 -0
  3. package/bridge-runtime/dist/chat-display-store.d.ts +13 -0
  4. package/bridge-runtime/dist/chat-display-store.js +29 -0
  5. package/bridge-runtime/dist/chat-display.d.ts +11 -0
  6. package/bridge-runtime/dist/chat-display.js +290 -0
  7. package/bridge-runtime/dist/chat-sync.d.ts +6 -0
  8. package/bridge-runtime/dist/chat-sync.js +88 -0
  9. package/bridge-runtime/dist/extract-page.js +99 -3
  10. package/bridge-runtime/dist/history-pipeline-log.d.ts +16 -0
  11. package/bridge-runtime/dist/history-pipeline-log.js +29 -0
  12. package/bridge-runtime/dist/jsonl-index.d.ts +15 -3
  13. package/bridge-runtime/dist/jsonl-index.js +48 -12
  14. package/bridge-runtime/dist/message-filter.d.ts +10 -0
  15. package/bridge-runtime/dist/message-filter.js +65 -5
  16. package/bridge-runtime/dist/pairing-code.d.ts +3 -0
  17. package/bridge-runtime/dist/pairing-code.js +17 -0
  18. package/bridge-runtime/dist/pairing-identity.js +4 -7
  19. package/bridge-runtime/dist/relay.d.ts +8 -0
  20. package/bridge-runtime/dist/relay.js +254 -25
  21. package/bridge-runtime/dist/sidebar-merge.js +2 -2
  22. package/bridge-runtime/dist/types.d.ts +9 -1
  23. package/config.env.defaults +3 -0
  24. package/dist/bridge-dir.js +5 -0
  25. package/dist/cli-version.js +13 -0
  26. package/dist/diagnose.js +224 -0
  27. package/dist/index.js +56 -55
  28. package/dist/launch.js +45 -13
  29. package/dist/pairing-code.js +18 -0
  30. package/dist/pairing-identity.js +3 -6
  31. package/dist/print-pairing.js +9 -7
  32. package/dist/relay-config.js +49 -0
  33. package/dist/semver.js +21 -0
  34. package/dist/version-check.js +31 -0
  35. package/package.json +6 -2
  36. package/version-policy.json +8 -0
package/README.md CHANGED
@@ -17,15 +17,16 @@ cd CursorConnect && npm install
17
17
  npm run install:cli
18
18
  ```
19
19
 
20
- `install:cli` собирает пакет и ставит глобально `cursorconnect`, вызывает `init` для текущего репо.
20
+ `install:cli` собирает пакет и ставит глобально `cursorconnect` (relay встроен из `bridge/.env` при bundle).
21
21
 
22
- В `bridge/.env` задайте `RELAY_URL` и `RELAY_TOKEN`.
22
+ Свой relay опционально `~/.cursorconnect/config.env`.
23
23
 
24
24
  ## Команды
25
25
 
26
26
  ```bash
27
- cursorconnect start # bridge + QR в терминале
28
- cursorconnect start -r # перезапуск Cursor с CDP без вопроса
27
+ cursorconnect start # CDP (авто Cmd+Q + Cursor с :9222) + bridge + код
28
+ cursorconnect start -r # то же (явный перезапуск)
29
+ cursorconnect start --no-restart-cursor # не трогать Cursor (спросит y/n)
29
30
  cursorconnect status
30
31
  cursorconnect stop
31
32
  cursorconnect init /path/to/CursorConnect
@@ -26,6 +26,22 @@ export function agentTitleMatchScore(sidebarTitle, jsonlLabel) {
26
26
  if (wordsB.has(w))
27
27
  score += 2;
28
28
  }
29
+ // Russian / iOS sidebar titles vs first user message (different wording, same topic).
30
+ const stems = [
31
+ [/\bios\b/i, 5],
32
+ [/приложен/i, 5],
33
+ [/полноцен/i, 4],
34
+ [/testflight/i, 4],
35
+ [/\bgit\b/i, 4],
36
+ [/репоз/i, 3],
37
+ [/развер/i, 3],
38
+ [/деплой/i, 3],
39
+ [/проект/i, 2],
40
+ ];
41
+ for (const [re, bonus] of stems) {
42
+ if (re.test(a) && re.test(b))
43
+ score += bonus;
44
+ }
29
45
  if (/\bgit\b/.test(a) && (/\bgit\b/.test(b) || b.includes('гит') || b.includes('репо')))
30
46
  score += 4;
31
47
  if (/\b(repository|repo|deployment|deploy|project)\b/.test(a)) {
@@ -0,0 +1,13 @@
1
+ import type { ChatMessage, HistoryMessage } from './types.js';
2
+ /** Per-agent prepared history + merge with live DOM for subscribed chat. */
3
+ export declare class ChatDisplayStore {
4
+ private historyByAgent;
5
+ clearAgent(agentId: string): void;
6
+ /** JSONL / HTTP / socket history — returns display-ready messages. */
7
+ applyHistory(agentId: string, rows: HistoryMessage[], opts?: {
8
+ mergeWithCache?: boolean;
9
+ }): ChatMessage[];
10
+ getHistory(agentId: string): ChatMessage[];
11
+ /** Raw DOM extract for active composer → merged display list when history exists. */
12
+ mergeLiveForAgent(agentId: string, rawDom: ChatMessage[]): ChatMessage[];
13
+ }
@@ -0,0 +1,29 @@
1
+ import { filterClientDisplayList, historyRowsToChat, mergeDomWithHistory, mergeHistoryTail, prepareChatMessagesForDisplay, } from './chat-display.js';
2
+ /** Per-agent prepared history + merge with live DOM for subscribed chat. */
3
+ export class ChatDisplayStore {
4
+ historyByAgent = new Map();
5
+ clearAgent(agentId) {
6
+ this.historyByAgent.delete(agentId);
7
+ }
8
+ /** JSONL / HTTP / socket history — returns display-ready messages. */
9
+ applyHistory(agentId, rows, opts) {
10
+ const incoming = prepareChatMessagesForDisplay(historyRowsToChat(rows));
11
+ const prev = this.historyByAgent.get(agentId) ?? [];
12
+ const merged = opts?.mergeWithCache && prev.length
13
+ ? mergeHistoryTail(prev, incoming)
14
+ : incoming;
15
+ const out = filterClientDisplayList(merged);
16
+ this.historyByAgent.set(agentId, out);
17
+ return out;
18
+ }
19
+ getHistory(agentId) {
20
+ return this.historyByAgent.get(agentId) ?? [];
21
+ }
22
+ /** Raw DOM extract for active composer → merged display list when history exists. */
23
+ mergeLiveForAgent(agentId, rawDom) {
24
+ const live = prepareChatMessagesForDisplay(rawDom);
25
+ const hist = this.historyByAgent.get(agentId) ?? [];
26
+ const merged = hist.length ? mergeDomWithHistory(hist, live) : live;
27
+ return filterClientDisplayList(merged);
28
+ }
29
+ }
@@ -0,0 +1,11 @@
1
+ import type { ChatMessage, HistoryMessage } from './types.js';
2
+ export declare function compareUserText(text: string): string;
3
+ export declare function userTextsEquivalent(ta: string, tb: string): boolean;
4
+ export declare function userMessagesEquivalent(a: ChatMessage, b: ChatMessage): boolean;
5
+ export declare function sortMessagesChronologically(messages: ChatMessage[]): ChatMessage[];
6
+ export declare function prepareChatMessagesForDisplay(messages: ChatMessage[]): ChatMessage[];
7
+ export declare function mergeHistoryTail(prev: ChatMessage[], incoming: ChatMessage[]): ChatMessage[];
8
+ export declare function mergeDomWithHistory(history: ChatMessage[], live: ChatMessage[]): ChatMessage[];
9
+ export declare function historyRowsToChat(rows: HistoryMessage[]): ChatMessage[];
10
+ export declare function filterClientDisplayList(messages: ChatMessage[]): ChatMessage[];
11
+ export declare function userMessageCoversExisting(existing: ChatMessage, sentText: string): boolean;
@@ -0,0 +1,290 @@
1
+ import { cleanUserText, isAssistantReflectionText, isPassiveStatusChatLine, parseUserImagePaths, stripJsonlRedactionArtifacts, } from './message-filter.js';
2
+ function normalizeUserChatMessage(m) {
3
+ const images = [...new Set([...(m.images ?? []), ...parseUserImagePaths(m.text ?? '')])];
4
+ const text = cleanUserText(m.text ?? '');
5
+ return {
6
+ ...m,
7
+ text,
8
+ images: images.length ? images : undefined,
9
+ };
10
+ }
11
+ function normalizeMessage(m) {
12
+ return m.role === 'user' ? normalizeUserChatMessage(m) : m;
13
+ }
14
+ function normalizeAll(messages) {
15
+ return messages.map(normalizeMessage);
16
+ }
17
+ export function compareUserText(text) {
18
+ return cleanUserText(text).replace(/\s+/g, ' ').trim();
19
+ }
20
+ function userImagePaths(m) {
21
+ return [...new Set([...(m.images ?? []), ...parseUserImagePaths(m.text ?? '')])];
22
+ }
23
+ export function userTextsEquivalent(ta, tb) {
24
+ const a = compareUserText(ta);
25
+ const b = compareUserText(tb);
26
+ if (!a || !b)
27
+ return !a && !b;
28
+ if (a === b)
29
+ return true;
30
+ const short = a.length <= b.length ? a : b;
31
+ const long = a.length <= b.length ? b : a;
32
+ if (short.length < 12)
33
+ return false;
34
+ if (long.includes(short)) {
35
+ if (long.endsWith(short) || long.startsWith(short))
36
+ return true;
37
+ if (short.length / long.length >= 0.4)
38
+ return true;
39
+ }
40
+ return false;
41
+ }
42
+ export function userMessagesEquivalent(a, b) {
43
+ if (a.role !== 'user' || b.role !== 'user')
44
+ return false;
45
+ const ta = compareUserText(a.text ?? '');
46
+ const tb = compareUserText(b.text ?? '');
47
+ if (ta && tb && userTextsEquivalent(ta, tb))
48
+ return true;
49
+ const pa = userImagePaths(a);
50
+ 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
+ if (ta && tb && pa.length > 0 && pb.length > 0) {
57
+ const setB = new Set(pb);
58
+ if (pa.every((p) => setB.has(p)))
59
+ return true;
60
+ }
61
+ return false;
62
+ }
63
+ function pickPreferredUserMessage(a, b, preferB) {
64
+ const na = normalizeUserChatMessage(a);
65
+ const nb = normalizeUserChatMessage(b);
66
+ const score = (m) => {
67
+ let s = 0;
68
+ const nImg = m.images?.length ?? 0;
69
+ if (nImg)
70
+ s += 20 + nImg;
71
+ if (!/<image_files>/i.test(m.text ?? ''))
72
+ s += 40;
73
+ if ((m.text ?? '').trim())
74
+ s += 5;
75
+ if (!m.id.startsWith('hist-'))
76
+ s += 8;
77
+ return s;
78
+ };
79
+ const sa = score(na);
80
+ const sb = score(nb);
81
+ let pick = sa > sb ? na : sb > sa ? nb : preferB ? nb : na;
82
+ const other = pick === na ? nb : na;
83
+ if ((pick.text?.length ?? 0) < (other.text?.length ?? 0))
84
+ pick = other;
85
+ const images = [...new Set([...(pick.images ?? []), ...(other.images ?? [])])];
86
+ const flatPick = Math.min(a.flatIndex ?? Number.MAX_SAFE_INTEGER, b.flatIndex ?? Number.MAX_SAFE_INTEGER);
87
+ const id = !pick.id.startsWith('hist-')
88
+ ? pick.id
89
+ : !other.id.startsWith('hist-')
90
+ ? other.id
91
+ : pick.id;
92
+ return {
93
+ ...pick,
94
+ id,
95
+ images: images.length ? images : undefined,
96
+ flatIndex: Number.isFinite(flatPick) ? flatPick : pick.flatIndex ?? other.flatIndex,
97
+ };
98
+ }
99
+ function messageKey(m) {
100
+ const n = normalizeMessage(m);
101
+ if (n.role === 'user') {
102
+ return `user:${compareUserText(n.text ?? '')}:${(n.images ?? []).join('|')}`;
103
+ }
104
+ const html = n.html?.trim() ? '1' : '0';
105
+ return `${n.role}:${n.text.trim().replace(/\s+/g, ' ')}:${(n.images ?? []).join('|')}:${html}`;
106
+ }
107
+ function messagesEquivalent(a, b) {
108
+ if (a.role === 'user' && b.role === 'user')
109
+ return userMessagesEquivalent(a, b);
110
+ return messageKey(a) === messageKey(b);
111
+ }
112
+ function pickPreferredMessage(prev, next, preferNext) {
113
+ if (prev.role === 'user' && next.role === 'user') {
114
+ return pickPreferredUserMessage(prev, next, preferNext);
115
+ }
116
+ if (prev.role !== 'assistant' || next.role !== 'assistant') {
117
+ return preferNext ? next : prev;
118
+ }
119
+ const score = (m) => {
120
+ let s = 0;
121
+ if (m.html?.trim())
122
+ s += 30;
123
+ s += Math.min(m.text?.length ?? 0, 5000) / 100;
124
+ if (!m.id.startsWith('hist-'))
125
+ s += 5;
126
+ return s;
127
+ };
128
+ const sp = score(prev);
129
+ const sn = score(next);
130
+ if (sn > sp)
131
+ return next;
132
+ if (sp > sn)
133
+ return prev;
134
+ return preferNext ? next : prev;
135
+ }
136
+ function messageOrderKey(m) {
137
+ if (m.flatIndex != null && Number.isFinite(m.flatIndex))
138
+ return m.flatIndex;
139
+ const hist = /^hist-(\d+)/.exec(m.id);
140
+ if (hist)
141
+ return Number(hist[1]);
142
+ return 0;
143
+ }
144
+ export function sortMessagesChronologically(messages) {
145
+ return [...messages].sort((a, b) => {
146
+ const ka = messageOrderKey(a);
147
+ const kb = messageOrderKey(b);
148
+ if (ka !== kb)
149
+ return ka - kb;
150
+ return a.id.localeCompare(b.id);
151
+ });
152
+ }
153
+ function dedupeUserMessages(messages) {
154
+ const sorted = sortMessagesChronologically(normalizeAll(messages));
155
+ const out = [];
156
+ for (const m of sorted) {
157
+ if (m.role !== 'user') {
158
+ out.push(m);
159
+ continue;
160
+ }
161
+ const idx = out.findIndex((x) => x.role === 'user' && userMessagesEquivalent(x, m));
162
+ if (idx >= 0) {
163
+ out[idx] = pickPreferredUserMessage(out[idx], m, false);
164
+ }
165
+ else {
166
+ out.push(normalizeUserChatMessage(m));
167
+ }
168
+ }
169
+ return out;
170
+ }
171
+ function collapseAssistantBursts(messages) {
172
+ const sorted = sortMessagesChronologically(normalizeAll(messages));
173
+ const out = [];
174
+ let run = [];
175
+ const flush = () => {
176
+ if (!run.length)
177
+ return;
178
+ const visible = run.filter((m) => !isAssistantReflectionText(m.text ?? ''));
179
+ const pool = visible.length ? visible : run;
180
+ if (pool.length === 1) {
181
+ out.push(pool[0]);
182
+ }
183
+ else {
184
+ out.push(pool.reduce((a, b) => ((a.flatIndex ?? 0) >= (b.flatIndex ?? 0) ? a : b)));
185
+ }
186
+ run = [];
187
+ };
188
+ for (const m of sorted) {
189
+ if (m.role === 'assistant')
190
+ run.push(m);
191
+ else {
192
+ flush();
193
+ out.push(m);
194
+ }
195
+ }
196
+ flush();
197
+ return out;
198
+ }
199
+ export function prepareChatMessagesForDisplay(messages) {
200
+ return collapseAssistantBursts(dedupeUserMessages(messages));
201
+ }
202
+ function tailOverlap(history, live) {
203
+ const max = Math.min(history.length, live.length, 40);
204
+ let overlap = 0;
205
+ for (let n = 1; n <= max; n++) {
206
+ const a = history.slice(-n);
207
+ const b = live.slice(0, n);
208
+ if (a.every((m, i) => messagesEquivalent(m, b[i])))
209
+ overlap = n;
210
+ }
211
+ return overlap;
212
+ }
213
+ export function mergeHistoryTail(prev, incoming) {
214
+ if (!incoming.length)
215
+ return prepareChatMessagesForDisplay(prev);
216
+ if (!prev.length)
217
+ return prepareChatMessagesForDisplay(incoming);
218
+ const prevN = normalizeAll(prev);
219
+ const inc = normalizeAll(incoming);
220
+ let overlap = 0;
221
+ const maxOverlap = Math.min(prevN.length, inc.length, 40);
222
+ for (let n = 1; n <= maxOverlap; n++) {
223
+ const a = prevN.slice(-n);
224
+ const b = inc.slice(0, n);
225
+ if (a.every((m, i) => messagesEquivalent(m, b[i])))
226
+ overlap = n;
227
+ }
228
+ const prefix = prevN.slice(0, Math.max(0, prevN.length - overlap));
229
+ const merged = [...prefix];
230
+ for (const m of inc.slice(overlap)) {
231
+ const dupIdx = merged.findIndex((x) => messagesEquivalent(x, m));
232
+ if (dupIdx >= 0)
233
+ merged[dupIdx] = pickPreferredMessage(merged[dupIdx], m, true);
234
+ else
235
+ merged.push(m);
236
+ }
237
+ return prepareChatMessagesForDisplay(merged);
238
+ }
239
+ export function mergeDomWithHistory(history, live) {
240
+ const hist = dedupeUserMessages(history);
241
+ const collapsedLive = collapseAssistantBursts(live);
242
+ if (!collapsedLive.length)
243
+ return prepareChatMessagesForDisplay(hist);
244
+ if (!hist.length)
245
+ return prepareChatMessagesForDisplay(collapsedLive);
246
+ const overlap = tailOverlap(hist, collapsedLive);
247
+ const prefix = hist.slice(0, Math.max(0, hist.length - overlap));
248
+ const merged = [...prefix];
249
+ for (const m of collapsedLive) {
250
+ const dupIdx = merged.findIndex((x) => messagesEquivalent(x, m));
251
+ if (dupIdx >= 0)
252
+ merged[dupIdx] = pickPreferredMessage(merged[dupIdx], m, true);
253
+ else
254
+ merged.push(m);
255
+ }
256
+ return prepareChatMessagesForDisplay(merged);
257
+ }
258
+ export function historyRowsToChat(rows) {
259
+ return rows.map((m, i) => {
260
+ const msg = {
261
+ id: `hist-${m.ts ?? i}`,
262
+ role: m.role,
263
+ text: stripJsonlRedactionArtifacts(m.text),
264
+ html: m.html,
265
+ images: m.images?.length ? m.images : undefined,
266
+ flatIndex: m.ts ?? i,
267
+ };
268
+ return m.role === 'user' ? normalizeUserChatMessage(msg) : msg;
269
+ });
270
+ }
271
+ export function filterClientDisplayList(messages) {
272
+ return messages.filter((m) => {
273
+ const text = stripJsonlRedactionArtifacts(m.text ?? '');
274
+ if (!text && !(m.images?.length ?? 0))
275
+ return false;
276
+ if (m.role === 'assistant' && isAssistantReflectionText(text))
277
+ return false;
278
+ if (m.role === 'assistant' && isPassiveStatusChatLine(text))
279
+ return false;
280
+ if (m.role === 'user') {
281
+ return Boolean(text.trim()) || (m.images?.length ?? 0) > 0;
282
+ }
283
+ return true;
284
+ });
285
+ }
286
+ export function userMessageCoversExisting(existing, sentText) {
287
+ if (existing.role !== 'user')
288
+ return false;
289
+ return userTextsEquivalent(existing.text ?? '', sentText);
290
+ }
@@ -0,0 +1,6 @@
1
+ import type { CursorState } from './types.js';
2
+ export declare function normalizeAgentTitle(title: string): string;
3
+ export declare function isComposerUuid(id: string | undefined): boolean;
4
+ export declare function resolveCursorActiveComposerId(state: CursorState): string | undefined;
5
+ /** Live DOM applies only when the subscribed app chat matches Cursor's active composer. */
6
+ export declare function isChatSyncedWithCursor(selectedAgentId: string | undefined, selectedTitle: string | undefined, state: Pick<CursorState, 'activeComposerId' | 'tabs' | 'composerIdByTitle' | 'activeChatTitle'>): boolean;
@@ -0,0 +1,88 @@
1
+ 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
+ export function normalizeAgentTitle(title) {
3
+ return title
4
+ .trim()
5
+ .replace(/\s+/g, ' ')
6
+ .replace(/\d+\s*(?:s|m|h|d|w)\b/gi, '')
7
+ .replace(/\d+[smhdw]$/i, '')
8
+ .trim()
9
+ .toLowerCase();
10
+ }
11
+ export function isComposerUuid(id) {
12
+ return !!id && UUID_RE.test(id);
13
+ }
14
+ function lookupComposerIdByTitle(title, map) {
15
+ if (!title?.trim() || !map)
16
+ return undefined;
17
+ return map[normalizeAgentTitle(title)];
18
+ }
19
+ export function resolveCursorActiveComposerId(state) {
20
+ const activeTab = state.tabs.find((t) => t.active);
21
+ if (isComposerUuid(state.activeComposerId))
22
+ return state.activeComposerId;
23
+ const fromTabTitle = lookupComposerIdByTitle(activeTab?.title, state.composerIdByTitle);
24
+ if (fromTabTitle)
25
+ return fromTabTitle;
26
+ const fromHeader = lookupComposerIdByTitle(state.activeChatTitle, state.composerIdByTitle);
27
+ if (fromHeader)
28
+ return fromHeader;
29
+ if (activeTab?.composerId && isComposerUuid(activeTab.composerId)) {
30
+ return activeTab.composerId;
31
+ }
32
+ return activeTab?.composerId || activeTab?.id || state.activeComposerId;
33
+ }
34
+ function isSyntheticRouteId(agentId) {
35
+ return /^sidebar-\d+$/.test(agentId) || agentId.startsWith('title:');
36
+ }
37
+ /** Live DOM applies only when the subscribed app chat matches Cursor's active composer. */
38
+ export function isChatSyncedWithCursor(selectedAgentId, selectedTitle, state) {
39
+ if (!selectedAgentId)
40
+ return false;
41
+ const cursorActiveId = resolveCursorActiveComposerId({
42
+ connected: true,
43
+ windows: [],
44
+ tabs: state.tabs,
45
+ messages: [],
46
+ todos: [],
47
+ queuedMessages: [],
48
+ pendingApprovals: [],
49
+ activeComposerId: state.activeComposerId,
50
+ composerIdByTitle: state.composerIdByTitle,
51
+ activeChatTitle: state.activeChatTitle,
52
+ updatedAt: 0,
53
+ });
54
+ if (!cursorActiveId)
55
+ return false;
56
+ if (selectedAgentId === cursorActiveId)
57
+ return true;
58
+ const normSelected = selectedTitle ? normalizeAgentTitle(selectedTitle) : '';
59
+ if (normSelected &&
60
+ state.activeChatTitle &&
61
+ normSelected === normalizeAgentTitle(state.activeChatTitle) &&
62
+ isComposerUuid(cursorActiveId)) {
63
+ return true;
64
+ }
65
+ const mappedFromTitle = lookupComposerIdByTitle(selectedTitle, state.composerIdByTitle);
66
+ if (mappedFromTitle &&
67
+ mappedFromTitle === cursorActiveId &&
68
+ (selectedAgentId === mappedFromTitle || isSyntheticRouteId(selectedAgentId))) {
69
+ return true;
70
+ }
71
+ const tabForActive = state.tabs.find((t) => t.active ||
72
+ t.composerId === cursorActiveId ||
73
+ t.id === cursorActiveId);
74
+ if (tabForActive && normSelected && isComposerUuid(cursorActiveId)) {
75
+ if (normalizeAgentTitle(tabForActive.title) === normSelected)
76
+ return true;
77
+ }
78
+ if (!tabForActive || !selectedTitle)
79
+ return false;
80
+ if (normalizeAgentTitle(tabForActive.title) !== normalizeAgentTitle(selectedTitle)) {
81
+ return false;
82
+ }
83
+ const tabComposerId = tabForActive.composerId || tabForActive.id;
84
+ if (isComposerUuid(selectedAgentId)) {
85
+ return tabComposerId === selectedAgentId || cursorActiveId === selectedAgentId;
86
+ }
87
+ return false;
88
+ }
@@ -543,9 +543,27 @@ export function extractionFunction(containerSelectors, tabSelectors, inputSelect
543
543
  return undefined;
544
544
  return hit;
545
545
  }
546
+ function isAssistantReflectionText(text) {
547
+ const t = text.trim().replace(/\s+/g, ' ');
548
+ if (!t || isNoiseChatText(t))
549
+ return true;
550
+ if (/^Thought\s*for\s*\d/i.test(t) && t.length < 160)
551
+ return true;
552
+ if (/^(Exploring|Grepped|Searched|Listed|Read |Ran |Edited |Loading|Planning|Using image)/i.test(t)) {
553
+ return true;
554
+ }
555
+ const toolHits = t.match(/\b(Explored|Grepped|Searched|Listed|Read )\b/gi);
556
+ if (toolHits && toolHits.length >= 2)
557
+ return true;
558
+ if (t.length > 160 &&
559
+ /\b(state\.messages|flat-index|mapKeyedChildren|humanEl|extract-page|userMessagesEquivalent)\b/i.test(t)) {
560
+ return true;
561
+ }
562
+ return false;
563
+ }
546
564
  function isMeaningfulAssistantText(text) {
547
565
  const t = text.trim();
548
- if (!t || isNoiseChatText(t))
566
+ if (!t || isNoiseChatText(t) || isAssistantReflectionText(t))
549
567
  return false;
550
568
  if (t.length >= 20 && /\s/.test(t))
551
569
  return true;
@@ -553,12 +571,28 @@ export function extractionFunction(containerSelectors, tabSelectors, inputSelect
553
571
  return true;
554
572
  return t.length >= 50;
555
573
  }
574
+ function isAssistantThinkingRow(el) {
575
+ if (el.getAttribute('data-message-kind') === 'thinking')
576
+ return true;
577
+ if (el.querySelector('[class*="thought"], [class*="thinking"]'))
578
+ return true;
579
+ if (el.querySelector('.composer-tool-former-message, [class*="tool-call-header"], [class*="ui-tool-call"]') &&
580
+ !el.querySelector('.markdown-root')) {
581
+ return true;
582
+ }
583
+ return false;
584
+ }
556
585
  function cleanUserText(text) {
557
586
  return text
587
+ .replace(/^\[Image\]\s*$/gim, '')
588
+ .replace(/<image_files>[\s\S]*?<\/image_files>/gi, '')
589
+ .replace(/(?:^|\n)\s*The following images? (?:were |has been )?provid(?:ed|ied)[^\n]*(?:\n\s*\d+\.\s*[^\n]+)*/gi, '\n')
590
+ .replace(/(?:^|\n)\s*These images can be copied for use in other locations\.?\s*/gi, '\n')
558
591
  .replace(/<user_query>\s*/gi, '')
559
592
  .replace(/<\/user_query>/gi, '')
560
593
  .replace(/([.!?…])(\d+\.\s*)/g, '$1\n$2')
561
594
  .replace(/([^\n\d\s])(\d+\.\s+)/g, '$1\n$2')
595
+ .replace(/\n{3,}/g, '\n\n')
562
596
  .trim();
563
597
  }
564
598
  function normalizeImageSrc(src) {
@@ -605,6 +639,10 @@ export function extractionFunction(containerSelectors, tabSelectors, inputSelect
605
639
  function isToolNoiseElement(el, msgKind) {
606
640
  if (msgKind === 'tool')
607
641
  return true;
642
+ // Row may contain tool chrome + prose; keep when human/assistant body exists.
643
+ if (el.querySelector('.markdown-root, .aislash-editor-input-readonly, .composer-human-tiptap-readonly-editor, .composer-human-message-content, .composer-human-message')) {
644
+ return false;
645
+ }
608
646
  if (el.querySelector('.loading-indicator-v3'))
609
647
  return true;
610
648
  if (el.querySelector('.composer-tool-former-message'))
@@ -633,8 +671,14 @@ export function extractionFunction(containerSelectors, tabSelectors, inputSelect
633
671
  recordDrop('tool', el);
634
672
  return;
635
673
  }
674
+ if (isAssistantThinkingRow(el)) {
675
+ recordDrop('assistantNotMeaningful', el);
676
+ return;
677
+ }
636
678
  const humanEl = el.querySelector('.aislash-editor-input-readonly, .composer-human-tiptap-readonly-editor, .composer-human-message-content, .composer-human-message');
637
- const mdRoot = el.querySelector('.markdown-root');
679
+ const mdRoot = el.querySelector('.markdown-root') ||
680
+ el.querySelector('[class*="markdown-root"]') ||
681
+ el.querySelector('.anysphere-markdown-container');
638
682
  const isHuman = role === 'human' || !!humanEl;
639
683
  if (isHuman) {
640
684
  const images = extractRowImages(el);
@@ -662,13 +706,19 @@ export function extractionFunction(containerSelectors, tabSelectors, inputSelect
662
706
  messageDebug.extracted++;
663
707
  return;
664
708
  }
709
+ const rowText = (el.textContent || '').trim();
665
710
  const text = mdRoot?.textContent?.trim() ?? '';
666
711
  if (text && textImpliesAgentWorking(text)) {
667
712
  recordDrop('assistantWorking', el);
668
713
  return;
669
714
  }
670
715
  if (!mdRoot || !text) {
671
- recordDrop('assistantNoMarkdown', el);
716
+ if (rowText && isAssistantReflectionText(rowText)) {
717
+ recordDrop('assistantNotMeaningful', el);
718
+ }
719
+ else {
720
+ recordDrop('assistantNoMarkdown', el);
721
+ }
672
722
  return;
673
723
  }
674
724
  if (!isMeaningfulAssistantText(text)) {
@@ -685,6 +735,52 @@ export function extractionFunction(containerSelectors, tabSelectors, inputSelect
685
735
  messageDebug.extracted++;
686
736
  });
687
737
  messages.sort((a, b) => (a.flatIndex ?? 0) - (b.flatIndex ?? 0));
738
+ function compareUserText(text) {
739
+ return cleanUserText(text).replace(/\s+/g, ' ').trim();
740
+ }
741
+ function userTextDup(a, b) {
742
+ const ta = compareUserText(a);
743
+ const tb = compareUserText(b);
744
+ if (!ta || !tb)
745
+ return !ta && !tb;
746
+ if (ta === tb)
747
+ return true;
748
+ const short = ta.length <= tb.length ? ta : tb;
749
+ const long = ta.length <= tb.length ? tb : ta;
750
+ if (short.length < 12)
751
+ return false;
752
+ if (long.includes(short)) {
753
+ if (long.endsWith(short) || long.startsWith(short))
754
+ return true;
755
+ if (short.length / long.length >= 0.4)
756
+ return true;
757
+ }
758
+ return false;
759
+ }
760
+ const dedupedMessages = [];
761
+ for (const m of messages) {
762
+ if (m.role !== 'user') {
763
+ dedupedMessages.push(m);
764
+ continue;
765
+ }
766
+ const dupIdx = dedupedMessages.findIndex((x) => x.role === 'user' && userTextDup(x.text, m.text));
767
+ if (dupIdx >= 0) {
768
+ const prev = dedupedMessages[dupIdx];
769
+ const keep = prev.text.length >= m.text.length ? prev : m;
770
+ const drop = prev.text.length >= m.text.length ? m : prev;
771
+ dedupedMessages[dupIdx] = {
772
+ ...keep,
773
+ images: keep.images?.length || drop.images?.length
774
+ ? [...new Set([...(keep.images ?? []), ...(drop.images ?? [])])]
775
+ : undefined,
776
+ flatIndex: Math.min(prev.flatIndex ?? 0, m.flatIndex ?? 0),
777
+ };
778
+ continue;
779
+ }
780
+ dedupedMessages.push(m);
781
+ }
782
+ messages.length = 0;
783
+ messages.push(...dedupedMessages);
688
784
  let containerComposerId = container.getAttribute('data-composer-id') ||
689
785
  container.closest('[data-composer-id]')?.getAttribute('data-composer-id') ||
690
786
  '';
@@ -0,0 +1,16 @@
1
+ export interface PipelineEntry {
2
+ at: number;
3
+ layer: 'bridge';
4
+ dir: 'in' | 'out' | 'internal';
5
+ event: string;
6
+ requestId?: string;
7
+ agentId?: string;
8
+ bytes?: number;
9
+ msgs?: number;
10
+ detail?: string;
11
+ }
12
+ export declare function bridgePipelineLog(entry: Omit<PipelineEntry, 'at' | 'layer'> & {
13
+ at?: number;
14
+ }): void;
15
+ export declare function bridgePipelineSnapshot(): PipelineEntry[];
16
+ export declare function bridgePipelineReportLines(): string[];