lazy-gravity 0.2.0 → 0.4.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 (66) hide show
  1. package/README.md +77 -15
  2. package/dist/bin/cli.js +0 -0
  3. package/dist/bin/commands/doctor.js +19 -2
  4. package/dist/bin/commands/open.js +1 -1
  5. package/dist/bin/commands/setup.js +286 -70
  6. package/dist/bot/eventRouter.js +70 -0
  7. package/dist/bot/index.js +355 -147
  8. package/dist/bot/telegramCommands.js +478 -0
  9. package/dist/bot/telegramMessageHandler.js +308 -0
  10. package/dist/bot/telegramProjectCommand.js +137 -0
  11. package/dist/bot/workspaceQueue.js +61 -0
  12. package/dist/commands/joinCommandHandler.js +4 -1
  13. package/dist/database/telegramBindingRepository.js +97 -0
  14. package/dist/database/userPreferenceRepository.js +46 -1
  15. package/dist/events/interactionCreateHandler.js +36 -0
  16. package/dist/events/messageCreateHandler.js +11 -7
  17. package/dist/handlers/approvalButtonAction.js +99 -0
  18. package/dist/handlers/autoAcceptButtonAction.js +43 -0
  19. package/dist/handlers/buttonHandler.js +55 -0
  20. package/dist/handlers/commandHandler.js +44 -0
  21. package/dist/handlers/errorPopupButtonAction.js +137 -0
  22. package/dist/handlers/messageHandler.js +70 -0
  23. package/dist/handlers/modeSelectAction.js +63 -0
  24. package/dist/handlers/modelButtonAction.js +102 -0
  25. package/dist/handlers/planningButtonAction.js +118 -0
  26. package/dist/handlers/selectHandler.js +41 -0
  27. package/dist/handlers/templateButtonAction.js +54 -0
  28. package/dist/platform/adapter.js +8 -0
  29. package/dist/platform/discord/discordAdapter.js +99 -0
  30. package/dist/platform/discord/index.js +15 -0
  31. package/dist/platform/discord/wrappers.js +331 -0
  32. package/dist/platform/index.js +18 -0
  33. package/dist/platform/richContentBuilder.js +76 -0
  34. package/dist/platform/telegram/index.js +16 -0
  35. package/dist/platform/telegram/telegramAdapter.js +195 -0
  36. package/dist/platform/telegram/telegramFormatter.js +134 -0
  37. package/dist/platform/telegram/wrappers.js +333 -0
  38. package/dist/platform/types.js +28 -0
  39. package/dist/services/approvalDetector.js +15 -2
  40. package/dist/services/cdpBridgeManager.js +91 -146
  41. package/dist/services/cdpService.js +88 -2
  42. package/dist/services/chatSessionService.js +50 -10
  43. package/dist/services/defaultModelApplicator.js +54 -0
  44. package/dist/services/modeService.js +16 -1
  45. package/dist/services/modelService.js +57 -16
  46. package/dist/services/notificationSender.js +149 -0
  47. package/dist/services/responseMonitor.js +1 -2
  48. package/dist/services/screenshotService.js +2 -2
  49. package/dist/ui/autoAcceptUi.js +37 -0
  50. package/dist/ui/modeUi.js +38 -1
  51. package/dist/ui/modelsUi.js +96 -0
  52. package/dist/ui/outputUi.js +32 -0
  53. package/dist/ui/projectListUi.js +55 -0
  54. package/dist/ui/screenshotUi.js +26 -0
  55. package/dist/ui/sessionPickerUi.js +35 -1
  56. package/dist/ui/templateUi.js +41 -0
  57. package/dist/utils/configLoader.js +63 -12
  58. package/dist/utils/lockfile.js +5 -5
  59. package/dist/utils/logger.js +7 -0
  60. package/dist/utils/telegramImageHandler.js +127 -0
  61. package/package.json +6 -3
  62. package/dist/commands/joinDetachCommandHandler.js +0 -285
  63. package/dist/services/retryStore.js +0 -46
  64. package/dist/ui/buttonUtils.js +0 -33
  65. package/dist/utils/antigravityPaths.js +0 -94
  66. package/dist/utils/logFileTransport.js +0 -147
@@ -16,10 +16,9 @@ exports.ensureApprovalDetector = ensureApprovalDetector;
16
16
  exports.ensurePlanningDetector = ensurePlanningDetector;
17
17
  exports.ensureErrorPopupDetector = ensureErrorPopupDetector;
18
18
  exports.ensureUserMessageDetector = ensureUserMessageDetector;
19
- const discord_js_1 = require("discord.js");
20
19
  const i18n_1 = require("../utils/i18n");
21
20
  const logger_1 = require("../utils/logger");
22
- const discordButtonUtils_1 = require("../utils/discordButtonUtils");
21
+ const notificationSender_1 = require("./notificationSender");
23
22
  const approvalDetector_1 = require("./approvalDetector");
24
23
  const autoAcceptService_1 = require("./autoAcceptService");
25
24
  const cdpConnectionPool_1 = require("./cdpConnectionPool");
