lazy-gravity 0.0.4 → 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.
Files changed (44) hide show
  1. package/README.md +22 -7
  2. package/dist/bin/cli.js +18 -18
  3. package/dist/bin/commands/doctor.js +25 -19
  4. package/dist/bin/commands/start.js +25 -2
  5. package/dist/bot/index.js +445 -126
  6. package/dist/commands/joinCommandHandler.js +302 -0
  7. package/dist/commands/joinDetachCommandHandler.js +285 -0
  8. package/dist/commands/registerSlashCommands.js +40 -0
  9. package/dist/commands/workspaceCommandHandler.js +17 -28
  10. package/dist/database/chatSessionRepository.js +10 -0
  11. package/dist/database/userPreferenceRepository.js +72 -0
  12. package/dist/events/interactionCreateHandler.js +338 -30
  13. package/dist/events/messageCreateHandler.js +161 -47
  14. package/dist/services/antigravityLauncher.js +4 -3
  15. package/dist/services/approvalDetector.js +7 -0
  16. package/dist/services/assistantDomExtractor.js +339 -0
  17. package/dist/services/cdpBridgeManager.js +323 -39
  18. package/dist/services/cdpConnectionPool.js +117 -33
  19. package/dist/services/cdpService.js +149 -53
  20. package/dist/services/chatSessionService.js +229 -8
  21. package/dist/services/errorPopupDetector.js +271 -0
  22. package/dist/services/planningDetector.js +318 -0
  23. package/dist/services/responseMonitor.js +308 -70
  24. package/dist/services/retryStore.js +46 -0
  25. package/dist/services/updateCheckService.js +147 -0
  26. package/dist/services/userMessageDetector.js +221 -0
  27. package/dist/ui/buttonUtils.js +33 -0
  28. package/dist/ui/modeUi.js +11 -1
  29. package/dist/ui/modelsUi.js +24 -13
  30. package/dist/ui/outputUi.js +30 -0
  31. package/dist/ui/projectListUi.js +83 -0
  32. package/dist/ui/sessionPickerUi.js +48 -0
  33. package/dist/utils/antigravityPaths.js +94 -0
  34. package/dist/utils/configLoader.js +18 -0
  35. package/dist/utils/discordButtonUtils.js +33 -0
  36. package/dist/utils/discordFormatter.js +149 -16
  37. package/dist/utils/htmlToDiscordMarkdown.js +184 -0
  38. package/dist/utils/logBuffer.js +47 -0
  39. package/dist/utils/logFileTransport.js +147 -0
  40. package/dist/utils/logger.js +86 -21
  41. package/dist/utils/pathUtils.js +57 -0
  42. package/dist/utils/plainTextFormatter.js +70 -0
  43. package/dist/utils/processLogBuffer.js +4 -0
  44. 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
