lazy-gravity 0.9.2 → 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.
Files changed (41) hide show
  1. package/dist/bin/commands/doctor.js +45 -7
  2. package/dist/bot/index.js +401 -41
  3. package/dist/bot/telegramMessageHandler.js +1 -0
  4. package/dist/commands/chatCommandHandler.js +43 -3
  5. package/dist/commands/registerSlashCommands.js +13 -1
  6. package/dist/events/interactionCreateHandler.js +456 -27
  7. package/dist/events/messageCreateHandler.js +66 -7
  8. package/dist/handlers/__tests__/fileChangeButtonAction.test.js +85 -0
  9. package/dist/handlers/approvalButtonAction.js +2 -1
  10. package/dist/handlers/errorPopupButtonAction.js +2 -1
  11. package/dist/handlers/fileChangeButtonAction.js +57 -0
  12. package/dist/handlers/genericActionButtonAction.js +72 -0
  13. package/dist/handlers/planningButtonAction.js +126 -23
  14. package/dist/handlers/planningModalSubmitAction.js +38 -0
  15. package/dist/handlers/questionSelectAction.js +70 -0
  16. package/dist/handlers/questionSkipAction.js +68 -0
  17. package/dist/handlers/runCommandButtonAction.js +2 -1
  18. package/dist/platform/discord/discordResponseRenderer.js +164 -0
  19. package/dist/platform/discord/wrappers.js +76 -0
  20. package/dist/services/approvalDetector.js +110 -35
  21. package/dist/services/artifactService.js +103 -37
  22. package/dist/services/assistantDomExtractor.js +194 -3
  23. package/dist/services/cdpBridgeManager.js +149 -8
  24. package/dist/services/cdpConnectionPool.js +22 -2
  25. package/dist/services/cdpService.js +134 -15
  26. package/dist/services/chatSessionService.js +147 -40
  27. package/dist/services/errorPopupDetector.js +23 -11
  28. package/dist/services/notificationSender.js +80 -3
  29. package/dist/services/planningDetector.js +69 -25
  30. package/dist/services/promptDispatcher.js +27 -1
  31. package/dist/services/questionDetector.js +428 -0
  32. package/dist/services/responseMonitor.js +45 -3
  33. package/dist/services/runCommandDetector.js +14 -7
  34. package/dist/ui/artifactsUi.js +4 -3
  35. package/dist/utils/consecutiveEmptyPollGate.js +21 -0
  36. package/dist/utils/fileOpenCache.js +29 -0
  37. package/dist/utils/htmlToDiscordMarkdown.js +5 -2
  38. package/dist/utils/pathUtils.js +12 -0
  39. package/dist/utils/projectResolver.js +10 -0
  40. package/dist/utils/questionActionUtils.js +47 -0
  41. package/package.json +1 -1
