lazy-gravity 0.9.2 → 0.10.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 (38) hide show
  1. package/dist/bot/index.js +232 -41
  2. package/dist/bot/telegramMessageHandler.js +1 -0
  3. package/dist/commands/chatCommandHandler.js +71 -1
  4. package/dist/commands/registerSlashCommands.js +13 -1
  5. package/dist/events/interactionCreateHandler.js +324 -26
  6. package/dist/events/messageCreateHandler.js +53 -4
  7. package/dist/handlers/__tests__/fileChangeButtonAction.test.js +85 -0
  8. package/dist/handlers/approvalButtonAction.js +2 -1
  9. package/dist/handlers/errorPopupButtonAction.js +2 -1
  10. package/dist/handlers/fileChangeButtonAction.js +69 -0
  11. package/dist/handlers/genericActionButtonAction.js +74 -0
  12. package/dist/handlers/planningButtonAction.js +24 -9
  13. package/dist/handlers/questionSelectAction.js +69 -0
  14. package/dist/handlers/questionSkipAction.js +67 -0
  15. package/dist/handlers/runCommandButtonAction.js +2 -1
  16. package/dist/platform/discord/discordResponseRenderer.js +164 -0
  17. package/dist/services/approvalDetector.js +70 -24
  18. package/dist/services/artifactService.js +57 -31
  19. package/dist/services/assistantDomExtractor.js +174 -3
  20. package/dist/services/cdpBridgeManager.js +114 -6
  21. package/dist/services/cdpConnectionPool.js +15 -0
  22. package/dist/services/cdpService.js +21 -7
  23. package/dist/services/chatSessionService.js +133 -33
  24. package/dist/services/errorPopupDetector.js +23 -11
  25. package/dist/services/notificationSender.js +70 -3
  26. package/dist/services/planningDetector.js +69 -25
  27. package/dist/services/promptDispatcher.js +7 -1
  28. package/dist/services/questionDetector.js +376 -0
  29. package/dist/services/responseMonitor.js +28 -2
  30. package/dist/services/runCommandDetector.js +14 -7
  31. package/dist/ui/artifactsUi.js +4 -3
  32. package/dist/utils/consecutiveEmptyPollGate.js +21 -0
  33. package/dist/utils/fileOpenCache.js +33 -0
  34. package/dist/utils/htmlToDiscordMarkdown.js +5 -2
  35. package/dist/utils/pathUtils.js +12 -0
  36. package/dist/utils/projectResolver.js +10 -0
  37. package/dist/utils/questionActionUtils.js +25 -0
  38. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FILE_CHANGE_REJECT_ACTION_PREFIX = exports.FILE_CHANGE_ACCEPT_ACTION_PREFIX = exports.QUESTION_SELECT_ACTION_PREFIX = void 0;
3
4
  exports.getCurrentChatTitle = getCurrentChatTitle;
4
5
  exports.registerApprovalWorkspaceChannel = registerApprovalWorkspaceChannel;
5
6
  exports.registerApprovalSessionChannel = registerApprovalSessionChannel;
@@ -12,6 +13,8 @@ exports.buildErrorPopupCustomId = buildErrorPopupCustomId;
12
13
  exports.parseErrorPopupCustomId = parseErrorPopupCustomId;
13
14
  exports.buildRunCommandCustomId = buildRunCommandCustomId;
14
15
  exports.parseRunCommandCustomId = parseRunCommandCustomId;
16
+ exports.buildFileChangeCustomId = buildFileChangeCustomId;
17
+ exports.parseFileChangeCustomId = parseFileChangeCustomId;
15
18
  exports.initCdpBridge = initCdpBridge;
16
19
  exports.getCurrentCdp = getCurrentCdp;
17
20
  exports.ensureApprovalDetector = ensureApprovalDetector;
@@ -19,6 +22,7 @@ exports.ensurePlanningDetector = ensurePlanningDetector;
19
22
  exports.ensureErrorPopupDetector = ensureErrorPopupDetector;
20
23
  exports.ensureRunCommandDetector = ensureRunCommandDetector;