- let opened = clickByPatterns([
239
- 'past conversations',
240
- 'past conversation',
241
- 'conversation history',
242
- 'past chats',
243
- '過去の会話',
244
- 'chat history',
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
  *
@@ -0,0 +1,271 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ErrorPopupDetector = void 0;
4
+ const logger_1 = require("../utils/logger");
5
+ const approvalDetector_1 = require("./approvalDetector");
6
+ /**
7
+ * Detection script for the Antigravity UI error popup.
8
+ *
9
+ * Looks for dialog/modal containers containing error-related text patterns
10
+ * like "agent terminated", "error", "failed", etc. and extracts popup info.
11
+ */
12
+ const DETECT_ERROR_POPUP_SCRIPT = `(() => {
13
+ const ERROR_PATTERNS = [
14
+ 'agent terminated',
15
+ 'terminated due to error',
16
+ 'unexpected error',
17
+ 'something went wrong',
18
+ 'an error occurred',
19
+ ];
20
+
21
+ const normalize = (text) => (text || '').toLowerCase().replace(/\\s+/g, ' ').trim();
22
+
23
+ // Try dialog/modal first
24
+ const dialogs = Array.from(document.querySelectorAll(
25
+ '[role="dialog"], [role="alertdialog"], .modal, .dialog'
26
+ )).filter(el => el.offsetParent !== null || el.getAttribute('aria-modal') === 'true');
27
+
28
+ // Fallback: look for fixed/absolute positioned overlays
29
+ if (dialogs.length === 0) {
30
+ const overlays = Array.from(document.querySelectorAll('div[class*="fixed"], div[class*="absolute"]'))
31
+ .filter(el => {
32
+ const style = window.getComputedStyle(el);
33
+ return (style.position === 'fixed' || style.position === 'absolute')
34
+ && style.zIndex && parseInt(style.zIndex, 10) > 10
35
+ && el.querySelector('button');
36
+ });
37
+ dialogs.push(...overlays);
38
+ }
39
+
40
+ for (const dialog of dialogs) {
41
+ const fullText = normalize(dialog.textContent || '');
42
+ const isError = ERROR_PATTERNS.some(p => fullText.includes(p));
43
+ if (!isError) continue;
44
+
45
+ // Extract title from heading elements or first prominent text
46
+ const headingEl = dialog.querySelector('h1, h2, h3, h4, [class*="title"], [class*="heading"]');
47
+ const title = headingEl ? (headingEl.textContent || '').trim() : '';
48
+
49
+ // Extract body text (excluding button text and title)
50
+ const allButtons = Array.from(dialog.querySelectorAll('button'))
51
+ .filter(btn => btn.offsetParent !== null);
52
+ const buttonTexts = new Set(allButtons.map(btn => (btn.textContent || '').trim()));
53
+
54
+ const bodyParts = [];
55
+ const walker = document.createTreeWalker(dialog, NodeFilter.SHOW_TEXT);
56
+ let node;
57
+ while ((node = walker.nextNode())) {
58
+ const text = (node.textContent || '').trim();
59
+ if (!text) continue;
60
+ if (buttonTexts.has(text)) continue;
61
+ if (text === title) continue;
62
+ bodyParts.push(text);
63
+ }
64
+ const body = bodyParts.join(' ').slice(0, 1000);
65
+
66
+ const buttons = allButtons.map(btn => (btn.textContent || '').trim()).filter(t => t.length > 0);
67
+
68
+ if (buttons.length === 0) continue;
69
+
70
+ return { title: title || 'Error', body, buttons };
71
+ }
72
+
73
+ return null;
74
+ })()`;
75
+ /**
76
+ * Read clipboard content via navigator.clipboard.readText().
77
+ * Requires awaitPromise=true since clipboard API returns a Promise.
78
+ */
79
+ const READ_CLIPBOARD_SCRIPT = `(async () => {
80
+ try {
81
+ const text = await navigator.clipboard.readText();
82
+ return text || null;
83
+ } catch (e) {
84
+ return null;
85
+ }
86
+ })()`;
87
+ /**
88
+ * Detects error popup dialogs (e.g. "Agent terminated due to error") in the
89
+ * Antigravity UI via polling.
90
+ *
91
+ * Follows the same polling pattern as PlanningDetector / ApprovalDetector:
92
+ * - start()/stop() lifecycle
93
+ * - Duplicate notification prevention via lastDetectedKey
94
+ * - Cooldown to suppress rapid re-detection
95
+ * - CDP error tolerance (continues polling on error)
96
+ */
97
+ class ErrorPopupDetector {
98
+ cdpService;
99
+ pollIntervalMs;
100
+ onErrorPopup;
101
+ onResolved;
102
+ pollTimer = null;
103
+ isRunning = false;
104
+ /** Key of the last detected error popup (for duplicate notification prevention) */
105
+ lastDetectedKey = null;
106
+ /** Full ErrorPopupInfo from the last detection */
107
+ lastDetectedInfo = null;
108
+ /** Timestamp of last notification (for cooldown-based dedup) */
109
+ lastNotifiedAt = 0;
110
+ /** Cooldown period in ms to suppress duplicate notifications (10s for error popups) */
111
+ static COOLDOWN_MS = 10000;
112
+ constructor(options) {
113
+ this.cdpService = options.cdpService;
114
+ this.pollIntervalMs = options.pollIntervalMs ?? 3000;
115
+ this.onErrorPopup = options.onErrorPopup;
116
+ this.onResolved = options.onResolved;
117
+ }
118
+ /** Start monitoring. */
119
+ start() {
120
+ if (this.isRunning)
121
+ return;
122
+ this.isRunning = true;
123
+ this.lastDetectedKey = null;
124
+ this.lastDetectedInfo = null;
125
+ this.lastNotifiedAt = 0;
126
+ this.schedulePoll();
127
+ }
128
+ /** Stop monitoring. */
129
+ async stop() {
130
+ this.isRunning = false;
131
+ if (this.pollTimer) {
132
+ clearTimeout(this.pollTimer);
133
+ this.pollTimer = null;
134
+ }
135
+ }
136
+ /** Return the last detected error popup info. Returns null if nothing has been detected. */
137
+ getLastDetectedInfo() {
138
+ return this.lastDetectedInfo;
139
+ }
140
+ /** Returns whether monitoring is currently active. */
141
+ isActive() {
142
+ return this.isRunning;
143
+ }
144
+ /**
145
+ * Click the Dismiss button via CDP.
146
+ * @returns true if click succeeded
147
+ */
148
+ async clickDismissButton() {
149
+ return this.clickButton('Dismiss');
150
+ }
151
+ /**
152
+ * Click the "Copy debug info" button via CDP.
153
+ * @returns true if click succeeded
154
+ */
155
+ async clickCopyDebugInfoButton() {
156
+ return this.clickButton('Copy debug info');
157
+ }
158
+ /**
159
+ * Click the Retry button via CDP.
160
+ * @returns true if click succeeded
161
+ */
162
+ async clickRetryButton() {
163
+ return this.clickButton('Retry');
164
+ }
165
+ /**
166
+ * Read clipboard content from the browser via navigator.clipboard.readText().
167
+ * Should be called after clickCopyDebugInfoButton() with a short delay.
168
+ * @returns Clipboard text or null if unavailable
169
+ */
170
+ async readClipboard() {
171
+ try {
172
+ const result = await this.runEvaluateScript(READ_CLIPBOARD_SCRIPT, true);
173
+ return typeof result === 'string' ? result : null;
174
+ }
175
+ catch (error) {
176
+ logger_1.logger.error('[ErrorPopupDetector] Error reading clipboard:', error);
177
+ return null;
178
+ }
179
+ }
180
+ /** Schedule the next poll. */
181
+ schedulePoll() {
182
+ if (!this.isRunning)
183
+ return;
184
+ this.pollTimer = setTimeout(async () => {
185
+ await this.poll();
186
+ if (this.isRunning) {
187
+ this.schedulePoll();
188
+ }
189
+ }, this.pollIntervalMs);
190
+ }
191
+ /**
192
+ * Single poll iteration:
193
+ * 1. Detect error popup from DOM (with contextId)
194
+ * 2. Notify via callback only on new detection (prevent duplicates)
195
+ * 3. Reset lastDetectedKey / lastDetectedInfo when popup disappears
196
+ */
197
+ async poll() {
198
+ try {
199
+ const contextId = this.cdpService.getPrimaryContextId();
200
+ const callParams = {
201
+ expression: DETECT_ERROR_POPUP_SCRIPT,
202
+ returnByValue: true,
203
+ awaitPromise: false,
204
+ };
205
+ if (contextId !== null) {
206
+ callParams.contextId = contextId;
207
+ }
208
+ const result = await this.cdpService.call('Runtime.evaluate', callParams);
209
+ const info = result?.result?.value ?? null;
210
+ if (info) {
211
+ // Duplicate prevention: use title + body snippet as key
212
+ const key = `${info.title}::${info.body.slice(0, 100)}`;
213
+ const now = Date.now();
214
+ const withinCooldown = (now - this.lastNotifiedAt) < ErrorPopupDetector.COOLDOWN_MS;
215
+ if (key !== this.lastDetectedKey && !withinCooldown) {
216
+ this.lastDetectedKey = key;
217
+ this.lastDetectedInfo = info;
218
+ this.lastNotifiedAt = now;
219
+ this.onErrorPopup(info);
220
+ }
221
+ else if (key === this.lastDetectedKey) {
222
+ // Same key -- update stored info silently
223
+ this.lastDetectedInfo = info;
224
+ }
225
+ }
226
+ else {
227
+ // Reset when popup disappears (prepare for next detection)
228
+ const wasDetected = this.lastDetectedKey !== null;
229
+ this.lastDetectedKey = null;
230
+ this.lastDetectedInfo = null;
231
+ if (wasDetected && this.onResolved) {
232
+ this.onResolved();
233
+ }
234
+ }
235
+ }
236
+ catch (error) {
237
+ // Ignore CDP errors and continue monitoring
238
+ const message = error instanceof Error ? error.message : String(error);
239
+ if (message.includes('WebSocket is not connected')) {
240
+ return;
241
+ }
242
+ logger_1.logger.error('[ErrorPopupDetector] Error during polling:', error);
243
+ }
244
+ }
245
+ /** Internal click handler using buildClickScript from approvalDetector. */
246
+ async clickButton(buttonText) {
247
+ try {
248
+ const result = await this.runEvaluateScript((0, approvalDetector_1.buildClickScript)(buttonText));
249
+ return result?.ok === true;
250
+ }
251
+ catch (error) {
252
+ logger_1.logger.error('[ErrorPopupDetector] Error while clicking button:', error);
253
+ return false;
254
+ }
255
+ }
256
+ /** Execute Runtime.evaluate with contextId and return result.value. */
257
+ async runEvaluateScript(expression, awaitPromise = false) {
258
+ const contextId = this.cdpService.getPrimaryContextId();
259
+ const callParams = {
260
+ expression,
261
+ returnByValue: true,
262
+ awaitPromise,
263
+ };
264
+ if (contextId !== null) {
265
+ callParams.contextId = contextId;
266
+ }
267
+ const result = await this.cdpService.call('Runtime.evaluate', callParams);
268
+ return result?.result?.value;
269
+ }
270
+ }
271
+ exports.ErrorPopupDetector = ErrorPopupDetector;