cursorconnect 0.1.7 → 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 +7 -1
- package/bridge-runtime/connector-version.json +1 -1
- package/bridge-runtime/dist/agent-completion-push.d.ts +27 -22
- package/bridge-runtime/dist/agent-completion-push.js +242 -122
- 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 +99 -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/config.js +2 -0
- package/bridge-runtime/dist/connector-client-version.js +1 -1
- 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 +3 -1
- package/bridge-runtime/dist/dom-transcript-store.js +18 -3
- package/bridge-runtime/dist/extract-page.js +5 -4
- package/bridge-runtime/dist/index.js +9 -0
- package/bridge-runtime/dist/keep-awake.d.ts +5 -0
- package/bridge-runtime/dist/keep-awake.js +48 -0
- 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-upstream.d.ts +3 -0
- package/bridge-runtime/dist/relay-upstream.js +21 -0
- package/bridge-runtime/dist/relay.d.ts +47 -3
- package/bridge-runtime/dist/relay.js +667 -96
- package/bridge-runtime/dist/types.d.ts +13 -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 +2 -2
|
@@ -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
|
|
@@ -13,7 +15,7 @@ WEBAPP_PASSWORD=
|
|
|
13
15
|
# CURSOR_PROJECTS_DIR=
|
|
14
16
|
LOG_LEVEL=info
|
|
15
17
|
|
|
16
|
-
# Chat lenta: JSONL → agent:messages
|
|
18
|
+
# Chat lenta: JSONL → agent:messages; liveMessages = DOM overlay until turn is in .jsonl.
|
|
17
19
|
# CHAT_HISTORY_JSONL=1 — extra socket agent:history on app (legacy).
|
|
18
20
|
# CHAT_HISTORY_JSONL=1
|
|
19
21
|
# Follow active Cursor window in CDP (1.5s poll). Set FOCUS_SYNC_ENABLED=0 to disable.
|
|
@@ -25,3 +27,7 @@ LOG_LEVEL=info
|
|
|
25
27
|
# RELAY_URL=https://your-domain.example
|
|
26
28
|
# RELAY_TOKEN=same-as-relay-.env
|
|
27
29
|
# RELAY_ROOM_ID=default
|
|
30
|
+
# macOS: не давать Mac уснуть, пока bridge запущен (cursorconnect start). Выключить: KEEP_AWAKE=0
|
|
31
|
+
# KEEP_AWAKE=0
|
|
32
|
+
# Периодический ping relay upstream (NAT/idle). 0 = выкл.
|
|
33
|
+
# RELAY_KEEPALIVE_MS=20000
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"cliVersion":"0.1.
|
|
1
|
+
{"cliVersion":"0.1.9","bundledAt":"2026-05-26T11:11:21Z"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { type AgentCompletionReadinessDeps } from './agent-completion-readiness.js';
|
|
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;
|
|
4
5
|
export interface AgentCompletionPushPayload {
|
|
@@ -7,36 +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 when
|
|
12
|
-
* If JSONL flushes while DOM still shows "working", retry when DOM goes idle.
|
|
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;
|
|
17
21
|
private getState;
|
|
18
|
-
|
|
19
|
-
private lastJsonlAssistantKey;
|
|
20
|
-
/** Agents that were generating on previous DOM poll. */
|
|
22
|
+
private deps;
|
|
21
23
|
private prevWorking;
|
|
24
|
+
private workingPolls;
|
|
25
|
+
private confirmedWorking;
|
|
26
|
+
private idlePolls;
|
|
22
27
|
private lastEmitAt;
|
|
23
|
-
|
|
24
|
-
private
|
|
25
|
-
private
|
|
26
|
-
private
|
|
27
|
-
private
|
|
28
|
+
private emittedThisSession;
|
|
29
|
+
private pendingContent;
|
|
30
|
+
private contentTimers;
|
|
31
|
+
private lastSkipLogAt;
|
|
32
|
+
private static readonly WORKING_POLLS_REQUIRED;
|
|
33
|
+
private static readonly IDLE_POLLS_REQUIRED;
|
|
28
34
|
private static readonly MIN_EMIT_GAP_MS;
|
|
29
|
-
|
|
30
|
-
private static readonly
|
|
31
|
-
constructor(emit: AgentCompletionPushEmit, agentsIndex: () => AgentsIndex | null, getState: () => CursorState);
|
|
32
|
-
/** Sync working snapshot; flush pending push when a task leaves DOM "working". */
|
|
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);
|
|
33
38
|
observe(state: CursorState): void;
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
private scheduleDeferTimeout;
|
|
40
|
-
private clearDeferTimer;
|
|
39
|
+
retryPendingContent(state: CursorState): void;
|
|
40
|
+
private logSkip;
|
|
41
|
+
private trackWorkingSessions;
|
|
42
|
+
private detectIdleCompletions;
|
|
43
|
+
private syncPrevWorking;
|
|
41
44
|
private tryEmit;
|
|
45
|
+
private scheduleContentTimeout;
|
|
46
|
+
private clearContentTimer;
|
|
42
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) {
|
|
@@ -9,15 +11,63 @@ function isSuppressed() {
|
|
|
9
11
|
function normalizeTitle(title) {
|
|
10
12
|
return title.trim().replace(/\s+/g, ' ');
|
|
11
13
|
}
|
|
12
|
-
/** Ephemeral sidebar ids (tab-0) must not drive push — composerId can appear next poll. */
|
|
13
14
|
function isStableComposerId(id) {
|
|
14
15
|
if (!id?.trim())
|
|
15
16
|
return false;
|
|
16
17
|
const t = id.trim();
|
|
17
18
|
if (t.startsWith('tab-'))
|
|
18
19
|
return false;
|
|
20
|
+
if (t.startsWith('sidebar-'))
|
|
21
|
+
return false;
|
|
19
22
|
return t.length >= 8;
|
|
20
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
|
+
}
|
|
21
71
|
function titleForComposerId(agentId, state) {
|
|
22
72
|
const activeTitle = state.activeChatTitle?.trim();
|
|
23
73
|
if (state.activeComposerId === agentId && activeTitle)
|
|
@@ -26,11 +76,17 @@ function titleForComposerId(agentId, state) {
|
|
|
26
76
|
const id = tab.composerId ?? tab.id;
|
|
27
77
|
if (id === agentId)
|
|
28
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;
|
|
29
82
|
}
|
|
30
83
|
for (const repo of state.sidebarRepos ?? []) {
|
|
31
84
|
for (const agent of repo.agents) {
|
|
32
85
|
if (agent.id === agentId)
|
|
33
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;
|
|
34
90
|
}
|
|
35
91
|
}
|
|
36
92
|
return undefined;
|
|
@@ -47,174 +103,238 @@ function resolveAgentChatTitle(agentId, state, agentsIndex) {
|
|
|
47
103
|
}
|
|
48
104
|
return 'Чат';
|
|
49
105
|
}
|
|
50
|
-
/**
|
|
51
|
-
|
|
52
|
-
* Sidebar: stable composerId only (no tab-N flicker).
|
|
53
|
-
*/
|
|
54
|
-
function snapshotWorkingAgents(state, agentsIndex) {
|
|
106
|
+
/** Sidebar loading-indicator on list rows (`tabs` + `sidebarRepos`). */
|
|
107
|
+
function snapshotSidebarWorkingAgents(state, agentsIndex) {
|
|
55
108
|
const map = new Map();
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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 ?? '');
|
|
62
121
|
}
|
|
63
122
|
for (const tab of state.tabs) {
|
|
64
123
|
if (!tab.isWorking)
|
|
65
124
|
continue;
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
+
}
|
|
70
133
|
}
|
|
71
134
|
return map;
|
|
72
135
|
}
|
|
73
|
-
function agentPausedForInput(state) {
|
|
136
|
+
function agentPausedForInput(state, agentId) {
|
|
137
|
+
const isActive = state.activeComposerId === agentId;
|
|
138
|
+
if (!isActive)
|
|
139
|
+
return false;
|
|
74
140
|
return ((state.pendingApprovals?.length ?? 0) > 0 ||
|
|
75
141
|
!!state.pendingQuestionnaire ||
|
|
76
142
|
state.agentStatus === 'waiting_approval' ||
|
|
77
|
-
state.agentStatus === 'waiting_questionnaire'
|
|
143
|
+
state.agentStatus === 'waiting_questionnaire' ||
|
|
144
|
+
(state.agentStatus === 'background_shell' && state.agentWorking !== true));
|
|
78
145
|
}
|
|
79
|
-
function
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
146
|
+
function isStillWorkingWithTitle(title, now) {
|
|
147
|
+
const norm = normalizeTitle(title);
|
|
148
|
+
if (!norm)
|
|
149
|
+
return false;
|
|
150
|
+
for (const t of now.values()) {
|
|
151
|
+
if (normalizeTitle(t) === norm)
|
|
152
|
+
return true;
|
|
84
153
|
}
|
|
85
|
-
return
|
|
154
|
+
return false;
|
|
86
155
|
}
|
|
87
156
|
/**
|
|
88
|
-
* Push when
|
|
89
|
-
* If JSONL flushes while DOM still shows "working", retry when DOM goes idle.
|
|
157
|
+
* Push when sidebar loader off + archive/lenta ready for open-from-push.
|
|
90
158
|
*/
|
|
91
159
|
export class AgentCompletionPush {
|
|
92
160
|
emit;
|
|
93
161
|
agentsIndex;
|
|
94
162
|
getState;
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
163
|
+
deps;
|
|
164
|
+
prevWorking = null;
|
|
165
|
+
workingPolls = new Map();
|
|
166
|
+
confirmedWorking = new Set();
|
|
167
|
+
idlePolls = new Map();
|
|
99
168
|
lastEmitAt = new Map();
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
static
|
|
106
|
-
|
|
107
|
-
static
|
|
108
|
-
|
|
169
|
+
emittedThisSession = new Set();
|
|
170
|
+
pendingContent = new Set();
|
|
171
|
+
contentTimers = new Map();
|
|
172
|
+
lastSkipLogAt = new Map();
|
|
173
|
+
static WORKING_POLLS_REQUIRED = 2;
|
|
174
|
+
static IDLE_POLLS_REQUIRED = 1;
|
|
175
|
+
static MIN_EMIT_GAP_MS = 8_000;
|
|
176
|
+
static CONTENT_WAIT_MAX_MS = 12_000;
|
|
177
|
+
static SKIP_LOG_GAP_MS = 5_000;
|
|
178
|
+
constructor(emit, agentsIndex, getState, deps) {
|
|
109
179
|
this.emit = emit;
|
|
110
180
|
this.agentsIndex = agentsIndex;
|
|
111
181
|
this.getState = getState;
|
|
182
|
+
this.deps = deps;
|
|
112
183
|
}
|
|
113
|
-
/** Sync working snapshot; flush pending push when a task leaves DOM "working". */
|
|
114
184
|
observe(state) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
this.prevWorking = snapshotWorkingAgents(state, index);
|
|
185
|
+
if (isSuppressed()) {
|
|
186
|
+
this.syncPrevWorking(state);
|
|
118
187
|
return;
|
|
119
188
|
}
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
189
|
+
const index = this.agentsIndex();
|
|
190
|
+
const now = snapshotSidebarWorkingAgents(state, index);
|
|
191
|
+
this.trackWorkingSessions(now, state);
|
|
192
|
+
this.detectIdleCompletions(now, state);
|
|
193
|
+
this.retryPendingContent(state);
|
|
194
|
+
}
|
|
195
|
+
retryPendingContent(state) {
|
|
196
|
+
for (const agentId of [...this.pendingContent]) {
|
|
197
|
+
this.tryEmit(agentId, state, { sidebarIdle: true });
|
|
128
198
|
}
|
|
129
|
-
this.prevWorking = nowWorking;
|
|
130
199
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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);
|
|
200
|
+
logSkip(agentId, reason) {
|
|
201
|
+
const last = this.lastSkipLogAt.get(agentId) ?? 0;
|
|
202
|
+
if (Date.now() - last < AgentCompletionPush.SKIP_LOG_GAP_MS)
|
|
147
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);
|
|
148
212
|
}
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
this.
|
|
213
|
+
for (const id of now.keys()) {
|
|
214
|
+
const polls = (this.workingPolls.get(id) ?? 0) + 1;
|
|
215
|
+
this.workingPolls.set(id, polls);
|
|
216
|
+
if (polls >= AgentCompletionPush.WORKING_POLLS_REQUIRED) {
|
|
217
|
+
this.confirmedWorking.add(id);
|
|
218
|
+
this.emittedThisSession.delete(id);
|
|
219
|
+
this.idlePolls.delete(id);
|
|
220
|
+
this.pendingContent.delete(id);
|
|
221
|
+
this.clearContentTimer(id);
|
|
158
222
|
}
|
|
159
|
-
this.scheduleDeferTimeout(agentId);
|
|
160
|
-
console.log(`[agent-completion-push] defer agentId=${agentId.slice(0, 12)} line=${lineKey} (dom still working)`);
|
|
161
|
-
return;
|
|
162
223
|
}
|
|
163
|
-
this.
|
|
164
|
-
|
|
165
|
-
|
|
224
|
+
for (const id of this.workingPolls.keys()) {
|
|
225
|
+
if (!now.has(id))
|
|
226
|
+
this.workingPolls.delete(id);
|
|
227
|
+
}
|
|
166
228
|
}
|
|
167
|
-
|
|
168
|
-
|
|
229
|
+
detectIdleCompletions(now, state) {
|
|
230
|
+
const prev = this.prevWorking;
|
|
231
|
+
if (prev === null) {
|
|
232
|
+
this.prevWorking = new Map(now);
|
|
169
233
|
return;
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
this.
|
|
182
|
-
|
|
183
|
-
|
|
234
|
+
}
|
|
235
|
+
for (const [agentId] of prev) {
|
|
236
|
+
if (now.has(agentId)) {
|
|
237
|
+
this.idlePolls.delete(agentId);
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
if (!this.confirmedWorking.has(agentId)) {
|
|
241
|
+
this.logSkip(agentId, 'no-confirmed-working');
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
const polls = (this.idlePolls.get(agentId) ?? 0) + 1;
|
|
245
|
+
this.idlePolls.set(agentId, polls);
|
|
246
|
+
if (polls < AgentCompletionPush.IDLE_POLLS_REQUIRED)
|
|
247
|
+
continue;
|
|
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 });
|
|
250
|
+
}
|
|
251
|
+
for (const id of this.idlePolls.keys()) {
|
|
252
|
+
if (now.has(id))
|
|
253
|
+
this.idlePolls.delete(id);
|
|
254
|
+
}
|
|
255
|
+
this.prevWorking = new Map(now);
|
|
184
256
|
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if (t)
|
|
188
|
-
clearTimeout(t);
|
|
189
|
-
this.deferTimers.delete(agentId);
|
|
257
|
+
syncPrevWorking(state) {
|
|
258
|
+
this.prevWorking = snapshotSidebarWorkingAgents(state, this.agentsIndex());
|
|
190
259
|
}
|
|
191
|
-
tryEmit(agentId,
|
|
192
|
-
|
|
193
|
-
|
|
260
|
+
tryEmit(agentId, state, opts) {
|
|
261
|
+
if (agentPausedForInput(state, agentId)) {
|
|
262
|
+
this.logSkip(agentId, 'paused-for-input');
|
|
194
263
|
return;
|
|
195
|
-
|
|
196
|
-
if (
|
|
264
|
+
}
|
|
265
|
+
if (this.emittedThisSession.has(agentId)) {
|
|
266
|
+
this.logSkip(agentId, 'already-emitted');
|
|
197
267
|
return;
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
this.
|
|
201
|
-
if (!isCurrentTask)
|
|
268
|
+
}
|
|
269
|
+
if (!this.confirmedWorking.has(agentId)) {
|
|
270
|
+
this.logSkip(agentId, 'no-confirmed-working');
|
|
202
271
|
return;
|
|
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');
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const title = normalizeTitle(resolveAgentChatTitle(agentId, state, index)) || 'Чат';
|
|
282
|
+
if (isStillWorkingWithTitle(title, now)) {
|
|
283
|
+
this.logSkip(agentId, 'same-title-still-working');
|
|
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
|
+
}
|
|
203
304
|
const last = this.lastEmitAt.get(agentId) ?? 0;
|
|
204
|
-
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');
|
|
205
307
|
return;
|
|
206
|
-
|
|
207
|
-
resolveAgentChatTitle(agentId, state, this.agentsIndex())) || 'Чат';
|
|
308
|
+
}
|
|
208
309
|
this.emit({
|
|
209
310
|
agentId,
|
|
210
|
-
chatTitle,
|
|
311
|
+
chatTitle: title,
|
|
211
312
|
body: AGENT_COMPLETION_PUSH_BODY,
|
|
212
313
|
});
|
|
213
314
|
this.lastEmitAt.set(agentId, Date.now());
|
|
214
|
-
this.
|
|
215
|
-
this.
|
|
216
|
-
this.
|
|
217
|
-
this.
|
|
218
|
-
|
|
315
|
+
this.emittedThisSession.add(agentId);
|
|
316
|
+
this.confirmedWorking.delete(agentId);
|
|
317
|
+
this.idlePolls.delete(agentId);
|
|
318
|
+
this.workingPolls.delete(agentId);
|
|
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);
|
|
219
339
|
}
|
|
220
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
|
+
}
|