cursorconnect 0.1.2 → 0.1.5
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/README.md +6 -5
- package/bridge-runtime/dist/agent-title-match.js +16 -0
- package/bridge-runtime/dist/chat-display-store.d.ts +13 -0
- package/bridge-runtime/dist/chat-display-store.js +29 -0
- package/bridge-runtime/dist/chat-display.d.ts +11 -0
- package/bridge-runtime/dist/chat-display.js +290 -0
- package/bridge-runtime/dist/chat-sync.d.ts +6 -0
- package/bridge-runtime/dist/chat-sync.js +88 -0
- package/bridge-runtime/dist/extract-page.js +99 -3
- package/bridge-runtime/dist/history-pipeline-log.d.ts +16 -0
- package/bridge-runtime/dist/history-pipeline-log.js +29 -0
- package/bridge-runtime/dist/jsonl-index.d.ts +15 -3
- package/bridge-runtime/dist/jsonl-index.js +48 -12
- package/bridge-runtime/dist/message-filter.d.ts +10 -0
- package/bridge-runtime/dist/message-filter.js +65 -5
- package/bridge-runtime/dist/pairing-code.d.ts +3 -0
- package/bridge-runtime/dist/pairing-code.js +17 -0
- package/bridge-runtime/dist/pairing-identity.js +4 -7
- package/bridge-runtime/dist/relay.d.ts +8 -0
- package/bridge-runtime/dist/relay.js +254 -25
- package/bridge-runtime/dist/sidebar-merge.js +2 -2
- package/bridge-runtime/dist/types.d.ts +9 -1
- package/config.env.defaults +3 -0
- package/dist/big-code.js +36 -5
- package/dist/bridge-dir.js +6 -1
- package/dist/cli-version.js +13 -0
- package/dist/diagnose.js +224 -0
- package/dist/index.js +56 -92
- package/dist/launch.js +52 -14
- package/dist/pairing-code.js +18 -0
- package/dist/pairing-identity.js +6 -8
- package/dist/pairing-ttl.js +3 -0
- package/dist/print-pairing.js +18 -25
- package/dist/relay-config.js +49 -0
- package/dist/repo-root.js +2 -2
- package/dist/semver.js +21 -0
- package/dist/version-check.js +31 -0
- package/package.json +7 -3
- package/version-policy.json +8 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# cursorconnect
|
|
2
2
|
|
|
3
|
-
CLI для Mac:
|
|
3
|
+
CLI для Mac: Cursor Connect в фоне, pairing (код), Cursor с CDP.
|
|
4
4
|
|
|
5
5
|
## Установка (пользователи)
|
|
6
6
|
|
|
@@ -17,15 +17,16 @@ cd CursorConnect && npm install
|
|
|
17
17
|
npm run install:cli
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
`install:cli` собирает пакет и ставит глобально `cursorconnect
|
|
20
|
+
`install:cli` собирает пакет и ставит глобально `cursorconnect` (relay встроен из `bridge/.env` при bundle).
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
Свой relay — опционально `~/.cursorconnect/config.env`.
|
|
23
23
|
|
|
24
24
|
## Команды
|
|
25
25
|
|
|
26
26
|
```bash
|
|
27
|
-
cursorconnect start #
|
|
28
|
-
cursorconnect start -r #
|
|
27
|
+
cursorconnect start # CDP (авто Cmd+Q + Cursor с :9222) + Cursor Connect + код
|
|
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
|
-
|
|
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[];
|