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.
- package/bridge-runtime/.env.example +10 -2
- package/bridge-runtime/connector-version.json +1 -0
- 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/connector-client-version.d.ts +2 -0
- package/bridge-runtime/dist/connector-client-version.js +43 -0
- 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 +4 -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 +13 -2
- 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/bundled-bridge-check.js +25 -0
- package/dist/index.js +87 -10
- package/dist/launch.js +47 -0
- 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
|
@@ -3,15 +3,23 @@ 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 always []). DOM = state:patch (working, approve).
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"cliVersion":"0.1.7","bundledAt":"2026-05-25T18:20:35Z"}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { AgentsIndex, CursorState, HistoryMessage } 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 when a new assistant row lands in JSONL for the current/recent task.
|
|
12
|
+
* If JSONL flushes while DOM still shows "working", retry when DOM goes idle.
|
|
13
|
+
*/
|
|
14
|
+
export declare class AgentCompletionPush {
|
|
15
|
+
private emit;
|
|
16
|
+
private agentsIndex;
|
|
17
|
+
private getState;
|
|
18
|
+
/** Last seen assistant line id in JSONL (line number / ts). */
|
|
19
|
+
private lastJsonlAssistantKey;
|
|
20
|
+
/** Agents that were generating on previous DOM poll. */
|
|
21
|
+
private prevWorking;
|
|
22
|
+
private lastEmitAt;
|
|
23
|
+
/** JSONL had a new assistant line while DOM was still working — emit after idle. */
|
|
24
|
+
private pendingAfterIdle;
|
|
25
|
+
private pendingDeferSince;
|
|
26
|
+
private deferTimers;
|
|
27
|
+
private lastJsonlMessages;
|
|
28
|
+
private static readonly MIN_EMIT_GAP_MS;
|
|
29
|
+
/** DOM can lie "working" after JSONL finished — emit anyway after this. */
|
|
30
|
+
private static readonly MAX_DEFER_MS;
|
|
31
|
+
constructor(emit: AgentCompletionPushEmit, agentsIndex: () => AgentsIndex | null, getState: () => CursorState);
|
|
32
|
+
/** Sync working snapshot; flush pending push when a task leaves DOM "working". */
|
|
33
|
+
observe(state: CursorState): void;
|
|
34
|
+
/**
|
|
35
|
+
* JSONL file updated — push if a new assistant message was appended for this task
|
|
36
|
+
* and DOM no longer shows generation for that composer.
|
|
37
|
+
*/
|
|
38
|
+
onJsonlUpdated(agentId: string, messages: HistoryMessage[], state: CursorState): void;
|
|
39
|
+
private scheduleDeferTimeout;
|
|
40
|
+
private clearDeferTimer;
|
|
41
|
+
private tryEmit;
|
|
42
|
+
}
|
|
@@ -0,0 +1,220 @@
|
|
|
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
|
+
/** Ephemeral sidebar ids (tab-0) must not drive push — composerId can appear next poll. */
|
|
13
|
+
function isStableComposerId(id) {
|
|
14
|
+
if (!id?.trim())
|
|
15
|
+
return false;
|
|
16
|
+
const t = id.trim();
|
|
17
|
+
if (t.startsWith('tab-'))
|
|
18
|
+
return false;
|
|
19
|
+
return t.length >= 8;
|
|
20
|
+
}
|
|
21
|
+
function titleForComposerId(agentId, state) {
|
|
22
|
+
const activeTitle = state.activeChatTitle?.trim();
|
|
23
|
+
if (state.activeComposerId === agentId && activeTitle)
|
|
24
|
+
return activeTitle;
|
|
25
|
+
for (const tab of state.tabs) {
|
|
26
|
+
const id = tab.composerId ?? tab.id;
|
|
27
|
+
if (id === agentId)
|
|
28
|
+
return tab.title?.trim() || undefined;
|
|
29
|
+
}
|
|
30
|
+
for (const repo of state.sidebarRepos ?? []) {
|
|
31
|
+
for (const agent of repo.agents) {
|
|
32
|
+
if (agent.id === agentId)
|
|
33
|
+
return agent.title?.trim() || undefined;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return undefined;
|
|
37
|
+
}
|
|
38
|
+
function resolveAgentChatTitle(agentId, state, agentsIndex) {
|
|
39
|
+
const fromState = titleForComposerId(agentId, state);
|
|
40
|
+
if (fromState)
|
|
41
|
+
return fromState;
|
|
42
|
+
for (const repo of agentsIndex?.repos ?? []) {
|
|
43
|
+
for (const agent of repo.agents) {
|
|
44
|
+
if (agent.id === agentId)
|
|
45
|
+
return agent.title?.trim() || 'Чат';
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return 'Чат';
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Working agents for correlating JSONL flush with the task that was running.
|
|
52
|
+
* Sidebar: stable composerId only (no tab-N flicker).
|
|
53
|
+
*/
|
|
54
|
+
function snapshotWorkingAgents(state, agentsIndex) {
|
|
55
|
+
const map = new Map();
|
|
56
|
+
const activeId = state.activeComposerId;
|
|
57
|
+
const activeGenerating = state.agentStatus === 'working' &&
|
|
58
|
+
!!activeId &&
|
|
59
|
+
isStableComposerId(activeId);
|
|
60
|
+
if (activeGenerating && activeId) {
|
|
61
|
+
map.set(activeId, resolveAgentChatTitle(activeId, state, agentsIndex));
|
|
62
|
+
}
|
|
63
|
+
for (const tab of state.tabs) {
|
|
64
|
+
if (!tab.isWorking)
|
|
65
|
+
continue;
|
|
66
|
+
const id = tab.composerId?.trim();
|
|
67
|
+
if (!id || !isStableComposerId(id))
|
|
68
|
+
continue;
|
|
69
|
+
map.set(id, tab.title?.trim() || resolveAgentChatTitle(id, state, agentsIndex));
|
|
70
|
+
}
|
|
71
|
+
return map;
|
|
72
|
+
}
|
|
73
|
+
function agentPausedForInput(state) {
|
|
74
|
+
return ((state.pendingApprovals?.length ?? 0) > 0 ||
|
|
75
|
+
!!state.pendingQuestionnaire ||
|
|
76
|
+
state.agentStatus === 'waiting_approval' ||
|
|
77
|
+
state.agentStatus === 'waiting_questionnaire');
|
|
78
|
+
}
|
|
79
|
+
function lastAssistantLineKey(messages) {
|
|
80
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
81
|
+
const m = messages[i];
|
|
82
|
+
if (m.role === 'assistant')
|
|
83
|
+
return m.ts ?? i;
|
|
84
|
+
}
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Push when a new assistant row lands in JSONL for the current/recent task.
|
|
89
|
+
* If JSONL flushes while DOM still shows "working", retry when DOM goes idle.
|
|
90
|
+
*/
|
|
91
|
+
export class AgentCompletionPush {
|
|
92
|
+
emit;
|
|
93
|
+
agentsIndex;
|
|
94
|
+
getState;
|
|
95
|
+
/** Last seen assistant line id in JSONL (line number / ts). */
|
|
96
|
+
lastJsonlAssistantKey = new Map();
|
|
97
|
+
/** Agents that were generating on previous DOM poll. */
|
|
98
|
+
prevWorking = new Map();
|
|
99
|
+
lastEmitAt = new Map();
|
|
100
|
+
/** JSONL had a new assistant line while DOM was still working — emit after idle. */
|
|
101
|
+
pendingAfterIdle = new Set();
|
|
102
|
+
pendingDeferSince = new Map();
|
|
103
|
+
deferTimers = new Map();
|
|
104
|
+
lastJsonlMessages = new Map();
|
|
105
|
+
static MIN_EMIT_GAP_MS = 45_000;
|
|
106
|
+
/** DOM can lie "working" after JSONL finished — emit anyway after this. */
|
|
107
|
+
static MAX_DEFER_MS = 12_000;
|
|
108
|
+
constructor(emit, agentsIndex, getState) {
|
|
109
|
+
this.emit = emit;
|
|
110
|
+
this.agentsIndex = agentsIndex;
|
|
111
|
+
this.getState = getState;
|
|
112
|
+
}
|
|
113
|
+
/** Sync working snapshot; flush pending push when a task leaves DOM "working". */
|
|
114
|
+
observe(state) {
|
|
115
|
+
const index = this.agentsIndex();
|
|
116
|
+
if (isSuppressed() || agentPausedForInput(state)) {
|
|
117
|
+
this.prevWorking = snapshotWorkingAgents(state, index);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
const nowWorking = snapshotWorkingAgents(state, index);
|
|
121
|
+
for (const agentId of this.pendingAfterIdle) {
|
|
122
|
+
if (nowWorking.has(agentId))
|
|
123
|
+
continue;
|
|
124
|
+
const messages = this.lastJsonlMessages.get(agentId);
|
|
125
|
+
if (messages?.length) {
|
|
126
|
+
this.tryEmit(agentId, messages, state, 'dom-idle');
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
this.prevWorking = nowWorking;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* JSONL file updated — push if a new assistant message was appended for this task
|
|
133
|
+
* and DOM no longer shows generation for that composer.
|
|
134
|
+
*/
|
|
135
|
+
onJsonlUpdated(agentId, messages, state) {
|
|
136
|
+
if (isSuppressed() || agentPausedForInput(state))
|
|
137
|
+
return;
|
|
138
|
+
if (!isStableComposerId(agentId))
|
|
139
|
+
return;
|
|
140
|
+
const lineKey = lastAssistantLineKey(messages);
|
|
141
|
+
if (!lineKey)
|
|
142
|
+
return;
|
|
143
|
+
const prevKey = this.lastJsonlAssistantKey.get(agentId);
|
|
144
|
+
if (prevKey === undefined) {
|
|
145
|
+
this.lastJsonlAssistantKey.set(agentId, lineKey);
|
|
146
|
+
this.lastJsonlMessages.set(agentId, messages);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
if (lineKey <= prevKey)
|
|
150
|
+
return;
|
|
151
|
+
this.lastJsonlAssistantKey.set(agentId, lineKey);
|
|
152
|
+
this.lastJsonlMessages.set(agentId, messages);
|
|
153
|
+
const nowWorking = snapshotWorkingAgents(state, this.agentsIndex());
|
|
154
|
+
if (nowWorking.has(agentId)) {
|
|
155
|
+
this.pendingAfterIdle.add(agentId);
|
|
156
|
+
if (!this.pendingDeferSince.has(agentId)) {
|
|
157
|
+
this.pendingDeferSince.set(agentId, Date.now());
|
|
158
|
+
}
|
|
159
|
+
this.scheduleDeferTimeout(agentId);
|
|
160
|
+
console.log(`[agent-completion-push] defer agentId=${agentId.slice(0, 12)} line=${lineKey} (dom still working)`);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
this.clearDeferTimer(agentId);
|
|
164
|
+
this.pendingDeferSince.delete(agentId);
|
|
165
|
+
this.tryEmit(agentId, messages, state, 'jsonl');
|
|
166
|
+
}
|
|
167
|
+
scheduleDeferTimeout(agentId) {
|
|
168
|
+
if (this.deferTimers.has(agentId))
|
|
169
|
+
return;
|
|
170
|
+
const timer = setTimeout(() => {
|
|
171
|
+
this.deferTimers.delete(agentId);
|
|
172
|
+
const since = this.pendingDeferSince.get(agentId) ?? 0;
|
|
173
|
+
if (!this.pendingAfterIdle.has(agentId))
|
|
174
|
+
return;
|
|
175
|
+
if (Date.now() - since < AgentCompletionPush.MAX_DEFER_MS - 100)
|
|
176
|
+
return;
|
|
177
|
+
const messages = this.lastJsonlMessages.get(agentId);
|
|
178
|
+
if (!messages?.length)
|
|
179
|
+
return;
|
|
180
|
+
console.log(`[agent-completion-push] defer timeout agentId=${agentId.slice(0, 12)} (dom still working, jsonl done)`);
|
|
181
|
+
this.tryEmit(agentId, messages, this.getState(), 'defer-timeout');
|
|
182
|
+
}, AgentCompletionPush.MAX_DEFER_MS);
|
|
183
|
+
this.deferTimers.set(agentId, timer);
|
|
184
|
+
}
|
|
185
|
+
clearDeferTimer(agentId) {
|
|
186
|
+
const t = this.deferTimers.get(agentId);
|
|
187
|
+
if (t)
|
|
188
|
+
clearTimeout(t);
|
|
189
|
+
this.deferTimers.delete(agentId);
|
|
190
|
+
}
|
|
191
|
+
tryEmit(agentId, messages, state, reason) {
|
|
192
|
+
const lineKey = lastAssistantLineKey(messages);
|
|
193
|
+
if (!lineKey)
|
|
194
|
+
return;
|
|
195
|
+
const nowWorking = snapshotWorkingAgents(state, this.agentsIndex());
|
|
196
|
+
if (reason !== 'defer-timeout' && nowWorking.has(agentId))
|
|
197
|
+
return;
|
|
198
|
+
const isCurrentTask = state.activeComposerId === agentId ||
|
|
199
|
+
this.prevWorking.has(agentId) ||
|
|
200
|
+
this.pendingAfterIdle.has(agentId);
|
|
201
|
+
if (!isCurrentTask)
|
|
202
|
+
return;
|
|
203
|
+
const last = this.lastEmitAt.get(agentId) ?? 0;
|
|
204
|
+
if (Date.now() - last < AgentCompletionPush.MIN_EMIT_GAP_MS)
|
|
205
|
+
return;
|
|
206
|
+
const chatTitle = normalizeTitle(this.prevWorking.get(agentId) ??
|
|
207
|
+
resolveAgentChatTitle(agentId, state, this.agentsIndex())) || 'Чат';
|
|
208
|
+
this.emit({
|
|
209
|
+
agentId,
|
|
210
|
+
chatTitle,
|
|
211
|
+
body: AGENT_COMPLETION_PUSH_BODY,
|
|
212
|
+
});
|
|
213
|
+
this.lastEmitAt.set(agentId, Date.now());
|
|
214
|
+
this.pendingAfterIdle.delete(agentId);
|
|
215
|
+
this.pendingDeferSince.delete(agentId);
|
|
216
|
+
this.clearDeferTimer(agentId);
|
|
217
|
+
this.prevWorking.delete(agentId);
|
|
218
|
+
console.log(`[agent-completion-push] → relay reason=${reason} agentId=${agentId.slice(0, 12)} title=${chatTitle.slice(0, 48)} line=${lineKey}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
@@ -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 transcript kept for debug snapshot only — not sent as liveMessages.
|
|
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,100 @@
|
|
|
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 transcript kept for debug snapshot only — not sent as liveMessages.
|
|
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
|
+
const prev = this.jsonlBaseline.get(agentId) ?? [];
|
|
28
|
+
let prepared;
|
|
29
|
+
if (rows.length > prevRows && prevRows > 0 && prev.length && rows.length - prevRows <= 128) {
|
|
30
|
+
const delta = rows.slice(prevRows);
|
|
31
|
+
const deltaChat = prepareChatMessagesForDisplay(historyRowsToChat(delta));
|
|
32
|
+
prepared = mergeHistoryTail(prev, deltaChat);
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
prepared = rows.length
|
|
36
|
+
? prepareChatMessagesForDisplay(historyRowsToChat(rows))
|
|
37
|
+
: [];
|
|
38
|
+
}
|
|
39
|
+
this.jsonlRowCount.set(agentId, rows.length);
|
|
40
|
+
this.jsonlBaseline.set(agentId, prepared);
|
|
41
|
+
this.pruneDomOverlay(agentId);
|
|
42
|
+
}
|
|
43
|
+
/** Append JSONL rows during live stream; returns new/updated bubbles for socket `append` emit. */
|
|
44
|
+
appendJsonlRows(agentId, deltaRows) {
|
|
45
|
+
if (!deltaRows.length)
|
|
46
|
+
return [];
|
|
47
|
+
const prev = this.jsonlBaseline.get(agentId) ?? [];
|
|
48
|
+
const prevLen = prev.length;
|
|
49
|
+
const deltaChat = prepareChatMessagesForDisplay(historyRowsToChat(deltaRows));
|
|
50
|
+
const prepared = mergeHistoryTail(prev, deltaChat);
|
|
51
|
+
this.jsonlRowCount.set(agentId, (this.jsonlRowCount.get(agentId) ?? 0) + deltaRows.length);
|
|
52
|
+
this.jsonlBaseline.set(agentId, prepared);
|
|
53
|
+
this.pruneDomOverlay(agentId);
|
|
54
|
+
if (prepared.length > prevLen)
|
|
55
|
+
return prepared.slice(prevLen);
|
|
56
|
+
if (prepared.length && prevLen) {
|
|
57
|
+
const last = prepared[prepared.length - 1];
|
|
58
|
+
const prevLast = prev[prevLen - 1];
|
|
59
|
+
if (prevLast && last.id === prevLast.id && last.text !== prevLast.text)
|
|
60
|
+
return [last];
|
|
61
|
+
if (prevLast &&
|
|
62
|
+
last.role === prevLast.role &&
|
|
63
|
+
(last.text?.length ?? 0) > (prevLast.text?.length ?? 0) &&
|
|
64
|
+
last.text?.includes((prevLast.text ?? '').slice(0, 24))) {
|
|
65
|
+
return [last];
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
if (!prevLen && prepared.length)
|
|
69
|
+
return prepared;
|
|
70
|
+
return deltaChat.length ? deltaChat : [];
|
|
71
|
+
}
|
|
72
|
+
/** Ingest DOM extract; overlay keeps only rows not in JSONL. */
|
|
23
73
|
mergeLiveForAgent(agentId, rawDom) {
|
|
24
74
|
const live = prepareChatMessagesForDisplay(rawDom);
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
75
|
+
if (live.length)
|
|
76
|
+
this.domTranscript.ingest(agentId, live);
|
|
77
|
+
this.pruneDomOverlay(agentId);
|
|
78
|
+
}
|
|
79
|
+
/** Debug / snapshot: combined view. */
|
|
80
|
+
getDisplayMessages(agentId) {
|
|
81
|
+
return [...this.getJsonlHistory(agentId), ...this.getDomLive(agentId)];
|
|
82
|
+
}
|
|
83
|
+
getDomTranscript(agentId) {
|
|
84
|
+
return this.getDisplayMessages(agentId);
|
|
85
|
+
}
|
|
86
|
+
domOverlayOnly(agentId, history) {
|
|
87
|
+
const dom = this.domTranscript.list(agentId);
|
|
88
|
+
if (!dom.length)
|
|
89
|
+
return [];
|
|
90
|
+
if (!history.length)
|
|
91
|
+
return dom;
|
|
92
|
+
return dom.filter((d) => !history.some((h) => messagesEquivalent(h, d)));
|
|
93
|
+
}
|
|
94
|
+
pruneDomOverlay(agentId) {
|
|
95
|
+
const history = this.jsonlBaseline.get(agentId) ?? [];
|
|
96
|
+
if (!history.length)
|
|
97
|
+
return;
|
|
98
|
+
this.domTranscript.pruneCoveredBy(agentId, history);
|
|
28
99
|
}
|
|
29
100
|
}
|
|
@@ -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[];
|