lazy-gravity 0.9.1 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/bot/index.js +233 -41
  2. package/dist/bot/telegramJoinCommand.js +9 -0
  3. package/dist/bot/telegramMessageHandler.js +7 -0
  4. package/dist/commands/chatCommandHandler.js +71 -1
  5. package/dist/commands/registerSlashCommands.js +13 -1
  6. package/dist/events/interactionCreateHandler.js +324 -26
  7. package/dist/events/messageCreateHandler.js +53 -4
  8. package/dist/handlers/__tests__/fileChangeButtonAction.test.js +85 -0
  9. package/dist/handlers/approvalButtonAction.js +2 -1
  10. package/dist/handlers/errorPopupButtonAction.js +2 -1
  11. package/dist/handlers/fileChangeButtonAction.js +69 -0
  12. package/dist/handlers/genericActionButtonAction.js +74 -0
  13. package/dist/handlers/planningButtonAction.js +24 -9
  14. package/dist/handlers/questionSelectAction.js +69 -0
  15. package/dist/handlers/questionSkipAction.js +67 -0
  16. package/dist/handlers/runCommandButtonAction.js +2 -1
  17. package/dist/platform/discord/discordResponseRenderer.js +164 -0
  18. package/dist/services/approvalDetector.js +70 -24
  19. package/dist/services/artifactService.js +57 -31
  20. package/dist/services/assistantDomExtractor.js +174 -3
  21. package/dist/services/cdpBridgeManager.js +114 -6
  22. package/dist/services/cdpConnectionPool.js +15 -0
  23. package/dist/services/cdpService.js +21 -7
  24. package/dist/services/chatSessionService.js +148 -27
  25. package/dist/services/errorPopupDetector.js +23 -11
  26. package/dist/services/notificationSender.js +70 -3
  27. package/dist/services/planningDetector.js +69 -25
  28. package/dist/services/promptDispatcher.js +7 -1
  29. package/dist/services/questionDetector.js +376 -0
  30. package/dist/services/responseMonitor.js +28 -2
  31. package/dist/services/runCommandDetector.js +14 -7
  32. package/dist/ui/artifactsUi.js +4 -3
  33. package/dist/utils/consecutiveEmptyPollGate.js +21 -0
  34. package/dist/utils/fileOpenCache.js +33 -0
  35. package/dist/utils/htmlToDiscordMarkdown.js +5 -2
  36. package/dist/utils/pathUtils.js +12 -0
  37. package/dist/utils/projectResolver.js +10 -0
  38. package/dist/utils/questionActionUtils.js +25 -0
  39. package/package.json +2 -2
@@ -1,16 +1,30 @@
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");
11
24
  const cleanupCommandHandler_1 = require("../commands/cleanupCommandHandler");
12
25
  const projectListUi_1 = require("../ui/projectListUi");
13
26
  const modeService_1 = require("../services/modeService");
27
+ const approvalDetector_1 = require("../services/approvalDetector");
14
28
  const sessionPickerUi_1 = require("../ui/sessionPickerUi");
15
29
  const artifactsUi_1 = require("../ui/artifactsUi");
16
30
  const artifactService_1 = require("../services/artifactService");
@@ -37,16 +51,35 @@ function createInteractionCreateHandler(deps) {
37
51
  accountPrefRepo: deps.accountPrefRepo,
38
52
  accounts: deps.antigravityAccounts,
39
53
  });
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));
54
+ const resolveProjectFromChannel = (interaction) => {
55
+ if (!interaction.channelId)
56
+ return null;
57
+ let workspacePath = deps.wsHandler.getWorkspaceForChannel(interaction.channelId);
58
+ if (!workspacePath) {
59
+ const parentId = getParentChannelId(interaction);
60
+ if (parentId) {
61
+ workspacePath = deps.wsHandler.getWorkspaceForChannel(parentId);
62
+ }
45
63
  }
46
- return deps.bridge.lastActiveWorkspace
47
- ? deps.bridge.pool.getConnected(deps.bridge.lastActiveWorkspace, resolveSelectedAccount(channelId, userId))
48
- : null;
64
+ return workspacePath ? deps.bridge.pool.extractProjectName(workspacePath) : null;
65
+ };
66
+ const getChannelCdp = (interaction) => (() => {
67
+ if (!interaction.channelId)
68
+ return null;
69
+ const projectName = resolveProjectFromChannel(interaction);
70
+ if (projectName) {
71
+ return deps.bridge.pool.getConnected(projectName, resolveSelectedAccount(interaction.channelId, interaction.user.id, getParentChannelId(interaction)));
72
+ }
73
+ return null;
49
74
  })();
