lazy-gravity 0.0.2 → 0.0.3

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 (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +224 -0
  3. package/dist/bin/cli.js +79 -0
  4. package/dist/bin/commands/doctor.js +156 -0
  5. package/dist/bin/commands/open.js +145 -0
  6. package/dist/bin/commands/setup.js +366 -0
  7. package/dist/bin/commands/start.js +15 -0
  8. package/dist/bot/index.js +914 -0
  9. package/dist/commands/chatCommandHandler.js +145 -0
  10. package/dist/commands/cleanupCommandHandler.js +396 -0
  11. package/dist/commands/messageParser.js +28 -0
  12. package/dist/commands/registerSlashCommands.js +149 -0
  13. package/dist/commands/slashCommandHandler.js +104 -0
  14. package/dist/commands/workspaceCommandHandler.js +230 -0
  15. package/dist/database/chatSessionRepository.js +88 -0
  16. package/dist/database/scheduleRepository.js +119 -0
  17. package/dist/database/templateRepository.js +103 -0
  18. package/dist/database/workspaceBindingRepository.js +109 -0
  19. package/dist/events/interactionCreateHandler.js +286 -0
  20. package/dist/events/messageCreateHandler.js +154 -0
  21. package/dist/index.js +10 -0
  22. package/dist/middleware/auth.js +10 -0
  23. package/dist/middleware/sanitize.js +20 -0
  24. package/dist/services/antigravityLauncher.js +89 -0
  25. package/dist/services/approvalDetector.js +384 -0
  26. package/dist/services/autoAcceptService.js +80 -0
  27. package/dist/services/cdpBridgeManager.js +204 -0
  28. package/dist/services/cdpConnectionPool.js +157 -0
  29. package/dist/services/cdpService.js +1311 -0
  30. package/dist/services/channelManager.js +118 -0
  31. package/dist/services/chatSessionService.js +516 -0
  32. package/dist/services/modeService.js +73 -0
  33. package/dist/services/modelService.js +63 -0
  34. package/dist/services/processManager.js +61 -0
  35. package/dist/services/progressSender.js +61 -0
  36. package/dist/services/promptDispatcher.js +17 -0
  37. package/dist/services/quotaService.js +185 -0
  38. package/dist/services/responseMonitor.js +645 -0
  39. package/dist/services/scheduleService.js +134 -0
  40. package/dist/services/screenshotService.js +85 -0
  41. package/dist/services/titleGeneratorService.js +113 -0
  42. package/dist/services/workspaceService.js +64 -0
  43. package/dist/ui/autoAcceptUi.js +34 -0
  44. package/dist/ui/modeUi.js +34 -0
  45. package/dist/ui/modelsUi.js +97 -0
  46. package/dist/ui/screenshotUi.js +51 -0
  47. package/dist/ui/templateUi.js +67 -0
  48. package/dist/utils/cdpPorts.js +5 -0
  49. package/dist/utils/config.js +20 -0
  50. package/dist/utils/configLoader.js +160 -0
  51. package/dist/utils/discordFormatter.js +167 -0
  52. package/dist/utils/i18n.js +77 -0
  53. package/dist/utils/imageHandler.js +154 -0
  54. package/dist/utils/lockfile.js +113 -0
  55. package/dist/utils/logger.js +32 -0
  56. package/dist/utils/logo.js +13 -0
  57. package/dist/utils/metadataExtractor.js +15 -0
  58. package/dist/utils/processLogBuffer.js +98 -0
  59. package/dist/utils/streamMessageFormatter.js +90 -0
  60. package/package.json +73 -5
@@ -0,0 +1,645 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ResponseMonitor = exports.RESPONSE_SELECTORS = void 0;
4
+ const logger_1 = require("../utils/logger");
5
+ /** Lean DOM selectors for response extraction */
6
+ exports.RESPONSE_SELECTORS = {
7
+ /** Scored selector approach for extracting response text.
8
+ * Tie-breaking: newest wins (first found in reverse iteration).
9
+ * DOM is normal order: index 0 = oldest, N-1 = newest.
10
+ * Reverse iteration (N-1→0) visits newest first; strict > keeps it. */
11
+ RESPONSE_TEXT: `(() => {
12
+ const panel = document.querySelector('.antigravity-agent-side-panel');
13
+ const scopes = [panel, document].filter(Boolean);
14
+
15
+ const selectors = [
16
+ { sel: '.rendered-markdown', score: 10 },
17
+ { sel: '.leading-relaxed.select-text', score: 9 },
18
+ { sel: '.flex.flex-col.gap-y-3', score: 8 },
19
+ { sel: '[data-message-author-role="assistant"]', score: 7 },
20
+ { sel: '[data-message-role="assistant"]', score: 6 },
21
+ { sel: '[class*="assistant-message"]', score: 5 },
22
+ { sel: '[class*="message-content"]', score: 4 },
23
+ { sel: '[class*="markdown-body"]', score: 3 },
24
+ { sel: '.prose', score: 2 },
25
+ ];
26
+
27
+ const looksLikeActivityLog = (text) => {
28
+ const normalized = (text || '').trim().toLowerCase();
29
+ if (!normalized) return false;
30
+ const activityPattern = /^(?:analy[sz]ing|reading|writing|running|searching|planning|thinking|processing|loading|executing|testing|debugging|analyzed|read|wrote|ran)/i;
31
+ if (activityPattern.test(normalized) && normalized.length <= 220) return true;
32
+ if (/^initiating\\s/i.test(normalized) && normalized.length <= 500) return true;
33
+ if (/^thought for\\s/i.test(normalized) && normalized.length <= 500) return true;
34
+ return false;
35
+ };
36
+
37
+ const looksLikeFeedbackFooter = (text) => {
38
+ const normalized = (text || '').trim().toLowerCase().replace(/\\s+/g, ' ');
39
+ if (!normalized) return false;
40
+ return normalized === 'good bad' || normalized === 'good' || normalized === 'bad';
41
+ };
42
+
43
+ const isInsideExcludedContainer = (node) => {
44
+ if (node.closest('details')) return true;
45
+ if (node.closest('[class*="feedback"], footer')) return true;
46
+ return false;
47
+ };
48
+
49
+ const looksLikeToolOutput = (text) => {
50
+ const first = (text || '').trim().split('\\n')[0] || '';
51
+ if (/^[a-z0-9._-]+\\s*\\/\\s*[a-z0-9._-]+$/i.test(first)) return true;
52
+ if (/^full output written to\\b/i.test(first)) return true;
53
+ if (/^output\\.[a-z0-9._-]+(?:#l\\d+(?:-\\d+)?)?$/i.test(first)) return true;
54
+ var lower = (text || '').trim().toLowerCase();
55
+ if (/^title:\\s/.test(lower) && /\\surl:\\s/.test(lower) && /\\ssnippet:\\s/.test(lower)) return true;
56
+ if (/^(json|javascript|typescript|python|bash|sh|html|css|xml|yaml|yml|toml|sql|graphql|markdown|text|plaintext|log|ruby|go|rust|java|c|cpp|csharp|php|swift|kotlin)$/i.test(first)) return true;
57
+ return false;
58
+ };
59
+
60
+ const combinedSelector = selectors.map((s) => s.sel).join(', ');
61
+ const seen = new Set();
62
+
63
+ for (const scope of scopes) {
64
+ const nodes = scope.querySelectorAll(combinedSelector);
65
+ for (let i = nodes.length - 1; i >= 0; i--) {
66
+ const node = nodes[i];
67
+ if (!node || seen.has(node)) continue;
68
+ seen.add(node);
69
+ if (isInsideExcludedContainer(node)) continue;
70
+ const text = (node.innerText || node.textContent || '').replace(/\\r/g, '').trim();
71
+ if (!text || text.length < 2) continue;
72
+ if (looksLikeActivityLog(text)) continue;
73
+ if (looksLikeFeedbackFooter(text)) continue;
74
+ if (looksLikeToolOutput(text)) continue;
75
+ // Prefer recency first: return the newest acceptable node.
76
+ return text;
77
+ }
78
+ }
79
+
80
+ return null;
81
+ })()`,
82
+ /** Stop button detection via tooltip-id + text fallback */
83
+ STOP_BUTTON: `(() => {
84
+ const panel = document.querySelector('.antigravity-agent-side-panel');
85
+ const scopes = [panel, document].filter(Boolean);
86
+
87
+ for (const scope of scopes) {
88
+ const el = scope.querySelector('[data-tooltip-id="input-send-button-cancel-tooltip"]');
89
+ if (el) return { isGenerating: true };
90
+ }
91
+
92
+ const normalize = (value) => (value || '').toLowerCase().replace(/\\s+/g, ' ').trim();
93
+ const STOP_PATTERNS = [
94
+ /^stop$/,
95
+ /^stop generating$/,
96
+ /^stop response$/,
97
+ /^停止$/,
98
+ /^生成を停止$/,
99
+ /^応答を停止$/,
100
+ ];
101
+ const isStopLabel = (value) => {
102
+ const normalized = normalize(value);
103
+ if (!normalized) return false;
104
+ return STOP_PATTERNS.some((re) => re.test(normalized));
105
+ };
106
+ for (const scope of scopes) {
107
+ const buttons = scope.querySelectorAll('button, [role="button"]');
108
+ for (let i = 0; i < buttons.length; i++) {
109
+ const btn = buttons[i];
110
+ const labels = [
111
+ btn.textContent || '',
112
+ btn.getAttribute('aria-label') || '',
113
+ btn.getAttribute('title') || '',
114
+ ];
115
+ if (labels.some(isStopLabel)) {
116
+ return { isGenerating: true };
117
+ }
118
+ }
119
+ }
120
+
121
+ return { isGenerating: false };
122
+ })()`,
123
+ /** Click stop button via tooltip-id + text fallback */
124
+ CLICK_STOP_BUTTON: `(() => {
125
+ const panel = document.querySelector('.antigravity-agent-side-panel');
126
+ const scopes = [panel, document].filter(Boolean);
127
+
128
+ for (const scope of scopes) {
129
+ const el = scope.querySelector('[data-tooltip-id="input-send-button-cancel-tooltip"]');
130
+ if (el && typeof el.click === 'function') {
131
+ el.click();
132
+ return { ok: true, method: 'tooltip-id' };
133
+ }
134
+ }
135
+
136
+ const normalize = (value) => (value || '').toLowerCase().replace(/\\s+/g, ' ').trim();
137
+ const STOP_PATTERNS = [
138
+ /^stop$/,
139
+ /^stop generating$/,
140
+ /^stop response$/,
141
+ /^停止$/,
142
+ /^生成を停止$/,
143
+ /^応答を停止$/,
144
+ ];
145
+ const isStopLabel = (value) => {
146
+ const normalized = normalize(value);
147
+ if (!normalized) return false;
148
+ return STOP_PATTERNS.some((re) => re.test(normalized));
149
+ };
150
+ for (const scope of scopes) {
151
+ const buttons = scope.querySelectorAll('button, [role="button"]');
152
+ for (let i = 0; i < buttons.length; i++) {
153
+ const btn = buttons[i];
154
+ const labels = [
155
+ btn.textContent || '',
156
+ btn.getAttribute('aria-label') || '',
157
+ btn.getAttribute('title') || '',
158
+ ];
159
+ if (labels.some(isStopLabel) && typeof btn.click === 'function') {
160
+ btn.click();
161
+ return { ok: true, method: 'text-fallback' };
162
+ }
163
+ }
164
+ }
165
+
166
+ return { ok: false, error: 'Stop button not found' };
167
+ })()`,
168
+ /** Diagnostic: dump ALL candidate text nodes with filter classification */
169
+ DUMP_ALL_TEXTS: `(() => {
170
+ const panel = document.querySelector('.antigravity-agent-side-panel');
171
+ const scopes = [panel, document].filter(Boolean);
172
+
173
+ const selectors = [
174
+ { sel: '.rendered-markdown', score: 10 },
175
+ { sel: '.leading-relaxed.select-text', score: 9 },
176
+ { sel: '.flex.flex-col.gap-y-3', score: 8 },
177
+ { sel: '[data-message-author-role="assistant"]', score: 7 },
178
+ { sel: '[data-message-role="assistant"]', score: 6 },
179
+ { sel: '[class*="assistant-message"]', score: 5 },
180
+ { sel: '[class*="message-content"]', score: 4 },
181
+ { sel: '[class*="markdown-body"]', score: 3 },
182
+ { sel: '.prose', score: 2 },
183
+ ];
184
+
185
+ const looksLikeActivityLog = (text) => {
186
+ const normalized = (text || '').trim().toLowerCase();
187
+ if (!normalized) return false;
188
+ const activityPattern = /^(?:analy[sz]ing|reading|writing|running|searching|planning|thinking|processing|loading|executing|testing|debugging|analyzed|read|wrote|ran)/i;
189
+ if (activityPattern.test(normalized) && normalized.length <= 220) return true;
190
+ if (/^initiating\\s/i.test(normalized) && normalized.length <= 500) return true;
191
+ if (/^thought for\\s/i.test(normalized) && normalized.length <= 500) return true;
192
+ return false;
193
+ };
194
+ const looksLikeFeedbackFooter = (text) => {
195
+ const normalized = (text || '').trim().toLowerCase().replace(/\\s+/g, ' ');
196
+ if (!normalized) return false;
197
+ return normalized === 'good bad' || normalized === 'good' || normalized === 'bad';
198
+ };
199
+ const isInsideExcludedContainer = (node) => {
200
+ if (node.closest('details')) return true;
201
+ if (node.closest('[class*="feedback"], footer')) return true;
202
+ return false;
203
+ };
204
+ const looksLikeToolOutput = (text) => {
205
+ const first = (text || '').trim().split('\\n')[0] || '';
206
+ if (/^[a-z0-9._-]+\\s*\\/\\s*[a-z0-9._-]+$/i.test(first)) return true;
207
+ if (/^full output written to\\b/i.test(first)) return true;
208
+ if (/^output\\.[a-z0-9._-]+(?:#l\\d+(?:-\\d+)?)?$/i.test(first)) return true;
209
+ var lower = (text || '').trim().toLowerCase();
210
+ if (/^title:\\s/.test(lower) && /\\surl:\\s/.test(lower) && /\\ssnippet:\\s/.test(lower)) return true;
211
+ if (/^(json|javascript|typescript|python|bash|sh|html|css|xml|yaml|yml|toml|sql|graphql|markdown|text|plaintext|log|ruby|go|rust|java|c|cpp|csharp|php|swift|kotlin)$/i.test(first)) return true;
212
+ return false;
213
+ };
214
+
215
+ const results = [];
216
+ const seen = new Set();
217
+
218
+ for (const scope of scopes) {
219
+ for (const { sel, score } of selectors) {
220
+ const nodes = scope.querySelectorAll(sel);
221
+ for (let i = nodes.length - 1; i >= 0; i--) {
222
+ const node = nodes[i];
223
+ if (!node || seen.has(node)) continue;
224
+ seen.add(node);
225
+ const text = (node.innerText || node.textContent || '').replace(/\\r/g, '').trim();
226
+ let skip = null;
227
+ if (!text || text.length < 2) skip = 'too-short';
228
+ else if (isInsideExcludedContainer(node)) skip = 'excluded-container';
229
+ else if (looksLikeActivityLog(text)) skip = 'activity-log';
230
+ else if (looksLikeFeedbackFooter(text)) skip = 'feedback-footer';
231
+ else if (looksLikeToolOutput(text)) skip = 'tool-output';
232
+ const classes = (node.className || '').toString().slice(0, 80);
233
+ results.push({
234
+ sel,
235
+ score,
236
+ skip,
237
+ len: text.length,
238
+ classes,
239
+ preview: text.slice(0, 120),
240
+ });
241
+ }
242
+ }
243
+ }
244
+ return results;
245
+ })()`,
246
+ /** Extract process log entries (activity messages + tool output) from DOM */
247
+ PROCESS_LOGS: `(() => {
248
+ const panel = document.querySelector('.antigravity-agent-side-panel');
249
+ const scopes = [panel, document].filter(Boolean);
250
+
251
+ const selectors = [
252
+ { sel: '.rendered-markdown', score: 10 },
253
+ { sel: '.leading-relaxed.select-text', score: 9 },
254
+ { sel: '.flex.flex-col.gap-y-3', score: 8 },
255
+ { sel: '[data-message-author-role="assistant"]', score: 7 },
256
+ { sel: '[data-message-role="assistant"]', score: 6 },
257
+ { sel: '[class*="assistant-message"]', score: 5 },
258
+ { sel: '[class*="message-content"]', score: 4 },
259
+ { sel: '[class*="markdown-body"]', score: 3 },
260
+ { sel: '.prose', score: 2 },
261
+ ];
262
+
263
+ const looksLikeActivityLog = (text) => {
264
+ const normalized = (text || '').trim().toLowerCase();
265
+ if (!normalized) return false;
266
+ const activityPattern = /^(?:analy[sz]ing|reading|writing|running|searching|planning|thinking|processing|loading|executing|testing|debugging|analyzed|read|wrote|ran)/i;
267
+ if (activityPattern.test(normalized) && normalized.length <= 220) return true;
268
+ if (/^initiating\\s/i.test(normalized) && normalized.length <= 500) return true;
269
+ if (/^thought for\\s/i.test(normalized) && normalized.length <= 500) return true;
270
+ return false;
271
+ };
272
+
273
+ const looksLikeToolOutput = (text) => {
274
+ const first = (text || '').trim().split('\\n')[0] || '';
275
+ if (/^[a-z0-9._-]+\\s*\\/\\s*[a-z0-9._-]+$/i.test(first)) return true;
276
+ if (/^full output written to\\b/i.test(first)) return true;
277
+ if (/^output\\.[a-z0-9._-]+(?:#l\\d+(?:-\\d+)?)?$/i.test(first)) return true;
278
+ var lower = (text || '').trim().toLowerCase();
279
+ if (/^title:\\s/.test(lower) && /\\surl:\\s/.test(lower) && /\\ssnippet:\\s/.test(lower)) return true;
280
+ if (/^(json|javascript|typescript|python|bash|sh|html|css|xml|yaml|yml|toml|sql|graphql|markdown|text|plaintext|log|ruby|go|rust|java|c|cpp|csharp|php|swift|kotlin)$/i.test(first)) return true;
281
+ return false;
282
+ };
283
+
284
+ const isInsideExcludedContainer = (node) => {
285
+ if (node.closest('details')) return true;
286
+ if (node.closest('[class*="feedback"], footer')) return true;
287
+ return false;
288
+ };
289
+
290
+ const results = [];
291
+ const seen = new Set();
292
+
293
+ for (const scope of scopes) {
294
+ for (const { sel } of selectors) {
295
+ const nodes = scope.querySelectorAll(sel);
296
+ for (let i = 0; i < nodes.length; i++) {
297
+ const node = nodes[i];
298
+ if (!node || seen.has(node)) continue;
299
+ seen.add(node);
300
+ if (isInsideExcludedContainer(node)) continue;
301
+ const text = (node.innerText || node.textContent || '').replace(/\\r/g, '').trim();
302
+ if (!text || text.length < 4) continue;
303
+ if (looksLikeActivityLog(text) || looksLikeToolOutput(text)) {
304
+ results.push(text.slice(0, 300));
305
+ }
306
+ }
307
+ }
308
+ }
309
+
310
+ return results;
311
+ })()`,
312
+ /** Quota error detection */
313
+ QUOTA_ERROR: `(() => {
314
+ const panel = document.querySelector('.antigravity-agent-side-panel');
315
+ const scope = panel || document;
316
+
317
+ const errorSelectors = [
318
+ '[role="alert"]',
319
+ '[class*="error"]',
320
+ '[class*="warning"]',
321
+ '[class*="toast"]',
322
+ '[class*="banner"]',
323
+ '[class*="notification"]',
324
+ '[class*="alert"]',
325
+ '[class*="quota"]',
326
+ '[class*="rate-limit"]',
327
+ ];
328
+ const errorElements = scope.querySelectorAll(errorSelectors.join(', '));
329
+ for (const el of errorElements) {
330
+ if (el.closest('.rendered-markdown, .prose, pre, code, [data-message-author-role="assistant"], [data-message-role="assistant"], [class*="message-content"]')) {
331
+ continue;
332
+ }
333
+ const text = (el.textContent || '').trim().toLowerCase();
334
+ if (text.includes('model quota reached') || text.includes('rate limit') || text.includes('quota exceeded')) {
335
+ return true;
336
+ }
337
+ }
338
+ return false;
339
+ })()`,
340
+ };
341
+ /**
342
+ * Lean AI response monitor.
343
+ *
344
+ * Each poll makes exactly 3 CDP calls: stop button, quota, text extraction.
345
+ * Completion: stop button gone N consecutive times -> complete.
346
+ * Simple baseline suppression via string comparison.
347
+ * NO network event subscription.
348
+ */
349
+ class ResponseMonitor {
350
+ cdpService;
351
+ pollIntervalMs;
352
+ maxDurationMs;
353
+ stopGoneConfirmCount;
354
+ onProgress;
355
+ onComplete;
356
+ onTimeout;
357
+ onPhaseChange;
358
+ onProcessLog;
359
+ pollTimer = null;
360
+ timeoutTimer = null;
361
+ isRunning = false;
362
+ lastText = null;
363
+ baselineText = null;
364
+ generationStarted = false;
365
+ currentPhase = 'waiting';
366
+ stopGoneCount = 0;
367
+ quotaDetected = false;
368
+ seenProcessLogKeys = new Set();
369
+ constructor(options) {
370
+ this.cdpService = options.cdpService;
371
+ this.pollIntervalMs = options.pollIntervalMs ?? 2000;
372
+ this.maxDurationMs = options.maxDurationMs ?? 300000;
373
+ this.stopGoneConfirmCount = options.stopGoneConfirmCount ?? 3;
374
+ this.onProgress = options.onProgress;
375
+ this.onComplete = options.onComplete;
376
+ this.onTimeout = options.onTimeout;
377
+ this.onPhaseChange = options.onPhaseChange;
378
+ this.onProcessLog = options.onProcessLog;
379
+ }
380
+ /** Start monitoring */
381
+ async start() {
382
+ if (this.isRunning)
383
+ return;
384
+ this.isRunning = true;
385
+ this.lastText = null;
386
+ this.baselineText = null;
387
+ this.generationStarted = false;
388
+ this.currentPhase = 'waiting';
389
+ this.stopGoneCount = 0;
390
+ this.quotaDetected = false;
391
+ this.seenProcessLogKeys = new Set();
392
+ // Always fire callback on start, even though phase is already 'waiting'
393
+ this.onPhaseChange?.('waiting', null);
394
+ // Capture baseline text
395
+ try {
396
+ const baseResult = await this.cdpService.call('Runtime.evaluate', this.buildEvaluateParams(exports.RESPONSE_SELECTORS.RESPONSE_TEXT));
397
+ const rawValue = baseResult?.result?.value;
398
+ this.baselineText = typeof rawValue === 'string' ? rawValue.trim() || null : null;
399
+ }
400
+ catch {
401
+ this.baselineText = null;
402
+ }
403
+ // Capture baseline process logs as already-seen keys
404
+ try {
405
+ const logResult = await this.cdpService.call('Runtime.evaluate', this.buildEvaluateParams(exports.RESPONSE_SELECTORS.PROCESS_LOGS));
406
+ const logEntries = logResult?.result?.value;
407
+ if (Array.isArray(logEntries)) {
408
+ this.seenProcessLogKeys = new Set(logEntries
409
+ .map((s) => (s || '').replace(/\r/g, '').trim())
410
+ .filter((s) => s.length > 0)
411
+ .map((s) => s.slice(0, 200)));
412
+ }
413
+ }
414
+ catch {
415
+ // baseline capture only
416
+ }
417
+ // Set timeout timer
418
+ if (this.maxDurationMs > 0) {
419
+ this.timeoutTimer = setTimeout(async () => {
420
+ const lastText = this.lastText ?? '';
421
+ this.setPhase('timeout', lastText);
422
+ await this.stop();
423
+ try {
424
+ await Promise.resolve(this.onTimeout?.(lastText));
425
+ }
426
+ catch (error) {
427
+ logger_1.logger.error('[ResponseMonitor] timeout callback failed:', error);
428
+ }
429
+ }, this.maxDurationMs);
430
+ }
431
+ logger_1.logger.info(`── Monitoring started | poll=${this.pollIntervalMs}ms timeout=${this.maxDurationMs / 1000}s baseline=${this.baselineText?.length ?? 0}ch`);
432
+ // Start polling
433
+ this.schedulePoll();
434
+ }
435
+ /** Stop monitoring */
436
+ async stop() {
437
+ this.isRunning = false;
438
+ if (this.pollTimer) {
439
+ clearTimeout(this.pollTimer);
440
+ this.pollTimer = null;
441
+ }
442
+ if (this.timeoutTimer) {
443
+ clearTimeout(this.timeoutTimer);
444
+ this.timeoutTimer = null;
445
+ }
446
+ }
447
+ /** Get current phase */
448
+ getPhase() {
449
+ return this.currentPhase;
450
+ }
451
+ /** Whether quota error was detected */
452
+ getQuotaDetected() {
453
+ return this.quotaDetected;
454
+ }
455
+ /** Whether monitoring is active */
456
+ isActive() {
457
+ return this.isRunning;
458
+ }
459
+ /** Get last extracted text */
460
+ getLastText() {
461
+ return this.lastText;
462
+ }
463
+ /** Click the stop button to interrupt LLM generation */
464
+ async clickStopButton() {
465
+ try {
466
+ const result = await this.cdpService.call('Runtime.evaluate', this.buildEvaluateParams(exports.RESPONSE_SELECTORS.CLICK_STOP_BUTTON));
467
+ const value = result?.result?.value;
468
+ if (this.isRunning) {
469
+ await this.stop();
470
+ }
471
+ return value ?? { ok: false, error: 'CDP evaluation returned empty' };
472
+ }
473
+ catch (error) {
474
+ return { ok: false, error: error.message || 'Failed to click stop button' };
475
+ }
476
+ }
477
+ setPhase(phase, text) {
478
+ if (this.currentPhase !== phase) {
479
+ this.currentPhase = phase;
480
+ const len = text?.length ?? 0;
481
+ switch (phase) {
482
+ case 'thinking':
483
+ logger_1.logger.phase('Thinking');
484
+ break;
485
+ case 'generating':
486
+ logger_1.logger.phase(`Generating (${len} chars)`);
487
+ break;
488
+ case 'complete':
489
+ logger_1.logger.done(`Complete (${len} chars)`);
490
+ break;
491
+ case 'timeout':
492
+ logger_1.logger.warn(`Timeout (${len} chars captured)`);
493
+ break;
494
+ case 'quotaReached':
495
+ logger_1.logger.warn('Quota Reached');
496
+ break;
497
+ default:
498
+ logger_1.logger.phase(`${phase}`);
499
+ }
500
+ this.onPhaseChange?.(phase, text);
501
+ }
502
+ }
503
+ schedulePoll() {
504
+ if (!this.isRunning)
505
+ return;
506
+ this.pollTimer = setTimeout(async () => {
507
+ await this.poll();
508
+ if (this.isRunning) {
509
+ this.schedulePoll();
510
+ }
511
+ }, this.pollIntervalMs);
512
+ }
513
+ buildEvaluateParams(expression) {
514
+ const params = {
515
+ expression,
516
+ returnByValue: true,
517
+ awaitPromise: true,
518
+ };
519
+ const contextId = this.cdpService.getPrimaryContextId?.();
520
+ if (contextId !== null && contextId !== undefined) {
521
+ params.contextId = contextId;
522
+ }
523
+ return params;
524
+ }
525
+ /**
526
+ * Single poll: exactly 4 CDP calls.
527
+ * 1. Stop button check
528
+ * 2. Quota error check
529
+ * 3. Text extraction
530
+ * 4. Process log extraction
531
+ */
532
+ async poll() {
533
+ try {
534
+ // 1. Stop button check
535
+ const stopResult = await this.cdpService.call('Runtime.evaluate', this.buildEvaluateParams(exports.RESPONSE_SELECTORS.STOP_BUTTON));
536
+ const stopValue = stopResult?.result?.value;
537
+ const isGenerating = !!(stopValue && typeof stopValue === 'object' && stopValue.isGenerating);
538
+ // 2. Quota error check
539
+ const quotaResult = await this.cdpService.call('Runtime.evaluate', this.buildEvaluateParams(exports.RESPONSE_SELECTORS.QUOTA_ERROR));
540
+ const quotaDetected = quotaResult?.result?.value === true;
541
+ // 3. Text extraction
542
+ const textResult = await this.cdpService.call('Runtime.evaluate', this.buildEvaluateParams(exports.RESPONSE_SELECTORS.RESPONSE_TEXT));
543
+ const rawText = textResult?.result?.value;
544
+ const exceptionDetail = textResult?.result?.exceptionDetails ?? textResult?.exceptionDetails;
545
+ if (exceptionDetail) {
546
+ logger_1.logger.warn('[ResponseMonitor:poll] RESPONSE_TEXT threw:', exceptionDetail.text ?? JSON.stringify(exceptionDetail).slice(0, 200));
547
+ }
548
+ const currentText = typeof rawText === 'string' ? rawText.trim() || null : null;
549
+ // 4. Process log extraction
550
+ try {
551
+ const logResult = await this.cdpService.call('Runtime.evaluate', this.buildEvaluateParams(exports.RESPONSE_SELECTORS.PROCESS_LOGS));
552
+ const logEntries = logResult?.result?.value;
553
+ if (Array.isArray(logEntries)) {
554
+ const newEntries = [];
555
+ for (const raw of logEntries) {
556
+ const normalized = (raw || '').replace(/\r/g, '').trim();
557
+ if (!normalized)
558
+ continue;
559
+ const key = normalized.slice(0, 200);
560
+ if (this.seenProcessLogKeys.has(key))
561
+ continue;
562
+ this.seenProcessLogKeys.add(key);
563
+ newEntries.push(normalized.slice(0, 300));
564
+ }
565
+ if (newEntries.length > 0) {
566
+ try {
567
+ this.onProcessLog?.(newEntries.join('\n\n'));
568
+ }
569
+ catch {
570
+ // callback error
571
+ }
572
+ }
573
+ }
574
+ }
575
+ catch {
576
+ // process log extraction is best-effort
577
+ }
578
+ // Handle stop button appearing
579
+ if (isGenerating) {
580
+ if (!this.generationStarted) {
581
+ this.generationStarted = true;
582
+ this.setPhase('thinking', null);
583
+ }
584
+ this.stopGoneCount = 0;
585
+ }
586
+ // Handle quota detection
587
+ if (quotaDetected) {
588
+ const hasText = !!(this.lastText && this.lastText.trim().length > 0);
589
+ logger_1.logger.warn(`[ResponseMonitor] quota detected hasText=${hasText}`);
590
+ if (hasText) {
591
+ this.quotaDetected = true;
592
+ }
593
+ else {
594
+ this.setPhase('quotaReached', '');
595
+ await this.stop();
596
+ try {
597
+ await Promise.resolve(this.onComplete?.(''));
598
+ }
599
+ catch (error) {
600
+ logger_1.logger.error('[ResponseMonitor] complete callback failed:', error);
601
+ }
602
+ return;
603
+ }
604
+ }
605
+ // Baseline suppression: do not emit progress for pre-existing text.
606
+ // IMPORTANT: do not early-return here; completion logic must still run.
607
+ const effectiveText = (currentText !== null &&
608
+ this.baselineText !== null &&
609
+ currentText === this.baselineText &&
610
+ this.lastText === null) ? null : currentText;
611
+ // Text change handling
612
+ const textChanged = effectiveText !== null && effectiveText !== this.lastText;
613
+ if (textChanged) {
614
+ this.lastText = effectiveText;
615
+ if (this.currentPhase === 'waiting' || this.currentPhase === 'thinking') {
616
+ this.setPhase('generating', effectiveText);
617
+ if (!this.generationStarted) {
618
+ this.generationStarted = true;
619
+ }
620
+ }
621
+ this.onProgress?.(effectiveText);
622
+ }
623
+ // Completion: stop button gone N consecutive times
624
+ if (!isGenerating && this.generationStarted) {
625
+ this.stopGoneCount++;
626
+ if (this.stopGoneCount >= this.stopGoneConfirmCount) {
627
+ const finalText = this.lastText ?? '';
628
+ this.setPhase('complete', finalText);
629
+ await this.stop();
630
+ try {
631
+ await Promise.resolve(this.onComplete?.(finalText));
632
+ }
633
+ catch (error) {
634
+ logger_1.logger.error('[ResponseMonitor] complete callback failed:', error);
635
+ }
636
+ return;
637
+ }
638
+ }
639
+ }
640
+ catch (error) {
641
+ logger_1.logger.error('[ResponseMonitor] poll error:', error);
642
+ }
643
+ }
644
+ }
645
+ exports.ResponseMonitor = ResponseMonitor;