@@ -102,6 +102,12 @@ const WORKSPACE_STATE_SCRIPT = `(() => {
102
102
  if (isGenerating) break;
103
103
  }
104
104
  }
105
+ if (!isGenerating && panel) {
106
+ const panelText = (panel.textContent || '').trim();
107
+ if (/Working\.\s*$/i.test(panelText)) {
108
+ isGenerating = true;
109
+ }
110
+ }
105
111
 
106
112
  if (!panel) {
107
113
  return { isGenerating, sessionTitle: '', hasActiveChat: false };
@@ -407,6 +413,9 @@ class CdpService extends events_1.EventEmitter {
407
413
  getCurrentWorkspaceName() {
408
414
  return this.currentWorkspaceName;
409
415
  }
416
+ getCurrentWorkspacePath() {
417
+ return this.currentWorkspacePath;
418
+ }
410
419
  /**
411
420
  * Discover and connect to the workbench page for the specified workspace.
412
421
  * Does nothing if already connected to the correct page.
@@ -459,7 +468,8 @@ class CdpService extends events_1.EventEmitter {
459
468
  returnByValue: true,
460
469
  });
461
470
  const liveTitle = String(titleResult?.result?.value || '');
462
- if (liveTitle.toLowerCase().includes(projectName.toLowerCase())) {
471
+ const titleParts = liveTitle.split(' - ');
472
+ if (titleParts[0].trim().toLowerCase() === projectName.toLowerCase()) {
463
473
  this.currentWorkspaceName = projectName;
464
474
  return true;
465
475
  }
@@ -506,7 +516,12 @@ class CdpService extends events_1.EventEmitter {
506
516
  logger_1.logger.debug(` - title="${p.title}" url=${p.url}`);
507
517
  }
508
518
  // 1. Title match (fast path)
509
- const titleMatch = workbenchPages.find((t) => t.title?.includes(projectName));
519
+ const titleMatch = workbenchPages.find((t) => {
520
+ if (!t.title)
521
+ return false;
522
+ const parts = t.title.split(' - ');
523
+ return parts[0].trim().toLowerCase() === projectName.toLowerCase();
524
+ });
510
525
  if (titleMatch) {
511
526
  return this.connectToPage(titleMatch, projectName);
512
527
  }
@@ -559,9 +574,8 @@ class CdpService extends events_1.EventEmitter {
559
574
  returnByValue: true,
560
575
  });
561
576
  const liveTitle = String(result?.result?.value || '');
562
- const normalizedLiveTitle = liveTitle.toLowerCase();
563
- const normalizedProject = projectName.toLowerCase();
564
- if (normalizedLiveTitle.includes(normalizedProject)) {
577
+ const liveParts = liveTitle.split(' - ');
578
+ if (liveParts[0].trim().toLowerCase() === projectName.toLowerCase()) {
565
579
  this.currentWorkspaceName = projectName;
566
580
  logger_1.logger.debug(`[CdpService] Probe success: detected "${projectName}"`);
567
581
  return true;
@@ -628,9 +642,11 @@ class CdpService extends events_1.EventEmitter {
628
642
  const detectedValue = value.value;
629
643
  const normalizedDetected = detectedValue.toLowerCase();
630
644
  const normalizedProject = projectName.toLowerCase();
631
- const normalizedWorkspace = workspacePath.toLowerCase();
632
- if (normalizedDetected.includes(normalizedProject) ||
633
- normalizedDetected.includes(normalizedWorkspace)) {
645
+ const normalizedWorkspace = workspacePath.toLowerCase().replace(/\\/g, '/');
646
+ const detectedParts = normalizedDetected.split(',').map((s) => s.trim());
647
+ if (detectedParts.includes(normalizedProject) ||
648
+ normalizedDetected.includes(normalizedWorkspace) ||
649
+ normalizedDetected.replace(/\\/g, '/').includes(normalizedWorkspace)) {
634
650
  this.currentWorkspaceName = projectName;
635
651
  logger_1.logger.debug(`[CdpService] Folder path match success: "${projectName}"`);
636
652
  return true;
@@ -641,11 +657,20 @@ class CdpService extends events_1.EventEmitter {
641
657
  expression: 'window.location.href',
642
658
  returnByValue: true,
643
659
  });
644
- const pageUrl = (urlResult?.result?.value || '').toLowerCase();
645
- const normalizedWorkspaceUri = encodeURIComponent(workspacePath).toLowerCase();
646
- if (pageUrl.includes(normalizedWorkspaceUri) || pageUrl.includes(projectName.toLowerCase())) {
660
+ const pageUrl = decodeURIComponent(urlResult?.result?.value || '').toLowerCase().replace(/\\/g, '/');
661
+ const normalizedWorkspaceUri = workspacePath.toLowerCase().replace(/\\/g, '/');
662
+ if (pageUrl.includes(normalizedWorkspaceUri)) {
647
663
  this.currentWorkspaceName = projectName;
648
- logger_1.logger.debug(`[CdpService] URL parameter match success: "${projectName}"`);
664
+ logger_1.logger.debug(`[CdpService] URL parameter match success: "${projectName}" (workspace URI)`);
665
+ return true;
666
+ }
667
+ // If we only have the project name to go on, make sure it's a discrete boundary in the URL path
668
+ // e.g. "folder=.../test" or "workspace=.../test". We prevent false positives like "latest" matching "test".
669
+ const safeProjectName = projectName.toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
670
+ const projectBoundaryRegex = new RegExp(`[=/]${safeProjectName}(?:$|&|\\?)`);
671
+ if (projectBoundaryRegex.test(pageUrl)) {
672
+ this.currentWorkspaceName = projectName;
673
+ logger_1.logger.debug(`[CdpService] URL parameter match success: "${projectName}" (boundary regex)`);
649
674
  return true;
650
675
  }
651
676
  }
@@ -747,7 +772,12 @@ class CdpService extends events_1.EventEmitter {
747
772
  !t.url?.includes('workbench-jetski-agent') &&
748
773
  t.url?.includes('workbench'));
749
774
  // Title match
750
- const titleMatch = workbenchPages.find((t) => t.title?.toLowerCase().includes(projectName.toLowerCase()));
775
+ const titleMatch = workbenchPages.find((t) => {
776
+ if (!t.title)
777
+ return false;
778
+ const parts = t.title.split(' - ');
779
+ return parts[0].trim().toLowerCase() === projectName.toLowerCase();
780
+ });
751
781
  if (titleMatch) {
752
782
  return this.connectToPage(titleMatch, projectName);
753
783
  }
@@ -1276,9 +1306,15 @@ class CdpService extends events_1.EventEmitter {
1276
1306
  ].some((fragment) => message.includes(fragment));
1277
1307
  }
1278
1308
  async injectMessageCore(text, imageFilePaths) {
1279
- const focusResult = await this.waitForChatInputReady();
1309
+ // Check if chat input is already available
1310
+ let focusResult = await this.waitForChatInputReady(500);
1280
1311
  if (!focusResult.ok) {
1281
- return { ok: false, error: focusResult.error || 'Chat input field not found' };
1312
+ // Send Cmd+L / Ctrl+L to guarantee application-level focus on the chat panel (and open it if closed)
1313
+ await this.focusChatPanelViaShortcut();
1314
+ focusResult = await this.waitForChatInputReady();
1315
+ if (!focusResult.ok) {
1316
+ return { ok: false, error: focusResult.error || 'Chat input field not found' };
1317
+ }
1282
1318
  }
1283
1319
  // Clear any existing text in the input field before injecting.
1284
1320
  await this.clearInputField();
@@ -1377,6 +1413,89 @@ class CdpService extends events_1.EventEmitter {
1377
1413
  nativeVirtualKeyCode: 13,
1378
1414
  });
1379
1415
  }
1416
+ /**
1417
+ * Opens the chat panel and waits for the UI to be ready.
1418
+ * Can be called right after the IDE launches to ensure the panel is visible.
1419
+ */
1420
+ async openChatPanel() {
1421
+ if (!await this.isConnected())
1422
+ return;
1423
+ // Ensure IDE UI is fully loaded before sending shortcut
1424
+ const deadline = Date.now() + 15000;
1425
+ let workbenchReady = false;
1426
+ while (Date.now() < deadline) {
1427
+ try {
1428
+ const res = await this.call('Runtime.evaluate', {
1429
+ expression: '!!document.querySelector(".monaco-workbench")',
1430
+ returnByValue: true
1431
+ });
1432
+ if (res?.result?.value) {
1433
+ workbenchReady = true;
1434
+ break;
1435
+ }
1436
+ }
1437
+ catch {
1438
+ // ignore
1439
+ }
1440
+ await new Promise(r => setTimeout(r, 500));
1441
+ }
1442
+ if (workbenchReady) {
1443
+ // Check if chat panel is already open to avoid toggling it closed
1444
+ const focusResult = await this.waitForChatInputReady(1000);
1445
+ if (!focusResult.ok) {
1446
+ await this.focusChatPanelViaShortcut();
1447
+ }
1448
+ }
1449
+ }
1450
+ /**
1451
+ * Send Cmd+L / Ctrl+L to focus the chat panel via shortcut.
1452
+ */
1453
+ async focusChatPanelViaShortcut() {
1454
+ const modifiers = process.platform === 'darwin' ? 4 : 2; // Meta : Ctrl
1455
+ await this.call('Input.dispatchKeyEvent', {
1456
+ type: 'keyDown',
1457
+ key: 'l',
1458
+ code: 'KeyL',
1459
+ modifiers,
1460
+ windowsVirtualKeyCode: 76,
1461
+ nativeVirtualKeyCode: 76,
1462
+ });
1463
+ await this.call('Input.dispatchKeyEvent', {
1464
+ type: 'keyUp',
1465
+ key: 'l',
1466
+ code: 'KeyL',
1467
+ modifiers,
1468
+ windowsVirtualKeyCode: 76,
1469
+ nativeVirtualKeyCode: 76,
1470
+ });
1471
+ await new Promise(r => setTimeout(r, 100)); // wait for focus to shift
1472
+ }
1473
+ /**
1474
+ * Send Cmd+W / Ctrl+W to close the active editor.
1475
+ */
1476
+ async closeActiveEditor() {
1477
+ if (!this.isConnectedFlag || !this.ws) {
1478
+ throw new Error('Not connected to CDP. Call connect() first.');
1479
+ }
1480
+ const modifiers = process.platform === 'darwin' ? 4 : 2; // Meta : Ctrl
1481
+ await this.call('Input.dispatchKeyEvent', {
1482
+ type: 'keyDown',
1483
+ key: 'w',
1484
+ code: 'KeyW',
1485
+ modifiers,
1486
+ windowsVirtualKeyCode: 87,
1487
+ nativeVirtualKeyCode: 87,
1488
+ });
1489
+ await this.call('Input.dispatchKeyEvent', {
1490
+ type: 'keyUp',
1491
+ key: 'w',
1492
+ code: 'KeyW',
1493
+ modifiers,
1494
+ windowsVirtualKeyCode: 87,
1495
+ nativeVirtualKeyCode: 87,
1496
+ });
1497
+ await new Promise(r => setTimeout(r, 100));
1498
+ }
1380
1499
  /**
1381
1500
  * Detect file input in the UI and attach the specified files.
1382
1501
  */
@@ -3,11 +3,29 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ChatSessionService = void 0;
4
4
  exports.buildActivateViaPastConversationsScript = buildActivateViaPastConversationsScript;
5
5
  const logger_1 = require("../utils/logger");
6
+ /** Shared fuzzy-matching logic for in-page scripts */
7
+ const FUZZY_MATCH_HELPERS_SCRIPT = `
8
+ const getWords = (str) => {
9
+ return (str || '').toLowerCase()
10
+ .replace(/[^a-z0-9\\u3040-\\u30ff\\u4e00-\\u9faf\\s]/g, '')
11
+ .split(/\\s+/)
12
+ .filter(w => w.length > 1 && !/^(ago|wks?|days?|mins?|hours?|hrs?|secs?|weeks?|months?|years?)$/i.test(w));
13
+ };
14
+ const wordsMatch = (t, p) => {
15
+ const tWords = getWords(t);
16
+ const pWords = getWords(p);
17
+ if (pWords.length === 0) return false;
18
+ return pWords.every(pw => tWords.some(tw => tw === pw));
19
+ };
20
+ `;
6
21
  /** Script to get the state of the new chat button */
7
22
  const GET_NEW_CHAT_BUTTON_SCRIPT = `(() => {
8
23
  const btn = document.querySelector('[data-tooltip-id="new-conversation-tooltip"]');
9
24
  if (!btn) return { found: false };
10
25
  const cursor = window.getComputedStyle(btn).cursor;
26
+ if (typeof btn.scrollIntoView === 'function') {
27
+ btn.scrollIntoView({ block: 'center', inline: 'nearest' });
28
+ }
11
29
  const rect = btn.getBoundingClientRect();
12
30
  return {
13
31
  found: true,
@@ -22,7 +40,7 @@ const GET_NEW_CHAT_BUTTON_SCRIPT = `(() => {
22
40
  * The title element is a div with the text-ellipsis class inside the header.
23
41
  */
24
42
  const GET_CHAT_TITLE_SCRIPT = `(() => {
25
- const panel = document.querySelector('.antigravity-agent-side-panel');
43
+ const panel = document.querySelector('.antigravity-agent-side-panel') || document.body;
26
44
  if (!panel) return { title: '', hasActiveChat: false };
27
45
  const header = panel.querySelector('div[class*="border-b"]');
28
46
  if (!header) return { title: '', hasActiveChat: false };
@@ -40,7 +58,7 @@ const GET_CHAT_TITLE_SCRIPT = `(() => {
40
58
  return { title: title || '(Untitled)', hasActiveChat };
41
59
  })()`;
42
60
  const GET_SESSION_VIEW_STATE_SCRIPT = `(() => {
43
- const panel = document.querySelector('.antigravity-agent-side-panel');
61
+ const panel = document.querySelector('.antigravity-agent-side-panel') || document.body;
44
62
  if (!panel) {
45
63
  return {
46
64
  panelFound: false,
@@ -106,6 +124,9 @@ const GET_SESSION_VIEW_STATE_SCRIPT = `(() => {
106
124
  const FIND_PAST_CONVERSATIONS_BUTTON_SCRIPT = `(() => {
107
125
  const isVisible = (el) => !!el && el instanceof HTMLElement && el.offsetParent !== null;
108
126
  const getRect = (el) => {
127
+ if (typeof el.scrollIntoView === 'function') {
128
+ el.scrollIntoView({ block: 'center', inline: 'nearest' });
129
+ }
109
130
  const rect = el.getBoundingClientRect();
110
131
  return { found: true, x: Math.round(rect.x + rect.width / 2), y: Math.round(rect.y + rect.height / 2) };
111
132
  };
@@ -148,7 +169,8 @@ const SCRAPE_PAST_CONVERSATIONS_SCRIPT = `(() => {
148
169
  // Try the visible QuickInput dialog first, then fall back to the side panel.
149
170
  const quickInputPanels = Array.from(document.querySelectorAll('div[class*="bg-quickinput-background"]'));
150
171
  const panel = quickInputPanels.find((el) => isVisible(el))
151
- || document.querySelector('.antigravity-agent-side-panel');
172
+ || document.querySelector('.antigravity-agent-side-panel')
173
+ || document.body;
152
174
  if (!panel) return null;
153
175
 
154
176
  const items = [];
@@ -214,6 +236,9 @@ const FIND_SHOW_MORE_BUTTON_SCRIPT = `(() => {
214
236
  if (!isVisible(el)) continue;
215
237
  const text = (el.textContent || '').trim();
216
238
  if (/^Show\\s+\\d+\\s+more/i.test(text)) {
239
+ if (typeof el.scrollIntoView === 'function') {
240
+ el.scrollIntoView({ block: 'center', inline: 'nearest' });
241
+ }
217
242
  const rect = el.getBoundingClientRect();
218
243
  return { found: true, x: Math.round(rect.x + rect.width / 2), y: Math.round(rect.y + rect.height / 2) };
219
244
  }
@@ -241,18 +266,25 @@ function buildActivateChatByTitleScript(title) {
241
266
  return true;
242
267
  };
243
268
 
269
+ ${FUZZY_MATCH_HELPERS_SCRIPT}
270
+
244
271
  const nodes = Array.from(panel.querySelectorAll('button, [role="button"], a, li, div, span'))
245
272
  .filter(isVisible);
246
273
 
247
274
  const exact = [];
248
275
  const includes = [];
276
+ const fuzzy = [];
249
277
  for (const node of nodes) {
250
278
  const text = normalize(node.textContent || '');
251
279
  if (!text) continue;
280
+
281
+ const isShort = wanted.length < 5;
252
282
  if (text === wanted) {
253
283
  exact.push({ node, textLength: text.length });
254
- } else if (text.includes(wanted)) {
284
+ } else if (!isShort && text.includes(wanted)) {
255
285
  includes.push({ node, textLength: text.length });
286
+ } else if (wordsMatch(text, wantedRaw)) {
287
+ fuzzy.push({ node, textLength: text.length });
256
288
  }
257
289
  }
258
290
 
@@ -262,10 +294,18 @@ function buildActivateChatByTitleScript(title) {
262
294
  return list[0].node;
263
295
  };
264
296
 
265
- const target = pick(exact) || pick(includes);
297
+ const target = pick(exact) || pick(includes) || pick(fuzzy);
266
298
  if (!target) return { ok: false, error: 'Chat title not found in side panel' };
267
- if (!clickTarget(target)) return { ok: false, error: 'Matched element is not clickable' };
268
- return { ok: true };
299
+ const clickable = target.closest('button, [role="button"], a, li, [data-testid*="conversation"]') || target;
300
+ if (typeof clickable.scrollIntoView === 'function') {
301
+ clickable.scrollIntoView({ block: 'center', inline: 'nearest' });
302
+ }
303
+ const rect = clickable.getBoundingClientRect();
304
+ return {
305
+ ok: true,
306
+ x: Math.round(rect.x + rect.width / 2),
307
+ y: Math.round(rect.y + rect.height / 2)
308
+ };
269
309
  })()`;
270
310
  }
271
311
  /**
@@ -303,14 +343,6 @@ function buildActivateViaPastConversationsScript(title) {
303
343
  ];
304
344
  };
305
345
  const getLabelText = (el) => getLabelParts(el).filter(Boolean).join(' ');
306
- // Verify the option that was actually selected really is the requested
307
- // title. getLabelText concatenates several attributes, so a naive
308
- // whole-string comparison can fail even for a correct pick (e.g. when
309
- // textContent and aria-label duplicate the same value). Instead, check
310
- // each label component individually for an exact (or loose-exact) match.
311
- // If any component matches, report the requested title; otherwise report
312
- // the actual visible text so the acceptance gate rejects a mere
313
- // substring/loose selection.
314
346
  const resolveMatchedTitle = (el) => {
315
347
  const parts = getLabelParts(el);
316
348
  for (const part of parts) {
@@ -327,6 +359,8 @@ function buildActivateViaPastConversationsScript(title) {
327
359
  const clickable = el.closest('button, [role="button"], a, li, [role="option"], [data-testid*="conversation"]');
328
360
  return clickable instanceof HTMLElement ? clickable : (el instanceof HTMLElement ? el : null);
329
361
  };
362
+ ${FUZZY_MATCH_HELPERS_SCRIPT}
363
+
330
364
  const pickBest = (elements, patterns) => {
331
365
  const matched = [];
332
366
  for (const el of elements) {
@@ -338,13 +372,18 @@ function buildActivateViaPastConversationsScript(title) {
338
372
  if (!pattern) continue;
339
373
  const p = normalize(pattern);
340
374
  const pLoose = normalizeLoose(pattern);
375
+ const isShort = p.length < 5;
376
+
341
377
  if (
342
378
  text === p ||
343
- text.includes(p) ||
344
- (pLoose && (textLoose === pLoose || textLoose.includes(pLoose)))
379
+ (pLoose && textLoose === pLoose) ||
380
+ (!isShort && (text.includes(p) || (pLoose && textLoose.includes(pLoose))))
345
381
  ) {
346
382
  matched.push({ el, score: Math.abs(text.length - pattern.length) });
347
383
  break;
384
+ } else if (wordsMatch(text, pattern)) {
385
+ matched.push({ el, score: Math.abs(text.length - pattern.length) + 1000 });
386
+ break;
348
387
  }
349
388
  }
350
389
  }
@@ -366,7 +405,13 @@ function buildActivateViaPastConversationsScript(title) {
366
405
  if (!el) return false;
367
406
  if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
368
407
  el.focus();
369
- el.value = value;
408
+ const proto = el instanceof HTMLInputElement ? window.HTMLInputElement.prototype : window.HTMLTextAreaElement.prototype;
409
+ const nativeSetter = Object.getOwnPropertyDescriptor(proto, 'value')?.set;
410
+ if (nativeSetter) {
411
+ nativeSetter.call(el, value);
412
+ } else {
413
+ el.value = value;
414
+ }
370
415
  el.dispatchEvent(new Event('input', { bubbles: true }));
371
416
  el.dispatchEvent(new Event('change', { bubbles: true }));
372
417
  return true;
@@ -489,29 +534,41 @@ function buildActivateViaPastConversationsScript(title) {
489
534
  clickByPatterns(['select a conversation', 'select conversation', 'conversation'], '[role="button"], button, [aria-haspopup], [data-testid*="conversation"]');
490
535
  await wait(220);
491
536
 
492
- const input = findSearchInput();
493
- if (input) {
494
- setInputValue(input, wantedRaw);
495
- await wait(260);
537
+ // First, try to find it in the default visible list (works for recent chats)
538
+ let selectedOption = pickBest(
539
+ asArray(document.querySelectorAll('[role="option"], li, button, [data-testid*="conversation"]')),
540
+ [wanted, wantedLoose],
541
+ );
542
+
543
+ // If not found, fall back to the search input but use a shorter prefix to avoid strict-match bugs
544
+ if (!selectedOption) {
545
+ const input = findSearchInput();
546
+ if (input) {
547
+ // Use first 2 words to bypass IDE renaming/truncation issues
548
+ const searchPrefix = (wantedRaw || '').split(/\\s+/).slice(0, 2).join(' ');
549
+ setInputValue(input, searchPrefix.length > 3 ? searchPrefix : wantedRaw);
550
+ await wait(350);
551
+ selectedOption = pickBest(
552
+ asArray(document.querySelectorAll('[role="option"], li, button, [data-testid*="conversation"]')),
553
+ [wanted, wantedLoose],
554
+ );
555
+ }
496
556
  }
497
-
498
- // Resolve the exact option element, then click + focus + press Enter
499
- // on that same element so the click target and the verification
500
- // target can never drift apart. Mirror clickByPatterns' scoped
501
- // selector -> broad fallback behaviour.
502
- const optionSelector = '[role="option"], li, button, [data-testid*="conversation"]';
503
- const scopedOptions = asArray(document.querySelectorAll(optionSelector));
504
- const broadOptions = asArray(document.querySelectorAll('button, [role="button"], a, li, div, span'));
505
- const optionSource = scopedOptions.length > 0 ? scopedOptions : broadOptions;
506
- const selectedOption = pickBest(optionSource, [wanted, wantedLoose]);
507
- const selectedClickable = getClickable(selectedOption);
508
- if (!selectedClickable) {
557
+
558
+ if (!selectedOption) {
509
559
  return { ok: false, error: 'Conversation not found in Past Conversations' };
510
560
  }
511
- selectedClickable.click();
512
- selectedClickable.focus();
513
- pressEnter(selectedClickable);
514
- return { ok: true, matchedTitle: resolveMatchedTitle(selectedOption) };
561
+ const clickable = getClickable(selectedOption) || selectedOption;
562
+ if (typeof clickable.scrollIntoView === 'function') {
563
+ clickable.scrollIntoView({ block: 'center', inline: 'nearest' });
564
+ }
565
+ const rect = clickable.getBoundingClientRect();
566
+ return {
567
+ ok: true,
568
+ x: Math.round(rect.x + rect.width / 2),
569
+ y: Math.round(rect.y + rect.height / 2),
570
+ matchedTitle: resolveMatchedTitle(selectedOption)
571
+ };
515
572
  })();
516
573
  })()`;
517
574
  }
@@ -559,7 +616,8 @@ class ChatSessionService {
559
616
  const isVisible = (el) => !!el && el instanceof HTMLElement && el.offsetParent !== null;
560
617
  const quickInputPanels = Array.from(document.querySelectorAll('div[class*="bg-quickinput-background"]'));
561
618
  const panel = quickInputPanels.find((el) => isVisible(el))
562
- || document.querySelector('.antigravity-agent-side-panel');
619
+ || document.querySelector('.antigravity-agent-side-panel')
620
+ || document.body;
563
621
  if (!panel) return false;
564
622
  const containers = Array.from(
565
623
  panel.querySelectorAll('div[class*="overflow-auto"], div[class*="overflow-y-scroll"]')
@@ -893,7 +951,12 @@ class ChatSessionService {
893
951
  if (!clicked) {
894
952
  pastResult = await this.tryActivateByPastConversations(cdpService, title);
895
953
  clicked = pastResult.ok;
896
- usedPastConversations = pastResult.ok;
954
+ // If we attempted past conversations, the panel is open (unless it clicked something)
955
+ usedPastConversations = true;
956
+ // If it failed to click anything, the panel is still open and blocking the UI.
957
+ if (!clicked) {
958
+ await this.closePanelWithEscape(cdpService);
959
+ }
897
960
  }
898
961
  if (clicked) {
899
962
  break;
@@ -964,6 +1027,7 @@ class ChatSessionService {
964
1027
  error: `Past Conversations selected a different chat (expected="${title}", actual="${afterPast.title}")`,
965
1028
  };
966
1029
  }
1030
+ await this.closePanelWithEscape(cdpService);
967
1031
  return {
968
1032
  ok: false,
969
1033
  error: `Activated chat did not match target title (expected="${title}", actual="${after.title}") ` +
@@ -994,6 +1058,9 @@ class ChatSessionService {
994
1058
  });
995
1059
  const value = result?.result?.value;
996
1060
  if (value?.ok) {
1061
+ if (typeof value.x === 'number' && typeof value.y === 'number') {
1062
+ await this.cdpMouseClick(cdpService, value.x, value.y);
1063
+ }
997
1064
  return {
998
1065
  ok: true,
999
1066
  ...(typeof value.matchedTitle === 'string'
@@ -1031,5 +1098,45 @@ class ChatSessionService {
1031
1098
  }
1032
1099
  return { found: false, enabled: false, x: 0, y: 0 };
1033
1100
  }
1101
+ /**
1102
+ * Rename the current chat in the Antigravity UI directly by updating the DOM.
1103
+ * Note: This is a cosmetic change until Antigravity persists a rename.
1104
+ * IDE syncing for chat renaming is strictly "best-effort".
1105
+ */
1106
+ async renameCurrentChatInUI(cdpService, newTitle) {
1107
+ try {
1108
+ const contextId = cdpService.getPrimaryContextId();
1109
+ const RENAME_SCRIPT = `(() => {
1110
+ const panel = document.querySelector('.antigravity-agent-side-panel') || document.body;
1111
+ if (!panel) return { ok: false, error: 'Panel not found' };
1112
+ const header = panel.querySelector('div[class*="border-b"]');
1113
+ const titleEl = header?.querySelector('div[class*="text-ellipsis"]');
1114
+ if (titleEl) {
1115
+ titleEl.textContent = ${JSON.stringify(newTitle)};
1116
+ return { ok: true };
1117
+ }
1118
+
1119
+ const activeRow = Array.from(panel.querySelectorAll('div[class*="focusBackground"]'))
1120
+ .find((el) => el instanceof HTMLElement && el.offsetParent !== null);
1121
+ const activeTitle = activeRow?.querySelector('span.text-sm span, span.text-sm');
1122
+ if (activeTitle) {
1123
+ activeTitle.textContent = ${JSON.stringify(newTitle)};
1124
+ return { ok: true };
1125
+ }
1126
+
1127
+ return { ok: false, error: 'Title element not found' };
1128
+ })()`;
1129
+ const result = await cdpService.call('Runtime.evaluate', {
1130
+ expression: RENAME_SCRIPT,
1131
+ returnByValue: true,
1132
+ ...(contextId !== null ? { contextId } : {}),
1133
+ awaitPromise: true,
1134
+ });
1135
+ return result?.result?.value || { ok: false, error: 'Eval failed' };
1136
+ }
1137
+ catch (e) {
1138
+ return { ok: false, error: e.message };
1139
+ }
1140
+ }
1034
1141
  }
1035
1142
  exports.ChatSessionService = ChatSessionService;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ErrorPopupDetector = void 0;
4
+ const consecutiveEmptyPollGate_1 = require("../utils/consecutiveEmptyPollGate");
4
5
  const logger_1 = require("../utils/logger");
5
6
  const approvalDetector_1 = require("./approvalDetector");
6
7
  /**
@@ -107,8 +108,10 @@ class ErrorPopupDetector {
107
108
  lastDetectedInfo = null;
108
109
  /** Timestamp of last notification (for cooldown-based dedup) */
109
110
  lastNotifiedAt = 0;
110
- /** Cooldown period in ms to suppress duplicate notifications (10s for error popups) */
111
- static COOLDOWN_MS = 10000;
111
+ /** Cooldown period in ms to suppress duplicate notifications */
112
+ static COOLDOWN_MS = 5000;
113
+ /** Gate for empty polls before reset */
114
+ emptyPollGate = new consecutiveEmptyPollGate_1.ConsecutiveEmptyPollGate(3);
112
115
  constructor(options) {
113
116
  this.cdpService = options.cdpService;
114
117
  this.pollIntervalMs = options.pollIntervalMs ?? 3000;
@@ -123,6 +126,7 @@ class ErrorPopupDetector {
123
126
  this.lastDetectedKey = null;
124
127
  this.lastDetectedInfo = null;
125
128
  this.lastNotifiedAt = 0;
129
+ this.emptyPollGate.reset();
126
130
  this.schedulePoll();
127
131
  }
128
132
  /** Stop monitoring. */
@@ -208,28 +212,36 @@ class ErrorPopupDetector {
208
212
  const result = await this.cdpService.call('Runtime.evaluate', callParams);
209
213
  const info = result?.result?.value ?? null;
210
214
  if (info) {
215
+ this.emptyPollGate.recordDetection();
211
216
  // Duplicate prevention: use title + body snippet as key
212
217
  const key = `${info.title}::${info.body.slice(0, 100)}`;
213
218
  const now = Date.now();
214
219
  const withinCooldown = (now - this.lastNotifiedAt) < ErrorPopupDetector.COOLDOWN_MS;
215
- if (key !== this.lastDetectedKey && !withinCooldown) {
220
+ if (key !== this.lastDetectedKey) {
221
+ this.lastNotifiedAt = now;
216
222
  this.lastDetectedKey = key;
217
223
  this.lastDetectedInfo = info;
224
+ this.onErrorPopup(info);
225
+ }
226
+ else if (!withinCooldown) {
218
227
  this.lastNotifiedAt = now;
228
+ this.lastDetectedInfo = info;
219
229
  this.onErrorPopup(info);
220
230
  }
221
- else if (key === this.lastDetectedKey) {
222
- // Same key -- update stored info silently
231
+ else {
232
+ // Same key, within cooldown -- update stored info silently
223
233
  this.lastDetectedInfo = info;
224
234
  }
225
235
  }
226
236
  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();
237
+ if (this.emptyPollGate.recordEmptyPoll()) {
238
+ // Reset when popup disappears (prepare for next detection)
239
+ const wasDetected = this.lastDetectedKey !== null;
240
+ this.lastDetectedKey = null;
241
+ this.lastDetectedInfo = null;
242
+ if (wasDetected && this.onResolved) {
243
+ this.onResolved();
244
+ }
233
245
  }
234
246
  }
235
247
  }