cursorconnect 0.1.8 → 0.1.9
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 +2 -0
- package/bridge-runtime/connector-version.json +1 -1
- package/bridge-runtime/dist/agent-completion-push.d.ts +18 -6
- package/bridge-runtime/dist/agent-completion-push.js +186 -41
- package/bridge-runtime/dist/agent-completion-readiness.d.ts +19 -0
- package/bridge-runtime/dist/agent-completion-readiness.js +42 -0
- package/bridge-runtime/dist/chat-display-store.d.ts +32 -7
- package/bridge-runtime/dist/chat-display-store.js +96 -21
- package/bridge-runtime/dist/chat-display.d.ts +36 -0
- package/bridge-runtime/dist/chat-display.js +287 -24
- package/bridge-runtime/dist/chat-sync.d.ts +3 -1
- package/bridge-runtime/dist/chat-sync.js +20 -0
- package/bridge-runtime/dist/debug-chats-page.d.ts +1 -1
- package/bridge-runtime/dist/debug-chats-page.js +148 -26
- package/bridge-runtime/dist/dom-transcript-store.d.ts +2 -0
- package/bridge-runtime/dist/dom-transcript-store.js +17 -2
- package/bridge-runtime/dist/extract-page.js +5 -4
- package/bridge-runtime/dist/lenta-capture.d.ts +46 -0
- package/bridge-runtime/dist/lenta-capture.js +146 -0
- package/bridge-runtime/dist/lenta-debug.d.ts +42 -0
- package/bridge-runtime/dist/lenta-debug.js +221 -0
- package/bridge-runtime/dist/lenta-delivery.d.ts +3 -0
- package/bridge-runtime/dist/lenta-delivery.js +10 -0
- package/bridge-runtime/dist/lenta-seq-journal.d.ts +48 -0
- package/bridge-runtime/dist/lenta-seq-journal.js +109 -0
- package/bridge-runtime/dist/message-filter.d.ts +5 -0
- package/bridge-runtime/dist/message-filter.js +4 -0
- package/bridge-runtime/dist/relay.d.ts +37 -3
- package/bridge-runtime/dist/relay.js +557 -51
- package/bridge-runtime/dist/types.d.ts +9 -4
- package/dist/bridge-build.js +50 -0
- package/dist/index.js +9 -6
- package/dist/launch.js +5 -1
- package/dist/run-service.js +10 -4
- package/dist/startup-check.js +6 -0
- package/package.json +1 -1
- package/version-policy.json +1 -1
|
@@ -3,6 +3,8 @@ SERVER_HOST=127.0.0.1
|
|
|
3
3
|
SERVER_PORT=3847
|
|
4
4
|
POLL_INTERVAL_MS=400
|
|
5
5
|
DEBOUNCE_MS=150
|
|
6
|
+
# Coalesce socket agent:messages for DOM overlay (store ingest is immediate)
|
|
7
|
+
# DOM_OVERLAY_EMIT_MS=50
|
|
6
8
|
# JSONL live lenta: poll subscribed (120ms), live watcher on subscribe (immediate), debounce off for live
|
|
7
9
|
# JSONL_POLL_MS=120
|
|
8
10
|
# JSONL_EMIT_DEBOUNCE_MS=0
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"cliVersion":"0.1.
|
|
1
|
+
{"cliVersion":"0.1.9","bundledAt":"2026-05-26T11:11:21Z"}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type AgentCompletionReadinessDeps } from './agent-completion-readiness.js';
|
|
1
2
|
import type { AgentsIndex, CursorState } from './types.js';
|
|
2
3
|
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
4
|
export declare function suppressAgentCompletionPush(ms?: number): void;
|
|
@@ -7,29 +8,40 @@ export interface AgentCompletionPushPayload {
|
|
|
7
8
|
body: string;
|
|
8
9
|
}
|
|
9
10
|
export type AgentCompletionPushEmit = (payload: AgentCompletionPushPayload) => void;
|
|
11
|
+
export interface AgentCompletionPushDeps {
|
|
12
|
+
readiness: AgentCompletionReadinessDeps;
|
|
13
|
+
requestContentSync(agentId: string, state: CursorState): void;
|
|
14
|
+
}
|
|
10
15
|
/**
|
|
11
|
-
* Push
|
|
12
|
-
* then stably idle (≥2 polls). Opening a finished chat may flicker isWorking for 1 poll — ignored.
|
|
16
|
+
* Push when sidebar loader off + archive/lenta ready for open-from-push.
|
|
13
17
|
*/
|
|
14
18
|
export declare class AgentCompletionPush {
|
|
15
19
|
private emit;
|
|
16
20
|
private agentsIndex;
|
|
21
|
+
private getState;
|
|
22
|
+
private deps;
|
|
17
23
|
private prevWorking;
|
|
18
24
|
private workingPolls;
|
|
19
|
-
/** Agent had a real generation episode this bridge session (not open-chat flicker). */
|
|
20
25
|
private confirmedWorking;
|
|
21
26
|
private idlePolls;
|
|
22
27
|
private lastEmitAt;
|
|
23
28
|
private emittedThisSession;
|
|
24
|
-
|
|
29
|
+
private pendingContent;
|
|
30
|
+
private contentTimers;
|
|
31
|
+
private lastSkipLogAt;
|
|
25
32
|
private static readonly WORKING_POLLS_REQUIRED;
|
|
26
|
-
/** ~2 × 400ms idle before push. */
|
|
27
33
|
private static readonly IDLE_POLLS_REQUIRED;
|
|
28
34
|
private static readonly MIN_EMIT_GAP_MS;
|
|
29
|
-
|
|
35
|
+
private static readonly CONTENT_WAIT_MAX_MS;
|
|
36
|
+
private static readonly SKIP_LOG_GAP_MS;
|
|
37
|
+
constructor(emit: AgentCompletionPushEmit, agentsIndex: () => AgentsIndex | null, getState: () => CursorState, deps: AgentCompletionPushDeps);
|
|
30
38
|
observe(state: CursorState): void;
|
|
39
|
+
retryPendingContent(state: CursorState): void;
|
|
40
|
+
private logSkip;
|
|
31
41
|
private trackWorkingSessions;
|
|
32
42
|
private detectIdleCompletions;
|
|
33
43
|
private syncPrevWorking;
|
|
34
44
|
private tryEmit;
|
|
45
|
+
private scheduleContentTimeout;
|
|
46
|
+
private clearContentTimer;
|
|
35
47
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { isComposerUuid, normalizeAgentTitle } from './chat-sync.js';
|
|
2
|
+
import { assessAgentCompletionReadiness, } from './agent-completion-readiness.js';
|
|
1
3
|
export const AGENT_COMPLETION_PUSH_BODY = 'Агент завершил работу';
|
|
2
4
|
let suppressUntil = 0;
|
|
3
5
|
export function suppressAgentCompletionPush(ms = 6000) {
|
|
@@ -15,8 +17,57 @@ function isStableComposerId(id) {
|
|
|
15
17
|
const t = id.trim();
|
|
16
18
|
if (t.startsWith('tab-'))
|
|
17
19
|
return false;
|
|
20
|
+
if (t.startsWith('sidebar-'))
|
|
21
|
+
return false;
|
|
18
22
|
return t.length >= 8;
|
|
19
23
|
}
|
|
24
|
+
function lookupComposerIdByTitle(title, map) {
|
|
25
|
+
if (!title?.trim() || !map)
|
|
26
|
+
return undefined;
|
|
27
|
+
return map[normalizeAgentTitle(title)];
|
|
28
|
+
}
|
|
29
|
+
function resolveAgentIdFromIndex(title, agentsIndex) {
|
|
30
|
+
const norm = normalizeAgentTitle(title);
|
|
31
|
+
if (!norm || !agentsIndex)
|
|
32
|
+
return undefined;
|
|
33
|
+
for (const repo of agentsIndex.repos) {
|
|
34
|
+
for (const agent of repo.agents) {
|
|
35
|
+
if (normalizeAgentTitle(agent.title) !== norm)
|
|
36
|
+
continue;
|
|
37
|
+
if (isStableComposerId(agent.id) || isComposerUuid(agent.id))
|
|
38
|
+
return agent.id.trim();
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return undefined;
|
|
42
|
+
}
|
|
43
|
+
function resolveSidebarAgentId(rawId, title, state, agentsIndex) {
|
|
44
|
+
if (isStableComposerId(rawId))
|
|
45
|
+
return rawId.trim();
|
|
46
|
+
if (isComposerUuid(rawId))
|
|
47
|
+
return rawId.trim();
|
|
48
|
+
const activeId = state.activeComposerId?.trim();
|
|
49
|
+
const activeTab = state.tabs.find((t) => t.active);
|
|
50
|
+
if (activeTab?.isWorking && activeId && isStableComposerId(activeId)) {
|
|
51
|
+
const raw = String(rawId).trim();
|
|
52
|
+
const tabComposer = activeTab.composerId ? String(activeTab.composerId).trim() : '';
|
|
53
|
+
const tabTitle = normalizeAgentTitle(activeTab.title ?? '');
|
|
54
|
+
const titleNorm = normalizeAgentTitle(title);
|
|
55
|
+
const sameRow = raw === activeTab.id ||
|
|
56
|
+
(tabComposer && raw === tabComposer) ||
|
|
57
|
+
!titleNorm ||
|
|
58
|
+
tabTitle === titleNorm;
|
|
59
|
+
if (sameRow && (!titleNorm || tabTitle === titleNorm || !tabTitle)) {
|
|
60
|
+
return activeId;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const fromTitle = lookupComposerIdByTitle(title, state.composerIdByTitle);
|
|
64
|
+
if (fromTitle && isStableComposerId(fromTitle))
|
|
65
|
+
return fromTitle;
|
|
66
|
+
const fromIndex = resolveAgentIdFromIndex(title, agentsIndex);
|
|
67
|
+
if (fromIndex)
|
|
68
|
+
return fromIndex;
|
|
69
|
+
return undefined;
|
|
70
|
+
}
|
|
20
71
|
function titleForComposerId(agentId, state) {
|
|
21
72
|
const activeTitle = state.activeChatTitle?.trim();
|
|
22
73
|
if (state.activeComposerId === agentId && activeTitle)
|
|
@@ -25,11 +76,17 @@ function titleForComposerId(agentId, state) {
|
|
|
25
76
|
const id = tab.composerId ?? tab.id;
|
|
26
77
|
if (id === agentId)
|
|
27
78
|
return tab.title?.trim() || undefined;
|
|
79
|
+
const resolved = resolveSidebarAgentId(tab.composerId ?? tab.id, tab.title ?? '', state);
|
|
80
|
+
if (resolved === agentId)
|
|
81
|
+
return tab.title?.trim() || undefined;
|
|
28
82
|
}
|
|
29
83
|
for (const repo of state.sidebarRepos ?? []) {
|
|
30
84
|
for (const agent of repo.agents) {
|
|
31
85
|
if (agent.id === agentId)
|
|
32
86
|
return agent.title?.trim() || undefined;
|
|
87
|
+
const resolved = resolveSidebarAgentId(agent.id, agent.title, state);
|
|
88
|
+
if (resolved === agentId)
|
|
89
|
+
return agent.title?.trim() || undefined;
|
|
33
90
|
}
|
|
34
91
|
}
|
|
35
92
|
return undefined;
|
|
@@ -46,26 +103,33 @@ function resolveAgentChatTitle(agentId, state, agentsIndex) {
|
|
|
46
103
|
}
|
|
47
104
|
return 'Чат';
|
|
48
105
|
}
|
|
49
|
-
/**
|
|
50
|
-
function
|
|
106
|
+
/** Sidebar loading-indicator on list rows (`tabs` + `sidebarRepos`). */
|
|
107
|
+
function snapshotSidebarWorkingAgents(state, agentsIndex) {
|
|
51
108
|
const map = new Map();
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
map.set(
|
|
109
|
+
const add = (rawId, title) => {
|
|
110
|
+
const id = resolveSidebarAgentId(rawId, title, state, agentsIndex);
|
|
111
|
+
if (!id)
|
|
112
|
+
return;
|
|
113
|
+
if (agentPausedForInput(state, id))
|
|
114
|
+
return;
|
|
115
|
+
map.set(id, title.trim() || resolveAgentChatTitle(id, state, agentsIndex));
|
|
116
|
+
};
|
|
117
|
+
const activeId = state.activeComposerId?.trim();
|
|
118
|
+
const activeTab = state.tabs.find((t) => t.active);
|
|
119
|
+
if (activeTab?.isWorking && activeId && isStableComposerId(activeId)) {
|
|
120
|
+
add(activeId, activeTab.title ?? state.activeChatTitle ?? '');
|
|
59
121
|
}
|
|
60
122
|
for (const tab of state.tabs) {
|
|
61
123
|
if (!tab.isWorking)
|
|
62
124
|
continue;
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
125
|
+
add(tab.composerId ?? tab.id, tab.title ?? '');
|
|
126
|
+
}
|
|
127
|
+
for (const repo of state.sidebarRepos ?? []) {
|
|
128
|
+
for (const agent of repo.agents) {
|
|
129
|
+
if (!agent.isWorking)
|
|
130
|
+
continue;
|
|
131
|
+
add(agent.id, agent.title);
|
|
132
|
+
}
|
|
69
133
|
}
|
|
70
134
|
return map;
|
|
71
135
|
}
|
|
@@ -76,7 +140,8 @@ function agentPausedForInput(state, agentId) {
|
|
|
76
140
|
return ((state.pendingApprovals?.length ?? 0) > 0 ||
|
|
77
141
|
!!state.pendingQuestionnaire ||
|
|
78
142
|
state.agentStatus === 'waiting_approval' ||
|
|
79
|
-
state.agentStatus === 'waiting_questionnaire'
|
|
143
|
+
state.agentStatus === 'waiting_questionnaire' ||
|
|
144
|
+
(state.agentStatus === 'background_shell' && state.agentWorking !== true));
|
|
80
145
|
}
|
|
81
146
|
function isStillWorkingWithTitle(title, now) {
|
|
82
147
|
const norm = normalizeTitle(title);
|
|
@@ -89,27 +154,32 @@ function isStillWorkingWithTitle(title, now) {
|
|
|
89
154
|
return false;
|
|
90
155
|
}
|
|
91
156
|
/**
|
|
92
|
-
* Push
|
|
93
|
-
* then stably idle (≥2 polls). Opening a finished chat may flicker isWorking for 1 poll — ignored.
|
|
157
|
+
* Push when sidebar loader off + archive/lenta ready for open-from-push.
|
|
94
158
|
*/
|
|
95
159
|
export class AgentCompletionPush {
|
|
96
160
|
emit;
|
|
97
161
|
agentsIndex;
|
|
162
|
+
getState;
|
|
163
|
+
deps;
|
|
98
164
|
prevWorking = null;
|
|
99
165
|
workingPolls = new Map();
|
|
100
|
-
/** Agent had a real generation episode this bridge session (not open-chat flicker). */
|
|
101
166
|
confirmedWorking = new Set();
|
|
102
167
|
idlePolls = new Map();
|
|
103
168
|
lastEmitAt = new Map();
|
|
104
169
|
emittedThisSession = new Set();
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
static
|
|
170
|
+
pendingContent = new Set();
|
|
171
|
+
contentTimers = new Map();
|
|
172
|
+
lastSkipLogAt = new Map();
|
|
173
|
+
static WORKING_POLLS_REQUIRED = 2;
|
|
174
|
+
static IDLE_POLLS_REQUIRED = 1;
|
|
109
175
|
static MIN_EMIT_GAP_MS = 8_000;
|
|
110
|
-
|
|
176
|
+
static CONTENT_WAIT_MAX_MS = 12_000;
|
|
177
|
+
static SKIP_LOG_GAP_MS = 5_000;
|
|
178
|
+
constructor(emit, agentsIndex, getState, deps) {
|
|
111
179
|
this.emit = emit;
|
|
112
180
|
this.agentsIndex = agentsIndex;
|
|
181
|
+
this.getState = getState;
|
|
182
|
+
this.deps = deps;
|
|
113
183
|
}
|
|
114
184
|
observe(state) {
|
|
115
185
|
if (isSuppressed()) {
|
|
@@ -117,18 +187,38 @@ export class AgentCompletionPush {
|
|
|
117
187
|
return;
|
|
118
188
|
}
|
|
119
189
|
const index = this.agentsIndex();
|
|
120
|
-
const now =
|
|
121
|
-
this.trackWorkingSessions(now);
|
|
190
|
+
const now = snapshotSidebarWorkingAgents(state, index);
|
|
191
|
+
this.trackWorkingSessions(now, state);
|
|
122
192
|
this.detectIdleCompletions(now, state);
|
|
193
|
+
this.retryPendingContent(state);
|
|
123
194
|
}
|
|
124
|
-
|
|
195
|
+
retryPendingContent(state) {
|
|
196
|
+
for (const agentId of [...this.pendingContent]) {
|
|
197
|
+
this.tryEmit(agentId, state, { sidebarIdle: true });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
logSkip(agentId, reason) {
|
|
201
|
+
const last = this.lastSkipLogAt.get(agentId) ?? 0;
|
|
202
|
+
if (Date.now() - last < AgentCompletionPush.SKIP_LOG_GAP_MS)
|
|
203
|
+
return;
|
|
204
|
+
this.lastSkipLogAt.set(agentId, Date.now());
|
|
205
|
+
console.log(`[agent-completion-push] skip agentId=${agentId.slice(0, 12)} reason=${reason}`);
|
|
206
|
+
}
|
|
207
|
+
trackWorkingSessions(now, state) {
|
|
208
|
+
const activeId = state.activeComposerId?.trim();
|
|
209
|
+
if (activeId && isStableComposerId(activeId) && now.has(activeId)) {
|
|
210
|
+
this.confirmedWorking.add(activeId);
|
|
211
|
+
this.emittedThisSession.delete(activeId);
|
|
212
|
+
}
|
|
125
213
|
for (const id of now.keys()) {
|
|
126
214
|
const polls = (this.workingPolls.get(id) ?? 0) + 1;
|
|
127
215
|
this.workingPolls.set(id, polls);
|
|
128
|
-
if (polls >= AgentCompletionPush.WORKING_POLLS_REQUIRED
|
|
216
|
+
if (polls >= AgentCompletionPush.WORKING_POLLS_REQUIRED) {
|
|
129
217
|
this.confirmedWorking.add(id);
|
|
130
218
|
this.emittedThisSession.delete(id);
|
|
131
219
|
this.idlePolls.delete(id);
|
|
220
|
+
this.pendingContent.delete(id);
|
|
221
|
+
this.clearContentTimer(id);
|
|
132
222
|
}
|
|
133
223
|
}
|
|
134
224
|
for (const id of this.workingPolls.keys()) {
|
|
@@ -147,13 +237,16 @@ export class AgentCompletionPush {
|
|
|
147
237
|
this.idlePolls.delete(agentId);
|
|
148
238
|
continue;
|
|
149
239
|
}
|
|
150
|
-
if (!this.confirmedWorking.has(agentId))
|
|
240
|
+
if (!this.confirmedWorking.has(agentId)) {
|
|
241
|
+
this.logSkip(agentId, 'no-confirmed-working');
|
|
151
242
|
continue;
|
|
243
|
+
}
|
|
152
244
|
const polls = (this.idlePolls.get(agentId) ?? 0) + 1;
|
|
153
245
|
this.idlePolls.set(agentId, polls);
|
|
154
246
|
if (polls < AgentCompletionPush.IDLE_POLLS_REQUIRED)
|
|
155
247
|
continue;
|
|
156
|
-
|
|
248
|
+
console.log(`[agent-completion-push] sidebar idle agentId=${agentId.slice(0, 12)} title=${(prev.get(agentId) ?? '').slice(0, 40)}`);
|
|
249
|
+
this.tryEmit(agentId, state, { sidebarIdle: true });
|
|
157
250
|
}
|
|
158
251
|
for (const id of this.idlePolls.keys()) {
|
|
159
252
|
if (now.has(id))
|
|
@@ -162,24 +255,57 @@ export class AgentCompletionPush {
|
|
|
162
255
|
this.prevWorking = new Map(now);
|
|
163
256
|
}
|
|
164
257
|
syncPrevWorking(state) {
|
|
165
|
-
this.prevWorking =
|
|
258
|
+
this.prevWorking = snapshotSidebarWorkingAgents(state, this.agentsIndex());
|
|
166
259
|
}
|
|
167
|
-
tryEmit(agentId, state) {
|
|
168
|
-
if (agentPausedForInput(state, agentId))
|
|
260
|
+
tryEmit(agentId, state, opts) {
|
|
261
|
+
if (agentPausedForInput(state, agentId)) {
|
|
262
|
+
this.logSkip(agentId, 'paused-for-input');
|
|
169
263
|
return;
|
|
170
|
-
|
|
264
|
+
}
|
|
265
|
+
if (this.emittedThisSession.has(agentId)) {
|
|
266
|
+
this.logSkip(agentId, 'already-emitted');
|
|
171
267
|
return;
|
|
172
|
-
|
|
268
|
+
}
|
|
269
|
+
if (!this.confirmedWorking.has(agentId)) {
|
|
270
|
+
this.logSkip(agentId, 'no-confirmed-working');
|
|
173
271
|
return;
|
|
174
|
-
|
|
175
|
-
|
|
272
|
+
}
|
|
273
|
+
const index = this.agentsIndex();
|
|
274
|
+
const now = snapshotSidebarWorkingAgents(state, index);
|
|
275
|
+
if (now.has(agentId)) {
|
|
276
|
+
this.pendingContent.delete(agentId);
|
|
277
|
+
this.clearContentTimer(agentId);
|
|
278
|
+
this.logSkip(agentId, 'sidebar-still-working');
|
|
176
279
|
return;
|
|
177
|
-
|
|
178
|
-
|
|
280
|
+
}
|
|
281
|
+
const title = normalizeTitle(resolveAgentChatTitle(agentId, state, index)) || 'Чат';
|
|
282
|
+
if (isStillWorkingWithTitle(title, now)) {
|
|
283
|
+
this.logSkip(agentId, 'same-title-still-working');
|
|
179
284
|
return;
|
|
285
|
+
}
|
|
286
|
+
const sidebarIdle = opts?.sidebarIdle === true;
|
|
287
|
+
let contentDetail = 'no-check';
|
|
288
|
+
if (!opts?.forceContent) {
|
|
289
|
+
const readiness = assessAgentCompletionReadiness(agentId, state, this.deps.readiness, { sidebarIdle });
|
|
290
|
+
contentDetail = readiness.detail;
|
|
291
|
+
if (!readiness.ready) {
|
|
292
|
+
if (!this.pendingContent.has(agentId)) {
|
|
293
|
+
this.pendingContent.add(agentId);
|
|
294
|
+
console.log(`[agent-completion-push] wait content agentId=${agentId.slice(0, 12)} detail=${readiness.detail}`);
|
|
295
|
+
this.deps.requestContentSync(agentId, state);
|
|
296
|
+
this.scheduleContentTimeout(agentId);
|
|
297
|
+
}
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
contentDetail = 'content-timeout';
|
|
303
|
+
}
|
|
180
304
|
const last = this.lastEmitAt.get(agentId) ?? 0;
|
|
181
|
-
if (Date.now() - last < AgentCompletionPush.MIN_EMIT_GAP_MS)
|
|
305
|
+
if (Date.now() - last < AgentCompletionPush.MIN_EMIT_GAP_MS) {
|
|
306
|
+
this.logSkip(agentId, 'emit-gap');
|
|
182
307
|
return;
|
|
308
|
+
}
|
|
183
309
|
this.emit({
|
|
184
310
|
agentId,
|
|
185
311
|
chatTitle: title,
|
|
@@ -190,6 +316,25 @@ export class AgentCompletionPush {
|
|
|
190
316
|
this.confirmedWorking.delete(agentId);
|
|
191
317
|
this.idlePolls.delete(agentId);
|
|
192
318
|
this.workingPolls.delete(agentId);
|
|
193
|
-
|
|
319
|
+
this.pendingContent.delete(agentId);
|
|
320
|
+
this.clearContentTimer(agentId);
|
|
321
|
+
console.log(`[agent-completion-push] → relay reason=sidebar-idle+${contentDetail} agentId=${agentId.slice(0, 12)} title=${title.slice(0, 48)}`);
|
|
322
|
+
}
|
|
323
|
+
scheduleContentTimeout(agentId) {
|
|
324
|
+
this.clearContentTimer(agentId);
|
|
325
|
+
const timer = setTimeout(() => {
|
|
326
|
+
this.contentTimers.delete(agentId);
|
|
327
|
+
if (!this.pendingContent.has(agentId))
|
|
328
|
+
return;
|
|
329
|
+
console.log(`[agent-completion-push] content wait timeout agentId=${agentId.slice(0, 12)} — push anyway`);
|
|
330
|
+
this.tryEmit(agentId, this.getState(), { forceContent: true, sidebarIdle: true });
|
|
331
|
+
}, AgentCompletionPush.CONTENT_WAIT_MAX_MS);
|
|
332
|
+
this.contentTimers.set(agentId, timer);
|
|
333
|
+
}
|
|
334
|
+
clearContentTimer(agentId) {
|
|
335
|
+
const t = this.contentTimers.get(agentId);
|
|
336
|
+
if (t)
|
|
337
|
+
clearTimeout(t);
|
|
338
|
+
this.contentTimers.delete(agentId);
|
|
194
339
|
}
|
|
195
340
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { ChatMessage, CursorState } from './types.js';
|
|
2
|
+
export interface AgentCompletionReadinessDeps {
|
|
3
|
+
isSubscribed(agentId: string): boolean;
|
|
4
|
+
getSubscribeTitle(agentId: string): string | undefined;
|
|
5
|
+
getDisplayMessages(agentId: string): ChatMessage[];
|
|
6
|
+
getJsonlHistory(agentId: string): ChatMessage[];
|
|
7
|
+
}
|
|
8
|
+
export interface AgentCompletionReadinessOpts {
|
|
9
|
+
/** Sidebar loader already off for this agent (DOM list). */
|
|
10
|
+
sidebarIdle?: boolean;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Push when opening the chat shows a complete turn.
|
|
14
|
+
* When sidebar is idle, do not block on `lenta-pending` (DOM overlay may still churn seq).
|
|
15
|
+
*/
|
|
16
|
+
export declare function assessAgentCompletionReadiness(agentId: string, state: CursorState, deps: AgentCompletionReadinessDeps, opts?: AgentCompletionReadinessOpts): {
|
|
17
|
+
ready: boolean;
|
|
18
|
+
detail: string;
|
|
19
|
+
};
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { isChatSyncedWithCursor } from './chat-sync.js';
|
|
2
|
+
import { isLentaDeliveryPending } from './lenta-delivery.js';
|
|
3
|
+
function hasAssistantTail(history) {
|
|
4
|
+
const last = history[history.length - 1];
|
|
5
|
+
return !!last && last.role === 'assistant' && !!(last.text?.trim() || last.html?.trim());
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Push when opening the chat shows a complete turn.
|
|
9
|
+
* When sidebar is idle, do not block on `lenta-pending` (DOM overlay may still churn seq).
|
|
10
|
+
*/
|
|
11
|
+
export function assessAgentCompletionReadiness(agentId, state, deps, opts) {
|
|
12
|
+
const messages = deps.getDisplayMessages(agentId);
|
|
13
|
+
const history = deps.getJsonlHistory(agentId);
|
|
14
|
+
const subscribed = deps.isSubscribed(agentId);
|
|
15
|
+
const title = deps.getSubscribeTitle(agentId);
|
|
16
|
+
const sidebarIdle = opts?.sidebarIdle === true;
|
|
17
|
+
if (sidebarIdle && hasAssistantTail(history)) {
|
|
18
|
+
if (subscribed && title && isChatSyncedWithCursor(agentId, title, state)) {
|
|
19
|
+
if (isLentaDeliveryPending(agentId, messages)) {
|
|
20
|
+
return { ready: true, detail: 'jsonl-ready-sidebar-idle' };
|
|
21
|
+
}
|
|
22
|
+
return { ready: true, detail: 'lenta-delivered' };
|
|
23
|
+
}
|
|
24
|
+
return { ready: true, detail: 'jsonl-archive' };
|
|
25
|
+
}
|
|
26
|
+
if (subscribed && title && isChatSyncedWithCursor(agentId, title, state)) {
|
|
27
|
+
if (!messages.length && !history.length) {
|
|
28
|
+
return { ready: false, detail: 'lenta-empty' };
|
|
29
|
+
}
|
|
30
|
+
if (isLentaDeliveryPending(agentId, messages)) {
|
|
31
|
+
return { ready: false, detail: 'lenta-pending' };
|
|
32
|
+
}
|
|
33
|
+
return { ready: true, detail: 'lenta-delivered' };
|
|
34
|
+
}
|
|
35
|
+
if (hasAssistantTail(history)) {
|
|
36
|
+
if (subscribed && messages.length && isLentaDeliveryPending(agentId, messages)) {
|
|
37
|
+
return { ready: false, detail: 'lenta-pending-bg' };
|
|
38
|
+
}
|
|
39
|
+
return { ready: true, detail: 'jsonl-archive' };
|
|
40
|
+
}
|
|
41
|
+
return { ready: false, detail: 'no-archive' };
|
|
42
|
+
}
|
|
@@ -1,25 +1,50 @@
|
|
|
1
1
|
import type { ChatMessage, HistoryMessage } from './types.js';
|
|
2
2
|
/**
|
|
3
|
-
* Chat UI store: JSONL baseline
|
|
4
|
-
*
|
|
3
|
+
* Chat UI store: JSONL baseline + DOM overlay tail.
|
|
4
|
+
*
|
|
5
|
+
* Invariant:
|
|
6
|
+
* - JSONL baseline = everything already in `.jsonl` (monotonic archive order).
|
|
7
|
+
* - DOM overlay = viewport rows **not covered** by JSONL (`archiveCoversOverlay`).
|
|
8
|
+
* - While a row streams in DOM but JSONL is shorter → overlay keeps it; when JSONL catches up → overlay drops (no duplicate).
|
|
9
|
+
* - App lenta = `[...jsonlHistory, ...domOverlay]` — never sort both by DOM flatIndex.
|
|
5
10
|
*/
|
|
6
11
|
export declare class ChatDisplayStore {
|
|
7
12
|
private readonly domTranscript;
|
|
13
|
+
/** Last CDP viewport extract per agent — sole source for live overlay. */
|
|
14
|
+
private readonly latestDomViewport;
|
|
8
15
|
private readonly jsonlBaseline;
|
|
9
16
|
/** Raw JSONL row count per agent — incremental display merge during streaming. */
|
|
10
17
|
private readonly jsonlRowCount;
|
|
18
|
+
/** While true, bridge pushes lenta on DOM ingest (emit path); overlay filter unchanged. */
|
|
19
|
+
private readonly generatingByAgent;
|
|
11
20
|
clearAgent(agentId: string): void;
|
|
21
|
+
setAgentGenerating(agentId: string, generating: boolean): void;
|
|
22
|
+
isAgentGenerating(agentId: string): boolean;
|
|
12
23
|
getJsonlHistory(agentId: string): ChatMessage[];
|
|
13
24
|
getDomLive(agentId: string): ChatMessage[];
|
|
14
|
-
/** Reload JSONL;
|
|
25
|
+
/** Reload JSONL; drop viewport rows now covered by archive. */
|
|
15
26
|
setJsonlBaseline(agentId: string, rows: HistoryMessage[]): void;
|
|
16
27
|
/** Append JSONL rows during live stream; returns new/updated bubbles for socket `append` emit. */
|
|
17
28
|
appendJsonlRows(agentId: string, deltaRows: HistoryMessage[]): ChatMessage[];
|
|
18
|
-
/** Ingest DOM extract; overlay
|
|
29
|
+
/** Ingest DOM extract; overlay = viewport minus JSONL-covered rows. */
|
|
19
30
|
mergeLiveForAgent(agentId: string, rawDom: ChatMessage[]): void;
|
|
20
|
-
/**
|
|
31
|
+
/** Canonical UI lenta: archive then live tail (append-only, no cross-sort). */
|
|
21
32
|
getDisplayMessages(agentId: string): ChatMessage[];
|
|
22
|
-
|
|
23
|
-
|
|
33
|
+
/** Raw DOM ingest before JSONL filter / stamp (debug only). */
|
|
34
|
+
getDomTranscriptRaw(agentId: string): ChatMessage[];
|
|
35
|
+
/** Snapshot for `/debug/lenta` and lenta:watch. */
|
|
36
|
+
getLentaDebug(agentId: string): {
|
|
37
|
+
jsonlRowCount: number;
|
|
38
|
+
jsonlHistory: ChatMessage[];
|
|
39
|
+
domOverlay: ChatMessage[];
|
|
40
|
+
domRaw: ChatMessage[];
|
|
41
|
+
messages: ChatMessage[];
|
|
42
|
+
source: 'jsonl' | 'hybrid';
|
|
43
|
+
agentGenerating: boolean;
|
|
44
|
+
};
|
|
45
|
+
/** JSONL rows used to suppress DOM overlay (baseline + displayed archive). */
|
|
46
|
+
private archiveForOverlayMatch;
|
|
47
|
+
/** Shrink cached viewport to rows still missing from JSONL (handoff DOM → JSONL). */
|
|
48
|
+
private reconcileDomViewportToArchive;
|
|
24
49
|
private pruneDomOverlay;
|
|
25
50
|
}
|