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.
- package/dist/bin/commands/doctor.js +45 -7
- package/dist/bot/index.js +401 -41
- package/dist/bot/telegramMessageHandler.js +1 -0
- package/dist/commands/chatCommandHandler.js +43 -3
- package/dist/commands/registerSlashCommands.js +13 -1
- package/dist/events/interactionCreateHandler.js +456 -27
- package/dist/events/messageCreateHandler.js +66 -7
- package/dist/handlers/__tests__/fileChangeButtonAction.test.js +85 -0
- package/dist/handlers/approvalButtonAction.js +2 -1
- package/dist/handlers/errorPopupButtonAction.js +2 -1
- package/dist/handlers/fileChangeButtonAction.js +57 -0
- package/dist/handlers/genericActionButtonAction.js +72 -0
- package/dist/handlers/planningButtonAction.js +126 -23
- package/dist/handlers/planningModalSubmitAction.js +38 -0
- package/dist/handlers/questionSelectAction.js +70 -0
- package/dist/handlers/questionSkipAction.js +68 -0
- package/dist/handlers/runCommandButtonAction.js +2 -1
- package/dist/platform/discord/discordResponseRenderer.js +164 -0
- package/dist/platform/discord/wrappers.js +76 -0
- package/dist/services/approvalDetector.js +110 -35
- package/dist/services/artifactService.js +103 -37
- package/dist/services/assistantDomExtractor.js +194 -3
- package/dist/services/cdpBridgeManager.js +149 -8
- package/dist/services/cdpConnectionPool.js +22 -2
- package/dist/services/cdpService.js +134 -15
- package/dist/services/chatSessionService.js +147 -40
- package/dist/services/errorPopupDetector.js +23 -11
- package/dist/services/notificationSender.js +80 -3
- package/dist/services/planningDetector.js +69 -25
- package/dist/services/promptDispatcher.js +27 -1
- package/dist/services/questionDetector.js +428 -0
- package/dist/services/responseMonitor.js +45 -3
- package/dist/services/runCommandDetector.js +14 -7
- package/dist/ui/artifactsUi.js +4 -3
- package/dist/utils/consecutiveEmptyPollGate.js +21 -0
- package/dist/utils/fileOpenCache.js +29 -0
- package/dist/utils/htmlToDiscordMarkdown.js +5 -2
- package/dist/utils/pathUtils.js +12 -0
- package/dist/utils/projectResolver.js +10 -0
- package/dist/utils/questionActionUtils.js +47 -0
- 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
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
|
304
|
-
|
|
305
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
.
|
|
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 =
|
|
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
|
-
.
|
|
79
|
+
.followUp({ text: 'Planning detector not found.', ephemeral: true })
|
|
42
80
|
.catch(() => { });
|
|
43
81
|
return;
|
|
44
82
|
}
|
|
45
83
|
if (action === 'open') {
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
//
|
|
54
|
-
await
|
|
55
|
-
|
|
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
|
-
.
|
|
159
|
+
.editReply({
|
|
65
160
|
text: 'š Plan opened',
|
|
66
161
|
components: [],
|
|
67
162
|
})
|
|
68
163
|
.catch((err) => {
|
|
69
|
-
logger_1.logger.warn('[PlanningAction]
|
|
164
|
+
logger_1.logger.warn('[PlanningAction] editReply failed:', err);
|
|
70
165
|
});
|
|
71
166
|
if (planContent) {
|
|
72
|
-
|
|
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: '
|
|
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
|
-
.
|
|
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]
|
|
210
|
+
logger_1.logger.warn('[PlanningAction] editReply failed:', err);
|
|
108
211
|
});
|
|
109
212
|
}
|
|
110
213
|
else {
|
|
111
214
|
await interaction
|
|
112
|
-
.
|
|
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
|
+
}
|