lazy-gravity 0.9.1 → 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 (39) hide show
  1. package/dist/bot/index.js +233 -41
  2. package/dist/bot/telegramJoinCommand.js +9 -0
  3. package/dist/bot/telegramMessageHandler.js +7 -0
  4. package/dist/commands/chatCommandHandler.js +71 -1
  5. package/dist/commands/registerSlashCommands.js +13 -1
  6. package/dist/events/interactionCreateHandler.js +324 -26
  7. package/dist/events/messageCreateHandler.js +53 -4
  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 +69 -0
  12. package/dist/handlers/genericActionButtonAction.js +74 -0
  13. package/dist/handlers/planningButtonAction.js +24 -9
  14. package/dist/handlers/questionSelectAction.js +69 -0
  15. package/dist/handlers/questionSkipAction.js +67 -0
  16. package/dist/handlers/runCommandButtonAction.js +2 -1
  17. package/dist/platform/discord/discordResponseRenderer.js +164 -0
  18. package/dist/services/approvalDetector.js +70 -24
  19. package/dist/services/artifactService.js +57 -31
  20. package/dist/services/assistantDomExtractor.js +174 -3
  21. package/dist/services/cdpBridgeManager.js +114 -6
  22. package/dist/services/cdpConnectionPool.js +15 -0
  23. package/dist/services/cdpService.js +21 -7
  24. package/dist/services/chatSessionService.js +148 -27
  25. package/dist/services/errorPopupDetector.js +23 -11
  26. package/dist/services/notificationSender.js +70 -3
  27. package/dist/services/planningDetector.js +69 -25
  28. package/dist/services/promptDispatcher.js +7 -1
  29. package/dist/services/questionDetector.js +376 -0
  30. package/dist/services/responseMonitor.js +28 -2
  31. package/dist/services/runCommandDetector.js +14 -7
  32. package/dist/ui/artifactsUi.js +4 -3
  33. package/dist/utils/consecutiveEmptyPollGate.js +21 -0
  34. package/dist/utils/fileOpenCache.js +33 -0
  35. package/dist/utils/htmlToDiscordMarkdown.js +5 -2
  36. package/dist/utils/pathUtils.js +12 -0
  37. package/dist/utils/projectResolver.js +10 -0
  38. package/dist/utils/questionActionUtils.js +25 -0
  39. package/package.json +2 -2
@@ -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
  }
