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
package/dist/bot/index.js CHANGED
@@ -46,6 +46,9 @@ const logger_1 = require("../utils/logger");
46
46
  const logBuffer_1 = require("../utils/logBuffer");
47
47
  const discord_js_1 = require("discord.js");
48
48
  const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
49
+ const fs_1 = __importDefault(require("fs"));
50
+ const child_process_1 = require("child_process");
51
+ const path_1 = __importDefault(require("path"));
49
52
  const wrappers_1 = require("../platform/discord/wrappers");
50
53
  const config_1 = require("../utils/config");
51
54
  const slashCommandHandler_1 = require("../commands/slashCommandHandler");
@@ -72,10 +75,12 @@ const chatSessionService_1 = require("../services/chatSessionService");
72
75
  const responseMonitor_1 = require("../services/responseMonitor");
73
76
  const antigravityLauncher_1 = require("../services/antigravityLauncher");
74
77
  const pathUtils_1 = require("../utils/pathUtils");
78
+ const fileOpenCache_1 = require("../utils/fileOpenCache");
75
79
  const promptDispatcher_1 = require("../services/promptDispatcher");
76
80
  const cdpBridgeManager_1 = require("../services/cdpBridgeManager");
77
81
  const streamMessageFormatter_1 = require("../utils/streamMessageFormatter");
78
82
  const discordFormatter_1 = require("../utils/discordFormatter");
83
+ const discordResponseRenderer_1 = require("../platform/discord/discordResponseRenderer");
79
84
  const processLogBuffer_1 = require("../utils/processLogBuffer");
80
85
  const imageHandler_1 = require("../utils/imageHandler");
81
86
  const modeUi_1 = require("../ui/modeUi");
@@ -106,8 +111,11 @@ const planningButtonAction_1 = require("../handlers/planningButtonAction");
106
111
  const errorPopupButtonAction_1 = require("../handlers/errorPopupButtonAction");
107
112
  const runCommandButtonAction_1 = require("../handlers/runCommandButtonAction");
108
113
  const modelButtonAction_1 = require("../handlers/modelButtonAction");
109
- const autoAcceptButtonAction_1 = require("../handlers/autoAcceptButtonAction");
114
+ const questionSelectAction_1 = require("../handlers/questionSelectAction");
110
115
  const templateButtonAction_1 = require("../handlers/templateButtonAction");
116
+ const fileChangeButtonAction_1 = require("../handlers/fileChangeButtonAction");
117
+ const autoAcceptButtonAction_1 = require("../handlers/autoAcceptButtonAction");
118
+ const genericActionButtonAction_1 = require("../handlers/genericActionButtonAction");
111
119
  const modeSelectAction_1 = require("../handlers/modeSelectAction");
112
120
  const accountSelectAction_1 = require("../handlers/accountSelectAction");
113
121
  const telegramStartupTarget_1 = require("./telegramStartupTarget");
@@ -172,8 +180,9 @@ function createSerialTaskQueueForTest(queueName, traceId) {
172
180
  return queue;
173
181
  };
174
182
  }
