lazy-gravity 0.10.0 → 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.
@@ -21,6 +21,8 @@ const notificationSender_1 = require("../services/notificationSender");
21
21
  const questionSelectAction_1 = require("../handlers/questionSelectAction");
22
22
  const questionSkipAction_1 = require("../handlers/questionSkipAction");
23
23
  const wrappers_1 = require("../platform/discord/wrappers");
24
+ const questionActionUtils_1 = require("../utils/questionActionUtils");
25
+ const projectResolver_1 = require("../utils/projectResolver");
24
26
  const cleanupCommandHandler_1 = require("../commands/cleanupCommandHandler");
25
27
  const projectListUi_1 = require("../ui/projectListUi");
26
28
  const modeService_1 = require("../services/modeService");
@@ -226,6 +228,19 @@ function createInteractionCreateHandler(deps) {
226
228
  embeds: [updatedEmbed],
227
229
  components: (0, discordButtonUtils_1.disableAllButtons)(interaction.message.components),
228
230
  });
231
+ if (success && (approvalAction.action === 'approve' || approvalAction.action === 'always_allow') && deps.promptDispatcher && interaction.channelId && interaction.message) {
232
+ const cdp = projectName
233
+ ? deps.bridge.pool.getConnected(projectName, resolveSelectedAccount(interaction.channelId, interaction.user.id, getParentChannelId(interaction)))
234
+ : null;
235
+ if (cdp) {
236
+ deps.promptDispatcher.resume({
237
+ message: interaction.message,
238
+ prompt: '',
239
+ cdp,
240
+ options: buildResumeOptions()
241
+ }).catch(e => logger_1.logger.error('[Approval] Failed to resume monitoring:', e));
242
+ }
243
+ }
229
244
  }
