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.
- package/bridge-runtime/.env.example +10 -2
- package/bridge-runtime/connector-version.json +1 -1
- package/bridge-runtime/dist/agent-completion-push.d.ts +42 -0
- package/bridge-runtime/dist/agent-completion-push.js +220 -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 +94 -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/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 +1 -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/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 +2 -1
- package/bridge-runtime/dist/relay-upstream.js +4 -1
- package/bridge-runtime/dist/relay.d.ts +21 -0
- package/bridge-runtime/dist/relay.js +332 -28
- package/bridge-runtime/dist/types.d.ts +21 -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 +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
|
-
|
|
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({
|
|
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
|
|
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
|
-
|
|
148
|
-
const
|
|
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: '
|
|
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: '
|
|
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: '
|
|
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
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
|
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
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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*='
|
|
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": [
|