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.
Files changed (55) hide show
  1. package/bridge-runtime/.env.example +14 -2
  2. package/bridge-runtime/connector-version.json +1 -1
  3. package/bridge-runtime/dist/agent-completion-push.d.ts +35 -0
  4. package/bridge-runtime/dist/agent-completion-push.js +195 -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 +97 -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/config.js +2 -0
  18. package/bridge-runtime/dist/connector-client-version.js +1 -1
  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 +10 -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/keep-awake.d.ts +5 -0
  34. package/bridge-runtime/dist/keep-awake.js +48 -0
  35. package/bridge-runtime/dist/media-path.d.ts +2 -0
  36. package/bridge-runtime/dist/media-path.js +17 -0
  37. package/bridge-runtime/dist/message-filter.d.ts +2 -0
  38. package/bridge-runtime/dist/message-filter.js +21 -5
  39. package/bridge-runtime/dist/pairing-code.d.ts +2 -0
  40. package/bridge-runtime/dist/pairing-code.js +9 -2
  41. package/bridge-runtime/dist/relay-upstream.d.ts +5 -1
  42. package/bridge-runtime/dist/relay-upstream.js +25 -1
  43. package/bridge-runtime/dist/relay.d.ts +31 -0
  44. package/bridge-runtime/dist/relay.js +401 -32
  45. package/bridge-runtime/dist/types.d.ts +25 -0
  46. package/bridge-runtime/selectors.json +4 -5
  47. package/dist/index.js +79 -20
  48. package/dist/launch.js +23 -5
  49. package/dist/macos-autostart.js +87 -0
  50. package/dist/pairing-code.js +12 -3
  51. package/dist/print-pairing.js +2 -0
  52. package/dist/run-service.js +31 -0
  53. package/dist/startup-check.js +165 -0
  54. package/package.json +1 -1
  55. package/version-policy.json +2 -2
@@ -3,17 +3,29 @@ SERVER_HOST=127.0.0.1
3
3
  SERVER_PORT=3847
4
4
  POLL_INTERVAL_MS=400
5
5
  DEBOUNCE_MS=150
6
+ # JSONL live lenta: poll subscribed (120ms), live watcher on subscribe (immediate), debounce off for live
7
+ # JSONL_POLL_MS=120
8
+ # JSONL_EMIT_DEBOUNCE_MS=0
9
+ # JSONL_WRITE_STABILITY_MS=50
6
10
  WINDOW_POLL_INTERVAL_MS=8000
7
11
  WEBAPP_PASSWORD=
8
12
  # Leave empty to use ~/.cursor/projects
9
13
  # CURSOR_PROJECTS_DIR=
10
14
  LOG_LEVEL=info
15
+
16
+ # Chat lenta: JSONL → agent:messages; liveMessages = DOM overlay until turn is in .jsonl.
17
+ # CHAT_HISTORY_JSONL=1 — extra socket agent:history on app (legacy).
18
+ # CHAT_HISTORY_JSONL=1
11
19
  # Follow active Cursor window in CDP (1.5s poll). Set FOCUS_SYNC_ENABLED=0 to disable.
12
20
  # FOCUS_SYNC_INTERVAL_MS=1500
13
- # Voice → text in app (POST /api/transcribe, Whisper)
14
- OPENAI_API_KEY=
21
+ # Voice → text only for local bridge without relay (prod: relay/.env OPENAI_API_KEY)
22
+ # OPENAI_API_KEY=
15
23
  # OPENAI_TRANSCRIBE_MODEL=whisper-1
16
24
  # Remote relay (VPS): transparent Socket.io forward; processing stays on this Mac
17
25
  # RELAY_URL=https://your-domain.example
18
26
  # RELAY_TOKEN=same-as-relay-.env
19
27
  # RELAY_ROOM_ID=default
