lazy-gravity 0.9.1 → 0.10.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/bot/index.js +233 -41
- package/dist/bot/telegramJoinCommand.js +9 -0
- package/dist/bot/telegramMessageHandler.js +7 -0
- package/dist/commands/chatCommandHandler.js +71 -1
- package/dist/commands/registerSlashCommands.js +13 -1
- package/dist/events/interactionCreateHandler.js +324 -26
- package/dist/events/messageCreateHandler.js +53 -4
- 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 +69 -0
- package/dist/handlers/genericActionButtonAction.js +74 -0
- package/dist/handlers/planningButtonAction.js +24 -9
- package/dist/handlers/questionSelectAction.js +69 -0
- package/dist/handlers/questionSkipAction.js +67 -0
- package/dist/handlers/runCommandButtonAction.js +2 -1
- package/dist/platform/discord/discordResponseRenderer.js +164 -0
- package/dist/services/approvalDetector.js +70 -24
- package/dist/services/artifactService.js +57 -31
- package/dist/services/assistantDomExtractor.js +174 -3
- package/dist/services/cdpBridgeManager.js +114 -6
- package/dist/services/cdpConnectionPool.js +15 -0
- package/dist/services/cdpService.js +21 -7
- package/dist/services/chatSessionService.js +148 -27
- package/dist/services/errorPopupDetector.js +23 -11
- package/dist/services/notificationSender.js +70 -3
- package/dist/services/planningDetector.js +69 -25
- package/dist/services/promptDispatcher.js +7 -1
- package/dist/services/questionDetector.js +376 -0
- package/dist/services/responseMonitor.js +28 -2
- 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 +33 -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 +25 -0
- package/package.json +2 -2
|
@@ -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,69 @@
|
|
|
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 approvalDetector_1 = require("../services/approvalDetector");
|
|
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
|
+
try {
|
|
40
|
+
const result = await cdp.call('Runtime.evaluate', {
|
|
41
|
+
expression: (0, approvalDetector_1.buildClickScript)(targetText),
|
|
42
|
+
returnByValue: true,
|
|
43
|
+
awaitPromise: true,
|
|
44
|
+
});
|
|
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
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
logger_1.logger.error('[FileChangeAction] Error clicking button:', error);
|
|
63
|
+
await interaction
|
|
64
|
+
.followUp({ text: 'An error occurred while interacting with the IDE.' })
|
|
65
|
+
.catch(() => { });
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
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 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
|
+
*/
|
|
15
|
+
function parseGenericActionCustomId(customId) {
|
|
16
|
+
if (!customId.startsWith('action_btn_'))
|
|
17
|
+
return null;
|
|
18
|
+
const parts = customId.split(':');
|
|
19
|
+
const actionName = parts[0].replace('action_btn_', '').replace(/_/g, ' ');
|
|
20
|
+
// Capitalize first letter
|
|
21
|
+
const formattedName = actionName.charAt(0).toUpperCase() + actionName.slice(1);
|
|
22
|
+
return {
|
|
23
|
+
actionName: formattedName,
|
|
24
|
+
projectName: parts[1] || undefined,
|
|
25
|
+
channelId: parts[2] || undefined,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function createGenericActionButtonAction(deps) {
|
|
29
|
+
return {
|
|
30
|
+
match(customId) {
|
|
31
|
+
const parsed = parseGenericActionCustomId(customId);
|
|
32
|
+
if (!parsed)
|
|
33
|
+
return null;
|
|
34
|
+
const result = { actionName: parsed.actionName };
|
|
35
|
+
if (parsed.projectName)
|
|
36
|
+
result.projectName = parsed.projectName;
|
|
37
|
+
if (parsed.channelId)
|
|
38
|
+
result.channelId = parsed.channelId;
|
|
39
|
+
return result;
|
|
40
|
+
},
|
|
41
|
+
async execute(interaction, params) {
|
|
42
|
+
const actionName = params.actionName;
|
|
43
|
+
const channelId = params.channelId || interaction.channel?.id;
|
|
44
|
+
if (!channelId) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const projectName = (0, projectResolver_1.resolveProjectName)(deps, channelId, params.projectName);
|
|
48
|
+
await interaction.deferUpdate().catch(() => { });
|
|
49
|
+
const cdp = projectName
|
|
50
|
+
? deps.bridge.pool.getConnected(projectName)
|
|
51
|
+
: null;
|
|
52
|
+
if (!cdp) {
|
|
53
|
+
await interaction.followUp({
|
|
54
|
+
text: 'Not connected to Antigravity. Send the action as a message instead.',
|
|
55
|
+
}).catch(() => { });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
// Simulate DOM click instead of chat injection
|
|
59
|
+
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) {
|
|
67
|
+
await interaction.followUp({
|
|
68
|
+
text: `Failed to execute action: Button "${actionName}" not found or obscured.`,
|
|
69
|
+
}).catch(() => { });
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
9
|
exports.createPlanningButtonAction = createPlanningButtonAction;
|
|
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
|
const MAX_PLAN_CONTENT = 4096;
|
|
13
14
|
function createPlanningButtonAction(deps) {
|
|
@@ -32,7 +33,7 @@ function createPlanningButtonAction(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
|
const detector = projectName
|
|
37
38
|
? deps.bridge.pool.getPlanningDetector(projectName)
|
|
38
39
|
: undefined;
|
|
@@ -43,15 +44,18 @@ function createPlanningButtonAction(deps) {
|
|
|
43
44
|
return;
|
|
44
45
|
}
|
|
45
46
|
if (action === 'open') {
|
|
46
|
-
|
|
47
|
-
if (
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
47
|
+
let clicked = false;
|
|
48
|
+
if (detector.getLastDetectedInfo()?.hasOpenButton) {
|
|
49
|
+
try {
|
|
50
|
+
clicked = await detector.clickOpenButton();
|
|
51
|
+
if (clicked) {
|
|
52
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
catch (e) {
|
|
56
|
+
logger_1.logger.error('[PlanningAction] clickOpenButton failed:', e);
|
|
57
|
+
}
|
|
52
58
|
}
|
|
53
|
-
// Wait for DOM to update after Open click
|
|
54
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
55
59
|
// Extract plan content with retry
|
|
56
60
|
let planContent = null;
|
|
57
61
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
@@ -60,6 +64,12 @@ function createPlanningButtonAction(deps) {
|
|
|
60
64
|
break;
|
|
61
65
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
62
66
|
}
|
|
67
|
+
if (!planContent && !clicked) {
|
|
68
|
+
await interaction
|
|
69
|
+
.reply({ text: 'Plan content could not be extracted from the IDE.' })
|
|
70
|
+
.catch(() => { });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
63
73
|
await interaction
|
|
64
74
|
.update({
|
|
65
75
|
text: '📋 Plan opened',
|
|
@@ -84,6 +94,11 @@ function createPlanningButtonAction(deps) {
|
|
|
84
94
|
.catch(() => { });
|
|
85
95
|
}
|
|
86
96
|
}
|
|
97
|
+
else if (action === 'reject') {
|
|
98
|
+
// Reject action
|
|
99
|
+
await interaction.reply({ text: 'Rejection of the plan is not allowed.' }).catch(() => { });
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
87
102
|
else {
|
|
88
103
|
// Proceed action
|
|
89
104
|
await interaction.deferUpdate().catch(() => { });
|
|
@@ -0,0 +1,69 @@
|
|
|
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;
|
|
17
|
+
const selectedOption = parseInt(values[0], 10);
|
|
18
|
+
if (isNaN(selectedOption))
|
|
19
|
+
return;
|
|
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;
|
|
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;
|
|
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;
|
|
51
|
+
}
|
|
52
|
+
if (success) {
|
|
53
|
+
const updatePayload = { text: `✅ Submitted option ${selectedOption + 1}`, 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}` }).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
|
+
},
|
|
68
|
+
};
|
|
69
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
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;
|
|
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;
|
|
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;
|
|
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;
|
|
49
|
+
}
|
|
50
|
+
if (success) {
|
|
51
|
+
const updatePayload = { text: `✅ Skipped question`, 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` }).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
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
@@ -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 =
|
|
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
|
+
}
|