183
+ // Shared fileOpenCache is imported from utils/fileOpenCache
175
184
  /**
176
- * Send a Discord message (prompt) to Antigravity, wait for the response, and relay it back to Discord
185
+ * Send a user's prompt to Antigravity and monitor generation.
177
186
  *
178
187
  * Message strategy:
179
188
  * - Send new messages per phase instead of editing, to preserve history
@@ -408,16 +417,20 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
408
417
  return;
409
418
  lastLiveResponseKey = renderKey;
410
419
  for (let i = 0; i < plainChunks.length; i++) {
420
+ const messageOpts = { content: plainChunks[i] };
421
+ if (i === plainChunks.length - 1 && opts?.components?.length) {
422
+ messageOpts.components = opts.components;
423
+ }
411
424
  if (!liveResponseMessages[i]) {
412
- liveResponseMessages[i] = await channel.send({ content: plainChunks[i] }).catch((error) => {
425
+ liveResponseMessages[i] = await channel.send(messageOpts).catch((error) => {
413
426
  logDeliveryError('liveResponse/plain/send', error);
414
427
  return null;
415
428
  });
416
429
  continue;
417
430
  }
418
- await liveResponseMessages[i].edit({ content: plainChunks[i] }).catch(async (error) => {
431
+ await liveResponseMessages[i].edit(messageOpts).catch(async (error) => {
419
432
  logDeliveryError('liveResponse/plain/edit', error);
420
- liveResponseMessages[i] = await channel.send({ content: plainChunks[i] }).catch((sendError) => {
433
+ liveResponseMessages[i] = await channel.send(messageOpts).catch((sendError) => {
421
434
  logDeliveryError('liveResponse/plain/resend', sendError);
422
435
  return null;
423
436
  });
@@ -445,15 +458,23 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
445
458
  .setFooter({ text: footerText })
446
459
  .setTimestamp();
447
460
  if (!liveResponseMessages[i]) {
448
- liveResponseMessages[i] = await channel.send({ embeds: [embed] }).catch((error) => {
461
+ const messageOpts = { embeds: [embed] };
462
+ if (i === descriptions.length - 1 && opts?.components?.length) {
463
+ messageOpts.components = opts.components;
464
+ }
465
+ liveResponseMessages[i] = await channel.send(messageOpts).catch((error) => {
449
466
  logDeliveryError('liveResponse/embed/send', error);
450
467
  return null;
451
468
  });
452
469
  continue;
453
470
  }
454
- await liveResponseMessages[i].edit({ embeds: [embed] }).catch(async (error) => {
471
+ const messageOpts = { embeds: [embed] };
472
+ if (i === descriptions.length - 1 && opts?.components?.length) {
473
+ messageOpts.components = opts.components;
474
+ }
475
+ await liveResponseMessages[i].edit(messageOpts).catch(async (error) => {
455
476
  logDeliveryError('liveResponse/embed/edit', error);
456
- liveResponseMessages[i] = await channel.send({ embeds: [embed] }).catch((sendError) => {
477
+ liveResponseMessages[i] = await channel.send(messageOpts).catch((sendError) => {
457
478
  logDeliveryError('liveResponse/embed/resend', sendError);
458
479
  return null;
459
480
  });
@@ -544,16 +565,18 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
544
565
  try {
545
566
  const baseline = await (0, responseMonitor_1.captureResponseMonitorBaseline)(cdp);
546
567
  logger_1.logger.prompt(prompt);
547
- let injectResult;
548
- if (inboundImages.length > 0) {
549
- injectResult = await cdp.injectMessageWithImageFiles(prompt, inboundImages.map((image) => image.localPath));
550
- if (!injectResult.ok) {
551
- await sendEmbed((0, i18n_1.t)('🖼️ Attached image fallback'), (0, i18n_1.t)('Failed to attach image directly, resending via URL reference.'), PHASE_COLORS.thinking);
552
- injectResult = await cdp.injectMessage((0, imageHandler_1.buildPromptWithAttachmentUrls)(prompt, inboundImages));
568
+ let injectResult = { ok: true };
569
+ if (!options?.resumeOnly) {
570
+ if (inboundImages.length > 0) {
571
+ injectResult = await cdp.injectMessageWithImageFiles(prompt, inboundImages.map((image) => image.localPath));
572
+ if (!injectResult.ok) {
573
+ await sendEmbed((0, i18n_1.t)('🖼️ Attached image fallback'), (0, i18n_1.t)('Failed to attach image directly, resending via URL reference.'), PHASE_COLORS.thinking);
574
+ injectResult = await cdp.injectMessage((0, imageHandler_1.buildPromptWithAttachmentUrls)(prompt, inboundImages));
575
+ }
576
+ }
577
+ else {
578
+ injectResult = await cdp.injectMessage(prompt);
553
579
  }
554
- }
555
- else {
556
- injectResult = await cdp.injectMessage(prompt);
557
580
  }
558
581
  if (!injectResult.ok) {
559
582
  isFinalized = true;
@@ -600,7 +623,7 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
600
623
  lastProgressText = separated.output;
601
624
  }
602
625
  },
603
- onComplete: async (finalText) => {
626
+ onComplete: async (finalText, citedFiles, fileChanges) => {
604
627
  isFinalized = true;
605
628
  try {
606
629
  // If the user explicitly pressed /stop, skip output display entirely
@@ -680,18 +703,9 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
680
703
  });
681
704
  liveResponseUpdateVersion += 1;
682
705
  const responseVersion = liveResponseUpdateVersion;
683
- if (finalOutputText && finalOutputText.trim().length > 0) {
684
- await upsertLiveResponseEmbeds(`${PHASE_ICONS.complete} Final Output`, finalOutputText, PHASE_COLORS.complete, (0, i18n_1.t)(`⏱️ Time: ${elapsed}s | Complete`), {
685
- source: 'complete',
686
- expectedVersion: responseVersion,
687
- });
688
- }
689
- else {
690
- await upsertLiveResponseEmbeds(`${PHASE_ICONS.complete} Complete`, (0, i18n_1.t)('Failed to extract response. Use `/screenshot` to verify.'), PHASE_COLORS.complete, (0, i18n_1.t)(`⏱️ Time: ${elapsed}s | Complete`), {
691
- source: 'complete',
692
- expectedVersion: responseVersion,
693
- });
694
- }
706
+ const components = [];
707
+ if (!citedFiles)
708
+ citedFiles = [];
695
709
  if (options && message.guild) {
696
710
  try {
697
711
  const sessionInfo = await options.chatSessionService.getCurrentSessionInfo(cdp);
@@ -711,7 +725,12 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
711
725
  }
712
726
  // Persist conversation_id for artifact picker resolution
713
727
  if (options.artifactService) {
714
- const convId = options.artifactService.findConversationByTitle(sessionInfo.title);
728
+ const session = options.chatSessionRepo.findByChannelId(message.channelId);
729
+ let workspaceDirName;
730
+ if (session && session.workspacePath) {
731
+ workspaceDirName = bridge.pool.extractProjectName(session.workspacePath);
732
+ }
733
+ const convId = options.artifactService.findConversationByTitle(sessionInfo.title, workspaceDirName);
715
734
  if (convId) {
716
735
  options.chatSessionRepo.setConversationId(message.channelId, convId);
717
736
  }
@@ -722,6 +741,112 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
722
741
  logger_1.logger.error('[Rename] Failed to get title from Antigravity and rename:', e);
723
742
  }
724
743
  }
744
+ if (citedFiles && citedFiles.length > 0) {
745
+ // Strip trailing punctuation and deduplicate
746
+ let uniqueFiles = Array.from(new Set(citedFiles.map(f => {
747
+ let clean = f.trim();
748
+ while (clean.match(/[.,;:!?]$/)) {
749
+ clean = clean.slice(0, -1);
750
+ }
751
+ return clean;
752
+ })));
753
+ // Filter out obviously bad paths
754
+ uniqueFiles = uniqueFiles.filter(f => f.length > 0 && f !== 'unknown');
755
+ const row = new discord_js_1.ActionRowBuilder();
756
+ // Max 5 buttons per row in Discord
757
+ for (let i = 0; i < Math.min(uniqueFiles.length, 5); i++) {
758
+ let fileUrl = uniqueFiles[i];
759
+ const wsPath = cdp.getCurrentWorkspacePath();
760
+ if (!wsPath) {
761
+ continue;
762
+ }
763
+ if (!fileUrl.startsWith('file:///') && !path_1.default.isAbsolute(fileUrl)) {
764
+ fileUrl = `file:///${path_1.default.resolve(wsPath, fileUrl).replace(/\\/g, '/')}`;
765
+ }
766
+ let fsPath = fileUrl.replace(/^file:\/\/\//, '');
767
+ if (path_1.default.sep === '/' && !fsPath.startsWith('/')) {
768
+ fsPath = '/' + fsPath;
769
+ }
770
+ else if (process.platform === 'win32' && fsPath.startsWith('/')) {
771
+ fsPath = fsPath.substring(1);
772
+ }
773
+ const relative = path_1.default.relative(wsPath, fsPath);
774
+ if (relative.startsWith('..') || path_1.default.isAbsolute(relative)) {
775
+ continue;
776
+ }
777
+ if (!fs_1.default.existsSync(fsPath)) {
778
+ continue;
779
+ }
780
+ let displayName = fileUrl;
781
+ if (displayName.startsWith('file:///')) {
782
+ const parts = displayName.split('/');
783
+ displayName = parts[parts.length - 1];
784
+ }
785
+ const hashId = Math.random().toString(36).substring(2, 10);
786
+ fileOpenCache_1.fileOpenCache.set(hashId, fileUrl);
787
+ row.addComponents(new discord_js_1.ButtonBuilder()
788
+ .setCustomId(`file_open:${hashId}`)
789
+ .setLabel(`Open ${displayName.substring(0, 20)}`)
790
+ .setStyle(discord_js_1.ButtonStyle.Primary));
791
+ }
792
+ if (row.components.length > 0) {
793
+ components.push(row);
794
+ }
795
+ }
796
+ if (fileChanges && fileChanges.length > 0) {
797
+ // The extracted text is the toolbar title, e.g. "1 File With Changes"
798
+ const widgetTitle = fileChanges[fileChanges.length - 1] || 'Files With Changes';
799
+ const fileChangesRow = new discord_js_1.ActionRowBuilder().addComponents(new discord_js_1.ButtonBuilder()
800
+ .setCustomId((0, cdpBridgeManager_1.buildFileChangeCustomId)('reject', cdp.getCurrentWorkspaceName() || 'unknown', message.channelId))
801
+ .setLabel(`Reject all (${widgetTitle})`)
802
+ .setStyle(discord_js_1.ButtonStyle.Danger), new discord_js_1.ButtonBuilder()
803
+ .setCustomId((0, cdpBridgeManager_1.buildFileChangeCustomId)('accept', cdp.getCurrentWorkspaceName() || 'unknown', message.channelId))
804
+ .setLabel('Accept all')
805
+ .setStyle(discord_js_1.ButtonStyle.Success));
806
+ components.push(fileChangesRow);
807
+ }
808
+ const classified = monitor.getLastClassified();
809
+ if (options?.extractionMode === 'structured' && classified && outputFormat !== 'plain') {
810
+ const messageOptions = (0, discordResponseRenderer_1.renderDiscordResponse)(classified, {
811
+ projectName: cdp.getCurrentWorkspaceName() || 'unknown',
812
+ channelId: message.channelId
813
+ });
814
+ for (const msg of liveResponseMessages) {
815
+ if (msg)
816
+ await msg.delete().catch(() => { });
817
+ }
818
+ liveResponseMessages.length = 0;
819
+ if (components.length > 0) {
820
+ if (!messageOptions.components)
821
+ messageOptions.components = [];
822
+ messageOptions.components = [...(messageOptions.components || []), ...components];
823
+ }
824
+ if (messageOptions.embeds && messageOptions.embeds.length > 0) {
825
+ const lastEmbed = messageOptions.embeds[messageOptions.embeds.length - 1];
826
+ if (lastEmbed && typeof lastEmbed.setFooter === 'function') {
827
+ lastEmbed.setFooter({ text: (0, i18n_1.t)(`⏱️ Time: ${elapsed}s | Complete`) });
828
+ }
829
+ }
830
+ if (channel) {
831
+ await channel.send(messageOptions).catch((error) => {
832
+ logDeliveryError('renderDiscordResponse/send', error);
833
+ });
834
+ }
835
+ }
836
+ else if (finalOutputText && finalOutputText.trim().length > 0) {
837
+ await upsertLiveResponseEmbeds(`${PHASE_ICONS.complete} Final Output`, finalOutputText, PHASE_COLORS.complete, (0, i18n_1.t)(`⏱️ Time: ${elapsed}s | Complete`), {
838
+ source: 'complete',
839
+ expectedVersion: responseVersion,
840
+ components: components.length > 0 ? components : undefined,
841
+ });
842
+ }
843
+ else {
844
+ await upsertLiveResponseEmbeds(`${PHASE_ICONS.complete} Complete`, (0, i18n_1.t)('Failed to extract response. Use `/screenshot` to verify.'), PHASE_COLORS.complete, (0, i18n_1.t)(`⏱️ Time: ${elapsed}s | Complete`), {
845
+ source: 'complete',
846
+ expectedVersion: responseVersion,
847
+ components: components.length > 0 ? components : undefined,
848
+ });
849
+ }
725
850
  await sendGeneratedImages(finalOutputText || '');
726
851
  await clearWatchingReaction();
727
852
  await message.react(finalOutputText && finalOutputText.trim().length > 0 ? '✅' : '⚠️').catch(() => { });
@@ -977,6 +1102,7 @@ const startBot = async (cliLogLevel) => {
977
1102
  wsHandler,
978
1103
  chatHandler,
979
1104
  client,
1105
+ promptDispatcher,
980
1106
  sendModeUI: modeUi_1.sendModeUI,
981
1107
  sendModelsUI: modelsUi_1.sendModelsUI,
982
1108
  sendAutoAcceptUI: autoAcceptUi_1.sendAutoAcceptUI,
@@ -984,6 +1110,7 @@ const startBot = async (cliLogLevel) => {
984
1110
  parseApprovalCustomId: cdpBridgeManager_1.parseApprovalCustomId,
985
1111
  parseErrorPopupCustomId: cdpBridgeManager_1.parseErrorPopupCustomId,
986
1112
  parsePlanningCustomId: cdpBridgeManager_1.parsePlanningCustomId,
1113
+ parseFileChangeCustomId: cdpBridgeManager_1.parseFileChangeCustomId,
987
1114
  parseRunCommandCustomId: cdpBridgeManager_1.parseRunCommandCustomId,
988
1115
  joinHandler,
989
1116
  userPrefRepo,
@@ -993,8 +1120,10 @@ const startBot = async (cliLogLevel) => {
993
1120
  chatSessionService,
994
1121
  artifactThreadRepo,
995
1122
  artifactService,
1123
+ channelManager,
1124
+ titleGenerator,
996
1125
  antigravityAccounts: config.antigravityAccounts,
997
- handleSlashInteraction: async (interaction, handler, bridgeArg, wsHandlerArg, chatHandlerArg, cleanupHandlerArg, modeServiceArg, modelServiceArg, autoAcceptServiceArg, clientArg, accountPrefRepoArg, channelPrefRepoArg, antigravityAccountsArg) => handleSlashInteraction(interaction, handler, bridgeArg, wsHandlerArg, chatHandlerArg, cleanupHandlerArg, chatSessionService, modeServiceArg, modelServiceArg, autoAcceptServiceArg, clientArg, promptDispatcher, templateRepo, joinHandler, userPrefRepo, accountPrefRepoArg, channelPrefRepoArg, antigravityAccountsArg, chatSessionRepo, artifactService),
1126
+ handleSlashInteraction: async (interaction, handler, bridgeArg, wsHandlerArg, chatHandlerArg, cleanupHandlerArg, modeServiceArg, modelServiceArg, autoAcceptServiceArg, clientArg, accountPrefRepoArg, channelPrefRepoArg, antigravityAccountsArg, chatSessionRepoArg, scheduleServiceArg) => handleSlashInteraction(interaction, handler, bridgeArg, wsHandlerArg, chatHandlerArg, cleanupHandlerArg, chatSessionService, modeServiceArg, modelServiceArg, autoAcceptServiceArg, clientArg, promptDispatcher, templateRepo, joinHandler, userPrefRepo, accountPrefRepoArg, channelPrefRepoArg, antigravityAccountsArg, chatSessionRepoArg, artifactService, scheduleServiceArg),
998
1127
  handleTemplateUse: async (interaction, templateId) => {
999
1128
  const template = templateRepo.findById(templateId);
1000
1129
  if (!template) {
@@ -1035,6 +1164,7 @@ const startBot = async (cliLogLevel) => {
1035
1164
  (0, cdpBridgeManager_1.ensureErrorPopupDetector)(bridge, cdp, projectName, selectedAccount);
1036
1165
  (0, cdpBridgeManager_1.ensurePlanningDetector)(bridge, cdp, projectName, selectedAccount);
1037
1166
  (0, cdpBridgeManager_1.ensureRunCommandDetector)(bridge, cdp, projectName, selectedAccount);
1167
+ (0, cdpBridgeManager_1.ensureQuestionDetector)(bridge, cdp, projectName, selectedAccount);
1038
1168
  }
1039
1169
  catch (e) {
1040
1170
  await interaction.followUp({
@@ -1184,10 +1314,12 @@ const startBot = async (cliLogLevel) => {
1184
1314
  : binding.workspacePath;
1185
1315
  },
1186
1316
  });
1317
+ const questionSelectAction = (0, questionSelectAction_1.createQuestionSelectAction)({ bridge, wsHandler });
1187
1318
  const telegramSelectHandler = (0, selectHandler_1.createPlatformSelectHandler)({
1188
1319
  actions: [
1189
1320
  modeSelectAction,
1190
1321
  accountSelectAction,
1322
+ questionSelectAction,
1191
1323
  ],
1192
1324
  });
1193
1325
  // Composite handler that routes to the right handler
@@ -1202,10 +1334,13 @@ const startBot = async (cliLogLevel) => {
1202
1334
  accountPrefRepo,
1203
1335
  channelPrefRepo,
1204
1336
  antigravityAccounts: config.antigravityAccounts,
1337
+ activeMonitors,
1205
1338
  }, interaction);
1206
1339
  return;
1207
1340
  }
1208
- if (interaction.customId === 'mode_select' || interaction.customId === 'account_select') {
1341
+ if (interaction.customId === 'mode_select' ||
1342
+ interaction.customId === 'account_select' ||
1343
+ interaction.customId.startsWith('question_select_action')) {
1209
1344
  await telegramSelectHandler(interaction);
1210
1345
  return;
1211
1346
  }
@@ -1220,10 +1355,10 @@ const startBot = async (cliLogLevel) => {
1220
1355
  }
1221
1356
  const telegramButtonHandler = (0, buttonHandler_1.createPlatformButtonHandler)({
1222
1357
  actions: [
1223
- (0, approvalButtonAction_1.createApprovalButtonAction)({ bridge }),
1224
- (0, planningButtonAction_1.createPlanningButtonAction)({ bridge }),
1225
- (0, errorPopupButtonAction_1.createErrorPopupButtonAction)({ bridge }),
1226
- (0, runCommandButtonAction_1.createRunCommandButtonAction)({ bridge }),
1358
+ (0, approvalButtonAction_1.createApprovalButtonAction)({ bridge, wsHandler }),
1359
+ (0, planningButtonAction_1.createPlanningButtonAction)({ bridge, wsHandler }),
1360
+ (0, errorPopupButtonAction_1.createErrorPopupButtonAction)({ bridge, wsHandler }),
1361
+ (0, runCommandButtonAction_1.createRunCommandButtonAction)({ bridge, wsHandler }),
1227
1362
  (0, modelButtonAction_1.createModelButtonAction)({
1228
1363
  bridge,
1229
1364
  fetchQuota: () => bridge.quota.fetchQuota(),
@@ -1261,6 +1396,8 @@ const startBot = async (cliLogLevel) => {
1261
1396
  }),
1262
1397
  (0, autoAcceptButtonAction_1.createAutoAcceptButtonAction)({ autoAcceptService: bridge.autoAccept }),
1263
1398
  (0, templateButtonAction_1.createTemplateButtonAction)({ bridge, templateRepo }),
1399
+ (0, fileChangeButtonAction_1.createFileChangeButtonAction)({ bridge, wsHandler }),
1400
+ (0, genericActionButtonAction_1.createGenericActionButtonAction)({ bridge, wsHandler }),
1264
1401
  ],
1265
1402
  });
1266
1403
  const eventRouter = new eventRouter_1.EventRouter({ allowedUsers }, {
@@ -1381,7 +1518,7 @@ async function autoRenameChannel(message, chatSessionRepo, titleGenerator, chann
1381
1518
  /**
1382
1519
  * Handle Discord Interactions API slash commands
1383
1520
  */