28
+ # macOS: не давать Mac уснуть, пока bridge запущен (cursorconnect start). Выключить: KEEP_AWAKE=0
29
+ # KEEP_AWAKE=0
30
+ # Периодический ping relay upstream (NAT/idle). 0 = выкл.
31
+ # RELAY_KEEPALIVE_MS=20000
@@ -1 +1 @@
1
- {"cliVersion":"0.1.6","bundledAt":"2026-05-25T09:55:59Z"}
1
+ {"cliVersion":"0.1.8","bundledAt":"2026-05-26T06:15:47Z"}
@@ -0,0 +1,35 @@
1
+ import type { AgentsIndex, CursorState } from './types.js';
2
+ export declare const AGENT_COMPLETION_PUSH_BODY = "\u0410\u0433\u0435\u043D\u0442 \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u043B \u0440\u0430\u0431\u043E\u0442\u0443";
3
+ export declare function suppressAgentCompletionPush(ms?: number): void;
4
+ export interface AgentCompletionPushPayload {
5
+ agentId: string;
6
+ chatTitle: string;
7
+ body: string;
8
+ }
9
+ export type AgentCompletionPushEmit = (payload: AgentCompletionPushPayload) => void;
10
+ /**
11
+ * Push once per real generation session: agent must be stably working (≥3 DOM polls),
12
+ * then stably idle (≥2 polls). Opening a finished chat may flicker isWorking for 1 poll — ignored.
13
+ */
14
+ export declare class AgentCompletionPush {
15
+ private emit;
16
+ private agentsIndex;
17
+ private prevWorking;
18
+ private workingPolls;
19
+ /** Agent had a real generation episode this bridge session (not open-chat flicker). */
20
+ private confirmedWorking;
21
+ private idlePolls;
22
+ private lastEmitAt;
23
+ private emittedThisSession;
24
+ /** ~3 × 400ms — ignore brief spinner when opening a finished chat. */
25
+ private static readonly WORKING_POLLS_REQUIRED;
26
+ /** ~2 × 400ms idle before push. */
27
+ private static readonly IDLE_POLLS_REQUIRED;
28
+ private static readonly MIN_EMIT_GAP_MS;
29
+ constructor(emit: AgentCompletionPushEmit, agentsIndex: () => AgentsIndex | null);
30
+ observe(state: CursorState): void;
31
+ private trackWorkingSessions;
32
+ private detectIdleCompletions;
33
+ private syncPrevWorking;
34
+ private tryEmit;
35
+ }
@@ -0,0 +1,195 @@
1
+ export const AGENT_COMPLETION_PUSH_BODY = 'Агент завершил работу';
2
+ let suppressUntil = 0;
3
+ export function suppressAgentCompletionPush(ms = 6000) {
4
+ suppressUntil = Date.now() + ms;
5
+ }
6
+ function isSuppressed() {
7
+ return Date.now() < suppressUntil;
8
+ }
9
+ function normalizeTitle(title) {
10
+ return title.trim().replace(/\s+/g, ' ');
11
+ }
12
+ function isStableComposerId(id) {
13
+ if (!id?.trim())
14
+ return false;
15
+ const t = id.trim();
16
+ if (t.startsWith('tab-'))
17
+ return false;
18
+ return t.length >= 8;
19
+ }
20
+ function titleForComposerId(agentId, state) {
21
+ const activeTitle = state.activeChatTitle?.trim();
22
+ if (state.activeComposerId === agentId && activeTitle)
23
+ return activeTitle;
24
+ for (const tab of state.tabs) {
25
+ const id = tab.composerId ?? tab.id;
26
+ if (id === agentId)
27
+ return tab.title?.trim() || undefined;
28
+ }
29
+ for (const repo of state.sidebarRepos ?? []) {
30
+ for (const agent of repo.agents) {
31
+ if (agent.id === agentId)
32
+ return agent.title?.trim() || undefined;
33
+ }
34
+ }
35
+ return undefined;
36
+ }
37
+ function resolveAgentChatTitle(agentId, state, agentsIndex) {
38
+ const fromState = titleForComposerId(agentId, state);
39
+ if (fromState)
40
+ return fromState;
41
+ for (const repo of agentsIndex?.repos ?? []) {
42
+ for (const agent of repo.agents) {
43
+ if (agent.id === agentId)
44
+ return agent.title?.trim() || 'Чат';
45
+ }
46
+ }
47
+ return 'Чат';
48
+ }
49
+ /** Active + background sidebar tabs that show generation. */
50
+ function snapshotWorkingAgents(state, agentsIndex) {
51
+ const map = new Map();
52
+ const activeId = state.activeComposerId;
53
+ const activeGenerating = !!state.agentWorking &&
54
+ !!activeId &&
55
+ isStableComposerId(activeId) &&
56
+ !agentPausedForInput(state, activeId);
57
+ if (activeGenerating && activeId) {
58
+ map.set(activeId, resolveAgentChatTitle(activeId, state, agentsIndex));
59
+ }
60
+ for (const tab of state.tabs) {
61
+ if (!tab.isWorking)
62
+ continue;
63
+ const id = tab.composerId?.trim();
64
+ if (!id || !isStableComposerId(id))
65
+ continue;
66
+ if (agentPausedForInput(state, id))
67
+ continue;
68
+ map.set(id, tab.title?.trim() || resolveAgentChatTitle(id, state, agentsIndex));
69
+ }
70
+ return map;
71
+ }
72
+ function agentPausedForInput(state, agentId) {
73
+ const isActive = state.activeComposerId === agentId;
74
+ if (!isActive)
75
+ return false;
76
+ return ((state.pendingApprovals?.length ?? 0) > 0 ||
77
+ !!state.pendingQuestionnaire ||
78
+ state.agentStatus === 'waiting_approval' ||
79
+ state.agentStatus === 'waiting_questionnaire');
80
+ }
81
+ function isStillWorkingWithTitle(title, now) {
82
+ const norm = normalizeTitle(title);
83
+ if (!norm)
84
+ return false;
85
+ for (const t of now.values()) {
86
+ if (normalizeTitle(t) === norm)
87
+ return true;
88
+ }
89
+ return false;
90
+ }
91
+ /**
92
+ * Push once per real generation session: agent must be stably working (≥3 DOM polls),
93
+ * then stably idle (≥2 polls). Opening a finished chat may flicker isWorking for 1 poll — ignored.
94
+ */
95
+ export class AgentCompletionPush {
96
+ emit;
97
+ agentsIndex;
98
+ prevWorking = null;
99
+ workingPolls = new Map();
100
+ /** Agent had a real generation episode this bridge session (not open-chat flicker). */
101
+ confirmedWorking = new Set();
102
+ idlePolls = new Map();
103
+ lastEmitAt = new Map();
104
+ emittedThisSession = new Set();
105
+ /** ~3 × 400ms — ignore brief spinner when opening a finished chat. */
106
+ static WORKING_POLLS_REQUIRED = 3;
107
+ /** ~2 × 400ms idle before push. */
108
+ static IDLE_POLLS_REQUIRED = 2;
109
+ static MIN_EMIT_GAP_MS = 8_000;
110
+ constructor(emit, agentsIndex) {
111
+ this.emit = emit;
112
+ this.agentsIndex = agentsIndex;
113
+ }
114
+ observe(state) {
115
+ if (isSuppressed()) {
116
+ this.syncPrevWorking(state);
117
+ return;
118
+ }
119
+ const index = this.agentsIndex();
120
+ const now = snapshotWorkingAgents(state, index);
121
+ this.trackWorkingSessions(now);
122
+ this.detectIdleCompletions(now, state);
123
+ }
124
+ trackWorkingSessions(now) {
125
+ for (const id of now.keys()) {
126
+ const polls = (this.workingPolls.get(id) ?? 0) + 1;
127
+ this.workingPolls.set(id, polls);
128
+ if (polls >= AgentCompletionPush.WORKING_POLLS_REQUIRED && !this.confirmedWorking.has(id)) {
129
+ this.confirmedWorking.add(id);
130
+ this.emittedThisSession.delete(id);
131
+ this.idlePolls.delete(id);
132
+ }
133
+ }
134
+ for (const id of this.workingPolls.keys()) {
135
+ if (!now.has(id))
136
+ this.workingPolls.delete(id);
137
+ }
138
+ }
139
+ detectIdleCompletions(now, state) {
140
+ const prev = this.prevWorking;
141
+ if (prev === null) {
142
+ this.prevWorking = new Map(now);
143
+ return;
144
+ }
145
+ for (const [agentId] of prev) {
146
+ if (now.has(agentId)) {
147
+ this.idlePolls.delete(agentId);
148
+ continue;
149
+ }
150
+ if (!this.confirmedWorking.has(agentId))
151
+ continue;
152
+ const polls = (this.idlePolls.get(agentId) ?? 0) + 1;
153
+ this.idlePolls.set(agentId, polls);
154
+ if (polls < AgentCompletionPush.IDLE_POLLS_REQUIRED)
155
+ continue;
156
+ this.tryEmit(agentId, state);
157
+ }
158
+ for (const id of this.idlePolls.keys()) {
159
+ if (now.has(id))
160
+ this.idlePolls.delete(id);
161
+ }
162
+ this.prevWorking = new Map(now);
163
+ }
164
+ syncPrevWorking(state) {
165
+ this.prevWorking = snapshotWorkingAgents(state, this.agentsIndex());
166
+ }
167
+ tryEmit(agentId, state) {
168
+ if (agentPausedForInput(state, agentId))
169
+ return;
170
+ if (this.emittedThisSession.has(agentId))
171
+ return;
172
+ if (!this.confirmedWorking.has(agentId))
173
+ return;
174
+ const now = snapshotWorkingAgents(state, this.agentsIndex());
175
+ if (now.has(agentId))
176
+ return;
177
+ const title = normalizeTitle(resolveAgentChatTitle(agentId, state, this.agentsIndex())) || 'Чат';
178
+ if (isStillWorkingWithTitle(title, now))
179
+ return;
180
+ const last = this.lastEmitAt.get(agentId) ?? 0;
181
+ if (Date.now() - last < AgentCompletionPush.MIN_EMIT_GAP_MS)
182
+ return;
183
+ this.emit({
184
+ agentId,
185
+ chatTitle: title,
186
+ body: AGENT_COMPLETION_PUSH_BODY,
187
+ });
188
+ this.lastEmitAt.set(agentId, Date.now());
189
+ this.emittedThisSession.add(agentId);
190
+ this.confirmedWorking.delete(agentId);
191
+ this.idlePolls.delete(agentId);
192
+ this.workingPolls.delete(agentId);
193
+ console.log(`[agent-completion-push] → relay dom-idle agentId=${agentId.slice(0, 12)} title=${title.slice(0, 48)}`);
194
+ }
195
+ }
@@ -6,12 +6,13 @@ export declare function findAgentById(jsonl: AgentsIndex, agentId: string): Agen
6
6
  /** Best JSONL agent for a Cursor sidebar row (generated title vs first user message). */
