lazy-gravity 0.2.0 → 0.3.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 (56) hide show
  1. package/README.md +76 -15
  2. package/dist/bin/commands/doctor.js +19 -2
  3. package/dist/bin/commands/setup.js +286 -70
  4. package/dist/bot/eventRouter.js +70 -0
  5. package/dist/bot/index.js +353 -147
  6. package/dist/bot/telegramCommands.js +428 -0
  7. package/dist/bot/telegramMessageHandler.js +304 -0
  8. package/dist/bot/telegramProjectCommand.js +137 -0
  9. package/dist/bot/workspaceQueue.js +61 -0
  10. package/dist/commands/joinCommandHandler.js +4 -1
  11. package/dist/database/telegramBindingRepository.js +97 -0
  12. package/dist/database/userPreferenceRepository.js +46 -1
  13. package/dist/events/interactionCreateHandler.js +36 -0
  14. package/dist/events/messageCreateHandler.js +11 -7
  15. package/dist/handlers/approvalButtonAction.js +99 -0
  16. package/dist/handlers/autoAcceptButtonAction.js +43 -0
  17. package/dist/handlers/buttonHandler.js +55 -0
  18. package/dist/handlers/commandHandler.js +44 -0
  19. package/dist/handlers/errorPopupButtonAction.js +137 -0
  20. package/dist/handlers/messageHandler.js +70 -0
  21. package/dist/handlers/modeSelectAction.js +63 -0
  22. package/dist/handlers/modelButtonAction.js +102 -0
  23. package/dist/handlers/planningButtonAction.js +118 -0
  24. package/dist/handlers/selectHandler.js +41 -0
  25. package/dist/handlers/templateButtonAction.js +54 -0
  26. package/dist/platform/adapter.js +8 -0
  27. package/dist/platform/discord/discordAdapter.js +99 -0
  28. package/dist/platform/discord/index.js +15 -0
  29. package/dist/platform/discord/wrappers.js +331 -0
  30. package/dist/platform/index.js +18 -0
  31. package/dist/platform/richContentBuilder.js +76 -0
  32. package/dist/platform/telegram/index.js +16 -0
  33. package/dist/platform/telegram/telegramAdapter.js +195 -0
  34. package/dist/platform/telegram/telegramFormatter.js +134 -0
  35. package/dist/platform/telegram/wrappers.js +329 -0
  36. package/dist/platform/types.js +28 -0
  37. package/dist/services/approvalDetector.js +15 -2
  38. package/dist/services/cdpBridgeManager.js +91 -146
  39. package/dist/services/defaultModelApplicator.js +54 -0
  40. package/dist/services/modeService.js +16 -1
  41. package/dist/services/modelService.js +57 -16
  42. package/dist/services/notificationSender.js +149 -0
  43. package/dist/services/responseMonitor.js +1 -2
  44. package/dist/ui/autoAcceptUi.js +37 -0
  45. package/dist/ui/modeUi.js +38 -1
  46. package/dist/ui/modelsUi.js +96 -0
  47. package/dist/ui/outputUi.js +32 -0
  48. package/dist/ui/projectListUi.js +55 -0
  49. package/dist/ui/screenshotUi.js +26 -0
  50. package/dist/ui/sessionPickerUi.js +35 -1
  51. package/dist/ui/templateUi.js +41 -0
  52. package/dist/utils/configLoader.js +63 -12
  53. package/dist/utils/lockfile.js +5 -5
  54. package/dist/utils/logger.js +7 -0
  55. package/dist/utils/telegramImageHandler.js +127 -0
  56. package/package.json +4 -2
@@ -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
  });
@@ -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,
@@ -3,9 +3,10 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ModelService = exports.DEFAULT_MODEL = exports.AVAILABLE_MODELS = void 0;
4
4
  const i18n_1 = require("../utils/i18n");
5
5
  /**
6
- * Available LLM models
7
- * Aligned with models selectable in the Antigravity (Cursor fork) UI
8
- * Note: Models may change with Antigravity version updates
6
+ * Fallback model list used when CDP is not connected.
7
+ * NOT used for validation CDP is the sole source of truth
8
+ * for available models. This list may become stale after
9
+ * Antigravity updates.
9
10
  */