21
24
  exports.ensureUserMessageDetector = ensureUserMessageDetector;
25
+ exports.ensureQuestionDetector = ensureQuestionDetector;
22
26
  const i18n_1 = require("../utils/i18n");
23
27
  const logger_1 = require("../utils/logger");
24
28
  const notificationSender_1 = require("./notificationSender");
@@ -30,16 +34,21 @@ const planningDetector_1 = require("./planningDetector");
30
34
  const runCommandDetector_1 = require("./runCommandDetector");
31
35
  const quotaService_1 = require("./quotaService");
32
36
  const userMessageDetector_1 = require("./userMessageDetector");
37
+ const questionDetector_1 = require("./questionDetector");
33
38
  const APPROVE_ACTION_PREFIX = 'approve_action';
34
39
  const ALWAYS_ALLOW_ACTION_PREFIX = 'always_allow_action';
35
40
  const DENY_ACTION_PREFIX = 'deny_action';
36
41
  const PLANNING_OPEN_ACTION_PREFIX = 'planning_open_action';
37
42
  const PLANNING_PROCEED_ACTION_PREFIX = 'planning_proceed_action';
43
+ const PLANNING_REJECT_ACTION_PREFIX = 'planning_reject_action';
38
44
  const ERROR_POPUP_DISMISS_ACTION_PREFIX = 'error_popup_dismiss_action';
39
45
  const ERROR_POPUP_COPY_DEBUG_ACTION_PREFIX = 'error_popup_copy_debug_action';
40
46
  const ERROR_POPUP_RETRY_ACTION_PREFIX = 'error_popup_retry_action';
41
47
  const RUN_COMMAND_RUN_ACTION_PREFIX = 'run_command_run_action';
42
48
  const RUN_COMMAND_REJECT_ACTION_PREFIX = 'run_command_reject_action';
49
+ exports.QUESTION_SELECT_ACTION_PREFIX = 'question_select_action';
50
+ exports.FILE_CHANGE_ACCEPT_ACTION_PREFIX = 'ide_file_accept_all';
51
+ exports.FILE_CHANGE_REJECT_ACTION_PREFIX = 'ide_file_reject_all';
43
52
  function normalizeSessionTitle(title) {
44
53
  return title.trim().toLowerCase();
45
54
  }
