cursorconnect 0.1.6 → 0.1.8

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 (55) hide show
  1. package/bridge-runtime/.env.example +14 -2
  2. package/bridge-runtime/connector-version.json +1 -1
  3. package/bridge-runtime/dist/agent-completion-push.d.ts +35 -0
  4. package/bridge-runtime/dist/agent-completion-push.js +195 -0
  5. package/bridge-runtime/dist/agent-title-match.d.ts +8 -7
  6. package/bridge-runtime/dist/agent-title-match.js +11 -1
  7. package/bridge-runtime/dist/chat-display-store.d.ts +21 -9
  8. package/bridge-runtime/dist/chat-display-store.js +97 -23
  9. package/bridge-runtime/dist/chat-display.d.ts +2 -0
  10. package/bridge-runtime/dist/chat-display.js +197 -33
  11. package/bridge-runtime/dist/chat-history-mode.d.ts +5 -0
  12. package/bridge-runtime/dist/chat-history-mode.js +7 -0
  13. package/bridge-runtime/dist/command-executor.d.ts +2 -0
  14. package/bridge-runtime/dist/command-executor.js +44 -0
  15. package/bridge-runtime/dist/composer-title-index.d.ts +1 -0
  16. package/bridge-runtime/dist/composer-title-index.js +7 -7
  17. package/bridge-runtime/dist/config.js +2 -0
  18. package/bridge-runtime/dist/connector-client-version.js +1 -1
  19. package/bridge-runtime/dist/debug-chats-page.d.ts +2 -0
  20. package/bridge-runtime/dist/debug-chats-page.js +491 -0
  21. package/bridge-runtime/dist/dom-transcript-store.d.ts +17 -0
  22. package/bridge-runtime/dist/dom-transcript-store.js +76 -0
  23. package/bridge-runtime/dist/extract-page.js +56 -85
  24. package/bridge-runtime/dist/history-limit.d.ts +2 -0
  25. package/bridge-runtime/dist/history-limit.js +2 -0
  26. package/bridge-runtime/dist/history-request.d.ts +8 -0
  27. package/bridge-runtime/dist/history-request.js +7 -0
  28. package/bridge-runtime/dist/index.js +10 -0
  29. package/bridge-runtime/dist/jsonl-index.d.ts +21 -3
  30. package/bridge-runtime/dist/jsonl-index.js +237 -73
  31. package/bridge-runtime/dist/jsonl-live-debug.d.ts +24 -0
  32. package/bridge-runtime/dist/jsonl-live-debug.js +175 -0
  33. package/bridge-runtime/dist/keep-awake.d.ts +5 -0
  34. package/bridge-runtime/dist/keep-awake.js +48 -0
  35. package/bridge-runtime/dist/media-path.d.ts +2 -0
  36. package/bridge-runtime/dist/media-path.js +17 -0
  37. package/bridge-runtime/dist/message-filter.d.ts +2 -0
  38. package/bridge-runtime/dist/message-filter.js +21 -5
  39. package/bridge-runtime/dist/pairing-code.d.ts +2 -0
  40. package/bridge-runtime/dist/pairing-code.js +9 -2
  41. package/bridge-runtime/dist/relay-upstream.d.ts +5 -1
  42. package/bridge-runtime/dist/relay-upstream.js +25 -1
  43. package/bridge-runtime/dist/relay.d.ts +31 -0
  44. package/bridge-runtime/dist/relay.js +401 -32
  45. package/bridge-runtime/dist/types.d.ts +25 -0
  46. package/bridge-runtime/selectors.json +4 -5
  47. package/dist/index.js +79 -20
  48. package/dist/launch.js +23 -5
  49. package/dist/macos-autostart.js +87 -0
  50. package/dist/pairing-code.js +12 -3
  51. package/dist/print-pairing.js +2 -0
  52. package/dist/run-service.js +31 -0
  53. package/dist/startup-check.js +165 -0
  54. package/package.json +1 -1
  55. package/version-policy.json +2 -2
@@ -4,14 +4,24 @@ import { randomBytes, timingSafeEqual } from 'crypto';
4
4
  import { basename } from 'path';
5
5
  import { readAllowedMediaFile, resolveMediaPathParam } from './media-path.js';
6
6
  import { Server as SocketServer } from 'socket.io';
7
+ import { resolveJsonlFilePath } from './agent-title-match.js';
7
8
  import { ChatDisplayStore } from './chat-display-store.js';
9
+ import { resolveHistoryLimit } from './history-request.js';
8
10
  import { filterClientDisplayList, prepareChatMessagesForDisplay, } from './chat-display.js';
9
- import { isChatSyncedWithCursor } from './chat-sync.js';
11
+ import { isKeepAwakeActive } from './keep-awake.js';
12
+ function sleepMs(ms) {
13
+ return new Promise((resolve) => setTimeout(resolve, ms));
14
+ }
10
15
  import { mergeSidebarWithJsonl } from './sidebar-merge.js';
11
16
  import { transcribeAudioBuffer } from './openai-transcribe.js';