75
+ const buildResumeOptions = () => ({
76
+ chatSessionService: deps.chatSessionService,
77
+ chatSessionRepo: deps.chatSessionRepo,
78
+ channelManager: deps.channelManager,
79
+ titleGenerator: deps.titleGenerator,
80
+ userPrefRepo: deps.userPrefRepo,
81
+ artifactService: deps.artifactService,
82
+ });
50
83
  const ensureBoundSessionActive = async (channelId, userId, cdp) => {
51
84
  const savedTitle = deps.chatSessionRepo?.findByChannelId(channelId)?.displayName?.trim() || '';
52
85
  if (!savedTitle || savedTitle === (0, i18n_1.t)('(Untitled)') || !deps.chatSessionService) {
@@ -114,6 +147,36 @@ function createInteractionCreateHandler(deps) {
114
147
  return;
115
148
  }
116
149
  try {
150
+ const genericAction = (0, genericActionButtonAction_1.parseGenericActionCustomId)(interaction.customId);
151
+ if (genericAction) {
152
+ const { actionName: formattedName, projectName: parsedProject, channelId } = genericAction;
153
+ if (channelId && channelId !== interaction.channelId) {
154
+ await interaction.reply({
155
+ content: (0, i18n_1.t)('This action is linked to a different session channel.'),
156
+ flags: discord_js_1.MessageFlags.Ephemeral
157
+ }).catch(logger_1.logger.error);
158
+ return;
159
+ }
160
+ const projectName = parsedProject ?? resolveProjectFromChannel(interaction);
161
+ const cdp = projectName
162
+ ? deps.bridge.pool.getConnected(projectName, resolveSelectedAccount(channelId || interaction.channelId, interaction.user.id, getParentChannelId(interaction)))
163
+ : await deps.getCurrentCdp(deps.bridge);
164
+ if (!cdp) {
165
+ await interaction.reply({ content: 'Not connected to CDP.', flags: discord_js_1.MessageFlags.Ephemeral });
166
+ return;
167
+ }
168
+ await interaction.deferUpdate();
169
+ const script = (0, approvalDetector_1.buildClickScript)(formattedName);
170
+ const evalResult = await cdp.call('Runtime.evaluate', {
171
+ expression: script,
172
+ returnByValue: true,
173
+ });
174
+ const resultValue = evalResult.result?.value;
175
+ if (!resultValue?.ok) {
176
+ await interaction.followUp({ content: `Failed to execute action: Button "${formattedName}" not found or obscured.`, flags: discord_js_1.MessageFlags.Ephemeral });
177
+ }
178
+ return;
179
+ }
117
180
  const approvalAction = deps.parseApprovalCustomId(interaction.customId);
118
181
  if (approvalAction) {
119
182
  if (approvalAction.channelId && approvalAction.channelId !== interaction.channelId) {
@@ -123,7 +186,7 @@ function createInteractionCreateHandler(deps) {
123
186
  }).catch(logger_1.logger.error);
124
187
  return;
125
188
  }
126
- const projectName = approvalAction.projectName ?? deps.bridge.lastActiveWorkspace;
189
+ const projectName = approvalAction.projectName ?? resolveProjectFromChannel(interaction);
127
190
  const detector = projectName
128
191
  ? deps.bridge.pool.getApprovalDetector(projectName, resolveSelectedAccount(interaction.channelId, interaction.user.id, getParentChannelId(interaction)))
129
192
  : undefined;
@@ -193,7 +256,7 @@ function createInteractionCreateHandler(deps) {
193
256
  }).catch(logger_1.logger.error);
194
257
  return;
195
258
  }
196
- const planWorkspaceDirName = planningAction.projectName ?? deps.bridge.lastActiveWorkspace;
259
+ const planWorkspaceDirName = planningAction.projectName ?? resolveProjectFromChannel(interaction);
197
260
  const planDetector = planWorkspaceDirName
198
261
  ? deps.bridge.pool.getPlanningDetector(planWorkspaceDirName, resolveSelectedAccount(interaction.channelId, interaction.user.id, getParentChannelId(interaction)))
199
262
  : undefined;
@@ -257,6 +320,39 @@ function createInteractionCreateHandler(deps) {
257
320
  }).catch(logger_1.logger.error);
258
321
  }
259
322
  }
