cursorconnect 0.1.6 → 0.1.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bridge-runtime/.env.example +14 -2
- package/bridge-runtime/connector-version.json +1 -1
- package/bridge-runtime/dist/agent-completion-push.d.ts +35 -0
- package/bridge-runtime/dist/agent-completion-push.js +195 -0
- package/bridge-runtime/dist/agent-title-match.d.ts +8 -7
- package/bridge-runtime/dist/agent-title-match.js +11 -1
- package/bridge-runtime/dist/chat-display-store.d.ts +21 -9
- package/bridge-runtime/dist/chat-display-store.js +97 -23
- package/bridge-runtime/dist/chat-display.d.ts +2 -0
- package/bridge-runtime/dist/chat-display.js +197 -33
- package/bridge-runtime/dist/chat-history-mode.d.ts +5 -0
- package/bridge-runtime/dist/chat-history-mode.js +7 -0
- package/bridge-runtime/dist/command-executor.d.ts +2 -0
- package/bridge-runtime/dist/command-executor.js +44 -0
- package/bridge-runtime/dist/composer-title-index.d.ts +1 -0
- package/bridge-runtime/dist/composer-title-index.js +7 -7
- package/bridge-runtime/dist/config.js +2 -0
- package/bridge-runtime/dist/connector-client-version.js +1 -1
- package/bridge-runtime/dist/debug-chats-page.d.ts +2 -0
- package/bridge-runtime/dist/debug-chats-page.js +491 -0
- package/bridge-runtime/dist/dom-transcript-store.d.ts +17 -0
- package/bridge-runtime/dist/dom-transcript-store.js +76 -0
- package/bridge-runtime/dist/extract-page.js +56 -85
- package/bridge-runtime/dist/history-limit.d.ts +2 -0
- package/bridge-runtime/dist/history-limit.js +2 -0
- package/bridge-runtime/dist/history-request.d.ts +8 -0
- package/bridge-runtime/dist/history-request.js +7 -0
- package/bridge-runtime/dist/index.js +10 -0
- package/bridge-runtime/dist/jsonl-index.d.ts +21 -3
- package/bridge-runtime/dist/jsonl-index.js +237 -73
- package/bridge-runtime/dist/jsonl-live-debug.d.ts +24 -0
- package/bridge-runtime/dist/jsonl-live-debug.js +175 -0
- package/bridge-runtime/dist/keep-awake.d.ts +5 -0
- package/bridge-runtime/dist/keep-awake.js +48 -0
- package/bridge-runtime/dist/media-path.d.ts +2 -0
- package/bridge-runtime/dist/media-path.js +17 -0
- package/bridge-runtime/dist/message-filter.d.ts +2 -0
- package/bridge-runtime/dist/message-filter.js +21 -5
- package/bridge-runtime/dist/pairing-code.d.ts +2 -0
- package/bridge-runtime/dist/pairing-code.js +9 -2
- package/bridge-runtime/dist/relay-upstream.d.ts +5 -1
- package/bridge-runtime/dist/relay-upstream.js +25 -1
- package/bridge-runtime/dist/relay.d.ts +31 -0
- package/bridge-runtime/dist/relay.js +401 -32
- package/bridge-runtime/dist/types.d.ts +25 -0
- package/bridge-runtime/selectors.json +4 -5
- package/dist/index.js +79 -20
- package/dist/launch.js +23 -5
- package/dist/macos-autostart.js +87 -0
- package/dist/pairing-code.js +12 -3
- package/dist/print-pairing.js +2 -0
- package/dist/run-service.js +31 -0
- package/dist/startup-check.js +165 -0
- package/package.json +1 -1
- package/version-policy.json +2 -2
|
@@ -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
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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,
|
|
2
|
-
|
|
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
|
-
|
|
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.
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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[];
|