lazy-gravity 0.1.0 → 0.2.0
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/README.md +18 -6
- package/dist/bin/cli.js +18 -18
- package/dist/bin/commands/doctor.js +2 -1
- package/dist/bin/commands/start.js +25 -2
- package/dist/bot/index.js +346 -152
- package/dist/commands/joinCommandHandler.js +302 -0
- package/dist/commands/joinDetachCommandHandler.js +285 -0
- package/dist/commands/registerSlashCommands.js +35 -0
- package/dist/database/chatSessionRepository.js +10 -0
- package/dist/database/userPreferenceRepository.js +72 -0
- package/dist/events/interactionCreateHandler.js +58 -36
- package/dist/events/messageCreateHandler.js +158 -53
- package/dist/services/antigravityLauncher.js +4 -3
- package/dist/services/approvalDetector.js +6 -0
- package/dist/services/cdpBridgeManager.js +184 -84
- package/dist/services/cdpConnectionPool.js +79 -51
- package/dist/services/cdpService.js +149 -51
- package/dist/services/chatSessionService.js +229 -8
- package/dist/services/errorPopupDetector.js +6 -0
- package/dist/services/planningDetector.js +6 -0
- package/dist/services/responseMonitor.js +125 -24
- package/dist/services/updateCheckService.js +147 -0
- package/dist/services/userMessageDetector.js +221 -0
- package/dist/ui/modeUi.js +11 -1
- package/dist/ui/outputUi.js +30 -0
- package/dist/ui/sessionPickerUi.js +48 -0
- package/dist/utils/antigravityPaths.js +94 -0
- package/dist/utils/configLoader.js +10 -0
- package/dist/utils/discordButtonUtils.js +33 -0
- package/dist/utils/logBuffer.js +47 -0
- package/dist/utils/logger.js +80 -20
- package/dist/utils/pathUtils.js +57 -0
- package/dist/utils/plainTextFormatter.js +70 -0
- package/package.json +4 -4
|
@@ -30,6 +30,118 @@ const GET_CHAT_TITLE_SCRIPT = `(() => {
|
|
|
30
30
|
const hasActiveChat = title.length > 0 && title !== 'Agent';
|
|
31
31
|
return { title: title || '(Untitled)', hasActiveChat };
|
|
32
32
|
})()`;
|
|
33
|
+
/**
|
|
34
|
+
* Script to find the Past Conversations button and return its coordinates.
|
|
35
|
+
* We use coordinates so that the actual click is done via CDP Input.dispatchMouseEvent,
|
|
36
|
+
* which works reliably in Electron (DOM .click() can be ignored).
|
|
37
|
+
*
|
|
38
|
+
* Returns: { found: boolean, x: number, y: number }
|
|
39
|
+
*/
|
|
40
|
+
const FIND_PAST_CONVERSATIONS_BUTTON_SCRIPT = `(() => {
|
|
41
|
+
const isVisible = (el) => !!el && el instanceof HTMLElement && el.offsetParent !== null;
|
|
42
|
+
const getRect = (el) => {
|
|
43
|
+
const rect = el.getBoundingClientRect();
|
|
44
|
+
return { found: true, x: Math.round(rect.x + rect.width / 2), y: Math.round(rect.y + rect.height / 2) };
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Strategy 1 (primary): data-past-conversations-toggle attribute
|
|
48
|
+
const toggle = document.querySelector('[data-past-conversations-toggle]');
|
|
49
|
+
if (toggle && isVisible(toggle)) return getRect(toggle);
|
|
50
|
+
|
|
51
|
+
// Strategy 2: data-tooltip-id containing "history"
|
|
52
|
+
const tooltipEls = Array.from(document.querySelectorAll('[data-tooltip-id]'));
|
|
53
|
+
for (const el of tooltipEls) {
|
|
54
|
+
if (!isVisible(el)) continue;
|
|
55
|
+
const tid = (el.getAttribute('data-tooltip-id') || '').toLowerCase();
|
|
56
|
+
if (tid.includes('history') || tid.includes('past-conversations')) {
|
|
57
|
+
return getRect(el);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Strategy 3: SVG with lucide-history class
|
|
62
|
+
const icons = Array.from(document.querySelectorAll('svg.lucide-history, svg[class*="lucide-history"]'));
|
|
63
|
+
for (const icon of icons) {
|
|
64
|
+
const parent = icon.closest('a, button, [role="button"], div[class*="cursor-pointer"]');
|
|
65
|
+
const target = parent instanceof HTMLElement && isVisible(parent) ? parent : icon;
|
|
66
|
+
if (isVisible(target)) return getRect(target);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
return { found: false, x: 0, y: 0 };
|
|
70
|
+
})()`;
|
|
71
|
+
/**
|
|
72
|
+
* Script to scrape session items from the open Past Conversations panel.
|
|
73
|
+
* Expects the panel to already be visible.
|
|
74
|
+
*
|
|
75
|
+
* Returns: { sessions: SessionListItem[] }
|
|
76
|
+
*/
|
|
77
|
+
const SCRAPE_PAST_CONVERSATIONS_SCRIPT = `(() => {
|
|
78
|
+
const isVisible = (el) => !!el && el instanceof HTMLElement && el.offsetParent !== null;
|
|
79
|
+
const normalize = (text) => (text || '').trim();
|
|
80
|
+
|
|
81
|
+
const items = [];
|
|
82
|
+
const seen = new Set();
|
|
83
|
+
|
|
84
|
+
// Find the scrollable conversation list container
|
|
85
|
+
const containers = Array.from(document.querySelectorAll('div[class*="overflow-auto"], div[class*="overflow-y-scroll"]'));
|
|
86
|
+
const container = containers.find((c) => isVisible(c) && c.querySelectorAll('div[class*="cursor-pointer"]').length > 0) || document;
|
|
87
|
+
|
|
88
|
+
// Detect the "Other Conversations" section boundary.
|
|
89
|
+
// Sessions below this header belong to other projects and must be excluded.
|
|
90
|
+
let boundaryTop = Infinity;
|
|
91
|
+
const headerCandidates = container.querySelectorAll('div[class*="text-xs"][class*="opacity"]');
|
|
92
|
+
for (const el of headerCandidates) {
|
|
93
|
+
if (!isVisible(el)) continue;
|
|
94
|
+
const t = normalize(el.textContent || '');
|
|
95
|
+
if (/^Other\\s+Conversations?$/i.test(t)) {
|
|
96
|
+
boundaryTop = el.getBoundingClientRect().top;
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Each session row is a div with cursor-pointer
|
|
102
|
+
const rows = Array.from(container.querySelectorAll('div[class*="cursor-pointer"]'));
|
|
103
|
+
for (const row of rows) {
|
|
104
|
+
if (!isVisible(row)) continue;
|
|
105
|
+
// Skip rows that are below the "Other Conversations" boundary
|
|
106
|
+
if (row.getBoundingClientRect().top >= boundaryTop) continue;
|
|
107
|
+
// Find the session title — nested span within the row
|
|
108
|
+
const spans = Array.from(row.querySelectorAll('span.text-sm span, span.text-sm'));
|
|
109
|
+
let title = '';
|
|
110
|
+
for (const span of spans) {
|
|
111
|
+
const t = normalize(span.textContent || '');
|
|
112
|
+
// Skip timestamp labels like "1 hr ago", "7 mins ago"
|
|
113
|
+
if (/^\\d+\\s+(min|hr|hour|day|sec|week|month|year)s?\\s+ago$/i.test(t)) continue;
|
|
114
|
+
// Skip very short or action-like labels
|
|
115
|
+
if (t.length < 2 || t.length > 200) continue;
|
|
116
|
+
if (/^(show\\s+\\d+\\s+more|new|past|history|settings|close|menu)\\b/i.test(t)) continue;
|
|
117
|
+
title = t;
|
|
118
|
+
break;
|
|
119
|
+
}
|
|
120
|
+
if (!title || seen.has(title)) continue;
|
|
121
|
+
seen.add(title);
|
|
122
|
+
// Detect if this is the active/current session (has focusBackground class)
|
|
123
|
+
const isActive = /focusBackground/i.test(row.className || '');
|
|
124
|
+
items.push({ title, isActive });
|
|
125
|
+
}
|
|
126
|
+
return { sessions: items };
|
|
127
|
+
})()`;
|
|
128
|
+
/**
|
|
129
|
+
* Script to find the "Show N more..." link and return its coordinates.
|
|
130
|
+
* Returns: { found: boolean, x: number, y: number }
|
|
131
|
+
*/
|
|
132
|
+
const FIND_SHOW_MORE_BUTTON_SCRIPT = `(() => {
|
|
133
|
+
const isVisible = (el) => !!el && el instanceof HTMLElement && el.offsetParent !== null;
|
|
134
|
+
const els = Array.from(document.querySelectorAll('div, span'));
|
|
135
|
+
for (const el of els) {
|
|
136
|
+
if (!isVisible(el)) continue;
|
|
137
|
+
const text = (el.textContent || '').trim();
|
|
138
|
+
if (/^Show\\s+\\d+\\s+more/i.test(text)) {
|
|
139
|
+
const rect = el.getBoundingClientRect();
|
|
140
|
+
return { found: true, x: Math.round(rect.x + rect.width / 2), y: Math.round(rect.y + rect.height / 2) };
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return { found: false, x: 0, y: 0 };
|
|
144
|
+
})()`;
|
|
33
145
|
/**
|
|
34
146
|
* Build a script that activates an existing chat in the side panel by its title.
|
|
35
147
|
* Uses broad selector fallbacks because Antigravity's DOM structure can vary across versions.
|
|
@@ -235,14 +347,35 @@ function buildActivateViaPastConversationsScript(title) {
|
|
|
235
347
|
};
|
|
236
348
|
|
|
237
349
|
return (async () => {
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
350
|
+
// Primary: click via data-past-conversations-toggle attribute
|
|
351
|
+
let opened = false;
|
|
352
|
+
const toggleBtn = document.querySelector('[data-past-conversations-toggle]');
|
|
353
|
+
if (toggleBtn && isVisible(toggleBtn)) {
|
|
354
|
+
const clickable = getClickable(toggleBtn);
|
|
355
|
+
if (clickable) { clickable.click(); opened = true; }
|
|
356
|
+
}
|
|
357
|
+
if (!opened) {
|
|
358
|
+
// Fallback: data-tooltip-id containing "history"
|
|
359
|
+
const tooltipEls = asArray(document.querySelectorAll('[data-tooltip-id]'));
|
|
360
|
+
for (const el of tooltipEls) {
|
|
361
|
+
if (!isVisible(el)) continue;
|
|
362
|
+
const tid = normalize(el.getAttribute('data-tooltip-id') || '');
|
|
363
|
+
if (tid.includes('history') || tid.includes('past-conversations')) {
|
|
364
|
+
const cl = getClickable(el);
|
|
365
|
+
if (cl) { cl.click(); opened = true; break; }
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
if (!opened) {
|
|
370
|
+
opened = clickByPatterns([
|
|
371
|
+
'past conversations',
|
|
372
|
+
'past conversation',
|
|
373
|
+
'conversation history',
|
|
374
|
+
'past chats',
|
|
375
|
+
'過去の会話',
|
|
376
|
+
'chat history',
|
|
377
|
+
]);
|
|
378
|
+
}
|
|
246
379
|
if (!opened) {
|
|
247
380
|
opened = clickIconHistoryButton();
|
|
248
381
|
}
|
|
@@ -286,6 +419,94 @@ function buildActivateViaPastConversationsScript(title) {
|
|
|
286
419
|
class ChatSessionService {
|
|
287
420
|
static ACTIVATE_SESSION_MAX_WAIT_MS = 30000;
|
|
288
421
|
static ACTIVATE_SESSION_RETRY_INTERVAL_MS = 800;
|
|
422
|
+
static LIST_SESSIONS_TARGET = 20;
|
|
423
|
+
/**
|
|
424
|
+
* List recent sessions by opening the Past Conversations panel.
|
|
425
|
+
*
|
|
426
|
+
* Flow (all clicks via CDP Input.dispatchMouseEvent for Electron compatibility):
|
|
427
|
+
* 1. Find Past Conversations button coordinates
|
|
428
|
+
* 2. Click it via CDP mouse events
|
|
429
|
+
* 3. Wait for panel to render
|
|
430
|
+
* 4. Scrape visible sessions
|
|
431
|
+
* 5. If < TARGET sessions, find & click "Show N more..."
|
|
432
|
+
* 6. Re-scrape
|
|
433
|
+
* 7. Close panel with Escape key
|
|
434
|
+
*
|
|
435
|
+
* @param cdpService CdpService instance to use
|
|
436
|
+
* @returns Array of session list items (empty array on failure)
|
|
437
|
+
*/
|
|
438
|
+
async listAllSessions(cdpService) {
|
|
439
|
+
try {
|
|
440
|
+
// Step 1: Find Past Conversations button
|
|
441
|
+
const btnState = await this.evaluateOnAnyContext(cdpService, FIND_PAST_CONVERSATIONS_BUTTON_SCRIPT, false);
|
|
442
|
+
if (!btnState?.found) {
|
|
443
|
+
return [];
|
|
444
|
+
}
|
|
445
|
+
// Step 2: Click via CDP mouse events (reliable in Electron)
|
|
446
|
+
await this.cdpMouseClick(cdpService, btnState.x, btnState.y);
|
|
447
|
+
// Step 3: Wait for panel to render
|
|
448
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
449
|
+
// Step 4: Scrape sessions
|
|
450
|
+
let scrapeResult = await this.evaluateOnAnyContext(cdpService, SCRAPE_PAST_CONVERSATIONS_SCRIPT, false);
|
|
451
|
+
let sessions = scrapeResult?.sessions ?? [];
|
|
452
|
+
// Step 5: If fewer than TARGET, click "Show N more..."
|
|
453
|
+
if (sessions.length < ChatSessionService.LIST_SESSIONS_TARGET) {
|
|
454
|
+
const showMoreState = await this.evaluateOnAnyContext(cdpService, FIND_SHOW_MORE_BUTTON_SCRIPT, false);
|
|
455
|
+
if (showMoreState?.found) {
|
|
456
|
+
await this.cdpMouseClick(cdpService, showMoreState.x, showMoreState.y);
|
|
457
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
458
|
+
// Step 6: Re-scrape
|
|
459
|
+
scrapeResult = await this.evaluateOnAnyContext(cdpService, SCRAPE_PAST_CONVERSATIONS_SCRIPT, false);
|
|
460
|
+
sessions = scrapeResult?.sessions ?? [];
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
// Step 7: Close panel with Escape
|
|
464
|
+
await cdpService.call('Input.dispatchKeyEvent', {
|
|
465
|
+
type: 'keyDown', key: 'Escape', code: 'Escape',
|
|
466
|
+
windowsVirtualKeyCode: 27, nativeVirtualKeyCode: 27,
|
|
467
|
+
});
|
|
468
|
+
await cdpService.call('Input.dispatchKeyEvent', {
|
|
469
|
+
type: 'keyUp', key: 'Escape', code: 'Escape',
|
|
470
|
+
windowsVirtualKeyCode: 27, nativeVirtualKeyCode: 27,
|
|
471
|
+
});
|
|
472
|
+
return sessions.slice(0, ChatSessionService.LIST_SESSIONS_TARGET);
|
|
473
|
+
}
|
|
474
|
+
catch (_) {
|
|
475
|
+
return [];
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
/**
|
|
479
|
+
* Evaluate a script on the first context that returns a truthy value.
|
|
480
|
+
*/
|
|
481
|
+
async evaluateOnAnyContext(cdpService, expression, awaitPromise) {
|
|
482
|
+
const contexts = cdpService.getContexts();
|
|
483
|
+
for (const ctx of contexts) {
|
|
484
|
+
try {
|
|
485
|
+
const result = await cdpService.call('Runtime.evaluate', {
|
|
486
|
+
expression, returnByValue: true, awaitPromise, contextId: ctx.id,
|
|
487
|
+
});
|
|
488
|
+
const value = result?.result?.value;
|
|
489
|
+
if (value)
|
|
490
|
+
return value;
|
|
491
|
+
}
|
|
492
|
+
catch (_) { /* try next context */ }
|
|
493
|
+
}
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
/**
|
|
497
|
+
* Click at coordinates via CDP Input.dispatchMouseEvent.
|
|
498
|
+
*/
|
|
499
|
+
async cdpMouseClick(cdpService, x, y) {
|
|
500
|
+
await cdpService.call('Input.dispatchMouseEvent', {
|
|
501
|
+
type: 'mouseMoved', x, y,
|
|
502
|
+
});
|
|
503
|
+
await cdpService.call('Input.dispatchMouseEvent', {
|
|
504
|
+
type: 'mousePressed', x, y, button: 'left', clickCount: 1,
|
|
505
|
+
});
|
|
506
|
+
await cdpService.call('Input.dispatchMouseEvent', {
|
|
507
|
+
type: 'mouseReleased', x, y, button: 'left', clickCount: 1,
|
|
508
|
+
});
|
|
509
|
+
}
|
|
289
510
|
/**
|
|
290
511
|
* Start a new chat session in the Antigravity UI.
|
|
291
512
|
*
|
|
@@ -98,6 +98,7 @@ class ErrorPopupDetector {
|
|
|
98
98
|
cdpService;
|
|
99
99
|
pollIntervalMs;
|
|
100
100
|
onErrorPopup;
|
|
101
|
+
onResolved;
|
|
101
102
|
pollTimer = null;
|
|
102
103
|
isRunning = false;
|
|
103
104
|
/** Key of the last detected error popup (for duplicate notification prevention) */
|
|
@@ -112,6 +113,7 @@ class ErrorPopupDetector {
|
|
|
112
113
|
this.cdpService = options.cdpService;
|
|
113
114
|
this.pollIntervalMs = options.pollIntervalMs ?? 3000;
|
|
114
115
|
this.onErrorPopup = options.onErrorPopup;
|
|
116
|
+
this.onResolved = options.onResolved;
|
|
115
117
|
}
|
|
116
118
|
/** Start monitoring. */
|
|
117
119
|
start() {
|
|
@@ -223,8 +225,12 @@ class ErrorPopupDetector {
|
|
|
223
225
|
}
|
|
224
226
|
else {
|
|
225
227
|
// Reset when popup disappears (prepare for next detection)
|
|
228
|
+
const wasDetected = this.lastDetectedKey !== null;
|
|
226
229
|
this.lastDetectedKey = null;
|
|
227
230
|
this.lastDetectedInfo = null;
|
|
231
|
+
if (wasDetected && this.onResolved) {
|
|
232
|
+
this.onResolved();
|
|
233
|
+
}
|
|
228
234
|
}
|
|
229
235
|
}
|
|
230
236
|
catch (error) {
|
|
@@ -149,6 +149,7 @@ class PlanningDetector {
|
|
|
149
149
|
cdpService;
|
|
150
150
|
pollIntervalMs;
|
|
151
151
|
onPlanningRequired;
|
|
152
|
+
onResolved;
|
|
152
153
|
pollTimer = null;
|
|
153
154
|
isRunning = false;
|
|
154
155
|
/** Key of the last detected planning info (for duplicate notification prevention) */
|
|
@@ -163,6 +164,7 @@ class PlanningDetector {
|
|
|
163
164
|
this.cdpService = options.cdpService;
|
|
164
165
|
this.pollIntervalMs = options.pollIntervalMs ?? 2000;
|
|
165
166
|
this.onPlanningRequired = options.onPlanningRequired;
|
|
167
|
+
this.onResolved = options.onResolved;
|
|
166
168
|
}
|
|
167
169
|
/** Start monitoring. */
|
|
168
170
|
start() {
|
|
@@ -270,8 +272,12 @@ class PlanningDetector {
|
|
|
270
272
|
}
|
|
271
273
|
else {
|
|
272
274
|
// Reset when buttons disappear (prepare for next planning detection)
|
|
275
|
+
const wasDetected = this.lastDetectedKey !== null;
|
|
273
276
|
this.lastDetectedKey = null;
|
|
274
277
|
this.lastDetectedInfo = null;
|
|
278
|
+
if (wasDetected && this.onResolved) {
|
|
279
|
+
this.onResolved();
|
|
280
|
+
}
|
|
275
281
|
}
|
|
276
282
|
}
|
|
277
283
|
catch (error) {
|
|
@@ -453,7 +453,6 @@ class ResponseMonitor {
|
|
|
453
453
|
onPhaseChange;
|
|
454
454
|
onProcessLog;
|
|
455
455
|
pollTimer = null;
|
|
456
|
-
timeoutTimer = null;
|
|
457
456
|
isRunning = false;
|
|
458
457
|
lastText = null;
|
|
459
458
|
baselineText = null;
|
|
@@ -463,6 +462,13 @@ class ResponseMonitor {
|
|
|
463
462
|
quotaDetected = false;
|
|
464
463
|
seenProcessLogKeys = new Set();
|
|
465
464
|
structuredDiagLogged = false;
|
|
465
|
+
// CDP disconnect handling (#48)
|
|
466
|
+
isPaused = false;
|
|
467
|
+
onCdpDisconnected = null;
|
|
468
|
+
onCdpReconnected = null;
|
|
469
|
+
onCdpReconnectFailed = null;
|
|
470
|
+
// Activity-based timeout (#49)
|
|
471
|
+
lastActivityTime = 0;
|
|
466
472
|
constructor(options) {
|
|
467
473
|
this.cdpService = options.cdpService;
|
|
468
474
|
this.pollIntervalMs = options.pollIntervalMs ?? 2000;
|
|
@@ -478,18 +484,31 @@ class ResponseMonitor {
|
|
|
478
484
|
}
|
|
479
485
|
/** Start monitoring */
|
|
480
486
|
async start() {
|
|
487
|
+
return this.initMonitoring(false);
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Start monitoring in passive mode.
|
|
491
|
+
* Same as start() but with generationStarted=true, so text changes
|
|
492
|
+
* are detected immediately without waiting for the stop button to appear.
|
|
493
|
+
* Used when joining an existing session that may already be generating.
|
|
494
|
+
*/
|
|
495
|
+
async startPassive() {
|
|
496
|
+
return this.initMonitoring(true);
|
|
497
|
+
}
|
|
498
|
+
/** Internal initialization shared between start() and startPassive() */
|
|
499
|
+
async initMonitoring(passive) {
|
|
481
500
|
if (this.isRunning)
|
|
482
501
|
return;
|
|
483
502
|
this.isRunning = true;
|
|
503
|
+
this.isPaused = false;
|
|
484
504
|
this.lastText = null;
|
|
485
505
|
this.baselineText = null;
|
|
486
|
-
this.generationStarted =
|
|
487
|
-
this.currentPhase = 'waiting';
|
|
506
|
+
this.generationStarted = passive;
|
|
507
|
+
this.currentPhase = passive ? 'generating' : 'waiting';
|
|
488
508
|
this.stopGoneCount = 0;
|
|
489
509
|
this.quotaDetected = false;
|
|
490
510
|
this.seenProcessLogKeys = new Set();
|
|
491
|
-
|
|
492
|
-
this.onPhaseChange?.('waiting', null);
|
|
511
|
+
this.onPhaseChange?.(this.currentPhase, null);
|
|
493
512
|
// Capture baseline text
|
|
494
513
|
try {
|
|
495
514
|
const baseResult = await this.cdpService.call('Runtime.evaluate', this.buildEvaluateParams(exports.RESPONSE_SELECTORS.RESPONSE_TEXT));
|
|
@@ -513,35 +532,44 @@ class ResponseMonitor {
|
|
|
513
532
|
catch {
|
|
514
533
|
// baseline capture only
|
|
515
534
|
}
|
|
516
|
-
//
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
535
|
+
// In structured mode, also capture activity lines from the structured
|
|
536
|
+
// extraction to align the baseline with polling logic. The PROCESS_LOGS
|
|
537
|
+
// script skips <details> content, but structured extraction (Pass 2)
|
|
538
|
+
// explicitly walks <details> elements — without this, tool-call/thinking
|
|
539
|
+
// entries from previous turns leak into the process log as "new" entries.
|
|
540
|
+
if (this.extractionMode === 'structured') {
|
|
541
|
+
try {
|
|
542
|
+
const structuredBaseline = await this.cdpService.call('Runtime.evaluate', this.buildEvaluateParams(exports.RESPONSE_SELECTORS.RESPONSE_STRUCTURED));
|
|
543
|
+
const baselineClassified = (0, assistantDomExtractor_1.classifyAssistantSegments)(structuredBaseline?.result?.value);
|
|
544
|
+
if (baselineClassified.diagnostics.source === 'dom-structured') {
|
|
545
|
+
for (const line of baselineClassified.activityLines) {
|
|
546
|
+
const key = (line || '').replace(/\r/g, '').trim().slice(0, 200);
|
|
547
|
+
if (key)
|
|
548
|
+
this.seenProcessLogKeys.add(key);
|
|
549
|
+
}
|
|
527
550
|
}
|
|
528
|
-
}
|
|
551
|
+
}
|
|
552
|
+
catch {
|
|
553
|
+
// structured baseline is best-effort
|
|
554
|
+
}
|
|
529
555
|
}
|
|
530
|
-
|
|
531
|
-
|
|
556
|
+
// Activity-based timeout: track last activity time instead of fixed timer (#49)
|
|
557
|
+
this.lastActivityTime = Date.now();
|
|
558
|
+
// Register CDP connection event listeners (#48)
|
|
559
|
+
this.registerCdpConnectionListeners();
|
|
560
|
+
const mode = passive ? 'Passive monitoring' : 'Monitoring';
|
|
561
|
+
logger_1.logger.debug(`── ${mode} started | poll=${this.pollIntervalMs}ms inactivityTimeout=${this.maxDurationMs / 1000}s baseline=${this.baselineText?.length ?? 0}ch`);
|
|
532
562
|
this.schedulePoll();
|
|
533
563
|
}
|
|
534
564
|
/** Stop monitoring */
|
|
535
565
|
async stop() {
|
|
536
566
|
this.isRunning = false;
|
|
567
|
+
this.isPaused = false;
|
|
568
|
+
this.unregisterCdpConnectionListeners();
|
|
537
569
|
if (this.pollTimer) {
|
|
538
570
|
clearTimeout(this.pollTimer);
|
|
539
571
|
this.pollTimer = null;
|
|
540
572
|
}
|
|
541
|
-
if (this.timeoutTimer) {
|
|
542
|
-
clearTimeout(this.timeoutTimer);
|
|
543
|
-
this.timeoutTimer = null;
|
|
544
|
-
}
|
|
545
573
|
}
|
|
546
574
|
/** Get current phase */
|
|
547
575
|
getPhase() {
|
|
@@ -593,14 +621,71 @@ class ResponseMonitor {
|
|
|
593
621
|
case 'quotaReached':
|
|
594
622
|
logger_1.logger.warn('Quota Reached');
|
|
595
623
|
break;
|
|
624
|
+
case 'disconnected':
|
|
625
|
+
logger_1.logger.warn(`CDP Disconnected — paused (${len} chars captured)`);
|
|
626
|
+
break;
|
|
596
627
|
default:
|
|
597
628
|
logger_1.logger.phase(`${phase}`);
|
|
598
629
|
}
|
|
599
630
|
this.onPhaseChange?.(phase, text);
|
|
600
631
|
}
|
|
601
632
|
}
|
|
633
|
+
registerCdpConnectionListeners() {
|
|
634
|
+
this.onCdpDisconnected = () => {
|
|
635
|
+
if (!this.isRunning)
|
|
636
|
+
return;
|
|
637
|
+
logger_1.logger.warn('[ResponseMonitor] CDP disconnected — pausing poll');
|
|
638
|
+
this.isPaused = true;
|
|
639
|
+
if (this.pollTimer) {
|
|
640
|
+
clearTimeout(this.pollTimer);
|
|
641
|
+
this.pollTimer = null;
|
|
642
|
+
}
|
|
643
|
+
this.setPhase('disconnected', this.lastText);
|
|
644
|
+
};
|
|
645
|
+
this.onCdpReconnected = () => {
|
|
646
|
+
if (!this.isRunning)
|
|
647
|
+
return;
|
|
648
|
+
logger_1.logger.warn('[ResponseMonitor] CDP reconnected — resuming poll');
|
|
649
|
+
this.isPaused = false;
|
|
650
|
+
this.lastActivityTime = Date.now();
|
|
651
|
+
const resumePhase = this.generationStarted ? 'generating' : 'waiting';
|
|
652
|
+
this.setPhase(resumePhase, this.lastText);
|
|
653
|
+
this.schedulePoll();
|
|
654
|
+
};
|
|
655
|
+
this.onCdpReconnectFailed = async (err) => {
|
|
656
|
+
if (!this.isRunning)
|
|
657
|
+
return;
|
|
658
|
+
logger_1.logger.error('[ResponseMonitor] CDP reconnection failed — stopping monitor:', err.message);
|
|
659
|
+
const lastText = this.lastText ?? '';
|
|
660
|
+
this.setPhase('disconnected', lastText);
|
|
661
|
+
await this.stop();
|
|
662
|
+
try {
|
|
663
|
+
await Promise.resolve(this.onTimeout?.(lastText));
|
|
664
|
+
}
|
|
665
|
+
catch (error) {
|
|
666
|
+
logger_1.logger.error('[ResponseMonitor] timeout callback failed:', error);
|
|
667
|
+
}
|
|
668
|
+
};
|
|
669
|
+
this.cdpService.on('disconnected', this.onCdpDisconnected);
|
|
670
|
+
this.cdpService.on('reconnected', this.onCdpReconnected);
|
|
671
|
+
this.cdpService.on('reconnectFailed', this.onCdpReconnectFailed);
|
|
672
|
+
}
|
|
673
|
+
unregisterCdpConnectionListeners() {
|
|
674
|
+
if (this.onCdpDisconnected) {
|
|
675
|
+
this.cdpService.removeListener('disconnected', this.onCdpDisconnected);
|
|
676
|
+
this.onCdpDisconnected = null;
|
|
677
|
+
}
|
|
678
|
+
if (this.onCdpReconnected) {
|
|
679
|
+
this.cdpService.removeListener('reconnected', this.onCdpReconnected);
|
|
680
|
+
this.onCdpReconnected = null;
|
|
681
|
+
}
|
|
682
|
+
if (this.onCdpReconnectFailed) {
|
|
683
|
+
this.cdpService.removeListener('reconnectFailed', this.onCdpReconnectFailed);
|
|
684
|
+
this.onCdpReconnectFailed = null;
|
|
685
|
+
}
|
|
686
|
+
}
|
|
602
687
|
schedulePoll() {
|
|
603
|
-
if (!this.isRunning)
|
|
688
|
+
if (!this.isRunning || this.isPaused)
|
|
604
689
|
return;
|
|
605
690
|
this.pollTimer = setTimeout(async () => {
|
|
606
691
|
await this.poll();
|
|
@@ -637,6 +722,7 @@ class ResponseMonitor {
|
|
|
637
722
|
newEntries.push(normalized.slice(0, 300));
|
|
638
723
|
}
|
|
639
724
|
if (newEntries.length > 0) {
|
|
725
|
+
this.lastActivityTime = Date.now();
|
|
640
726
|
try {
|
|
641
727
|
this.onProcessLog?.(newEntries.join('\n\n'));
|
|
642
728
|
}
|
|
@@ -714,6 +800,7 @@ class ResponseMonitor {
|
|
|
714
800
|
}
|
|
715
801
|
// Handle stop button appearing
|
|
716
802
|
if (isGenerating) {
|
|
803
|
+
this.lastActivityTime = Date.now();
|
|
717
804
|
if (!this.generationStarted) {
|
|
718
805
|
this.generationStarted = true;
|
|
719
806
|
this.setPhase('thinking', null);
|
|
@@ -748,6 +835,7 @@ class ResponseMonitor {
|
|
|
748
835
|
// Text change handling
|
|
749
836
|
const textChanged = effectiveText !== null && effectiveText !== this.lastText;
|
|
750
837
|
if (textChanged) {
|
|
838
|
+
this.lastActivityTime = Date.now();
|
|
751
839
|
this.lastText = effectiveText;
|
|
752
840
|
if (this.currentPhase === 'waiting' || this.currentPhase === 'thinking') {
|
|
753
841
|
this.setPhase('generating', effectiveText);
|
|
@@ -773,6 +861,19 @@ class ResponseMonitor {
|
|
|
773
861
|
return;
|
|
774
862
|
}
|
|
775
863
|
}
|
|
864
|
+
// Activity-based inactivity timeout (#49)
|
|
865
|
+
if (this.maxDurationMs > 0 && Date.now() - this.lastActivityTime >= this.maxDurationMs) {
|
|
866
|
+
const lastText = this.lastText ?? '';
|
|
867
|
+
this.setPhase('timeout', lastText);
|
|
868
|
+
await this.stop();
|
|
869
|
+
try {
|
|
870
|
+
await Promise.resolve(this.onTimeout?.(lastText));
|
|
871
|
+
}
|
|
872
|
+
catch (error) {
|
|
873
|
+
logger_1.logger.error('[ResponseMonitor] timeout callback failed:', error);
|
|
874
|
+
}
|
|
875
|
+
return;
|
|
876
|
+
}
|
|
776
877
|
}
|
|
777
878
|
catch (error) {
|
|
778
879
|
logger_1.logger.error('[ResponseMonitor] poll error:', error);
|