lazy-gravity 0.10.0 → 0.12.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 (33) hide show
  1. package/README.md +7 -0
  2. package/dist/bin/commands/doctor.js +45 -7
  3. package/dist/bot/index.js +526 -24
  4. package/dist/commands/chatCommandHandler.js +11 -41
  5. package/dist/commands/registerSlashCommands.js +62 -0
  6. package/dist/database/scheduleRepository.js +50 -6
  7. package/dist/events/interactionCreateHandler.js +163 -28
  8. package/dist/events/messageCreateHandler.js +24 -28
  9. package/dist/handlers/fileChangeButtonAction.js +12 -24
  10. package/dist/handlers/genericActionButtonAction.js +16 -18
  11. package/dist/handlers/planningButtonAction.js +105 -17
  12. package/dist/handlers/planningModalSubmitAction.js +38 -0
  13. package/dist/handlers/questionSelectAction.js +8 -7
  14. package/dist/handlers/questionSkipAction.js +7 -6
  15. package/dist/platform/discord/wrappers.js +76 -0
  16. package/dist/services/approvalDetector.js +43 -14
  17. package/dist/services/artifactService.js +61 -21
  18. package/dist/services/assistantDomExtractor.js +23 -3
  19. package/dist/services/cdpBridgeManager.js +35 -2
  20. package/dist/services/cdpConnectionPool.js +7 -2
  21. package/dist/services/cdpService.js +113 -8
  22. package/dist/services/chatSessionService.js +15 -8
  23. package/dist/services/heartbeatService.js +261 -0
  24. package/dist/services/notificationSender.js +12 -2
  25. package/dist/services/planningDetector.js +1 -1
  26. package/dist/services/promptDispatcher.js +21 -1
  27. package/dist/services/questionDetector.js +128 -76
  28. package/dist/services/responseMonitor.js +17 -1
  29. package/dist/services/scheduleService.js +101 -4
  30. package/dist/utils/configLoader.js +8 -0
  31. package/dist/utils/fileOpenCache.js +2 -6
  32. package/dist/utils/questionActionUtils.js +22 -0
  33. package/package.json +2 -1
@@ -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
  }
@@ -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
+ }
@@ -13,16 +13,16 @@ function createQuestionSelectAction(deps) {
13
13
  async execute(interaction, values) {
14
14
  const parsed = (0, questionActionUtils_1.parseQuestionCustomId)(interaction.customId, notificationSender_1.QUESTION_SELECT_ACTION_PREFIX);
15
15
  if (!parsed)
16
- return;
16
+ return false;
17
17
  const selectedOption = parseInt(values[0], 10);
18
18
  if (isNaN(selectedOption))
19
- return;
19
+ return false;
20
20
  const channelId = parsed.channelId;
21
21
  if (channelId && channelId !== interaction.channel.id) {
22
22
  await interaction
23
23
  .reply({ text: 'This question is linked to a different session channel.', ephemeral: true })
24
24
  .catch(() => { });
25
- return;
25
+ return false;
26
26
  }
27
27
  const projectName = (0, projectResolver_1.resolveProjectName)(deps, interaction.channel.id, parsed.projectName);
28
28
  logger_1.logger.debug(`[QuestionSelectAction] project=${projectName ?? 'null'} channel=${interaction.channel.id}`);
@@ -34,7 +34,7 @@ function createQuestionSelectAction(deps) {
34
34
  await interaction
35
35
  .reply({ text: 'Question detector not found.', ephemeral: true })
36
36
  .catch(() => { });
37
- return;
37
+ return false;
38
38
  }
39
39
  await interaction.deferUpdate().catch(() => { });
40
40
  let success = false;
@@ -47,16 +47,16 @@ function createQuestionSelectAction(deps) {
47
47
  await interaction
48
48
  .followUp({ text: `Failed to answer question: ${msg}`, ephemeral: true })
49
49
  .catch(() => { });
50
- return;
50
+ return false;
51
51
  }
52
52
  if (success) {
53
- const updatePayload = { text: `✅ Submitted option ${selectedOption + 1}`, components: [] };
53
+ const updatePayload = { text: `✅ Submitted option ${selectedOption + 1}.\n\n⏳ IDE is working on the response...`, components: [] };
54
54
  try {
55
55
  await interaction.editReply(updatePayload);
56
56
  }
57
57
  catch (editErr) {
58
58
  logger_1.logger.warn('[QuestionSelectAction] editReply failed, sending followUp:', editErr);
59
- await interaction.followUp({ text: `✅ Submitted option ${selectedOption + 1}` }).catch(() => { });
59
+ await interaction.followUp({ text: `✅ Submitted option ${selectedOption + 1}.\n\n⏳ IDE is working on the response...` }).catch(() => { });
60
60
  }
61
61
  }
62
62
  else {
@@ -64,6 +64,7 @@ function createQuestionSelectAction(deps) {
64
64
  .followUp({ text: 'Failed to answer question (it may have been resolved already).', ephemeral: true })
65
65
  .catch(() => { });
66
66
  }
67
+ return success;
67
68
  },
68
69
  };
69
70
  }
@@ -14,13 +14,13 @@ function createQuestionSkipAction(deps) {
14
14
  async execute(interaction, params) {
15
15
  const parsed = (0, questionActionUtils_1.parseQuestionCustomId)(interaction.customId, notificationSender_1.QUESTION_SKIP_ACTION_PREFIX);
16
16
  if (!parsed)
17
- return;
17
+ return false;
18
18
  const channelId = parsed.channelId;
19
19
  if (channelId && channelId !== interaction.channel.id) {
20
20
  await interaction
21
21
  .reply({ text: 'This question is linked to a different session channel.', ephemeral: true })
22
22
  .catch(() => { });
23
- return;
23
+ return false;
24
24
  }
25
25
  const projectName = (0, projectResolver_1.resolveProjectName)(deps, interaction.channel.id, parsed.projectName);
26
26
  logger_1.logger.debug(`[QuestionSkipAction] project=${projectName ?? 'null'} channel=${interaction.channel.id}`);
@@ -32,7 +32,7 @@ function createQuestionSkipAction(deps) {
32
32
  await interaction
33
33
  .reply({ text: 'Question detector not found.', ephemeral: true })
34
34
  .catch(() => { });
35
- return;
35
+ return false;
36
36
  }
37
37
  await interaction.deferUpdate().catch(() => { });
38
38
  let success = false;
@@ -45,16 +45,16 @@ function createQuestionSkipAction(deps) {
45
45
  await interaction
46
46
  .followUp({ text: `Failed to skip question: ${msg}`, ephemeral: true })
47
47
  .catch(() => { });
48
- return;
48
+ return false;
49
49
  }
50
50
  if (success) {
51
- const updatePayload = { text: `✅ Skipped question`, components: [] };
51
+ const updatePayload = { text: '✅ Skipped question.\n\n⏳ IDE is working on the response...', components: [] };
52
52
  try {
53
53
  await interaction.editReply(updatePayload);
54
54
  }
55
55
  catch (editErr) {
56
56
  logger_1.logger.warn('[QuestionSkipAction] editReply failed, sending followUp:', editErr);
57
- await interaction.followUp({ text: `✅ Skipped question` }).catch(() => { });
57
+ await interaction.followUp({ text: '✅ Skipped question.\n\n⏳ IDE is working on the response...' }).catch(() => { });
58
58
  }
59
59
  }
60
60
  else {
@@ -62,6 +62,7 @@ function createQuestionSkipAction(deps) {
62
62
  .followUp({ text: 'Failed to skip question (it may have been resolved already).', ephemeral: true })
63
63
  .catch(() => { });
64
64
  }
65
+ return success;
65
66
  },
66
67
  };
67
68
  }
@@ -15,6 +15,7 @@ exports.wrapDiscordMessage = wrapDiscordMessage;
15
15
  exports.wrapDiscordButton = wrapDiscordButton;
16
16
  exports.wrapDiscordSelect = wrapDiscordSelect;
17
17
  exports.wrapDiscordCommand = wrapDiscordCommand;
18
+ exports.wrapDiscordModalSubmit = wrapDiscordModalSubmit;
18
19
  const discord_js_1 = require("discord.js");
