cursorconnect 0.1.6 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/bridge-runtime/.env.example +10 -2
  2. package/bridge-runtime/connector-version.json +1 -1
  3. package/bridge-runtime/dist/agent-completion-push.d.ts +42 -0
  4. package/bridge-runtime/dist/agent-completion-push.js +220 -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 +94 -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/debug-chats-page.d.ts +2 -0
  18. package/bridge-runtime/dist/debug-chats-page.js +491 -0
  19. package/bridge-runtime/dist/dom-transcript-store.d.ts +17 -0
  20. package/bridge-runtime/dist/dom-transcript-store.js +76 -0
  21. package/bridge-runtime/dist/extract-page.js +56 -85
  22. package/bridge-runtime/dist/history-limit.d.ts +2 -0
  23. package/bridge-runtime/dist/history-limit.js +2 -0
  24. package/bridge-runtime/dist/history-request.d.ts +8 -0
  25. package/bridge-runtime/dist/history-request.js +7 -0
  26. package/bridge-runtime/dist/index.js +1 -0
  27. package/bridge-runtime/dist/jsonl-index.d.ts +21 -3
  28. package/bridge-runtime/dist/jsonl-index.js +237 -73
  29. package/bridge-runtime/dist/jsonl-live-debug.d.ts +24 -0
  30. package/bridge-runtime/dist/jsonl-live-debug.js +175 -0
  31. package/bridge-runtime/dist/media-path.d.ts +2 -0
  32. package/bridge-runtime/dist/media-path.js +17 -0
  33. package/bridge-runtime/dist/message-filter.d.ts +2 -0
  34. package/bridge-runtime/dist/message-filter.js +21 -5
  35. package/bridge-runtime/dist/pairing-code.d.ts +2 -0
  36. package/bridge-runtime/dist/pairing-code.js +9 -2
  37. package/bridge-runtime/dist/relay-upstream.d.ts +2 -1
  38. package/bridge-runtime/dist/relay-upstream.js +4 -1
  39. package/bridge-runtime/dist/relay.d.ts +21 -0
  40. package/bridge-runtime/dist/relay.js +332 -28
  41. package/bridge-runtime/dist/types.d.ts +21 -0
  42. package/bridge-runtime/selectors.json +4 -5
  43. package/dist/index.js +79 -20
  44. package/dist/launch.js +23 -5
  45. package/dist/macos-autostart.js +87 -0
  46. package/dist/pairing-code.js +12 -3
  47. package/dist/print-pairing.js +2 -0
  48. package/dist/run-service.js +31 -0
  49. package/dist/startup-check.js +165 -0
  50. package/package.json +1 -1
  51. package/version-policy.json +1 -1
@@ -4,14 +4,23 @@ 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 { AGENT_HISTORY_DEFAULT_LIMIT } from './history-limit.js';
10
+ import { resolveHistoryLimit } from './history-request.js';
8
11
  import { filterClientDisplayList, prepareChatMessagesForDisplay, } from './chat-display.js';
9
- import { isChatSyncedWithCursor } from './chat-sync.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 { readJsonlLiveSnapshot } from './jsonl-live-debug.js';
15
24
  export class Relay {
16
25
  stateManager;
17
26
  commandExecutor;
@@ -30,7 +39,11 @@ export class Relay {
30
39
  io;
31
40
  tokens = new Set();
32
41
  upstream = null;
42
+ agentCompletionPush = null;
43
+ pendingPushPayloads = [];
33
44
  chatDisplay = new ChatDisplayStore();
45
+ /** Raw JSONL row count last sent per agent (live `append` emits). */
46
+ lastEmittedJsonlRows = new Map();
34
47
  constructor(config, stateManager, commandExecutor, cdpBridge, jsonlIndex, messageDebugStore, domExtractor) {
35
48
  this.stateManager = stateManager;
36
49
  this.commandExecutor = commandExecutor;
@@ -53,11 +66,32 @@ export class Relay {
53
66
  else {
54
67
  this.upstream = new RelayUpstream(this.config, (event, ...args) => {
55
68
  this.handleRemoteClientEvent(event, ...args);
56
- });
69
+ }, () => this.flushPendingPushPayloads());
70
+ this.agentCompletionPush = new AgentCompletionPush((payload) => this.emitAgentCompletedPush(payload), () => this.lastJsonlIndex, () => this.stateManager.getState());
57
71
  this.upstream.connect();
58
72
  }
59
73
  }
60
74
  }