323
+ else if (planningAction.action === 'reject') {
324
+ const clicked = await planDetector.clickRejectButton();
325
+ const originalEmbed = interaction.message.embeds[0];
326
+ const updatedEmbed = originalEmbed
327
+ ? discord_js_1.EmbedBuilder.from(originalEmbed)
328
+ : new discord_js_1.EmbedBuilder().setTitle('Planning Mode');
329
+ const historyText = `Reject by <@${interaction.user.id}> (${new Date().toLocaleString('ja-JP')})`;
330
+ updatedEmbed
331
+ .setColor(clicked ? 0xE74C3C : 0x95A5A6)
332
+ .addFields({ name: 'Action History', value: historyText, inline: false })
333
+ .setTimestamp();
334
+ try {
335
+ await interaction.update({
336
+ embeds: [updatedEmbed],
337
+ components: (0, discordButtonUtils_1.disableAllButtons)(interaction.message.components),
338
+ });
339
+ }
340
+ catch (interactionError) {
341
+ if (interactionError?.code === 10062 || interactionError?.code === 40060) {
342
+ logger_1.logger.warn('[Planning] Interaction expired. Responding directly in the channel.');
343
+ if (interaction.channel && 'send' in interaction.channel) {
344
+ const fallbackMessage = clicked
345
+ ? (0, i18n_1.t)('Reject completed.')
346
+ : (0, i18n_1.t)('Reject button not found.');
347
+ await interaction.channel.send(fallbackMessage).catch(logger_1.logger.error);
348
+ }
349
+ }
350
+ else {
351
+ throw interactionError;
352
+ }
353
+ }
354
+ return;
355
+ }
260
356
  else {
261
357
  // Proceed action
262
358
  const clicked = await planDetector.clickProceedButton();
@@ -289,6 +385,18 @@ function createInteractionCreateHandler(deps) {
289
385
  throw interactionError;
290
386
  }
291
387
  }
388
+ if (clicked && deps.promptDispatcher && interaction.channelId && interaction.message) {
389
+ const cdp = getChannelCdp(interaction);
390
+ if (cdp) {
391
+ // Use 'as any' to bypass the type difference between Interaction's Message and Discord.js Message if needed
392
+ deps.promptDispatcher.resume({
393
+ message: interaction.message,
394
+ prompt: '',
395
+ cdp,
396
+ options: buildResumeOptions()
397
+ }).catch(e => logger_1.logger.error('[Planning] Failed to resume monitoring:', e));
398
+ }
399
+ }
292
400
  }
293
401
  }
294
402
  catch (planError) {
@@ -310,6 +418,81 @@ function createInteractionCreateHandler(deps) {
310
418
  }
311
419
  return;
312
420
  }
421
+ // File Change handling
422
+ const fileChangeAction = deps.parseFileChangeCustomId(interaction.customId);
423
+ if (fileChangeAction) {
424
+ if (fileChangeAction.channelId && fileChangeAction.channelId !== interaction.channelId) {
425
+ await interaction.reply({
426
+ content: (0, i18n_1.t)('This file change action is linked to a different session channel.'),
427
+ flags: discord_js_1.MessageFlags.Ephemeral,
428
+ }).catch(logger_1.logger.error);
429
+ return;
430
+ }
431
+ const projectName = fileChangeAction.projectName ?? resolveProjectFromChannel(interaction);
432
+ const cdp = projectName ? deps.bridge.pool.getConnected(projectName, resolveSelectedAccount(interaction.channelId, interaction.user.id, getParentChannelId(interaction))) : null;
433
+ if (!cdp) {
434
+ await interaction.reply({ content: (0, i18n_1.t)('Workspace CDP connection not found.'), flags: discord_js_1.MessageFlags.Ephemeral }).catch(logger_1.logger.error);
435
+ return;
436
+ }
437
+ let clicked = false;
438
+ try {
439
+ await interaction.deferUpdate();
440
+ const actionLabelEval = fileChangeAction.action === 'accept' ? 'Accept all' : 'Reject all';
441
+ const script = (0, approvalDetector_1.buildClickScript)(actionLabelEval);
442
+ const contextId = cdp.getPrimaryContextId();
443
+ const callParams = {
444
+ expression: script,
445
+ returnByValue: true,
446
+ };
447
+ if (contextId !== null)
448
+ callParams.contextId = contextId;
449
+ const res = await cdp.call('Runtime.evaluate', callParams);
450
+ clicked = res?.result?.value?.ok === true;
451
+ const originalEmbed = interaction.message.embeds[0];
452
+ const updatedEmbed = originalEmbed
453
+ ? discord_js_1.EmbedBuilder.from(originalEmbed)
454
+ : new discord_js_1.EmbedBuilder().setTitle('File Changes');
455
+ const actionLabel = fileChangeAction.action === 'accept' ? 'Accept All' : 'Reject All';
456
+ const historyText = `${actionLabel} by <@${interaction.user.id}> (${new Date().toLocaleString('ja-JP')})`;
457
+ updatedEmbed
458
+ .setColor(clicked ? (fileChangeAction.action === 'accept' ? 0x2ECC71 : 0xE74C3C) : 0x95A5A6)
459
+ .addFields({ name: 'Action History', value: historyText, inline: false })
460
+ .setTimestamp();
461
+ await interaction.editReply({
462
+ embeds: [updatedEmbed],
463
+ components: (0, discordButtonUtils_1.disableAllButtons)(interaction.message.components),
464
+ });
465
+ if (clicked && deps.promptDispatcher && interaction.channelId && interaction.message) {
466
+ // Resume monitoring, treating the completion of file changes as a need to poll again
467
+ deps.promptDispatcher.resume({
468
+ message: interaction.message,
469
+ prompt: '',
470
+ cdp,
471
+ options: buildResumeOptions()
472
+ }).catch(e => logger_1.logger.error('[FileChange] Failed to resume monitoring:', e));
473
+ }
474
+ }
475
+ catch (err) {
476
+ if (err?.code === 10062 || err?.code === 40060) {
477
+ logger_1.logger.warn('[FileChange] Interaction expired. Responding directly in the channel.');
478
+ if (interaction.channel && 'send' in interaction.channel) {
479
+ const actionLabel = fileChangeAction.action === 'accept' ? 'Accept All' : 'Reject All';
480
+ const fallbackMessage = clicked
481
+ ? `${actionLabel} completed.`
482
+ : (0, i18n_1.t)('File change button not found or error occurred.');
483
+ await interaction.channel.send(fallbackMessage).catch(logger_1.logger.error);
484
+ }
485
+ }
486
+ else {
487
+ logger_1.logger.error('[FileChange] action error:', err);
488
+ await interaction.followUp({
489
+ content: `❌ Error executing file change action: ${err.message || 'Unknown error'}`,
490
+ flags: discord_js_1.MessageFlags.Ephemeral
491
+ }).catch(() => { });
492
+ }
493
+ }
494
+ return;
495
+ }
313
496
  const errorPopupAction = deps.parseErrorPopupCustomId(interaction.customId);
