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,57 +4,113 @@ import { basename, join } from 'path';
|
|
|
4
4
|
import { createInterface } from 'readline';
|
|
5
5
|
import chokidar from 'chokidar';
|
|
6
6
|
import { resolveJsonlFilePath } from './agent-title-match.js';
|
|
7
|
-
import {
|
|
7
|
+
import { canonicalizeImagePaths } from './media-path.js';
|
|
8
|
+
import { cleanUserText, filterAssistantJsonlParts, isNoiseChatText, parseUserImagePaths, stripJsonlRedactionArtifacts, } from './message-filter.js';
|
|
8
9
|
import { bridgePipelineLog } from './history-pipeline-log.js';
|
|
10
|
+
const JSONL_POLL_MS = parseInt(process.env.JSONL_POLL_MS ?? '120', 10);
|
|
11
|
+
/** Debounce only for non–live-watched files (index rebuild path). */
|
|
12
|
+
const JSONL_EMIT_DEBOUNCE_MS = parseInt(process.env.JSONL_EMIT_DEBOUNCE_MS ?? '0', 10);
|
|
13
|
+
const JSONL_WRITE_STABILITY_MS = parseInt(process.env.JSONL_WRITE_STABILITY_MS ?? '50', 10);
|
|
9
14
|
export class JsonlIndex extends EventEmitter {
|
|
10
15
|
projectsDir;
|
|
11
16
|
debounceTimer = null;
|
|
12
17
|
subscribed = new Map();
|
|
13
18
|
mtimeByAgent = new Map();
|
|
14
19
|
pollTimer = null;
|
|
20
|
+
fileCache = new Map();
|
|
21
|
+
emitFileTimers = new Map();
|
|
22
|
+
liveWatcher = null;
|
|
23
|
+
liveWatchKey = '';
|
|
15
24
|
/** Skip poll `agent:history` while serving explicit `agents:history` (avoids relay race). */
|
|
16
25
|
historyReplyInFlight = new Set();
|
|
26
|
+
bridgeState = () => ({ tabs: [] });
|
|
17
27
|
constructor(projectsDir) {
|
|
18
28
|
super();
|
|
19
29
|
this.projectsDir = projectsDir;
|
|
20
30
|
}
|
|
31
|
+
setBridgeStateProvider(fn) {
|
|
32
|
+
this.bridgeState = fn;
|
|
33
|
+
}
|
|
34
|
+
resolveOpts(title) {
|
|
35
|
+
const state = this.bridgeState();
|
|
36
|
+
const activeTab = state.tabs?.find((t) => t.active);
|
|
37
|
+
return {
|
|
38
|
+
title,
|
|
39
|
+
composerIdByTitle: state.composerIdByTitle,
|
|
40
|
+
activeComposerId: state.activeComposerId,
|
|
41
|
+
activeTabTitle: activeTab?.title,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
21
44
|
start() {
|
|
22
45
|
void this.rebuild();
|
|
23
46
|
chokidar
|
|
24
47
|
.watch(this.projectsDir, {
|
|
25
48
|
ignoreInitial: true,
|
|
26
49
|
depth: 8,
|
|
27
|
-
awaitWriteFinish: {
|
|
50
|
+
awaitWriteFinish: {
|
|
51
|
+
stabilityThreshold: JSONL_WRITE_STABILITY_MS,
|
|
52
|
+
pollInterval: 50,
|
|
53
|
+
},
|
|
28
54
|
ignored: (p) => shouldIgnoreWatchPath(p),
|
|
29
55
|
})
|
|
30
56
|
.on('all', (_event, filePath) => {
|
|
31
57
|
if (!filePath || !filePath.endsWith('.jsonl'))
|
|
32
58
|
return;
|
|
33
59
|
this.scheduleRebuild();
|
|
34
|
-
|
|
60
|
+
this.scheduleEmitHistoryForFile(filePath);
|
|
35
61
|
});
|
|
36
|
-
this.pollTimer = setInterval(() => void this.pollSubscribed(),
|
|
62
|
+
this.pollTimer = setInterval(() => void this.pollSubscribed(), JSONL_POLL_MS);
|
|
37
63
|
}
|
|
38
64
|
stop() {
|
|
39
65
|
if (this.pollTimer)
|
|
40
66
|
clearInterval(this.pollTimer);
|
|
67
|
+
void this.liveWatcher?.close();
|
|
68
|
+
this.liveWatcher = null;
|
|
69
|
+
this.liveWatchKey = '';
|
|
41
70
|
}
|
|
42
|
-
subscribe(agentId, title) {
|
|
71
|
+
subscribe(agentId, title, opts) {
|
|
43
72
|
this.subscribed.set(agentId, { title });
|
|
44
|
-
|
|
73
|
+
this.refreshLiveWatcher();
|
|
74
|
+
if (opts?.emitHistory !== false) {
|
|
75
|
+
void this.emitHistoryForAgent(agentId, title);
|
|
76
|
+
}
|
|
45
77
|
}
|
|
46
78
|
unsubscribe(agentId) {
|
|
47
79
|
this.subscribed.delete(agentId);
|
|
48
80
|
this.mtimeByAgent.delete(agentId);
|
|
81
|
+
this.refreshLiveWatcher();
|
|
82
|
+
}
|
|
83
|
+
/** Immediate `change` on subscribed `.jsonl` (no awaitWriteFinish delay). */
|
|
84
|
+
refreshLiveWatcher() {
|
|
85
|
+
const paths = [];
|
|
86
|
+
for (const [agentId, meta] of this.subscribed) {
|
|
87
|
+
const p = resolveJsonlFilePath(this.projectsDir, agentId, this.resolveOpts(meta.title));
|
|
88
|
+
if (p)
|
|
89
|
+
paths.push(p);
|
|
90
|
+
}
|
|
91
|
+
const key = paths.sort().join('\0');
|
|
92
|
+
if (key === this.liveWatchKey)
|
|
93
|
+
return;
|
|
94
|
+
this.liveWatchKey = key;
|
|
95
|
+
void this.liveWatcher?.close();
|
|
96
|
+
this.liveWatcher = null;
|
|
97
|
+
if (!paths.length)
|
|
98
|
+
return;
|
|
99
|
+
this.liveWatcher = chokidar.watch(paths, { ignoreInitial: true, persistent: true });
|
|
100
|
+
const onLive = (filePath) => {
|
|
101
|
+
if (!filePath.endsWith('.jsonl'))
|
|
102
|
+
return;
|
|
103
|
+
void this.emitHistoryForFileNow(filePath);
|
|
104
|
+
};
|
|
105
|
+
this.liveWatcher.on('change', onLive);
|
|
106
|
+
this.liveWatcher.on('add', onLive);
|
|
49
107
|
}
|
|
50
108
|
getSubscribedAgents() {
|
|
51
109
|
return this.subscribed;
|
|
52
110
|
}
|
|
53
111
|
async pollSubscribed() {
|
|
54
112
|
for (const [agentId, meta] of this.subscribed) {
|
|
55
|
-
const filePath = resolveJsonlFilePath(this.projectsDir, agentId,
|
|
56
|
-
title: meta.title,
|
|
57
|
-
});
|
|
113
|
+
const filePath = resolveJsonlFilePath(this.projectsDir, agentId, this.resolveOpts(meta.title));
|
|
58
114
|
if (!filePath)
|
|
59
115
|
continue;
|
|
60
116
|
try {
|
|
@@ -63,7 +119,7 @@ export class JsonlIndex extends EventEmitter {
|
|
|
63
119
|
if (prev === mtime)
|
|
64
120
|
continue;
|
|
65
121
|
this.mtimeByAgent.set(agentId, mtime);
|
|
66
|
-
|
|
122
|
+
void this.emitHistoryForFileNow(filePath, agentId);
|
|
67
123
|
}
|
|
68
124
|
catch {
|
|
69
125
|
/* skip */
|
|
@@ -71,9 +127,14 @@ export class JsonlIndex extends EventEmitter {
|
|
|
71
127
|
}
|
|
72
128
|
}
|
|
73
129
|
async emitHistoryForAgent(agentId, title) {
|
|
74
|
-
const filePath = resolveJsonlFilePath(this.projectsDir, agentId,
|
|
130
|
+
const filePath = resolveJsonlFilePath(this.projectsDir, agentId, this.resolveOpts(title));
|
|
75
131
|
if (!filePath) {
|
|
76
|
-
this.emit('agent:history', {
|
|
132
|
+
this.emit('agent:history', {
|
|
133
|
+
agentId,
|
|
134
|
+
messages: [],
|
|
135
|
+
totalMessages: 0,
|
|
136
|
+
updatedAt: Date.now(),
|
|
137
|
+
});
|
|
77
138
|
return;
|
|
78
139
|
}
|
|
79
140
|
try {
|
|
@@ -82,26 +143,101 @@ export class JsonlIndex extends EventEmitter {
|
|
|
82
143
|
catch {
|
|
83
144
|
/* ignore */
|
|
84
145
|
}
|
|
85
|
-
await this.
|
|
146
|
+
await this.emitHistoryForFileNow(filePath, agentId);
|
|
86
147
|
}
|
|
87
|
-
|
|
148
|
+
scheduleEmitHistoryForFile(filePath, replyAgentId) {
|
|
149
|
+
if (this.isLiveWatchedFile(filePath)) {
|
|
150
|
+
void this.emitHistoryForFileNow(filePath, replyAgentId);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (JSONL_EMIT_DEBOUNCE_MS <= 0) {
|
|
154
|
+
void this.emitHistoryForFileNow(filePath, replyAgentId);
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
const key = `${filePath}\0${replyAgentId ?? ''}`;
|
|
158
|
+
const prev = this.emitFileTimers.get(key);
|
|
159
|
+
if (prev)
|
|
160
|
+
clearTimeout(prev);
|
|
161
|
+
this.emitFileTimers.set(key, setTimeout(() => {
|
|
162
|
+
this.emitFileTimers.delete(key);
|
|
163
|
+
void this.emitHistoryForFileNow(filePath, replyAgentId);
|
|
164
|
+
}, JSONL_EMIT_DEBOUNCE_MS));
|
|
165
|
+
}
|
|
166
|
+
isLiveWatchedFile(filePath) {
|
|
167
|
+
if (!this.liveWatchKey)
|
|
168
|
+
return false;
|
|
169
|
+
return this.liveWatchKey.split('\0').includes(filePath);
|
|
170
|
+
}
|
|
171
|
+
async loadMessagesForFile(filePath) {
|
|
172
|
+
const st = statSync(filePath);
|
|
173
|
+
const cached = this.fileCache.get(filePath);
|
|
174
|
+
if (cached && cached.size === st.size)
|
|
175
|
+
return cached.messages;
|
|
176
|
+
if (cached && st.size > cached.size) {
|
|
177
|
+
const tail = await parseJsonlTail(filePath, cached.lineNo);
|
|
178
|
+
const messages = [...cached.messages, ...tail.appended];
|
|
179
|
+
this.fileCache.set(filePath, {
|
|
180
|
+
lineNo: tail.lineNo,
|
|
181
|
+
messages,
|
|
182
|
+
size: st.size,
|
|
183
|
+
});
|
|
184
|
+
return messages;
|
|
185
|
+
}
|
|
186
|
+
const full = await parseJsonlFileFull(filePath);
|
|
187
|
+
this.fileCache.set(filePath, {
|
|
188
|
+
lineNo: full.lineNo,
|
|
189
|
+
messages: full.messages,
|
|
190
|
+
size: st.size,
|
|
191
|
+
});
|
|
192
|
+
return full.messages;
|
|
193
|
+
}
|
|
194
|
+
async emitHistoryForFileNow(filePath, replyAgentId) {
|
|
88
195
|
const fileAgentId = basename(filePath, '.jsonl');
|
|
89
196
|
if (!fileAgentId || fileAgentId.includes('/'))
|
|
90
197
|
return;
|
|
91
198
|
const agentId = replyAgentId ?? fileAgentId;
|
|
92
|
-
if (this.historyReplyInFlight.has(agentId) || this.historyReplyInFlight.has(fileAgentId)) {
|
|
93
|
-
bridgePipelineLog({
|
|
94
|
-
dir: 'internal',
|
|
95
|
-
event: 'poll:agent:history:SKIP',
|
|
96
|
-
agentId,
|
|
97
|
-
detail: `inFlight file=${fileAgentId.slice(0, 8)}`,
|
|
98
|
-
});
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
199
|
try {
|
|
102
|
-
const messages = await
|
|
200
|
+
const messages = await this.loadMessagesForFile(filePath);
|
|
201
|
+
const rawLineNo = this.fileCache.get(filePath)?.lineNo ?? messages.length;
|
|
103
202
|
this.mtimeByAgent.set(agentId, statSync(filePath).mtimeMs);
|
|
104
|
-
this.emit('agent:
|
|
203
|
+
this.emit('agent:jsonl:updated', {
|
|
204
|
+
agentId: fileAgentId,
|
|
205
|
+
messages,
|
|
206
|
+
totalMessages: rawLineNo,
|
|
207
|
+
updatedAt: Date.now(),
|
|
208
|
+
});
|
|
209
|
+
const replyIds = new Set([agentId]);
|
|
210
|
+
for (const [subId, meta] of this.subscribed) {
|
|
211
|
+
if (subId === fileAgentId)
|
|
212
|
+
continue;
|
|
213
|
+
const subPath = resolveJsonlFilePath(this.projectsDir, subId, this.resolveOpts(meta.title));
|
|
214
|
+
if (subPath === filePath)
|
|
215
|
+
replyIds.add(subId);
|
|
216
|
+
}
|
|
217
|
+
const hasSubscriber = [...replyIds].some((id) => this.subscribed.has(id));
|
|
218
|
+
if (!hasSubscriber)
|
|
219
|
+
return;
|
|
220
|
+
if (this.historyReplyInFlight.has(agentId) ||
|
|
221
|
+
this.historyReplyInFlight.has(fileAgentId) ||
|
|
222
|
+
[...replyIds].some((id) => this.historyReplyInFlight.has(id))) {
|
|
223
|
+
bridgePipelineLog({
|
|
224
|
+
dir: 'internal',
|
|
225
|
+
event: 'poll:agent:history:SKIP',
|
|
226
|
+
agentId,
|
|
227
|
+
detail: `inFlight file=${fileAgentId.slice(0, 8)}`,
|
|
228
|
+
});
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
for (const replyId of replyIds) {
|
|
232
|
+
if (!this.subscribed.has(replyId))
|
|
233
|
+
continue;
|
|
234
|
+
this.emit('agent:history', {
|
|
235
|
+
agentId: replyId,
|
|
236
|
+
messages,
|
|
237
|
+
totalMessages: rawLineNo,
|
|
238
|
+
updatedAt: Date.now(),
|
|
239
|
+
});
|
|
240
|
+
}
|
|
105
241
|
}
|
|
106
242
|
catch {
|
|
107
243
|
/* skip */
|
|
@@ -159,16 +295,26 @@ export class JsonlIndex extends EventEmitter {
|
|
|
159
295
|
return filePath.slice(0, idx);
|
|
160
296
|
}
|
|
161
297
|
async loadHistory(agentId, opts) {
|
|
162
|
-
const filePath = resolveJsonlFilePath(this.projectsDir, agentId,
|
|
298
|
+
const filePath = resolveJsonlFilePath(this.projectsDir, agentId, {
|
|
299
|
+
...this.resolveOpts(opts?.title),
|
|
300
|
+
...opts,
|
|
301
|
+
});
|
|
163
302
|
if (!filePath) {
|
|
164
303
|
return { agentId, messages: [], totalMessages: 0 };
|
|
165
304
|
}
|
|
166
|
-
const messages = await
|
|
305
|
+
const messages = await this.loadMessagesForFile(filePath);
|
|
167
306
|
const totalMessages = messages.length;
|
|
168
307
|
const limit = opts?.limit;
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
308
|
+
const offset = Math.max(0, opts?.offset ?? 0);
|
|
309
|
+
let trimmed = messages;
|
|
310
|
+
if (limit && limit > 0) {
|
|
311
|
+
const end = offset > 0 ? -offset : undefined;
|
|
312
|
+
const start = -(offset + limit);
|
|
313
|
+
trimmed = messages.slice(start, end);
|
|
314
|
+
}
|
|
315
|
+
else if (offset > 0) {
|
|
316
|
+
trimmed = messages.slice(0, -offset);
|
|
317
|
+
}
|
|
172
318
|
return { agentId, messages: trimmed, totalMessages };
|
|
173
319
|
}
|
|
174
320
|
}
|
|
@@ -285,54 +431,72 @@ function walkForAgent(dir, agentId, depth = 0) {
|
|
|
285
431
|
}
|
|
286
432
|
return null;
|
|
287
433
|
}
|
|
288
|
-
|
|
434
|
+
function parseJsonlLine(line, lineNo) {
|
|
435
|
+
if (!line.trim())
|
|
436
|
+
return null;
|
|
437
|
+
try {
|
|
438
|
+
const row = JSON.parse(line);
|
|
439
|
+
const parts = [];
|
|
440
|
+
const rawParts = [];
|
|
441
|
+
for (const c of row.message?.content ?? []) {
|
|
442
|
+
if (c.type === 'text' && c.text) {
|
|
443
|
+
rawParts.push(c.text);
|
|
444
|
+
const t = cleanUserText(c.text);
|
|
445
|
+
if (t)
|
|
446
|
+
parts.push(t);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
if (row.role === 'user') {
|
|
450
|
+
const raw = rawParts.join('\n');
|
|
451
|
+
const images = canonicalizeImagePaths(parseUserImagePaths(raw));
|
|
452
|
+
const text = stripJsonlRedactionArtifacts(cleanUserText(parts.join('\n').trim()));
|
|
453
|
+
const hasImages = images.length > 0;
|
|
454
|
+
if ((!text || isNoiseChatText(text)) && !hasImages)
|
|
455
|
+
return null;
|
|
456
|
+
if (text && isNoiseChatText(text) && !hasImages)
|
|
457
|
+
return null;
|
|
458
|
+
return {
|
|
459
|
+
role: 'user',
|
|
460
|
+
text: text || (hasImages ? 'Изображение' : ''),
|
|
461
|
+
images: hasImages ? images : undefined,
|
|
462
|
+
ts: lineNo,
|
|
463
|
+
};
|
|
464
|
+
}
|
|
465
|
+
if (row.role === 'assistant') {
|
|
466
|
+
const filtered = filterAssistantJsonlParts(parts);
|
|
467
|
+
const text = filtered.join('\n').trim();
|
|
468
|
+
if (text)
|
|
469
|
+
return { role: 'assistant', text, ts: lineNo };
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
catch {
|
|
473
|
+
/* skip bad line */
|
|
474
|
+
}
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
async function parseJsonlFileFull(filePath) {
|
|
289
478
|
const messages = [];
|
|
290
479
|
const rl = createInterface({ input: createReadStream(filePath), crlfDelay: Infinity });
|
|
291
480
|
let lineNo = 0;
|
|
292
481
|
for await (const line of rl) {
|
|
293
482
|
lineNo++;
|
|
294
|
-
|
|
483
|
+
const m = parseJsonlLine(line, lineNo);
|
|
484
|
+
if (m)
|
|
485
|
+
messages.push(m);
|
|
486
|
+
}
|
|
487
|
+
return { lineNo, messages };
|
|
488
|
+
}
|
|
489
|
+
async function parseJsonlTail(filePath, afterLineNo) {
|
|
490
|
+
const appended = [];
|
|
491
|
+
const rl = createInterface({ input: createReadStream(filePath), crlfDelay: Infinity });
|
|
492
|
+
let lineNo = 0;
|
|
493
|
+
for await (const line of rl) {
|
|
494
|
+
lineNo++;
|
|
495
|
+
if (lineNo <= afterLineNo)
|
|
295
496
|
continue;
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
const rawParts = [];
|
|
300
|
-
for (const c of row.message?.content ?? []) {
|
|
301
|
-
if (c.type === 'text' && c.text) {
|
|
302
|
-
rawParts.push(c.text);
|
|
303
|
-
const t = cleanUserText(c.text);
|
|
304
|
-
if (t)
|
|
305
|
-
parts.push(t);
|
|
306
|
-
}
|
|
307
|
-
// tool_use, tool_result — ignore
|
|
308
|
-
}
|
|
309
|
-
if (row.role === 'user') {
|
|
310
|
-
const raw = rawParts.join('\n');
|
|
311
|
-
const images = parseUserImagePaths(raw);
|
|
312
|
-
const text = stripJsonlRedactionArtifacts(cleanUserText(parts.join('\n').trim()));
|
|
313
|
-
const hasImages = images.length > 0;
|
|
314
|
-
if ((!text || isNoiseChatText(text)) && !hasImages)
|
|
315
|
-
continue;
|
|
316
|
-
if (text && isNoiseChatText(text) && !hasImages)
|
|
317
|
-
continue;
|
|
318
|
-
messages.push({
|
|
319
|
-
role: 'user',
|
|
320
|
-
text: text || (hasImages ? 'Изображение' : ''),
|
|
321
|
-
images: hasImages ? images : undefined,
|
|
322
|
-
ts: lineNo,
|
|
323
|
-
});
|
|
324
|
-
continue;
|
|
325
|
-
}
|
|
326
|
-
if (row.role === 'assistant') {
|
|
327
|
-
const filtered = filterAssistantJsonlParts(parts);
|
|
328
|
-
const text = filtered.join('\n').trim();
|
|
329
|
-
if (text)
|
|
330
|
-
messages.push({ role: 'assistant', text, ts: lineNo });
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
catch {
|
|
334
|
-
/* skip bad line */
|
|
335
|
-
}
|
|
497
|
+
const m = parseJsonlLine(line, lineNo);
|
|
498
|
+
if (m)
|
|
499
|
+
appended.push(m);
|
|
336
500
|
}
|
|
337
|
-
return
|
|
501
|
+
return { lineNo, appended };
|
|
338
502
|
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export type JsonlLiveRow = {
|
|
2
|
+
lineNo: number;
|
|
3
|
+
role: string;
|
|
4
|
+
textPreview: string;
|
|
5
|
+
textLen: number;
|
|
6
|
+
tools: string[];
|
|
7
|
+
/** Попадёт в ленту app после filter + collapse (отдельная строка до collapse). */
|
|
8
|
+
inLenta: boolean;
|
|
9
|
+
skipReason?: string;
|
|
10
|
+
};
|
|
11
|
+
export type JsonlLiveSnapshot = {
|
|
12
|
+
agentId: string;
|
|
13
|
+
filePath: string | null;
|
|
14
|
+
fileSize: number;
|
|
15
|
+
totalLines: number;
|
|
16
|
+
updatedAt: number;
|
|
17
|
+
rows: JsonlLiveRow[];
|
|
18
|
+
};
|
|
19
|
+
/** Read JSONL file rows for debug live view (`afterLine` exclusive, or tail when afterLine=0). */
|
|
20
|
+
export declare function readJsonlLiveSnapshot(filePath: string, agentId: string, opts?: {
|
|
21
|
+
afterLine?: number;
|
|
22
|
+
tail?: number;
|
|
23
|
+
maxNew?: number;
|
|
24
|
+
}): Promise<JsonlLiveSnapshot>;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { createReadStream } from 'fs';
|
|
2
|
+
import { statSync } from 'fs';
|
|
3
|
+
import { createInterface } from 'readline';
|
|
4
|
+
import { cleanUserText, filterAssistantJsonlParts, isAssistantReflectionText, isMeaningfulAssistantText, isNoiseChatText, parseUserImagePaths, stripJsonlRedactionArtifacts, } from './message-filter.js';
|
|
5
|
+
import { canonicalizeImagePaths } from './media-path.js';
|
|
6
|
+
function classifyJsonlLine(line, lineNo) {
|
|
7
|
+
if (!line.trim()) {
|
|
8
|
+
return {
|
|
9
|
+
lineNo,
|
|
10
|
+
role: '—',
|
|
11
|
+
textPreview: '',
|
|
12
|
+
textLen: 0,
|
|
13
|
+
tools: [],
|
|
14
|
+
inLenta: false,
|
|
15
|
+
skipReason: 'blank',
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
try {
|
|
19
|
+
const row = JSON.parse(line);
|
|
20
|
+
const role = row.role ?? '?';
|
|
21
|
+
const tools = [];
|
|
22
|
+
const parts = [];
|
|
23
|
+
const rawParts = [];
|
|
24
|
+
for (const c of row.message?.content ?? []) {
|
|
25
|
+
if (c.type === 'tool_use' && c.name)
|
|
26
|
+
tools.push(String(c.name));
|
|
27
|
+
if (c.type === 'text' && c.text) {
|
|
28
|
+
rawParts.push(c.text);
|
|
29
|
+
const t = cleanUserText(c.text);
|
|
30
|
+
if (t)
|
|
31
|
+
parts.push(t);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
const rawJoined = rawParts.join('\n').trim();
|
|
35
|
+
const preview = rawJoined.slice(0, 600);
|
|
36
|
+
if (role === 'user') {
|
|
37
|
+
const images = canonicalizeImagePaths(parseUserImagePaths(rawJoined));
|
|
38
|
+
const text = stripJsonlRedactionArtifacts(cleanUserText(parts.join('\n').trim()));
|
|
39
|
+
const hasImages = images.length > 0;
|
|
40
|
+
if ((!text || isNoiseChatText(text)) && !hasImages) {
|
|
41
|
+
return {
|
|
42
|
+
lineNo,
|
|
43
|
+
role,
|
|
44
|
+
textPreview: preview || '(noise)',
|
|
45
|
+
textLen: rawJoined.length,
|
|
46
|
+
tools,
|
|
47
|
+
inLenta: false,
|
|
48
|
+
skipReason: 'user-noise',
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
return {
|
|
52
|
+
lineNo,
|
|
53
|
+
role,
|
|
54
|
+
textPreview: text || (hasImages ? '[images]' : preview),
|
|
55
|
+
textLen: rawJoined.length,
|
|
56
|
+
tools,
|
|
57
|
+
inLenta: true,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
if (role === 'assistant') {
|
|
61
|
+
if (!parts.length && tools.length) {
|
|
62
|
+
return {
|
|
63
|
+
lineNo,
|
|
64
|
+
role,
|
|
65
|
+
textPreview: preview || '[REDACTED]',
|
|
66
|
+
textLen: rawJoined.length,
|
|
67
|
+
tools,
|
|
68
|
+
inLenta: false,
|
|
69
|
+
skipReason: 'tool-only',
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
const filtered = filterAssistantJsonlParts(parts);
|
|
73
|
+
const text = filtered.join('\n').trim();
|
|
74
|
+
if (!text) {
|
|
75
|
+
let skipReason = 'empty-parts';
|
|
76
|
+
for (const p of parts) {
|
|
77
|
+
if (isAssistantReflectionText(p))
|
|
78
|
+
skipReason = 'reflection';
|
|
79
|
+
else if (isNoiseChatText(p))
|
|
80
|
+
skipReason = 'noise';
|
|
81
|
+
else if (!isMeaningfulAssistantText(p))
|
|
82
|
+
skipReason = 'not-meaningful';
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
lineNo,
|
|
86
|
+
role,
|
|
87
|
+
textPreview: preview || '[REDACTED]',
|
|
88
|
+
textLen: rawJoined.length,
|
|
89
|
+
tools,
|
|
90
|
+
inLenta: false,
|
|
91
|
+
skipReason,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
return {
|
|
95
|
+
lineNo,
|
|
96
|
+
role,
|
|
97
|
+
textPreview: text.slice(0, 600),
|
|
98
|
+
textLen: rawJoined.length,
|
|
99
|
+
tools,
|
|
100
|
+
inLenta: true,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
lineNo,
|
|
105
|
+
role,
|
|
106
|
+
textPreview: preview,
|
|
107
|
+
textLen: rawJoined.length,
|
|
108
|
+
tools,
|
|
109
|
+
inLenta: false,
|
|
110
|
+
skipReason: 'unknown-role',
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return {
|
|
115
|
+
lineNo,
|
|
116
|
+
role: '?',
|
|
117
|
+
textPreview: line.slice(0, 120),
|
|
118
|
+
textLen: line.length,
|
|
119
|
+
tools: [],
|
|
120
|
+
inLenta: false,
|
|
121
|
+
skipReason: 'json-error',
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/** Read JSONL file rows for debug live view (`afterLine` exclusive, or tail when afterLine=0). */
|
|
126
|
+
export async function readJsonlLiveSnapshot(filePath, agentId, opts) {
|
|
127
|
+
const afterLine = Math.max(0, opts?.afterLine ?? 0);
|
|
128
|
+
const tail = Math.min(500, Math.max(0, opts?.tail ?? 0));
|
|
129
|
+
const maxNew = Math.min(128, Math.max(1, opts?.maxNew ?? 64));
|
|
130
|
+
let fileSize = 0;
|
|
131
|
+
let totalLines = 0;
|
|
132
|
+
try {
|
|
133
|
+
fileSize = statSync(filePath).size;
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
return {
|
|
137
|
+
agentId,
|
|
138
|
+
filePath: null,
|
|
139
|
+
fileSize: 0,
|
|
140
|
+
totalLines: 0,
|
|
141
|
+
updatedAt: Date.now(),
|
|
142
|
+
rows: [],
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
const buffered = [];
|
|
146
|
+
const rows = [];
|
|
147
|
+
const rl = createInterface({ input: createReadStream(filePath), crlfDelay: Infinity });
|
|
148
|
+
for await (const line of rl) {
|
|
149
|
+
totalLines++;
|
|
150
|
+
if (afterLine > 0) {
|
|
151
|
+
if (totalLines <= afterLine)
|
|
152
|
+
continue;
|
|
153
|
+
rows.push(classifyJsonlLine(line, totalLines));
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
buffered.push(classifyJsonlLine(line, totalLines));
|
|
157
|
+
}
|
|
158
|
+
if (afterLine === 0) {
|
|
159
|
+
if (tail > 0)
|
|
160
|
+
rows.push(...buffered.slice(-tail));
|
|
161
|
+
else
|
|
162
|
+
rows.push(...buffered.slice(-maxNew));
|
|
163
|
+
}
|
|
164
|
+
else if (rows.length > maxNew) {
|
|
165
|
+
rows.splice(0, rows.length - maxNew);
|
|
166
|
+
}
|
|
167
|
+
return {
|
|
168
|
+
agentId,
|
|
169
|
+
filePath,
|
|
170
|
+
fileSize,
|
|
171
|
+
totalLines,
|
|
172
|
+
updatedAt: Date.now(),
|
|
173
|
+
rows,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
/** `vscode-file://` / `file://` → absolute path on disk (or http URL). */
|
|
2
2
|
export declare function normalizeMediaRef(src: string): string | null;
|
|
3
|
+
/** One path per file for merge/dedupe (JSONL absolute vs DOM vscode-file://). */
|
|
4
|
+
export declare function canonicalizeImagePaths(paths: Iterable<string>): string[];
|
|
3
5
|
/** Resolve query `path` from client (absolute path or vscode-file URL). */
|
|
4
6
|
export declare function resolveMediaPathParam(rawPath: string): string | null;
|
|
5
7
|
export declare function isAllowedMediaPath(filePath: string): boolean;
|
|
@@ -21,6 +21,23 @@ export function normalizeMediaRef(src) {
|
|
|
21
21
|
return path.normalize(fsPath);
|
|
22
22
|
return null;
|
|
23
23
|
}
|
|
24
|
+
/** One path per file for merge/dedupe (JSONL absolute vs DOM vscode-file://). */
|
|
25
|
+
export function canonicalizeImagePaths(paths) {
|
|
26
|
+
const out = [];
|
|
27
|
+
const seen = new Set();
|
|
28
|
+
for (const raw of paths) {
|
|
29
|
+
const trimmed = raw.trim();
|
|
30
|
+
if (!trimmed)
|
|
31
|
+
continue;
|
|
32
|
+
const canon = normalizeMediaRef(trimmed) ??
|
|
33
|
+
(path.isAbsolute(trimmed) ? path.normalize(trimmed) : trimmed);
|
|
34
|
+
if (seen.has(canon))
|
|
35
|
+
continue;
|
|
36
|
+
seen.add(canon);
|
|
37
|
+
out.push(canon);
|
|
38
|
+
}
|
|
39
|
+
return out;
|
|
40
|
+
}
|
|
24
41
|
/** Resolve query `path` from client (absolute path or vscode-file URL). */
|
|
25
42
|
export function resolveMediaPathParam(rawPath) {
|
|
26
43
|
const trimmed = rawPath.trim();
|
|
@@ -16,6 +16,8 @@ export declare function textImpliesAgentWorking(text: string): boolean;
|
|
|
16
16
|
/** Cursor JSONL redacts tool/thinking tails as `[REDACTED]` — not shown in live DOM. */
|
|
17
17
|
export declare function stripJsonlRedactionArtifacts(text: string): string;
|
|
18
18
|
export declare function isNoiseChatText(text: string): boolean;
|
|
19
|
+
/** Markdown / structured answer — must stay in chat lenta even if it mentions bridge symbols. */
|
|
20
|
+
export declare function isUserFacingAssistantProse(text: string): boolean;
|
|
19
21
|
/** Chain-of-thought / tool trace rows — not user-facing answers (DOM + JSONL). */
|
|
20
22
|
export declare function isAssistantReflectionText(text: string): boolean;
|
|
21
23
|
/** Short agent status worth showing (not tool log). */
|