12
17
  import { extensionForMime, saveUploadedImage } from './image-upload-store.js';
18
+ import { AgentCompletionPush, suppressAgentCompletionPush } from './agent-completion-push.js';
13
19
  import { RelayUpstream } from './relay-upstream.js';
14
20
  import { bridgePipelineLog, bridgePipelineReportLines, bridgePipelineSnapshot, } from './history-pipeline-log.js';
21
+ import { DEBUG_CHATS_PAGE_HTML } from './debug-chats-page.js';
22
+ import { isChatHistoryFromJsonl } from './chat-history-mode.js';
23
+ import { isChatSyncedWithCursor } from './chat-sync.js';
24
+ import { readJsonlLiveSnapshot } from './jsonl-live-debug.js';
15
25
  export class Relay {
16
26
  stateManager;
17
27
  commandExecutor;
@@ -30,7 +40,16 @@ export class Relay {
30
40
  io;
31
41
  tokens = new Set();
32
42
  upstream = null;
43
+ agentCompletionPush = null;
44
+ pendingPushPayloads = [];
33
45
  chatDisplay = new ChatDisplayStore();
46
+ /** Raw JSONL row count last sent per agent (live `append` emits). */
47
+ lastEmittedJsonlRows = new Map();
48
+ /** Last display tail pushed to app — re-emit when user/assistant tail changes. */
49
+ lastEmittedLentaSig = new Map();
50
+ /** Display bubble count last sent — block regression 214→74 style wipes on phone. */
51
+ lastEmittedHistLen = new Map();
52
+ domOverlayTimer = null;
34
53
  constructor(config, stateManager, commandExecutor, cdpBridge, jsonlIndex, messageDebugStore, domExtractor) {
35
54
  this.stateManager = stateManager;
36
55
  this.commandExecutor = commandExecutor;
@@ -53,11 +72,35 @@ export class Relay {
53
72
  else {
54
73
  this.upstream = new RelayUpstream(this.config, (event, ...args) => {
55
74
  this.handleRemoteClientEvent(event, ...args);
56
- });
75
+ }, () => this.flushPendingPushPayloads());
76
+ this.agentCompletionPush = new AgentCompletionPush((payload) => this.emitAgentCompletedPush(payload), () => this.lastJsonlIndex);
57
77
  this.upstream.connect();
58
78
  }
59
79
  }
60
80
  }
