cursorconnect 0.1.5 → 0.1.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/bridge-runtime/.env.example +10 -2
  2. package/bridge-runtime/connector-version.json +1 -0
  3. package/bridge-runtime/dist/agent-completion-push.d.ts +42 -0
  4. package/bridge-runtime/dist/agent-completion-push.js +220 -0
  5. package/bridge-runtime/dist/agent-title-match.d.ts +8 -7
  6. package/bridge-runtime/dist/agent-title-match.js +11 -1
  7. package/bridge-runtime/dist/chat-display-store.d.ts +21 -9
  8. package/bridge-runtime/dist/chat-display-store.js +94 -23
  9. package/bridge-runtime/dist/chat-display.d.ts +2 -0
  10. package/bridge-runtime/dist/chat-display.js +197 -33
  11. package/bridge-runtime/dist/chat-history-mode.d.ts +5 -0
  12. package/bridge-runtime/dist/chat-history-mode.js +7 -0
  13. package/bridge-runtime/dist/command-executor.d.ts +2 -0
  14. package/bridge-runtime/dist/command-executor.js +44 -0
  15. package/bridge-runtime/dist/composer-title-index.d.ts +1 -0
  16. package/bridge-runtime/dist/composer-title-index.js +7 -7
  17. package/bridge-runtime/dist/connector-client-version.d.ts +2 -0
  18. package/bridge-runtime/dist/connector-client-version.js +43 -0
  19. package/bridge-runtime/dist/debug-chats-page.d.ts +2 -0
  20. package/bridge-runtime/dist/debug-chats-page.js +491 -0
  21. package/bridge-runtime/dist/dom-transcript-store.d.ts +17 -0
  22. package/bridge-runtime/dist/dom-transcript-store.js +76 -0
  23. package/bridge-runtime/dist/extract-page.js +56 -85
  24. package/bridge-runtime/dist/history-limit.d.ts +2 -0
  25. package/bridge-runtime/dist/history-limit.js +2 -0
  26. package/bridge-runtime/dist/history-request.d.ts +8 -0
  27. package/bridge-runtime/dist/history-request.js +7 -0
  28. package/bridge-runtime/dist/index.js +4 -0
  29. package/bridge-runtime/dist/jsonl-index.d.ts +21 -3
  30. package/bridge-runtime/dist/jsonl-index.js +237 -73
  31. package/bridge-runtime/dist/jsonl-live-debug.d.ts +24 -0
  32. package/bridge-runtime/dist/jsonl-live-debug.js +175 -0
  33. package/bridge-runtime/dist/media-path.d.ts +2 -0
  34. package/bridge-runtime/dist/media-path.js +17 -0
  35. package/bridge-runtime/dist/message-filter.d.ts +2 -0
  36. package/bridge-runtime/dist/message-filter.js +21 -5
  37. package/bridge-runtime/dist/pairing-code.d.ts +2 -0
  38. package/bridge-runtime/dist/pairing-code.js +9 -2
  39. package/bridge-runtime/dist/relay-upstream.d.ts +2 -1
  40. package/bridge-runtime/dist/relay-upstream.js +13 -2
  41. package/bridge-runtime/dist/relay.d.ts +21 -0
  42. package/bridge-runtime/dist/relay.js +332 -28
  43. package/bridge-runtime/dist/types.d.ts +21 -0
  44. package/bridge-runtime/selectors.json +4 -5
  45. package/dist/bundled-bridge-check.js +25 -0
  46. package/dist/index.js +87 -10
  47. package/dist/launch.js +47 -0
  48. package/dist/macos-autostart.js +87 -0
  49. package/dist/pairing-code.js +12 -3
  50. package/dist/print-pairing.js +2 -0
  51. package/dist/run-service.js +31 -0
  52. package/dist/startup-check.js +165 -0
  53. package/package.json +1 -1
  54. 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 { cleanUserText, parseUserImagePaths, filterAssistantJsonlParts, isNoiseChatText, stripJsonlRedactionArtifacts, } from './message-filter.js';
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: { stabilityThreshold: 200, pollInterval: 100 },
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
- void this.emitHistoryForFile(filePath);
60
+ this.scheduleEmitHistoryForFile(filePath);
35
61
  });
36
- this.pollTimer = setInterval(() => void this.pollSubscribed(), 700);
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
- void this.emitHistoryForAgent(agentId, title);
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
- await this.emitHistoryForFile(filePath, agentId);
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, { title });
130
+ const filePath = resolveJsonlFilePath(this.projectsDir, agentId, this.resolveOpts(title));
75
131
  if (!filePath) {
76
- this.emit('agent:history', { agentId, messages: [], updatedAt: Date.now() });
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.emitHistoryForFile(filePath, agentId);
146
+ await this.emitHistoryForFileNow(filePath, agentId);
86
147
  }
87
- async emitHistoryForFile(filePath, replyAgentId) {
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 parseJsonlFile(filePath);
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:history', { agentId, messages, updatedAt: Date.now() });
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, opts);
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 parseJsonlFile(filePath);
305
+ const messages = await this.loadMessagesForFile(filePath);
167
306
  const totalMessages = messages.length;
168
307
  const limit = opts?.limit;
169
- const trimmed = limit && limit > 0 && messages.length > limit
170
- ? messages.slice(-limit)
171
- : messages;
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
- async function parseJsonlFile(filePath) {
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
- if (!line.trim())
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
- try {
297
- const row = JSON.parse(line);
298
- const parts = [];
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 messages;
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). */