7
7
  export declare function matchJsonlAgentForSidebarTitle(rowTitle: string, jsonl: AgentsIndex, composerIdByTitle?: Record<string, string>): AgentSummary | undefined;
8
8
  export declare function isSyntheticAgentId(agentId: string): boolean;
9
- /** Map app/relay agentId (+ optional sidebar title) to an on-disk JSONL id. */
10
- export declare function resolveJsonlAgentId(projectsDir: string, agentId: string, opts?: {
11
- title?: string;
12
- composerIdByTitle?: Record<string, string>;
13
- }): string;
14
- export declare function resolveJsonlFilePath(projectsDir: string, agentId: string, opts?: {
9
+ export type ResolveJsonlOpts = {
15
10
  title?: string;
16
11
  composerIdByTitle?: Record<string, string>;
17
- }): string | null;
12
+ /** Cursor's active composer UUID — binds `sidebar-N` to the open chat file. */
13
+ activeComposerId?: string;
14
+ activeTabTitle?: string;
15
+ };
16
+ /** Map app/relay agentId (+ optional sidebar title) to an on-disk JSONL id. */
17
+ export declare function resolveJsonlAgentId(projectsDir: string, agentId: string, opts?: ResolveJsonlOpts): string;
18
+ export declare function resolveJsonlFilePath(projectsDir: string, agentId: string, opts?: ResolveJsonlOpts): string | null;
@@ -1,5 +1,6 @@
1
1
  import { basename, join } from 'path';
