lazy-gravity 0.10.0 → 0.11.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.
@@ -61,24 +61,37 @@ class QuestionDetector extends events_1.EventEmitter {
61
61
  const callParams = {
62
62
  expression: `
63
63
  (() => {
64
+ const getInteractiveItems = (elContainer) => {
65
+ return Array.from(elContainer.querySelectorAll('li, label, a, [role="radio"], [role="option"], [class*="cursor-pointer"]'))
66
+ .filter(el => {
67
+ if (el.tagName === 'BUTTON' || el.closest('button')) return false;
68
+ const role = el.getAttribute('role');
69
+ if (['radio', 'option', 'checkbox', 'button', 'menuitem'].includes(role)) return true;
70
+ if (el.tagName === 'A' || el.tagName === 'LABEL') return true;
71
+ const style = window.getComputedStyle(el);
72
+ return style.cursor === 'pointer';
73
+ });
74
+ };
64
75
  const containers = Array.from(document.querySelectorAll('div, form, dialog')).reverse();
65
76
  let targetList = null;
66
77
  let submitBtn = null;
67
78
 
68
79
  for (const container of containers) {
80
+ const hasTextInput = container.querySelector('textarea, input:not([type="hidden"]):not([type="radio"]):not([type="checkbox"]), [contenteditable], [role="textbox"], .monaco-editor, .editor-container, .inputarea, vscode-text-field, vscode-text-area');
81
+ const hasExplicitQuestionItems = container.querySelector('[role="radio"], input[type="radio"], input[type="checkbox"], [role="option"], [role="checkbox"]');
82
+ if (hasTextInput && !hasExplicitQuestionItems) continue;
69
83
  const buttons = Array.from(container.querySelectorAll('button'));
70
84
  let possibleSubmitBtn = null;
71
85
  for (const btn of buttons) {
72
86
  const text = btn.textContent?.toLowerCase() || '';
73
- if (text.includes('submit')) {
87
+ if (text.includes('submit') || text.includes('continue')) {
74
88
  possibleSubmitBtn = btn;
75
89
  break;
76
90
  }
77
91
  }
78
92
 
79
93
  if (possibleSubmitBtn) {
80
- const items = Array.from(container.querySelectorAll('li, label, a, [role="radio"], [role="option"], [class*="cursor-pointer"]'))
81
- .filter(el => el.tagName !== 'BUTTON' && !el.closest('button'));
94
+ const items = getInteractiveItems(container);
82
95
 
83
96
  if (items.length > 1) {
84
97
  targetList = container;
@@ -90,30 +103,38 @@ class QuestionDetector extends events_1.EventEmitter {
90
103
 
91
104
  if (!targetList || !submitBtn) return { found: false };
92
105
 
93
- const items = Array.from(targetList.querySelectorAll('li, label, a, [role="radio"], [role="option"], [class*="cursor-pointer"]'))
94
- .filter(el => el.tagName !== 'BUTTON' && !el.closest('button'));
106
+ const items = getInteractiveItems(targetList);
95
107
  if (items.length <= ${index}) return { found: false };
96
108
 
97
109
  const targetOption = items[${index}];
98
110
 
99
- const optionRect = targetOption.getBoundingClientRect();
100
- const btnRect = submitBtn.getBoundingClientRect();
101
-
102
- return {
103
- found: true,
104
- option: {
105
- x: Math.round(optionRect.left + optionRect.width / 2),
106
- y: Math.round(optionRect.top + optionRect.height / 2)
107
- },
108
- button: {
109
- x: Math.round(btnRect.left + btnRect.width / 2),
110
- y: Math.round(btnRect.top + btnRect.height / 2)
111
+ const clickElement = (el) => {
112
+ const rect = el.getBoundingClientRect();
113
+ const clickX = rect.left + rect.width / 2;
114
+ const clickY = rect.top + rect.height / 2;
115
+ const events = ['pointerdown', 'mousedown', 'mouseup', 'click'];
116
+ for (const type of events) {
117
+ el.dispatchEvent(new MouseEvent(type, {
118
+ bubbles: true,
119
+ cancelable: true,
120
+ view: window,
121
+ clientX: clickX,
122
+ clientY: clickY,
123
+ }));
111
124
  }
112
125
  };
126
+
127
+ return new Promise(resolve => {
128
+ clickElement(targetOption);
129
+ setTimeout(() => {
130
+ clickElement(submitBtn);
131
+ resolve({ found: true });
132
+ }, 50);
133
+ });
113
134
  })()
114
135
  `,
115
136
  returnByValue: true,
116
- awaitPromise: false,
137
+ awaitPromise: true,
117
138
  };
118
139
  if (contextId !== null) {
119
140
  callParams.contextId = contextId;
@@ -124,37 +145,6 @@ class QuestionDetector extends events_1.EventEmitter {
124
145
  this.logger.warn(`[QuestionDetector:${this.projectName}] Could not find question modal elements during submission.`);
125
146
  return false;
126
147
  }
127
- // Click the option
128
- await this.cdp.call('Input.dispatchMouseEvent', {
129
- type: 'mousePressed',
130
- x: result.option.x,
131
- y: result.option.y,
132
- button: 'left',
133
- clickCount: 1,
134
- });
135
- await this.cdp.call('Input.dispatchMouseEvent', {
136
- type: 'mouseReleased',
137
- x: result.option.x,
138
- y: result.option.y,
139
- button: 'left',
140
- clickCount: 1,
141
- });
142
- await new Promise(resolve => setTimeout(resolve, 50));
143
- // Click the submit button
144
- await this.cdp.call('Input.dispatchMouseEvent', {
145
- type: 'mousePressed',
146
- x: result.button.x,
147
- y: result.button.y,
148
- button: 'left',
149
- clickCount: 1,
150
- });
151
- await this.cdp.call('Input.dispatchMouseEvent', {
152
- type: 'mouseReleased',
153
- x: result.button.x,
154
- y: result.button.y,
155
- button: 'left',
156
- clickCount: 1,
157
- });
158
148
  this.lastQuestionDetected = false;
159
149
  return true;
160
150
  }
@@ -170,12 +160,26 @@ class QuestionDetector extends events_1.EventEmitter {
170
160
  const callParams = {
171
161
  expression: `
172
162
  (() => {
163
+ const getInteractiveItems = (elContainer) => {
164
+ return Array.from(elContainer.querySelectorAll('li, label, a, [role="radio"], [role="option"], [class*="cursor-pointer"]'))
165
+ .filter(el => {
166
+ if (el.tagName === 'BUTTON' || el.closest('button')) return false;
167
+ const role = el.getAttribute('role');
168
+ if (['radio', 'option', 'checkbox', 'button', 'menuitem'].includes(role)) return true;
169
+ if (el.tagName === 'A' || el.tagName === 'LABEL') return true;
170
+ const style = window.getComputedStyle(el);
171
+ return style.cursor === 'pointer';
172
+ });
173
+ };
174
+
173
175
  const containers = Array.from(document.querySelectorAll('div, form, dialog')).reverse();
174
176
  let skipBtn = null;
175
177
 
176
178
  for (const container of containers) {
177
- const items = Array.from(container.querySelectorAll('li, [role="radio"], [role="option"], .cursor-pointer'))
178
- .filter(el => el.tagName !== 'BUTTON' && !el.closest('button'));
179
+ const hasTextInput = container.querySelector('textarea, input:not([type="hidden"]):not([type="radio"]):not([type="checkbox"]), [contenteditable="true"], [contenteditable=""], [role="textbox"]');
180
+ const hasExplicitQuestionItems = container.querySelector('[role="radio"], input[type="radio"], input[type="checkbox"], [role="option"], [role="checkbox"]');
181
+ if (hasTextInput && !hasExplicitQuestionItems) continue;
182
+ const items = getInteractiveItems(container);
179
183
  const hasList = items.length > 1;
180
184
 
181
185
  const buttons = Array.from(container.querySelectorAll('button'));
@@ -191,14 +195,24 @@ class QuestionDetector extends events_1.EventEmitter {
191
195
 
192
196
  if (!skipBtn) return { found: false };
193
197
 
194
- const btnRect = skipBtn.getBoundingClientRect();
195
- return {
196
- found: true,
197
- button: {
198
- x: Math.round(btnRect.left + btnRect.width / 2),
199
- y: Math.round(btnRect.top + btnRect.height / 2)
198
+ const clickElement = (el) => {
199
+ const rect = el.getBoundingClientRect();
200
+ const clickX = rect.left + rect.width / 2;
201
+ const clickY = rect.top + rect.height / 2;
202
+ const events = ['pointerdown', 'mousedown', 'mouseup', 'click'];
203
+ for (const type of events) {
204
+ el.dispatchEvent(new MouseEvent(type, {
205
+ bubbles: true,
206
+ cancelable: true,
207
+ view: window,
208
+ clientX: clickX,
209
+ clientY: clickY,
210
+ }));
200
211
  }
201
212
  };
213
+
214
+ clickElement(skipBtn);
215
+ return { found: true };
202
216
  })()
203
217
  `,
204
218
  returnByValue: true,
@@ -212,20 +226,6 @@ class QuestionDetector extends events_1.EventEmitter {
212
226
  this.logger.warn(`[QuestionDetector:${this.projectName}] Could not find skip button.`);
213
227
  return false;
214
228
  }
215
- await this.cdp.call('Input.dispatchMouseEvent', {
216
- type: 'mousePressed',
217
- x: result.button.x,
218
- y: result.button.y,
219
- button: 'left',
220
- clickCount: 1,
221
- });
222
- await this.cdp.call('Input.dispatchMouseEvent', {
223
- type: 'mouseReleased',
224
- x: result.button.x,
225
- y: result.button.y,
226
- button: 'left',
227
- clickCount: 1,
228
- });
229
229
  this.lastQuestionDetected = false;
230
230
  return true;
231
231
  }
@@ -241,24 +241,73 @@ class QuestionDetector extends events_1.EventEmitter {
241
241
  const callParams = {
242
242
  expression: `
243
243
  (() => {
244
+ const STOP_PATTERNS = [
245
+ /^stop$/,
246
+ /^stop generating$/,
247
+ /^stop response$/,
248
+ /^停止$/,
249
+ /^生成を停止$/,
250
+ /^応答を停止$/,
251
+ ];
252
+ const normalize = (value) => (value || '').toLowerCase().replace(/\\s+/g, ' ').trim();
253
+ const isStopLabel = (value) => {
254
+ const normalized = normalize(value);
255
+ if (!normalized) return false;
256
+ return STOP_PATTERNS.some((re) => re.test(normalized));
257
+ };
258
+ const isGenerating = Array.from(document.querySelectorAll('button, [role="button"]')).some(btn => {
259
+ const labels = [
260
+ btn.textContent || '',
261
+ btn.getAttribute('aria-label') || '',
262
+ btn.getAttribute('title') || '',
263
+ ];
264
+ return labels.some(isStopLabel);
265
+ }) || (() => {
266
+ const panel = document.querySelector('.antigravity-agent-side-panel');
267
+ if (panel) {
268
+ const panelText = (panel.textContent || '').trim();
269
+ return /Working\.\s*$/i.test(panelText);
270
+ }
271
+ return false;
272
+ })();
273
+ if (isGenerating) {
274
+ return { detected: false, reason: "IDE is generating" };
275
+ }
276
+
277
+ const getInteractiveItems = (elContainer) => {
278
+ return Array.from(elContainer.querySelectorAll('li, label, a, [role="radio"], [role="option"], [class*="cursor-pointer"]'))
279
+ .filter(el => {
280
+ if (el.tagName === 'BUTTON' || el.closest('button')) return false;
281
+ const text = (el.innerText || el.textContent || '').trim();
282
+ if (!text) return false;
283
+ const role = el.getAttribute('role');
284
+ if (['radio', 'option', 'checkbox', 'button', 'menuitem'].includes(role)) return true;
285
+ if (el.tagName === 'A' || el.tagName === 'LABEL') return true;
286
+ const style = window.getComputedStyle(el);
287
+ return style.cursor === 'pointer';
288
+ });
289
+ };
290
+
244
291
  const containers = Array.from(document.querySelectorAll('div, form, dialog')).reverse();
245
292
  let targetList = null;
246
293
  let submitBtn = null;
247
294
 
248
295
  for (const container of containers) {
296
+ const hasTextInput = container.querySelector('textarea, input:not([type="hidden"]):not([type="radio"]):not([type="checkbox"]), [contenteditable], [role="textbox"], .monaco-editor, .editor-container, .inputarea, vscode-text-field, vscode-text-area');
297
+ const hasExplicitQuestionItems = container.querySelector('[role="radio"], input[type="radio"], input[type="checkbox"], [role="option"], [role="checkbox"]');
298
+ if (hasTextInput && !hasExplicitQuestionItems) continue;
249
299
  const buttons = Array.from(container.querySelectorAll('button'));
250
300
  let possibleSubmitBtn = null;
251
301
  for (const btn of buttons) {
252
302
  const text = btn.textContent?.toLowerCase() || '';
253
- if (text.includes('submit')) {
303
+ if (text.includes('submit') || text.includes('continue')) {
254
304
  possibleSubmitBtn = btn;
255
305
  break;
256
306
  }
257
307
  }
258
308
 
259
309
  if (possibleSubmitBtn) {
260
- const items = Array.from(container.querySelectorAll('li, label, a, [role="radio"], [role="option"], [class*="cursor-pointer"]'))
261
- .filter(el => el.tagName !== 'BUTTON' && !el.closest('button'));
310
+ const items = getInteractiveItems(container);
262
311
 
263
312
  if (items.length > 1) {
264
313
  targetList = container;
@@ -280,8 +329,7 @@ class QuestionDetector extends events_1.EventEmitter {
280
329
  }
281
330
  const title = titleEl ? (titleEl.innerText || titleEl.textContent || '').trim() : 'Question';
282
331
 
283
- const items = Array.from(targetList.querySelectorAll('li, label, a, [role="radio"], [role="option"], [class*="cursor-pointer"]'))
284
- .filter(el => el.tagName !== 'BUTTON' && !el.closest('button'));
332
+ const items = getInteractiveItems(targetList);
285
333
  const options = items.map(n => {
286
334
  const rect = n.getBoundingClientRect();
287
335
  const finalLabel = n.innerText || n.textContent || 'Option';
@@ -321,6 +369,8 @@ class QuestionDetector extends events_1.EventEmitter {
321
369
  this.logger.debug(`[QuestionDetector] Error evaluating on ctx ${contextId}: ${e.message}`);
322
370
  return null;
323
371
  });
372
+ if (!this._isStarted)
373
+ return;
324
374
  if (response?.exceptionDetails) {
325
375
  this.logger.debug(`[QuestionDetector] Exception on ctx ${contextId}: ${response.exceptionDetails.exception?.description || response.exceptionDetails.text}`);
326
376
  }
@@ -336,6 +386,8 @@ class QuestionDetector extends events_1.EventEmitter {
336
386
  lastReason = result.reason;
337
387
  }
338
388
  }
389
+ if (!this._isStarted)
390
+ return;
339
391
  const result = detectedResult;
340
392
  if (result && result.detected) {
341
393
  this.emptyPollCount = 0;
@@ -146,6 +146,13 @@ exports.RESPONSE_SELECTORS = {
146
146
  }
147
147
  }
148
148
 
149
+ if (panel) {
150
+ const panelText = (panel.textContent || '').trim();
151
+ if (/Working\.\s*$/i.test(panelText)) {
152
+ return { isGenerating: true };
153
+ }
154
+ }
155
+
149
156
  return { isGenerating: false };
150
157
  })()`,
151
158
  /** Click stop button via tooltip-id + text fallback */
@@ -629,6 +636,7 @@ class ResponseMonitor {
629
636
  onCdpReconnectFailed = null;
630
637
  // Activity-based timeout (#49)
631
638
  lastActivityTime = 0;
639
+ startTime = 0;
632
640
  constructor(options) {
633
641
  this.cdpService = options.cdpService;
634
642
  this.pollIntervalMs = options.pollIntervalMs ?? 2000;
@@ -731,6 +739,7 @@ class ResponseMonitor {
731
739
  }
732
740
  // Activity-based timeout: track last activity time instead of fixed timer (#49)
733
741
  this.lastActivityTime = Date.now();
742
+ this.startTime = Date.now();
734
743
  // Register CDP connection event listeners (#48)
735
744
  this.registerCdpConnectionListeners();
736
745
  const mode = passive ? 'Passive monitoring' : 'Monitoring';
@@ -1109,7 +1118,14 @@ class ResponseMonitor {
1109
1118
  // Guard: never timeout while the stop button is visible — it means
1110
1119
  // Antigravity is still actively generating (extended thinking, long
1111
1120
  // shell commands, large file operations, etc.).
1112
- if (this.maxDurationMs > 0 && !isGenerating && Date.now() - this.lastActivityTime >= this.maxDurationMs) {
1121
+ // Hard timeout limit (20 minutes absolute hard timeout)
1122
+ const hardTimeoutLimit = 1200000;
1123
+ const totalElapsed = Date.now() - this.startTime;
1124
+ const isHardTimeout = totalElapsed >= hardTimeoutLimit;
1125
+ if (isHardTimeout || (this.maxDurationMs > 0 && !isGenerating && Date.now() - this.lastActivityTime >= this.maxDurationMs)) {
1126
+ if (isHardTimeout) {
1127
+ logger_1.logger.warn(`[ResponseMonitor] Hard timeout of 20 minutes reached.`);
1128
+ }
1113
1129
  const lastText = this.lastText ?? '';
1114
1130
  this.setPhase('timeout', lastText);
1115
1131
  await this.stop();
@@ -3,7 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.fileOpenCache = void 0;
4
4
  /**
5
5
  * In-memory cache to map button customIds to file URLs.
6
- * Bounded to a maximum size to prevent memory leaks, and entries are removed once consumed.
6
+ * Bounded to a maximum size to prevent memory leaks, using a FIFO eviction policy when the cache size limit is reached.
7
7
  */
8
8
  class BoundedCache {
9
9
  max_size;
@@ -23,11 +23,7 @@ class BoundedCache {
23
23
  this.map.set(key, value);
24
24
  }
25
25
  get(key) {
26
- const value = this.map.get(key);
27
- if (value !== undefined) {
28
- this.map.delete(key); // Remove upon consumption
29
- }
30
- return value;
26
+ return this.map.get(key);
31
27
  }
32
28
  }
33
29
  exports.fileOpenCache = new BoundedCache(1000);
@@ -1,6 +1,9 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.parseQuestionCustomId = parseQuestionCustomId;
4
+ exports.executeBrowserClick = executeBrowserClick;
5
+ const logger_1 = require("./logger");
6
+ const approvalDetector_1 = require("../services/approvalDetector");
4
7
  function parseQuestionCustomId(customId, expectedPrefix) {
5
8
  if (customId !== expectedPrefix && !customId.startsWith(expectedPrefix + ':'))
6
9
  return null;
@@ -23,3 +26,22 @@ function parseQuestionCustomId(customId, expectedPrefix) {
23
26
  }
24
27
  return result;
25
28
  }
29
+ /**
30
+ * Executes a click on an element matching the target text in the IDE.
31
+ * Returns true if the click was successfully dispatched, false otherwise.
32
+ */
33
+ async function executeBrowserClick(cdp, buttonText) {
34
+ try {
35
+ const script = (0, approvalDetector_1.buildClickScript)(buttonText);
36
+ const evalResult = await cdp.call('Runtime.evaluate', {
37
+ expression: script,
38
+ returnByValue: true,
39
+ awaitPromise: true,
40
+ });
41
+ return !!evalResult?.result?.value?.ok;
42
+ }
43
+ catch (error) {
44
+ logger_1.logger.error(`[executeBrowserClick] Error clicking button "${buttonText}":`, error);
45
+ return false;
46
+ }
47
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lazy-gravity",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "Control Antigravity from anywhere — a local, secure bot (Discord + Telegram) that lets you remotely operate Antigravity on your home PC from your smartphone.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {