lazy-gravity 0.9.2 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/dist/bin/commands/doctor.js +45 -7
  2. package/dist/bot/index.js +401 -41
  3. package/dist/bot/telegramMessageHandler.js +1 -0
  4. package/dist/commands/chatCommandHandler.js +43 -3
  5. package/dist/commands/registerSlashCommands.js +13 -1
  6. package/dist/events/interactionCreateHandler.js +456 -27
  7. package/dist/events/messageCreateHandler.js +66 -7
  8. package/dist/handlers/__tests__/fileChangeButtonAction.test.js +85 -0
  9. package/dist/handlers/approvalButtonAction.js +2 -1
  10. package/dist/handlers/errorPopupButtonAction.js +2 -1
  11. package/dist/handlers/fileChangeButtonAction.js +57 -0
  12. package/dist/handlers/genericActionButtonAction.js +72 -0
  13. package/dist/handlers/planningButtonAction.js +126 -23
  14. package/dist/handlers/planningModalSubmitAction.js +38 -0
  15. package/dist/handlers/questionSelectAction.js +70 -0
  16. package/dist/handlers/questionSkipAction.js +68 -0
  17. package/dist/handlers/runCommandButtonAction.js +2 -1
  18. package/dist/platform/discord/discordResponseRenderer.js +164 -0
  19. package/dist/platform/discord/wrappers.js +76 -0
  20. package/dist/services/approvalDetector.js +110 -35
  21. package/dist/services/artifactService.js +103 -37
  22. package/dist/services/assistantDomExtractor.js +194 -3
  23. package/dist/services/cdpBridgeManager.js +149 -8
  24. package/dist/services/cdpConnectionPool.js +22 -2
  25. package/dist/services/cdpService.js +134 -15
  26. package/dist/services/chatSessionService.js +147 -40
  27. package/dist/services/errorPopupDetector.js +23 -11
  28. package/dist/services/notificationSender.js +80 -3
  29. package/dist/services/planningDetector.js +69 -25
  30. package/dist/services/promptDispatcher.js +27 -1
  31. package/dist/services/questionDetector.js +428 -0
  32. package/dist/services/responseMonitor.js +45 -3
  33. package/dist/services/runCommandDetector.js +14 -7
  34. package/dist/ui/artifactsUi.js +4 -3
  35. package/dist/utils/consecutiveEmptyPollGate.js +21 -0
  36. package/dist/utils/fileOpenCache.js +29 -0
  37. package/dist/utils/htmlToDiscordMarkdown.js +5 -2
  38. package/dist/utils/pathUtils.js +12 -0
  39. package/dist/utils/projectResolver.js +10 -0
  40. package/dist/utils/questionActionUtils.js +47 -0
  41. package/package.json +1 -1
@@ -1,16 +1,32 @@
1
1
  "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
2
5
  Object.defineProperty(exports, "__esModule", { value: true });
3
6
  exports.createInteractionCreateHandler = createInteractionCreateHandler;
4
7
  const discord_js_1 = require("discord.js");
8
+ const child_process_1 = require("child_process");
9
+ const path_1 = __importDefault(require("path"));
10
+ const fs_1 = __importDefault(require("fs"));
11
+ const genericActionButtonAction_1 = require("../handlers/genericActionButtonAction");
5
12
  const i18n_1 = require("../utils/i18n");
6
13
  const logger_1 = require("../utils/logger");
7
14
  const discordButtonUtils_1 = require("../utils/discordButtonUtils");
15
+ const pathUtils_1 = require("../utils/pathUtils");
16
+ const fileOpenCache_1 = require("../utils/fileOpenCache");
8
17
  const templateUi_1 = require("../ui/templateUi");
9
18
  const autoAcceptUi_1 = require("../ui/autoAcceptUi");
10
19
  const outputUi_1 = require("../ui/outputUi");
20
+ const notificationSender_1 = require("../services/notificationSender");
21
+ const questionSelectAction_1 = require("../handlers/questionSelectAction");
22
+ const questionSkipAction_1 = require("../handlers/questionSkipAction");
23
+ const wrappers_1 = require("../platform/discord/wrappers");
24
+ const questionActionUtils_1 = require("../utils/questionActionUtils");
25
+ const projectResolver_1 = require("../utils/projectResolver");
11
26
  const cleanupCommandHandler_1 = require("../commands/cleanupCommandHandler");
12
27
  const projectListUi_1 = require("../ui/projectListUi");
13
28
  const modeService_1 = require("../services/modeService");
29
+ const approvalDetector_1 = require("../services/approvalDetector");
14
30
  const sessionPickerUi_1 = require("../ui/sessionPickerUi");
15
31
  const artifactsUi_1 = require("../ui/artifactsUi");
16
32
  const artifactService_1 = require("../services/artifactService");
@@ -37,16 +53,35 @@ function createInteractionCreateHandler(deps) {
37
53
  accountPrefRepo: deps.accountPrefRepo,
38
54
  accounts: deps.antigravityAccounts,
39
55
  });