75
+ emitAgentCompletedPush(payload) {
76
+ if (this.upstream?.connected) {
77
+ this.upstream.emit('push:agent-completed', payload);
78
+ return;
79
+ }
80
+ this.pendingPushPayloads.push(payload);
81
+ console.warn(`[agent-completion-push] queued for relay (${this.pendingPushPayloads.length}) upstream disconnected`);
82
+ }
83
+ flushPendingPushPayloads() {
84
+ if (!this.upstream?.connected || !this.pendingPushPayloads.length)
85
+ return;
86
+ const batch = this.pendingPushPayloads.splice(0);
87
+ for (const payload of batch) {
88
+ this.upstream.emit('push:agent-completed', payload);
89
+ }
90
+ console.log(`[agent-completion-push] flushed ${batch.length} queued push(es) to relay`);
91
+ }
92
+ observeAgentCompletionForPush() {
93
+ this.agentCompletionPush?.observe(this.stateManager.getState());
94
+ }
61
95
  listen() {
62
96
  return new Promise((resolve) => {
63
97
  this.httpServer.listen(this.config.serverPort, this.config.serverHost, () => {
@@ -69,6 +103,107 @@ export class Relay {
69
103
  get authEnabled() {
70
104
  return this.config.webappPassword.length > 0;
71
105
  }
106
+ /** Read-only view of in-memory bridge state (no writes to stores). */
107
+ readOnlyChatSnapshot() {
108
+ const state = this.stateManager.getState();
109
+ const subscribed = [...this.jsonlIndex.getSubscribedAgents().entries()].map(([agentId, meta]) => ({ agentId, title: meta.title }));
110
+ const displayCache = {};
111
+ const cacheIds = new Set([
112
+ ...subscribed.map((s) => s.agentId),
113
+ ...(state.activeComposerId ? [state.activeComposerId] : []),
114
+ ]);
115
+ const domTranscript = {};
116
+ for (const agentId of cacheIds) {
117
+ const dom = this.chatDisplay.getDomTranscript(agentId);
118
+ if (dom.length)
119
+ domTranscript[agentId] = dom;
120
+ const jsonl = this.chatDisplay.getJsonlHistory(agentId);
121
+ if (jsonl.length)
122
+ displayCache[agentId] = jsonl;
123
+ }
124
+ return {
125
+ at: Date.now(),
126
+ health: { cdp: this.cdpBridge.getClient()?.isConnected() ?? false },
127
+ cursor: {
128
+ activeComposerId: state.activeComposerId,
129
+ activeChatTitle: state.activeChatTitle,
130
+ updatedAt: state.updatedAt,
131
+ lastError: state.lastError,
132
+ tabs: state.tabs,
133
+ composerIdByTitle: state.composerIdByTitle,
134
+ domMessageCount: state.messages.length,
135
+ domMessages: state.messages,
136
+ },
137
+ domExtractDebug: {
138
+ latest: this.messageDebugStore.latest(),
139
+ ringSize: this.messageDebugStore.list().length,
140
+ },
141
+ subscribed,
142
+ displayCache,
143
+ chatHistoryJsonl: isChatHistoryFromJsonl(),
144
+ domTranscript,
145
+ };
146
+ }
147
+ /** Push JSONL file updates to every subscribed route id (e.g. sidebar-0) for that composer. */
148
+ syncJsonlToSubscribedAgents(fileAgentId, messages, totalMessages) {
149
+ const state = this.stateManager.getState();
150
+ const activeTab = state.tabs.find((t) => t.active);
151
+ const targets = new Set([fileAgentId]);
152
+ for (const [subId, meta] of this.jsonlIndex.getSubscribedAgents()) {
153
+ const path = resolveJsonlFilePath(this.config.cursorProjectsDir, subId, {
154
+ title: meta.title,
155
+ composerIdByTitle: state.composerIdByTitle,
156
+ activeComposerId: state.activeComposerId,
157
+ activeTabTitle: activeTab?.title,
158
+ });
159
+ if (path && basename(path, '.jsonl') === fileAgentId)
160
+ targets.add(subId);
161
+ }
162
+ for (const agentId of targets) {
163
+ this.emitJsonlLiveForAgent(agentId, messages, totalMessages);
164
+ }
165
+ }
166
+ /** Live JSONL: push display deltas to app; `totalMessages` = raw `.jsonl` line count. */
167
+ emitJsonlLiveForAgent(agentId, rows, totalMessages) {
168
+ const prevDisplay = this.chatDisplay.getJsonlHistory(agentId);
169
+ const prevLen = prevDisplay.length;
170
+ const prevLast = prevLen ? prevDisplay[prevLen - 1] : undefined;
171
+ this.chatDisplay.setJsonlBaseline(agentId, rows);
172
+ this.lastEmittedJsonlRows.set(agentId, totalMessages);
173
+ const historyMessages = this.chatDisplay.getJsonlHistory(agentId);
174
+ if (!historyMessages.length)
175
+ return;
176
+ if (!prevLen) {
177
+ this.emitAgentMessages(agentId, historyMessages, [], 'jsonl', totalMessages, this.nextHistorySeq(agentId), false);
178
+ return;
179
+ }
180
+ const appended = historyMessages.slice(prevLen);
181
+ if (appended.length) {
182
+ this.emitAgentMessages(agentId, appended, [], 'jsonl', totalMessages, this.nextHistorySeq(agentId), true);
183
+ return;
184
+ }
185
+ const last = historyMessages[historyMessages.length - 1];
186
+ if (prevLast &&
187
+ last &&
188
+ last.role === prevLast.role &&
189
+ (last.text ?? '') !== (prevLast.text ?? '')) {
190
+ this.emitAgentMessages(agentId, [last], [], 'jsonl', totalMessages, this.nextHistorySeq(agentId), true);
191
+ return;
192
+ }
193
+ if (prevLen !== historyMessages.length || !this.jsonlIndex.getSubscribedAgents().has(agentId)) {
194
+ this.emitAgentMessages(agentId, historyMessages, [], 'jsonl', totalMessages, this.nextHistorySeq(agentId), false);
195
+ }
196
+ }
197
+ /** Client lenta: JSONL only (`liveMessages` always empty). DOM stays in debug snapshot. */
198
+ agentMessagesSnapshot(agentId, totalMessages) {
199
+ const historyMessages = this.chatDisplay.getJsonlHistory(agentId);
200
+ return {
201
+ historyMessages,
202
+ liveMessages: [],
203
+ messages: historyMessages,
204
+ totalMessages: totalMessages ?? historyMessages.length,
205
+ };
206
+ }
72
207
  checkMediaAuth(req) {
73
208
  if (!this.authEnabled)
74
209
  return true;
@@ -80,7 +215,13 @@ export class Relay {
80
215
  }
81
216
  setupHttp() {
82
217
  this.app.get('/health', (_req, res) => {
83
- res.json({ ok: true, cdp: this.cdpBridge.getClient()?.isConnected() ?? false });
218
+ res.json({
219
+ ok: true,
220
+ cdp: this.cdpBridge.getClient()?.isConnected() ?? false,
221
+ debugChats: '/debug/chats',
222
+ debugSnapshot: '/debug/chat-snapshot',
223
+ debugJsonlLive: '/debug/jsonl-live',
224
+ });
84
225
  });
85
226
  this.app.get('/debug/messages', (_req, res) => {
86
227
  const state = this.stateManager.getState();
@@ -92,6 +233,62 @@ export class Relay {
92
233
  lastError: state.lastError,
93
234
  });
94
235
  });
236
+ this.app.get('/debug/chats', (_req, res) => {
237
+ res.type('html').send(DEBUG_CHATS_PAGE_HTML);
238
+ });
239
+ this.app.get('/debug/chat-snapshot', (_req, res) => {
240
+ res.json(this.readOnlyChatSnapshot());
241
+ });
242
+ this.app.get('/debug/jsonl-live', async (req, res) => {
243
+ if (!this.checkMediaAuth(req)) {
244
+ res.status(401).json({ error: 'Unauthorized' });
245
+ return;
246
+ }
247
+ const agentId = String(req.query.agentId ?? '').trim();
248
+ if (!agentId) {
249
+ res.status(400).json({ error: 'agentId required' });
250
+ return;
251
+ }
252
+ const title = typeof req.query.title === 'string' ? req.query.title.trim() : undefined;
253
+ const afterRaw = Number(req.query.afterLine ?? 0);
254
+ const tailRaw = Number(req.query.tail ?? 0);
255
+ const afterLine = Number.isFinite(afterRaw) && afterRaw > 0 ? Math.floor(afterRaw) : 0;
256
+ const tail = afterLine > 0
257
+ ? 0
258
+ : Number.isFinite(tailRaw) && tailRaw > 0
259
+ ? Math.min(500, Math.floor(tailRaw))
260
+ : 80;
261
+ const state = this.stateManager.getState();
262
+ const activeTab = state.tabs.find((t) => t.active);
263
+ const filePath = resolveJsonlFilePath(this.config.cursorProjectsDir, agentId, {
264
+ title,
265
+ composerIdByTitle: state.composerIdByTitle,
266
+ activeComposerId: state.activeComposerId,
267
+ activeTabTitle: activeTab?.title,
268
+ });
269
+ if (!filePath) {
270
+ res.json({
271
+ agentId,
272
+ filePath: null,
273
+ fileSize: 0,
274
+ totalLines: 0,
275
+ updatedAt: Date.now(),
276
+ rows: [],
277
+ });
278
+ return;
279
+ }
280
+ try {
281
+ const snap = await readJsonlLiveSnapshot(filePath, agentId, {
282
+ afterLine,
283
+ tail,
284
+ maxNew: 96,
285
+ });
286
+ res.json(snap);
287
+ }
288
+ catch (err) {
289
+ res.status(500).json({ error: err.message });
290
+ }
291
+ });
95
292
  this.app.get('/api/agents/index', async (req, res) => {
96
293
  if (!this.checkMediaAuth(req)) {
97
294
  res.status(401).json({ error: 'Unauthorized' });
@@ -125,7 +322,13 @@ export class Relay {
125
322
  const agentId = String(req.query.agentId ?? '').trim();
126
323
  const title = typeof req.query.title === 'string' ? req.query.title.trim() : undefined;
127
324
  const limitRaw = Number(req.query.limit ?? 0);
128
- const limit = Number.isFinite(limitRaw) && limitRaw > 0 ? limitRaw : undefined;
325
+ const minRaw = Number(req.query.minMessages ?? 0);
326
+ const limit = resolveHistoryLimit({
327
+ limit: Number.isFinite(limitRaw) && limitRaw > 0 ? limitRaw : undefined,
328
+ minMessages: Number.isFinite(minRaw) && minRaw > 0 ? minRaw : undefined,
329
+ });
330
+ const offsetRaw = Number(req.query.offset ?? 0);
331
+ const offset = Number.isFinite(offsetRaw) && offsetRaw > 0 ? offsetRaw : undefined;
129
332
  const requestId = typeof req.query.requestId === 'string' ? req.query.requestId.trim() : undefined;
130
333
  if (!agentId) {
131
334
  res.status(400).json({ error: 'agentId required' });
@@ -139,13 +342,31 @@ export class Relay {
139
342
  detail: `limit=${limit ?? 'all'} title=${title?.slice(0, 32) ?? '-'}`,
140
343
  });
141
344
  try {
345
+ if (!isChatHistoryFromJsonl()) {
346
+ const history = await this.jsonlIndex.loadHistory(agentId, {
347
+ title,
348
+ composerIdByTitle: this.stateManager.getState().composerIdByTitle,
349
+ limit,
350
+ offset,
351
+ });
352
+ this.chatDisplay.setJsonlBaseline(agentId, history.messages);
353
+ res.json({
354
+ agentId,
355
+ ...this.agentMessagesSnapshot(agentId, history.totalMessages),
356
+ requestId,
357
+ updatedAt: Date.now(),
358
+ });
359
+ return;
360
+ }
142
361
  const history = await this.jsonlIndex.loadHistory(agentId, {
143
362
  title,
144
363
  composerIdByTitle: this.stateManager.getState().composerIdByTitle,
145
364
  limit,
365
+ offset,
146
366
  });
147
- const messages = this.chatDisplay.applyHistory(agentId, history.messages);
148
- const body = { ...history, messages, requestId, updatedAt: Date.now() };
367
+ this.chatDisplay.setJsonlBaseline(agentId, history.messages);
368
+ const snap = this.agentMessagesSnapshot(agentId, history.totalMessages);
369
+ const body = { ...history, ...snap, requestId, updatedAt: Date.now() };
149
370
  const bytes = JSON.stringify(body).length;
150
371
  bridgePipelineLog({
151
372
  dir: 'out',
@@ -196,7 +417,7 @@ export class Relay {
196
417
  res.setHeader('Cache-Control', 'private, max-age=3600');
197
418
  res.send(file.data);
198
419
  });
199
- this.app.post('/api/upload-image', express.json({ limit: '12mb' }), async (req, res) => {
420
+ this.app.post('/api/upload-image', express.json({ limit: '20mb' }), async (req, res) => {
200
421
  if (!this.checkMediaAuth(req)) {
201
422
  res.status(401).json({ error: 'Unauthorized' });
202
423
  return;
@@ -235,7 +456,7 @@ export class Relay {
235
456
  res.status(500).json({ error: err.message });
236
457
  }
237
458
  });
238
- this.app.post('/api/transcribe', express.json({ limit: '12mb' }), async (req, res) => {
459
+ this.app.post('/api/transcribe', express.json({ limit: '20mb' }), async (req, res) => {
239
460
  if (!this.checkMediaAuth(req)) {
240
461
  res.status(401).json({ error: 'Unauthorized' });
241
462
  return;
@@ -243,7 +464,7 @@ export class Relay {
243
464
  const apiKey = this.config.openaiApiKey;
244
465
  if (!apiKey) {
245
466
  res.status(503).json({
246
- error: 'OPENAI_API_KEY not set on Cursor Connect Mac (add to bridge/.env)',
467
+ error: 'Голосовой ввод недоступен: задайте OPENAI_API_KEY в bridge/.env (локальный режим без relay)',
247
468
  });
248
469
  return;
249
470
  }
@@ -323,13 +544,29 @@ export class Relay {
323
544
  this.io.emit(event, payload);
324
545
  this.upstream?.emit(event, payload);
325
546
  }
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
- }
547
+ historySeqByAgent = new Map();
548
+ nextHistorySeq(agentId) {
549
+ const n = (this.historySeqByAgent.get(agentId) ?? 0) + 1;
550
+ this.historySeqByAgent.set(agentId, n);
551
+ return n;
552
+ }
553
+ emitAgentMessages(agentId, historyMessages, liveMessages, source, totalMessages, seq, append = false) {
554
+ const outSeq = seq ?? this.nextHistorySeq(agentId);
555
+ const messages = [...historyMessages, ...liveMessages];
556
+ this.broadcast('agent:messages', {
557
+ agentId,
558
+ historyMessages,
559
+ liveMessages,
560
+ messages,
561
+ totalMessages: totalMessages ?? messages.length,
562
+ source,
563
+ updatedAt: Date.now(),
564
+ seq: outSeq,
565
+ append: append || undefined,
566
+ });
567
+ }
568
+ /** DOM messages in state:patch — debug/UI chrome only; chat lenta is JSONL via agent:messages. */
569
+ prepareStateMessages(raw) {
333
570
  return filterClientDisplayList(prepareChatMessagesForDisplay(raw));
334
571
  }
335
572
  withDisplayState(payload) {
@@ -337,16 +574,18 @@ export class Relay {
337
574
  return payload;
338
575
  return {
339
576
  ...payload,
340
- messages: this.prepareStateMessages(payload.messages, payload),
577
+ messages: this.prepareStateMessages(payload.messages),
341
578
  };
342
579
  }
343
580
  wireEvents() {
344
581
  this.stateManager.on('state:full', (state) => {
345
582
  this.broadcast('state:full', this.withDisplayState(state));
346
583
  this.emitAgentsIndex();
584
+ this.observeAgentCompletionForPush();
347
585
  });
348
586
  this.stateManager.on('state:patch', (patch) => {
349
587
  this.broadcast('state:patch', this.withDisplayState(patch));
588
+ this.observeAgentCompletionForPush();
350
589
  if (patch.sidebarRepos || patch.composerIdByTitle) {
351
590
  const key = JSON.stringify([patch.sidebarRepos, patch.composerIdByTitle]);
352
591
  if (key !== this.lastSidebarIndexKey) {
@@ -363,11 +602,25 @@ export class Relay {
363
602
  this.lastJsonlIndex = index;
364
603
  this.emitAgentsIndex();
365
604
  });
605
+ this.jsonlIndex.on('agent:jsonl:updated', (payload) => {
606
+ const total = payload.totalMessages ?? payload.messages.length;
607
+ this.syncJsonlToSubscribedAgents(payload.agentId, payload.messages, total);
608
+ this.agentCompletionPush?.onJsonlUpdated(payload.agentId, payload.messages, this.stateManager.getState());
609
+ });
366
610
  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 });
611
+ const total = history.totalMessages ?? history.messages.length;
612
+ this.emitJsonlLiveForAgent(history.agentId, history.messages, total);
613
+ if (isChatHistoryFromJsonl()) {
614
+ const snap = this.agentMessagesSnapshot(history.agentId, total);
615
+ this.broadcast('agent:history', {
616
+ ...history,
617
+ historyMessages: snap.historyMessages,
618
+ liveMessages: snap.liveMessages,
619
+ messages: snap.messages,
620
+ seq: this.historySeqByAgent.get(history.agentId),
621
+ totalMessages: total,
622
+ });
623
+ }
371
624
  });
372
625
  }
373
626
  emitAgentsIndex(force = false) {
@@ -517,6 +770,7 @@ export class Relay {
517
770
  const result = await this.commandExecutor.execute(payload);
518
771
  reply('command:result', result);
519
772
  if (payload.type === 'stop_agent' && result.ok) {
773
+ suppressAgentCompletionPush();
520
774
  this.stateManager.patchNow({
521
775
  agentWorking: false,
522
776
  agentStatus: undefined,
@@ -527,10 +781,10 @@ export class Relay {
527
781
  this.domExtractor.pollNow();
528
782
  }
529
783
  }
530
- async runAgentsHistory({ agentId, title, requestId, limit, }, reply) {
784
+ async runAgentsHistory({ agentId, title, requestId, limit, offset, minMessages, }, reply) {
531
785
  const t0 = Date.now();
532
786
  const viaUpstream = this.upstream?.connected ?? false;
533
- const socketLimit = limit && limit > 0 ? limit : 15;
787
+ const socketLimit = resolveHistoryLimit({ limit, minMessages });
534
788
  console.log(`[relay] agents:history req agentId=${agentId} title=${title?.slice(0, 48) ?? '-'} limit=${socketLimit} rid=${requestId ?? '-'} upstream=${viaUpstream}`);
535
789
  this.jsonlIndex.historyReplyInFlight.add(agentId);
536
790
  bridgePipelineLog({
@@ -541,14 +795,38 @@ export class Relay {
541
795
  detail: `limit=${socketLimit} title=${title?.slice(0, 32) ?? '-'} upstream=${viaUpstream}`,
542
796
  });
543
797
  try {
798
+ if (!isChatHistoryFromJsonl()) {
799
+ const history = await this.jsonlIndex.loadHistory(agentId, {
800
+ title,
801
+ composerIdByTitle: this.stateManager.getState().composerIdByTitle,
802
+ limit: socketLimit,
803
+ offset,
804
+ });
805
+ this.emitJsonlLiveForAgent(agentId, history.messages, history.totalMessages);
806
+ const snap = this.agentMessagesSnapshot(agentId, history.totalMessages);
807
+ const seq = this.historySeqByAgent.get(agentId) ?? this.nextHistorySeq(agentId);
808
+ const ms = Date.now() - t0;
809
+ console.log(`[relay] agents:history jsonl agentId=${agentId} hist=${snap.historyMessages.length} total=${history.totalMessages} ms=${ms} rid=${requestId ?? '-'}`);
810
+ reply('agents:history', {
811
+ agentId,
812
+ ...snap,
813
+ totalMessages: history.totalMessages,
814
+ requestId,
815
+ updatedAt: Date.now(),
816
+ seq,
817
+ });
818
+ return;
819
+ }
544
820
  const history = await this.jsonlIndex.loadHistory(agentId, {
545
821
  title,
546
822
  composerIdByTitle: this.stateManager.getState().composerIdByTitle,
547
823
  limit: socketLimit,
824
+ offset,
548
825
  });
549
- const messages = this.chatDisplay.applyHistory(agentId, history.messages);
826
+ this.emitJsonlLiveForAgent(agentId, history.messages, history.totalMessages);
827
+ const snap = this.agentMessagesSnapshot(agentId, history.totalMessages);
550
828
  const ms = Date.now() - t0;
551
- const bytes = JSON.stringify(messages).length;
829
+ const bytes = JSON.stringify(snap.messages).length;
552
830
  console.log(`[relay] agents:history ok agentId=${history.agentId} msgs=${history.messages?.length ?? 0}/${history.totalMessages ?? '?'} bytes=${bytes} ms=${ms} rid=${requestId ?? '-'}`);
553
831
  bridgePipelineLog({
554
832
  dir: 'out',
@@ -559,11 +837,15 @@ export class Relay {
559
837
  msgs: history.messages?.length ?? 0,
560
838
  detail: `total=${history.totalMessages ?? '?'} ms=${ms}`,
561
839
  });
840
+ const seq = this.nextHistorySeq(history.agentId);
562
841
  reply('agents:history', {
563
842
  ...history,
564
- messages,
843
+ historyMessages: snap.historyMessages,
844
+ liveMessages: snap.liveMessages,
845
+ messages: snap.messages,
565
846
  requestId,
566
847
  updatedAt: Date.now(),
848
+ seq,
567
849
  });
568
850
  }
569
851
  catch (err) {
@@ -590,11 +872,13 @@ export class Relay {
590
872
  async runAgentsSubscribe({ agentId, title, focus, }) {
591
873
  if (!agentId)
592
874
  return;
593
- this.jsonlIndex.subscribe(agentId, title);
875
+ this.jsonlIndex.subscribe(agentId, title, { emitHistory: true });
594
876
  this.stateManager.patchNow({ lastError: undefined });
595
877
  await this.trySwitchWindowForAgent(agentId);
596
- if (focus === false)
878
+ if (focus === false) {
879
+ void this.refreshDomChatOnSubscribe(agentId, title);
597
880
  return;
881
+ }
598
882
  const result = await this.commandExecutor.execute({
599
883
  id: `subscribe-focus-${Date.now()}`,
600
884
  type: 'focus_agent',
@@ -604,11 +888,31 @@ export class Relay {
604
888
  if (!result.ok) {
605
889
  console.warn('[relay] subscribe focus (non-fatal):', result.error);
606
890
  }
891
+ void this.refreshDomChatOnSubscribe(agentId, title);
892
+ }
893
+ /** Открытие чата: JSONL baseline + focus/scroll (DOM poll только для state: working/approve). */
894
+ async refreshDomChatOnSubscribe(agentId, title) {
895
+ try {
896
+ this.chatDisplay.clearAgent(agentId);
897
+ this.lastEmittedJsonlRows.delete(agentId);
898
+ const history = await this.jsonlIndex.loadHistory(agentId, {
899
+ title,
900
+ composerIdByTitle: this.stateManager.getState().composerIdByTitle,
901
+ limit: AGENT_HISTORY_DEFAULT_LIMIT,
902
+ });
903
+ this.emitJsonlLiveForAgent(agentId, history.messages, history.totalMessages);
904
+ await this.commandExecutor.scrollChatToBottom();
905
+ this.domExtractor.pollNow();
906
+ }
907
+ catch (err) {
908
+ console.warn('[relay] refreshDomChatOnSubscribe (non-fatal):', err.message);
909
+ }
607
910
  }
608
911
  runAgentsUnsubscribe({ agentId }) {
609
912
  if (agentId) {
610
913
  this.jsonlIndex.unsubscribe(agentId);
611
914
  this.chatDisplay.clearAgent(agentId);
915
+ this.lastEmittedJsonlRows.delete(agentId);
612
916
  }
613
917
  }
614
918
  async runAgentsFocus({ agentId, title }, reply) {
@@ -90,7 +90,10 @@ export interface ChatMessage {
90
90
  html?: string;
91
91
  /** Absolute path or URL from DOM `image-pill-img` (served via GET /media/file). */
92
92
  images?: string[];
93
+ /** Cursor `data-flat-index` when present; else extract sequence. */
93
94
  flatIndex?: number;
95
+ /** Document order within one DOM extract (tie-break when flatIndex ties). */
96
+ domSeq?: number;
94
97
  }
95
98
  /** Per-poll DOM extract stats — why rows were skipped (for missing-message debug). */
96
99
  export interface MessageExtractDebug {
@@ -247,5 +250,23 @@ export interface AgentHistory {
247
250
  updatedAt?: number;
248
251
  requestId?: string;
249
252
  totalMessages?: number;
253
+ /** Monotonic per agentId — stale snapshots should be ignored. */
254
+ seq?: number;
255
+ }
256
+ /** Per-agent chat lenta (JSONL). `state.messages` in state:patch is DOM/debug only. */
257
+ export interface AgentMessagesPayload {
258
+ agentId: string;
259
+ /** JSONL archive tail (history). When `append`, only new display rows since last emit. */
260
+ historyMessages: ChatMessage[];
261
+ /** Legacy field; always `[]` — lenta is JSONL-only. */
262
+ liveMessages: ChatMessage[];
263
+ /** Same as historyMessages (debug). */
264
+ messages: ChatMessage[];
265
+ totalMessages?: number;
266
+ source: 'dom' | 'jsonl' | 'hybrid';
267
+ updatedAt: number;
268
+ seq: number;
269
+ /** Live JSONL stream: merge `historyMessages` into client state (small payload). */
270
+ append?: boolean;
250
271
  }
251
272
  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": [