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
@@ -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,60 @@ 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 controller = new AbortController();
229
+ const timeoutId = setTimeout(() => controller.abort(), 10000);
230
+ const res = await fetch(textAtt.url, { signal: controller.signal });
231
+ clearTimeout(timeoutId);
232
+ if (res.ok) {
233
+ const content = await res.text();
234
+ return `\n\n[Attached File: ${textAtt.name}]\n\`\`\`\n${content}\n\`\`\``;
235
+ }
236
+ else {
237
+ logger_1.logger.warn(`[MessageCreate] Non-ok status fetching text attachment ${textAtt.name}: ${res.status}`);
238
+ return '';
239
+ }
240
+ }
241
+ catch (e) {
242
+ logger_1.logger.warn(`[MessageCreate] Failed to fetch text attachment ${textAtt.name}:`, e);
243
+ return '';
244
+ }
245
+ }));
246
+ promptText += results.join('');
247
+ }
197
248
  const inboundImages = await downloadInboundImageAttachments(message);
198
249
  if (hasImageAttachments && inboundImages.length === 0) {
199
250
  await message.reply('Failed to retrieve attached images. Please wait and try again.').catch(() => { });
@@ -253,6 +304,7 @@ function createMessageCreateHandler(deps) {
253
304
  ensureErrorPopupDetector(deps.bridge, cdp, projectName, selectedAccount);
254
305
  ensurePlanningDetector(deps.bridge, cdp, projectName, selectedAccount);
255
306
  ensureRunCommandDetector(deps.bridge, cdp, projectName, selectedAccount);
307
+ ensureQuestionDetector(deps.bridge, cdp, projectName, selectedAccount);
256
308
  let session = deps.chatSessionRepo.findByChannelId(message.channelId);
257
309
  const staleSessionAccount = session?.isRenamed
258
310
  && ((session.activeAccountName && session.activeAccountName !== selectedAccount)
@@ -300,9 +352,16 @@ function createMessageCreateHandler(deps) {
300
352
  }
301
353
  }
302
354
  if (!activationResult.ok) {
303
- const reason = activationResult.error ? ` (${activationResult.error})` : '';
304
- await message.reply(`āš ļø Could not route this message to the bound session (${session.displayName}). ` +
305
- `Please open /chat and verify the session${reason}.`).catch(() => { });
355
+ const isDeleted = activationResult.error?.includes('Conversation not found in Past Conversations');
356
+ let replyText = `āš ļø Could not route this message to the bound session (${session.displayName}).\n*Reason: ${activationResult.error}*`;
357
+ if (isDeleted) {
358
+ deps.chatSessionRepo.deleteByChannelId(message.channelId);
359
+ replyText += `\n\nšŸ’” **Tip**: This session appears to have been deleted in the IDE. I have unbound this channel so your next message will start a fresh chat. You can also type \`/new\` anytime.`;
360
+ }
361
+ else {
362
+ replyText += `\n\nšŸ’” If this session is broken, type \`/new\` to force a new chat.`;
363
+ }
364
+ await message.reply(replyText).catch(() => { });
306
365
  return;
307
366
  }
308
367
  }
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const fileChangeButtonAction_1 = require("../fileChangeButtonAction");
4
+ describe('fileChangeButtonAction', () => {
5
+ let mockInteraction;
6
+ let mockCdp;
7
+ let mockBridge;
8
+ let mockWsHandler;
9
+ beforeEach(() => {
10
+ mockInteraction = {
11
+ deferUpdate: jest.fn().mockResolvedValue(undefined),
12
+ reply: jest.fn().mockResolvedValue(undefined),
13
+ update: jest.fn().mockResolvedValue(undefined),
14
+ followUp: jest.fn().mockResolvedValue(undefined),
15
+ };
16
+ mockCdp = {
17
+ isConnected: jest.fn().mockReturnValue(true),
18
+ call: jest.fn(),
19
+ };
20
+ mockBridge = {
21
+ lastActiveWorkspace: 'test_project',
22
+ pool: {
23
+ getConnected: jest.fn().mockReturnValue(mockCdp),
24
+ extractProjectName: jest.fn().mockReturnValue('test_project'),
25
+ },
26
+ };
27
+ mockWsHandler = {
28
+ getWorkspaceForChannel: jest.fn().mockReturnValue('test_project'),
29
+ };
30
+ });
31
+ it('matches valid legacy custom IDs', () => {
32
+ const action = (0, fileChangeButtonAction_1.createFileChangeButtonAction)({ bridge: mockBridge, wsHandler: mockWsHandler });
33
+ expect(action.match('ide_file_accept_all')).toEqual({ action: 'accept', projectName: '', channelId: '' });
34
+ expect(action.match('ide_file_reject_all')).toEqual({ action: 'reject', projectName: '', channelId: '' });
35
+ expect(action.match('unknown_button')).toBeNull();
36
+ });
37
+ it('matches valid custom IDs with project and channel', () => {
38
+ const action = (0, fileChangeButtonAction_1.createFileChangeButtonAction)({ bridge: mockBridge, wsHandler: mockWsHandler });
39
+ expect(action.match('file_change_accept:proj:chan1')).toEqual({ action: 'accept', projectName: 'proj', channelId: 'chan1' });
40
+ expect(action.match('file_change_reject:proj:chan2')).toEqual({ action: 'reject', projectName: 'proj', channelId: 'chan2' });
41
+ });
42
+ it('fails if linked to a different session channel', async () => {
43
+ mockInteraction.channel = { id: 'channel_123' };
44
+ const action = (0, fileChangeButtonAction_1.createFileChangeButtonAction)({ bridge: mockBridge, wsHandler: mockWsHandler });
45
+ await action.execute(mockInteraction, { action: 'accept', channelId: 'channel_456' });
46
+ expect(mockInteraction.reply).toHaveBeenCalledWith({ text: 'This file change action is linked to a different session channel.' });
47
+ });
48
+ it('fails if not connected to IDE', async () => {
49
+ mockInteraction.channel = { id: 'channel_123' };
50
+ mockCdp.isConnected.mockReturnValue(false);
51
+ const action = (0, fileChangeButtonAction_1.createFileChangeButtonAction)({ bridge: mockBridge, wsHandler: mockWsHandler });
52
+ await action.execute(mockInteraction, { action: 'accept', channelId: 'channel_123' });
53
+ expect(mockInteraction.reply).toHaveBeenCalledWith({ text: 'Not connected to IDE.' });
54
+ });
55
+ it('successfully accepts changes when button is found', async () => {
56
+ mockInteraction.channel = { id: 'channel_123' };
57
+ mockCdp.call.mockResolvedValue({ result: { value: true } });
58
+ const action = (0, fileChangeButtonAction_1.createFileChangeButtonAction)({ bridge: mockBridge, wsHandler: mockWsHandler });
59
+ await action.execute(mockInteraction, { action: 'accept' });
60
+ expect(mockCdp.call).toHaveBeenCalled();
61
+ expect(mockInteraction.update).toHaveBeenCalledWith({
62
+ text: 'āœ… Accepted file changes.',
63
+ components: [],
64
+ });
65
+ });
66
+ it('successfully rejects changes when button is found', async () => {
67
+ mockInteraction.channel = { id: 'channel_123' };
68
+ mockCdp.call.mockResolvedValue({ result: { value: true } });
69
+ const action = (0, fileChangeButtonAction_1.createFileChangeButtonAction)({ bridge: mockBridge, wsHandler: mockWsHandler });
70
+ await action.execute(mockInteraction, { action: 'reject' });
71
+ expect(mockInteraction.update).toHaveBeenCalledWith({
72
+ text: 'āŒ Rejected file changes.',
73
+ components: [],
74
+ });
75
+ });
76
+ it('sends followup if button is not found', async () => {
77
+ mockInteraction.channel = { id: 'channel_123' };
78
+ mockCdp.call.mockResolvedValue({ result: { value: false } });
79
+ const action = (0, fileChangeButtonAction_1.createFileChangeButtonAction)({ bridge: mockBridge, wsHandler: mockWsHandler });
80
+ await action.execute(mockInteraction, { action: 'accept' });
81
+ expect(mockInteraction.followUp).toHaveBeenCalledWith({
82
+ text: expect.stringContaining('Could not find the "Accept all" button'),
83
+ });
84
+ });
85
+ });
@@ -8,6 +8,7 @@
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.createApprovalButtonAction = createApprovalButtonAction;
10
10
  const cdpBridgeManager_1 = require("../services/cdpBridgeManager");
11
+ const projectResolver_1 = require("../utils/projectResolver");
11
12
  const logger_1 = require("../utils/logger");
12
13
  function createApprovalButtonAction(deps) {
13
14
  return {
@@ -32,7 +33,7 @@ function createApprovalButtonAction(deps) {
32
33
  .catch(() => { });
33
34
  return;
34
35
  }
35
- const projectName = params.projectName || deps.bridge.lastActiveWorkspace;
36
+ const projectName = (0, projectResolver_1.resolveProjectName)(deps, interaction.channel.id, params.projectName);
36
37
  logger_1.logger.debug(`[ApprovalAction] action=${action} project=${projectName ?? 'null'} channel=${interaction.channel.id}`);
37
38
  const detector = projectName
38
39
  ? deps.bridge.pool.getApprovalDetector(projectName)
@@ -7,6 +7,7 @@
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.createErrorPopupButtonAction = createErrorPopupButtonAction;
10
+ const projectResolver_1 = require("../utils/projectResolver");
10
11
  const cdpBridgeManager_1 = require("../services/cdpBridgeManager");
11
12
  const logger_1 = require("../utils/logger");
12
13
  const MAX_DEBUG_CONTENT = 4096;
@@ -30,7 +31,7 @@ function createErrorPopupButtonAction(deps) {
30
31
  .catch(() => { });
31
32
  return;
32
33
  }
33
- const projectName = params.projectName || deps.bridge.lastActiveWorkspace;
34
+ const projectName = (0, projectResolver_1.resolveProjectName)(deps, interaction.channel.id, params.projectName);
34
35
  const detector = projectName
35
36
  ? deps.bridge.pool.getErrorPopupDetector(projectName)
36
37
  : undefined;
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createFileChangeButtonAction = createFileChangeButtonAction;
4
+ const cdpBridgeManager_1 = require("../services/cdpBridgeManager");
5
+ const projectResolver_1 = require("../utils/projectResolver");
6
+ const logger_1 = require("../utils/logger");
7
+ const questionActionUtils_1 = require("../utils/questionActionUtils");
8
+ function createFileChangeButtonAction(deps) {
9
+ return {
10
+ match(customId) {
11
+ const parsed = (0, cdpBridgeManager_1.parseFileChangeCustomId)(customId);
12
+ if (!parsed)
13
+ return null;
14
+ return {
15
+ action: parsed.action,
16
+ projectName: parsed.projectName ?? '',
17
+ channelId: parsed.channelId ?? '',
18
+ };
19
+ },
20
+ async execute(interaction, params) {
21
+ const { action, channelId } = params;
22
+ // Acknowledge immediately so Telegram/Discord doesn't time out
23
+ await interaction.deferUpdate().catch(() => { });
24
+ if (channelId && channelId !== interaction.channel.id) {
25
+ await interaction
26
+ .reply({ text: 'This file change action is linked to a different session channel.' })
27
+ .catch(() => { });
28
+ return;
29
+ }
30
+ const projectName = (0, projectResolver_1.resolveProjectName)(deps, interaction.channel.id, params.projectName);
31
+ const cdp = projectName ? deps.bridge.pool.getConnected(projectName) : undefined;
32
+ if (!cdp || !cdp.isConnected()) {
33
+ await interaction
34
+ .reply({ text: 'Not connected to IDE.' })
35
+ .catch(() => { });
36
+ return;
37
+ }
38
+ const targetText = action === 'accept' ? 'Accept all' : 'Reject all';
39
+ const success = await (0, questionActionUtils_1.executeBrowserClick)(cdp, targetText);
40
+ if (success) {
41
+ await interaction
42
+ .update({
43
+ text: `${action === 'accept' ? 'āœ… Accepted' : 'āŒ Rejected'} file changes.`,
44
+ components: [],
45
+ })
46
+ .catch((err) => {
47
+ logger_1.logger.warn('[FileChangeAction] update failed:', err);
48
+ });
49
+ }
50
+ else {
51
+ await interaction
52
+ .followUp({ text: `Could not find the "${targetText}" button in the IDE. The file change prompt may have been dismissed.` })
53
+ .catch(() => { });
54
+ }
55
+ },
56
+ };
57
+ }
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseGenericActionCustomId = parseGenericActionCustomId;
4
+ exports.createGenericActionButtonAction = createGenericActionButtonAction;
5
+ const logger_1 = require("../utils/logger");
6
+ const projectResolver_1 = require("../utils/projectResolver");
7
+ const questionActionUtils_1 = require("../utils/questionActionUtils");
8
+ function parseGenericActionCustomId(customId) {
9
+ if (!customId.startsWith('action_btn_'))
10
+ return null;
11
+ const parts = customId.split(':');
12
+ const actionName = parts[0].replace('action_btn_', '').replace(/_/g, ' ');
13
+ // Capitalize first letter
14
+ const formattedName = actionName.charAt(0).toUpperCase() + actionName.slice(1);
15
+ if (parts.length < 3) {
16
+ return { actionName: formattedName };
17
+ }
18
+ const channelId = parts[parts.length - 1];
19
+ const projectName = parts.slice(1, parts.length - 1).join(':');
20
+ return {
21
+ actionName: formattedName,
22
+ projectName: projectName || undefined,
23
+ channelId: channelId || undefined,
24
+ };
25
+ }
26
+ function createGenericActionButtonAction(deps) {
27
+ return {
28
+ match(customId) {
29
+ const parsed = parseGenericActionCustomId(customId);
30
+ if (!parsed)
31
+ return null;
32
+ const result = { actionName: parsed.actionName };
33
+ if (parsed.projectName)
34
+ result.projectName = parsed.projectName;
35
+ if (parsed.channelId)
36
+ result.channelId = parsed.channelId;
37
+ return result;
38
+ },
39
+ async execute(interaction, params) {
40
+ const actionName = params.actionName;
41
+ const channelId = params.channelId || interaction.channel?.id;
42
+ if (!channelId) {
43
+ await interaction.reply({
44
+ text: 'Error: Cannot resolve channel ID.',
45
+ ephemeral: true,
46
+ }).catch(() => { });
47
+ return;
48
+ }
49
+ const projectName = (0, projectResolver_1.resolveProjectName)(deps, channelId, params.projectName);
50
+ await interaction.deferUpdate().catch(() => { });
51
+ const accountName = deps.bridge.selectedAccountByChannel?.get(channelId) || 'default';
52
+ const cdp = projectName
53
+ ? deps.bridge.pool.getConnected(projectName, accountName)
54
+ : null;
55
+ if (!cdp) {
56
+ await interaction.followUp({
57
+ text: 'Not connected to Antigravity. Send the action as a message instead.',
58
+ }).catch(() => { });
59
+ return;
60
+ }
61
+ // Simulate DOM click instead of chat injection
62
+ logger_1.logger.info(`[GenericActionButton] Clicking action button "${actionName}" via DOM script`);
63
+ const success = await (0, questionActionUtils_1.executeBrowserClick)(cdp, actionName);
64
+ if (!success) {
65
+ await interaction.followUp({
66
+ text: `Failed to execute action: Button "${actionName}" not found or obscured.`,
67
+ }).catch(() => { });
68
+ return;
69
+ }
70
+ },
71
+ };
72
+ }
@@ -5,10 +5,48 @@
5
5
  * Handles Open / Proceed button presses for the planning mode dialog
6
6
  * from both Discord and Telegram using the ButtonAction interface.
7
7
  */
8
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
9
+ if (k2 === undefined) k2 = k;
10
+ var desc = Object.getOwnPropertyDescriptor(m, k);
11
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
12
+ desc = { enumerable: true, get: function() { return m[k]; } };
13
+ }
14
+ Object.defineProperty(o, k2, desc);
15
+ }) : (function(o, m, k, k2) {
16
+ if (k2 === undefined) k2 = k;
17
+ o[k2] = m[k];
18
+ }));
19
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
20
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
21
+ }) : function(o, v) {
22
+ o["default"] = v;
23
+ });
24
+ var __importStar = (this && this.__importStar) || (function () {
25
+ var ownKeys = function(o) {
26
+ ownKeys = Object.getOwnPropertyNames || function (o) {
27
+ var ar = [];
28
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
29
+ return ar;
30
+ };
31
+ return ownKeys(o);
32
+ };
33
+ return function (mod) {
34
+ if (mod && mod.__esModule) return mod;
35
+ var result = {};
36
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
37
+ __setModuleDefault(result, mod);
38
+ return result;
39
+ };
40
+ })();
8
41
  Object.defineProperty(exports, "__esModule", { value: true });
9
42
  exports.createPlanningButtonAction = createPlanningButtonAction;
10
43
  const cdpBridgeManager_1 = require("../services/cdpBridgeManager");
44
+ const projectResolver_1 = require("../utils/projectResolver");
11
45
  const logger_1 = require("../utils/logger");
46
+ const fs = __importStar(require("fs"));
47
+ const path = __importStar(require("path"));
48
+ const child_process_1 = require("child_process");
49
+ const pathUtils_1 = require("../utils/pathUtils");
12
50
  const MAX_PLAN_CONTENT = 4096;
13
51
  function createPlanningButtonAction(deps) {
14
52
  return {
@@ -24,35 +62,66 @@ function createPlanningButtonAction(deps) {
24
62
  },
25
63
  async execute(interaction, params) {
26
64
  const { action, channelId } = params;
27
- // Acknowledge immediately so Telegram doesn't time out
28
- await interaction.deferUpdate().catch(() => { });
29
65
  if (channelId && channelId !== interaction.channel.id) {
66
+ await interaction.deferUpdate().catch(() => { });
30
67
  await interaction
31
- .reply({ text: 'This planning action is linked to a different session channel.' })
68
+ .followUp({ text: 'This planning action is linked to a different session channel.', ephemeral: true })
32
69
  .catch(() => { });
33
70
  return;
34
71
  }
35
- const projectName = params.projectName || deps.bridge.lastActiveWorkspace;
72
+ const projectName = (0, projectResolver_1.resolveProjectName)(deps, interaction.channel.id, params.projectName);
36
73
  const detector = projectName
37
74
  ? deps.bridge.pool.getPlanningDetector(projectName)
38
75
  : undefined;
39
76
  if (!detector) {
77
+ await interaction.deferUpdate().catch(() => { });
40
78
  await interaction
41
- .reply({ text: 'Planning detector not found.' })
79
+ .followUp({ text: 'Planning detector not found.', ephemeral: true })
42
80
  .catch(() => { });
43
81
  return;
44
82
  }
45
83
  if (action === 'open') {
46
- const clicked = await detector.clickOpenButton();
47
- if (!clicked) {
48
- await interaction
49
- .reply({ text: 'Open button not found.' })
50
- .catch(() => { });
51
- return;
84
+ const info = detector.getLastDetectedInfo();
85
+ const isReview = info?.openText?.toLowerCase().includes('review');
86
+ let modalFailed = false;
87
+ if (isReview && interaction.showModal) {
88
+ try {
89
+ await interaction.showModal({
90
+ title: 'Submit Comment',
91
+ customId: `plan_comment_${projectName || ''}_${channelId}`,
92
+ components: [{
93
+ components: [{
94
+ type: 'textInput',
95
+ customId: 'comment',
96
+ label: 'Add a message...',
97
+ style: 'paragraph',
98
+ required: false
99
+ }]
100
+ }]
101
+ });
102
+ return;
103
+ }
104
+ catch (err) {
105
+ logger_1.logger.error('[PlanningAction] showModal failed:', err);
106
+ modalFailed = true;
107
+ }
52
108
  }
53
- // Wait for DOM to update after Open click
54
- await new Promise((resolve) => setTimeout(resolve, 500));
55
- // Extract plan content with retry
109
+ // Acknowledge if we didn't show a modal
110
+ await interaction.deferUpdate().catch(() => { });
111
+ let clicked = false;
112
+ if (info?.hasOpenButton && (!isReview || modalFailed)) {
113
+ try {
114
+ clicked = await detector.clickOpenButton();
115
+ if (clicked) {
116
+ await new Promise((resolve) => setTimeout(resolve, 500));
117
+ }
118
+ }
119
+ catch (e) {
120
+ logger_1.logger.error('[PlanningAction] clickOpenButton failed:', e);
121
+ }
122
+ }
123
+ // Since we only come here if it's NOT a review (or modal failed),
124
+ // we can just extract plan content and followUp
56
125
  let planContent = null;
57
126
  for (let attempt = 0; attempt < 3; attempt++) {
58
127
  planContent = await detector.extractPlanContent();
@@ -60,16 +129,42 @@ function createPlanningButtonAction(deps) {
60
129
  break;
61
130
  await new Promise((resolve) => setTimeout(resolve, 500));
62
131
  }
132
+ if (!planContent && !clicked) {
133
+ // Fallback to reading implementation_plan.md from the workspace
134
+ const workspaceId = deps.wsHandler.getWorkspaceForChannel(interaction.channel.id);
135
+ if (workspaceId) {
136
+ const planFile = path.join(workspaceId, 'implementation_plan.md');
137
+ if (fs.existsSync(planFile)) {
138
+ try {
139
+ planContent = fs.readFileSync(planFile, 'utf8');
140
+ // Proactively open the file in the IDE
141
+ (0, child_process_1.execFile)((0, pathUtils_1.getAntigravityCliPath)(), [planFile], (err) => {
142
+ if (err)
143
+ logger_1.logger.error(`[PlanningAction] Failed to open plan via CLI: ${err.message}`);
144
+ });
145
+ }
146
+ catch (err) {
147
+ logger_1.logger.error(`[PlanningAction] Failed to read plan file: ${err.message}`);
148
+ }
149
+ }
150
+ }
151
+ }
152
+ if (!planContent && !clicked) {
153
+ await interaction
154
+ .followUp({ text: 'Plan content could not be extracted from the IDE or workspace.' })
155
+ .catch(() => { });
156
+ return;
157
+ }
63
158
  await interaction
64
- .update({
159
+ .editReply({
65
160
  text: 'šŸ“‹ Plan opened',
66
161
  components: [],
67
162
  })
68
163
  .catch((err) => {
69
- logger_1.logger.warn('[PlanningAction] update failed:', err);
164
+ logger_1.logger.warn('[PlanningAction] editReply failed:', err);
70
165
  });
71
166
  if (planContent) {
72
- const truncated = planContent.length > MAX_PLAN_CONTENT
167
+ let truncated = planContent.length > MAX_PLAN_CONTENT
73
168
  ? planContent.substring(0, MAX_PLAN_CONTENT - 15) + '\n\n(truncated)'
74
169
  : planContent;
75
170
  await interaction
@@ -80,10 +175,18 @@ function createPlanningButtonAction(deps) {
80
175
  }
81
176
  else {
82
177
  await interaction
83
- .followUp({ text: 'Could not extract plan content from the editor.' })
84
- .catch(() => { });
178
+ .followUp({ text: 'Plan opened in IDE, but content could not be extracted.' })
179
+ .catch((err) => {
180
+ logger_1.logger.warn('[PlanningAction] followUp failed:', err);
181
+ });
85
182
  }
86
183
  }
184
+ else if (action === 'reject') {
185
+ await interaction.deferUpdate().catch(() => { });
186
+ // Reject action
187
+ await interaction.followUp({ text: 'Rejection of the plan is not allowed.', ephemeral: true }).catch(() => { });
188
+ return;
189
+ }
87
190
  else {
88
191
  // Proceed action
89
192
  await interaction.deferUpdate().catch(() => { });
@@ -99,17 +202,17 @@ function createPlanningButtonAction(deps) {
99
202
  }
100
203
  if (clicked) {
101
204
  await interaction
102
- .update({
103
- text: 'ā–¶ļø Proceed started',
205
+ .editReply({
206
+ text: 'ā–¶ļø Proceed started.\n\nā³ IDE is working on the response...',
104
207
  components: [],
105
208
  })
106
209
  .catch((err) => {
107
- logger_1.logger.warn('[PlanningAction] update failed:', err);
210
+ logger_1.logger.warn('[PlanningAction] editReply failed:', err);
108
211
  });
109
212
  }
110
213
  else {
111
214
  await interaction
112
- .reply({ text: 'Proceed button not found.' })
215
+ .followUp({ text: 'Proceed button not found.', ephemeral: true })
113
216
  .catch(() => { });
114
217
  }
115
218
  }
@@ -0,0 +1,38 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.execute = execute;
4
+ const i18n_1 = require("../utils/i18n");
5
+ const logger_1 = require("../utils/logger");
6
+ async function execute(interaction, projectName, pool, accountName) {
7
+ try {
8
+ if (!projectName) {
9
+ await interaction.reply({ text: (0, i18n_1.t)('Project name not found.'), ephemeral: true });
10
+ return;
11
+ }
12
+ const cdp = pool.getConnected(projectName, accountName);
13
+ if (!cdp) {
14
+ await interaction.reply({ text: (0, i18n_1.t)('Not connected to workspace.'), ephemeral: true });
15
+ return;
16
+ }
17
+ const comment = interaction.fields.get('comment')?.trim();
18
+ if (!comment) {
19
+ await interaction.reply({ text: (0, i18n_1.t)('Comment cannot be empty.'), ephemeral: true });
20
+ return;
21
+ }
22
+ await interaction.deferUpdate();
23
+ // Inject the comment into the chat
24
+ const result = await cdp.injectMessage(comment);
25
+ if (!result.ok) {
26
+ await interaction.followUp({ text: (0, i18n_1.t)('Failed to submit comment: {{error}}', { error: result.error }), ephemeral: true });
27
+ return;
28
+ }
29
+ await interaction.followUp({ text: (0, i18n_1.t)('Comment submitted to the plan successfully.'), ephemeral: true });
30
+ }
31
+ catch (error) {
32
+ logger_1.logger.error('[PlanningModalSubmit] Error:', error);
33
+ try {
34
+ await interaction.followUp({ text: (0, i18n_1.t)('An error occurred while submitting your comment.'), ephemeral: true });
35
+ }
36
+ catch { /* ignore */ }
37
+ }
38
+ }