19
20
  // ---------------------------------------------------------------------------
20
21
  // Style mapping
@@ -107,6 +108,32 @@ function toDiscordComponents(rows) {
107
108
  return actionRow;
108
109
  });
109
110
  }
111
+ /**
112
+ * Convert a platform ModalDef to a discord.js ModalBuilder.
113
+ */
114
+ function toDiscordModal(modal) {
115
+ const builder = new discord_js_1.ModalBuilder()
116
+ .setCustomId(modal.customId)
117
+ .setTitle(modal.title);
118
+ for (const row of modal.components) {
119
+ const actionRow = new discord_js_1.ActionRowBuilder();
120
+ for (const comp of row.components) {
121
+ const input = new discord_js_1.TextInputBuilder()
122
+ .setCustomId(comp.customId)
123
+ .setLabel(comp.label)
124
+ .setStyle(comp.style === 'paragraph' ? discord_js_1.TextInputStyle.Paragraph : discord_js_1.TextInputStyle.Short);
125
+ if (comp.placeholder !== undefined) {
126
+ input.setPlaceholder(comp.placeholder);
127
+ }
128
+ if (comp.required !== undefined) {
129
+ input.setRequired(comp.required);
130
+ }
131
+ actionRow.addComponents(input);
132
+ }
133
+ builder.addComponents(actionRow);
134
+ }
135
+ return builder;
136
+ }
110
137
  /**
111
138
  * Convert a platform MessagePayload to discord.js message send/reply options.
112
139
  *
@@ -256,6 +283,9 @@ function wrapDiscordButton(interaction) {
256
283
  const sent = await interaction.followUp(toDiscordPayload(payload, EPHEMERAL_ALLOWED));
257
284
  return wrapDiscordSentMessage(sent);
258
285
  },
286
+ async showModal(modal) {
287
+ await interaction.showModal(toDiscordModal(modal));
288
+ },
259
289
  };
260
290
  }
261
291
  /** Wrap a discord.js StringSelectMenuInteraction as a PlatformSelectInteraction. */
@@ -329,3 +359,49 @@ function wrapDiscordCommand(interaction) {
329
359
  },
330
360
  };
331
361
  }
362
+ /** Wrap a discord.js ModalSubmitInteraction as a PlatformModalSubmitInteraction. */
363
+ function wrapDiscordModalSubmit(interaction) {
364
+ const user = wrapDiscordUser(interaction.user);
365
+ const channel = interaction.channel
366
+ ? wrapDiscordChannel(interaction.channel)
367
+ : buildFallbackChannel(interaction.channelId ?? 'unknown');
368
+ const fields = new Map();
369
+ for (const [key, field] of interaction.fields.fields.entries()) {
370
+ fields.set(key, interaction.fields.getTextInputValue(key));
371
+ }
372
+ return {
373
+ id: interaction.id,
374
+ platform: 'discord',
375
+ customId: interaction.customId,
376
+ user,
377
+ channel,
378
+ messageId: interaction.message?.id ?? null,
379
+ fields,
380
+ async deferUpdate() {
381
+ if (interaction.isFromMessage()) {
382
+ await interaction.deferUpdate();
383
+ }
384
+ else {
385
+ await interaction.deferReply({ ephemeral: true });
386
+ }
387
+ },
388
+ async reply(payload) {
389
+ await interaction.reply(toDiscordPayload(payload, EPHEMERAL_ALLOWED));
390
+ },
391
+ async update(payload) {
392
+ if (interaction.isFromMessage()) {
393
+ await interaction.update(toDiscordPayload(payload, EPHEMERAL_ALLOWED));
394
+ }
395
+ else {
396
+ await interaction.reply(toDiscordPayload(payload, EPHEMERAL_ALLOWED));
397
+ }
398
+ },
399
+ async editReply(payload) {
400
+ await interaction.editReply(toDiscordPayload(payload, EPHEMERAL_ALLOWED));
401
+ },
402
+ async followUp(payload) {
403
+ const sent = await interaction.followUp(toDiscordPayload(payload, EPHEMERAL_ALLOWED));
404
+ return wrapDiscordSentMessage(sent);
405
+ },
406
+ };
407
+ }