230
245
  else {
231
246
  await interaction.reply({ content: 'Approval button not found.', flags: discord_js_1.MessageFlags.Ephemeral });
@@ -873,8 +888,39 @@ function createInteractionCreateHandler(deps) {
873
888
  }
874
889
  if (interaction.customId.startsWith('file_open:')) {
875
890
  await interaction.deferUpdate().catch(() => { });
876
- const hashId = interaction.customId.replace('file_open:', '');
877
- const fileUrl = fileOpenCache_1.fileOpenCache.get(hashId);
891
+ let fileUrl = undefined;
892
+ const suffix = interaction.customId.substring('file_open:'.length);
893
+ if (suffix.startsWith('rel:')) {
894
+ const relativePath = suffix.substring('rel:'.length);
895
+ const workspacePath = interaction.channelId ? (deps.wsHandler.getWorkspaceForChannel(interaction.channelId) ||
896
+ deps.chatSessionRepo?.findByChannelId(interaction.channelId)?.workspacePath) : undefined;
897
+ if (workspacePath) {
898
+ fileUrl = `file:///${path_1.default.join(workspacePath, relativePath).replace(/\\/g, '/')}`;
899
+ }
900
+ }
901
+ else if (suffix.startsWith('art:')) {
902
+ const parts = suffix.substring('art:'.length).split(':');
903
+ if (parts.length >= 2) {
904
+ const convId = parts[0];
905
+ const filename = parts.slice(1).join(':');
906
+ if (deps.artifactService) {
907
+ const artifacts = deps.artifactService.listArtifacts(convId);
908
+ const matched = artifacts.find(art => art.filename.toLowerCase() === filename.toLowerCase());
909
+ if (matched) {
910
+ fileUrl = `file:///${matched.absolutePath.replace(/\\/g, '/')}`;
911
+ }
912
+ }
913
+ if (!fileUrl) {
914
+ const brainDir = path_1.default.join(require('os').homedir(), '.gemini', 'antigravity', 'brain');
915
+ fileUrl = `file:///${path_1.default.join(brainDir, convId, filename).replace(/\\/g, '/')}`;
916
+ }
917
+ }
918
+ }
919
+ else {
920
+ // Fallback to cache (for legacy file_open:<hashId> or explicit cache usage)
921
+ const hashId = suffix.startsWith('cache:') ? suffix.substring('cache:'.length) : suffix;
922
+ fileUrl = fileOpenCache_1.fileOpenCache.get(hashId);
923
+ }
878
924
  if (!fileUrl) {
879
925
  await interaction.followUp({
880
926
  content: (0, i18n_1.t)('File URL not found in cache or expired.'),
@@ -893,37 +939,58 @@ function createInteractionCreateHandler(deps) {
893
939
  }
894
940
  resolvedPath = path_1.default.resolve(rawPath);
895
941
  }
896
- let workspacePath = undefined;
897
- if (interaction.channelId) {
898
- workspacePath = deps.wsHandler.getWorkspaceForChannel(interaction.channelId) ||
899
- deps.chatSessionRepo?.findByChannelId(interaction.channelId)?.workspacePath;
900
- }
901
- if (!workspacePath) {
902
- await interaction.followUp({
903
- content: `❌ Error: Cannot open files without an active workspace.`,
904
- flags: discord_js_1.MessageFlags.Ephemeral
905
- }).catch(logger_1.logger.error);
906
- return;
907
- }
908
- const relative = path_1.default.relative(workspacePath, resolvedPath);
909
- if (relative.startsWith('..') || path_1.default.isAbsolute(relative)) {
910
- await interaction.followUp({
911
- content: `❌ Error: Cannot open files outside the workspace root.`,
912
- flags: discord_js_1.MessageFlags.Ephemeral
913
- }).catch(logger_1.logger.error);
914
- return;
942
+ const isArtifact = resolvedPath.includes('.gemini') && resolvedPath.includes('antigravity') && resolvedPath.includes('brain');
943
+ if (!isArtifact) {
944
+ let workspacePath = undefined;
945
+ if (interaction.channelId) {
946
+ workspacePath = deps.wsHandler.getWorkspaceForChannel(interaction.channelId) ||
947
+ deps.chatSessionRepo?.findByChannelId(interaction.channelId)?.workspacePath;
948
+ }
949
+ if (!workspacePath) {
950
+ await interaction.followUp({
951
+ content: `❌ Error: Cannot open files without an active workspace.`,
952
+ flags: discord_js_1.MessageFlags.Ephemeral
953
+ }).catch(logger_1.logger.error);
954
+ return;
955
+ }
956
+ const resolvedWorkspacePath = path_1.default.resolve(workspacePath);
957
+ const relative = path_1.default.relative(resolvedWorkspacePath, resolvedPath);
958
+ if (relative.startsWith('..') || path_1.default.isAbsolute(relative)) {
959
+ await interaction.followUp({
960
+ content: `❌ Error: Cannot open files outside the workspace root.`,
961
+ flags: discord_js_1.MessageFlags.Ephemeral
962
+ }).catch(logger_1.logger.error);
963
+ return;
964
+ }
915
965
  }
916
966
  try {
967
+ const comp = interaction.component;
968
+ const buttonLabel = comp?.type === 2 ? comp.label : null;
969
+ const isReviewBtn = suffix.startsWith('art:') ||
970
+ buttonLabel?.toLowerCase().includes('review') ||
971
+ resolvedPath.endsWith('walkthrough.md') ||
972
+ resolvedPath.endsWith('task.md') ||
973
+ resolvedPath.endsWith('implementation_plan.md');
917
974
  const fileContent = fs_1.default.readFileSync(resolvedPath, 'utf8');
918
975
  const MAX_DESC_LEN = 4096;
919
- const truncated = fileContent.length > MAX_DESC_LEN
976
+ let truncated = fileContent.length > MAX_DESC_LEN
920
977
  ? fileContent.substring(0, MAX_DESC_LEN - 15) + '\n\n(truncated)'
921
978
  : fileContent;
979
+ if (isReviewBtn) {
980
+ truncated += '\n\n💡 *To request changes to this artifact, simply reply in this channel.*';
981
+ }
922
982
  const embed = new discord_js_1.EmbedBuilder()
923
- .setTitle(`Opened: ${path_1.default.basename(resolvedPath)}`)
983
+ .setTitle(isReviewBtn ? `Reviewing: ${path_1.default.basename(resolvedPath)}` : `Opened: ${path_1.default.basename(resolvedPath)}`)
924
984
  .setDescription(truncated)
925
- .setColor(0x3498DB)
985
+ .setColor(isReviewBtn ? 0x2ECC71 : 0x3498DB)
926
986
  .setTimestamp();
987
+ if (isReviewBtn) {
988
+ await interaction.followUp({
989
+ embeds: [embed],
990
+ components: []
991
+ }).catch(logger_1.logger.error);
992
+ return;
993
+ }
927
994
  (0, child_process_1.execFile)((0, pathUtils_1.getAntigravityCliPath)(), [resolvedPath], async (error) => {
928
995
  if (error) {
929
996
  logger_1.logger.error(`Failed to open file via CLI: ${error.message}`);
@@ -933,9 +1000,14 @@ function createInteractionCreateHandler(deps) {
933
1000
  }).catch(() => { });
934
1001
  }
935
1002
  else {
1003
+ const closeRow = new discord_js_1.ActionRowBuilder().addComponents(new discord_js_1.ButtonBuilder()
1004
+ .setCustomId('file_close_active')
1005
+ .setLabel('Close Active Tab in IDE')
1006
+ .setStyle(discord_js_1.ButtonStyle.Secondary));
936
1007
  await interaction.followUp({
937
1008
  content: `✅ Opened file in IDE: **${path_1.default.basename(resolvedPath)}**`,
938
1009
  embeds: [embed],
1010
+ components: [closeRow],
939
1011
  flags: discord_js_1.MessageFlags.Ephemeral
940
1012
  }).catch(() => { });
941
1013
  }
@@ -949,9 +1021,38 @@ function createInteractionCreateHandler(deps) {
949
1021
  }
950
1022
  return;
951
1023
  }
1024
+ if (interaction.customId === 'file_close_active') {
1025
+ await interaction.deferUpdate().catch(() => { });
1026
+ try {
1027
+ const cdp = deps.getCurrentCdp(deps.bridge);
1028
+ if (!cdp) {
1029
+ await interaction.followUp({
1030
+ content: `❌ No active CDP connection found.`,
1031
+ flags: discord_js_1.MessageFlags.Ephemeral
1032
+ }).catch(() => { });
1033
+ return;
1034
+ }
1035
+ if (!cdp.isConnected()) {
1036
+ await cdp.connect();
1037
+ }
1038
+ await cdp.closeActiveEditor();
1039
+ await interaction.followUp({
1040
+ content: `✅ Sent close command to IDE.`,
1041
+ flags: discord_js_1.MessageFlags.Ephemeral
1042
+ }).catch(() => { });
1043
+ }
1044
+ catch (e) {
1045
+ logger_1.logger.error(`Failed to close active editor: ${e.message}`);
1046
+ await interaction.followUp({
1047
+ content: `❌ Error closing editor: ${e.message}`,
1048
+ flags: discord_js_1.MessageFlags.Ephemeral
1049
+ }).catch(() => { });
1050
+ }
1051
+ return;
1052
+ }
952
1053
  }
953
1054
  catch (error) {
954
- logger_1.logger.error('Error during button interaction handling:', error);
1055
+ logger_1.logger.error(`[InteractionCreate] Error handling interaction: ${error instanceof Error ? error.message : String(error)}`);
955
1056
  try {
956
1057
  if (!interaction.replied && !interaction.deferred) {
957
1058
  await interaction.reply({ content: 'An error occurred while processing the button action.', flags: discord_js_1.MessageFlags.Ephemeral });
@@ -1243,7 +1344,22 @@ function createInteractionCreateHandler(deps) {
1243
1344
  try {
1244
1345
  const action = (0, questionSelectAction_1.createQuestionSelectAction)({ bridge: deps.bridge, wsHandler: deps.wsHandler });
1245
1346
  const wrapped = (0, wrappers_1.wrapDiscordSelect)(interaction);
1246
- await action.execute(wrapped, interaction.values);
1347
+ const success = await action.execute(wrapped, interaction.values);
1348
+ if (success && deps.promptDispatcher && interaction.channelId && interaction.message) {
1349
+ const parsed = (0, questionActionUtils_1.parseQuestionCustomId)(interaction.customId, notificationSender_1.QUESTION_SELECT_ACTION_PREFIX);
1350
+ if (parsed) {
1351
+ const projectName = (0, projectResolver_1.resolveProjectName)({ bridge: deps.bridge, wsHandler: deps.wsHandler }, interaction.channelId, parsed.projectName);
1352
+ const cdp = projectName ? deps.bridge.pool.getConnected(projectName, resolveSelectedAccount(interaction.channelId, interaction.user.id, getParentChannelId(interaction))) : null;
1353
+ if (cdp) {
1354
+ deps.promptDispatcher.resume({
1355
+ message: interaction.message,
1356
+ prompt: '',
1357
+ cdp,
1358
+ options: buildResumeOptions()
1359
+ }).catch(e => logger_1.logger.error('[QuestionSelectAction] Failed to resume monitoring:', e));
1360
+ }
1361
+ }
1362
+ }
1247
1363
  }
1248
1364
  catch (err) {
1249
1365
  logger_1.logger.error('[QuestionSelectAction] Error executing action:', err);
@@ -1260,7 +1376,22 @@ function createInteractionCreateHandler(deps) {
1260
1376
  try {
1261
1377
  const action = (0, questionSkipAction_1.createQuestionSkipAction)({ bridge: deps.bridge, wsHandler: deps.wsHandler });
1262
1378
  const wrapped = (0, wrappers_1.wrapDiscordButton)(interaction);
1263
- await action.execute(wrapped, {});
1379
+ const success = await action.execute(wrapped, {});
1380
+ if (success && deps.promptDispatcher && interaction.channelId && interaction.message) {
1381
+ const parsed = (0, questionActionUtils_1.parseQuestionCustomId)(interaction.customId, notificationSender_1.QUESTION_SKIP_ACTION_PREFIX);
1382
+ if (parsed) {
1383
+ const projectName = (0, projectResolver_1.resolveProjectName)({ bridge: deps.bridge, wsHandler: deps.wsHandler }, interaction.channelId, parsed.projectName);
1384
+ const cdp = projectName ? deps.bridge.pool.getConnected(projectName, resolveSelectedAccount(interaction.channelId, interaction.user.id, getParentChannelId(interaction))) : null;
1385
+ if (cdp) {
1386
+ deps.promptDispatcher.resume({
1387
+ message: interaction.message,
1388
+ prompt: '',
1389
+ cdp,
1390
+ options: buildResumeOptions()
1391
+ }).catch(e => logger_1.logger.error('[QuestionSkipAction] Failed to resume monitoring:', e));
1392
+ }
1393
+ }
1394
+ }
1264
1395
  }
1265
1396
  catch (err) {
1266
1397
  logger_1.logger.error('[QuestionSkipAction] Error executing action:', err);
@@ -225,7 +225,10 @@ function createMessageCreateHandler(deps) {
225
225
  const chunk = textAttachments.slice(i, i + CONCURRENCY_LIMIT);
226
226
  const results = await Promise.all(chunk.map(async (textAtt) => {
227
227
  try {
228
- const res = await fetch(textAtt.url);
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);
229
232
  if (res.ok) {
230
233
  const content = await res.text();
231
234
  return `\n\n[Attached File: ${textAtt.name}]\n\`\`\`\n${content}\n\`\`\``;
@@ -349,9 +352,16 @@ function createMessageCreateHandler(deps) {
349
352
  }
350
353
  }
351
354
  if (!activationResult.ok) {
352
- const reason = activationResult.error ? ` (${activationResult.error})` : '';
353
- await message.reply(`⚠️ Could not route this message to the bound session (${session.displayName}). ` +
354
- `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(() => { });
355
365
  return;
356
366
  }
357
367
  }
@@ -4,7 +4,7 @@ exports.createFileChangeButtonAction = createFileChangeButtonAction;
4
4
  const cdpBridgeManager_1 = require("../services/cdpBridgeManager");
5
5
  const projectResolver_1 = require("../utils/projectResolver");
6
6
  const logger_1 = require("../utils/logger");
7
- const approvalDetector_1 = require("../services/approvalDetector");
7
+ const questionActionUtils_1 = require("../utils/questionActionUtils");
8
8
  function createFileChangeButtonAction(deps) {
9
9
  return {
10
10
  match(customId) {
@@ -36,32 +36,20 @@ function createFileChangeButtonAction(deps) {
36
36
  return;
37
37
  }
38
38
  const targetText = action === 'accept' ? 'Accept all' : 'Reject all';
39
- try {
40
- const result = await cdp.call('Runtime.evaluate', {
41
- expression: (0, approvalDetector_1.buildClickScript)(targetText),
42
- returnByValue: true,
43
- awaitPromise: true,
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);
44
48
  });
45
- if (result?.result?.value?.ok) {
46
- await interaction
47
- .update({
48
- text: `${action === 'accept' ? '✅ Accepted' : '❌ Rejected'} file changes.`,
49
- components: [],
50
- })
51
- .catch((err) => {
52
- logger_1.logger.warn('[FileChangeAction] update failed:', err);
53
- });
54
- }
55
- else {
56
- await interaction
57
- .followUp({ text: `Could not find the "${targetText}" button in the IDE. The file change prompt may have been dismissed.` })
58
- .catch(() => { });
59
- }
60
49
  }
61
- catch (error) {
62
- logger_1.logger.error('[FileChangeAction] Error clicking button:', error);
50
+ else {
63
51
  await interaction
64
- .followUp({ text: 'An error occurred while interacting with the IDE.' })
52
+ .followUp({ text: `Could not find the "${targetText}" button in the IDE. The file change prompt may have been dismissed.` })
65
53
  .catch(() => { });
66
54
  }
67
55
  },
@@ -4,14 +4,7 @@ exports.parseGenericActionCustomId = parseGenericActionCustomId;
4
4
  exports.createGenericActionButtonAction = createGenericActionButtonAction;
5
5
  const logger_1 = require("../utils/logger");
6
6
  const projectResolver_1 = require("../utils/projectResolver");
7
- const approvalDetector_1 = require("../services/approvalDetector");
8
- /**
9
- * Parses a generic action button customId into its components.
10
- * Format: action_btn_[action_name]:[project_name]:[channel_id]
11
- *
12
- * @param customId The customId to parse
13
- * @returns Parsed action details, or null if not an action button
14
- */
7
+ const questionActionUtils_1 = require("../utils/questionActionUtils");
15
8
  function parseGenericActionCustomId(customId) {
16
9
  if (!customId.startsWith('action_btn_'))
17
10
  return null;
@@ -19,10 +12,15 @@ function parseGenericActionCustomId(customId) {
19
12
  const actionName = parts[0].replace('action_btn_', '').replace(/_/g, ' ');
20
13
  // Capitalize first letter
21
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(':');
22
20
  return {
23
21
  actionName: formattedName,
24
- projectName: parts[1] || undefined,
25
- channelId: parts[2] || undefined,
22
+ projectName: projectName || undefined,
23
+ channelId: channelId || undefined,
26
24
  };
27
25
  }
28
26
  function createGenericActionButtonAction(deps) {
@@ -42,12 +40,17 @@ function createGenericActionButtonAction(deps) {
42
40
  const actionName = params.actionName;
43
41
  const channelId = params.channelId || interaction.channel?.id;
44
42
  if (!channelId) {
43
+ await interaction.reply({
44
+ text: 'Error: Cannot resolve channel ID.',
45
+ ephemeral: true,
46
+ }).catch(() => { });
45
47
  return;
46
48
  }
47
49
  const projectName = (0, projectResolver_1.resolveProjectName)(deps, channelId, params.projectName);
48
50
  await interaction.deferUpdate().catch(() => { });
51
+ const accountName = deps.bridge.selectedAccountByChannel?.get(channelId) || 'default';
49
52
  const cdp = projectName
50
- ? deps.bridge.pool.getConnected(projectName)
53
+ ? deps.bridge.pool.getConnected(projectName, accountName)
51
54
  : null;
52
55
  if (!cdp) {
53
56
  await interaction.followUp({
@@ -57,13 +60,8 @@ function createGenericActionButtonAction(deps) {
57
60
  }
58
61
  // Simulate DOM click instead of chat injection
59
62
  logger_1.logger.info(`[GenericActionButton] Clicking action button "${actionName}" via DOM script`);
60
- const script = (0, approvalDetector_1.buildClickScript)(actionName);
61
- const evalResult = await cdp.call('Runtime.evaluate', {
62
- expression: script,
63
- returnByValue: true,
64
- });
65
- const resultValue = evalResult.result?.value;
66
- if (!resultValue?.ok) {
63
+ const success = await (0, questionActionUtils_1.executeBrowserClick)(cdp, actionName);
64
+ if (!success) {
67
65
  await interaction.followUp({
68
66
  text: `Failed to execute action: Button "${actionName}" not found or obscured.`,
69
67
  }).catch(() => { });
@@ -5,11 +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");
11
44
  const projectResolver_1 = require("../utils/projectResolver");
12
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");
13
50
  const MAX_PLAN_CONTENT = 4096;
14
51
  function createPlanningButtonAction(deps) {
15
52
  return {
@@ -25,11 +62,10 @@ function createPlanningButtonAction(deps) {
25
62
  },
26
63
  async execute(interaction, params) {
27
64
  const { action, channelId } = params;
28
- // Acknowledge immediately so Telegram doesn't time out
29
- await interaction.deferUpdate().catch(() => { });
30
65
  if (channelId && channelId !== interaction.channel.id) {
66
+ await interaction.deferUpdate().catch(() => { });
31
67
  await interaction
32
- .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 })
33
69
  .catch(() => { });
34
70
  return;
35
71
  }
@@ -38,14 +74,42 @@ function createPlanningButtonAction(deps) {
38
74
  ? deps.bridge.pool.getPlanningDetector(projectName)
39
75
  : undefined;
40
76
  if (!detector) {
77
+ await interaction.deferUpdate().catch(() => { });
41
78
  await interaction
42
- .reply({ text: 'Planning detector not found.' })
79
+ .followUp({ text: 'Planning detector not found.', ephemeral: true })
43
80
  .catch(() => { });
44
81
  return;
45
82
  }
46
83
  if (action === 'open') {
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
+ }
108
+ }
109
+ // Acknowledge if we didn't show a modal
110
+ await interaction.deferUpdate().catch(() => { });
47
111
  let clicked = false;
48
- if (detector.getLastDetectedInfo()?.hasOpenButton) {
112
+ if (info?.hasOpenButton && (!isReview || modalFailed)) {
49
113
  try {
50
114
  clicked = await detector.clickOpenButton();
51
115
  if (clicked) {
@@ -56,7 +120,8 @@ function createPlanningButtonAction(deps) {
56
120
  logger_1.logger.error('[PlanningAction] clickOpenButton failed:', e);
57
121
  }
58
122
  }
59
- // Extract plan content with retry
123
+ // Since we only come here if it's NOT a review (or modal failed),
124
+ // we can just extract plan content and followUp
60
125
  let planContent = null;
61
126
  for (let attempt = 0; attempt < 3; attempt++) {
62
127
  planContent = await detector.extractPlanContent();
@@ -64,22 +129,42 @@ function createPlanningButtonAction(deps) {
64
129
  break;
65
130
  await new Promise((resolve) => setTimeout(resolve, 500));
66
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
+ }
67
152
  if (!planContent && !clicked) {
68
153
  await interaction
69
- .reply({ text: 'Plan content could not be extracted from the IDE.' })
154
+ .followUp({ text: 'Plan content could not be extracted from the IDE or workspace.' })
70
155
  .catch(() => { });
71
156
  return;
72
157
  }
73
158
  await interaction
74
- .update({
159
+ .editReply({
75
160
  text: '📋 Plan opened',
76
161
  components: [],
77
162
  })
78
163
  .catch((err) => {
79
- logger_1.logger.warn('[PlanningAction] update failed:', err);
164
+ logger_1.logger.warn('[PlanningAction] editReply failed:', err);
80
165
  });
81
166
  if (planContent) {
82
- const truncated = planContent.length > MAX_PLAN_CONTENT
167
+ let truncated = planContent.length > MAX_PLAN_CONTENT
83
168
  ? planContent.substring(0, MAX_PLAN_CONTENT - 15) + '\n\n(truncated)'
84
169
  : planContent;
85
170
  await interaction
@@ -90,13 +175,16 @@ function createPlanningButtonAction(deps) {
90
175
  }
91
176
  else {
92
177
  await interaction
93
- .followUp({ text: 'Could not extract plan content from the editor.' })
94
- .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
+ });
95
182
  }
96
183
  }
97
184
  else if (action === 'reject') {
185
+ await interaction.deferUpdate().catch(() => { });
98
186
  // Reject action
99
- await interaction.reply({ text: 'Rejection of the plan is not allowed.' }).catch(() => { });
187
+ await interaction.followUp({ text: 'Rejection of the plan is not allowed.', ephemeral: true }).catch(() => { });
100
188
  return;
101
189
  }
102
190
  else {
@@ -114,17 +202,17 @@ function createPlanningButtonAction(deps) {
114
202
  }
115
203
  if (clicked) {
116
204
  await interaction
117
- .update({
118
- text: '▶️ Proceed started',
205
+ .editReply({
206
+ text: '▶️ Proceed started.\n\n⏳ IDE is working on the response...',
119
207
  components: [],
120
208
  })
121
209
  .catch((err) => {
122
- logger_1.logger.warn('[PlanningAction] update failed:', err);
210
+ logger_1.logger.warn('[PlanningAction] editReply failed:', err);
123
211
  });
124
212
  }
125
213
  else {
126
214
  await interaction
127
- .reply({ text: 'Proceed button not found.' })
215
+ .followUp({ text: 'Proceed button not found.', ephemeral: true })
128
216
  .catch(() => { });
129
217
  }
130
218
  }