2
2
  import { existsSync, readdirSync, readFileSync } from 'fs';
3
+ import { normalizeComposerTitle, titlesAlign } from './composer-title-index.js';
3
4
  export function normalizeAgentTitle(title) {
4
5
  return title
5
6
  .trim()
@@ -182,10 +183,19 @@ export function resolveJsonlAgentId(projectsDir, agentId, opts) {
182
183
  const title = opts?.title?.trim();
183
184
  const map = opts?.composerIdByTitle;
184
185
  if (title && map) {
185
- const mapped = map[normalizeAgentTitle(title)];
186
+ const mapped = map[normalizeComposerTitle(title)] ?? map[normalizeAgentTitle(title)];
186
187
  if (mapped && findJsonlByAgentId(projectsDir, mapped))
187
188
  return mapped;
188
189
  }
190
+ const activeComposerId = opts?.activeComposerId?.trim();
191
+ if (isSyntheticAgentId(agentId) &&
192
+ activeComposerId &&
193
+ UUID_RE.test(activeComposerId) &&
194
+ findJsonlByAgentId(projectsDir, activeComposerId)) {
195
+ const tabT = opts?.activeTabTitle?.trim();
196
+ if (!title || !tabT || titlesAlign(title, tabT))
197
+ return activeComposerId;
198
+ }
189
199
  if (title) {
190
200
  let best;
191
201
  for (const row of collectJsonlAgents(projectsDir)) {
@@ -1,13 +1,25 @@
1
1
  import type { ChatMessage, HistoryMessage } from './types.js';
2
- /** Per-agent prepared history + merge with live DOM for subscribed chat. */
2
+ /**
3
+ * Chat UI store: JSONL baseline for `agent:messages` (client lenta).
4
+ * DOM overlay → `liveMessages` in `agent:messages` until JSONL has the same turn.
5
+ */
3
6
  export declare class ChatDisplayStore {
4
- private historyByAgent;
7
+ private readonly domTranscript;
8
+ private readonly jsonlBaseline;
9
+ /** Raw JSONL row count per agent — incremental display merge during streaming. */
10
+ private readonly jsonlRowCount;
5
11
  clearAgent(agentId: string): void;
6
- /** JSONL / HTTP / socket history — returns display-ready messages. */
7
- applyHistory(agentId: string, rows: HistoryMessage[], opts?: {
8
- mergeWithCache?: boolean;
9
- }): ChatMessage[];
10
- getHistory(agentId: string): ChatMessage[];
11
- /** Raw DOM extract for active composer → merged display list when history exists. */
12
- mergeLiveForAgent(agentId: string, rawDom: ChatMessage[]): ChatMessage[];
12
+ getJsonlHistory(agentId: string): ChatMessage[];
13
+ getDomLive(agentId: string): ChatMessage[];
14
+ /** Reload JSONL; prune DOM rows now covered by archive. */
15
+ setJsonlBaseline(agentId: string, rows: HistoryMessage[]): void;
16
+ /** Append JSONL rows during live stream; returns new/updated bubbles for socket `append` emit. */
17
+ appendJsonlRows(agentId: string, deltaRows: HistoryMessage[]): ChatMessage[];
18
+ /** Ingest DOM extract; overlay keeps only rows not in JSONL. */
19
+ mergeLiveForAgent(agentId: string, rawDom: ChatMessage[]): void;
20
+ /** Debug / snapshot: combined view. */
21
+ getDisplayMessages(agentId: string): ChatMessage[];
22
+ getDomTranscript(agentId: string): ChatMessage[];
23
+ private domOverlayOnly;
24
+ private pruneDomOverlay;
13
25
  }
@@ -1,29 +1,103 @@
1
- import { filterClientDisplayList, historyRowsToChat, mergeDomWithHistory, mergeHistoryTail, prepareChatMessagesForDisplay, } from './chat-display.js';
2
- /** Per-agent prepared history + merge with live DOM for subscribed chat. */
1
+ import { filterClientDisplayList, historyRowsToChat, mergeHistoryTail, messagesEquivalent, prepareChatMessagesForDisplay, } from './chat-display.js';
2
+ import { DomTranscriptStore } from './dom-transcript-store.js';
3
+ /**
4
+ * Chat UI store: JSONL baseline for `agent:messages` (client lenta).
5
+ * DOM overlay → `liveMessages` in `agent:messages` until JSONL has the same turn.
6
+ */
3
7
  export class ChatDisplayStore {
4
- historyByAgent = new Map();
8
+ domTranscript = new DomTranscriptStore();
9
+ jsonlBaseline = new Map();
10
+ /** Raw JSONL row count per agent — incremental display merge during streaming. */
11
+ jsonlRowCount = new Map();
5
12
  clearAgent(agentId) {
6
- this.historyByAgent.delete(agentId);
7
- }
8
- /** JSONL / HTTP / socket history — returns display-ready messages. */
9
- applyHistory(agentId, rows, opts) {
10
- const incoming = prepareChatMessagesForDisplay(historyRowsToChat(rows));
11
- const prev = this.historyByAgent.get(agentId) ?? [];
12
- const merged = opts?.mergeWithCache && prev.length
13
- ? mergeHistoryTail(prev, incoming)
14
- : incoming;
15
- const out = filterClientDisplayList(merged);
16
- this.historyByAgent.set(agentId, out);
17
- return out;
18
- }
19
- getHistory(agentId) {
20
- return this.historyByAgent.get(agentId) ?? [];
21
- }
22
- /** Raw DOM extract for active composer → merged display list when history exists. */
13
+ this.domTranscript.clear(agentId);
14
+ this.jsonlBaseline.delete(agentId);
15
+ this.jsonlRowCount.delete(agentId);
16
+ }
17
+ getJsonlHistory(agentId) {
18
+ return filterClientDisplayList(this.jsonlBaseline.get(agentId) ?? []);
19
+ }
20
+ getDomLive(agentId) {
21
+ const history = this.jsonlBaseline.get(agentId) ?? [];
22
+ return filterClientDisplayList(this.domOverlayOnly(agentId, history));
23
+ }
24
+ /** Reload JSONL; prune DOM rows now covered by archive. */
25
+ setJsonlBaseline(agentId, rows) {
26
+ const prevRows = this.jsonlRowCount.get(agentId) ?? 0;
27
+ if (rows.length > 0 && prevRows > 24 && rows.length < prevRows - 12) {
28
+ return;
29
+ }
30
+ const prev = this.jsonlBaseline.get(agentId) ?? [];
31
+ let prepared;
32
+ if (rows.length > prevRows && prevRows > 0 && prev.length && rows.length - prevRows <= 128) {
33
+ const delta = rows.slice(prevRows);
34
+ const deltaChat = prepareChatMessagesForDisplay(historyRowsToChat(delta));
35
+ prepared = mergeHistoryTail(prev, deltaChat);
36
+ }
37
+ else {
38
+ prepared = rows.length
39
+ ? prepareChatMessagesForDisplay(historyRowsToChat(rows))
40
+ : [];
41
+ }
42
+ this.jsonlRowCount.set(agentId, rows.length);
43
+ this.jsonlBaseline.set(agentId, prepared);
44
+ this.pruneDomOverlay(agentId);
45
+ }
46
+ /** Append JSONL rows during live stream; returns new/updated bubbles for socket `append` emit. */
47
+ appendJsonlRows(agentId, deltaRows) {
48
+ if (!deltaRows.length)
49
+ return [];
50
+ const prev = this.jsonlBaseline.get(agentId) ?? [];
51
+ const prevLen = prev.length;
52
+ const deltaChat = prepareChatMessagesForDisplay(historyRowsToChat(deltaRows));
53
+ const prepared = mergeHistoryTail(prev, deltaChat);
54
+ this.jsonlRowCount.set(agentId, (this.jsonlRowCount.get(agentId) ?? 0) + deltaRows.length);
55
+ this.jsonlBaseline.set(agentId, prepared);
56
+ this.pruneDomOverlay(agentId);
57
+ if (prepared.length > prevLen)
58
+ return prepared.slice(prevLen);
59
+ if (prepared.length && prevLen) {
60
+ const last = prepared[prepared.length - 1];
61
+ const prevLast = prev[prevLen - 1];
62
+ if (prevLast && last.id === prevLast.id && last.text !== prevLast.text)
63
+ return [last];
64
+ if (prevLast &&
65
+ last.role === prevLast.role &&
66
+ (last.text?.length ?? 0) > (prevLast.text?.length ?? 0) &&
67
+ last.text?.includes((prevLast.text ?? '').slice(0, 24))) {
68
+ return [last];
69
+ }
70
+ }
71
+ if (!prevLen && prepared.length)
72
+ return prepared;
73
+ return deltaChat.length ? deltaChat : [];
74
+ }
75
+ /** Ingest DOM extract; overlay keeps only rows not in JSONL. */
23
76
  mergeLiveForAgent(agentId, rawDom) {
24
77
  const live = prepareChatMessagesForDisplay(rawDom);
25
- const hist = this.historyByAgent.get(agentId) ?? [];
26
- const merged = hist.length ? mergeDomWithHistory(hist, live) : live;
27
- return filterClientDisplayList(merged);
78
+ if (live.length)
79
+ this.domTranscript.ingest(agentId, live);
80
+ this.pruneDomOverlay(agentId);
81
+ }
82
+ /** Debug / snapshot: combined view. */
83
+ getDisplayMessages(agentId) {
84
+ return [...this.getJsonlHistory(agentId), ...this.getDomLive(agentId)];
85
+ }
86
+ getDomTranscript(agentId) {
87
+ return this.getDisplayMessages(agentId);
88
+ }
89
+ domOverlayOnly(agentId, history) {
90
+ const dom = this.domTranscript.list(agentId);
91
+ if (!dom.length)
92
+ return [];
93
+ if (!history.length)
94
+ return dom;
95
+ return dom.filter((d) => !history.some((h) => messagesEquivalent(h, d)));
96
+ }
97
+ pruneDomOverlay(agentId) {
98
+ const history = this.jsonlBaseline.get(agentId) ?? [];
99
+ if (!history.length)
100
+ return;
101
+ this.domTranscript.pruneCoveredBy(agentId, history);
28
102
  }
29
103
  }
@@ -2,6 +2,8 @@ import type { ChatMessage, HistoryMessage } from './types.js';
2
2
  export declare function compareUserText(text: string): string;
3
3
  export declare function userTextsEquivalent(ta: string, tb: string): boolean;
4
4
  export declare function userMessagesEquivalent(a: ChatMessage, b: ChatMessage): boolean;
5
+ export declare function messagesEquivalent(a: ChatMessage, b: ChatMessage): boolean;
6
+ export declare function pickPreferredMessage(prev: ChatMessage, next: ChatMessage, preferNext: boolean): ChatMessage;
5
7
  export declare function sortMessagesChronologically(messages: ChatMessage[]): ChatMessage[];
6
8
  export declare function prepareChatMessagesForDisplay(messages: ChatMessage[]): ChatMessage[];
7
9
  export declare function mergeHistoryTail(prev: ChatMessage[], incoming: ChatMessage[]): ChatMessage[];