314
497
  if (errorPopupAction) {
315
498
  if (errorPopupAction.channelId && errorPopupAction.channelId !== interaction.channelId) {
@@ -319,7 +502,7 @@ function createInteractionCreateHandler(deps) {
319
502
  }).catch(logger_1.logger.error);
320
503
  return;
321
504
  }
322
- const errorWorkspaceDirName = errorPopupAction.projectName ?? deps.bridge.lastActiveWorkspace;
505
+ const errorWorkspaceDirName = errorPopupAction.projectName ?? resolveProjectFromChannel(interaction);
323
506
  const errorDetector = errorWorkspaceDirName
324
507
  ? deps.bridge.pool.getErrorPopupDetector(errorWorkspaceDirName, resolveSelectedAccount(interaction.channelId, interaction.user.id, getParentChannelId(interaction)))
325
508
  : undefined;
@@ -469,7 +652,7 @@ function createInteractionCreateHandler(deps) {
469
652
  }).catch(logger_1.logger.error);
470
653
  return;
471
654
  }
472
- const runCmdWorkspace = runCommandAction.projectName ?? deps.bridge.lastActiveWorkspace;
655
+ const runCmdWorkspace = runCommandAction.projectName ?? resolveProjectFromChannel(interaction);
473
656
  const runCmdDetector = runCmdWorkspace
474
657
  ? deps.bridge.pool.getRunCommandDetector(runCmdWorkspace, resolveSelectedAccount(interaction.channelId, interaction.user.id, getParentChannelId(interaction)))
475
658
  : undefined;
@@ -540,7 +723,7 @@ function createInteractionCreateHandler(deps) {
540
723
  }