40
- const getChannelCdp = (channelId, userId) => (() => {
41
- const workspacePath = deps.wsHandler.getWorkspaceForChannel(channelId);
42
- if (workspacePath) {
43
- const projectName = deps.bridge.pool.extractProjectName(workspacePath);
44
- return deps.bridge.pool.getConnected(projectName, resolveSelectedAccount(channelId, userId));
56
+ const resolveProjectFromChannel = (interaction) => {
57
+ if (!interaction.channelId)
58
+ return null;
59
+ let workspacePath = deps.wsHandler.getWorkspaceForChannel(interaction.channelId);
60
+ if (!workspacePath) {
61
+ const parentId = getParentChannelId(interaction);
62
+ if (parentId) {
63
+ workspacePath = deps.wsHandler.getWorkspaceForChannel(parentId);
64
+ }
45
65
  }
46
- return deps.bridge.lastActiveWorkspace
47
- ? deps.bridge.pool.getConnected(deps.bridge.lastActiveWorkspace, resolveSelectedAccount(channelId, userId))
48
- : null;
66
+ return workspacePath ? deps.bridge.pool.extractProjectName(workspacePath) : null;
67
+ };
68
+ const getChannelCdp = (interaction) => (() => {
69
+ if (!interaction.channelId)
70
+ return null;
71
+ const projectName = resolveProjectFromChannel(interaction);
72
+ if (projectName) {
73
+ return deps.bridge.pool.getConnected(projectName, resolveSelectedAccount(interaction.channelId, interaction.user.id, getParentChannelId(interaction)));
74
+ }
75
+ return null;
49
76
  })();
77
+ const buildResumeOptions = () => ({
78
+ chatSessionService: deps.chatSessionService,
79
+ chatSessionRepo: deps.chatSessionRepo,
80
+ channelManager: deps.channelManager,
81
+ titleGenerator: deps.titleGenerator,
82
+ userPrefRepo: deps.userPrefRepo,
83
+ artifactService: deps.artifactService,
84
+ });
50
85
  const ensureBoundSessionActive = async (channelId, userId, cdp) => {
51
86
  const savedTitle = deps.chatSessionRepo?.findByChannelId(channelId)?.displayName?.trim() || '';
52
87
  if (!savedTitle || savedTitle === (0, i18n_1.t)('(Untitled)') || !deps.chatSessionService) {
@@ -114,6 +149,36 @@ function createInteractionCreateHandler(deps) {
114
149
  return;
115
150
  }
116
151
  try {
152
+ const genericAction = (0, genericActionButtonAction_1.parseGenericActionCustomId)(interaction.customId);
153
+ if (genericAction) {
154
+ const { actionName: formattedName, projectName: parsedProject, channelId } = genericAction;
155
+ if (channelId && channelId !== interaction.channelId) {
156
+ await interaction.reply({
157
+ content: (0, i18n_1.t)('This action is linked to a different session channel.'),
158
+ flags: discord_js_1.MessageFlags.Ephemeral
159
+ }).catch(logger_1.logger.error);
160
+ return;
161
+ }
162
+ const projectName = parsedProject ?? resolveProjectFromChannel(interaction);
163
+ const cdp = projectName
164
+ ? deps.bridge.pool.getConnected(projectName, resolveSelectedAccount(channelId || interaction.channelId, interaction.user.id, getParentChannelId(interaction)))
165
+ : await deps.getCurrentCdp(deps.bridge);
166
+ if (!cdp) {
167
+ await interaction.reply({ content: 'Not connected to CDP.', flags: discord_js_1.MessageFlags.Ephemeral });
168
+ return;
169
+ }
170
+ await interaction.deferUpdate();
171
+ const script = (0, approvalDetector_1.buildClickScript)(formattedName);
172
+ const evalResult = await cdp.call('Runtime.evaluate', {
173
+ expression: script,
174
+ returnByValue: true,
175
+ });
176
+ const resultValue = evalResult.result?.value;
177
+ if (!resultValue?.ok) {
178
+ await interaction.followUp({ content: `Failed to execute action: Button "${formattedName}" not found or obscured.`, flags: discord_js_1.MessageFlags.Ephemeral });
179
+ }
180
+ return;
181
+ }
117
182
  const approvalAction = deps.parseApprovalCustomId(interaction.customId);
118
183
  if (approvalAction) {
119
184
  if (approvalAction.channelId && approvalAction.channelId !== interaction.channelId) {
@@ -123,7 +188,7 @@ function createInteractionCreateHandler(deps) {
123
188
  }).catch(logger_1.logger.error);
124
189
  return;
125
190
  }
126
- const projectName = approvalAction.projectName ?? deps.bridge.lastActiveWorkspace;
191
+ const projectName = approvalAction.projectName ?? resolveProjectFromChannel(interaction);
127
192
  const detector = projectName
128
193
  ? deps.bridge.pool.getApprovalDetector(projectName, resolveSelectedAccount(interaction.channelId, interaction.user.id, getParentChannelId(interaction)))
129
194
  : undefined;
@@ -163,6 +228,19 @@ function createInteractionCreateHandler(deps) {
163
228
  embeds: [updatedEmbed],
164
229
  components: (0, discordButtonUtils_1.disableAllButtons)(interaction.message.components),
165
230
  });
231
+ if (success && (approvalAction.action === 'approve' || approvalAction.action === 'always_allow') && deps.promptDispatcher && interaction.channelId && interaction.message) {
232
+ const cdp = projectName
233
+ ? deps.bridge.pool.getConnected(projectName, resolveSelectedAccount(interaction.channelId, interaction.user.id, getParentChannelId(interaction)))
234
+ : null;
235
+ if (cdp) {
236
+ deps.promptDispatcher.resume({
237
+ message: interaction.message,
238
+ prompt: '',
239
+ cdp,
240
+ options: buildResumeOptions()
241
+ }).catch(e => logger_1.logger.error('[Approval] Failed to resume monitoring:', e));
242
+ }
243
+ }
166
244
  }
167
245
  else {
168
246
  await interaction.reply({ content: 'Approval button not found.', flags: discord_js_1.MessageFlags.Ephemeral });
@@ -193,7 +271,7 @@ function createInteractionCreateHandler(deps) {
193
271
  }).catch(logger_1.logger.error);
194
272
  return;
195
273
  }
196
- const planWorkspaceDirName = planningAction.projectName ?? deps.bridge.lastActiveWorkspace;
274
+ const planWorkspaceDirName = planningAction.projectName ?? resolveProjectFromChannel(interaction);
197
275
  const planDetector = planWorkspaceDirName
198
276
  ? deps.bridge.pool.getPlanningDetector(planWorkspaceDirName, resolveSelectedAccount(interaction.channelId, interaction.user.id, getParentChannelId(interaction)))
199
277
  : undefined;
@@ -257,6 +335,39 @@ function createInteractionCreateHandler(deps) {
257
335
  }).catch(logger_1.logger.error);
258
336
  }
259
337
  }
