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
@@ -0,0 +1,70 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createQuestionSelectAction = createQuestionSelectAction;
4
+ const logger_1 = require("../utils/logger");
5
+ const notificationSender_1 = require("../services/notificationSender");
6
+ const questionActionUtils_1 = require("../utils/questionActionUtils");
7
+ const projectResolver_1 = require("../utils/projectResolver");
8
+ function createQuestionSelectAction(deps) {
9
+ return {
10
+ match(customId) {
11
+ return customId.startsWith(notificationSender_1.QUESTION_SELECT_ACTION_PREFIX);
12
+ },
13
+ async execute(interaction, values) {
14
+ const parsed = (0, questionActionUtils_1.parseQuestionCustomId)(interaction.customId, notificationSender_1.QUESTION_SELECT_ACTION_PREFIX);
15
+ if (!parsed)
16
+ return false;
17
+ const selectedOption = parseInt(values[0], 10);
18
+ if (isNaN(selectedOption))
19
+ return false;
20
+ const channelId = parsed.channelId;
21
+ if (channelId && channelId !== interaction.channel.id) {
22
+ await interaction
23
+ .reply({ text: 'This question is linked to a different session channel.', ephemeral: true })
24
+ .catch(() => { });
25
+ return false;
26
+ }
27
+ const projectName = (0, projectResolver_1.resolveProjectName)(deps, interaction.channel.id, parsed.projectName);
28
+ logger_1.logger.debug(`[QuestionSelectAction] project=${projectName ?? 'null'} channel=${interaction.channel.id}`);
29
+ const detector = projectName
30
+ ? deps.bridge.pool.getQuestionDetector(projectName)
31
+ : undefined;
32
+ if (!detector) {
33
+ logger_1.logger.warn(`[QuestionSelectAction] No detector for project=${projectName}`);
34
+ await interaction
35
+ .reply({ text: 'Question detector not found.', ephemeral: true })
36
+ .catch(() => { });
37
+ return false;
38
+ }
39
+ await interaction.deferUpdate().catch(() => { });
40
+ let success = false;
41
+ try {
42
+ success = await detector.submitOption(selectedOption);
43
+ }
44
+ catch (err) {
45
+ const msg = err instanceof Error ? err.message : String(err);
46
+ logger_1.logger.error(`[QuestionSelectAction] CDP click failed: ${msg}`);
47
+ await interaction
48
+ .followUp({ text: `Failed to answer question: ${msg}`, ephemeral: true })
49
+ .catch(() => { });
50
+ return false;
51
+ }
52
+ if (success) {
53
+ const updatePayload = { text: `✅ Submitted option ${selectedOption + 1}.\n\n⏳ IDE is working on the response...`, components: [] };
54
+ try {
55
+ await interaction.editReply(updatePayload);
56
+ }
57
+ catch (editErr) {
58
+ logger_1.logger.warn('[QuestionSelectAction] editReply failed, sending followUp:', editErr);
59
+ await interaction.followUp({ text: `✅ Submitted option ${selectedOption + 1}.\n\n⏳ IDE is working on the response...` }).catch(() => { });
60
+ }
61
+ }
62
+ else {
63
+ await interaction
64
+ .followUp({ text: 'Failed to answer question (it may have been resolved already).', ephemeral: true })
65
+ .catch(() => { });
66
+ }
67
+ return success;
68
+ },
69
+ };
70
+ }
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createQuestionSkipAction = createQuestionSkipAction;
4
+ const logger_1 = require("../utils/logger");
5
+ const notificationSender_1 = require("../services/notificationSender");
6
+ const questionActionUtils_1 = require("../utils/questionActionUtils");
7
+ const projectResolver_1 = require("../utils/projectResolver");
8
+ function createQuestionSkipAction(deps) {
9
+ return {
10
+ match(customId) {
11
+ const parsed = (0, questionActionUtils_1.parseQuestionCustomId)(customId, notificationSender_1.QUESTION_SKIP_ACTION_PREFIX);
12
+ return parsed ? { action: parsed.action } : null;
13
+ },
14
+ async execute(interaction, params) {
15
+ const parsed = (0, questionActionUtils_1.parseQuestionCustomId)(interaction.customId, notificationSender_1.QUESTION_SKIP_ACTION_PREFIX);
16
+ if (!parsed)
17
+ return false;
18
+ const channelId = parsed.channelId;
19
+ if (channelId && channelId !== interaction.channel.id) {
20
+ await interaction
21
+ .reply({ text: 'This question is linked to a different session channel.', ephemeral: true })
22
+ .catch(() => { });
23
+ return false;
24
+ }
25
+ const projectName = (0, projectResolver_1.resolveProjectName)(deps, interaction.channel.id, parsed.projectName);
26
+ logger_1.logger.debug(`[QuestionSkipAction] project=${projectName ?? 'null'} channel=${interaction.channel.id}`);
27
+ const detector = projectName
28
+ ? deps.bridge.pool.getQuestionDetector(projectName)
29
+ : undefined;
30
+ if (!detector) {
31
+ logger_1.logger.warn(`[QuestionSkipAction] No detector for project=${projectName}`);
32
+ await interaction
33
+ .reply({ text: 'Question detector not found.', ephemeral: true })
34
+ .catch(() => { });
35
+ return false;
36
+ }
37
+ await interaction.deferUpdate().catch(() => { });
38
+ let success = false;
39
+ try {
40
+ success = await detector.skipQuestion();
41
+ }
42
+ catch (err) {
43
+ const msg = err instanceof Error ? err.message : String(err);
44
+ logger_1.logger.error(`[QuestionSkipAction] CDP click failed: ${msg}`);
45
+ await interaction
46
+ .followUp({ text: `Failed to skip question: ${msg}`, ephemeral: true })
47
+ .catch(() => { });
48
+ return false;
49
+ }
50
+ if (success) {
51
+ const updatePayload = { text: '✅ Skipped question.\n\n⏳ IDE is working on the response...', components: [] };
52
+ try {
53
+ await interaction.editReply(updatePayload);
54
+ }
55
+ catch (editErr) {
56
+ logger_1.logger.warn('[QuestionSkipAction] editReply failed, sending followUp:', editErr);
57
+ await interaction.followUp({ text: '✅ Skipped question.\n\n⏳ IDE is working on the response...' }).catch(() => { });
58
+ }
59
+ }
60
+ else {
61
+ await interaction
62
+ .followUp({ text: 'Failed to skip question (it may have been resolved already).', ephemeral: true })
63
+ .catch(() => { });
64
+ }
65
+ return success;
66
+ },
67
+ };
68
+ }
@@ -8,6 +8,7 @@
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.createRunCommandButtonAction = createRunCommandButtonAction;
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 createRunCommandButtonAction(deps) {
13
14
  return {
@@ -31,7 +32,7 @@ function createRunCommandButtonAction(deps) {
31
32
  .catch(() => { });
32
33
  return;
33
34
  }
34
- const projectName = params.projectName || deps.bridge.lastActiveWorkspace;
35
+ const projectName = (0, projectResolver_1.resolveProjectName)(deps, interaction.channel.id, params.projectName);
35
36
  const detector = projectName
36
37
  ? deps.bridge.pool.getRunCommandDetector(projectName)
37
38
  : undefined;
@@ -0,0 +1,164 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.renderDiscordResponse = renderDiscordResponse;
4
+ const discord_js_1 = require("discord.js");
5
+ /**
6
+ * Splits a long string into chunks that fit within Discord's embed description limit (4096 chars).
7
+ * Tries to split on double newlines, then single newlines, then spaces, before hard splitting.
8
+ */
9
+ function chunkText(text, limit = 4000) {
10
+ if (!text)
11
+ return [];
12
+ if (text.length <= limit)
13
+ return [text];
14
+ const chunks = [];
15
+ let remaining = text;
16
+ while (remaining.length > 0) {
17
+ if (remaining.length <= limit) {
18
+ chunks.push(remaining);
19
+ break;
20
+ }
21
+ let slice = remaining.slice(0, limit);
22
+ let splitIndex = slice.lastIndexOf('\n\n');
23
+ if (splitIndex === -1 || splitIndex < limit * 0.5) {
24
+ splitIndex = slice.lastIndexOf('\n');
25
+ }
26
+ if (splitIndex === -1 || splitIndex < limit * 0.5) {
27
+ splitIndex = slice.lastIndexOf(' ');
28
+ }
29
+ if (splitIndex === -1 || splitIndex < limit * 0.5) {
30
+ splitIndex = limit; // Hard split
31
+ }
32
+ chunks.push(remaining.slice(0, splitIndex).trim());
33
+ remaining = remaining.slice(splitIndex).trim();
34
+ }
35
+ return chunks;
36
+ }
37
+ /**
38
+ * Renders a classified Antigravity response into a Discord Message payload.
39
+ *
40
+ * Handles chunking for large outputs and structured cards (plan, files, actions).
41
+ */
42
+ function renderDiscordResponse(result, options) {
43
+ const embeds = [];
44
+ const components = [];
45
+ // 1. Final Output Text
46
+ if (result.finalOutputText) {
47
+ const textChunks = chunkText(result.finalOutputText, 4000); // leave some buffer
48
+ for (let i = 0; i < textChunks.length && i < 8; i++) { // max 10 embeds total, leave room
49
+ const embed = new discord_js_1.EmbedBuilder()
50
+ .setColor(0x2b2d31) // Discord dark mode background-ish / neutral
51
+ .setDescription(textChunks[i]);
52
+ embeds.push(embed);
53
+ }
54
+ }
55
+ // 2. Plan Cards
56
+ if (result.planCards && result.planCards.length > 0) {
57
+ const planText = result.planCards.join('\n\n');
58
+ const chunks = chunkText(planText, 4000);
59
+ for (let i = 0; i < chunks.length && embeds.length < 10; i++) {
60
+ const embed = new discord_js_1.EmbedBuilder()
61
+ .setTitle(i === 0 ? 'Plan' : null)
62
+ .setColor(0x5865f2) // Blurple
63
+ .setDescription(chunks[i]);
64
+ embeds.push(embed);
65
+ }
66
+ }
67
+ // 3. File Changes
68
+ if (result.fileChanges && result.fileChanges.length > 0) {
69
+ const embed = new discord_js_1.EmbedBuilder()
70
+ .setTitle('File Changes')
71
+ .setColor(0x3ba55c); // Greenish
72
+ // Chunk into fields if necessary (max 25 fields, 1024 chars per field)
73
+ let currentFieldVal = '';
74
+ let fieldCount = 0;
75
+ let processedCount = 0;
76
+ let truncated = false;
77
+ for (const file of result.fileChanges) {
78
+ if (fieldCount >= 24) {
79
+ truncated = true;
80
+ break;
81
+ }
82
+ const line = `- \`${file.path}\` (${file.type})\n`;
83
+ if (line.length > 1000) {
84
+ if (currentFieldVal) {
85
+ embed.addFields({ name: fieldCount === 0 ? 'Files' : '...', value: currentFieldVal });
86
+ currentFieldVal = '';
87
+ fieldCount++;
88
+ }
89
+ const splitLine = chunkText(line, 1000);
90
+ for (const sl of splitLine) {
91
+ if (fieldCount >= 24) {
92
+ truncated = true;
93
+ break;
94
+ }
95
+ embed.addFields({ name: fieldCount === 0 ? 'Files' : '...', value: sl || '-' });
96
+ fieldCount++;
97
+ }
98
+ }
99
+ else if (currentFieldVal.length + line.length > 1000) {
100
+ if (currentFieldVal) {
101
+ embed.addFields({ name: fieldCount === 0 ? 'Files' : '...', value: currentFieldVal });
102
+ fieldCount++;
103
+ }
104
+ currentFieldVal = line;
105
+ }
106
+ else {
107
+ currentFieldVal += line;
108
+ }
109
+ if (!truncated) {
110
+ processedCount++;
111
+ }
112
+ }
113
+ if (currentFieldVal && fieldCount < 24) {
114
+ embed.addFields({ name: fieldCount === 0 ? 'Files' : '...', value: currentFieldVal });
115
+ fieldCount++;
116
+ }
117
+ if (processedCount < result.fileChanges.length) {
118
+ const remaining = result.fileChanges.length - processedCount;
119
+ embed.addFields({ name: '...', value: `*...and ${remaining} more*` });
120
+ }
121
+ embeds.push(embed);
122
+ }
123
+ // 4. Action Buttons
124
+ if (result.actionButtons && result.actionButtons.length > 0) {
125
+ const row = new discord_js_1.ActionRowBuilder();
126
+ for (let i = 0; i < result.actionButtons.length && i < 5; i++) { // Max 5 buttons per row
127
+ const btnText = result.actionButtons[i];
128
+ let customId = `action_btn_${btnText.toLowerCase().replace(/\s+/g, '_')}`;
129
+ if (options?.projectName && options?.channelId) {
130
+ const overhead = customId.length + 2 + options.channelId.length;
131
+ const safeProject = options.projectName.length > (100 - overhead)
132
+ ? options.projectName.substring(0, 100 - overhead)
133
+ : options.projectName;
134
+ customId = `${customId}:${safeProject}:${options.channelId}`;
135
+ }
136
+ const btn = new discord_js_1.ButtonBuilder()
137
+ .setCustomId(customId)
138
+ .setLabel(btnText)
139
+ .setStyle(btnText.toLowerCase() === 'proceed' || btnText.toLowerCase() === 'open'
140
+ ? discord_js_1.ButtonStyle.Primary
141
+ : discord_js_1.ButtonStyle.Secondary);
142
+ row.addComponents(btn);
143
+ }
144
+ components.push(row);
145
+ }
146
+ // 5. Fallback if absolutely empty
147
+ if (embeds.length === 0) {
148
+ if (result.activityLines && result.activityLines.length > 0) {
149
+ const embed = new discord_js_1.EmbedBuilder()
150
+ .setColor(0x2b2d31)
151
+ .setDescription('*(Processing...)*\n' + result.activityLines.slice(-3).join('\n').slice(0, 3900));
152
+ embeds.push(embed);
153
+ }
154
+ else {
155
+ return { content: '*(No output)*' };
156
+ }
157
+ }
158
+ // Discord allows max 10 embeds per message.
159
+ const finalEmbeds = embeds.slice(0, 10);
160
+ return {
161
+ embeds: finalEmbeds,
162
+ components: components.length > 0 ? components : undefined,
163
+ };
164
+ }
@@ -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
+ }