@@ -1,12 +1,31 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ChatSessionService = void 0;
4
+ exports.buildActivateViaPastConversationsScript = buildActivateViaPastConversationsScript;
4
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
+ `;
5
21
  /** Script to get the state of the new chat button */
6
22
  const GET_NEW_CHAT_BUTTON_SCRIPT = `(() => {
7
23
  const btn = document.querySelector('[data-tooltip-id="new-conversation-tooltip"]');
8
24
  if (!btn) return { found: false };
9
25
  const cursor = window.getComputedStyle(btn).cursor;
26
+ if (typeof btn.scrollIntoView === 'function') {
27
+ btn.scrollIntoView({ block: 'center', inline: 'nearest' });
28
+ }
10
29
  const rect = btn.getBoundingClientRect();
11
30
  return {
12
31
  found: true,
@@ -105,6 +124,9 @@ const GET_SESSION_VIEW_STATE_SCRIPT = `(() => {
105
124
  const FIND_PAST_CONVERSATIONS_BUTTON_SCRIPT = `(() => {
106
125
  const isVisible = (el) => !!el && el instanceof HTMLElement && el.offsetParent !== null;
107
126
  const getRect = (el) => {
127
+ if (typeof el.scrollIntoView === 'function') {
128
+ el.scrollIntoView({ block: 'center', inline: 'nearest' });
129
+ }
108
130
  const rect = el.getBoundingClientRect();
109
131
  return { found: true, x: Math.round(rect.x + rect.width / 2), y: Math.round(rect.y + rect.height / 2) };
110
132
  };
@@ -213,6 +235,9 @@ const FIND_SHOW_MORE_BUTTON_SCRIPT = `(() => {
213
235
  if (!isVisible(el)) continue;
214
236
  const text = (el.textContent || '').trim();
215
237
  if (/^Show\\s+\\d+\\s+more/i.test(text)) {
238
+ if (typeof el.scrollIntoView === 'function') {
239
+ el.scrollIntoView({ block: 'center', inline: 'nearest' });
240
+ }
216
241
  const rect = el.getBoundingClientRect();
217
242
  return { found: true, x: Math.round(rect.x + rect.width / 2), y: Math.round(rect.y + rect.height / 2) };
218
243
  }
@@ -240,11 +265,14 @@ function buildActivateChatByTitleScript(title) {
240
265
  return true;
241
266
  };
242
267
 
268
+ ${FUZZY_MATCH_HELPERS_SCRIPT}
269
+
243
270
  const nodes = Array.from(panel.querySelectorAll('button, [role="button"], a, li, div, span'))
244
271
  .filter(isVisible);
245
272
 
246
273
  const exact = [];
247
274
  const includes = [];
275
+ const fuzzy = [];
248
276
  for (const node of nodes) {
249
277
  const text = normalize(node.textContent || '');
250
278
  if (!text) continue;
@@ -252,6 +280,8 @@ function buildActivateChatByTitleScript(title) {
252
280
  exact.push({ node, textLength: text.length });
253
281
  } else if (text.includes(wanted)) {
254
282
  includes.push({ node, textLength: text.length });
283
+ } else if (wordsMatch(text, wantedRaw)) {
284
+ fuzzy.push({ node, textLength: text.length });
255
285
  }
256
286
  }
257
287
 
@@ -261,10 +291,18 @@ function buildActivateChatByTitleScript(title) {
261
291
  return list[0].node;
262
292
  };
263
293
 
264
- const target = pick(exact) || pick(includes);
294
+ const target = pick(exact) || pick(includes) || pick(fuzzy);
265
295
  if (!target) return { ok: false, error: 'Chat title not found in side panel' };
266
- if (!clickTarget(target)) return { ok: false, error: 'Matched element is not clickable' };
267
- 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
+ };
268
306
  })()`;
269
307
  }
270
308
  /**
@@ -290,9 +328,9 @@ function buildActivateViaPastConversationsScript(title) {
290
328
 
291
329
  const isVisible = (el) => !!el && el instanceof HTMLElement && el.offsetParent !== null;
292
330
  const asArray = (nodeList) => Array.from(nodeList || []);
293
- const getLabelText = (el) => {
294
- if (!el || !(el instanceof Element)) return '';
295
- const parts = [
331
+ const getLabelParts = (el) => {
332
+ if (!el || !(el instanceof Element)) return [];
333
+ return [
296
334
  el.textContent || '',
297
335
  el.getAttribute('aria-label') || '',
298
336
  el.getAttribute('title') || '',
@@ -300,13 +338,26 @@ function buildActivateViaPastConversationsScript(title) {
300
338
  el.getAttribute('data-tooltip-content') || '',
301
339
  el.getAttribute('data-testid') || '',
302
340
  ];
303
- return parts.filter(Boolean).join(' ');
341
+ };
342
+ const getLabelText = (el) => getLabelParts(el).filter(Boolean).join(' ');
343
+ const resolveMatchedTitle = (el) => {
344
+ const parts = getLabelParts(el);
345
+ for (const part of parts) {
346
+ if (!part) continue;
347
+ if (normalize(part) === wanted || (wantedLoose && normalizeLoose(part) === wantedLoose)) {
348
+ return wantedRaw;
349
+ }
350
+ }
351
+ const visible = (parts[0] || '').trim();
352
+ return visible || null;
304
353
  };
305
354
  const getClickable = (el) => {
306
355
  if (!el || !(el instanceof Element)) return null;
307
356
  const clickable = el.closest('button, [role="button"], a, li, [role="option"], [data-testid*="conversation"]');
308
357
  return clickable instanceof HTMLElement ? clickable : (el instanceof HTMLElement ? el : null);
309
358
  };
359
+ ${FUZZY_MATCH_HELPERS_SCRIPT}
360
+
310
361
  const pickBest = (elements, patterns) => {
311
362
  const matched = [];
312
363
  for (const el of elements) {
@@ -325,6 +376,9 @@ function buildActivateViaPastConversationsScript(title) {
325
376
  ) {
326
377
  matched.push({ el, score: Math.abs(text.length - pattern.length) });
327
378
  break;
379
+ } else if (wordsMatch(text, pattern)) {
380
+ matched.push({ el, score: Math.abs(text.length - pattern.length) + 1000 });
381
+ break;
328
382
  }
329
383
  }
330
384
  }
@@ -346,7 +400,13 @@ function buildActivateViaPastConversationsScript(title) {
346
400
  if (!el) return false;
347
401
  if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
348
402
  el.focus();
349
- 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
+ }
350
410
  el.dispatchEvent(new Event('input', { bubbles: true }));
351
411
  el.dispatchEvent(new Event('change', { bubbles: true }));
352
412
  return true;
@@ -469,28 +529,41 @@ function buildActivateViaPastConversationsScript(title) {
469
529
  clickByPatterns(['select a conversation', 'select conversation', 'conversation'], '[role="button"], button, [aria-haspopup], [data-testid*="conversation"]');
470
530
  await wait(220);
471
531
 
472
- const input = findSearchInput();
473
- if (input) {
474
- setInputValue(input, wantedRaw);
475
- await wait(260);
476
- }
477
-
478
- let selected = clickByPatterns([wanted, wantedLoose], '[role="option"], li, button, [data-testid*="conversation"]');
479
- if (selected) {
480
- const selectedOption = pickBest(
481
- asArray(document.querySelectorAll('[role="option"], li, button, [data-testid*="conversation"]')),
482
- [wanted, wantedLoose],
483
- );
484
- const selectedClickable = getClickable(selectedOption);
485
- if (selectedClickable) {
486
- selectedClickable.focus();
487
- pressEnter(selectedClickable);
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
+ );
488
550
  }
489
551
  }
490
- if (!selected) {
552
+
553
+ if (!selectedOption) {
491
554
  return { ok: false, error: 'Conversation not found in Past Conversations' };
492
555
  }
493
- return { ok: true, matchedTitle: wantedRaw };
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
+ };
494
567
  })();
495
568
  })()`;
496
569
  }
@@ -872,7 +945,12 @@ class ChatSessionService {
872
945
  if (!clicked) {
873
946
  pastResult = await this.tryActivateByPastConversations(cdpService, title);
874
947
  clicked = pastResult.ok;
875
- 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
+ }
876
954
  }
877
955
  if (clicked) {
878
956
  break;
@@ -943,6 +1021,7 @@ class ChatSessionService {
943
1021
  error: `Past Conversations selected a different chat (expected="${title}", actual="${afterPast.title}")`,
944
1022
  };
945
1023
  }
1024
+ await this.closePanelWithEscape(cdpService);
946
1025
  return {
947
1026
  ok: false,
948
1027
  error: `Activated chat did not match target title (expected="${title}", actual="${after.title}") ` +
@@ -973,6 +1052,9 @@ class ChatSessionService {
973
1052
  });
974
1053
  const value = result?.result?.value;
975
1054
  if (value?.ok) {
1055
+ if (typeof value.x === 'number' && typeof value.y === 'number') {
1056
+ await this.cdpMouseClick(cdpService, value.x, value.y);
1057
+ }
976
1058
  return {
977
1059
  ok: true,
978
1060
  ...(typeof value.matchedTitle === 'string'
@@ -1010,5 +1092,44 @@ class ChatSessionService {
1010
1092
  }
1011
1093
  return { found: false, enabled: false, x: 0, y: 0 };
1012
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
+ }
1013
1134
  }
1014
1135
  exports.ChatSessionService = ChatSessionService;