338
+ else if (planningAction.action === 'reject') {
339
+ const clicked = await planDetector.clickRejectButton();
340
+ const originalEmbed = interaction.message.embeds[0];
341
+ const updatedEmbed = originalEmbed
342
+ ? discord_js_1.EmbedBuilder.from(originalEmbed)
343
+ : new discord_js_1.EmbedBuilder().setTitle('Planning Mode');
344
+ const historyText = `Reject by <@${interaction.user.id}> (${new Date().toLocaleString('ja-JP')})`;
345
+ updatedEmbed
346
+ .setColor(clicked ? 0xE74C3C : 0x95A5A6)
347
+ .addFields({ name: 'Action History', value: historyText, inline: false })
348
+ .setTimestamp();
349
+ try {
350
+ await interaction.update({
351
+ embeds: [updatedEmbed],
352
+ components: (0, discordButtonUtils_1.disableAllButtons)(interaction.message.components),
353
+ });
354
+ }
355
+ catch (interactionError) {
356
+ if (interactionError?.code === 10062 || interactionError?.code === 40060) {
357
+ logger_1.logger.warn('[Planning] Interaction expired. Responding directly in the channel.');
358
+ if (interaction.channel && 'send' in interaction.channel) {
359
+ const fallbackMessage = clicked
360
+ ? (0, i18n_1.t)('Reject completed.')
361
+ : (0, i18n_1.t)('Reject button not found.');
362
+ await interaction.channel.send(fallbackMessage).catch(logger_1.logger.error);
363
+ }
364
+ }
365
+ else {
366
+ throw interactionError;
367
+ }
368
+ }
369
+ return;
370
+ }
260
371
  else {
261
372
  // Proceed action
262
373
  const clicked = await planDetector.clickProceedButton();
@@ -289,6 +400,18 @@ function createInteractionCreateHandler(deps) {
289
400
  throw interactionError;
290
401
  }
291
402
  }
403
+ if (clicked && deps.promptDispatcher && interaction.channelId && interaction.message) {
404
+ const cdp = getChannelCdp(interaction);
405
+ if (cdp) {
406
+ // Use 'as any' to bypass the type difference between Interaction's Message and Discord.js Message if needed
407
+ deps.promptDispatcher.resume({
408
+ message: interaction.message,
409
+ prompt: '',
410
+ cdp,
411
+ options: buildResumeOptions()
412
+ }).catch(e => logger_1.logger.error('[Planning] Failed to resume monitoring:', e));
413
+ }
414
+ }
292
415
  }
293
416
  }
294
417
  catch (planError) {
@@ -310,6 +433,81 @@ function createInteractionCreateHandler(deps) {
310
433
  }
311
434
  return;
312
435
  }