81
+ emitAgentCompletedPush(payload) {
82
+ if (this.upstream?.connected) {
83
+ this.upstream.emit('push:agent-completed', payload);
84
+ return;
85
+ }
86
+ this.pendingPushPayloads.push(payload);
87
+ console.warn(`[agent-completion-push] queued for relay (${this.pendingPushPayloads.length}) upstream disconnected`);
88
+ }
89
+ flushPendingPushPayloads() {
90
+ if (!this.upstream?.connected || !this.pendingPushPayloads.length)
91
+ return;
92
+ const byAgent = new Map();
93
+ for (const payload of this.pendingPushPayloads.splice(0)) {
94
+ byAgent.set(payload.agentId, payload);
95
+ }
96
+ for (const payload of byAgent.values()) {
97
+ this.upstream.emit('push:agent-completed', payload);
98
+ }
99
+ console.log(`[agent-completion-push] flushed ${byAgent.size} queued push(es) to relay`);
100
+ }
101
+ observeAgentCompletionForPush() {
102
+ this.agentCompletionPush?.observe(this.stateManager.getState());
103
+ }
61
104
  listen() {
62
105
  return new Promise((resolve) => {
63
106
  this.httpServer.listen(this.config.serverPort, this.config.serverHost, () => {
@@ -69,6 +112,157 @@ export class Relay {
69
112
  get authEnabled() {
70
113
  return this.config.webappPassword.length > 0;
71
114
  }
115
+ /** Read-only view of in-memory bridge state (no writes to stores). */
116
+ readOnlyChatSnapshot() {
117
+ const state = this.stateManager.getState();
118
+ const subscribed = [...this.jsonlIndex.getSubscribedAgents().entries()].map(([agentId, meta]) => ({ agentId, title: meta.title }));
119
+ const displayCache = {};
120
+ const cacheIds = new Set([
121
+ ...subscribed.map((s) => s.agentId),
122
+ ...(state.activeComposerId ? [state.activeComposerId] : []),
123
+ ]);
124
+ const domTranscript = {};
125
+ for (const agentId of cacheIds) {
126
+ const dom = this.chatDisplay.getDomTranscript(agentId);
127
+ if (dom.length)
128
+ domTranscript[agentId] = dom;
129
+ const jsonl = this.chatDisplay.getJsonlHistory(agentId);
130
+ if (jsonl.length)
131
+ displayCache[agentId] = jsonl;
132
+ }
133
+ return {
134
+ at: Date.now(),
135
+ health: { cdp: this.cdpBridge.getClient()?.isConnected() ?? false },
136
+ cursor: {
137
+ activeComposerId: state.activeComposerId,
138
+ activeChatTitle: state.activeChatTitle,
139
+ updatedAt: state.updatedAt,
140
+ lastError: state.lastError,
141
+ tabs: state.tabs,
142
+ composerIdByTitle: state.composerIdByTitle,
143
+ domMessageCount: state.messages.length,
144
+ domMessages: state.messages,
145
+ },
146
+ domExtractDebug: {
147
+ latest: this.messageDebugStore.latest(),
148
+ ringSize: this.messageDebugStore.list().length,
149
+ },
150
+ subscribed,
151
+ displayCache,
152
+ chatHistoryJsonl: isChatHistoryFromJsonl(),
153
+ domTranscript,
154
+ };
155
+ }
156
+ /** Push JSONL file updates to every subscribed route id (e.g. sidebar-0) for that composer. */
157
+ syncJsonlToSubscribedAgents(fileAgentId, messages, totalMessages) {
158
+ const state = this.stateManager.getState();
159
+ const activeTab = state.tabs.find((t) => t.active);
160
+ const targets = new Set([fileAgentId]);
161
+ for (const [subId, meta] of this.jsonlIndex.getSubscribedAgents()) {
162
+ const path = resolveJsonlFilePath(this.config.cursorProjectsDir, subId, {
163
+ title: meta.title,
164
+ composerIdByTitle: state.composerIdByTitle,
165
+ activeComposerId: state.activeComposerId,
166
+ activeTabTitle: activeTab?.title,
167
+ });
168
+ if (path && basename(path, '.jsonl') === fileAgentId)
169
+ targets.add(subId);
170
+ }
171
+ for (const agentId of targets) {
172
+ this.emitJsonlLiveForAgent(agentId, messages, totalMessages);
173
+ }
174
+ }
175
+ /** JSONL baseline + DOM rows not yet in file (`liveMessages`). */
176
+ lentaSnapshot(agentId, totalMessages) {
177
+ const historyMessages = this.chatDisplay.getJsonlHistory(agentId);
178
+ const liveMessages = this.chatDisplay.getDomLive(agentId);
179
+ const source = liveMessages.length ? 'hybrid' : 'jsonl';
180
+ return {
181
+ historyMessages,
182
+ liveMessages,
183
+ messages: [...historyMessages, ...liveMessages],
184
+ totalMessages: totalMessages ?? historyMessages.length,
185
+ source: source,
186
+ };
187
+ }
188
+ emitLentaForAgent(agentId, opts) {
189
+ const snap = this.lentaSnapshot(agentId, opts.totalMessages);
190
+ const historyOut = opts.historyPayload ?? snap.historyMessages;
191
+ if (!historyOut.length && !snap.liveMessages.length)
192
+ return;
193
+ this.emitAgentMessages(agentId, historyOut, snap.liveMessages, snap.source, opts.totalMessages, opts.seq, opts.append);
194
+ }
195
+ lentaTailSignature(messages) {
196
+ return messages
197
+ .slice(-4)
198
+ .map((m) => `${m.role}:${m.id ?? ''}:${(m.text ?? '').length}:${(m.text ?? '').slice(-48)}`)
199
+ .join('|');
200
+ }
201
+ /** Live JSONL → `agent:messages` for subscribed chats (phone lenta). */
202
+ emitJsonlLiveForAgent(agentId, rows, totalMessages) {
203
+ const prevTotal = this.lastEmittedJsonlRows.get(agentId) ?? 0;
204
+ const prevSig = this.lastEmittedLentaSig.get(agentId);
205
+ this.chatDisplay.setJsonlBaseline(agentId, rows);
206
+ this.lastEmittedJsonlRows.set(agentId, totalMessages);
207
+ const historyMessages = this.chatDisplay.getJsonlHistory(agentId);
208
+ if (!historyMessages.length && !this.chatDisplay.getDomLive(agentId).length)
209
+ return;
210
+ if (!this.jsonlIndex.getSubscribedAgents().has(agentId))
211
+ return;
212
+ const sig = this.lentaTailSignature(historyMessages);
213
+ const totalGrew = totalMessages > prevTotal;
214
+ if (!totalGrew && sig === prevSig && prevSig)
215
+ return;
216
+ const histLen = historyMessages.length;
217
+ const prevHistLen = this.lastEmittedHistLen.get(agentId) ?? 0;
218
+ if (prevHistLen > 24 && histLen < prevHistLen - 12) {
219
+ bridgePipelineLog({
220
+ dir: 'internal',
221
+ event: 'agent:messages:SKIP_REGRESSION',
222
+ agentId,
223
+ msgs: histLen,
224
+ detail: `prev=${prevHistLen} total=${totalMessages}`,
225
+ });
226
+ return;
227
+ }
228
+ this.lastEmittedLentaSig.set(agentId, sig);
229
+ this.lastEmittedHistLen.set(agentId, Math.max(prevHistLen, histLen));
230
+ this.emitLentaForAgent(agentId, {
231
+ totalMessages,
232
+ seq: this.nextHistorySeq(agentId),
233
+ });
234
+ }
235
+ scheduleDomOverlayEmit() {
236
+ if (this.domOverlayTimer)
237
+ clearTimeout(this.domOverlayTimer);
238
+ this.domOverlayTimer = setTimeout(() => {
239
+ this.domOverlayTimer = null;
240
+ this.emitDomOverlayForSyncedSubscribers();
241
+ }, 120);
242
+ }
243
+ /** DOM poll while agent works — overlay until the same turn lands in JSONL. */
244
+ emitDomOverlayForSyncedSubscribers() {
245
+ const state = this.stateManager.getState();
246
+ if (!state.messages?.length)
247
+ return;
248
+ const domRows = this.prepareStateMessages(state.messages);
249
+ for (const [subId, meta] of this.jsonlIndex.getSubscribedAgents()) {
250
+ if (!isChatSyncedWithCursor(subId, meta.title, state))
251
+ continue;
252
+ this.chatDisplay.mergeLiveForAgent(subId, domRows);
253
+ const snap = this.lentaSnapshot(subId, this.lastEmittedJsonlRows.get(subId) ?? undefined);
254
+ if (!snap.liveMessages.length && !state.agentWorking)
255
+ continue;
256
+ this.lastEmittedLentaSig.delete(subId);
257
+ this.emitLentaForAgent(subId, {
258
+ totalMessages: snap.totalMessages,
259
+ seq: this.nextHistorySeq(subId),
260
+ });
261
+ }
262
+ }
263
+ agentMessagesSnapshot(agentId, totalMessages) {
264
+ return this.lentaSnapshot(agentId, totalMessages);
265
+ }
72
266
  checkMediaAuth(req) {
73
267
  if (!this.authEnabled)
74
268
  return true;
@@ -80,7 +274,14 @@ export class Relay {
80
274
  }
81
275
  setupHttp() {
82
276
  this.app.get('/health', (_req, res) => {
83
- res.json({ ok: true, cdp: this.cdpBridge.getClient()?.isConnected() ?? false });
277
+ res.json({
278
+ ok: true,
279
+ cdp: this.cdpBridge.getClient()?.isConnected() ?? false,
280
+ keepAwake: isKeepAwakeActive(),
281
+ debugChats: '/debug/chats',
282
+ debugSnapshot: '/debug/chat-snapshot',
283
+ debugJsonlLive: '/debug/jsonl-live',
284
+ });
84
285
  });
85
286
  this.app.get('/debug/messages', (_req, res) => {
86
287
  const state = this.stateManager.getState();
@@ -92,6 +293,62 @@ export class Relay {
92
293
  lastError: state.lastError,
93
294
  });
94
295
  });
296
+ this.app.get('/debug/chats', (_req, res) => {
297
+ res.type('html').send(DEBUG_CHATS_PAGE_HTML);
298
+ });
299
+ this.app.get('/debug/chat-snapshot', (_req, res) => {
300
+ res.json(this.readOnlyChatSnapshot());
301
+ });
302
+ this.app.get('/debug/jsonl-live', async (req, res) => {
303
+ if (!this.checkMediaAuth(req)) {
304
+ res.status(401).json({ error: 'Unauthorized' });
305
+ return;
306
+ }
307
+ const agentId = String(req.query.agentId ?? '').trim();
308
+ if (!agentId) {
309
+ res.status(400).json({ error: 'agentId required' });
310
+ return;
311
+ }
312
+ const title = typeof req.query.title === 'string' ? req.query.title.trim() : undefined;
313
+ const afterRaw = Number(req.query.afterLine ?? 0);
314
+ const tailRaw = Number(req.query.tail ?? 0);
315
+ const afterLine = Number.isFinite(afterRaw) && afterRaw > 0 ? Math.floor(afterRaw) : 0;
316
+ const tail = afterLine > 0
317
+ ? 0
318
+ : Number.isFinite(tailRaw) && tailRaw > 0
319
+ ? Math.min(500, Math.floor(tailRaw))
320
+ : 80;
321
+ const state = this.stateManager.getState();
322
+ const activeTab = state.tabs.find((t) => t.active);
323
+ const filePath = resolveJsonlFilePath(this.config.cursorProjectsDir, agentId, {
324
+ title,
325
+ composerIdByTitle: state.composerIdByTitle,
326
+ activeComposerId: state.activeComposerId,
327
+ activeTabTitle: activeTab?.title,
328
+ });
329
+ if (!filePath) {
330
+ res.json({
331
+ agentId,
332
+ filePath: null,
333
+ fileSize: 0,
334
+ totalLines: 0,
335
+ updatedAt: Date.now(),
336
+ rows: [],
337
+ });
338
+ return;
339
+ }
340
+ try {
341
+ const snap = await readJsonlLiveSnapshot(filePath, agentId, {
342
+ afterLine,
343
+ tail,
344
+ maxNew: 96,
345
+ });
346
+ res.json(snap);
347
+ }
348
+ catch (err) {
349
+ res.status(500).json({ error: err.message });
350
+ }
351
+ });
95
352
  this.app.get('/api/agents/index', async (req, res) => {
96
353
  if (!this.checkMediaAuth(req)) {
97
354
  res.status(401).json({ error: 'Unauthorized' });
@@ -125,7 +382,13 @@ export class Relay {
125
382
  const agentId = String(req.query.agentId ?? '').trim();
126
383
  const title = typeof req.query.title === 'string' ? req.query.title.trim() : undefined;
127
384
  const limitRaw = Number(req.query.limit ?? 0);
128
- const limit = Number.isFinite(limitRaw) && limitRaw > 0 ? limitRaw : undefined;
385
+ const minRaw = Number(req.query.minMessages ?? 0);
386
+ const limit = resolveHistoryLimit({
387
+ limit: Number.isFinite(limitRaw) && limitRaw > 0 ? limitRaw : undefined,
388
+ minMessages: Number.isFinite(minRaw) && minRaw > 0 ? minRaw : undefined,
389
+ });
390
+ const offsetRaw = Number(req.query.offset ?? 0);
391
+ const offset = Number.isFinite(offsetRaw) && offsetRaw > 0 ? offsetRaw : undefined;
129
392
  const requestId = typeof req.query.requestId === 'string' ? req.query.requestId.trim() : undefined;
130
393
  if (!agentId) {
131
394
  res.status(400).json({ error: 'agentId required' });
@@ -139,13 +402,26 @@ export class Relay {
139
402
  detail: `limit=${limit ?? 'all'} title=${title?.slice(0, 32) ?? '-'}`,
140
403
  });
141
404
  try {
142
- const history = await this.jsonlIndex.loadHistory(agentId, {
405
+ const full = await this.jsonlIndex.loadHistory(agentId, {
143
406
  title,
144
407
  composerIdByTitle: this.stateManager.getState().composerIdByTitle,
145
- limit,
408
+ offset,
146
409
  });
147
- const messages = this.chatDisplay.applyHistory(agentId, history.messages);
148
- const body = { ...history, messages, requestId, updatedAt: Date.now() };
410
+ this.chatDisplay.setJsonlBaseline(agentId, full.messages);
411
+ const history = limit > 0 && full.messages.length > limit
412
+ ? { ...full, messages: full.messages.slice(-limit) }
413
+ : full;
414
+ if (!isChatHistoryFromJsonl()) {
415
+ res.json({
416
+ agentId,
417
+ ...this.agentMessagesSnapshot(agentId, full.totalMessages),
418
+ requestId,
419
+ updatedAt: Date.now(),
420
+ });
421
+ return;
422
+ }
423
+ const snap = this.agentMessagesSnapshot(agentId, history.totalMessages);
424
+ const body = { ...history, ...snap, requestId, updatedAt: Date.now() };
149
425
  const bytes = JSON.stringify(body).length;
150
426
  bridgePipelineLog({
151
427
  dir: 'out',
@@ -196,7 +472,7 @@ export class Relay {
196
472
  res.setHeader('Cache-Control', 'private, max-age=3600');
197
473
  res.send(file.data);
198
474
  });
199
- this.app.post('/api/upload-image', express.json({ limit: '12mb' }), async (req, res) => {
475
+ this.app.post('/api/upload-image', express.json({ limit: '20mb' }), async (req, res) => {
200
476
  if (!this.checkMediaAuth(req)) {
201
477
  res.status(401).json({ error: 'Unauthorized' });
202
478
  return;
@@ -235,7 +511,7 @@ export class Relay {
235
511
  res.status(500).json({ error: err.message });
236
512
  }
237
513
  });
238
- this.app.post('/api/transcribe', express.json({ limit: '12mb' }), async (req, res) => {
514
+ this.app.post('/api/transcribe', express.json({ limit: '20mb' }), async (req, res) => {
239
515
  if (!this.checkMediaAuth(req)) {
240
516
  res.status(401).json({ error: 'Unauthorized' });
241
517
  return;
@@ -243,7 +519,7 @@ export class Relay {
243
519
  const apiKey = this.config.openaiApiKey;
244
520
  if (!apiKey) {
245
521
  res.status(503).json({
246
- error: 'OPENAI_API_KEY not set on Cursor Connect Mac (add to bridge/.env)',
522
+ error: 'Голосовой ввод недоступен: задайте OPENAI_API_KEY в bridge/.env (локальный режим без relay)',
247
523
  });
248
524
  return;
249
525
  }
@@ -307,6 +583,16 @@ export class Relay {
307
583
  this.io.on('connection', (socket) => this.onConnect(socket));
308
584
  }
309
585
  broadcast(event, payload) {
586
+ if (event === 'agent:messages') {
587
+ const p = payload;
588
+ bridgePipelineLog({
589
+ dir: 'out',
590
+ event: 'broadcast:agent:messages',
591
+ agentId: p?.agentId,
592
+ msgs: p?.historyMessages?.length ?? 0,
593
+ detail: `live=${p?.liveMessages?.length ?? 0} append=${!!p?.append} seq=${p?.seq ?? '-'} upstream=${this.upstream?.connected ?? false}`,
594
+ });
595
+ }
310
596
  if (event === 'agents:history' || event === 'agent:history') {
311
597
  const h = payload;
312
598
  const bytes = JSON.stringify(payload ?? {}).length;
@@ -323,13 +609,28 @@ export class Relay {
323
609
  this.io.emit(event, payload);
324
610
  this.upstream?.emit(event, payload);
325
611
  }
326
- prepareStateMessages(raw, patch) {
327
- const state = { ...this.stateManager.getState(), ...patch };
328
- for (const [agentId, meta] of this.jsonlIndex.getSubscribedAgents()) {
329
- if (isChatSyncedWithCursor(agentId, meta.title, state)) {
330
- return this.chatDisplay.mergeLiveForAgent(agentId, raw);
331
- }
332
- }
612
+ historySeqByAgent = new Map();
613
+ nextHistorySeq(agentId) {
614
+ const n = (this.historySeqByAgent.get(agentId) ?? 0) + 1;
615
+ this.historySeqByAgent.set(agentId, n);
616
+ return n;
617
+ }
618
+ emitAgentMessages(agentId, historyMessages, liveMessages, source, totalMessages, seq, append = false) {
619
+ const outSeq = seq ?? this.nextHistorySeq(agentId);
620
+ const messages = [...historyMessages, ...liveMessages];
621
+ this.broadcast('agent:messages', {
622
+ agentId,
623
+ historyMessages,
624
+ liveMessages,
625
+ messages,
626
+ totalMessages: totalMessages ?? messages.length,
627
+ source,
628
+ updatedAt: Date.now(),
629
+ seq: outSeq,
630
+ append: append || undefined,
631
+ });
632
+ }
633
+ prepareStateMessages(raw) {
333
634
  return filterClientDisplayList(prepareChatMessagesForDisplay(raw));
334
635
  }
335
636
  withDisplayState(payload) {
@@ -337,16 +638,21 @@ export class Relay {
337
638
  return payload;
338
639
  return {
339
640
  ...payload,
340
- messages: this.prepareStateMessages(payload.messages, payload),
641
+ messages: this.prepareStateMessages(payload.messages),
341
642
  };
342
643
  }
343
644
  wireEvents() {
344
645
  this.stateManager.on('state:full', (state) => {
345
646
  this.broadcast('state:full', this.withDisplayState(state));
346
647
  this.emitAgentsIndex();
648
+ this.observeAgentCompletionForPush();
347
649
  });
348
650
  this.stateManager.on('state:patch', (patch) => {
349
651
  this.broadcast('state:patch', this.withDisplayState(patch));
652
+ this.observeAgentCompletionForPush();
653
+ if (patch.messages?.length) {
654
+ this.scheduleDomOverlayEmit();
655
+ }
350
656
  if (patch.sidebarRepos || patch.composerIdByTitle) {
351
657
  const key = JSON.stringify([patch.sidebarRepos, patch.composerIdByTitle]);
352
658
  if (key !== this.lastSidebarIndexKey) {
@@ -363,11 +669,24 @@ export class Relay {
363
669
  this.lastJsonlIndex = index;
364
670
  this.emitAgentsIndex();
365
671
  });
672
+ this.jsonlIndex.on('agent:jsonl:updated', (payload) => {
673
+ const total = payload.totalMessages ?? payload.messages.length;
674
+ this.syncJsonlToSubscribedAgents(payload.agentId, payload.messages, total);
675
+ });
366
676
  this.jsonlIndex.on('agent:history', (history) => {
367
- const messages = this.chatDisplay.applyHistory(history.agentId, history.messages, {
368
- mergeWithCache: true,
369
- });
370
- this.broadcast('agent:history', { ...history, messages });
677
+ const total = history.totalMessages ?? history.messages.length;
678
+ this.emitJsonlLiveForAgent(history.agentId, history.messages, total);
679
+ if (isChatHistoryFromJsonl()) {
680
+ const snap = this.agentMessagesSnapshot(history.agentId, total);
681
+ this.broadcast('agent:history', {
682
+ ...history,
683
+ historyMessages: snap.historyMessages,
684
+ liveMessages: snap.liveMessages,
685
+ messages: snap.messages,
686
+ seq: this.historySeqByAgent.get(history.agentId),
687
+ totalMessages: total,
688
+ });
689
+ }
371
690
  });
372
691
  }
373
692
  emitAgentsIndex(force = false) {
@@ -517,6 +836,7 @@ export class Relay {
517
836
  const result = await this.commandExecutor.execute(payload);
518
837
  reply('command:result', result);
519
838
  if (payload.type === 'stop_agent' && result.ok) {
839
+ suppressAgentCompletionPush();
520
840
  this.stateManager.patchNow({
521
841
  agentWorking: false,
522
842
  agentStatus: undefined,
@@ -527,10 +847,10 @@ export class Relay {
527
847
  this.domExtractor.pollNow();
528
848
  }
529
849
  }
530
- async runAgentsHistory({ agentId, title, requestId, limit, }, reply) {
850
+ async runAgentsHistory({ agentId, title, requestId, limit, offset, minMessages, }, reply) {
531
851
  const t0 = Date.now();
532
852
  const viaUpstream = this.upstream?.connected ?? false;
533
- const socketLimit = limit && limit > 0 ? limit : 15;
853
+ const socketLimit = resolveHistoryLimit({ limit, minMessages });
534
854
  console.log(`[relay] agents:history req agentId=${agentId} title=${title?.slice(0, 48) ?? '-'} limit=${socketLimit} rid=${requestId ?? '-'} upstream=${viaUpstream}`);
535
855
  this.jsonlIndex.historyReplyInFlight.add(agentId);
536
856
  bridgePipelineLog({
@@ -541,14 +861,30 @@ export class Relay {
541
861
  detail: `limit=${socketLimit} title=${title?.slice(0, 32) ?? '-'} upstream=${viaUpstream}`,
542
862
  });
543
863
  try {
544
- const history = await this.jsonlIndex.loadHistory(agentId, {
864
+ const full = await this.jsonlIndex.loadHistory(agentId, {
545
865
  title,
546
866
  composerIdByTitle: this.stateManager.getState().composerIdByTitle,
547
- limit: socketLimit,
867
+ offset,
548
868
  });
549
- const messages = this.chatDisplay.applyHistory(agentId, history.messages);
869
+ this.emitJsonlLiveForAgent(agentId, full.messages, full.totalMessages);
870
+ const snap = this.agentMessagesSnapshot(agentId, full.totalMessages);
871
+ if (!isChatHistoryFromJsonl()) {
872
+ const seq = this.historySeqByAgent.get(agentId) ?? this.nextHistorySeq(agentId);
873
+ const ms = Date.now() - t0;
874
+ console.log(`[relay] agents:history jsonl agentId=${agentId} hist=${snap.historyMessages.length} total=${full.totalMessages} ms=${ms} rid=${requestId ?? '-'}`);
875
+ reply('agents:history', {
876
+ agentId,
877
+ ...snap,
878
+ totalMessages: full.totalMessages,
879
+ requestId,
880
+ updatedAt: Date.now(),
881
+ seq,
882
+ });
883
+ return;
884
+ }
885
+ const history = full;
550
886
  const ms = Date.now() - t0;
551
- const bytes = JSON.stringify(messages).length;
887
+ const bytes = JSON.stringify(snap.messages).length;
552
888
  console.log(`[relay] agents:history ok agentId=${history.agentId} msgs=${history.messages?.length ?? 0}/${history.totalMessages ?? '?'} bytes=${bytes} ms=${ms} rid=${requestId ?? '-'}`);
553
889
  bridgePipelineLog({
554
890
  dir: 'out',
@@ -559,11 +895,15 @@ export class Relay {
559
895
  msgs: history.messages?.length ?? 0,
560
896
  detail: `total=${history.totalMessages ?? '?'} ms=${ms}`,
561
897
  });
898
+ const seq = this.nextHistorySeq(history.agentId);
562
899
  reply('agents:history', {
563
900
  ...history,
564
- messages,
901
+ historyMessages: snap.historyMessages,
902
+ liveMessages: snap.liveMessages,
903
+ messages: snap.messages,
565
904
  requestId,
566
905
  updatedAt: Date.now(),
906
+ seq,
567
907
  });
568
908
  }
569
909
  catch (err) {
@@ -590,11 +930,14 @@ export class Relay {
590
930
  async runAgentsSubscribe({ agentId, title, focus, }) {
591
931
  if (!agentId)
592
932
  return;
593
- this.jsonlIndex.subscribe(agentId, title);
933
+ const alreadySubscribed = this.jsonlIndex.getSubscribedAgents().has(agentId);
934
+ this.jsonlIndex.subscribe(agentId, title, { emitHistory: !alreadySubscribed });
594
935
  this.stateManager.patchNow({ lastError: undefined });
595
936
  await this.trySwitchWindowForAgent(agentId);
596
- if (focus === false)
937
+ if (focus === false) {
938
+ void this.refreshDomChatOnSubscribe(agentId, title, { clear: !alreadySubscribed });
597
939
  return;
940
+ }
598
941
  const result = await this.commandExecutor.execute({
599
942
  id: `subscribe-focus-${Date.now()}`,
600
943
  type: 'focus_agent',
@@ -604,11 +947,37 @@ export class Relay {
604
947
  if (!result.ok) {
605
948
  console.warn('[relay] subscribe focus (non-fatal):', result.error);
606
949
  }
950
+ void this.refreshDomChatOnSubscribe(agentId, title, { clear: !alreadySubscribed });
951
+ }
952
+ /** Открытие чата: JSONL baseline + focus/scroll (DOM poll только для state: working/approve). */
953
+ async refreshDomChatOnSubscribe(agentId, title, opts) {
954
+ try {
955
+ if (opts?.clear !== false) {
956
+ this.chatDisplay.clearAgent(agentId);
957
+ this.lastEmittedJsonlRows.delete(agentId);
958
+ this.lastEmittedLentaSig.delete(agentId);
959
+ this.lastEmittedHistLen.delete(agentId);
960
+ }
961
+ const history = await this.jsonlIndex.loadHistory(agentId, {
962
+ title,
963
+ composerIdByTitle: this.stateManager.getState().composerIdByTitle,
964
+ });
965
+ this.lastEmittedLentaSig.delete(agentId);
966
+ this.emitJsonlLiveForAgent(agentId, history.messages, history.totalMessages);
967
+ await this.commandExecutor.scrollChatToBottom();
968
+ this.domExtractor.pollNow();
969
+ }
970
+ catch (err) {
971
+ console.warn('[relay] refreshDomChatOnSubscribe (non-fatal):', err.message);
972
+ }
607
973
  }
608
974
  runAgentsUnsubscribe({ agentId }) {
609
975
  if (agentId) {
610
976
  this.jsonlIndex.unsubscribe(agentId);
611
977
  this.chatDisplay.clearAgent(agentId);
978
+ this.lastEmittedJsonlRows.delete(agentId);
979
+ this.lastEmittedLentaSig.delete(agentId);
980
+ this.lastEmittedHistLen.delete(agentId);
612
981
  }
613
982
  }
614
983
  async runAgentsFocus({ agentId, title }, reply) {
@@ -33,6 +33,10 @@ export interface ServerConfig {
33
33
  relayUrl: string;
34
34
  relayToken: string;
35
35
  relayRoomId: string;
36
+ /** macOS: prevent system sleep while bridge runs (`KEEP_AWAKE=0` to disable). */
37
+ keepAwakeEnabled: boolean;
38
+ /** Periodic upstream ping when `relayUrl` set; `0` = off. */
39
+ relayKeepaliveMs: number;
36
40
  /** Client token from ~/.cursorconnect/identity.json (relay room pairing). */
37
41
  pairingClientToken?: string;
38
42
  }
@@ -90,7 +94,10 @@ export interface ChatMessage {
90
94
  html?: string;
91
95
  /** Absolute path or URL from DOM `image-pill-img` (served via GET /media/file). */
92
96
  images?: string[];
97
+ /** Cursor `data-flat-index` when present; else extract sequence. */
93
98
  flatIndex?: number;
99
+ /** Document order within one DOM extract (tie-break when flatIndex ties). */
100
+ domSeq?: number;
94
101
  }
95
102
  /** Per-poll DOM extract stats — why rows were skipped (for missing-message debug). */
96
103
  export interface MessageExtractDebug {
@@ -247,5 +254,23 @@ export interface AgentHistory {
247
254
  updatedAt?: number;
248
255
  requestId?: string;
249
256
  totalMessages?: number;
257
+ /** Monotonic per agentId — stale snapshots should be ignored. */
258
+ seq?: number;
259
+ }
260
+ /** Per-agent chat lenta (JSONL). `state.messages` in state:patch is DOM/debug only. */
261
+ export interface AgentMessagesPayload {
262
+ agentId: string;
263
+ /** JSONL archive tail (history). When `append`, only new display rows since last emit. */
264
+ historyMessages: ChatMessage[];
265
+ /** Legacy field; always `[]` — lenta is JSONL-only. */
266
+ liveMessages: ChatMessage[];
267
+ /** Same as historyMessages (debug). */
268
+ messages: ChatMessage[];
269
+ totalMessages?: number;
270
+ source: 'dom' | 'jsonl' | 'hybrid';
271
+ updatedAt: number;
272
+ seq: number;
273
+ /** Live JSONL stream: merge `historyMessages` into client state (small payload). */
274
+ append?: boolean;
250
275
  }
251
276
  export type ExtractedPageState = Omit<CursorState, 'connected' | 'windows' | 'activeWindowId' | 'windowSnapshots' | 'updatedAt'>;
@@ -47,12 +47,11 @@
47
47
  "strategies": [
48
48
  "button.ui-shell-tool-call__run-btn",
49
49
  "button.ui-shell-tool-call__allowlist-button",
50
- "button[aria-label*='Accept']",
51
- "button[aria-label*='Approve']",
52
- "button[aria-label*='Run']",
53
- "button[aria-label*='Allow']"
50
+ ".ui-shell-tool-call__approval-row button[aria-label*='Accept']",
51
+ ".ui-shell-tool-call__approval-row button[aria-label*='Approve']",
52
+ ".ui-shell-tool-call__approval-row button[aria-label*='Allow']"
54
53
  ],
55
- "textMatch": ["Accept", "Approve", "Run", "Allow", "Accept All"]
54
+ "textMatch": ["Accept", "Approve", "Allow", "Accept All"]
56
55
  },
57
56
  "rejectButton": {
58
57
  "strategies": [