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.
- package/bridge-runtime/.env.example +14 -2
- package/bridge-runtime/connector-version.json +1 -1
- package/bridge-runtime/dist/agent-completion-push.d.ts +35 -0
- package/bridge-runtime/dist/agent-completion-push.js +195 -0
- package/bridge-runtime/dist/agent-title-match.d.ts +8 -7
- package/bridge-runtime/dist/agent-title-match.js +11 -1
- package/bridge-runtime/dist/chat-display-store.d.ts +21 -9
- package/bridge-runtime/dist/chat-display-store.js +97 -23
- package/bridge-runtime/dist/chat-display.d.ts +2 -0
- package/bridge-runtime/dist/chat-display.js +197 -33
- package/bridge-runtime/dist/chat-history-mode.d.ts +5 -0
- package/bridge-runtime/dist/chat-history-mode.js +7 -0
- package/bridge-runtime/dist/command-executor.d.ts +2 -0
- package/bridge-runtime/dist/command-executor.js +44 -0
- package/bridge-runtime/dist/composer-title-index.d.ts +1 -0
- package/bridge-runtime/dist/composer-title-index.js +7 -7
- package/bridge-runtime/dist/config.js +2 -0
- package/bridge-runtime/dist/connector-client-version.js +1 -1
- package/bridge-runtime/dist/debug-chats-page.d.ts +2 -0
- package/bridge-runtime/dist/debug-chats-page.js +491 -0
- package/bridge-runtime/dist/dom-transcript-store.d.ts +17 -0
- package/bridge-runtime/dist/dom-transcript-store.js +76 -0
- package/bridge-runtime/dist/extract-page.js +56 -85
- package/bridge-runtime/dist/history-limit.d.ts +2 -0
- package/bridge-runtime/dist/history-limit.js +2 -0
- package/bridge-runtime/dist/history-request.d.ts +8 -0
- package/bridge-runtime/dist/history-request.js +7 -0
- package/bridge-runtime/dist/index.js +10 -0
- package/bridge-runtime/dist/jsonl-index.d.ts +21 -3
- package/bridge-runtime/dist/jsonl-index.js +237 -73
- package/bridge-runtime/dist/jsonl-live-debug.d.ts +24 -0
- package/bridge-runtime/dist/jsonl-live-debug.js +175 -0
- package/bridge-runtime/dist/keep-awake.d.ts +5 -0
- package/bridge-runtime/dist/keep-awake.js +48 -0
- package/bridge-runtime/dist/media-path.d.ts +2 -0
- package/bridge-runtime/dist/media-path.js +17 -0
- package/bridge-runtime/dist/message-filter.d.ts +2 -0
- package/bridge-runtime/dist/message-filter.js +21 -5
- package/bridge-runtime/dist/pairing-code.d.ts +2 -0
- package/bridge-runtime/dist/pairing-code.js +9 -2
- package/bridge-runtime/dist/relay-upstream.d.ts +5 -1
- package/bridge-runtime/dist/relay-upstream.js +25 -1
- package/bridge-runtime/dist/relay.d.ts +31 -0
- package/bridge-runtime/dist/relay.js +401 -32
- package/bridge-runtime/dist/types.d.ts +25 -0
- package/bridge-runtime/selectors.json +4 -5
- package/dist/index.js +79 -20
- package/dist/launch.js +23 -5
- package/dist/macos-autostart.js +87 -0
- package/dist/pairing-code.js +12 -3
- package/dist/print-pairing.js +2 -0
- package/dist/run-service.js +31 -0
- package/dist/startup-check.js +165 -0
- package/package.json +1 -1
- 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 {
|
|
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({
|
|
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
|
|
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
|
|
405
|
+
const full = await this.jsonlIndex.loadHistory(agentId, {
|
|
143
406
|
title,
|
|
144
407
|
composerIdByTitle: this.stateManager.getState().composerIdByTitle,
|
|
145
|
-
|
|
408
|
+
offset,
|
|
146
409
|
});
|
|
147
|
-
|
|
148
|
-
const
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
|
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
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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 =
|
|
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
|
|
864
|
+
const full = await this.jsonlIndex.loadHistory(agentId, {
|
|
545
865
|
title,
|
|
546
866
|
composerIdByTitle: this.stateManager.getState().composerIdByTitle,
|
|
547
|
-
|
|
867
|
+
offset,
|
|
548
868
|
});
|
|
549
|
-
|
|
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
|
-
|
|
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.
|
|
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*='
|
|
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", "
|
|
54
|
+
"textMatch": ["Accept", "Approve", "Allow", "Accept All"]
|
|
56
55
|
},
|
|
57
56
|
"rejectButton": {
|
|
58
57
|
"strategies": [
|