436
+ // File Change handling
437
+ const fileChangeAction = deps.parseFileChangeCustomId(interaction.customId);
438
+ if (fileChangeAction) {
439
+ if (fileChangeAction.channelId && fileChangeAction.channelId !== interaction.channelId) {
440
+ await interaction.reply({
441
+ content: (0, i18n_1.t)('This file change action is linked to a different session channel.'),
442
+ flags: discord_js_1.MessageFlags.Ephemeral,
443
+ }).catch(logger_1.logger.error);
444
+ return;
445
+ }
446
+ const projectName = fileChangeAction.projectName ?? resolveProjectFromChannel(interaction);
447
+ const cdp = projectName ? deps.bridge.pool.getConnected(projectName, resolveSelectedAccount(interaction.channelId, interaction.user.id, getParentChannelId(interaction))) : null;
448
+ if (!cdp) {
449
+ await interaction.reply({ content: (0, i18n_1.t)('Workspace CDP connection not found.'), flags: discord_js_1.MessageFlags.Ephemeral }).catch(logger_1.logger.error);
450
+ return;
451
+ }
452
+ let clicked = false;
453
+ try {
454
+ await interaction.deferUpdate();
455
+ const actionLabelEval = fileChangeAction.action === 'accept' ? 'Accept all' : 'Reject all';
456
+ const script = (0, approvalDetector_1.buildClickScript)(actionLabelEval);
457
+ const contextId = cdp.getPrimaryContextId();
458
+ const callParams = {
459
+ expression: script,
460
+ returnByValue: true,
461
+ };
462
+ if (contextId !== null)
463
+ callParams.contextId = contextId;
464
+ const res = await cdp.call('Runtime.evaluate', callParams);
465
+ clicked = res?.result?.value?.ok === true;
466
+ const originalEmbed = interaction.message.embeds[0];
467
+ const updatedEmbed = originalEmbed
468
+ ? discord_js_1.EmbedBuilder.from(originalEmbed)
469
+ : new discord_js_1.EmbedBuilder().setTitle('File Changes');
470
+ const actionLabel = fileChangeAction.action === 'accept' ? 'Accept All' : 'Reject All';
471
+ const historyText = `${actionLabel} by <@${interaction.user.id}> (${new Date().toLocaleString('ja-JP')})`;
472
+ updatedEmbed
473
+ .setColor(clicked ? (fileChangeAction.action === 'accept' ? 0x2ECC71 : 0xE74C3C) : 0x95A5A6)
474
+ .addFields({ name: 'Action History', value: historyText, inline: false })
475
+ .setTimestamp();
476
+ await interaction.editReply({
477
+ embeds: [updatedEmbed],
478
+ components: (0, discordButtonUtils_1.disableAllButtons)(interaction.message.components),
479
+ });
480
+ if (clicked && deps.promptDispatcher && interaction.channelId && interaction.message) {
481
+ // Resume monitoring, treating the completion of file changes as a need to poll again
482
+ deps.promptDispatcher.resume({
483
+ message: interaction.message,
484
+ prompt: '',
485
+ cdp,
486
+ options: buildResumeOptions()
487
+ }).catch(e => logger_1.logger.error('[FileChange] Failed to resume monitoring:', e));
488
+ }
489
+ }
490
+ catch (err) {
491
+ if (err?.code === 10062 || err?.code === 40060) {
492
+ logger_1.logger.warn('[FileChange] Interaction expired. Responding directly in the channel.');
493
+ if (interaction.channel && 'send' in interaction.channel) {
494
+ const actionLabel = fileChangeAction.action === 'accept' ? 'Accept All' : 'Reject All';
495
+ const fallbackMessage = clicked
496
+ ? `${actionLabel} completed.`
497
+ : (0, i18n_1.t)('File change button not found or error occurred.');
498
+ await interaction.channel.send(fallbackMessage).catch(logger_1.logger.error);
499
+ }
500
+ }
501
+ else {
502
+ logger_1.logger.error('[FileChange] action error:', err);
503
+ await interaction.followUp({
504
+ content: `❌ Error executing file change action: ${err.message || 'Unknown error'}`,
505
+ flags: discord_js_1.MessageFlags.Ephemeral
506
+ }).catch(() => { });
507
+ }
508
+ }
509
+ return;
510
+ }
313
511
  const errorPopupAction = deps.parseErrorPopupCustomId(interaction.customId);
314
512
  if (errorPopupAction) {
315
513
  if (errorPopupAction.channelId && errorPopupAction.channelId !== interaction.channelId) {
@@ -319,7 +517,7 @@ function createInteractionCreateHandler(deps) {
319
517
  }).catch(logger_1.logger.error);
320
518
  return;
321
519
  }
322
- const errorWorkspaceDirName = errorPopupAction.projectName ?? deps.bridge.lastActiveWorkspace;
520
+ const errorWorkspaceDirName = errorPopupAction.projectName ?? resolveProjectFromChannel(interaction);
323
521
  const errorDetector = errorWorkspaceDirName
324
522
  ? deps.bridge.pool.getErrorPopupDetector(errorWorkspaceDirName, resolveSelectedAccount(interaction.channelId, interaction.user.id, getParentChannelId(interaction)))
325
523
  : undefined;
@@ -469,7 +667,7 @@ function createInteractionCreateHandler(deps) {
469
667
  }).catch(logger_1.logger.error);
470
668
  return;
471
669
  }
472
- const runCmdWorkspace = runCommandAction.projectName ?? deps.bridge.lastActiveWorkspace;
670
+ const runCmdWorkspace = runCommandAction.projectName ?? resolveProjectFromChannel(interaction);
473
671
  const runCmdDetector = runCmdWorkspace
474
672
  ? deps.bridge.pool.getRunCommandDetector(runCmdWorkspace, resolveSelectedAccount(interaction.channelId, interaction.user.id, getParentChannelId(interaction)))
475
673
  : undefined;
@@ -540,7 +738,7 @@ function createInteractionCreateHandler(deps) {
540
738
  }