@@ -230,89 +229,66 @@ function getCurrentCdp(bridge) {
230
229
  * Helper to start an approval detector for each workspace.
231
230
  * Does nothing if a detector for the same workspace is already running.
232
231
  */
233
- function ensureApprovalDetector(bridge, cdp, projectName, client) {
232
+ function ensureApprovalDetector(bridge, cdp, projectName) {
234
233
  const existing = bridge.pool.getApprovalDetector(projectName);
235
234
  if (existing && existing.isActive())
236
235
  return;
237
- // Track the most recent button message for auto-disable on resolve.
238
- // Only the latest message is tracked; if a new detection fires before the previous
239
- // is resolved, the older message reference is overwritten. This is acceptable because
236
+ // Track the most recent notification for auto-disable on resolve.
237
+ // Only the latest is tracked; if a new detection fires before the previous
238
+ // is resolved, the older reference is overwritten. This is acceptable because
240
239
  // the detector's lastDetectedKey deduplication prevents rapid successive notifications.
241
- let lastButtonMessage = null;
240
+ let lastNotification = null;
242
241
  const detector = new approvalDetector_1.ApprovalDetector({
243
242
  cdpService: cdp,
244
243
  pollIntervalMs: 2000,
245
244
  onResolved: () => {
246
- if (!lastButtonMessage)
245
+ if (!lastNotification)
247
246
  return;
248
- const msg = lastButtonMessage;
249
- lastButtonMessage = null;
250
- const originalEmbed = msg.embeds[0];
251
- const updatedEmbed = originalEmbed
252
- ? discord_js_1.EmbedBuilder.from(originalEmbed)
253
- : new discord_js_1.EmbedBuilder().setTitle((0, i18n_1.t)('Approval Required'));
254
- updatedEmbed
255
- .setColor(0x95A5A6)
256
- .addFields({ name: (0, i18n_1.t)('Status'), value: (0, i18n_1.t)('Resolved in Antigravity'), inline: false });
257
- msg.edit({
258
- embeds: [updatedEmbed],
259
- components: (0, discordButtonUtils_1.disableAllButtons)(msg.components),
260
- }).catch(logger_1.logger.error);
247
+ const { sent, payload } = lastNotification;
248
+ lastNotification = null;
249
+ const resolved = (0, notificationSender_1.buildResolvedOverlay)(payload, (0, i18n_1.t)('Resolved in Antigravity'));
250
+ sent.edit(resolved).catch(logger_1.logger.error);
261
251
  },
262
252
  onApprovalRequired: async (info) => {
263
253
  logger_1.logger.debug(`[ApprovalDetector:${projectName}] Approval button detected (allow="${info.approveText}", deny="${info.denyText}")`);
264
254
  const currentChatTitle = await getCurrentChatTitle(cdp);
265
255
  const targetChannel = resolveApprovalChannelForCurrentChat(bridge, projectName, currentChatTitle);
266
- const targetChannelId = targetChannel && 'id' in targetChannel ? String(targetChannel.id) : '';
267
- if (!targetChannel || !targetChannelId || !('send' in targetChannel)) {
268
- logger_1.logger.warn(`[ApprovalDetector:${projectName}] Skipped approval notification because chat is not linked to a Discord session` +
256
+ const targetChannelId = targetChannel ? targetChannel.id : '';
257
+ if (!targetChannel || !targetChannelId) {
258
+ logger_1.logger.warn(`[ApprovalDetector:${projectName}] Skipped approval notification because chat is not linked to a session` +
269
259
  `${currentChatTitle ? ` (title="${currentChatTitle}")` : ''}`);
270
260
  return;
271
261
  }
272
262
  if (bridge.autoAccept.isEnabled()) {
273
263
  const accepted = await detector.alwaysAllowButton() || await detector.approveButton();
274
- const autoEmbed = new discord_js_1.EmbedBuilder()
275
- .setTitle(accepted ? (0, i18n_1.t)('Auto-approved') : (0, i18n_1.t)('Auto-approve failed'))
276
- .setDescription(accepted ? (0, i18n_1.t)('An action was automatically approved.') : (0, i18n_1.t)('Auto-approve attempted but failed. Manual approval required.'))
277
- .setColor(accepted ? 0x2ECC71 : 0xF39C12)
278
- .addFields({ name: (0, i18n_1.t)('Auto-approve mode'), value: (0, i18n_1.t)('ON'), inline: true }, { name: (0, i18n_1.t)('Workspace'), value: projectName, inline: true }, { name: (0, i18n_1.t)('Result'), value: accepted ? (0, i18n_1.t)('Executed Always Allow/Allow') : (0, i18n_1.t)('Manual approval required'), inline: true });
279
- if (info.description) {
280
- autoEmbed.addFields({ name: (0, i18n_1.t)('Action Detail'), value: info.description.substring(0, 1024), inline: false });
281
- }
282
- if (info.approveText) {
283
- autoEmbed.addFields({ name: (0, i18n_1.t)('Approved via'), value: info.approveText, inline: true });
284
- }
285
- autoEmbed.setTimestamp();
286
- await targetChannel.send({ embeds: [autoEmbed] }).catch(logger_1.logger.error);
264
+ const autoPayload = (0, notificationSender_1.buildAutoApprovedNotification)({
265
+ accepted,
266
+ projectName,
267
+ description: info.description ?? undefined,
268
+ approveText: info.approveText ?? undefined,
269
+ });
270
+ await targetChannel.send(autoPayload).catch(logger_1.logger.error);
287
271
  if (accepted) {
288
272
  return;
289
273
  }
290
274
  }
291
- const embed = new discord_js_1.EmbedBuilder()
292
- .setTitle((0, i18n_1.t)('Approval Required'))
293
- .setDescription(info.description || (0, i18n_1.t)('Antigravity is requesting approval for an action'))
294
- .setColor(0xFFA500)
295
- .addFields({ name: (0, i18n_1.t)('Allow button'), value: info.approveText, inline: true }, { name: (0, i18n_1.t)('Allow Chat button'), value: info.alwaysAllowText || (0, i18n_1.t)('In Dropdown'), inline: true }, { name: (0, i18n_1.t)('Deny button'), value: info.denyText || (0, i18n_1.t)('(None)'), inline: true }, { name: (0, i18n_1.t)('Workspace'), value: projectName, inline: true })
296
- .setTimestamp();
297
- const approveBtn = new discord_js_1.ButtonBuilder()
298
- .setCustomId(buildApprovalCustomId('approve', projectName, targetChannelId))
299
- .setLabel((0, i18n_1.t)('Allow'))
300
- .setStyle(discord_js_1.ButtonStyle.Success);
301
- const alwaysAllowBtn = new discord_js_1.ButtonBuilder()
302
- .setCustomId(buildApprovalCustomId('always_allow', projectName, targetChannelId))
303
- .setLabel((0, i18n_1.t)('Allow Chat'))
304
- .setStyle(discord_js_1.ButtonStyle.Primary);
305
- const denyBtn = new discord_js_1.ButtonBuilder()
306
- .setCustomId(buildApprovalCustomId('deny', projectName, targetChannelId))
307
- .setLabel((0, i18n_1.t)('Deny'))
308
- .setStyle(discord_js_1.ButtonStyle.Danger);
309
- const row = new discord_js_1.ActionRowBuilder().addComponents(approveBtn, alwaysAllowBtn, denyBtn);
310
- const sent = await targetChannel.send({
311
- embeds: [embed],
312
- components: [row],
313
- }).catch((err) => { logger_1.logger.error(err); return null; });
275
+ const payload = (0, notificationSender_1.buildApprovalNotification)({
276
+ title: (0, i18n_1.t)('Approval Required'),
277
+ description: info.description || (0, i18n_1.t)('Antigravity is requesting approval for an action'),
278
+ projectName,
279
+ channelId: targetChannelId,
280
+ extraFields: [
281
+ { name: (0, i18n_1.t)('Allow button'), value: info.approveText, inline: true },
282
+ { name: (0, i18n_1.t)('Allow Chat button'), value: info.alwaysAllowText || (0, i18n_1.t)('In Dropdown'), inline: true },
283
+ { name: (0, i18n_1.t)('Deny button'), value: info.denyText || (0, i18n_1.t)('(None)'), inline: true },
284
+ ],
285
+ });
286
+ const sent = await targetChannel.send(payload).catch((err) => {
287
+ logger_1.logger.error(err);
288
+ return null;
289
+ });
314
290
  if (sent) {
315
- lastButtonMessage = sent;
291
+ lastNotification = { sent, payload };
316
292
  }
317
293
  },
318
294
  });
@@ -324,68 +300,55 @@ function ensureApprovalDetector(bridge, cdp, projectName, client) {
324
300
  * Helper to start a planning detector for each workspace.
325
301
  * Does nothing if a detector for the same workspace is already running.
326
302
  */
327
- function ensurePlanningDetector(bridge, cdp, projectName, _client) {
303
+ function ensurePlanningDetector(bridge, cdp, projectName) {
328
304
  const existing = bridge.pool.getPlanningDetector(projectName);
329
305
  if (existing && existing.isActive())
330
306
  return;
331
- // Track the most recent planning message for auto-disable on resolve.
307
+ // Track the most recent planning notification for auto-disable on resolve.
332
308
  // See ensureApprovalDetector comment for tracking limitation rationale.
333
- let lastPlanningMessage = null;
309
+ let lastNotification = null;
334
310
  const detector = new planningDetector_1.PlanningDetector({
335
311
  cdpService: cdp,
336
312
  pollIntervalMs: 2000,
337
313
  onResolved: () => {
338
- if (!lastPlanningMessage)
314
+ if (!lastNotification)
339
315
  return;
340
- const msg = lastPlanningMessage;
341
- lastPlanningMessage = null;
342
- const originalEmbed = msg.embeds[0];
343
- const updatedEmbed = originalEmbed
344
- ? discord_js_1.EmbedBuilder.from(originalEmbed)
345
- : new discord_js_1.EmbedBuilder().setTitle((0, i18n_1.t)('Planning Mode'));
346
- updatedEmbed
347
- .setColor(0x95A5A6)
348
- .addFields({ name: (0, i18n_1.t)('Status'), value: (0, i18n_1.t)('Resolved in Antigravity'), inline: false });
349
- msg.edit({
350
- embeds: [updatedEmbed],
351
- components: (0, discordButtonUtils_1.disableAllButtons)(msg.components),
352
- }).catch(logger_1.logger.error);
316
+ const { sent, payload } = lastNotification;
317
+ lastNotification = null;
318
+ const resolved = (0, notificationSender_1.buildResolvedOverlay)(payload, (0, i18n_1.t)('Resolved in Antigravity'));
319
+ sent.edit(resolved).catch(logger_1.logger.error);
353
320
  },
354
321
  onPlanningRequired: async (info) => {
355
322
  logger_1.logger.debug(`[PlanningDetector:${projectName}] Planning buttons detected (title="${info.planTitle}")`);
356
323
  const currentChatTitle = await getCurrentChatTitle(cdp);
357
324
  const targetChannel = resolveApprovalChannelForCurrentChat(bridge, projectName, currentChatTitle);
358
- const targetChannelId = targetChannel && 'id' in targetChannel ? String(targetChannel.id) : '';
359
- if (!targetChannel || !targetChannelId || !('send' in targetChannel)) {
360
- logger_1.logger.warn(`[PlanningDetector:${projectName}] Skipped planning notification because chat is not linked to a Discord session` +
325
+ const targetChannelId = targetChannel ? targetChannel.id : '';
326
+ if (!targetChannel || !targetChannelId) {
327
+ logger_1.logger.warn(`[PlanningDetector:${projectName}] Skipped planning notification because chat is not linked to a session` +
361
328
  `${currentChatTitle ? ` (title="${currentChatTitle}")` : ''}`);
362
329
  return;
363
330
  }
364
331
  const descriptionText = info.description || info.planSummary || (0, i18n_1.t)('A plan has been generated and is awaiting your review.');
365
- const embed = new discord_js_1.EmbedBuilder()
366
- .setTitle((0, i18n_1.t)('Planning Mode'))
367
- .setDescription(descriptionText)
368
- .setColor(0x3498DB)
369
- .addFields({ name: (0, i18n_1.t)('Plan'), value: info.planTitle || (0, i18n_1.t)('Implementation Plan'), inline: true }, { name: (0, i18n_1.t)('Workspace'), value: projectName, inline: true })
370
- .setTimestamp();
332
+ const extraFields = [
333
+ { name: (0, i18n_1.t)('Plan'), value: info.planTitle || (0, i18n_1.t)('Implementation Plan'), inline: true },
334
+ { name: (0, i18n_1.t)('Workspace'), value: projectName, inline: true },
335
+ ];
371
336
  if (info.planSummary && info.description) {
372
- embed.addFields({ name: (0, i18n_1.t)('Summary'), value: info.planSummary.substring(0, 1024), inline: false });
337
+ extraFields.push({ name: (0, i18n_1.t)('Summary'), value: info.planSummary.substring(0, 1024), inline: false });
373
338
  }
374
- const openBtn = new discord_js_1.ButtonBuilder()
375
- .setCustomId(buildPlanningCustomId('open', projectName, targetChannelId))
376
- .setLabel((0, i18n_1.t)('Open'))
377
- .setStyle(discord_js_1.ButtonStyle.Secondary);
378
- const proceedBtn = new discord_js_1.ButtonBuilder()
379
- .setCustomId(buildPlanningCustomId('proceed', projectName, targetChannelId))
380
- .setLabel((0, i18n_1.t)('Proceed'))
381
- .setStyle(discord_js_1.ButtonStyle.Primary);
382
- const row = new discord_js_1.ActionRowBuilder().addComponents(openBtn, proceedBtn);
383
- const sent = await targetChannel.send({
384
- embeds: [embed],
385
- components: [row],
386
- }).catch((err) => { logger_1.logger.error(err); return null; });
339
+ const payload = (0, notificationSender_1.buildPlanningNotification)({
340
+ title: (0, i18n_1.t)('Planning Mode'),
341
+ description: descriptionText,
342
+ projectName,
343
+ channelId: targetChannelId,
344
+ extraFields,
345
+ });
346
+ const sent = await targetChannel.send(payload).catch((err) => {
347
+ logger_1.logger.error(err);
348
+ return null;
349
+ });
387
350
  if (sent) {
388
- lastPlanningMessage = sent;
351
+ lastNotification = { sent, payload };
389
352
  }
390
353
  },
391
354
  });
@@ -397,69 +360,51 @@ function ensurePlanningDetector(bridge, cdp, projectName, _client) {
397
360
  * Helper to start an error popup detector for each workspace.
398
361
  * Does nothing if a detector for the same workspace is already running.
399
362
  */
400
- function ensureErrorPopupDetector(bridge, cdp, projectName, _client) {
363
+ function ensureErrorPopupDetector(bridge, cdp, projectName) {
401
364
  const existing = bridge.pool.getErrorPopupDetector(projectName);
402
365
  if (existing && existing.isActive())
403
366
  return;
404
- // Track the most recent error message for auto-disable on resolve.
367
+ // Track the most recent error notification for auto-disable on resolve.
405
368
  // See ensureApprovalDetector comment for tracking limitation rationale.
406
- let lastErrorMessage = null;
369
+ let lastNotification = null;
407
370
  const detector = new errorPopupDetector_1.ErrorPopupDetector({
408
371
  cdpService: cdp,
409
372
  pollIntervalMs: 3000,
410
373
  onResolved: () => {
411
- if (!lastErrorMessage)
374
+ if (!lastNotification)
412
375
  return;
413
- const msg = lastErrorMessage;
414
- lastErrorMessage = null;
415
- const originalEmbed = msg.embeds[0];
416
- const updatedEmbed = originalEmbed
417
- ? discord_js_1.EmbedBuilder.from(originalEmbed)
418
- : new discord_js_1.EmbedBuilder().setTitle((0, i18n_1.t)('Agent Error'));
419
- updatedEmbed
420
- .setColor(0x95A5A6)
421
- .addFields({ name: (0, i18n_1.t)('Status'), value: (0, i18n_1.t)('Resolved in Antigravity'), inline: false });
422
- msg.edit({
423
- embeds: [updatedEmbed],
424
- components: (0, discordButtonUtils_1.disableAllButtons)(msg.components),
425
- }).catch(logger_1.logger.error);
376
+ const { sent, payload } = lastNotification;
377
+ lastNotification = null;
378
+ const resolved = (0, notificationSender_1.buildResolvedOverlay)(payload, (0, i18n_1.t)('Resolved in Antigravity'));
379
+ sent.edit(resolved).catch(logger_1.logger.error);
426
380
  },
427
381
  onErrorPopup: async (info) => {
428
382
  logger_1.logger.debug(`[ErrorPopupDetector:${projectName}] Error popup detected (title="${info.title}")`);
429
383
  const currentChatTitle = await getCurrentChatTitle(cdp);
430
384
  const targetChannel = resolveApprovalChannelForCurrentChat(bridge, projectName, currentChatTitle);
431
- const targetChannelId = targetChannel && 'id' in targetChannel ? String(targetChannel.id) : '';
432
- if (!targetChannel || !targetChannelId || !('send' in targetChannel)) {
433
- logger_1.logger.warn(`[ErrorPopupDetector:${projectName}] Skipped error popup notification because chat is not linked to a Discord session` +
385
+ const targetChannelId = targetChannel ? targetChannel.id : '';
386
+ if (!targetChannel || !targetChannelId) {
387
+ logger_1.logger.warn(`[ErrorPopupDetector:${projectName}] Skipped error popup notification because chat is not linked to a session` +
434
388
  `${currentChatTitle ? ` (title="${currentChatTitle}")` : ''}`);
435
389
  return;
436
390
  }
437
391
  const bodyText = info.body || (0, i18n_1.t)('An error occurred in the Antigravity agent.');
438
- const embed = new discord_js_1.EmbedBuilder()
439
- .setTitle(info.title || (0, i18n_1.t)('Agent Error'))
440
- .setDescription(bodyText.substring(0, 4096))
441
- .setColor(0xE74C3C)
442
- .addFields({ name: (0, i18n_1.t)('Buttons'), value: info.buttons.join(', ') || (0, i18n_1.t)('(None)'), inline: true }, { name: (0, i18n_1.t)('Workspace'), value: projectName, inline: true })
443
- .setTimestamp();
444
- const dismissBtn = new discord_js_1.ButtonBuilder()
445
- .setCustomId(buildErrorPopupCustomId('dismiss', projectName, targetChannelId))
446
- .setLabel((0, i18n_1.t)('Dismiss'))
447
- .setStyle(discord_js_1.ButtonStyle.Secondary);
448
- const copyDebugBtn = new discord_js_1.ButtonBuilder()
449
- .setCustomId(buildErrorPopupCustomId('copy_debug', projectName, targetChannelId))
450
- .setLabel((0, i18n_1.t)('Copy debug info'))
451
- .setStyle(discord_js_1.ButtonStyle.Primary);
452
- const retryBtn = new discord_js_1.ButtonBuilder()
453
- .setCustomId(buildErrorPopupCustomId('retry', projectName, targetChannelId))
454
- .setLabel((0, i18n_1.t)('Retry'))
455
- .setStyle(discord_js_1.ButtonStyle.Success);
456
- const row = new discord_js_1.ActionRowBuilder().addComponents(dismissBtn, copyDebugBtn, retryBtn);
457
- const sent = await targetChannel.send({
458
- embeds: [embed],
459
- components: [row],
460
- }).catch((err) => { logger_1.logger.error(err); return null; });
392
+ const payload = (0, notificationSender_1.buildErrorPopupNotification)({
393
+ title: info.title || (0, i18n_1.t)('Agent Error'),
394
+ errorMessage: bodyText.substring(0, 4096),
395
+ projectName,
396
+ channelId: targetChannelId,
397
+ extraFields: [
398
+ { name: (0, i18n_1.t)('Buttons'), value: info.buttons.join(', ') || (0, i18n_1.t)('(None)'), inline: true },
399
+ { name: (0, i18n_1.t)('Workspace'), value: projectName, inline: true },
400
+ ],
401
+ });
402
+ const sent = await targetChannel.send(payload).catch((err) => {
403
+ logger_1.logger.error(err);
404
+ return null;
405
+ });
461
406
  if (sent) {
462
- lastErrorMessage = sent;
407
+ lastNotification = { sent, payload };
463
408
  }
464
409
  },
465
410
  });
@@ -225,6 +225,26 @@ class CdpService extends events_1.EventEmitter {
225
225
  this.ws.send(JSON.stringify({ id, method, params }));
226
226
  });
227
227
  }
228
+ /**
229
+ * Try call(), and on WebSocket connection error,
230
+ * attempt a single on-demand reconnect then retry once.
231
+ * Non-connection errors (timeout, protocol) are NOT retried.
232
+ */
233
+ async callWithRetry(method, params = {}, timeoutMs = 10000) {
234
+ try {
235
+ return await this.call(method, params);
236
+ }
237
+ catch (error) {
238
+ const message = error instanceof Error ? error.message : String(error);
239
+ const isConnectionError = message === 'WebSocket is not connected' ||
240
+ message === 'WebSocket disconnected';
241
+ if (!isConnectionError) {
242
+ throw error;
243
+ }
244
+ await this.reconnectOnDemand(timeoutMs);
245
+ return await this.call(method, params);
246
+ }
247
+ }
228
248
  async disconnect() {
229
249
  // Stop reconnection attempts
230
250
  this.maxReconnectAttempts = 0;
@@ -644,6 +664,72 @@ class CdpService extends events_1.EventEmitter {
644
664
  logger_1.logger.error('[CdpService]', finalError.message);
645
665
  this.emit('reconnectFailed', finalError);
646
666
  }
667
+ /**
668
+ * Wait for an in-progress reconnection to complete.
669
+ * Resolves when 'reconnected' fires, rejects on 'reconnectFailed' or timeout.
670
+ */
671
+ waitForReconnection(timeoutMs = 15000) {
672
+ return new Promise((resolve, reject) => {
673
+ const timer = setTimeout(() => {
674
+ cleanup();
675
+ reject(new Error('WebSocket is not connected'));
676
+ }, timeoutMs);
677
+ const onReconnected = () => {
678
+ cleanup();
679
+ resolve();
680
+ };
681
+ const onFailed = (_err) => {
682
+ cleanup();
683
+ reject(new Error('WebSocket is not connected'));
684
+ };
685
+ const cleanup = () => {
686
+ clearTimeout(timer);
687
+ this.removeListener('reconnected', onReconnected);
688
+ this.removeListener('reconnectFailed', onFailed);
689
+ };
690
+ this.on('reconnected', onReconnected);
691
+ this.on('reconnectFailed', onFailed);
692
+ });
693
+ }
694
+ /** Shared promise to coalesce concurrent reconnectOnDemand() calls */
695
+ reconnectOnDemandPromise = null;
696
+ /**
697
+ * On-demand reconnect: if already reconnecting, wait; otherwise attempt once.
698
+ * Throws 'WebSocket is not connected' when no workspace path or reconnect fails.
699
+ */
700
+ async reconnectOnDemand(timeoutMs = 15000) {
701
+ if (this.isReconnecting) {
702
+ return this.waitForReconnection(timeoutMs);
703
+ }
704
+ if (!this.currentWorkspacePath) {
705
+ throw new Error('WebSocket is not connected');
706
+ }
707
+ // Coalesce concurrent calls
708
+ if (!this.reconnectOnDemandPromise) {
709
+ this.reconnectOnDemandPromise = (async () => {
710
+ try {
711
+ await this.discoverAndConnectForWorkspace(this.currentWorkspacePath);
712
+ }
713
+ catch {
714
+ throw new Error('WebSocket is not connected');
715
+ }
716
+ finally {
717
+ this.reconnectOnDemandPromise = null;
718
+ }
719
+ })();
720
+ }
721
+ let timer;
722
+ const timeoutPromise = new Promise((_, reject) => {
723
+ timer = setTimeout(() => reject(new Error('WebSocket is not connected')), timeoutMs);
724
+ });
725
+ try {
726
+ await Promise.race([this.reconnectOnDemandPromise, timeoutPromise]);
727
+ }
728
+ finally {
729
+ if (timer)
730
+ clearTimeout(timer);
731
+ }
732
+ }
647
733
  isConnected() {
648
734
  return this.isConnectedFlag;
649
735
  }
@@ -1163,7 +1249,7 @@ class CdpService extends events_1.EventEmitter {
1163
1249
  */
1164
1250
  async setUiMode(modeName) {
1165
1251
  if (!this.isConnectedFlag || !this.ws) {
1166
- throw new Error('Not connected to CDP. Call connect() first.');
1252
+ await this.reconnectOnDemand();
1167
1253
  }
1168
1254
  const safeMode = JSON.stringify(modeName);
1169
1255
  // Internal mode name -> Antigravity UI display name mapping
@@ -1330,7 +1416,7 @@ class CdpService extends events_1.EventEmitter {
1330
1416
  */
1331
1417
  async setUiModel(modelName) {
1332
1418
  if (!this.isConnectedFlag || !this.ws) {
1333
- throw new Error('Not connected to CDP. Call connect() first.');
1419
+ await this.reconnectOnDemand();
1334
1420
  }
1335
1421
  // DOM manipulation script: based on actual Antigravity UI DOM structure
1336
1422
  // Model list uses div.cursor-pointer elements with class 'px-2 py-1 flex items-center justify-between'
@@ -78,12 +78,17 @@ const SCRAPE_PAST_CONVERSATIONS_SCRIPT = `(() => {
78
78
  const isVisible = (el) => !!el && el instanceof HTMLElement && el.offsetParent !== null;
79
79
  const normalize = (text) => (text || '').trim();
80
80
 
81
+ // Scope to the side panel to avoid picking up file tab names
82
+ const panel = document.querySelector('.antigravity-agent-side-panel');
83
+ if (!panel) return null;
84
+
81
85
  const items = [];
82
86
  const seen = new Set();
83
87
 
84
- // Find the scrollable conversation list container
85
- const containers = Array.from(document.querySelectorAll('div[class*="overflow-auto"], div[class*="overflow-y-scroll"]'));
86
- const container = containers.find((c) => isVisible(c) && c.querySelectorAll('div[class*="cursor-pointer"]').length > 0) || document;
88
+ // Find the scrollable conversation list container within the side panel
89
+ const containers = Array.from(panel.querySelectorAll('div[class*="overflow-auto"], div[class*="overflow-y-scroll"]'));
90
+ const container = containers.find((c) => isVisible(c) && c.querySelectorAll('div[class*="cursor-pointer"]').length > 0);
91
+ if (!container) return null;
87
92
 
88
93
  // Detect the "Other Conversations" section boundary.
89
94
  // Sessions below this header belong to other projects and must be excluded.
@@ -436,6 +441,7 @@ class ChatSessionService {
436
441
  * @returns Array of session list items (empty array on failure)
437
442
  */
438
443
  async listAllSessions(cdpService) {
444
+ let panelOpened = false;
439
445
  try {
440
446
  // Step 1: Find Past Conversations button
441
447
  const btnState = await this.evaluateOnAnyContext(cdpService, FIND_PAST_CONVERSATIONS_BUTTON_SCRIPT, false);
@@ -444,8 +450,30 @@ class ChatSessionService {
444
450
  }
445
451
  // Step 2: Click via CDP mouse events (reliable in Electron)
446
452
  await this.cdpMouseClick(cdpService, btnState.x, btnState.y);
447
- // Step 3: Wait for panel to render
448
- await new Promise((r) => setTimeout(r, 500));
453
+ panelOpened = true;
454
+ // Step 3: Wait for panel to render (poll for content, up to 3s)
455
+ const PANEL_READY_CHECK = `(() => {
456
+ const isVisible = (el) => !!el && el instanceof HTMLElement && el.offsetParent !== null;
457
+ const panel = document.querySelector('.antigravity-agent-side-panel');
458
+ if (!panel) return false;
459
+ const containers = Array.from(
460
+ panel.querySelectorAll('div[class*="overflow-auto"], div[class*="overflow-y-scroll"]')
461
+ );
462
+ return containers.some((c) =>
463
+ isVisible(c) && c.querySelector('div[class*="cursor-pointer"]')
464
+ );
465
+ })()`;
466
+ let panelReady = false;
467
+ const deadline = Date.now() + 3000;
468
+ while (Date.now() < deadline) {
469
+ panelReady = Boolean(await this.evaluateOnAnyContext(cdpService, PANEL_READY_CHECK, false));
470
+ if (panelReady)
471
+ break;
472
+ await new Promise((r) => setTimeout(r, 200));
473
+ }
474
+ if (!panelReady) {
475
+ return [];
476
+ }
449
477
  // Step 4: Scrape sessions
450
478
  let scrapeResult = await this.evaluateOnAnyContext(cdpService, SCRAPE_PAST_CONVERSATIONS_SCRIPT, false);
451
479
  let sessions = scrapeResult?.sessions ?? [];
@@ -460,7 +488,22 @@ class ChatSessionService {
460
488
  sessions = scrapeResult?.sessions ?? [];
461
489
  }
462
490
  }
463
- // Step 7: Close panel with Escape
491
+ return sessions.slice(0, ChatSessionService.LIST_SESSIONS_TARGET);
492
+ }
493
+ catch (_) {
494
+ return [];
495
+ }
496
+ finally {
497
+ if (panelOpened) {
498
+ await this.closePanelWithEscape(cdpService);
499
+ }
500
+ }
501
+ }
502
+ /**
503
+ * Close the Past Conversations panel by sending Escape key events.
504
+ */
505
+ async closePanelWithEscape(cdpService) {
506
+ try {
464
507
  await cdpService.call('Input.dispatchKeyEvent', {
465
508
  type: 'keyDown', key: 'Escape', code: 'Escape',
466
509
  windowsVirtualKeyCode: 27, nativeVirtualKeyCode: 27,
@@ -469,11 +512,8 @@ class ChatSessionService {
469
512
  type: 'keyUp', key: 'Escape', code: 'Escape',
470
513
  windowsVirtualKeyCode: 27, nativeVirtualKeyCode: 27,
471
514
  });
472
- return sessions.slice(0, ChatSessionService.LIST_SESSIONS_TARGET);
473
- }
474
- catch (_) {
475
- return [];
476
515
  }
516
+ catch (_) { /* best-effort cleanup */ }
477
517
  }
478
518
  /**
479
519
  * Evaluate a script on the first context that returns a truthy value.
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ /**
3
+ * Single-responsibility module for applying the user's default model
4
+ * preference when a CDP session connects.
5
+ *
6
+ * Strategy: exact match only — no fuzzy matching to avoid selecting
7
+ * the wrong model after Antigravity renames a model.
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.applyDefaultModel = applyDefaultModel;
11
+ const logger_1 = require("../utils/logger");
12
+ /**
13
+ * Apply the user's default model preference to a CDP session.
14
+ *
15
+ * 1. Read modelService.getDefaultModel() — if null, skip
16
+ * 2. Read cdp.getCurrentModel() — if already matches, skip (mark synced)
17
+ * 3. Get available models via cdp.getUiModels()
18
+ * 4. Exact match → cdp.setUiModel() → mark synced
19
+ * 5. No match → return stale result with message + available model list
20
+ */
21
+ async function applyDefaultModel(cdp, modelService) {
22
+ const defaultModel = modelService.getDefaultModel();
23
+ if (!defaultModel) {
24
+ return { applied: false, modelName: null, stale: false, staleMessage: null };
25
+ }
26
+ const currentModel = await cdp.getCurrentModel();
27
+ if (currentModel && currentModel.toLowerCase() === defaultModel.toLowerCase()) {
28
+ modelService.markSynced();
29
+ logger_1.logger.debug(`[DefaultModelApplicator] Already on default model: ${defaultModel}`);
30
+ return { applied: true, modelName: defaultModel, stale: false, staleMessage: null };
31
+ }
32
+ const availableModels = await cdp.getUiModels();
33
+ const exactMatch = availableModels.find(m => m.toLowerCase() === defaultModel.toLowerCase());
34
+ if (exactMatch) {
35
+ const result = await cdp.setUiModel(exactMatch);
36
+ if (result.ok) {
37
+ modelService.markSynced();
38
+ logger_1.logger.debug(`[DefaultModelApplicator] Applied default model: ${exactMatch}`);
39
+ return { applied: true, modelName: exactMatch, stale: false, staleMessage: null };
40
+ }
41
+ logger_1.logger.warn(`[DefaultModelApplicator] setUiModel failed: ${result.error}`);
42
+ return { applied: false, modelName: defaultModel, stale: false, staleMessage: null };
43
+ }
44
+ // No exact match — model is stale
45
+ const availableList = availableModels.join(', ');
46
+ const staleMessage = `Saved default model "${defaultModel}" is no longer available. Available models: ${availableList}`;
47
+ logger_1.logger.warn(`[DefaultModelApplicator] ${staleMessage}`);
48
+ return {
49
+ applied: false,
50
+ modelName: defaultModel,
51
+ stale: true,
52
+ staleMessage,
53
+ };
54
+ }
@@ -33,17 +33,31 @@ exports.DEFAULT_MODE = 'fast';
33
33
  */
34
34
  class ModeService {
35
35
  currentMode = exports.DEFAULT_MODE;
36
+ pendingSync = false;
36
37
  /**
37
38
  * Get the current execution mode
38
39
  */
39
40
  getCurrentMode() {
40
41
  return this.currentMode;
41
42
  }
43
+ /**
44
+ * Check if the current mode is pending sync to Antigravity
45
+ */
46
+ isPendingSync() {
47
+ return this.pendingSync;
48
+ }
49
+ /**
50
+ * Mark the pending mode as synced (clears pendingSync flag)
51
+ */
52
+ markSynced() {
53
+ this.pendingSync = false;
54
+ }
42
55
  /**
43
56
  * Switch execution mode
44
57
  * @param modeName Mode name to set (case-insensitive)
58
+ * @param synced Whether the mode has been synced to Antigravity (default: false)
45
59
  */
46
- setMode(modeName) {
60
+ setMode(modeName, synced = false) {
47
61
  if (!modeName || modeName.trim() === '') {
48
62
  return {
49
63
  success: false,
@@ -58,6 +72,7 @@ class ModeService {
58
72
  };
59
73
  }
60
74
  this.currentMode = normalized;
75
+ this.pendingSync = !synced;
61
76
  return {
62
77
  success: true,
63
78
  mode: this.currentMode,