541
724
  if (interaction.customId === 'model_set_default_btn') {
542
725
  await interaction.deferUpdate();
543
- const cdp = getChannelCdp(interaction.channelId, interaction.user.id);
726
+ const cdp = getChannelCdp(interaction);
544
727
  if (!cdp) {
545
728
  await interaction.followUp({ content: 'Not connected to CDP.', flags: discord_js_1.MessageFlags.Ephemeral });
546
729
  return;
@@ -560,7 +743,7 @@ function createInteractionCreateHandler(deps) {
560
743
  deps.userPrefRepo.setDefaultModel(interaction.user.id, currentModel);
561
744
  }
562
745
  await deps.sendModelsUI({ editReply: async (data) => await interaction.editReply(data) }, {
563
- getCurrentCdp: () => getChannelCdp(interaction.channelId, interaction.user.id),
746
+ getCurrentCdp: () => getChannelCdp(interaction),
564
747
  fetchQuota: async () => deps.bridge.quota.fetchQuota(),
565
748
  });
566
749
  await interaction.followUp({ content: `Default model set to **${currentModel}**.`, flags: discord_js_1.MessageFlags.Ephemeral });
@@ -568,7 +751,7 @@ function createInteractionCreateHandler(deps) {
568
751
  }
569
752
  if (interaction.customId === 'model_clear_default_btn') {
570
753
  await interaction.deferUpdate();
571
- const cdp = getChannelCdp(interaction.channelId, interaction.user.id);
754
+ const cdp = getChannelCdp(interaction);
572
755
  if (cdp) {
573
756
  const sessionReady = await ensureBoundSessionActive(interaction.channelId, interaction.user.id, cdp);
574
757
  if (!sessionReady.ok) {
@@ -581,7 +764,7 @@ function createInteractionCreateHandler(deps) {
581
764
  deps.userPrefRepo.setDefaultModel(interaction.user.id, null);
582
765
  }
583
766
  await deps.sendModelsUI({ editReply: async (data) => await interaction.editReply(data) }, {
584
- getCurrentCdp: () => getChannelCdp(interaction.channelId, interaction.user.id),
767
+ getCurrentCdp: () => getChannelCdp(interaction),
585
768
  fetchQuota: async () => deps.bridge.quota.fetchQuota(),
586
769
  });
587
770
  await interaction.followUp({ content: 'Default model cleared.', flags: discord_js_1.MessageFlags.Ephemeral });
@@ -589,7 +772,7 @@ function createInteractionCreateHandler(deps) {
589
772
  }
590
773
  if (interaction.customId === 'model_refresh_btn') {
591
774
  await interaction.deferUpdate();
592
- const cdp = getChannelCdp(interaction.channelId, interaction.user.id);
775
+ const cdp = getChannelCdp(interaction);
593
776
  if (cdp) {
594
777
  const sessionReady = await ensureBoundSessionActive(interaction.channelId, interaction.user.id, cdp);
595
778
  if (!sessionReady.ok) {
@@ -598,7 +781,7 @@ function createInteractionCreateHandler(deps) {
598
781
  }
599
782
  }
600
783
  await deps.sendModelsUI({ editReply: async (data) => await interaction.editReply(data) }, {
601
- getCurrentCdp: () => getChannelCdp(interaction.channelId, interaction.user.id),
784
+ getCurrentCdp: () => getChannelCdp(interaction),
602
785
  fetchQuota: async () => deps.bridge.quota.fetchQuota(),
603
786
  });
604
787
  return;
@@ -606,7 +789,7 @@ function createInteractionCreateHandler(deps) {
606
789
  if (interaction.customId.startsWith('model_btn_')) {
607
790
  await interaction.deferUpdate();
608
791
  const modelName = interaction.customId.replace('model_btn_', '');
609
- const cdp = getChannelCdp(interaction.channelId, interaction.user.id);
792
+ const cdp = getChannelCdp(interaction);
610
793
  if (!cdp) {
611
794
  await interaction.followUp({ content: 'Not connected to CDP.', flags: discord_js_1.MessageFlags.Ephemeral });
612
795
  return;
@@ -622,7 +805,7 @@ function createInteractionCreateHandler(deps) {
622
805
  }
623
806
  else {
624
807
  await deps.sendModelsUI({ editReply: async (data) => await interaction.editReply(data) }, {
625
- getCurrentCdp: () => getChannelCdp(interaction.channelId, interaction.user.id),
808
+ getCurrentCdp: () => getChannelCdp(interaction),
626
809
  fetchQuota: async () => deps.bridge.quota.fetchQuota(),
627
810
  });
628
811
  await interaction.followUp({ content: `Model changed to **${res.model}**!`, flags: discord_js_1.MessageFlags.Ephemeral });
@@ -688,6 +871,84 @@ function createInteractionCreateHandler(deps) {
688
871
  }, true, passedConvId);
689
872
  return;
690
873
  }
874
+ if (interaction.customId.startsWith('file_open:')) {
875
+ await interaction.deferUpdate().catch(() => { });
876
+ const hashId = interaction.customId.replace('file_open:', '');
877
+ const fileUrl = fileOpenCache_1.fileOpenCache.get(hashId);
878
+ if (!fileUrl) {
879
+ await interaction.followUp({
880
+ content: (0, i18n_1.t)('File URL not found in cache or expired.'),
881
+ flags: discord_js_1.MessageFlags.Ephemeral
882
+ }).catch(logger_1.logger.error);
883
+ return;
884
+ }
885
+ let resolvedPath = fileUrl;
886
+ if (fileUrl.startsWith('file:///')) {
887
+ let rawPath = fileUrl.replace(/^file:\/\/\//, '');
888
+ if (path_1.default.sep === '/' && !rawPath.startsWith('/')) {
889
+ rawPath = '/' + rawPath;
890
+ }
891
+ else if (process.platform === 'win32' && rawPath.startsWith('/')) {
892
+ rawPath = rawPath.substring(1);
893
+ }
894
+ resolvedPath = path_1.default.resolve(rawPath);
895
+ }
896
+ let workspacePath = undefined;
897
+ if (interaction.channelId) {
898
+ workspacePath = deps.wsHandler.getWorkspaceForChannel(interaction.channelId) ||
899
+ deps.chatSessionRepo?.findByChannelId(interaction.channelId)?.workspacePath;
900
+ }
901
+ if (!workspacePath) {
902
+ await interaction.followUp({
903
+ content: `❌ Error: Cannot open files without an active workspace.`,
904
+ flags: discord_js_1.MessageFlags.Ephemeral
905
+ }).catch(logger_1.logger.error);
906
+ return;
907
+ }
908
+ const relative = path_1.default.relative(workspacePath, resolvedPath);
909
+ if (relative.startsWith('..') || path_1.default.isAbsolute(relative)) {
910
+ await interaction.followUp({
911
+ content: `❌ Error: Cannot open files outside the workspace root.`,
912
+ flags: discord_js_1.MessageFlags.Ephemeral
913
+ }).catch(logger_1.logger.error);
914
+ return;
915
+ }
916
+ try {
917
+ const fileContent = fs_1.default.readFileSync(resolvedPath, 'utf8');
918
+ const MAX_DESC_LEN = 4096;
919
+ const truncated = fileContent.length > MAX_DESC_LEN
920
+ ? fileContent.substring(0, MAX_DESC_LEN - 15) + '\n\n(truncated)'
921
+ : fileContent;
922
+ const embed = new discord_js_1.EmbedBuilder()
923
+ .setTitle(`Opened: ${path_1.default.basename(resolvedPath)}`)
924
+ .setDescription(truncated)
925
+ .setColor(0x3498DB)
926
+ .setTimestamp();
927
+ (0, child_process_1.execFile)((0, pathUtils_1.getAntigravityCliPath)(), [resolvedPath], async (error) => {
928
+ if (error) {
929
+ logger_1.logger.error(`Failed to open file via CLI: ${error.message}`);
930
+ await interaction.followUp({
931
+ content: `❌ Error opening file via CLI: ${error.message}`,
932
+ flags: discord_js_1.MessageFlags.Ephemeral
933
+ }).catch(() => { });
934
+ }
935
+ else {
936
+ await interaction.followUp({
937
+ content: `✅ Opened file in IDE: **${path_1.default.basename(resolvedPath)}**`,
938
+ embeds: [embed],
939
+ flags: discord_js_1.MessageFlags.Ephemeral
940
+ }).catch(() => { });
941
+ }
942
+ });
943
+ }
944
+ catch (e) {
945
+ await interaction.followUp({
946
+ content: `❌ Error opening file: ${e.message}`,
947
+ flags: discord_js_1.MessageFlags.Ephemeral
948
+ }).catch(logger_1.logger.error);
949
+ }
950
+ return;
951
+ }
691
952
  }
692
953
  catch (error) {
693
954
  logger_1.logger.error('Error during button interaction handling:', error);
@@ -723,7 +984,7 @@ function createInteractionCreateHandler(deps) {
723
984
  try {
724
985
  const selectedMode = interaction.values[0];
725
986
  deps.modeService.setMode(selectedMode);
726
- const cdp = getChannelCdp(interaction.channelId, interaction.user.id);
987
+ const cdp = getChannelCdp(interaction);
727
988
  if (cdp) {
728
989
  const res = await cdp.setUiMode(selectedMode);
729
990
  if (!res.ok) {
@@ -739,6 +1000,9 @@ function createInteractionCreateHandler(deps) {
739
1000
  if (interaction.deferred || interaction.replied) {
740
1001
  await interaction.followUp({ content: 'An error occurred while changing the mode.', flags: discord_js_1.MessageFlags.Ephemeral }).catch(logger_1.logger.error);
741
1002
  }
1003
+ else {
1004
+ await interaction.reply({ content: 'An error occurred while changing the mode.', flags: discord_js_1.MessageFlags.Ephemeral }).catch(logger_1.logger.error);
1005
+ }
742
1006
  }
743
1007
  catch (e) {
744
1008
  logger_1.logger.error('Failed to send error message:', e);
@@ -866,10 +1130,10 @@ function createInteractionCreateHandler(deps) {
866
1130
  const artifactService = new artifactService_1.ArtifactService();
867
1131
  // Resolve the selected artifact by rescanning and matching the encoded value
868
1132
  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();
1133
+ const session = deps.chatSessionRepo?.findByChannelId(channelId);
1134
+ const sessionTitle = session?.displayName?.trim() ?? '';
1135
+ const workspaceDirName = (0, pathUtils_1.getWorkspaceDirName)(session);
1136
+ const conversationId = session?.conversationId || (sessionTitle ? artifactService.findConversationByTitle(sessionTitle, workspaceDirName) : null);
873
1137
  if (!conversationId) {
874
1138
  await interaction.editReply({ content: '📂 No artifacts found.', components: [], allowedMentions: { parse: [] } }).catch(logger_1.logger.error);
875
1139
  return;
@@ -975,6 +1239,40 @@ function createInteractionCreateHandler(deps) {
975
1239
  }
976
1240
  return;
977
1241
  }
1242
+ if (interaction.isStringSelectMenu() && interaction.customId.startsWith(notificationSender_1.QUESTION_SELECT_ACTION_PREFIX)) {
1243
+ try {
1244
+ const action = (0, questionSelectAction_1.createQuestionSelectAction)({ bridge: deps.bridge, wsHandler: deps.wsHandler });
1245
+ const wrapped = (0, wrappers_1.wrapDiscordSelect)(interaction);
1246
+ await action.execute(wrapped, interaction.values);
1247
+ }
1248
+ catch (err) {
1249
+ logger_1.logger.error('[QuestionSelectAction] Error executing action:', err);
1250
+ if (!interaction.replied && !interaction.deferred) {
1251
+ await interaction.reply({ content: 'An error occurred.', ephemeral: true }).catch(logger_1.logger.error);
1252
+ }
1253
+ else {
1254
+ await interaction.followUp({ content: 'An error occurred.', ephemeral: true }).catch(logger_1.logger.error);
1255
+ }
1256
+ }
1257
+ return;
1258
+ }
1259
+ if (interaction.isButton() && interaction.customId.startsWith(notificationSender_1.QUESTION_SKIP_ACTION_PREFIX)) {
1260
+ try {
1261
+ const action = (0, questionSkipAction_1.createQuestionSkipAction)({ bridge: deps.bridge, wsHandler: deps.wsHandler });
1262
+ const wrapped = (0, wrappers_1.wrapDiscordButton)(interaction);
1263
+ await action.execute(wrapped, {});
1264
+ }
1265
+ catch (err) {
1266
+ logger_1.logger.error('[QuestionSkipAction] Error executing action:', err);
1267
+ if (!interaction.replied && !interaction.deferred) {
1268
+ await interaction.reply({ content: 'An error occurred.', ephemeral: true }).catch(logger_1.logger.error);
1269
+ }
1270
+ else {
1271
+ await interaction.followUp({ content: 'An error occurred.', ephemeral: true }).catch(logger_1.logger.error);
1272
+ }
1273
+ }
1274
+ return;
1275
+ }
978
1276
  if (interaction.isStringSelectMenu() && (0, projectListUi_1.isProjectSelectId)(interaction.customId)) {
979
1277
  if (!deps.config.allowedUserIds.includes(interaction.user.id)) {
980
1278
  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 +1317,7 @@ function createInteractionCreateHandler(deps) {
1019
1317
  throw deferError;
1020
1318
  }
1021
1319
  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);
1320
+ 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
1321
  }
1024
1322
  catch (error) {
1025
1323
  logger_1.logger.error(`[SlashCommand] command=${commandInteraction.commandName} channel=${commandInteraction.channelId} ` +
@@ -16,6 +16,7 @@ function createMessageCreateHandler(deps) {
16
16
  const ensureErrorPopupDetector = deps.ensureErrorPopupDetector ?? cdpBridgeManager_1.ensureErrorPopupDetector;
17
17
  const ensurePlanningDetector = deps.ensurePlanningDetector ?? cdpBridgeManager_1.ensurePlanningDetector;
18
18
  const ensureRunCommandDetector = deps.ensureRunCommandDetector ?? cdpBridgeManager_1.ensureRunCommandDetector;
19
+ const ensureQuestionDetector = deps.ensureQuestionDetector ?? cdpBridgeManager_1.ensureQuestionDetector;
19
20
  const registerApprovalWorkspaceChannel = deps.registerApprovalWorkspaceChannel ?? cdpBridgeManager_1.registerApprovalWorkspaceChannel;
20
21
  const registerApprovalSessionChannel = deps.registerApprovalSessionChannel ?? cdpBridgeManager_1.registerApprovalSessionChannel;
21
22
  const downloadInboundImageAttachments = deps.downloadInboundImageAttachments ?? imageHandler_1.downloadInboundImageAttachments;
@@ -190,10 +191,57 @@ function createMessageCreateHandler(deps) {
190
191
  }
191
192
  return;
192
193
  }
193
- const hasImageAttachments = Array.from(message.attachments.values())
194
- .some((attachment) => isImageAttachment(attachment.contentType, attachment.name));
195
- if (message.content.trim() || hasImageAttachments) {
196
- const promptText = message.content.trim() || 'Please review the attached images and respond accordingly.';
194
+ const allAttachments = Array.from(message.attachments.values());
195
+ const hasImageAttachments = allAttachments.some((attachment) => isImageAttachment(attachment.contentType, attachment.name));
196
+ const MAX_TEXT_ATTACHMENT_SIZE = 50 * 1024; // 50KB
197
+ const textAttachments = allAttachments.filter((a) => {
198
+ if (isImageAttachment(a.contentType, a.name))
199
+ return false;
200
+ if (a.size > MAX_TEXT_ATTACHMENT_SIZE)
201
+ return false;
202
+ const isTextType = a.contentType?.startsWith('text/') || a.contentType?.startsWith('application/json') || a.contentType?.startsWith('application/javascript');
203
+ const hasTextExt = a.name?.match(/\.(txt|md|js|ts|py|json|html|css|csv|log|sh|yml|yaml|xml)$/i);
204
+ return isTextType || hasTextExt;
205
+ });
206
+ if (message.content.trim() || hasImageAttachments || textAttachments.length > 0) {
207
+ let promptText = message.content.trim() || 'Please review the attached content and respond accordingly.';
208
+ // Prepend reply context if replying to a message
209
+ if (message.reference && message.reference.messageId) {
210
+ try {
211
+ const repliedTo = await message.channel.messages.fetch(message.reference.messageId);
212
+ if (repliedTo) {
213
+ const cleanContent = (repliedTo.content || '(Attachment/Embed)').trim();
214
+ const truncated = cleanContent.length > 500 ? cleanContent.substring(0, 500) + '...' : cleanContent;
215
+ promptText = `[Replying to context: "${truncated}"]\n\n${promptText}`;
216
+ }
217
+ }
218
+ catch (e) {
219
+ logger_1.logger.warn('[MessageCreate] Failed to fetch replied-to message context:', e);
220
+ }
221
+ }
222
+ // Fetch and append text attachments in parallel with a small concurrency cap
223
+ const CONCURRENCY_LIMIT = 3;
224
+ for (let i = 0; i < textAttachments.length; i += CONCURRENCY_LIMIT) {
225
+ const chunk = textAttachments.slice(i, i + CONCURRENCY_LIMIT);
226
+ const results = await Promise.all(chunk.map(async (textAtt) => {
227
+ try {
228
+ const res = await fetch(textAtt.url);
229
+ if (res.ok) {
230
+ const content = await res.text();
231
+ return `\n\n[Attached File: ${textAtt.name}]\n\`\`\`\n${content}\n\`\`\``;
232
+ }
233
+ else {
234
+ logger_1.logger.warn(`[MessageCreate] Non-ok status fetching text attachment ${textAtt.name}: ${res.status}`);
235
+ return '';
236
+ }
237
+ }
238
+ catch (e) {
239
+ logger_1.logger.warn(`[MessageCreate] Failed to fetch text attachment ${textAtt.name}:`, e);
240
+ return '';
241
+ }
242
+ }));
243
+ promptText += results.join('');
244
+ }
197
245
  const inboundImages = await downloadInboundImageAttachments(message);
198
246
  if (hasImageAttachments && inboundImages.length === 0) {
199
247
  await message.reply('Failed to retrieve attached images. Please wait and try again.').catch(() => { });
@@ -253,6 +301,7 @@ function createMessageCreateHandler(deps) {
253
301
  ensureErrorPopupDetector(deps.bridge, cdp, projectName, selectedAccount);
254
302
  ensurePlanningDetector(deps.bridge, cdp, projectName, selectedAccount);
255
303
  ensureRunCommandDetector(deps.bridge, cdp, projectName, selectedAccount);
304
+ ensureQuestionDetector(deps.bridge, cdp, projectName, selectedAccount);
256
305
  let session = deps.chatSessionRepo.findByChannelId(message.channelId);
257
306
  const staleSessionAccount = session?.isRenamed
258
307
  && ((session.activeAccountName && session.activeAccountName !== selectedAccount)