1384
- async function handleSlashInteraction(interaction, handler, bridge, wsHandler, chatHandler, cleanupHandler, chatSessionService, modeService, modelService, autoAcceptService, _client, promptDispatcher, templateRepo, joinHandler, userPrefRepo, accountPrefRepo, channelPrefRepo, antigravityAccounts = [{ name: 'default', cdpPort: 9222 }], chatSessionRepo, artifactService) {
1521
+ async function handleSlashInteraction(interaction, handler, bridge, wsHandler, chatHandler, cleanupHandler, chatSessionService, modeService, modelService, autoAcceptService, _client, promptDispatcher, templateRepo, joinHandler, userPrefRepo, accountPrefRepo, channelPrefRepo, antigravityAccounts = [{ name: 'default', cdpPort: 9222 }], chatSessionRepo, artifactService, scheduleService) {
1385
1522
  const commandName = interaction.commandName;
1386
1523
  const getAccountPort = (accountName) => {
1387
1524
  const match = antigravityAccounts.find((account) => account.name === accountName);
@@ -1406,9 +1543,7 @@ async function handleSlashInteraction(interaction, handler, bridge, wsHandler, c
1406
1543
  const projectName = bridge.pool.extractProjectName(workspacePath);
1407
1544
  return bridge.pool.getConnected(projectName, resolveSelectedAccount());
1408
1545
  }
1409
- return bridge.lastActiveWorkspace
1410
- ? bridge.pool.getConnected(bridge.lastActiveWorkspace, resolveSelectedAccount())
1411
- : null;
1546
+ return null;
1412
1547
  })();
1413
1548
  const ensureChannelCdp = async () => {
1414
1549
  const existing = getChannelCdp();
@@ -1879,6 +2014,63 @@ async function handleSlashInteraction(interaction, handler, bridge, wsHandler, c
1879
2014
  await interaction.editReply({ content: codeBlock });
1880
2015
  break;
1881
2016
  }
2017
+ case 'open': {
2018
+ const filepath = interaction.options.getString('filepath', true);
2019
+ let resolvedPath = null;
2020
+ // 1. Try to resolve as an artifact
2021
+ if (chatSessionRepo && artifactService) {
2022
+ const session = chatSessionRepo.findByChannelId(interaction.channelId);
2023
+ if (session && session.conversationId) {
2024
+ const possibleArtifact = artifactService.getArtifactPath(session.conversationId, filepath);
2025
+ if (fs_1.default.existsSync(possibleArtifact)) {
2026
+ resolvedPath = possibleArtifact;
2027
+ }
2028
+ }
2029
+ }
2030
+ // 2. Try to resolve against the workspace
2031
+ if (!resolvedPath) {
2032
+ const cdp = await ensureChannelCdp();
2033
+ if (cdp) {
2034
+ const wsPath = cdp.getCurrentWorkspacePath();
2035
+ if (wsPath) {
2036
+ let rawPath = filepath;
2037
+ if (filepath.startsWith('file:///')) {
2038
+ rawPath = filepath.replace('file:///', '');
2039
+ if (process.platform === 'win32' && rawPath.startsWith('/')) {
2040
+ rawPath = rawPath.substring(1);
2041
+ }
2042
+ }
2043
+ const candidatePath = path_1.default.isAbsolute(rawPath) ? rawPath : path_1.default.join(wsPath, rawPath);
2044
+ const relative = path_1.default.relative(wsPath, candidatePath);
2045
+ if (!relative.startsWith('..') && !path_1.default.isAbsolute(relative)) {
2046
+ resolvedPath = candidatePath;
2047
+ }
2048
+ }
2049
+ }
2050
+ }
2051
+ if (!resolvedPath) {
2052
+ await interaction.editReply({
2053
+ content: '❌ Error: Cannot resolve file path. Ensure the file exists as an artifact or within the current workspace.'
2054
+ });
2055
+ break;
2056
+ }
2057
+ try {
2058
+ (0, child_process_1.execFile)((0, pathUtils_1.getAntigravityCliPath)(), [resolvedPath], (error) => {
2059
+ if (error) {
2060
+ logger_1.logger.error(`Failed to open file via CLI: ${error.message}`);
2061
+ interaction.editReply({ content: `❌ Error opening file via CLI.` }).catch(() => { });
2062
+ }
2063
+ else {
2064
+ interaction.editReply({ content: `✅ Opened file: **${path_1.default.basename(resolvedPath)}**` }).catch(() => { });
2065
+ }
2066
+ });
2067
+ }
2068
+ catch (e) {
2069
+ logger_1.logger.error(`Failed to open file: ${e.message}`);
2070
+ await interaction.editReply({ content: `❌ Error opening file.` });
2071
+ }
2072
+ break;
2073
+ }
1882
2074
  case 'artifacts': {
1883
2075
  await (0, artifactsUi_1.sendArtifactPickerUI)(interaction, {
1884
2076
  userPrefRepo,
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.handleJoin = handleJoin;
4
4
  exports.handleTelegramJoinSelect = handleTelegramJoinSelect;
5
5
  exports.handleMirror = handleMirror;
6
+ exports.startResponseMirror = startResponseMirror;
6
7
  const cdpBridgeManager_1 = require("../services/cdpBridgeManager");
7
8
  const responseMonitor_1 = require("../services/responseMonitor");
8
9
  const sessionPickerUi_1 = require("../ui/sessionPickerUi");
@@ -137,6 +138,14 @@ async function routeMirroredMessage(deps, cdp, workspacePath, info, channel) {
137
138
  startResponseMirror(deps, cdp, workspacePath, channel, chatTitle || 'Unknown');
138
139
  }
139
140
  function startResponseMirror(deps, cdp, workspacePath, channel, chatTitle) {
141
+ // If the primary message handler already has an active monitor for this workspace,
142
+ // skip the passive mirror — the primary handler delivers the response itself, and
143
+ // running both would send the same AI response twice.
144
+ const projectName = deps.bridge.pool.extractProjectName(workspacePath);
145
+ if (deps.activeMonitors?.has(projectName)) {
146
+ logger_1.logger.debug(`[TelegramMirror] Skipping passive monitor — primary monitor active for ${projectName}`);
147
+ return;
148
+ }
140
149
  const prev = activeResponseMonitors.get(workspacePath);
141
150
  if (prev?.isActive()) {
142
151
  prev.stop().catch(() => { });
@@ -152,6 +152,7 @@ function createTelegramMessageHandler(deps) {
152
152
  (0, cdpBridgeManager_1.ensureErrorPopupDetector)(deps.bridge, cdp, projectName, selectedAccount);
153
153
  (0, cdpBridgeManager_1.ensurePlanningDetector)(deps.bridge, cdp, projectName, selectedAccount);
154
154
  (0, cdpBridgeManager_1.ensureRunCommandDetector)(deps.bridge, cdp, projectName, selectedAccount);
155
+ (0, cdpBridgeManager_1.ensureQuestionDetector)(deps.bridge, cdp, projectName, selectedAccount);
155
156
  // Acknowledge receipt
156
157
  await message.react('\u{1F440}').catch(() => { });
157
158
  // Download image attachments if present
@@ -173,6 +174,12 @@ function createTelegramMessageHandler(deps) {
173
174
  // Determine the prompt text — use default for image-only messages
174
175
  const effectivePrompt = promptText || 'Please review the attached images and respond accordingly.';
175
176
  const baseline = await (0, responseMonitor_1.captureResponseMonitorBaseline)(cdp);
177
+ // Register the echo hash BEFORE injecting (the Discord path registers it later):
178
+ // UserMessageDetector polls the DOM every ~2s, so registering only after
179
+ // injectMessage() resolves leaves a window where the injected prompt is detected
180
+ // as PC-side input and mirrored back to Telegram. If injection fails, the stale
181
+ // hash is harmless — it expires via its 60s TTL.
182
+ deps.bridge.pool.getUserMessageDetector?.(projectName, selectedAccount)?.addEchoHash(effectivePrompt);
176
183
  // Inject prompt (with or without images) into Antigravity
177
184
  logger_1.logger.prompt(effectivePrompt);
178
185
  let injectResult;
@@ -1,4 +1,37 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  Object.defineProperty(exports, "__esModule", { value: true });
3
36
  exports.ChatCommandHandler = void 0;
4
37
  const i18n_1 = require("../utils/i18n");
@@ -73,9 +106,33 @@ class ChatCommandHandler {
73
106
  });
74
107
  return;
75
108
  }
109
+ // Start a new chat in the IDE
110
+ const newChatResult = await this.chatSessionService.startNewChat(workspaceCdp);
111
+ if (!newChatResult.ok) {
112
+ // Log but don't fail the command, as the channel still needs to be created
113
+ // and the user might just have to click it manually if the IDE state is strange.
114
+ Promise.resolve().then(() => __importStar(require('../utils/logger'))).then(({ logger }) => {
115
+ logger.warn(`[/new] Could not start new chat in IDE automatically: ${newChatResult.error}`);
116
+ });
117
+ }
118
+ const customNameRaw = interaction.options.getString('name');
119
+ let safeCustomName = null;
120
+ if (customNameRaw) {
121
+ // Import TitleGeneratorService to sanitize the channel name if needed,
122
+ // or just do basic sanitization inline.
123
+ safeCustomName = customNameRaw
124
+ .toLowerCase()
125
+ .replace(/\s+/g, '-')
126
+ .replace(/[^a-z0-9\-_\u3000-\u303f\u3040-\u309f\u30a0-\u30ff\uff00-\uff9f\u4e00-\u9faf]/g, '-')
127
+ .replace(/-{2,}/g, '-')
128
+ .replace(/^-+|-+$/g, '')
129
+ .substring(0, 80);
130
+ if (!safeCustomName)
131
+ safeCustomName = null;
132
+ }
76
133
  // Create a new session channel
77
134
  const sessionNumber = this.chatSessionRepo.getNextSessionNumber(parentId);
78
- const channelName = `session-${sessionNumber}`;
135
+ const channelName = safeCustomName ? `${sessionNumber}-${safeCustomName}` : `session-${sessionNumber}`;
79
136
  const sessionResult = await this.channelManager.createSessionChannel(guild, parentId, channelName);
80
137
  const newChannelId = sessionResult.channelId;
81
138
  // Register binding and session
@@ -92,6 +149,19 @@ class ChatCommandHandler {
92
149
  activeAccountName: selectedAccount,
93
150
  guildId: guild.id,
94
151
  });
152
+ if (safeCustomName) {
153
+ // Set the display name and mark it as renamed so autoRenameChannel skips it
154
+ this.chatSessionRepo.updateDisplayName(newChannelId, safeCustomName);
155
+ // Try to make the IDE's DOM match the Discord channel
156
+ if (workspaceCdp) {
157
+ const renameResult = await this.chatSessionService.renameCurrentChatInUI(workspaceCdp, safeCustomName);
158
+ if (!renameResult.ok) {
159
+ Promise.resolve().then(() => __importStar(require('../utils/logger'))).then(({ logger }) => {
160
+ logger.warn(`[/new] Could not rename chat in IDE automatically: ${renameResult.error}`);
161
+ });
162
+ }
163
+ }
164
+ }
95
165
  const embed = new discord_js_1.EmbedBuilder()
96
166
  .setTitle((0, i18n_1.t)('💬 Started a new session'))
97
167
  .setDescription((0, i18n_1.t)(`Created a new chat session\n→ <#${newChannelId}>`))
@@ -95,7 +95,10 @@ const projectCommand = new discord_js_1.SlashCommandBuilder()
95
95
  /** /new command definition (formerly /chat new, made into a standalone command) */
96
96
  const newCommand = new discord_js_1.SlashCommandBuilder()
97
97
  .setName('new')
98
- .setDescription((0, i18n_1.t)('Start a new chat session in the current project'));
98
+ .setDescription((0, i18n_1.t)('Start a new chat session in the current project'))
99
+ .addStringOption(option => option.setName('name')
100
+ .setDescription((0, i18n_1.t)('Name of the new chat session'))
101
+ .setRequired(false));
99
102
  /** /chat command definition (merged status + list) */
100
103
  const chatCommand = new discord_js_1.SlashCommandBuilder()
101
104
  .setName('chat')
@@ -157,6 +160,14 @@ const pingCommand = new discord_js_1.SlashCommandBuilder()
157
160
  const artifactsCommand = new discord_js_1.SlashCommandBuilder()
158
161
  .setName('artifacts')
159
162
  .setDescription((0, i18n_1.t)('Browse and view generated artifacts from the active session'));
163
+ /** /open command definition */
164
+ const openCommand = new discord_js_1.SlashCommandBuilder()
165
+ .setName('open')
166
+ .setDescription((0, i18n_1.t)('Open and read a file from the workspace'))
167
+ .addStringOption((option) => option
168
+ .setName('filepath')
169
+ .setDescription((0, i18n_1.t)('Absolute or relative path to the file'))
170
+ .setRequired(true));
160
171
  /** Array of commands to register */
161
172
  exports.slashCommands = [
162
173
  helpCommand,
@@ -179,6 +190,7 @@ exports.slashCommands = [
179
190
  pingCommand,
180
191
  logsCommand,
181
192
  artifactsCommand,
193
+ openCommand,
182
194
  ];
183
195
  /**
184
196
  * Register slash commands with Discord