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
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
@@ -182,11 +191,15 @@ function createSerialTaskQueueForTest(queueName, traceId) {
182
191
  async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService, modelService, inboundImages = [], options) {
183
192
  // Completion signal — called exactly once when the entire prompt lifecycle ends
184
193
  let completionSignaled = false;
194
+ let onApprovalRef = null;
185
195
  const signalCompletion = (exitPath) => {
186
196
  if (completionSignaled)
187
197
  return;
188
198
  completionSignaled = true;
189
199
  logger_1.logger.debug(`[sendPrompt:${message.channelId}] signalCompletion via ${exitPath}`);
200
+ if (onApprovalRef) {
201
+ cdp.off('approval_required', onApprovalRef);
202
+ }
190
203
  options?.onFullCompletion?.();
191
204
  };
192
205
  // Resolve output format once at the start (no mid-response switches)
@@ -408,16 +421,20 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
408
421
  return;
409
422
  lastLiveResponseKey = renderKey;
410
423
  for (let i = 0; i < plainChunks.length; i++) {
424
+ const messageOpts = { content: plainChunks[i] };
425
+ if (i === plainChunks.length - 1 && opts?.components?.length) {
426
+ messageOpts.components = opts.components;
427
+ }
411
428
  if (!liveResponseMessages[i]) {
412
- liveResponseMessages[i] = await channel.send({ content: plainChunks[i] }).catch((error) => {
429
+ liveResponseMessages[i] = await channel.send(messageOpts).catch((error) => {
413
430
  logDeliveryError('liveResponse/plain/send', error);
414
431
  return null;
415
432
  });
416
433
  continue;
417
434
  }
418
- await liveResponseMessages[i].edit({ content: plainChunks[i] }).catch(async (error) => {
435
+ await liveResponseMessages[i].edit(messageOpts).catch(async (error) => {
419
436
  logDeliveryError('liveResponse/plain/edit', error);
420
- liveResponseMessages[i] = await channel.send({ content: plainChunks[i] }).catch((sendError) => {
437
+ liveResponseMessages[i] = await channel.send(messageOpts).catch((sendError) => {
421
438
  logDeliveryError('liveResponse/plain/resend', sendError);
422
439
  return null;
423
440
  });
@@ -445,15 +462,23 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
445
462
  .setFooter({ text: footerText })
446
463
  .setTimestamp();
447
464
  if (!liveResponseMessages[i]) {
448
- liveResponseMessages[i] = await channel.send({ embeds: [embed] }).catch((error) => {
465
+ const messageOpts = { embeds: [embed] };
466
+ if (i === descriptions.length - 1 && opts?.components?.length) {
467
+ messageOpts.components = opts.components;
468
+ }
469
+ liveResponseMessages[i] = await channel.send(messageOpts).catch((error) => {
449
470
  logDeliveryError('liveResponse/embed/send', error);
450
471
  return null;
451
472
  });
452
473
  continue;
453
474
  }
454
- await liveResponseMessages[i].edit({ embeds: [embed] }).catch(async (error) => {
475
+ const messageOpts = { embeds: [embed] };
476
+ if (i === descriptions.length - 1 && opts?.components?.length) {
477
+ messageOpts.components = opts.components;
478
+ }
479
+ await liveResponseMessages[i].edit(messageOpts).catch(async (error) => {
455
480
  logDeliveryError('liveResponse/embed/edit', error);
456
- liveResponseMessages[i] = await channel.send({ embeds: [embed] }).catch((sendError) => {
481
+ liveResponseMessages[i] = await channel.send(messageOpts).catch((sendError) => {
457
482
  logDeliveryError('liveResponse/embed/resend', sendError);
458
483
  return null;
459
484
  });
@@ -544,16 +569,18 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
544
569
  try {
545
570
  const baseline = await (0, responseMonitor_1.captureResponseMonitorBaseline)(cdp);
546
571
  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));
572
+ let injectResult = { ok: true };
573
+ if (!options?.resumeOnly) {
574
+ if (inboundImages.length > 0) {
575
+ injectResult = await cdp.injectMessageWithImageFiles(prompt, inboundImages.map((image) => image.localPath));
576
+ if (!injectResult.ok) {
577
+ 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);
578
+ injectResult = await cdp.injectMessage((0, imageHandler_1.buildPromptWithAttachmentUrls)(prompt, inboundImages));
579
+ }
580
+ }
581
+ else {
582
+ injectResult = await cdp.injectMessage(prompt);
553
583
  }
554
- }
555
- else {
556
- injectResult = await cdp.injectMessage(prompt);
557
584
  }
558
585
  if (!injectResult.ok) {
559
586
  isFinalized = true;
@@ -564,6 +591,8 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
564
591
  return;
565
592
  }
566
593
  const startTime = Date.now();
594
+ let earlyConvIdResolved = false;
595
+ let earlyConvIdInFlight = false;
567
596
  await upsertLiveActivityEmbeds(`${PHASE_ICONS.thinking} Process Log`, '', PHASE_COLORS.thinking, (0, i18n_1.t)('⏱️ Elapsed: 0s | Process log'), { source: 'initial' });
568
597
  const monitor = new responseMonitor_1.ResponseMonitor({
569
598
  cdpService: cdp,
@@ -590,6 +619,30 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
590
619
  expectedVersion: activityVersion,
591
620
  skipWhenFinalized: true,
592
621
  }).catch(() => { });
622
+ // Try to resolve conversation_id early if not already bound
623
+ const { artifactService, chatSessionRepo, chatSessionService } = options || {};
624
+ if (!earlyConvIdResolved && !earlyConvIdInFlight && artifactService && chatSessionRepo && chatSessionService) {
625
+ earlyConvIdInFlight = true;
626
+ chatSessionService.getCurrentSessionInfo(cdp)
627
+ .then((sessionInfo) => {
628
+ if (sessionInfo && sessionInfo.title && sessionInfo.title !== (0, i18n_1.t)('(Untitled)')) {
629
+ const session = chatSessionRepo.findByChannelId(message.channelId);
630
+ const workspaceDirName = (session
631
+ ? bridge.pool.extractProjectName(session.workspacePath)
632
+ : cdp.getCurrentWorkspaceName()) ?? undefined;
633
+ const convId = artifactService.findConversationByTitle(sessionInfo.title, workspaceDirName);
634
+ if (convId) {
635
+ chatSessionRepo.setConversationId(message.channelId, convId);
636
+ earlyConvIdResolved = true;
637
+ logger_1.logger.debug(`[EarlyBinding] Saved conversation_id: "${convId}" for channel: ${message.channelId}`);
638
+ }
639
+ }
640
+ })
641
+ .catch(() => { })
642
+ .finally(() => {
643
+ earlyConvIdInFlight = false;
644
+ });
645
+ }
593
646
  },
594
647
  onProgress: (text) => {
595
648
  if (isFinalized)
@@ -600,7 +653,7 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
600
653
  lastProgressText = separated.output;
601
654
  }
602
655
  },
603
- onComplete: async (finalText) => {
656
+ onComplete: async (finalText, citedFiles, fileChanges) => {
604
657
  isFinalized = true;
605
658
  try {
606
659
  // If the user explicitly pressed /stop, skip output display entirely
@@ -680,18 +733,15 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
680
733
  });
681
734
  liveResponseUpdateVersion += 1;
682
735
  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
- });
736
+ const components = [];
737
+ if (!citedFiles)
738
+ citedFiles = [];
739
+ // Only pause when newly created conversation folders and files need to flush to disk
740
+ const existingConvId = options?.chatSessionRepo?.findByChannelId(message.channelId)?.conversationId;
741
+ if (!existingConvId) {
742
+ await new Promise((resolve) => setTimeout(resolve, 1000));
694
743
  }
744
+ let activeConversationId = undefined;
695
745
  if (options && message.guild) {
696
746
  try {
697
747
  const sessionInfo = await options.chatSessionService.getCurrentSessionInfo(cdp);
@@ -711,9 +761,15 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
711
761
  }
712
762
  // Persist conversation_id for artifact picker resolution
713
763
  if (options.artifactService) {
714
- const convId = options.artifactService.findConversationByTitle(sessionInfo.title);
764
+ const session = options.chatSessionRepo.findByChannelId(message.channelId);
765
+ let workspaceDirName;
766
+ if (session && session.workspacePath) {
767
+ workspaceDirName = bridge.pool.extractProjectName(session.workspacePath);
768
+ }
769
+ const convId = options.artifactService.findConversationByTitle(sessionInfo.title, workspaceDirName);
715
770
  if (convId) {
716
771
  options.chatSessionRepo.setConversationId(message.channelId, convId);
772
+ activeConversationId = convId;
717
773
  }
718
774
  }
719
775
  }
@@ -722,6 +778,201 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
722
778
  logger_1.logger.error('[Rename] Failed to get title from Antigravity and rename:', e);
723
779
  }
724
780
  }
781
+ const classifiedForFiles = monitor.getLastClassified();
782
+ if (classifiedForFiles?.fileChanges && classifiedForFiles.fileChanges.length > 0) {
783
+ for (const fc of classifiedForFiles.fileChanges) {
784
+ if (!citedFiles.includes(fc.path)) {
785
+ citedFiles.push(fc.path);
786
+ }
787
+ }
788
+ }
789
+ if (!activeConversationId && options?.chatSessionRepo) {
790
+ activeConversationId = options.chatSessionRepo.findByChannelId(message.channelId)?.conversationId ?? undefined;
791
+ }
792
+ if (activeConversationId && options?.artifactService) {
793
+ const artifacts = options.artifactService.listArtifacts(activeConversationId);
794
+ const planArt = artifacts.find(art => art.artifactType === 'ARTIFACT_TYPE_IMPLEMENTATION_PLAN' ||
795
+ art.filename.toLowerCase() === 'implementation_plan.md');
796
+ let planTime = 0;
797
+ if (planArt) {
798
+ try {
799
+ planTime = fs_1.default.statSync(planArt.absolutePath).mtimeMs;
800
+ }
801
+ catch { /* ignore */ }
802
+ }
803
+ for (const art of artifacts) {
804
+ const nameWithExt = art.filename;
805
+ const nameWithoutExt = art.filename.replace(/\.[^/.]+$/, "");
806
+ const regex = new RegExp(`\\b${nameWithoutExt.replace(/[-/\\^$*+?.()|[\]{}]/g, '\\$&')}\\b`, 'i');
807
+ const mentionsArtifact = finalOutputText.includes(nameWithExt) || finalOutputText.includes(`\`${nameWithoutExt}\``) || regex.test(finalOutputText);
808
+ const isTask = art.artifactType === 'ARTIFACT_TYPE_TASK' || nameWithExt.toLowerCase() === 'task.md';
809
+ const isWalkthrough = art.artifactType === 'ARTIFACT_TYPE_WALKTHROUGH' || nameWithExt.toLowerCase() === 'walkthrough.md';
810
+ let isSpecialArtifact = false;
811
+ if (isTask || isWalkthrough) {
812
+ let artTime = 0;
813
+ try {
814
+ artTime = fs_1.default.statSync(art.absolutePath).mtimeMs;
815
+ }
816
+ catch { /* ignore */ }
817
+ if (artTime > planTime) {
818
+ isSpecialArtifact = true;
819
+ }
820
+ }
821
+ if ((mentionsArtifact || isSpecialArtifact) && !citedFiles.includes(nameWithExt)) {
822
+ citedFiles.push(nameWithExt);
823
+ }
824
+ }
825
+ }
826
+ if (citedFiles && citedFiles.length > 0) {
827
+ // Strip trailing punctuation and deduplicate
828
+ let uniqueFiles = Array.from(new Set(citedFiles.map(f => {
829
+ let clean = f.trim();
830
+ while (clean.match(/[.,;:!?]$/)) {
831
+ clean = clean.slice(0, -1);
832
+ }
833
+ return clean;
834
+ })));
835
+ // Filter out obviously bad paths
836
+ uniqueFiles = uniqueFiles.filter(f => f.length > 0 && f !== 'unknown');
837
+ const row = new discord_js_1.ActionRowBuilder();
838
+ // Max 5 buttons per row in Discord
839
+ for (let i = 0; i < Math.min(uniqueFiles.length, 5); i++) {
840
+ let fileUrl = uniqueFiles[i];
841
+ let isArtifactMatch = false;
842
+ let artifactDisplayName = fileUrl;
843
+ if (activeConversationId && options?.artifactService) {
844
+ const artifacts = options.artifactService.listArtifacts(activeConversationId);
845
+ const rawName = fileUrl.replace(/^file:\/\/\//, '');
846
+ const matched = artifacts.find(a => a.filename.toLowerCase() === fileUrl.toLowerCase() ||
847
+ a.filename.toLowerCase() === rawName.toLowerCase());
848
+ if (matched) {
849
+ isArtifactMatch = true;
850
+ fileUrl = `file:///${matched.absolutePath.replace(/\\/g, '/')}`;
851
+ artifactDisplayName = matched.filename;
852
+ }
853
+ }
854
+ const wsPath = cdp.getCurrentWorkspacePath();
855
+ if (!wsPath) {
856
+ continue;
857
+ }
858
+ if (!isArtifactMatch) {
859
+ if (!fileUrl.startsWith('file:///') && !path_1.default.isAbsolute(fileUrl)) {
860
+ fileUrl = `file:///${path_1.default.resolve(wsPath, fileUrl).replace(/\\/g, '/')}`;
861
+ }
862
+ let fsPath = fileUrl.replace(/^file:\/\/\//, '');
863
+ if (path_1.default.sep === '/' && !fsPath.startsWith('/')) {
864
+ fsPath = '/' + fsPath;
865
+ }
866
+ else if (process.platform === 'win32' && fsPath.startsWith('/')) {
867
+ fsPath = fsPath.substring(1);
868
+ }
869
+ const relative = path_1.default.relative(wsPath, fsPath);
870
+ const isArtifact = fsPath.includes('.gemini') && fsPath.includes('antigravity') && fsPath.includes('brain');
871
+ if (!isArtifact && (relative.startsWith('..') || path_1.default.isAbsolute(relative))) {
872
+ continue;
873
+ }
874
+ if (!fs_1.default.existsSync(fsPath)) {
875
+ continue;
876
+ }
877
+ }
878
+ let displayName = fileUrl;
879
+ if (isArtifactMatch) {
880
+ displayName = artifactDisplayName;
881
+ }
882
+ else if (displayName.startsWith('file:///')) {
883
+ const parts = displayName.split('/');
884
+ displayName = parts[parts.length - 1];
885
+ }
886
+ const hashId = Math.random().toString(36).substring(2, 10);
887
+ fileOpenCache_1.fileOpenCache.set(hashId, fileUrl);
888
+ let customId = `file_open:cache:${hashId}`;
889
+ if (isArtifactMatch && activeConversationId && displayName) {
890
+ const artId = `file_open:art:${activeConversationId}:${displayName}`;
891
+ if (artId.length < 100) {
892
+ customId = artId;
893
+ }
894
+ }
895
+ else if (wsPath) {
896
+ let fsPath = fileUrl.replace(/^file:\/\/\//, '');
897
+ if (path_1.default.sep === '/' && !fsPath.startsWith('/')) {
898
+ fsPath = '/' + fsPath;
899
+ }
900
+ else if (process.platform === 'win32' && fsPath.startsWith('/')) {
901
+ fsPath = fsPath.substring(1);
902
+ }
903
+ const resolvedFsPath = path_1.default.resolve(fsPath);
904
+ const relative = path_1.default.relative(wsPath, resolvedFsPath);
905
+ if (!relative.startsWith('..') && !path_1.default.isAbsolute(relative)) {
906
+ const relSlash = relative.replace(/\\/g, '/');
907
+ const relId = `file_open:rel:${relSlash}`;
908
+ if (relId.length < 100) {
909
+ customId = relId;
910
+ }
911
+ }
912
+ }
913
+ row.addComponents(new discord_js_1.ButtonBuilder()
914
+ .setCustomId(customId)
915
+ .setLabel(isArtifactMatch ? `Review ${displayName.substring(0, 15)}` : `Open ${displayName.substring(0, 20)}`)
916
+ .setStyle(isArtifactMatch ? discord_js_1.ButtonStyle.Success : discord_js_1.ButtonStyle.Primary));
917
+ }
918
+ if (row.components.length > 0) {
919
+ components.push(row);
920
+ }
921
+ }
922
+ if (fileChanges && fileChanges.length > 0) {
923
+ // The extracted text is the toolbar title, e.g. "1 File With Changes"
924
+ const widgetTitle = fileChanges[fileChanges.length - 1] || 'Files With Changes';
925
+ const fileChangesRow = new discord_js_1.ActionRowBuilder().addComponents(new discord_js_1.ButtonBuilder()
926
+ .setCustomId((0, cdpBridgeManager_1.buildFileChangeCustomId)('reject', cdp.getCurrentWorkspaceName() || 'unknown', message.channelId))
927
+ .setLabel(`Reject all (${widgetTitle})`)
928
+ .setStyle(discord_js_1.ButtonStyle.Danger), new discord_js_1.ButtonBuilder()
929
+ .setCustomId((0, cdpBridgeManager_1.buildFileChangeCustomId)('accept', cdp.getCurrentWorkspaceName() || 'unknown', message.channelId))
930
+ .setLabel('Accept all')
931
+ .setStyle(discord_js_1.ButtonStyle.Success));
932
+ components.push(fileChangesRow);
933
+ }
934
+ const classified = monitor.getLastClassified();
935
+ if (options?.extractionMode === 'structured' && classified && outputFormat !== 'plain') {
936
+ const messageOptions = (0, discordResponseRenderer_1.renderDiscordResponse)(classified, {
937
+ projectName: cdp.getCurrentWorkspaceName() || 'unknown',
938
+ channelId: message.channelId
939
+ });
940
+ for (const msg of liveResponseMessages) {
941
+ if (msg)
942
+ await msg.delete().catch(() => { });
943
+ }
944
+ liveResponseMessages.length = 0;
945
+ if (components.length > 0) {
946
+ if (!messageOptions.components)
947
+ messageOptions.components = [];
948
+ messageOptions.components = [...(messageOptions.components || []), ...components];
949
+ }
950
+ if (messageOptions.embeds && messageOptions.embeds.length > 0) {
951
+ const lastEmbed = messageOptions.embeds[messageOptions.embeds.length - 1];
952
+ if (lastEmbed && typeof lastEmbed.setFooter === 'function') {
953
+ lastEmbed.setFooter({ text: (0, i18n_1.t)(`⏱️ Time: ${elapsed}s | Complete`) });
954
+ }
955
+ }
956
+ if (channel) {
957
+ await channel.send(messageOptions).catch((error) => {
958
+ logDeliveryError('renderDiscordResponse/send', error);
959
+ });
960
+ }
961
+ }
962
+ else if (finalOutputText && finalOutputText.trim().length > 0) {
963
+ await upsertLiveResponseEmbeds(`${PHASE_ICONS.complete} Final Output`, finalOutputText, PHASE_COLORS.complete, (0, i18n_1.t)(`⏱️ Time: ${elapsed}s | Complete`), {
964
+ source: 'complete',
965
+ expectedVersion: responseVersion,
966
+ components: components.length > 0 ? components : undefined,
967
+ });
968
+ }
969
+ else {
970
+ 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`), {
971
+ source: 'complete',
972
+ expectedVersion: responseVersion,
973
+ components: components.length > 0 ? components : undefined,
974
+ });
975
+ }
725
976
  await sendGeneratedImages(finalOutputText || '');
726
977
  await clearWatchingReaction();
727
978
  await message.react(finalOutputText && finalOutputText.trim().length > 0 ? '✅' : '⚠️').catch(() => { });
@@ -770,6 +1021,32 @@ async function sendPromptToAntigravity(bridge, message, prompt, cdp, modeService
770
1021
  }
771
1022
  },
772
1023
  });
1024
+ onApprovalRef = async () => {
1025
+ if (isFinalized)
1026
+ return;
1027
+ const textToUse = lastProgressText || monitor.getLastText() || '';
1028
+ const separated = (0, discordFormatter_1.splitOutputAndLogs)(textToUse);
1029
+ const outputText = separated.output || textToUse;
1030
+ if (outputText && outputText.trim().length > 0) {
1031
+ liveResponseUpdateVersion += 1;
1032
+ const responseVersion = liveResponseUpdateVersion;
1033
+ const elapsed = Math.round((Date.now() - startTime) / 1000);
1034
+ await upsertLiveResponseEmbeds((0, i18n_1.t)('Response'), outputText, PHASE_COLORS.thinking, (0, i18n_1.t)(`⏱️ Time: ${elapsed}s | Awaiting Approval`), {
1035
+ source: 'approval-pause',
1036
+ expectedVersion: responseVersion,
1037
+ skipWhenFinalized: true,
1038
+ }).catch(() => { });
1039
+ }
1040
+ };
1041
+ cdp.on('approval_required', onApprovalRef);
1042
+ if (options?.onMonitorCreated) {
1043
+ options.onMonitorCreated({
1044
+ stop: async () => {
1045
+ isFinalized = true;
1046
+ await monitor.stop();
1047
+ }
1048
+ });
1049
+ }
773
1050
  await monitor.start();
774
1051
  // 1-second elapsed timer — updates footer independently of process log events
775
1052
  const elapsedTimer = setInterval(() => {
@@ -834,6 +1111,8 @@ const startBot = async (cliLogLevel) => {
834
1111
  .filter((account) => typeof account.userDataDir === 'string' && account.userDataDir.trim().length > 0)
835
1112
  .map((account) => [account.name, account.userDataDir.trim()]));
836
1113
  const bridge = (0, cdpBridgeManager_1.initCdpBridge)(config.autoApproveFileEdits, accountPorts, accountUserDataDirs);
1114
+ bridge.chatSessionRepo = chatSessionRepo;
1115
+ bridge.artifactService = artifactService;
837
1116
  // Initialize CDP-dependent services (constructor CDP dependency removed)
838
1117
  const chatSessionService = new chatSessionService_1.ChatSessionService();
839
1118
  const titleGenerator = new titleGeneratorService_1.TitleGeneratorService();
@@ -884,7 +1163,20 @@ const startBot = async (cliLogLevel) => {
884
1163
  accountPrefRepo,
885
1164
  accounts: config.antigravityAccounts,
886
1165
  });
887
- await (0, antigravityLauncher_1.startAntigravity)(accountPorts[accountName] ?? 9222);
1166
+ const status = await (0, antigravityLauncher_1.startAntigravity)(accountPorts[accountName] ?? 9222);
1167
+ if (status === 'started') {
1168
+ const cdp = new cdpService_1.CdpService({ portsToScan: [accountPorts[accountName] ?? 9222] });
1169
+ try {
1170
+ await cdp.connect();
1171
+ await cdp.openChatPanel();
1172
+ }
1173
+ catch (e) {
1174
+ // ignore
1175
+ }
1176
+ finally {
1177
+ await cdp.disconnect().catch(() => { });
1178
+ }
1179
+ }
888
1180
  });
889
1181
  const chatHandler = new chatCommandHandler_1.ChatCommandHandler(chatSessionService, chatSessionRepo, workspaceBindingRepo, channelManager, workspaceService, bridge.pool, (channelId, userId) => (0, accountUtils_1.resolveScopedAccountName)({
890
1182
  channelId,
@@ -977,6 +1269,7 @@ const startBot = async (cliLogLevel) => {
977
1269
  wsHandler,
978
1270
  chatHandler,
979
1271
  client,
1272
+ promptDispatcher,
980
1273
  sendModeUI: modeUi_1.sendModeUI,
981
1274
  sendModelsUI: modelsUi_1.sendModelsUI,
982
1275
  sendAutoAcceptUI: autoAcceptUi_1.sendAutoAcceptUI,
@@ -984,6 +1277,7 @@ const startBot = async (cliLogLevel) => {
984
1277
  parseApprovalCustomId: cdpBridgeManager_1.parseApprovalCustomId,
985
1278
  parseErrorPopupCustomId: cdpBridgeManager_1.parseErrorPopupCustomId,
986
1279
  parsePlanningCustomId: cdpBridgeManager_1.parsePlanningCustomId,
1280
+ parseFileChangeCustomId: cdpBridgeManager_1.parseFileChangeCustomId,
987
1281
  parseRunCommandCustomId: cdpBridgeManager_1.parseRunCommandCustomId,
988
1282
  joinHandler,
989
1283
  userPrefRepo,
@@ -993,8 +1287,10 @@ const startBot = async (cliLogLevel) => {
993
1287
  chatSessionService,
994
1288
  artifactThreadRepo,
995
1289
  artifactService,
1290
+ channelManager,
1291
+ titleGenerator,
996
1292
  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),
1293
+ 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
1294
  handleTemplateUse: async (interaction, templateId) => {
999
1295
  const template = templateRepo.findById(templateId);
1000
1296
  if (!template) {
@@ -1035,6 +1331,7 @@ const startBot = async (cliLogLevel) => {
1035
1331
  (0, cdpBridgeManager_1.ensureErrorPopupDetector)(bridge, cdp, projectName, selectedAccount);
1036
1332
  (0, cdpBridgeManager_1.ensurePlanningDetector)(bridge, cdp, projectName, selectedAccount);
1037
1333
  (0, cdpBridgeManager_1.ensureRunCommandDetector)(bridge, cdp, projectName, selectedAccount);
1334
+ (0, cdpBridgeManager_1.ensureQuestionDetector)(bridge, cdp, projectName, selectedAccount);
1038
1335
  }
1039
1336
  catch (e) {
1040
1337
  await interaction.followUp({
@@ -1184,10 +1481,12 @@ const startBot = async (cliLogLevel) => {
1184
1481
  : binding.workspacePath;
1185
1482
  },
1186
1483
  });
1484
+ const questionSelectAction = (0, questionSelectAction_1.createQuestionSelectAction)({ bridge, wsHandler });
1187
1485
  const telegramSelectHandler = (0, selectHandler_1.createPlatformSelectHandler)({
1188
1486
  actions: [
1189
1487
  modeSelectAction,
1190
1488
  accountSelectAction,
1489
+ questionSelectAction,
1191
1490
  ],
1192
1491
  });
1193
1492
  // Composite handler that routes to the right handler
@@ -1206,7 +1505,9 @@ const startBot = async (cliLogLevel) => {
1206
1505
  }, interaction);
1207
1506
  return;
1208
1507
  }
1209
- if (interaction.customId === 'mode_select' || interaction.customId === 'account_select') {
1508
+ if (interaction.customId === 'mode_select' ||
1509
+ interaction.customId === 'account_select' ||
1510
+ interaction.customId.startsWith('question_select_action')) {
1210
1511
  await telegramSelectHandler(interaction);
1211
1512
  return;
1212
1513
  }
@@ -1221,10 +1522,10 @@ const startBot = async (cliLogLevel) => {
1221
1522
  }
1222
1523
  const telegramButtonHandler = (0, buttonHandler_1.createPlatformButtonHandler)({
1223
1524
  actions: [
1224
- (0, approvalButtonAction_1.createApprovalButtonAction)({ bridge }),
1225
- (0, planningButtonAction_1.createPlanningButtonAction)({ bridge }),
1226
- (0, errorPopupButtonAction_1.createErrorPopupButtonAction)({ bridge }),
1227
- (0, runCommandButtonAction_1.createRunCommandButtonAction)({ bridge }),
1525
+ (0, approvalButtonAction_1.createApprovalButtonAction)({ bridge, wsHandler }),
1526
+ (0, planningButtonAction_1.createPlanningButtonAction)({ bridge, wsHandler }),
1527
+ (0, errorPopupButtonAction_1.createErrorPopupButtonAction)({ bridge, wsHandler }),
1528
+ (0, runCommandButtonAction_1.createRunCommandButtonAction)({ bridge, wsHandler }),
1228
1529
  (0, modelButtonAction_1.createModelButtonAction)({
1229
1530
  bridge,
1230
1531
  fetchQuota: () => bridge.quota.fetchQuota(),
@@ -1262,6 +1563,8 @@ const startBot = async (cliLogLevel) => {
1262
1563
  }),
1263
1564
  (0, autoAcceptButtonAction_1.createAutoAcceptButtonAction)({ autoAcceptService: bridge.autoAccept }),
1264
1565
  (0, templateButtonAction_1.createTemplateButtonAction)({ bridge, templateRepo }),
1566
+ (0, fileChangeButtonAction_1.createFileChangeButtonAction)({ bridge, wsHandler }),
1567
+ (0, genericActionButtonAction_1.createGenericActionButtonAction)({ bridge, wsHandler }),
1265
1568
  ],
1266
1569
  });
1267
1570
  const eventRouter = new eventRouter_1.EventRouter({ allowedUsers }, {
@@ -1382,7 +1685,7 @@ async function autoRenameChannel(message, chatSessionRepo, titleGenerator, chann
1382
1685
  /**
1383
1686
  * Handle Discord Interactions API slash commands
1384
1687
  */
1385
- 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) {
1688
+ 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) {
1386
1689
  const commandName = interaction.commandName;
1387
1690
  const getAccountPort = (accountName) => {
1388
1691
  const match = antigravityAccounts.find((account) => account.name === accountName);
@@ -1407,9 +1710,7 @@ async function handleSlashInteraction(interaction, handler, bridge, wsHandler, c
1407
1710
  const projectName = bridge.pool.extractProjectName(workspacePath);
1408
1711
  return bridge.pool.getConnected(projectName, resolveSelectedAccount());
1409
1712
  }
1410
- return bridge.lastActiveWorkspace
1411
- ? bridge.pool.getConnected(bridge.lastActiveWorkspace, resolveSelectedAccount())
1412
- : null;
1713
+ return null;
1413
1714
  })();
1414
1715
  const ensureChannelCdp = async () => {
1415
1716
  const existing = getChannelCdp();
@@ -1880,6 +2181,65 @@ async function handleSlashInteraction(interaction, handler, bridge, wsHandler, c
1880
2181
  await interaction.editReply({ content: codeBlock });
1881
2182
  break;
1882
2183
  }
2184
+ case 'open': {
2185
+ const filepath = interaction.options.getString('filepath', true);
2186
+ let resolvedPath = null;
2187
+ // 1. Try to resolve as an artifact
2188
+ if (chatSessionRepo && artifactService) {
2189
+ const session = chatSessionRepo.findByChannelId(interaction.channelId);
2190
+ if (session && session.conversationId) {
2191
+ const possibleArtifact = artifactService.getArtifactPath(session.conversationId, filepath);
2192
+ if (fs_1.default.existsSync(possibleArtifact)) {
2193
+ resolvedPath = possibleArtifact;
2194
+ }
2195
+ }
2196
+ }
2197
+ // 2. Try to resolve against the workspace
2198
+ if (!resolvedPath) {
2199
+ const cdp = await ensureChannelCdp();
2200
+ if (cdp) {
2201
+ const wsPath = cdp.getCurrentWorkspacePath();
2202
+ if (wsPath) {
2203
+ let rawPath = filepath;
2204
+ if (filepath.startsWith('file:///')) {
2205
+ rawPath = filepath.replace('file:///', '');
2206
+ if (process.platform === 'win32' && rawPath.startsWith('/')) {
2207
+ rawPath = rawPath.substring(1);
2208
+ }
2209
+ }
2210
+ const candidatePath = path_1.default.isAbsolute(rawPath) ? rawPath : path_1.default.join(wsPath, rawPath);
2211
+ const relative = path_1.default.relative(wsPath, candidatePath);
2212
+ if (!relative.startsWith('..') && !path_1.default.isAbsolute(relative)) {
2213
+ if (fs_1.default.existsSync(candidatePath)) {
2214
+ resolvedPath = candidatePath;
2215
+ }
2216
+ }
2217
+ }
2218
+ }
2219
+ }
2220
+ if (!resolvedPath) {
2221
+ await interaction.editReply({
2222
+ content: '❌ Error: Cannot resolve file path. Ensure the file exists as an artifact or within the current workspace.'
2223
+ });
2224
+ break;
2225
+ }
2226
+ try {
2227
+ (0, child_process_1.execFile)((0, pathUtils_1.getAntigravityCliPath)(), [resolvedPath], (error) => {
2228
+ if (error) {
2229
+ logger_1.logger.error(`Failed to open file via CLI: ${error.message}`);
2230
+ interaction.editReply({ content: `❌ Error opening file via CLI.` }).catch(() => { });
2231
+ }
2232
+ else {
2233
+ interaction.editReply({ content: `✅ Opened file: **${path_1.default.basename(resolvedPath)}**` }).catch(() => { });
2234
+ }
2235
+ });
2236
+ }
2237
+ catch (e) {
2238
+ logger_1.logger.error(`Failed to open file: ${e.message}`);
2239
+ await interaction.editReply({ content: `❌ Error opening file.` });
2240
+ }
2241
+ break;
2242
+ }
1883
2243
  case 'artifacts': {
1884
2244
  await (0, artifactsUi_1.sendArtifactPickerUI)(interaction, {
1885
2245
  userPrefRepo,