cursorconnect 0.1.8 → 0.1.10
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 +2 -0
- package/bridge-runtime/connector-version.json +1 -1
- package/bridge-runtime/dist/agent-completion-push.d.ts +18 -6
- package/bridge-runtime/dist/agent-completion-push.js +186 -41
- package/bridge-runtime/dist/agent-completion-readiness.d.ts +19 -0
- package/bridge-runtime/dist/agent-completion-readiness.js +42 -0
- package/bridge-runtime/dist/chat-display-store.d.ts +32 -7
- package/bridge-runtime/dist/chat-display-store.js +96 -21
- package/bridge-runtime/dist/chat-display.d.ts +36 -0
- package/bridge-runtime/dist/chat-display.js +287 -24
- package/bridge-runtime/dist/chat-sync.d.ts +3 -1
- package/bridge-runtime/dist/chat-sync.js +20 -0
- package/bridge-runtime/dist/debug-chats-page.d.ts +1 -1
- package/bridge-runtime/dist/debug-chats-page.js +148 -26
- package/bridge-runtime/dist/dom-transcript-store.d.ts +2 -0
- package/bridge-runtime/dist/dom-transcript-store.js +17 -2
- package/bridge-runtime/dist/extract-page.js +5 -4
- package/bridge-runtime/dist/lenta-capture.d.ts +46 -0
- package/bridge-runtime/dist/lenta-capture.js +146 -0
- package/bridge-runtime/dist/lenta-debug.d.ts +42 -0
- package/bridge-runtime/dist/lenta-debug.js +221 -0
- package/bridge-runtime/dist/lenta-delivery.d.ts +3 -0
- package/bridge-runtime/dist/lenta-delivery.js +10 -0
- package/bridge-runtime/dist/lenta-seq-journal.d.ts +48 -0
- package/bridge-runtime/dist/lenta-seq-journal.js +109 -0
- package/bridge-runtime/dist/message-filter.d.ts +5 -0
- package/bridge-runtime/dist/message-filter.js +4 -0
- package/bridge-runtime/dist/relay.d.ts +37 -3
- package/bridge-runtime/dist/relay.js +557 -51
- package/bridge-runtime/dist/types.d.ts +9 -4
- package/dist/bridge-build.js +50 -0
- package/dist/index.js +9 -6
- package/dist/launch.js +5 -1
- package/dist/run-service.js +10 -4
- package/dist/startup-check.js +6 -0
- package/package.json +1 -1
- package/version-policy.json +1 -1
|
@@ -1,27 +1,58 @@
|
|
|
1
|
-
import { filterClientDisplayList, historyRowsToChat, mergeHistoryTail,
|
|
1
|
+
import { archiveOrderFloor, filterClientDisplayList, historyRowsToChat, mergeHistoryTail, prepareChatMessagesForDisplay, prepareDomOverlayForDisplay, selectDomOverlayFromViewport, stampOverlayAfterArchive, } from './chat-display.js';
|
|
2
2
|
import { DomTranscriptStore } from './dom-transcript-store.js';
|
|
3
3
|
/**
|
|
4
|
-
* Chat UI store: JSONL baseline
|
|
5
|
-
*
|
|
4
|
+
* Chat UI store: JSONL baseline + DOM overlay tail.
|
|
5
|
+
*
|
|
6
|
+
* Invariant:
|
|
7
|
+
* - JSONL baseline = everything already in `.jsonl` (monotonic archive order).
|
|
8
|
+
* - DOM overlay = viewport rows **not covered** by JSONL (`archiveCoversOverlay`).
|
|
9
|
+
* - While a row streams in DOM but JSONL is shorter → overlay keeps it; when JSONL catches up → overlay drops (no duplicate).
|
|
10
|
+
* - App lenta = `[...jsonlHistory, ...domOverlay]` — never sort both by DOM flatIndex.
|
|
6
11
|
*/
|
|
7
12
|
export class ChatDisplayStore {
|
|
8
13
|
domTranscript = new DomTranscriptStore();
|
|
14
|
+
/** Last CDP viewport extract per agent — sole source for live overlay. */
|
|
15
|
+
latestDomViewport = new Map();
|
|
9
16
|
jsonlBaseline = new Map();
|
|
10
17
|
/** Raw JSONL row count per agent — incremental display merge during streaming. */
|
|
11
18
|
jsonlRowCount = new Map();
|
|
19
|
+
/** While true, bridge pushes lenta on DOM ingest (emit path); overlay filter unchanged. */
|
|
20
|
+
generatingByAgent = new Map();
|
|
12
21
|
clearAgent(agentId) {
|
|
13
22
|
this.domTranscript.clear(agentId);
|
|
23
|
+
this.latestDomViewport.delete(agentId);
|
|
14
24
|
this.jsonlBaseline.delete(agentId);
|
|
15
25
|
this.jsonlRowCount.delete(agentId);
|
|
26
|
+
this.generatingByAgent.delete(agentId);
|
|
27
|
+
}
|
|
28
|
+
setAgentGenerating(agentId, generating) {
|
|
29
|
+
if (generating)
|
|
30
|
+
this.generatingByAgent.set(agentId, true);
|
|
31
|
+
else {
|
|
32
|
+
this.generatingByAgent.delete(agentId);
|
|
33
|
+
this.reconcileDomViewportToArchive(agentId);
|
|
34
|
+
this.pruneDomOverlay(agentId);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
isAgentGenerating(agentId) {
|
|
38
|
+
return this.generatingByAgent.get(agentId) ?? false;
|
|
16
39
|
}
|
|
17
40
|
getJsonlHistory(agentId) {
|
|
18
41
|
return filterClientDisplayList(this.jsonlBaseline.get(agentId) ?? []);
|
|
19
42
|
}
|
|
20
43
|
getDomLive(agentId) {
|
|
21
|
-
const
|
|
22
|
-
|
|
44
|
+
const archive = this.archiveForOverlayMatch(agentId);
|
|
45
|
+
const dom = selectDomOverlayFromViewport(this.latestDomViewport.get(agentId) ?? [], archive);
|
|
46
|
+
if (!dom.length)
|
|
47
|
+
return [];
|
|
48
|
+
if (!archive.length) {
|
|
49
|
+
return filterClientDisplayList(prepareChatMessagesForDisplay(dom));
|
|
50
|
+
}
|
|
51
|
+
const floor = archiveOrderFloor(archive, this.jsonlRowCount.get(agentId) ?? 0);
|
|
52
|
+
const stamped = stampOverlayAfterArchive(floor, dom);
|
|
53
|
+
return filterClientDisplayList(prepareDomOverlayForDisplay(stamped));
|
|
23
54
|
}
|
|
24
|
-
/** Reload JSONL;
|
|
55
|
+
/** Reload JSONL; drop viewport rows now covered by archive. */
|
|
25
56
|
setJsonlBaseline(agentId, rows) {
|
|
26
57
|
const prevRows = this.jsonlRowCount.get(agentId) ?? 0;
|
|
27
58
|
if (rows.length > 0 && prevRows > 24 && rows.length < prevRows - 12) {
|
|
@@ -41,6 +72,7 @@ export class ChatDisplayStore {
|
|
|
41
72
|
}
|
|
42
73
|
this.jsonlRowCount.set(agentId, rows.length);
|
|
43
74
|
this.jsonlBaseline.set(agentId, prepared);
|
|
75
|
+
this.reconcileDomViewportToArchive(agentId);
|
|
44
76
|
this.pruneDomOverlay(agentId);
|
|
45
77
|
}
|
|
46
78
|
/** Append JSONL rows during live stream; returns new/updated bubbles for socket `append` emit. */
|
|
@@ -53,6 +85,7 @@ export class ChatDisplayStore {
|
|
|
53
85
|
const prepared = mergeHistoryTail(prev, deltaChat);
|
|
54
86
|
this.jsonlRowCount.set(agentId, (this.jsonlRowCount.get(agentId) ?? 0) + deltaRows.length);
|
|
55
87
|
this.jsonlBaseline.set(agentId, prepared);
|
|
88
|
+
this.reconcileDomViewportToArchive(agentId);
|
|
56
89
|
this.pruneDomOverlay(agentId);
|
|
57
90
|
if (prepared.length > prevLen)
|
|
58
91
|
return prepared.slice(prevLen);
|
|
@@ -72,32 +105,74 @@ export class ChatDisplayStore {
|
|
|
72
105
|
return prepared;
|
|
73
106
|
return deltaChat.length ? deltaChat : [];
|
|
74
107
|
}
|
|
75
|
-
/** Ingest DOM extract; overlay
|
|
108
|
+
/** Ingest DOM extract; overlay = viewport minus JSONL-covered rows. */
|
|
76
109
|
mergeLiveForAgent(agentId, rawDom) {
|
|
77
110
|
const live = prepareChatMessagesForDisplay(rawDom);
|
|
78
|
-
if (live.length)
|
|
111
|
+
if (live.length) {
|
|
112
|
+
this.latestDomViewport.set(agentId, live);
|
|
79
113
|
this.domTranscript.ingest(agentId, live);
|
|
114
|
+
this.domTranscript.reconcileToViewport(agentId, live);
|
|
115
|
+
}
|
|
116
|
+
else {
|
|
117
|
+
this.latestDomViewport.delete(agentId);
|
|
118
|
+
this.domTranscript.reconcileToViewport(agentId, []);
|
|
119
|
+
}
|
|
120
|
+
this.reconcileDomViewportToArchive(agentId);
|
|
80
121
|
this.pruneDomOverlay(agentId);
|
|
81
122
|
}
|
|
82
|
-
/**
|
|
123
|
+
/** Canonical UI lenta: archive then live tail (append-only, no cross-sort). */
|
|
83
124
|
getDisplayMessages(agentId) {
|
|
84
125
|
return [...this.getJsonlHistory(agentId), ...this.getDomLive(agentId)];
|
|
85
126
|
}
|
|
86
|
-
|
|
87
|
-
|
|
127
|
+
/** Raw DOM ingest before JSONL filter / stamp (debug only). */
|
|
128
|
+
getDomTranscriptRaw(agentId) {
|
|
129
|
+
return this.domTranscript.list(agentId);
|
|
88
130
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
131
|
+
/** Snapshot for `/debug/lenta` and lenta:watch. */
|
|
132
|
+
getLentaDebug(agentId) {
|
|
133
|
+
const jsonlHistory = this.getJsonlHistory(agentId);
|
|
134
|
+
const domOverlay = this.getDomLive(agentId);
|
|
135
|
+
return {
|
|
136
|
+
jsonlRowCount: this.jsonlRowCount.get(agentId) ?? 0,
|
|
137
|
+
jsonlHistory,
|
|
138
|
+
domOverlay,
|
|
139
|
+
domRaw: this.getDomTranscriptRaw(agentId),
|
|
140
|
+
messages: this.getDisplayMessages(agentId),
|
|
141
|
+
source: domOverlay.length ? 'hybrid' : 'jsonl',
|
|
142
|
+
agentGenerating: this.isAgentGenerating(agentId),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
/** JSONL rows used to suppress DOM overlay (baseline + displayed archive). */
|
|
146
|
+
archiveForOverlayMatch(agentId) {
|
|
147
|
+
const baseline = this.jsonlBaseline.get(agentId) ?? [];
|
|
148
|
+
const displayed = this.getJsonlHistory(agentId);
|
|
149
|
+
const seen = new Set();
|
|
150
|
+
const out = [];
|
|
151
|
+
for (const m of [...baseline, ...displayed]) {
|
|
152
|
+
const key = `${m.role}:${m.id ?? ''}:${m.flatIndex ?? ''}:${(m.text ?? '').slice(0, 48)}`;
|
|
153
|
+
if (seen.has(key))
|
|
154
|
+
continue;
|
|
155
|
+
seen.add(key);
|
|
156
|
+
out.push(m);
|
|
157
|
+
}
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
/** Shrink cached viewport to rows still missing from JSONL (handoff DOM → JSONL). */
|
|
161
|
+
reconcileDomViewportToArchive(agentId) {
|
|
162
|
+
const dom = this.latestDomViewport.get(agentId);
|
|
163
|
+
if (!dom?.length)
|
|
164
|
+
return;
|
|
165
|
+
const archive = this.archiveForOverlayMatch(agentId);
|
|
166
|
+
const kept = selectDomOverlayFromViewport(dom, archive);
|
|
167
|
+
if (kept.length)
|
|
168
|
+
this.latestDomViewport.set(agentId, kept);
|
|
169
|
+
else
|
|
170
|
+
this.latestDomViewport.delete(agentId);
|
|
96
171
|
}
|
|
97
172
|
pruneDomOverlay(agentId) {
|
|
98
|
-
const
|
|
99
|
-
if (!
|
|
173
|
+
const archive = this.archiveForOverlayMatch(agentId);
|
|
174
|
+
if (!archive.length)
|
|
100
175
|
return;
|
|
101
|
-
this.domTranscript.pruneCoveredBy(agentId,
|
|
176
|
+
this.domTranscript.pruneCoveredBy(agentId, archive);
|
|
102
177
|
}
|
|
103
178
|
}
|
|
@@ -2,11 +2,47 @@ import type { ChatMessage, HistoryMessage } from './types.js';
|
|
|
2
2
|
export declare function compareUserText(text: string): string;
|
|
3
3
|
export declare function userTextsEquivalent(ta: string, tb: string): boolean;
|
|
4
4
|
export declare function userMessagesEquivalent(a: ChatMessage, b: ChatMessage): boolean;
|
|
5
|
+
/**
|
|
6
|
+
* JSONL `flatIndex` = file line number (~1..N); Cursor DOM = `data-flat-index` (often 1000+).
|
|
7
|
+
* When DOM is ahead of the archive tail, fuzzy cover hid live rows (debug: NEEDS_DOM_OVERLAY, overlay 0).
|
|
8
|
+
*/
|
|
9
|
+
export declare const JSONL_DOM_ORDER_GAP = 32;
|
|
10
|
+
export declare function maxArchiveOrderKey(archive: ChatMessage[]): number;
|
|
11
|
+
export declare function isDomOrderAheadOfJsonlArchive(overlay: ChatMessage, maxArchiveKey: number): boolean;
|
|
12
|
+
/** Exact / near-exact match only — no word-bag (safe when DOM timeline > JSONL tail). */
|
|
13
|
+
export declare function archiveCoversOverlayStrict(archive: ChatMessage, overlay: ChatMessage): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Viewport rows for the live tail: not yet represented in JSONL archive.
|
|
16
|
+
* Uses `archiveCoversOverlay` (strict / fuzzy / streaming) — same rule for idle and generating.
|
|
17
|
+
*/
|
|
18
|
+
export declare function selectDomOverlayFromViewport(viewport: ChatMessage[], archive: ChatMessage[]): ChatMessage[];
|
|
19
|
+
/** JSONL archive covers a DOM overlay row (DOM may be a truncated viewport). */
|
|
20
|
+
export declare function archiveCoversOverlay(archive: ChatMessage, overlay: ChatMessage, opts?: {
|
|
21
|
+
maxArchiveKey?: number;
|
|
22
|
+
}): boolean;
|
|
5
23
|
export declare function messagesEquivalent(a: ChatMessage, b: ChatMessage): boolean;
|
|
6
24
|
export declare function pickPreferredMessage(prev: ChatMessage, next: ChatMessage, preferNext: boolean): ChatMessage;
|
|
25
|
+
export declare function messageOrderKey(m: ChatMessage): number;
|
|
26
|
+
/** DOM flat-index vs JSONL line ts — see app `anchorDomOverlaySortKeys`. */
|
|
27
|
+
export declare function anchorDomOverlaySortKeys(history: ChatMessage[], live: ChatMessage[]): ChatMessage[];
|
|
7
28
|
export declare function sortMessagesChronologically(messages: ChatMessage[]): ChatMessage[];
|
|
8
29
|
export declare function prepareChatMessagesForDisplay(messages: ChatMessage[]): ChatMessage[];
|
|
9
30
|
export declare function mergeHistoryTail(prev: ChatMessage[], incoming: ChatMessage[]): ChatMessage[];
|
|
31
|
+
/** Document order inside one DOM extract — never Cursor viewport flatIndex alone. */
|
|
32
|
+
export declare function sortByDomDocumentOrder(messages: ChatMessage[]): ChatMessage[];
|
|
33
|
+
/** Force overlay keys above archive so any sort by flatIndex stays below JSONL tail. */
|
|
34
|
+
export declare function stampOverlayAfterArchive(archiveFloor: number, overlay: ChatMessage[]): ChatMessage[];
|
|
35
|
+
export declare function archiveOrderFloor(jsonlHistory: ChatMessage[], rawJsonlRows: number): number;
|
|
36
|
+
/** Dedupe/collapse overlay only; order = domSeq (then stamped flatIndex). */
|
|
37
|
+
export declare function prepareDomOverlayForDisplay(overlay: ChatMessage[]): ChatMessage[];
|
|
38
|
+
/**
|
|
39
|
+
* Lenta for app: JSONL archive first, DOM overlay append-only.
|
|
40
|
+
* Order = array order here; app must not re-sort by flatIndex across the join.
|
|
41
|
+
*/
|
|
42
|
+
export declare function composeDisplayLenta(jsonlHistory: ChatMessage[], domOverlay: ChatMessage[]): ChatMessage[];
|
|
43
|
+
/** Dev: warn if composed lenta inverts archive keys. */
|
|
44
|
+
export declare function assertMonotonicLenta(messages: ChatMessage[], label: string): void;
|
|
45
|
+
/** @deprecated Use `composeDisplayLenta` — kept for tests/tools. */
|
|
10
46
|
export declare function mergeDomWithHistory(history: ChatMessage[], live: ChatMessage[]): ChatMessage[];
|
|
11
47
|
export declare function historyRowsToChat(rows: HistoryMessage[]): ChatMessage[];
|
|
12
48
|
export declare function filterClientDisplayList(messages: ChatMessage[]): ChatMessage[];
|
|
@@ -49,6 +49,13 @@ export function userTextsEquivalent(ta, tb) {
|
|
|
49
49
|
if (short.length < 12)
|
|
50
50
|
return false;
|
|
51
51
|
if (long.includes(short)) {
|
|
52
|
+
// Новый user_query, цитирующий старый внутри длинного текста — не дедупить.
|
|
53
|
+
if (long.length > short.length * 1.2 &&
|
|
54
|
+
/<user_query>/i.test(long) &&
|
|
55
|
+
!long.startsWith(short) &&
|
|
56
|
+
!long.endsWith(short)) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
52
59
|
if (long.endsWith(short) || long.startsWith(short))
|
|
53
60
|
return true;
|
|
54
61
|
if (short.length / long.length >= 0.4)
|
|
@@ -118,6 +125,9 @@ function pickPreferredUserMessage(a, b, preferB) {
|
|
|
118
125
|
/** DOM textContent vs JSONL markdown — same turn, different surface form. */
|
|
119
126
|
function normalizeAssistantCompareText(text) {
|
|
120
127
|
return text
|
|
128
|
+
.replace(/[\u2013\u2014\u2212]/g, '-')
|
|
129
|
+
.replace(/\*\*/g, '')
|
|
130
|
+
.replace(/`+/g, '')
|
|
121
131
|
.replace(/^#+\s*/gm, '')
|
|
122
132
|
.replace(/\|/g, ' ')
|
|
123
133
|
.replace(/[-]{3,}/g, ' ')
|
|
@@ -125,6 +135,18 @@ function normalizeAssistantCompareText(text) {
|
|
|
125
135
|
.trim()
|
|
126
136
|
.toLowerCase();
|
|
127
137
|
}
|
|
138
|
+
function wordBagOverlapRatio(textA, textB) {
|
|
139
|
+
const wa = new Set((textA.match(/[\p{L}\p{N}]{3,}/gu) ?? []).map((w) => w.toLowerCase()));
|
|
140
|
+
const wb = new Set((textB.match(/[\p{L}\p{N}]{3,}/gu) ?? []).map((w) => w.toLowerCase()));
|
|
141
|
+
if (!wa.size || !wb.size)
|
|
142
|
+
return 0;
|
|
143
|
+
let shared = 0;
|
|
144
|
+
for (const w of wa) {
|
|
145
|
+
if (wb.has(w))
|
|
146
|
+
shared++;
|
|
147
|
+
}
|
|
148
|
+
return shared / Math.min(wa.size, wb.size);
|
|
149
|
+
}
|
|
128
150
|
function assistantTextsEquivalent(a, b) {
|
|
129
151
|
if (a.role !== 'assistant' || b.role !== 'assistant')
|
|
130
152
|
return false;
|
|
@@ -140,7 +162,167 @@ function assistantTextsEquivalent(a, b) {
|
|
|
140
162
|
return false;
|
|
141
163
|
if (!long.includes(short))
|
|
142
164
|
return false;
|
|
143
|
-
|
|
165
|
+
if (long.startsWith(short) && short.length >= 32)
|
|
166
|
+
return true;
|
|
167
|
+
return short.length / Math.min(long.length, 1200) >= 0.35;
|
|
168
|
+
}
|
|
169
|
+
/** Same idea as `/debug/chats` compare — DOM textContent vs JSONL markdown. */
|
|
170
|
+
function compactOverlayMatchText(text, role) {
|
|
171
|
+
const base = role === 'assistant' ? normalizeAssistantCompareText(text) : compareUserText(text);
|
|
172
|
+
return base.replace(/\s/g, '');
|
|
173
|
+
}
|
|
174
|
+
function overlaySurfaceText(m) {
|
|
175
|
+
const t = (m.text ?? '').trim();
|
|
176
|
+
if (t)
|
|
177
|
+
return t;
|
|
178
|
+
return (m.html ?? '').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim();
|
|
179
|
+
}
|
|
180
|
+
function compactTextsMatchArchiveOverlay(archive, overlay) {
|
|
181
|
+
const role = archive.role;
|
|
182
|
+
const ta = compactOverlayMatchText(overlaySurfaceText(archive), role);
|
|
183
|
+
const tb = compactOverlayMatchText(overlaySurfaceText(overlay), role);
|
|
184
|
+
if (!ta || !tb)
|
|
185
|
+
return false;
|
|
186
|
+
if (ta === tb)
|
|
187
|
+
return true;
|
|
188
|
+
const short = ta.length <= tb.length ? ta : tb;
|
|
189
|
+
const long = ta.length <= tb.length ? tb : ta;
|
|
190
|
+
if (short.length < 24)
|
|
191
|
+
return false;
|
|
192
|
+
if (long.startsWith(short))
|
|
193
|
+
return true;
|
|
194
|
+
if (long.includes(short) && short.length / Math.min(long.length, 1600) >= 0.28) {
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
function assistantArchiveCoversOverlay(archive, overlay) {
|
|
200
|
+
if (assistantTextsEquivalent(archive, overlay) || messagesEquivalent(archive, overlay)) {
|
|
201
|
+
return true;
|
|
202
|
+
}
|
|
203
|
+
if (compactTextsMatchArchiveOverlay(archive, overlay))
|
|
204
|
+
return true;
|
|
205
|
+
const ta = normalizeAssistantCompareText(overlaySurfaceText(archive));
|
|
206
|
+
const tb = normalizeAssistantCompareText(overlaySurfaceText(overlay));
|
|
207
|
+
if (!ta || !tb)
|
|
208
|
+
return false;
|
|
209
|
+
const head = 72;
|
|
210
|
+
if (ta.slice(0, head) === tb.slice(0, head) && head >= 40)
|
|
211
|
+
return true;
|
|
212
|
+
const short = ta.length <= tb.length ? ta : tb;
|
|
213
|
+
const long = ta.length <= tb.length ? tb : ta;
|
|
214
|
+
if (short.length >= 48 && long.includes(short))
|
|
215
|
+
return true;
|
|
216
|
+
// DOM textContent vs JSONL markdown: таблицы/тире дают 1–2 отличия на тысячи символов.
|
|
217
|
+
if (ta.length >= 120 && tb.length >= 120) {
|
|
218
|
+
if (wordBagOverlapRatio(ta, tb) >= 0.88)
|
|
219
|
+
return true;
|
|
220
|
+
if (wordBagOverlapRatio(ta.slice(0, 420), tb.slice(0, 420)) >= 0.92 &&
|
|
221
|
+
ta.slice(0, 56) === tb.slice(0, 56)) {
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* JSONL `flatIndex` = file line number (~1..N); Cursor DOM = `data-flat-index` (often 1000+).
|
|
229
|
+
* When DOM is ahead of the archive tail, fuzzy cover hid live rows (debug: NEEDS_DOM_OVERLAY, overlay 0).
|
|
230
|
+
*/
|
|
231
|
+
export const JSONL_DOM_ORDER_GAP = 32;
|
|
232
|
+
export function maxArchiveOrderKey(archive) {
|
|
233
|
+
let max = 0;
|
|
234
|
+
for (const m of archive)
|
|
235
|
+
max = Math.max(max, messageOrderKey(m));
|
|
236
|
+
return max;
|
|
237
|
+
}
|
|
238
|
+
function isCursorDomOverlayRow(overlay) {
|
|
239
|
+
const id = overlay.id ?? '';
|
|
240
|
+
if (id.startsWith('hist-'))
|
|
241
|
+
return false;
|
|
242
|
+
const flat = overlay.flatIndex;
|
|
243
|
+
return flat != null && Number.isFinite(flat) && flat >= 64;
|
|
244
|
+
}
|
|
245
|
+
export function isDomOrderAheadOfJsonlArchive(overlay, maxArchiveKey) {
|
|
246
|
+
if (!isCursorDomOverlayRow(overlay))
|
|
247
|
+
return false;
|
|
248
|
+
return (overlay.flatIndex ?? 0) > maxArchiveKey + JSONL_DOM_ORDER_GAP;
|
|
249
|
+
}
|
|
250
|
+
/** Exact / near-exact match only — no word-bag (safe when DOM timeline > JSONL tail). */
|
|
251
|
+
export function archiveCoversOverlayStrict(archive, overlay) {
|
|
252
|
+
if (archive.role !== overlay.role)
|
|
253
|
+
return false;
|
|
254
|
+
if (archive.role === 'user') {
|
|
255
|
+
if (compactTextsMatchArchiveOverlay(archive, overlay))
|
|
256
|
+
return true;
|
|
257
|
+
return userMessagesEquivalent(archive, overlay);
|
|
258
|
+
}
|
|
259
|
+
if (archive.role === 'assistant') {
|
|
260
|
+
return (assistantTextsEquivalent(archive, overlay) ||
|
|
261
|
+
messagesEquivalent(archive, overlay) ||
|
|
262
|
+
compactTextsMatchArchiveOverlay(archive, overlay));
|
|
263
|
+
}
|
|
264
|
+
return messagesEquivalent(archive, overlay);
|
|
265
|
+
}
|
|
266
|
+
/** Fuzzy match (markdown vs DOM textContent, word-bag, prefixes). */
|
|
267
|
+
function archiveCoversOverlayFuzzy(archive, overlay) {
|
|
268
|
+
if (archive.role !== overlay.role)
|
|
269
|
+
return false;
|
|
270
|
+
if (archive.role === 'user') {
|
|
271
|
+
if (compactTextsMatchArchiveOverlay(archive, overlay))
|
|
272
|
+
return true;
|
|
273
|
+
const a = compareUserText(overlaySurfaceText(archive));
|
|
274
|
+
const b = compareUserText(overlaySurfaceText(overlay));
|
|
275
|
+
if (!a || !b)
|
|
276
|
+
return false;
|
|
277
|
+
if (a === b)
|
|
278
|
+
return true;
|
|
279
|
+
const short = a.length <= b.length ? a : b;
|
|
280
|
+
const long = a.length <= b.length ? b : a;
|
|
281
|
+
return short.length >= 24 && long.includes(short);
|
|
282
|
+
}
|
|
283
|
+
if (archive.role === 'assistant') {
|
|
284
|
+
return assistantArchiveCoversOverlay(archive, overlay);
|
|
285
|
+
}
|
|
286
|
+
return messagesEquivalent(archive, overlay);
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* When DOM flat-index is ahead of JSONL line keys, allow fuzzy dedupe once JSONL caught up,
|
|
290
|
+
* but keep overlay if DOM body is still growing (streaming).
|
|
291
|
+
*/
|
|
292
|
+
const DOM_AHEAD_STREAM_MARGIN = { assistant: 48, user: 24 };
|
|
293
|
+
function overlayStillStreamingPastArchive(archive, overlay) {
|
|
294
|
+
const aLen = (archive.text ?? '').trim().length;
|
|
295
|
+
const oLen = (overlay.text ?? '').trim().length;
|
|
296
|
+
if (!aLen || !oLen)
|
|
297
|
+
return false;
|
|
298
|
+
const margin = overlay.role === 'user' ? DOM_AHEAD_STREAM_MARGIN.user : DOM_AHEAD_STREAM_MARGIN.assistant;
|
|
299
|
+
return oLen > aLen + margin;
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Viewport rows for the live tail: not yet represented in JSONL archive.
|
|
303
|
+
* Uses `archiveCoversOverlay` (strict / fuzzy / streaming) — same rule for idle and generating.
|
|
304
|
+
*/
|
|
305
|
+
export function selectDomOverlayFromViewport(viewport, archive) {
|
|
306
|
+
if (!viewport.length)
|
|
307
|
+
return [];
|
|
308
|
+
if (!archive.length)
|
|
309
|
+
return viewport;
|
|
310
|
+
const maxArchiveKey = maxArchiveOrderKey(archive);
|
|
311
|
+
return viewport.filter((d) => !archive.some((h) => archiveCoversOverlay(h, d, { maxArchiveKey })));
|
|
312
|
+
}
|
|
313
|
+
/** JSONL archive covers a DOM overlay row (DOM may be a truncated viewport). */
|
|
314
|
+
export function archiveCoversOverlay(archive, overlay, opts) {
|
|
315
|
+
const maxKey = opts?.maxArchiveKey;
|
|
316
|
+
if (maxKey != null && isDomOrderAheadOfJsonlArchive(overlay, maxKey)) {
|
|
317
|
+
if (archiveCoversOverlayStrict(archive, overlay))
|
|
318
|
+
return true;
|
|
319
|
+
if (!archiveCoversOverlayFuzzy(archive, overlay))
|
|
320
|
+
return false;
|
|
321
|
+
if (overlayStillStreamingPastArchive(archive, overlay))
|
|
322
|
+
return false;
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
return archiveCoversOverlayFuzzy(archive, overlay);
|
|
144
326
|
}
|
|
145
327
|
function messageKey(m) {
|
|
146
328
|
const n = normalizeMessage(m);
|
|
@@ -195,16 +377,30 @@ export function pickPreferredMessage(prev, next, preferNext) {
|
|
|
195
377
|
pick = preferNext ? next : prev;
|
|
196
378
|
return mergeTimelineFields(prev, next, pick);
|
|
197
379
|
}
|
|
198
|
-
function messageOrderKey(m) {
|
|
380
|
+
export function messageOrderKey(m) {
|
|
199
381
|
if (m.flatIndex != null && Number.isFinite(m.flatIndex))
|
|
200
382
|
return m.flatIndex;
|
|
201
383
|
if (m.domSeq != null && Number.isFinite(m.domSeq))
|
|
202
384
|
return m.domSeq;
|
|
203
|
-
const hist = /^hist-(\d+)/.exec(m.id);
|
|
385
|
+
const hist = /^hist-(\d+)/.exec(m.id ?? '');
|
|
204
386
|
if (hist)
|
|
205
387
|
return Number(hist[1]);
|
|
206
388
|
return 0;
|
|
207
389
|
}
|
|
390
|
+
/** DOM flat-index vs JSONL line ts — see app `anchorDomOverlaySortKeys`. */
|
|
391
|
+
export function anchorDomOverlaySortKeys(history, live) {
|
|
392
|
+
if (!live.length)
|
|
393
|
+
return live;
|
|
394
|
+
let maxKey = -1;
|
|
395
|
+
for (const m of history) {
|
|
396
|
+
maxKey = Math.max(maxKey, messageOrderKey(m));
|
|
397
|
+
}
|
|
398
|
+
const sortedLive = sortMessagesChronologically(live);
|
|
399
|
+
return sortedLive.map((m, i) => ({
|
|
400
|
+
...m,
|
|
401
|
+
flatIndex: maxKey + 1 + i,
|
|
402
|
+
}));
|
|
403
|
+
}
|
|
208
404
|
export function sortMessagesChronologically(messages) {
|
|
209
405
|
return [...messages].sort((a, b) => {
|
|
210
406
|
const ka = messageOrderKey(a);
|
|
@@ -331,6 +527,25 @@ function collapseAssistantBursts(messages) {
|
|
|
331
527
|
export function prepareChatMessagesForDisplay(messages) {
|
|
332
528
|
return collapseAssistantBursts(dedupeUserMessages(fixAssistantFragmentSandwich(messages)));
|
|
333
529
|
}
|
|
530
|
+
/** Tail overlap for JSONL incremental merge — user rows only on exact text (duplicate line in file). */
|
|
531
|
+
function tailOverlapForMerge(prev, incoming) {
|
|
532
|
+
const max = Math.min(prev.length, incoming.length, 40);
|
|
533
|
+
let overlap = 0;
|
|
534
|
+
for (let n = 1; n <= max; n++) {
|
|
535
|
+
const a = prev.slice(-n);
|
|
536
|
+
const b = incoming.slice(0, n);
|
|
537
|
+
if (a.every((m, i) => {
|
|
538
|
+
const o = b[i];
|
|
539
|
+
if (m.role === 'user' && o.role === 'user') {
|
|
540
|
+
return compareUserText(m.text ?? '') === compareUserText(o.text ?? '');
|
|
541
|
+
}
|
|
542
|
+
return messagesEquivalent(m, o);
|
|
543
|
+
})) {
|
|
544
|
+
overlap = n;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
return overlap;
|
|
548
|
+
}
|
|
334
549
|
function tailOverlap(history, live) {
|
|
335
550
|
const max = Math.min(history.length, live.length, 40);
|
|
336
551
|
let overlap = 0;
|
|
@@ -342,6 +557,13 @@ function tailOverlap(history, live) {
|
|
|
342
557
|
}
|
|
343
558
|
return overlap;
|
|
344
559
|
}
|
|
560
|
+
function mergeOverlappingBoundary(prev, incoming, overlap) {
|
|
561
|
+
let picked = prev[prev.length - overlap];
|
|
562
|
+
for (let i = 0; i < overlap; i++) {
|
|
563
|
+
picked = pickPreferredMessage(picked, incoming[i], true);
|
|
564
|
+
}
|
|
565
|
+
return picked;
|
|
566
|
+
}
|
|
345
567
|
function upsertLiveMessage(merged, m, live, overlap, preferNext) {
|
|
346
568
|
const tailWindow = Math.max(live.length - overlap, 0) + 6;
|
|
347
569
|
const searchFrom = Math.max(0, merged.length - tailWindow);
|
|
@@ -364,35 +586,76 @@ export function mergeHistoryTail(prev, incoming) {
|
|
|
364
586
|
return prepareChatMessagesForDisplay(incoming);
|
|
365
587
|
const prevN = normalizeAll(prev);
|
|
366
588
|
const inc = normalizeAll(incoming);
|
|
367
|
-
|
|
368
|
-
const maxOverlap = Math.min(prevN.length, inc.length, 40);
|
|
369
|
-
for (let n = 1; n <= maxOverlap; n++) {
|
|
370
|
-
const a = prevN.slice(-n);
|
|
371
|
-
const b = inc.slice(0, n);
|
|
372
|
-
if (a.every((m, i) => messagesEquivalent(m, b[i])))
|
|
373
|
-
overlap = n;
|
|
374
|
-
}
|
|
589
|
+
const overlap = tailOverlapForMerge(prevN, inc);
|
|
375
590
|
const prefix = prevN.slice(0, Math.max(0, prevN.length - overlap));
|
|
376
591
|
const merged = [...prefix];
|
|
592
|
+
if (overlap > 0) {
|
|
593
|
+
upsertLiveMessage(merged, mergeOverlappingBoundary(prevN, inc, overlap), inc, overlap, true);
|
|
594
|
+
}
|
|
377
595
|
for (const m of inc.slice(overlap)) {
|
|
378
596
|
upsertLiveMessage(merged, m, inc, overlap, true);
|
|
379
597
|
}
|
|
380
598
|
return prepareChatMessagesForDisplay(merged);
|
|
381
599
|
}
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
600
|
+
/** Document order inside one DOM extract — never Cursor viewport flatIndex alone. */
|
|
601
|
+
export function sortByDomDocumentOrder(messages) {
|
|
602
|
+
return [...messages].sort((a, b) => {
|
|
603
|
+
const da = a.domSeq ?? 0;
|
|
604
|
+
const db = b.domSeq ?? 0;
|
|
605
|
+
if (da !== db)
|
|
606
|
+
return da - db;
|
|
607
|
+
return (a.flatIndex ?? 0) - (b.flatIndex ?? 0);
|
|
608
|
+
});
|
|
609
|
+
}
|
|
610
|
+
/** Force overlay keys above archive so any sort by flatIndex stays below JSONL tail. */
|
|
611
|
+
export function stampOverlayAfterArchive(archiveFloor, overlay) {
|
|
612
|
+
let seq = Math.max(archiveFloor, 0);
|
|
613
|
+
return overlay.map((m) => {
|
|
614
|
+
seq += 1;
|
|
615
|
+
return { ...m, flatIndex: seq };
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
export function archiveOrderFloor(jsonlHistory, rawJsonlRows) {
|
|
619
|
+
let floor = Math.max(rawJsonlRows, 0);
|
|
620
|
+
for (const m of jsonlHistory) {
|
|
621
|
+
floor = Math.max(floor, messageOrderKey(m));
|
|
394
622
|
}
|
|
395
|
-
return
|
|
623
|
+
return floor;
|
|
624
|
+
}
|
|
625
|
+
/** Dedupe/collapse overlay only; order = domSeq (then stamped flatIndex). */
|
|
626
|
+
export function prepareDomOverlayForDisplay(overlay) {
|
|
627
|
+
if (!overlay.length)
|
|
628
|
+
return [];
|
|
629
|
+
return collapseAssistantBursts(dedupeUserMessages(sortByDomDocumentOrder(overlay)));
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Lenta for app: JSONL archive first, DOM overlay append-only.
|
|
633
|
+
* Order = array order here; app must not re-sort by flatIndex across the join.
|
|
634
|
+
*/
|
|
635
|
+
export function composeDisplayLenta(jsonlHistory, domOverlay) {
|
|
636
|
+
if (!domOverlay.length)
|
|
637
|
+
return [...jsonlHistory];
|
|
638
|
+
if (!jsonlHistory.length)
|
|
639
|
+
return [...domOverlay];
|
|
640
|
+
return [...jsonlHistory, ...domOverlay];
|
|
641
|
+
}
|
|
642
|
+
/** Dev: warn if composed lenta inverts archive keys. */
|
|
643
|
+
export function assertMonotonicLenta(messages, label) {
|
|
644
|
+
if (process.env.LENTA_ORDER_ASSERT !== '1')
|
|
645
|
+
return;
|
|
646
|
+
for (let i = 1; i < messages.length; i++) {
|
|
647
|
+
const prev = messageOrderKey(messages[i - 1]);
|
|
648
|
+
const cur = messageOrderKey(messages[i]);
|
|
649
|
+
if (cur < prev) {
|
|
650
|
+
console.warn(`[lenta-order] inversion ${label} i=${i} ${prev}>${cur}`);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
/** @deprecated Use `composeDisplayLenta` — kept for tests/tools. */
|
|
655
|
+
export function mergeDomWithHistory(history, live) {
|
|
656
|
+
const hist = prepareChatMessagesForDisplay(dedupeUserMessages(history));
|
|
657
|
+
const collapsedLive = prepareChatMessagesForDisplay(collapseAssistantBursts(live));
|
|
658
|
+
return composeDisplayLenta(hist, collapsedLive);
|
|
396
659
|
}
|
|
397
660
|
export function historyRowsToChat(rows) {
|
|
398
661
|
return rows.map((m, i) => {
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { CursorState } from './types.js';
|
|
2
2
|
export declare function normalizeAgentTitle(title: string): string;
|
|
3
|
-
export declare function isComposerUuid(id: string | undefined):
|
|
3
|
+
export declare function isComposerUuid(id: string | undefined): id is string;
|
|
4
4
|
export declare function resolveCursorActiveComposerId(state: CursorState): string | undefined;
|
|
5
|
+
/** Cursor is generating for this composer (active tab or sidebar spinner). */
|
|
6
|
+
export declare function isAgentGenerating(agentId: string, state: CursorState): boolean;
|
|
5
7
|
/** Live DOM applies only when the subscribed app chat matches Cursor's active composer. */
|
|
6
8
|
export declare function isChatSyncedWithCursor(selectedAgentId: string | undefined, selectedTitle: string | undefined, state: Pick<CursorState, 'activeComposerId' | 'tabs' | 'composerIdByTitle' | 'activeChatTitle'>): boolean;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isPassiveBackgroundShellState } from './message-filter.js';
|
|
1
2
|
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
2
3
|
export function normalizeAgentTitle(title) {
|
|
3
4
|
return title
|
|
@@ -31,6 +32,25 @@ export function resolveCursorActiveComposerId(state) {
|
|
|
31
32
|
}
|
|
32
33
|
return activeTab?.composerId || activeTab?.id || state.activeComposerId;
|
|
33
34
|
}
|
|
35
|
+
/** Cursor is generating for this composer (active tab or sidebar spinner). */
|
|
36
|
+
export function isAgentGenerating(agentId, state) {
|
|
37
|
+
if (!agentId)
|
|
38
|
+
return false;
|
|
39
|
+
const activeId = resolveCursorActiveComposerId(state);
|
|
40
|
+
if (activeId === agentId && isPassiveBackgroundShellState(state))
|
|
41
|
+
return false;
|
|
42
|
+
if (activeId === agentId && state.agentWorking)
|
|
43
|
+
return true;
|
|
44
|
+
for (const tab of state.tabs) {
|
|
45
|
+
const id = tab.composerId ?? tab.id;
|
|
46
|
+
if (id !== agentId || !tab.isWorking)
|
|
47
|
+
continue;
|
|
48
|
+
if (activeId === agentId && isPassiveBackgroundShellState(state))
|
|
49
|
+
continue;
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
34
54
|
function isSyntheticRouteId(agentId) {
|
|
35
55
|
return /^sidebar-\d+$/.test(agentId) || agentId.startsWith('title:');
|
|
36
56
|
}
|