541
739
  if (interaction.customId === 'model_set_default_btn') {
542
740
  await interaction.deferUpdate();
543
- const cdp = getChannelCdp(interaction.channelId, interaction.user.id);
741
+ const cdp = getChannelCdp(interaction);
544
742
  if (!cdp) {
545
743
  await interaction.followUp({ content: 'Not connected to CDP.', flags: discord_js_1.MessageFlags.Ephemeral });
546
744
  return;
@@ -560,7 +758,7 @@ function createInteractionCreateHandler(deps) {
560
758
  deps.userPrefRepo.setDefaultModel(interaction.user.id, currentModel);
561
759
  }
562
760
  await deps.sendModelsUI({ editReply: async (data) => await interaction.editReply(data) }, {
563
- getCurrentCdp: () => getChannelCdp(interaction.channelId, interaction.user.id),
761
+ getCurrentCdp: () => getChannelCdp(interaction),
564
762
  fetchQuota: async () => deps.bridge.quota.fetchQuota(),
565
763
  });
566
764
  await interaction.followUp({ content: `Default model set to **${currentModel}**.`, flags: discord_js_1.MessageFlags.Ephemeral });
@@ -568,7 +766,7 @@ function createInteractionCreateHandler(deps) {
568
766
  }
569
767
  if (interaction.customId === 'model_clear_default_btn') {
570
768
  await interaction.deferUpdate();
571
- const cdp = getChannelCdp(interaction.channelId, interaction.user.id);
769
+ const cdp = getChannelCdp(interaction);
572
770
  if (cdp) {
573
771
  const sessionReady = await ensureBoundSessionActive(interaction.channelId, interaction.user.id, cdp);
574
772
  if (!sessionReady.ok) {
@@ -581,7 +779,7 @@ function createInteractionCreateHandler(deps) {
581
779
  deps.userPrefRepo.setDefaultModel(interaction.user.id, null);
582
780
  }
583
781
  await deps.sendModelsUI({ editReply: async (data) => await interaction.editReply(data) }, {
584
- getCurrentCdp: () => getChannelCdp(interaction.channelId, interaction.user.id),
782
+ getCurrentCdp: () => getChannelCdp(interaction),
585
783
  fetchQuota: async () => deps.bridge.quota.fetchQuota(),
586
784
  });
587
785
  await interaction.followUp({ content: 'Default model cleared.', flags: discord_js_1.MessageFlags.Ephemeral });
@@ -589,7 +787,7 @@ function createInteractionCreateHandler(deps) {
589
787
  }
590
788
  if (interaction.customId === 'model_refresh_btn') {
591
789
  await interaction.deferUpdate();
592
- const cdp = getChannelCdp(interaction.channelId, interaction.user.id);
790
+ const cdp = getChannelCdp(interaction);
593
791
  if (cdp) {
594
792
  const sessionReady = await ensureBoundSessionActive(interaction.channelId, interaction.user.id, cdp);
595
793
  if (!sessionReady.ok) {
@@ -598,7 +796,7 @@ function createInteractionCreateHandler(deps) {
598
796
  }
599
797
  }
600
798
  await deps.sendModelsUI({ editReply: async (data) => await interaction.editReply(data) }, {
601
- getCurrentCdp: () => getChannelCdp(interaction.channelId, interaction.user.id),
799
+ getCurrentCdp: () => getChannelCdp(interaction),
602
800
  fetchQuota: async () => deps.bridge.quota.fetchQuota(),
603
801
  });
604
802
  return;
@@ -606,7 +804,7 @@ function createInteractionCreateHandler(deps) {
606
804
  if (interaction.customId.startsWith('model_btn_')) {
607
805
  await interaction.deferUpdate();
608
806
  const modelName = interaction.customId.replace('model_btn_', '');
609
- const cdp = getChannelCdp(interaction.channelId, interaction.user.id);
807
+ const cdp = getChannelCdp(interaction);
610
808
  if (!cdp) {
611
809
  await interaction.followUp({ content: 'Not connected to CDP.', flags: discord_js_1.MessageFlags.Ephemeral });
612
810
  return;
@@ -622,7 +820,7 @@ function createInteractionCreateHandler(deps) {
622
820
  }
623
821
  else {
624
822
  await deps.sendModelsUI({ editReply: async (data) => await interaction.editReply(data) }, {
625
- getCurrentCdp: () => getChannelCdp(interaction.channelId, interaction.user.id),
823
+ getCurrentCdp: () => getChannelCdp(interaction),
626
824
  fetchQuota: async () => deps.bridge.quota.fetchQuota(),
627
825
  });
628
826
  await interaction.followUp({ content: `Model changed to **${res.model}**!`, flags: discord_js_1.MessageFlags.Ephemeral });
@@ -688,9 +886,173 @@ function createInteractionCreateHandler(deps) {
688
886
  }, true, passedConvId);
689
887
  return;
690
888
  }
889
+ if (interaction.customId.startsWith('file_open:')) {
890
+ await interaction.deferUpdate().catch(() => { });
891
+ let fileUrl = undefined;
892
+ const suffix = interaction.customId.substring('file_open:'.length);
893
+ if (suffix.startsWith('rel:')) {
894
+ const relativePath = suffix.substring('rel:'.length);
895
+ const workspacePath = interaction.channelId ? (deps.wsHandler.getWorkspaceForChannel(interaction.channelId) ||
896
+ deps.chatSessionRepo?.findByChannelId(interaction.channelId)?.workspacePath) : undefined;
897
+ if (workspacePath) {
898
+ fileUrl = `file:///${path_1.default.join(workspacePath, relativePath).replace(/\\/g, '/')}`;
899
+ }
900
+ }
901
+ else if (suffix.startsWith('art:')) {
902
+ const parts = suffix.substring('art:'.length).split(':');
903
+ if (parts.length >= 2) {
904
+ const convId = parts[0];
905
+ const filename = parts.slice(1).join(':');
906
+ if (deps.artifactService) {
907
+ const artifacts = deps.artifactService.listArtifacts(convId);
908
+ const matched = artifacts.find(art => art.filename.toLowerCase() === filename.toLowerCase());
909
+ if (matched) {
910
+ fileUrl = `file:///${matched.absolutePath.replace(/\\/g, '/')}`;
911
+ }
912
+ }
913
+ if (!fileUrl) {
914
+ const brainDir = path_1.default.join(require('os').homedir(), '.gemini', 'antigravity', 'brain');
915
+ fileUrl = `file:///${path_1.default.join(brainDir, convId, filename).replace(/\\/g, '/')}`;
916
+ }
917
+ }
918
+ }
919
+ else {
920
+ // Fallback to cache (for legacy file_open:<hashId> or explicit cache usage)
921
+ const hashId = suffix.startsWith('cache:') ? suffix.substring('cache:'.length) : suffix;
922
+ fileUrl = fileOpenCache_1.fileOpenCache.get(hashId);
923
+ }
924
+ if (!fileUrl) {
925
+ await interaction.followUp({
926
+ content: (0, i18n_1.t)('File URL not found in cache or expired.'),
927
+ flags: discord_js_1.MessageFlags.Ephemeral
928
+ }).catch(logger_1.logger.error);
929
+ return;
930
+ }
931
+ let resolvedPath = fileUrl;
932
+ if (fileUrl.startsWith('file:///')) {
933
+ let rawPath = fileUrl.replace(/^file:\/\/\//, '');
934
+ if (path_1.default.sep === '/' && !rawPath.startsWith('/')) {
935
+ rawPath = '/' + rawPath;
936
+ }
937
+ else if (process.platform === 'win32' && rawPath.startsWith('/')) {
938
+ rawPath = rawPath.substring(1);
939
+ }
940
+ resolvedPath = path_1.default.resolve(rawPath);
941
+ }
942
+ const isArtifact = resolvedPath.includes('.gemini') && resolvedPath.includes('antigravity') && resolvedPath.includes('brain');
943
+ if (!isArtifact) {
944
+ let workspacePath = undefined;
945
+ if (interaction.channelId) {
946
+ workspacePath = deps.wsHandler.getWorkspaceForChannel(interaction.channelId) ||
947
+ deps.chatSessionRepo?.findByChannelId(interaction.channelId)?.workspacePath;
948
+ }
949
+ if (!workspacePath) {
950
+ await interaction.followUp({
951
+ content: `❌ Error: Cannot open files without an active workspace.`,
952
+ flags: discord_js_1.MessageFlags.Ephemeral
953
+ }).catch(logger_1.logger.error);
954
+ return;
955
+ }
956
+ const resolvedWorkspacePath = path_1.default.resolve(workspacePath);
957
+ const relative = path_1.default.relative(resolvedWorkspacePath, resolvedPath);
958
+ if (relative.startsWith('..') || path_1.default.isAbsolute(relative)) {
959
+ await interaction.followUp({
960
+ content: `❌ Error: Cannot open files outside the workspace root.`,
961
+ flags: discord_js_1.MessageFlags.Ephemeral
962
+ }).catch(logger_1.logger.error);
963
+ return;
964
+ }
965
+ }
966
+ try {
967
+ const comp = interaction.component;
968
+ const buttonLabel = comp?.type === 2 ? comp.label : null;
969
+ const isReviewBtn = suffix.startsWith('art:') ||
970
+ buttonLabel?.toLowerCase().includes('review') ||
971
+ resolvedPath.endsWith('walkthrough.md') ||
972
+ resolvedPath.endsWith('task.md') ||
973
+ resolvedPath.endsWith('implementation_plan.md');
974
+ const fileContent = fs_1.default.readFileSync(resolvedPath, 'utf8');
975
+ const MAX_DESC_LEN = 4096;
976
+ let truncated = fileContent.length > MAX_DESC_LEN
977
+ ? fileContent.substring(0, MAX_DESC_LEN - 15) + '\n\n(truncated)'
978
+ : fileContent;
979
+ if (isReviewBtn) {
980
+ truncated += '\n\n💡 *To request changes to this artifact, simply reply in this channel.*';
981
+ }
982
+ const embed = new discord_js_1.EmbedBuilder()
983
+ .setTitle(isReviewBtn ? `Reviewing: ${path_1.default.basename(resolvedPath)}` : `Opened: ${path_1.default.basename(resolvedPath)}`)
984
+ .setDescription(truncated)
985
+ .setColor(isReviewBtn ? 0x2ECC71 : 0x3498DB)
986
+ .setTimestamp();
987
+ if (isReviewBtn) {
988
+ await interaction.followUp({
989
+ embeds: [embed],
990
+ components: []
991
+ }).catch(logger_1.logger.error);
992
+ return;
993
+ }
994
+ (0, child_process_1.execFile)((0, pathUtils_1.getAntigravityCliPath)(), [resolvedPath], async (error) => {
995
+ if (error) {
996
+ logger_1.logger.error(`Failed to open file via CLI: ${error.message}`);
997
+ await interaction.followUp({
998
+ content: `❌ Error opening file via CLI: ${error.message}`,
999
+ flags: discord_js_1.MessageFlags.Ephemeral
1000
+ }).catch(() => { });
1001
+ }
1002
+ else {
1003
+ const closeRow = new discord_js_1.ActionRowBuilder().addComponents(new discord_js_1.ButtonBuilder()
1004
+ .setCustomId('file_close_active')
1005
+ .setLabel('Close Active Tab in IDE')
1006
+ .setStyle(discord_js_1.ButtonStyle.Secondary));
1007
+ await interaction.followUp({
1008
+ content: `✅ Opened file in IDE: **${path_1.default.basename(resolvedPath)}**`,
1009
+ embeds: [embed],
1010
+ components: [closeRow],
1011
+ flags: discord_js_1.MessageFlags.Ephemeral
1012
+ }).catch(() => { });
1013
+ }
1014
+ });
1015
+ }
1016
+ catch (e) {
1017
+ await interaction.followUp({
1018
+ content: `❌ Error opening file: ${e.message}`,
1019
+ flags: discord_js_1.MessageFlags.Ephemeral
1020
+ }).catch(logger_1.logger.error);
1021
+ }
1022
+ return;
1023
+ }
1024
+ if (interaction.customId === 'file_close_active') {
1025
+ await interaction.deferUpdate().catch(() => { });
1026
+ try {
1027
+ const cdp = deps.getCurrentCdp(deps.bridge);
1028
+ if (!cdp) {
1029
+ await interaction.followUp({
1030
+ content: `❌ No active CDP connection found.`,
1031
+ flags: discord_js_1.MessageFlags.Ephemeral
1032
+ }).catch(() => { });
1033
+ return;
1034
+ }
1035
+ if (!cdp.isConnected()) {
1036
+ await cdp.connect();
1037
+ }
1038
+ await cdp.closeActiveEditor();
1039
+ await interaction.followUp({
1040
+ content: `✅ Sent close command to IDE.`,
1041
+ flags: discord_js_1.MessageFlags.Ephemeral
1042
+ }).catch(() => { });
1043
+ }
1044
+ catch (e) {
1045
+ logger_1.logger.error(`Failed to close active editor: ${e.message}`);
1046
+ await interaction.followUp({
1047
+ content: `❌ Error closing editor: ${e.message}`,
1048
+ flags: discord_js_1.MessageFlags.Ephemeral
1049
+ }).catch(() => { });
1050
+ }
1051
+ return;
1052
+ }
691
1053
  }
692
1054
  catch (error) {
693
- logger_1.logger.error('Error during button interaction handling:', error);
1055
+ logger_1.logger.error(`[InteractionCreate] Error handling interaction: ${error instanceof Error ? error.message : String(error)}`);
694
1056
  try {
695
1057
  if (!interaction.replied && !interaction.deferred) {
696
1058
  await interaction.reply({ content: 'An error occurred while processing the button action.', flags: discord_js_1.MessageFlags.Ephemeral });
@@ -723,7 +1085,7 @@ function createInteractionCreateHandler(deps) {
723
1085
  try {
724
1086
  const selectedMode = interaction.values[0];
725
1087
  deps.modeService.setMode(selectedMode);
726
- const cdp = getChannelCdp(interaction.channelId, interaction.user.id);
1088
+ const cdp = getChannelCdp(interaction);
727
1089
  if (cdp) {
728
1090
  const res = await cdp.setUiMode(selectedMode);
729
1091
  if (!res.ok) {
@@ -739,6 +1101,9 @@ function createInteractionCreateHandler(deps) {
739
1101
  if (interaction.deferred || interaction.replied) {
740
1102
  await interaction.followUp({ content: 'An error occurred while changing the mode.', flags: discord_js_1.MessageFlags.Ephemeral }).catch(logger_1.logger.error);
741
1103
  }
1104
+ else {
1105
+ await interaction.reply({ content: 'An error occurred while changing the mode.', flags: discord_js_1.MessageFlags.Ephemeral }).catch(logger_1.logger.error);
1106
+ }
742
1107
  }
743
1108
  catch (e) {
744
1109
  logger_1.logger.error('Failed to send error message:', e);
@@ -866,10 +1231,10 @@ function createInteractionCreateHandler(deps) {
866
1231
  const artifactService = new artifactService_1.ArtifactService();
867
1232
  // Resolve the selected artifact by rescanning and matching the encoded value
868
1233
  const channelId = interaction.channelId;
869
- const sessionTitle = deps.chatSessionRepo?.findByChannelId(channelId)?.displayName?.trim() ?? '';
870
- let conversationId = sessionTitle ? artifactService.findConversationByTitle(sessionTitle) : null;
871
- if (!conversationId)
872
- conversationId = artifactService.getLatestConversationWithArtifacts();
1234
+ const session = deps.chatSessionRepo?.findByChannelId(channelId);
1235
+ const sessionTitle = session?.displayName?.trim() ?? '';
1236
+ const workspaceDirName = (0, pathUtils_1.getWorkspaceDirName)(session);
1237
+ const conversationId = session?.conversationId || (sessionTitle ? artifactService.findConversationByTitle(sessionTitle, workspaceDirName) : null);
873
1238
  if (!conversationId) {
874
1239
  await interaction.editReply({ content: '📂 No artifacts found.', components: [], allowedMentions: { parse: [] } }).catch(logger_1.logger.error);
875
1240
  return;
@@ -975,6 +1340,70 @@ function createInteractionCreateHandler(deps) {
975
1340
  }
976
1341
  return;
977
1342
  }
1343
+ if (interaction.isStringSelectMenu() && interaction.customId.startsWith(notificationSender_1.QUESTION_SELECT_ACTION_PREFIX)) {
1344
+ try {
1345
+ const action = (0, questionSelectAction_1.createQuestionSelectAction)({ bridge: deps.bridge, wsHandler: deps.wsHandler });
1346
+ const wrapped = (0, wrappers_1.wrapDiscordSelect)(interaction);
1347
+ const success = await action.execute(wrapped, interaction.values);
1348
+ if (success && deps.promptDispatcher && interaction.channelId && interaction.message) {
1349
+ const parsed = (0, questionActionUtils_1.parseQuestionCustomId)(interaction.customId, notificationSender_1.QUESTION_SELECT_ACTION_PREFIX);
1350
+ if (parsed) {
1351
+ const projectName = (0, projectResolver_1.resolveProjectName)({ bridge: deps.bridge, wsHandler: deps.wsHandler }, interaction.channelId, parsed.projectName);
1352
+ const cdp = projectName ? deps.bridge.pool.getConnected(projectName, resolveSelectedAccount(interaction.channelId, interaction.user.id, getParentChannelId(interaction))) : null;
1353
+ if (cdp) {
1354
+ deps.promptDispatcher.resume({
1355
+ message: interaction.message,
1356
+ prompt: '',
1357
+ cdp,
1358
+ options: buildResumeOptions()
1359
+ }).catch(e => logger_1.logger.error('[QuestionSelectAction] Failed to resume monitoring:', e));
1360
+ }
1361
+ }
1362
+ }
1363
+ }
1364
+ catch (err) {
1365
+ logger_1.logger.error('[QuestionSelectAction] Error executing action:', err);
1366
+ if (!interaction.replied && !interaction.deferred) {
1367
+ await interaction.reply({ content: 'An error occurred.', ephemeral: true }).catch(logger_1.logger.error);
1368
+ }
1369
+ else {
1370
+ await interaction.followUp({ content: 'An error occurred.', ephemeral: true }).catch(logger_1.logger.error);
1371
+ }
1372
+ }
1373
+ return;
1374
+ }
1375
+ if (interaction.isButton() && interaction.customId.startsWith(notificationSender_1.QUESTION_SKIP_ACTION_PREFIX)) {
1376
+ try {
1377
+ const action = (0, questionSkipAction_1.createQuestionSkipAction)({ bridge: deps.bridge, wsHandler: deps.wsHandler });
1378
+ const wrapped = (0, wrappers_1.wrapDiscordButton)(interaction);
1379
+ const success = await action.execute(wrapped, {});
1380
+ if (success && deps.promptDispatcher && interaction.channelId && interaction.message) {
1381
+ const parsed = (0, questionActionUtils_1.parseQuestionCustomId)(interaction.customId, notificationSender_1.QUESTION_SKIP_ACTION_PREFIX);
1382
+ if (parsed) {
1383
+ const projectName = (0, projectResolver_1.resolveProjectName)({ bridge: deps.bridge, wsHandler: deps.wsHandler }, interaction.channelId, parsed.projectName);
1384
+ const cdp = projectName ? deps.bridge.pool.getConnected(projectName, resolveSelectedAccount(interaction.channelId, interaction.user.id, getParentChannelId(interaction))) : null;
1385
+ if (cdp) {
1386
+ deps.promptDispatcher.resume({
1387
+ message: interaction.message,
1388
+ prompt: '',
1389
+ cdp,
1390
+ options: buildResumeOptions()
1391
+ }).catch(e => logger_1.logger.error('[QuestionSkipAction] Failed to resume monitoring:', e));
1392
+ }
1393
+ }
1394
+ }
1395
+ }
1396
+ catch (err) {
1397
+ logger_1.logger.error('[QuestionSkipAction] Error executing action:', err);
1398
+ if (!interaction.replied && !interaction.deferred) {
1399
+ await interaction.reply({ content: 'An error occurred.', ephemeral: true }).catch(logger_1.logger.error);
1400
+ }
1401
+ else {
1402
+ await interaction.followUp({ content: 'An error occurred.', ephemeral: true }).catch(logger_1.logger.error);
1403
+ }
1404
+ }
1405
+ return;
1406
+ }
978
1407
  if (interaction.isStringSelectMenu() && (0, projectListUi_1.isProjectSelectId)(interaction.customId)) {
979
1408
  if (!deps.config.allowedUserIds.includes(interaction.user.id)) {
980
1409
  await interaction.reply({ content: (0, i18n_1.t)('You do not have permission.'), flags: discord_js_1.MessageFlags.Ephemeral }).catch(logger_1.logger.error);
@@ -1019,7 +1448,7 @@ function createInteractionCreateHandler(deps) {
1019
1448
  throw deferError;
1020
1449
  }
1021
1450
  try {
1022
- await deps.handleSlashInteraction(commandInteraction, deps.slashCommandHandler, deps.bridge, deps.wsHandler, deps.chatHandler, deps.cleanupHandler, deps.modeService, deps.modelService, deps.bridge.autoAccept, deps.client, deps.accountPrefRepo, deps.channelPrefRepo, deps.antigravityAccounts, deps.chatSessionRepo);
1451
+ await deps.handleSlashInteraction(commandInteraction, deps.slashCommandHandler, deps.bridge, deps.wsHandler, deps.chatHandler, deps.cleanupHandler, deps.modeService, deps.modelService, deps.bridge.autoAccept, deps.client, deps.accountPrefRepo, deps.channelPrefRepo, deps.antigravityAccounts, deps.chatSessionRepo, deps.scheduleService);
1023
1452
  }
1024
1453
  catch (error) {
1025
1454
  logger_1.logger.error(`[SlashCommand] command=${commandInteraction.commandName} channel=${commandInteraction.channelId} ` +