10
11
  exports.AVAILABLE_MODELS = [
11
12
  'gemini-3.1-pro-high',
@@ -15,14 +16,20 @@ exports.AVAILABLE_MODELS = [
15
16
  'claude-opus-4.6-thinking',
16
17
  'gpt-oss-120b-medium'
17
18
  ];
18
- /** Default LLM model */
19
+ /** Default LLM model (initial value before CDP connects) */
19
20
  exports.DEFAULT_MODEL = 'gemini-3-flash';
20
21
  /**
21
22
  * Service class for managing LLM models.
22
23
  * Handles model switching via the /model command.
24
+ *
25
+ * Model validation is intentionally NOT performed here.
26
+ * The actual model list is dynamic (fetched from CDP via
27
+ * cdp.getUiModels()), so setModel() accepts any string.
23
28
  */
24
29
  class ModelService {
25
30
  currentModel = exports.DEFAULT_MODEL;
31
+ defaultModel = null;
32
+ pendingSync = false;
26
33
  /**
27
34
  * Get the current LLM model
28
35
  */
@@ -30,34 +37,68 @@ class ModelService {
30
37
  return this.currentModel;
31
38
  }
32
39
  /**
33
- * Switch LLM model
40
+ * Check if the current model is pending sync to Antigravity
41
+ */
42
+ isPendingSync() {
43
+ return this.pendingSync;
44
+ }
45
+ /**
46
+ * Mark the pending model as synced (clears pendingSync flag)
47
+ */
48
+ markSynced() {
49
+ this.pendingSync = false;
50
+ }
51
+ /**
52
+ * Switch LLM model.
53
+ * Accepts any model name — validation happens at the CDP layer
54
+ * (cdp.setUiModel) against the live model list.
55
+ *
34
56
  * @param modelName Model name to set (case-insensitive)
57
+ * @param synced Whether the model has been synced to Antigravity (default: false)
35
58
  */
36
- setModel(modelName) {
59
+ setModel(modelName, synced = false) {
37
60
  if (!modelName || modelName.trim() === '') {
38
61
  return {
39
62
  success: false,
40
- error: (0, i18n_1.t)('⚠️ Model name not specified. Available models: ') + exports.AVAILABLE_MODELS.join(', '),
41
- };
42
- }
43
- const normalized = modelName.trim().toLowerCase();
44
- if (!exports.AVAILABLE_MODELS.includes(normalized)) {
45
- return {
46
- success: false,
47
- error: (0, i18n_1.t)(`⚠️ Invalid model "${modelName}". Available models: ${exports.AVAILABLE_MODELS.join(', ')}`),
63
+ error: (0, i18n_1.t)('⚠️ Model name not specified.'),
48
64
  };
49
65
  }
50
- this.currentModel = normalized;
66
+ this.currentModel = modelName.trim().toLowerCase();
67
+ this.pendingSync = !synced;
51
68
  return {
52
69
  success: true,
53
70
  model: this.currentModel,
54
71
  };
55
72
  }
56
73
  /**
57
- * Get the list of available models
74
+ * Get the fallback list of available models.
75
+ * Prefer cdp.getUiModels() when CDP is connected.
58
76
  */
59
77
  getAvailableModels() {
60
78
  return exports.AVAILABLE_MODELS;
61
79
  }
80
+ /**
81
+ * Get the default model name (free-text, may not match current CDP models)
82
+ */
83
+ getDefaultModel() {
84
+ return this.defaultModel;
85
+ }
86
+ /**
87
+ * Set the default model name (free-text, persisted via DB separately)
88
+ * @param name Model name or null to clear
89
+ */
90
+ setDefaultModel(name) {
91
+ this.defaultModel = name ? name.trim() : null;
92
+ return { success: true, defaultModel: this.defaultModel };
93
+ }
94
+ /**
95
+ * Load the default model from an external source (e.g. DB).
96
+ * Only sets the in-memory value if not already set.
97
+ */
98
+ loadDefaultModel(name) {
99
+ if (this.defaultModel === null && name) {
100
+ this.defaultModel = name.trim();
101
+ }
102
+ }
62
103
  }
63
104
  exports.ModelService = ModelService;