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.
Files changed (37) hide show
  1. package/bridge-runtime/.env.example +2 -0
  2. package/bridge-runtime/connector-version.json +1 -1
  3. package/bridge-runtime/dist/agent-completion-push.d.ts +18 -6
  4. package/bridge-runtime/dist/agent-completion-push.js +186 -41
  5. package/bridge-runtime/dist/agent-completion-readiness.d.ts +19 -0
  6. package/bridge-runtime/dist/agent-completion-readiness.js +42 -0
  7. package/bridge-runtime/dist/chat-display-store.d.ts +32 -7
  8. package/bridge-runtime/dist/chat-display-store.js +96 -21
  9. package/bridge-runtime/dist/chat-display.d.ts +36 -0
  10. package/bridge-runtime/dist/chat-display.js +287 -24
  11. package/bridge-runtime/dist/chat-sync.d.ts +3 -1
  12. package/bridge-runtime/dist/chat-sync.js +20 -0
  13. package/bridge-runtime/dist/debug-chats-page.d.ts +1 -1
  14. package/bridge-runtime/dist/debug-chats-page.js +148 -26
  15. package/bridge-runtime/dist/dom-transcript-store.d.ts +2 -0
  16. package/bridge-runtime/dist/dom-transcript-store.js +17 -2
  17. package/bridge-runtime/dist/extract-page.js +5 -4
  18. package/bridge-runtime/dist/lenta-capture.d.ts +46 -0
  19. package/bridge-runtime/dist/lenta-capture.js +146 -0
  20. package/bridge-runtime/dist/lenta-debug.d.ts +42 -0
  21. package/bridge-runtime/dist/lenta-debug.js +221 -0
  22. package/bridge-runtime/dist/lenta-delivery.d.ts +3 -0
  23. package/bridge-runtime/dist/lenta-delivery.js +10 -0
  24. package/bridge-runtime/dist/lenta-seq-journal.d.ts +48 -0
  25. package/bridge-runtime/dist/lenta-seq-journal.js +109 -0
  26. package/bridge-runtime/dist/message-filter.d.ts +5 -0
  27. package/bridge-runtime/dist/message-filter.js +4 -0
  28. package/bridge-runtime/dist/relay.d.ts +37 -3
  29. package/bridge-runtime/dist/relay.js +557 -51
  30. package/bridge-runtime/dist/types.d.ts +9 -4
  31. package/dist/bridge-build.js +50 -0
  32. package/dist/index.js +9 -6
  33. package/dist/launch.js +5 -1
  34. package/dist/run-service.js +10 -4
  35. package/dist/startup-check.js +6 -0
  36. package/package.json +1 -1
  37. 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.8","bundledAt":"2026-05-26T06:15:47Z"}
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 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.
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
- /** ~3 × 400ms — ignore brief spinner when opening a finished chat. */
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
- constructor(emit: AgentCompletionPushEmit, agentsIndex: () => AgentsIndex | null);
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
- /** Active + background sidebar tabs that show generation. */
50
- function snapshotWorkingAgents(state, agentsIndex) {
106
+ /** Sidebar loading-indicator on list rows (`tabs` + `sidebarRepos`). */
107
+ function snapshotSidebarWorkingAgents(state, agentsIndex) {
51
108
  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));
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
- 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));
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 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.
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
- /** ~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;
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
- constructor(emit, agentsIndex) {
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 = snapshotWorkingAgents(state, index);
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
- trackWorkingSessions(now) {
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 && !this.confirmedWorking.has(id)) {
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
- this.tryEmit(agentId, state);
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 = snapshotWorkingAgents(state, this.agentsIndex());
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
- if (this.emittedThisSession.has(agentId))
264
+ }
265
+ if (this.emittedThisSession.has(agentId)) {
266
+ this.logSkip(agentId, 'already-emitted');
171
267
  return;
172
- if (!this.confirmedWorking.has(agentId))
268
+ }
269
+ if (!this.confirmedWorking.has(agentId)) {
270
+ this.logSkip(agentId, 'no-confirmed-working');
173
271
  return;
174
- const now = snapshotWorkingAgents(state, this.agentsIndex());
175
- if (now.has(agentId))
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
- const title = normalizeTitle(resolveAgentChatTitle(agentId, state, this.agentsIndex())) || 'Чат';
178
- if (isStillWorkingWithTitle(title, now))
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
- console.log(`[agent-completion-push] → relay dom-idle agentId=${agentId.slice(0, 12)} title=${title.slice(0, 48)}`);
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 for `agent:messages` (client lenta).
4
- * DOM overlay → `liveMessages` in `agent:messages` until JSONL has the same turn.
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; prune DOM rows now covered by archive. */
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 keeps only rows not in JSONL. */
29
+ /** Ingest DOM extract; overlay = viewport minus JSONL-covered rows. */
19
30
  mergeLiveForAgent(agentId: string, rawDom: ChatMessage[]): void;
20
- /** Debug / snapshot: combined view. */
31
+ /** Canonical UI lenta: archive then live tail (append-only, no cross-sort). */
21
32
  getDisplayMessages(agentId: string): ChatMessage[];
22
- getDomTranscript(agentId: string): ChatMessage[];
23
- private domOverlayOnly;
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
  }