@@ -137,7 +146,9 @@ function parseApprovalCustomId(customId) {
137
146
  function buildPlanningCustomId(action, projectName, channelId) {
138
147
  const prefix = action === 'open'
139
148
  ? PLANNING_OPEN_ACTION_PREFIX
140
- : PLANNING_PROCEED_ACTION_PREFIX;
149
+ : action === 'proceed'
150
+ ? PLANNING_PROCEED_ACTION_PREFIX
151
+ : PLANNING_REJECT_ACTION_PREFIX;
141
152
  if (channelId && channelId.trim().length > 0) {
142
153
  return `${prefix}:${projectName}:${channelId}`;
143
154
  }
@@ -150,6 +161,9 @@ function parsePlanningCustomId(customId) {
150
161
  if (customId === PLANNING_PROCEED_ACTION_PREFIX) {
151
162
  return { action: 'proceed', projectName: null, channelId: null };
152
163
  }
164
+ if (customId === PLANNING_REJECT_ACTION_PREFIX) {
165
+ return { action: 'reject', projectName: null, channelId: null };
166
+ }
153
167
  if (customId.startsWith(`${PLANNING_OPEN_ACTION_PREFIX}:`)) {
154
168
  const rest = customId.substring(`${PLANNING_OPEN_ACTION_PREFIX}:`.length);
155
169
  const [projectName, channelId] = rest.split(':');
@@ -160,6 +174,11 @@ function parsePlanningCustomId(customId) {
160
174
  const [projectName, channelId] = rest.split(':');
161
175
  return { action: 'proceed', projectName: projectName || null, channelId: channelId || null };
162
176
  }
177
+ if (customId.startsWith(`${PLANNING_REJECT_ACTION_PREFIX}:`)) {
178
+ const rest = customId.substring(`${PLANNING_REJECT_ACTION_PREFIX}:`.length);
179
+ const [projectName, channelId] = rest.split(':');
180
+ return { action: 'reject', projectName: projectName || null, channelId: channelId || null };
181
+ }
163
182
  return null;
164
183
  }
165
184
  function buildErrorPopupCustomId(action, projectName, channelId) {
@@ -228,6 +247,34 @@ function parseRunCommandCustomId(customId) {
228
247
  }
229
248
  return null;
230
249
  }
250
+ function buildFileChangeCustomId(action, projectName, channelId) {
251
+ const prefix = action === 'accept'
252
+ ? exports.FILE_CHANGE_ACCEPT_ACTION_PREFIX
253
+ : exports.FILE_CHANGE_REJECT_ACTION_PREFIX;
254
+ if (channelId && channelId.trim().length > 0) {
255
+ return `${prefix}:${projectName}:${channelId}`;
256
+ }
257
+ return `${prefix}:${projectName}`;
258
+ }
259
+ function parseFileChangeCustomId(customId) {
260
+ if (customId === exports.FILE_CHANGE_ACCEPT_ACTION_PREFIX) {
261
+ return { action: 'accept', projectName: null, channelId: null };
262
+ }
263
+ if (customId === exports.FILE_CHANGE_REJECT_ACTION_PREFIX) {
264
+ return { action: 'reject', projectName: null, channelId: null };
265
+ }
266
+ if (customId.startsWith(`${exports.FILE_CHANGE_ACCEPT_ACTION_PREFIX}:`)) {
267
+ const rest = customId.substring(`${exports.FILE_CHANGE_ACCEPT_ACTION_PREFIX}:`.length);
268
+ const [projectName, channelId] = rest.split(':');
269
+ return { action: 'accept', projectName: projectName || null, channelId: channelId || null };
270
+ }
271
+ if (customId.startsWith(`${exports.FILE_CHANGE_REJECT_ACTION_PREFIX}:`)) {
272
+ const rest = customId.substring(`${exports.FILE_CHANGE_REJECT_ACTION_PREFIX}:`.length);
273
+ const [projectName, channelId] = rest.split(':');
274
+ return { action: 'reject', projectName: projectName || null, channelId: channelId || null };
275
+ }
276
+ return null;
277
+ }
231
278
  /** Initialize the CDP bridge (lazy connection: pool creation only) */
232
279
  function initCdpBridge(autoApproveDefault, accountPorts = {}, accountUserDataDirs = {}, cdpHost = '127.0.0.1') {
233
280
  const pool = new cdpConnectionPool_1.CdpConnectionPool({
@@ -311,17 +358,27 @@ function ensureApprovalDetector(bridge, cdp, projectName, accountName = 'default
311
358
  return;
312
359
  }
313
360
  }
361
+ const extraFields = [
362
+ { name: (0, i18n_1.t)('Allow button'), value: info.approveText || (0, i18n_1.t)('(None)'), inline: true },
363
+ ];
364
+ if (info.alwaysAllowText) {
365
+ extraFields.push({ name: (0, i18n_1.t)('Allow Chat button'), value: info.alwaysAllowText, inline: true });
366
+ }
367
+ extraFields.push({ name: (0, i18n_1.t)('Deny button'), value: info.denyText || (0, i18n_1.t)('(None)'), inline: true });
314
368
  const payload = (0, notificationSender_1.buildApprovalNotification)({
315
369
  title: (0, i18n_1.t)('Approval Required'),
316
370
  description: info.description || (0, i18n_1.t)('Antigravity is requesting approval for an action'),
317
371
  projectName,
318
372
  channelId: targetChannelId,
319
- extraFields: [
320
- { name: (0, i18n_1.t)('Allow button'), value: info.approveText, inline: true },
321
- { name: (0, i18n_1.t)('Allow Chat button'), value: info.alwaysAllowText || (0, i18n_1.t)('In Dropdown'), inline: true },
322
- { name: (0, i18n_1.t)('Deny button'), value: info.denyText || (0, i18n_1.t)('(None)'), inline: true },
323
- ],
373
+ extraFields,
374
+ hasAlwaysAllow: !!info.alwaysAllowText,
375
+ alwaysAllowText: info.alwaysAllowText,
324
376
  });
377
+ if (lastNotification) {
378
+ lastNotification.payload = payload;
379
+ lastNotification.sent.edit(payload).catch(logger_1.logger.error);
380
+ return;
381
+ }
325
382
  const sent = await targetChannel.send(payload).catch((err) => {
326
383
  logger_1.logger.error(err);
327
384
  return null;
@@ -381,7 +438,15 @@ function ensurePlanningDetector(bridge, cdp, projectName, accountName = 'default
381
438
  projectName,
382
439
  channelId: targetChannelId,
383
440
  extraFields,
441
+ hasOpenButton: info.hasOpenButton,
442
+ openText: info.openText,
443
+ proceedText: info.proceedText,
384
444
  });
445
+ if (lastNotification) {
446
+ lastNotification.payload = payload;
447
+ lastNotification.sent.edit(payload).catch(logger_1.logger.error);
448
+ return;
449
+ }
385
450
  const sent = await targetChannel.send(payload).catch((err) => {
386
451
  logger_1.logger.error(err);
387
452
  return null;
@@ -549,3 +614,46 @@ function ensureUserMessageDetector(bridge, cdp, projectName, onUserMessage, acco
549
614
  bridge.pool.registerUserMessageDetector(projectName, detector, accountName);
550
615
  logger_1.logger.debug(`[UserMessageDetector:${projectName}] Started user message detection`);
551
616
  }
617
+ function ensureQuestionDetector(bridge, cdp, projectName, accountName = 'default') {
618
+ const existing = bridge.pool.getQuestionDetector(projectName, accountName);
619
+ if (existing && existing.isActive)
620
+ return;
621
+ let lastNotification = null;
622
+ const detector = new questionDetector_1.QuestionDetector({
623
+ cdpService: cdp,
624
+ pollIntervalMs: 2000,
625
+ onResolved: () => {
626
+ if (!lastNotification)
627
+ return;
628
+ const { sent, payload } = lastNotification;
629
+ lastNotification = null;
630
+ const resolved = (0, notificationSender_1.buildResolvedOverlay)(payload, (0, i18n_1.t)('Question answered'));
631
+ sent.edit(resolved).catch(logger_1.logger.error);
632
+ },
633
+ onQuestionRequired: async (info) => {
634
+ const currentChatTitle = await getCurrentChatTitle(cdp);
635
+ const targetChannel = resolveApprovalChannelForCurrentChat(bridge, projectName, currentChatTitle);
636
+ const targetChannelId = targetChannel ? targetChannel.id : '';
637
+ if (!targetChannel || !targetChannelId)
638
+ return;
639
+ const payload = (0, notificationSender_1.buildQuestionNotification)({
640
+ title: info.title,
641
+ description: info.description,
642
+ projectName,
643
+ channelId: targetChannelId,
644
+ options: info.options,
645
+ });
646
+ const sent = await targetChannel.send(payload).catch((err) => {
647
+ logger_1.logger.error(err);
648
+ return null;
649
+ });
650
+ if (sent) {
651
+ lastNotification = { sent, payload };
652
+ }
653
+ },
654
+ });
655
+ detector.setProjectName(projectName);
656
+ detector.start();
657
+ bridge.pool.registerQuestionDetector(projectName, detector, accountName);
658
+ logger_1.logger.debug(`[QuestionDetector:${projectName}] Started question detection`);
659
+ }
@@ -18,6 +18,7 @@ class CdpConnectionPool {
18
18
  planningDetectors = new Map();
19
19
  runCommandDetectors = new Map();
20
20
  userMessageDetectors = new Map();
21
+ questionDetectors = new Map();
21
22
  connectingPromises = new Map();
22
23
  cdpOptions;
23
24
  constructor(cdpOptions = {}) {
@@ -78,6 +79,8 @@ class CdpConnectionPool {
78
79
  this.approvalDetectors.delete(key);
79
80
  this.errorPopupDetectors.get(key)?.stop();
80
81
  this.errorPopupDetectors.delete(key);
82
+ this.questionDetectors.get(key)?.stop();
83
+ this.questionDetectors.delete(key);
81
84
  this.planningDetectors.get(key)?.stop();
82
85
  this.planningDetectors.delete(key);
83
86
  this.runCommandDetectors.get(key)?.stop();
@@ -117,6 +120,16 @@ class CdpConnectionPool {
117
120
  this.planningDetectors.get(key)?.stop();
118
121
  this.planningDetectors.set(key, detector);
119
122
  }
123
+ getQuestionDetector(projectName, accountName = 'default') {
124
+ const effectiveAccount = this.resolveAccountName(projectName, accountName);
125
+ return this.questionDetectors.get(buildConnectionKey(projectName, effectiveAccount));
126
+ }
127
+ registerQuestionDetector(projectName, detector, accountName = 'default') {
128
+ const effectiveAccount = this.resolveAccountName(projectName, accountName);
129
+ const key = buildConnectionKey(projectName, effectiveAccount);
130
+ this.questionDetectors.get(key)?.stop();
131
+ this.questionDetectors.set(key, detector);
132
+ }
120
133
  getPlanningDetector(projectName, accountName = 'default') {
121
134
  const effectiveAccount = this.resolveAccountName(projectName, accountName);
122
135
  return this.planningDetectors.get(buildConnectionKey(projectName, effectiveAccount));
@@ -182,6 +195,8 @@ class CdpConnectionPool {
182
195
  this.errorPopupDetectors.delete(key);
183
196
  this.planningDetectors.get(key)?.stop();
184
197
  this.planningDetectors.delete(key);
198
+ this.questionDetectors.get(key)?.stop();
199
+ this.questionDetectors.delete(key);
185
200
  this.runCommandDetectors.get(key)?.stop();
186
201
  this.runCommandDetectors.delete(key);
187
202
  this.userMessageDetectors.get(key)?.stop();
@@ -407,6 +407,9 @@ class CdpService extends events_1.EventEmitter {
407
407
  getCurrentWorkspaceName() {
408
408
  return this.currentWorkspaceName;
409
409
  }
410
+ getCurrentWorkspacePath() {
411
+ return this.currentWorkspacePath;
412
+ }
410
413
  /**
411
414
  * Discover and connect to the workbench page for the specified workspace.
412
415
  * Does nothing if already connected to the correct page.
@@ -628,9 +631,11 @@ class CdpService extends events_1.EventEmitter {
628
631
  const detectedValue = value.value;
629
632
  const normalizedDetected = detectedValue.toLowerCase();
630
633
  const normalizedProject = projectName.toLowerCase();
631
- const normalizedWorkspace = workspacePath.toLowerCase();
632
- if (normalizedDetected.includes(normalizedProject) ||
633
- normalizedDetected.includes(normalizedWorkspace)) {
634
+ const normalizedWorkspace = workspacePath.toLowerCase().replace(/\\/g, '/');
635
+ const detectedParts = normalizedDetected.split(',').map((s) => s.trim());
636
+ if (detectedParts.includes(normalizedProject) ||
637
+ normalizedDetected.includes(normalizedWorkspace) ||
638
+ normalizedDetected.replace(/\\/g, '/').includes(normalizedWorkspace)) {
634
639
  this.currentWorkspaceName = projectName;
635
640
  logger_1.logger.debug(`[CdpService] Folder path match success: "${projectName}"`);
636
641
  return true;
@@ -641,11 +646,20 @@ class CdpService extends events_1.EventEmitter {
641
646
  expression: 'window.location.href',
642
647
  returnByValue: true,
643
648
  });
644
- const pageUrl = (urlResult?.result?.value || '').toLowerCase();
645
- const normalizedWorkspaceUri = encodeURIComponent(workspacePath).toLowerCase();
646
- if (pageUrl.includes(normalizedWorkspaceUri) || pageUrl.includes(projectName.toLowerCase())) {
649
+ const pageUrl = decodeURIComponent(urlResult?.result?.value || '').toLowerCase().replace(/\\/g, '/');
650
+ const normalizedWorkspaceUri = workspacePath.toLowerCase().replace(/\\/g, '/');
651
+ if (pageUrl.includes(normalizedWorkspaceUri)) {
652
+ this.currentWorkspaceName = projectName;
653
+ logger_1.logger.debug(`[CdpService] URL parameter match success: "${projectName}" (workspace URI)`);
654
+ return true;
655
+ }
656
+ // If we only have the project name to go on, make sure it's a discrete boundary in the URL path
657
+ // e.g. "folder=.../test" or "workspace=.../test". We prevent false positives like "latest" matching "test".
658
+ const safeProjectName = projectName.toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
659
+ const projectBoundaryRegex = new RegExp(`[=/]${safeProjectName}(?:$|&|\\?)`);
660
+ if (projectBoundaryRegex.test(pageUrl)) {
647
661
  this.currentWorkspaceName = projectName;
648
- logger_1.logger.debug(`[CdpService] URL parameter match success: "${projectName}"`);
662
+ logger_1.logger.debug(`[CdpService] URL parameter match success: "${projectName}" (boundary regex)`);
649
663
  return true;
650
664
  }
651
665
  }
@@ -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,
@@ -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
  };
@@ -214,6 +235,9 @@ const FIND_SHOW_MORE_BUTTON_SCRIPT = `(() => {
214
235
  if (!isVisible(el)) continue;
215
236
  const text = (el.textContent || '').trim();
216
237
  if (/^Show\\s+\\d+\\s+more/i.test(text)) {
238
+ if (typeof el.scrollIntoView === 'function') {
239
+ el.scrollIntoView({ block: 'center', inline: 'nearest' });
240
+ }
217
241
  const rect = el.getBoundingClientRect();
218
242
  return { found: true, x: Math.round(rect.x + rect.width / 2), y: Math.round(rect.y + rect.height / 2) };
219
243
  }
@@ -241,11 +265,14 @@ function buildActivateChatByTitleScript(title) {
241
265
  return true;
242
266
  };
243
267
 
268
+ ${FUZZY_MATCH_HELPERS_SCRIPT}
269
+
244
270
  const nodes = Array.from(panel.querySelectorAll('button, [role="button"], a, li, div, span'))
245
271
  .filter(isVisible);
246
272
 
247
273
  const exact = [];
248
274
  const includes = [];
275
+ const fuzzy = [];
249
276
  for (const node of nodes) {
250
277
  const text = normalize(node.textContent || '');
251
278
  if (!text) continue;
@@ -253,6 +280,8 @@ function buildActivateChatByTitleScript(title) {
253
280
  exact.push({ node, textLength: text.length });
254
281
  } else if (text.includes(wanted)) {
255
282
  includes.push({ node, textLength: text.length });
283
+ } else if (wordsMatch(text, wantedRaw)) {
284
+ fuzzy.push({ node, textLength: text.length });
256
285
  }
257
286
  }
258
287
 
@@ -262,10 +291,18 @@ function buildActivateChatByTitleScript(title) {
262
291
  return list[0].node;
263
292
  };
264
293
 
265
- const target = pick(exact) || pick(includes);
294
+ const target = pick(exact) || pick(includes) || pick(fuzzy);
266
295
  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 };
296
+ const clickable = target.closest('button, [role="button"], a, li, [data-testid*="conversation"]') || target;
297
+ if (typeof clickable.scrollIntoView === 'function') {
298
+ clickable.scrollIntoView({ block: 'center', inline: 'nearest' });
299
+ }
300
+ const rect = clickable.getBoundingClientRect();
301
+ return {
302
+ ok: true,
303
+ x: Math.round(rect.x + rect.width / 2),
304
+ y: Math.round(rect.y + rect.height / 2)
305
+ };
269
306
  })()`;
270
307
  }
271
308
  /**
@@ -303,14 +340,6 @@ function buildActivateViaPastConversationsScript(title) {
303
340
  ];
304
341
  };
305
342
  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
343
  const resolveMatchedTitle = (el) => {
315
344
  const parts = getLabelParts(el);
316
345
  for (const part of parts) {
@@ -327,6 +356,8 @@ function buildActivateViaPastConversationsScript(title) {
327
356
  const clickable = el.closest('button, [role="button"], a, li, [role="option"], [data-testid*="conversation"]');
328
357
  return clickable instanceof HTMLElement ? clickable : (el instanceof HTMLElement ? el : null);
329
358
  };
359
+ ${FUZZY_MATCH_HELPERS_SCRIPT}
360
+
330
361
  const pickBest = (elements, patterns) => {
331
362
  const matched = [];
332
363
  for (const el of elements) {
@@ -345,6 +376,9 @@ function buildActivateViaPastConversationsScript(title) {
345
376
  ) {
346
377
  matched.push({ el, score: Math.abs(text.length - pattern.length) });
347
378
  break;
379
+ } else if (wordsMatch(text, pattern)) {
380
+ matched.push({ el, score: Math.abs(text.length - pattern.length) + 1000 });
381
+ break;
348
382
  }
349
383
  }
350
384
  }
@@ -366,7 +400,13 @@ function buildActivateViaPastConversationsScript(title) {
366
400
  if (!el) return false;
367
401
  if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
368
402
  el.focus();
369
- el.value = value;
403
+ const proto = el instanceof HTMLInputElement ? window.HTMLInputElement.prototype : window.HTMLTextAreaElement.prototype;
404
+ const nativeSetter = Object.getOwnPropertyDescriptor(proto, 'value')?.set;
405
+ if (nativeSetter) {
406
+ nativeSetter.call(el, value);
407
+ } else {
408
+ el.value = value;
409
+ }
370
410
  el.dispatchEvent(new Event('input', { bubbles: true }));
371
411
  el.dispatchEvent(new Event('change', { bubbles: true }));
372
412
  return true;
@@ -489,29 +529,41 @@ function buildActivateViaPastConversationsScript(title) {
489
529
  clickByPatterns(['select a conversation', 'select conversation', 'conversation'], '[role="button"], button, [aria-haspopup], [data-testid*="conversation"]');
490
530
  await wait(220);
491
531
 
492
- const input = findSearchInput();
493
- if (input) {
494
- setInputValue(input, wantedRaw);
495
- await wait(260);
532
+ // First, try to find it in the default visible list (works for recent chats)
533
+ let selectedOption = pickBest(
534
+ asArray(document.querySelectorAll('[role="option"], li, button, [data-testid*="conversation"]')),
535
+ [wanted, wantedLoose],
536
+ );
537
+
538
+ // If not found, fall back to the search input but use a shorter prefix to avoid strict-match bugs
539
+ if (!selectedOption) {
540
+ const input = findSearchInput();
541
+ if (input) {
542
+ // Use first 2 words to bypass IDE renaming/truncation issues
543
+ const searchPrefix = (wantedRaw || '').split(/\\s+/).slice(0, 2).join(' ');
544
+ setInputValue(input, searchPrefix.length > 3 ? searchPrefix : wantedRaw);
545
+ await wait(350);
546
+ selectedOption = pickBest(
547
+ asArray(document.querySelectorAll('[role="option"], li, button, [data-testid*="conversation"]')),
548
+ [wanted, wantedLoose],
549
+ );
550
+ }
496
551
  }
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) {
552
+
553
+ if (!selectedOption) {
509
554
  return { ok: false, error: 'Conversation not found in Past Conversations' };
510
555
  }
511
- selectedClickable.click();
512
- selectedClickable.focus();
513
- pressEnter(selectedClickable);
514
- return { ok: true, matchedTitle: resolveMatchedTitle(selectedOption) };
556
+ const clickable = getClickable(selectedOption) || selectedOption;
557
+ if (typeof clickable.scrollIntoView === 'function') {
558
+ clickable.scrollIntoView({ block: 'center', inline: 'nearest' });
559
+ }
560
+ const rect = clickable.getBoundingClientRect();
561
+ return {
562
+ ok: true,
563
+ x: Math.round(rect.x + rect.width / 2),
564
+ y: Math.round(rect.y + rect.height / 2),
565
+ matchedTitle: resolveMatchedTitle(selectedOption)
566
+ };
515
567
  })();
516
568
  })()`;
517
569
  }
@@ -893,7 +945,12 @@ class ChatSessionService {
893
945
  if (!clicked) {
894
946
  pastResult = await this.tryActivateByPastConversations(cdpService, title);
895
947
  clicked = pastResult.ok;
896
- usedPastConversations = pastResult.ok;
948
+ // If we attempted past conversations, the panel is open (unless it clicked something)
949
+ usedPastConversations = true;
950
+ // If it failed to click anything, the panel is still open and blocking the UI.
951
+ if (!clicked) {
952
+ await this.closePanelWithEscape(cdpService);
953
+ }
897
954
  }
898
955
  if (clicked) {
899
956
  break;
@@ -964,6 +1021,7 @@ class ChatSessionService {
964
1021
  error: `Past Conversations selected a different chat (expected="${title}", actual="${afterPast.title}")`,
965
1022
  };
966
1023
  }
1024
+ await this.closePanelWithEscape(cdpService);
967
1025
  return {
968
1026
  ok: false,
969
1027
  error: `Activated chat did not match target title (expected="${title}", actual="${after.title}") ` +
@@ -994,6 +1052,9 @@ class ChatSessionService {
994
1052
  });
995
1053
  const value = result?.result?.value;
996
1054
  if (value?.ok) {
1055
+ if (typeof value.x === 'number' && typeof value.y === 'number') {
1056
+ await this.cdpMouseClick(cdpService, value.x, value.y);
1057
+ }
997
1058
  return {
998
1059
  ok: true,
999
1060
  ...(typeof value.matchedTitle === 'string'
@@ -1031,5 +1092,44 @@ class ChatSessionService {
1031
1092
  }
1032
1093
  return { found: false, enabled: false, x: 0, y: 0 };
1033
1094
  }
1095
+ /**
1096
+ * Rename the current chat in the Antigravity UI directly by updating the DOM.
1097
+ * Note: This is a cosmetic change until Antigravity persists a rename.
1098
+ */
1099
+ async renameCurrentChatInUI(cdpService, newTitle) {
1100
+ try {
1101
+ const contextId = cdpService.getPrimaryContextId();
1102
+ const RENAME_SCRIPT = `(() => {
1103
+ const panel = document.querySelector('.antigravity-agent-side-panel');
1104
+ if (!panel) return { ok: false, error: 'Panel not found' };
1105
+ const header = panel.querySelector('div[class*="border-b"]');
1106
+ const titleEl = header?.querySelector('div[class*="text-ellipsis"]');
1107
+ if (titleEl) {
1108
+ titleEl.textContent = ${JSON.stringify(newTitle)};
1109
+ return { ok: true };
1110
+ }
1111
+
1112
+ const activeRow = Array.from(panel.querySelectorAll('div[class*="focusBackground"]'))
1113
+ .find((el) => el instanceof HTMLElement && el.offsetParent !== null);
1114
+ const activeTitle = activeRow?.querySelector('span.text-sm span, span.text-sm');
1115
+ if (activeTitle) {
1116
+ activeTitle.textContent = ${JSON.stringify(newTitle)};
1117
+ return { ok: true };
1118
+ }
1119
+
1120
+ return { ok: false, error: 'Title element not found' };
1121
+ })()`;
1122
+ const result = await cdpService.call('Runtime.evaluate', {
1123
+ expression: RENAME_SCRIPT,
1124
+ returnByValue: true,
1125
+ ...(contextId !== null ? { contextId } : {}),
1126
+ awaitPromise: true,
1127
+ });
1128
+ return result?.result?.value || { ok: false, error: 'Eval failed' };
1129
+ }
1130
+ catch (e) {
1131
+ return { ok: false, error: e.message };
1132
+ }
1133
+ }
1034
1134
  }
1035
1135
  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
  }