lazy-gravity 0.2.0 → 0.3.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/README.md +76 -15
- package/dist/bin/commands/doctor.js +19 -2
- package/dist/bin/commands/setup.js +286 -70
- package/dist/bot/eventRouter.js +70 -0
- package/dist/bot/index.js +353 -147
- package/dist/bot/telegramCommands.js +428 -0
- package/dist/bot/telegramMessageHandler.js +304 -0
- package/dist/bot/telegramProjectCommand.js +137 -0
- package/dist/bot/workspaceQueue.js +61 -0
- package/dist/commands/joinCommandHandler.js +4 -1
- package/dist/database/telegramBindingRepository.js +97 -0
- package/dist/database/userPreferenceRepository.js +46 -1
- package/dist/events/interactionCreateHandler.js +36 -0
- package/dist/events/messageCreateHandler.js +11 -7
- package/dist/handlers/approvalButtonAction.js +99 -0
- package/dist/handlers/autoAcceptButtonAction.js +43 -0
- package/dist/handlers/buttonHandler.js +55 -0
- package/dist/handlers/commandHandler.js +44 -0
- package/dist/handlers/errorPopupButtonAction.js +137 -0
- package/dist/handlers/messageHandler.js +70 -0
- package/dist/handlers/modeSelectAction.js +63 -0
- package/dist/handlers/modelButtonAction.js +102 -0
- package/dist/handlers/planningButtonAction.js +118 -0
- package/dist/handlers/selectHandler.js +41 -0
- package/dist/handlers/templateButtonAction.js +54 -0
- package/dist/platform/adapter.js +8 -0
- package/dist/platform/discord/discordAdapter.js +99 -0
- package/dist/platform/discord/index.js +15 -0
- package/dist/platform/discord/wrappers.js +331 -0
- package/dist/platform/index.js +18 -0
- package/dist/platform/richContentBuilder.js +76 -0
- package/dist/platform/telegram/index.js +16 -0
- package/dist/platform/telegram/telegramAdapter.js +195 -0
- package/dist/platform/telegram/telegramFormatter.js +134 -0
- package/dist/platform/telegram/wrappers.js +329 -0
- package/dist/platform/types.js +28 -0
- package/dist/services/approvalDetector.js +15 -2
- package/dist/services/cdpBridgeManager.js +91 -146
- package/dist/services/defaultModelApplicator.js +54 -0
- package/dist/services/modeService.js +16 -1
- package/dist/services/modelService.js +57 -16
- package/dist/services/notificationSender.js +149 -0
- package/dist/services/responseMonitor.js +1 -2
- package/dist/ui/autoAcceptUi.js +37 -0
- package/dist/ui/modeUi.js +38 -1
- package/dist/ui/modelsUi.js +96 -0
- package/dist/ui/outputUi.js +32 -0
- package/dist/ui/projectListUi.js +55 -0
- package/dist/ui/screenshotUi.js +26 -0
- package/dist/ui/sessionPickerUi.js +35 -1
- package/dist/ui/templateUi.js +41 -0
- package/dist/utils/configLoader.js +63 -12
- package/dist/utils/lockfile.js +5 -5
- package/dist/utils/logger.js +7 -0
- package/dist/utils/telegramImageHandler.js +127 -0
- package/package.json +4 -2
|
@@ -12,7 +12,7 @@ class UserPreferenceRepository {
|
|
|
12
12
|
this.initialize();
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
|
-
* Initialize table (create if not exists)
|
|
15
|
+
* Initialize table (create if not exists) and run migrations
|
|
16
16
|
*/
|
|
17
17
|
initialize() {
|
|
18
18
|
this.db.exec(`
|
|
@@ -24,6 +24,29 @@ class UserPreferenceRepository {
|
|
|
24
24
|
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
25
25
|
)
|
|
26
26
|
`);
|
|
27
|
+
this.migrateDefaultModel();
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Safe migration: add default_model column if it does not exist.
|
|
31
|
+
* Uses pragma when available, falls back to try/catch ALTER TABLE.
|
|
32
|
+
*/
|
|
33
|
+
migrateDefaultModel() {
|
|
34
|
+
if (typeof this.db.pragma === 'function') {
|
|
35
|
+
const columns = this.db.pragma('table_info(user_preferences)');
|
|
36
|
+
const hasColumn = columns.some(c => c.name === 'default_model');
|
|
37
|
+
if (!hasColumn) {
|
|
38
|
+
this.db.exec('ALTER TABLE user_preferences ADD COLUMN default_model TEXT DEFAULT NULL');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
else {
|
|
42
|
+
// Fallback for mock/alternate DB implementations without pragma
|
|
43
|
+
try {
|
|
44
|
+
this.db.exec('ALTER TABLE user_preferences ADD COLUMN default_model TEXT DEFAULT NULL');
|
|
45
|
+
}
|
|
46
|
+
catch {
|
|
47
|
+
// Column already exists — safe to ignore
|
|
48
|
+
}
|
|
49
|
+
}
|
|
27
50
|
}
|
|
28
51
|
/**
|
|
29
52
|
* Get the output format preference for a user.
|
|
@@ -47,6 +70,27 @@ class UserPreferenceRepository {
|
|
|
47
70
|
updated_at = datetime('now')
|
|
48
71
|
`).run(userId, format);
|
|
49
72
|
}
|
|
73
|
+
/**
|
|
74
|
+
* Get the default model for a user.
|
|
75
|
+
* Returns null if no default is stored.
|
|
76
|
+
*/
|
|
77
|
+
getDefaultModel(userId) {
|
|
78
|
+
const row = this.db.prepare('SELECT default_model FROM user_preferences WHERE user_id = ?').get(userId);
|
|
79
|
+
return row?.default_model ?? null;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Set the default model for a user (upsert).
|
|
83
|
+
* Pass null to clear the default.
|
|
84
|
+
*/
|
|
85
|
+
setDefaultModel(userId, modelName) {
|
|
86
|
+
this.db.prepare(`
|
|
87
|
+
INSERT INTO user_preferences (user_id, default_model)
|
|
88
|
+
VALUES (?, ?)
|
|
89
|
+
ON CONFLICT(user_id)
|
|
90
|
+
DO UPDATE SET default_model = excluded.default_model,
|
|
91
|
+
updated_at = datetime('now')
|
|
92
|
+
`).run(userId, modelName);
|
|
93
|
+
}
|
|
50
94
|
/**
|
|
51
95
|
* Get full preference record for a user
|
|
52
96
|
*/
|
|
@@ -64,6 +108,7 @@ class UserPreferenceRepository {
|
|
|
64
108
|
id: row.id,
|
|
65
109
|
userId: row.user_id,
|
|
66
110
|
outputFormat: row.output_format,
|
|
111
|
+
defaultModel: row.default_model ?? null,
|
|
67
112
|
createdAt: row.created_at,
|
|
68
113
|
updatedAt: row.updated_at,
|
|
69
114
|
};
|
|
@@ -378,6 +378,42 @@ function createInteractionCreateHandler(deps) {
|
|
|
378
378
|
await deps.cleanupHandler.handleCancel(interaction);
|
|
379
379
|
return;
|
|
380
380
|
}
|
|
381
|
+
if (interaction.customId === 'model_set_default_btn') {
|
|
382
|
+
await interaction.deferUpdate();
|
|
383
|
+
const cdp = deps.getCurrentCdp(deps.bridge);
|
|
384
|
+
if (!cdp) {
|
|
385
|
+
await interaction.followUp({ content: 'Not connected to CDP.', flags: discord_js_1.MessageFlags.Ephemeral });
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
const currentModel = await cdp.getCurrentModel();
|
|
389
|
+
if (!currentModel) {
|
|
390
|
+
await interaction.followUp({ content: 'No current model detected.', flags: discord_js_1.MessageFlags.Ephemeral });
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
393
|
+
deps.modelService.setDefaultModel(currentModel);
|
|
394
|
+
if (deps.userPrefRepo) {
|
|
395
|
+
deps.userPrefRepo.setDefaultModel(interaction.user.id, currentModel);
|
|
396
|
+
}
|
|
397
|
+
await deps.sendModelsUI({ editReply: async (data) => await interaction.editReply(data) }, {
|
|
398
|
+
getCurrentCdp: () => deps.getCurrentCdp(deps.bridge),
|
|
399
|
+
fetchQuota: async () => deps.bridge.quota.fetchQuota(),
|
|
400
|
+
});
|
|
401
|
+
await interaction.followUp({ content: `Default model set to **${currentModel}**.`, flags: discord_js_1.MessageFlags.Ephemeral });
|
|
402
|
+
return;
|
|
403
|
+
}
|
|
404
|
+
if (interaction.customId === 'model_clear_default_btn') {
|
|
405
|
+
await interaction.deferUpdate();
|
|
406
|
+
deps.modelService.setDefaultModel(null);
|
|
407
|
+
if (deps.userPrefRepo) {
|
|
408
|
+
deps.userPrefRepo.setDefaultModel(interaction.user.id, null);
|
|
409
|
+
}
|
|
410
|
+
await deps.sendModelsUI({ editReply: async (data) => await interaction.editReply(data) }, {
|
|
411
|
+
getCurrentCdp: () => deps.getCurrentCdp(deps.bridge),
|
|
412
|
+
fetchQuota: async () => deps.bridge.quota.fetchQuota(),
|
|
413
|
+
});
|
|
414
|
+
await interaction.followUp({ content: 'Default model cleared.', flags: discord_js_1.MessageFlags.Ephemeral });
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
381
417
|
if (interaction.customId === 'model_refresh_btn') {
|
|
382
418
|
await interaction.deferUpdate();
|
|
383
419
|
await deps.sendModelsUI({ editReply: async (data) => await interaction.editReply(data) }, {
|
|
@@ -4,6 +4,7 @@ exports.createMessageCreateHandler = createMessageCreateHandler;
|
|
|
4
4
|
const discord_js_1 = require("discord.js");
|
|
5
5
|
const messageParser_1 = require("../commands/messageParser");
|
|
6
6
|
const plainTextFormatter_1 = require("../utils/plainTextFormatter");
|
|
7
|
+
const wrappers_1 = require("../platform/discord/wrappers");
|
|
7
8
|
const cdpBridgeManager_1 = require("../services/cdpBridgeManager");
|
|
8
9
|
const modeService_1 = require("../services/modeService");
|
|
9
10
|
const imageHandler_1 = require("../utils/imageHandler");
|
|
@@ -115,6 +116,7 @@ function createMessageCreateHandler(deps) {
|
|
|
115
116
|
channelManager: deps.channelManager,
|
|
116
117
|
titleGenerator: deps.titleGenerator,
|
|
117
118
|
userPrefRepo: deps.userPrefRepo,
|
|
119
|
+
extractionMode: deps.config.extractionMode,
|
|
118
120
|
});
|
|
119
121
|
}
|
|
120
122
|
else {
|
|
@@ -162,14 +164,15 @@ function createMessageCreateHandler(deps) {
|
|
|
162
164
|
const cdp = await deps.bridge.pool.getOrConnect(workspacePath);
|
|
163
165
|
const projectName = deps.bridge.pool.extractProjectName(workspacePath);
|
|
164
166
|
deps.bridge.lastActiveWorkspace = projectName;
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
167
|
+
const platformChannel = (0, wrappers_1.wrapDiscordChannel)(message.channel);
|
|
168
|
+
deps.bridge.lastActiveChannel = platformChannel;
|
|
169
|
+
registerApprovalWorkspaceChannel(deps.bridge, projectName, platformChannel);
|
|
170
|
+
ensureApprovalDetector(deps.bridge, cdp, projectName);
|
|
171
|
+
ensureErrorPopupDetector(deps.bridge, cdp, projectName);
|
|
172
|
+
ensurePlanningDetector(deps.bridge, cdp, projectName);
|
|
170
173
|
const session = deps.chatSessionRepo.findByChannelId(message.channelId);
|
|
171
174
|
if (session?.displayName) {
|
|
172
|
-
registerApprovalSessionChannel(deps.bridge, projectName, session.displayName,
|
|
175
|
+
registerApprovalSessionChannel(deps.bridge, projectName, session.displayName, platformChannel);
|
|
173
176
|
}
|
|
174
177
|
if (session?.isRenamed && session.displayName) {
|
|
175
178
|
const activationResult = await deps.chatSessionService.activateSessionByTitle(cdp, session.displayName);
|
|
@@ -197,7 +200,7 @@ function createMessageCreateHandler(deps) {
|
|
|
197
200
|
// Re-register session channel after autoRenameChannel sets displayName
|
|
198
201
|
const updatedSession = deps.chatSessionRepo.findByChannelId(message.channelId);
|
|
199
202
|
if (updatedSession?.displayName) {
|
|
200
|
-
registerApprovalSessionChannel(deps.bridge, projectName, updatedSession.displayName,
|
|
203
|
+
registerApprovalSessionChannel(deps.bridge, projectName, updatedSession.displayName, platformChannel);
|
|
201
204
|
}
|
|
202
205
|
// Register echo hash so UserMessageDetector skips this message
|
|
203
206
|
const userMsgDetector = deps.bridge.pool.getUserMessageDetector?.(projectName);
|
|
@@ -232,6 +235,7 @@ function createMessageCreateHandler(deps) {
|
|
|
232
235
|
channelManager: deps.channelManager,
|
|
233
236
|
titleGenerator: deps.titleGenerator,
|
|
234
237
|
userPrefRepo: deps.userPrefRepo,
|
|
238
|
+
extractionMode: deps.config.extractionMode,
|
|
235
239
|
onFullCompletion: settle,
|
|
236
240
|
}).catch((err) => {
|
|
237
241
|
// sendPromptToAntigravity rejected before onFullCompletion fired
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Platform-agnostic approval button action.
|
|
4
|
+
*
|
|
5
|
+
* Handles Allow / Always Allow / Deny button presses from both Discord
|
|
6
|
+
* and Telegram using the ButtonAction interface.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.createApprovalButtonAction = createApprovalButtonAction;
|
|
10
|
+
const cdpBridgeManager_1 = require("../services/cdpBridgeManager");
|
|
11
|
+
const logger_1 = require("../utils/logger");
|
|
12
|
+
function createApprovalButtonAction(deps) {
|
|
13
|
+
return {
|
|
14
|
+
match(customId) {
|
|
15
|
+
const parsed = (0, cdpBridgeManager_1.parseApprovalCustomId)(customId);
|
|
16
|
+
if (!parsed)
|
|
17
|
+
return null;
|
|
18
|
+
return {
|
|
19
|
+
action: parsed.action,
|
|
20
|
+
projectName: parsed.projectName ?? '',
|
|
21
|
+
channelId: parsed.channelId ?? '',
|
|
22
|
+
};
|
|
23
|
+
},
|
|
24
|
+
async execute(interaction, params) {
|
|
25
|
+
const { action, channelId } = params;
|
|
26
|
+
// Acknowledge immediately so Telegram doesn't time out
|
|
27
|
+
await interaction.deferUpdate().catch(() => { });
|
|
28
|
+
// Channel scope check (skip if no channelId was encoded)
|
|
29
|
+
if (channelId && channelId !== interaction.channel.id) {
|
|
30
|
+
await interaction
|
|
31
|
+
.reply({ text: 'This approval action is linked to a different session channel.' })
|
|
32
|
+
.catch(() => { });
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const projectName = params.projectName || deps.bridge.lastActiveWorkspace;
|
|
36
|
+
logger_1.logger.debug(`[ApprovalAction] action=${action} project=${projectName ?? 'null'} channel=${interaction.channel.id}`);
|
|
37
|
+
const detector = projectName
|
|
38
|
+
? deps.bridge.pool.getApprovalDetector(projectName)
|
|
39
|
+
: undefined;
|
|
40
|
+
if (!detector) {
|
|
41
|
+
logger_1.logger.warn(`[ApprovalAction] No detector for project=${projectName}`);
|
|
42
|
+
await interaction
|
|
43
|
+
.reply({ text: 'Approval detector not found.' })
|
|
44
|
+
.catch(() => { });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const lastInfo = detector.getLastDetectedInfo();
|
|
48
|
+
logger_1.logger.debug(`[ApprovalAction] lastDetectedInfo: ${lastInfo ? JSON.stringify(lastInfo) : 'null'}`);
|
|
49
|
+
let success = false;
|
|
50
|
+
let actionLabel = '';
|
|
51
|
+
try {
|
|
52
|
+
if (action === 'approve') {
|
|
53
|
+
success = await detector.approveButton();
|
|
54
|
+
actionLabel = 'Allow';
|
|
55
|
+
}
|
|
56
|
+
else if (action === 'always_allow') {
|
|
57
|
+
success = await detector.alwaysAllowButton();
|
|
58
|
+
actionLabel = 'Allow Chat';
|
|
59
|
+
}
|
|
60
|
+
else {
|
|
61
|
+
success = await detector.denyButton();
|
|
62
|
+
actionLabel = 'Deny';
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch (err) {
|
|
66
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
67
|
+
logger_1.logger.error(`[ApprovalAction] CDP click failed: ${msg}`);
|
|
68
|
+
await interaction
|
|
69
|
+
.reply({ text: `Approval failed: ${msg}` })
|
|
70
|
+
.catch(() => { });
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
logger_1.logger.debug(`[ApprovalAction] ${actionLabel} result: ${success}`);
|
|
74
|
+
if (success) {
|
|
75
|
+
// Remove buttons by editing the original message.
|
|
76
|
+
// If update() fails, fall back to editReply(), then followUp().
|
|
77
|
+
const updatePayload = { text: `✅ ${actionLabel} completed`, components: [] };
|
|
78
|
+
try {
|
|
79
|
+
await interaction.update(updatePayload);
|
|
80
|
+
}
|
|
81
|
+
catch (updateErr) {
|
|
82
|
+
logger_1.logger.warn('[ApprovalAction] update failed, trying editReply:', updateErr);
|
|
83
|
+
try {
|
|
84
|
+
await interaction.editReply(updatePayload);
|
|
85
|
+
}
|
|
86
|
+
catch (editErr) {
|
|
87
|
+
logger_1.logger.warn('[ApprovalAction] editReply failed, sending followUp:', editErr);
|
|
88
|
+
await interaction.followUp({ text: `✅ ${actionLabel} completed` }).catch(() => { });
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
await interaction
|
|
94
|
+
.reply({ text: 'Approval button not found.' })
|
|
95
|
+
.catch(() => { });
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Platform-agnostic ButtonAction for auto-accept toggle interactions.
|
|
4
|
+
*
|
|
5
|
+
* Handles:
|
|
6
|
+
* autoaccept_btn_on — Enable auto-accept
|
|
7
|
+
* autoaccept_btn_off — Disable auto-accept
|
|
8
|
+
* autoaccept_btn_refresh — Refresh the auto-accept UI
|
|
9
|
+
*/
|
|
10
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
11
|
+
exports.createAutoAcceptButtonAction = createAutoAcceptButtonAction;
|
|
12
|
+
const autoAcceptUi_1 = require("../ui/autoAcceptUi");
|
|
13
|
+
function createAutoAcceptButtonAction(deps) {
|
|
14
|
+
return {
|
|
15
|
+
match(customId) {
|
|
16
|
+
if (customId === autoAcceptUi_1.AUTOACCEPT_BTN_ON)
|
|
17
|
+
return { action: 'on' };
|
|
18
|
+
if (customId === autoAcceptUi_1.AUTOACCEPT_BTN_OFF)
|
|
19
|
+
return { action: 'off' };
|
|
20
|
+
if (customId === autoAcceptUi_1.AUTOACCEPT_BTN_REFRESH)
|
|
21
|
+
return { action: 'refresh' };
|
|
22
|
+
return null;
|
|
23
|
+
},
|
|
24
|
+
async execute(interaction, params) {
|
|
25
|
+
await interaction.deferUpdate();
|
|
26
|
+
if (params.action === 'on' || params.action === 'off') {
|
|
27
|
+
const result = deps.autoAcceptService.handle(params.action);
|
|
28
|
+
// Only update UI if the state actually changed to avoid
|
|
29
|
+
// Telegram "message is not modified" error.
|
|
30
|
+
if (result.changed) {
|
|
31
|
+
const payload = (0, autoAcceptUi_1.buildAutoAcceptPayload)(deps.autoAcceptService.isEnabled());
|
|
32
|
+
await interaction.update(payload);
|
|
33
|
+
}
|
|
34
|
+
await interaction.followUp({ text: result.message }).catch(() => { });
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
// refresh — always update to show latest state
|
|
38
|
+
const payload = (0, autoAcceptUi_1.buildAutoAcceptPayload)(deps.autoAcceptService.isEnabled());
|
|
39
|
+
await interaction.update(payload);
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Platform-agnostic button interaction handler.
|
|
4
|
+
*
|
|
5
|
+
* Uses a registry pattern: each button type registers a match+execute pair.
|
|
6
|
+
* The first matching action wins (order matters).
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.createPlatformButtonHandler = createPlatformButtonHandler;
|
|
10
|
+
const logger_1 = require("../utils/logger");
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Factory
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
/**
|
|
15
|
+
* Create a platform-agnostic button interaction handler.
|
|
16
|
+
* Returns an async function that processes PlatformButtonInteraction events.
|
|
17
|
+
*/
|
|
18
|
+
function createPlatformButtonHandler(deps) {
|
|
19
|
+
return async (interaction) => {
|
|
20
|
+
for (const action of deps.actions) {
|
|
21
|
+
let params;
|
|
22
|
+
try {
|
|
23
|
+
params = action.match(interaction.customId);
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
27
|
+
logger_1.logger.error('[ButtonHandler] Match error:', errorMessage);
|
|
28
|
+
await interaction
|
|
29
|
+
.reply({
|
|
30
|
+
text: 'An error occurred while processing the button action.',
|
|
31
|
+
ephemeral: true,
|
|
32
|
+
})
|
|
33
|
+
.catch(() => { });
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
if (params !== null) {
|
|
37
|
+
try {
|
|
38
|
+
await action.execute(interaction, params);
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
42
|
+
logger_1.logger.error('[ButtonHandler] Action error:', errorMessage);
|
|
43
|
+
await interaction
|
|
44
|
+
.reply({
|
|
45
|
+
text: 'An error occurred while processing the button action.',
|
|
46
|
+
ephemeral: true,
|
|
47
|
+
})
|
|
48
|
+
.catch(() => { });
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
logger_1.logger.warn(`[ButtonHandler] No handler for customId: ${interaction.customId}`);
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Platform-agnostic slash command handler.
|
|
4
|
+
*
|
|
5
|
+
* Maintains a lookup map from command name to CommandDef for O(1) dispatch.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.createPlatformCommandHandler = createPlatformCommandHandler;
|
|
9
|
+
const logger_1 = require("../utils/logger");
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
// Factory
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
/**
|
|
14
|
+
* Create a platform-agnostic slash command handler.
|
|
15
|
+
* Returns an async function that processes PlatformCommandInteraction events.
|
|
16
|
+
*/
|
|
17
|
+
function createPlatformCommandHandler(deps) {
|
|
18
|
+
const commandMap = new Map();
|
|
19
|
+
for (const cmd of deps.commands) {
|
|
20
|
+
commandMap.set(cmd.name, cmd);
|
|
21
|
+
}
|
|
22
|
+
return async (interaction) => {
|
|
23
|
+
const cmd = commandMap.get(interaction.commandName);
|
|
24
|
+
if (!cmd) {
|
|
25
|
+
logger_1.logger.warn(`[CommandHandler] Unknown command: ${interaction.commandName}`);
|
|
26
|
+
await interaction.editReply({
|
|
27
|
+
text: `Unknown command: ${interaction.commandName}`,
|
|
28
|
+
}).catch(() => { });
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
await cmd.execute(interaction);
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
36
|
+
logger_1.logger.error(`[CommandHandler] Command "${interaction.commandName}" error:`, errorMessage);
|
|
37
|
+
await interaction
|
|
38
|
+
.editReply({
|
|
39
|
+
text: 'An error occurred while processing the command.',
|
|
40
|
+
})
|
|
41
|
+
.catch(() => { });
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Platform-agnostic error popup button action.
|
|
4
|
+
*
|
|
5
|
+
* Handles Dismiss / Copy Debug / Retry button presses for the error
|
|
6
|
+
* popup dialog from both Discord and Telegram using the ButtonAction interface.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.createErrorPopupButtonAction = createErrorPopupButtonAction;
|
|
10
|
+
const cdpBridgeManager_1 = require("../services/cdpBridgeManager");
|
|
11
|
+
const logger_1 = require("../utils/logger");
|
|
12
|
+
const MAX_DEBUG_CONTENT = 4096;
|
|
13
|
+
function createErrorPopupButtonAction(deps) {
|
|
14
|
+
return {
|
|
15
|
+
match(customId) {
|
|
16
|
+
const parsed = (0, cdpBridgeManager_1.parseErrorPopupCustomId)(customId);
|
|
17
|
+
if (!parsed)
|
|
18
|
+
return null;
|
|
19
|
+
return {
|
|
20
|
+
action: parsed.action,
|
|
21
|
+
projectName: parsed.projectName ?? '',
|
|
22
|
+
channelId: parsed.channelId ?? '',
|
|
23
|
+
};
|
|
24
|
+
},
|
|
25
|
+
async execute(interaction, params) {
|
|
26
|
+
const { action, channelId } = params;
|
|
27
|
+
if (channelId && channelId !== interaction.channel.id) {
|
|
28
|
+
await interaction
|
|
29
|
+
.reply({ text: 'This error popup action is linked to a different session channel.' })
|
|
30
|
+
.catch(() => { });
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const projectName = params.projectName || deps.bridge.lastActiveWorkspace;
|
|
34
|
+
const detector = projectName
|
|
35
|
+
? deps.bridge.pool.getErrorPopupDetector(projectName)
|
|
36
|
+
: undefined;
|
|
37
|
+
if (!detector) {
|
|
38
|
+
await interaction
|
|
39
|
+
.reply({ text: 'Error popup detector not found.' })
|
|
40
|
+
.catch(() => { });
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
// Acknowledge immediately so Telegram doesn't time out
|
|
44
|
+
await interaction.deferUpdate().catch(() => { });
|
|
45
|
+
if (action === 'dismiss') {
|
|
46
|
+
let clicked = false;
|
|
47
|
+
try {
|
|
48
|
+
clicked = await detector.clickDismissButton();
|
|
49
|
+
}
|
|
50
|
+
catch (err) {
|
|
51
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
52
|
+
logger_1.logger.error(`[ErrorPopupAction] CDP click failed: ${msg}`);
|
|
53
|
+
await interaction.reply({ text: `Dismiss failed: ${msg}` }).catch(() => { });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (clicked) {
|
|
57
|
+
await interaction
|
|
58
|
+
.update({
|
|
59
|
+
text: '🗑️ Dismissed',
|
|
60
|
+
components: [],
|
|
61
|
+
})
|
|
62
|
+
.catch((err) => {
|
|
63
|
+
logger_1.logger.warn('[ErrorPopupAction] update failed:', err);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
await interaction
|
|
68
|
+
.reply({ text: 'Dismiss button not found.' })
|
|
69
|
+
.catch(() => { });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
else if (action === 'copy_debug') {
|
|
73
|
+
const clicked = await detector.clickCopyDebugInfoButton();
|
|
74
|
+
if (!clicked) {
|
|
75
|
+
await interaction
|
|
76
|
+
.reply({ text: 'Copy debug info button not found.' })
|
|
77
|
+
.catch(() => { });
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
// Wait for clipboard to be populated
|
|
81
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
82
|
+
const clipboardContent = await detector.readClipboard();
|
|
83
|
+
await interaction
|
|
84
|
+
.update({
|
|
85
|
+
text: '📋 Debug info copied',
|
|
86
|
+
components: [],
|
|
87
|
+
})
|
|
88
|
+
.catch((err) => {
|
|
89
|
+
logger_1.logger.warn('[ErrorPopupAction] update failed:', err);
|
|
90
|
+
});
|
|
91
|
+
if (clipboardContent) {
|
|
92
|
+
const truncated = clipboardContent.length > MAX_DEBUG_CONTENT
|
|
93
|
+
? clipboardContent.substring(0, MAX_DEBUG_CONTENT - 15) + '\n\n(truncated)'
|
|
94
|
+
: clipboardContent;
|
|
95
|
+
await interaction
|
|
96
|
+
.followUp({ text: truncated })
|
|
97
|
+
.catch((err) => {
|
|
98
|
+
logger_1.logger.warn('[ErrorPopupAction] followUp failed:', err);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
await interaction
|
|
103
|
+
.followUp({ text: 'Could not read debug info from clipboard.' })
|
|
104
|
+
.catch(() => { });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
// Retry action
|
|
109
|
+
let clicked = false;
|
|
110
|
+
try {
|
|
111
|
+
clicked = await detector.clickRetryButton();
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
115
|
+
logger_1.logger.error(`[ErrorPopupAction] CDP click failed: ${msg}`);
|
|
116
|
+
await interaction.reply({ text: `Retry failed: ${msg}` }).catch(() => { });
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (clicked) {
|
|
120
|
+
await interaction
|
|
121
|
+
.update({
|
|
122
|
+
text: '🔄 Retry initiated',
|
|
123
|
+
components: [],
|
|
124
|
+
})
|
|
125
|
+
.catch((err) => {
|
|
126
|
+
logger_1.logger.warn('[ErrorPopupAction] update failed:', err);
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
await interaction
|
|
131
|
+
.reply({ text: 'Retry button not found.' })
|
|
132
|
+
.catch(() => { });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Platform-agnostic message handler.
|
|
4
|
+
*
|
|
5
|
+
* Extracts core message-handling logic from Discord-specific event handlers
|
|
6
|
+
* into a platform-independent factory that works with any PlatformMessage.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.createPlatformMessageHandler = createPlatformMessageHandler;
|
|
10
|
+
const logger_1 = require("../utils/logger");
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Factory
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
/**
|
|
15
|
+
* Create a platform-agnostic message handler.
|
|
16
|
+
* Returns an async function that processes PlatformMessage events.
|
|
17
|
+
*/
|
|
18
|
+
function createPlatformMessageHandler(deps) {
|
|
19
|
+
return async (message) => {
|
|
20
|
+
// Skip bot messages
|
|
21
|
+
if (message.author.isBot)
|
|
22
|
+
return;
|
|
23
|
+
const content = message.content.trim();
|
|
24
|
+
if (!content && message.attachments.length === 0)
|
|
25
|
+
return;
|
|
26
|
+
// Check for text commands (prefixed with !)
|
|
27
|
+
if (content.startsWith('!')) {
|
|
28
|
+
const parts = content.slice(1).split(/\s+/);
|
|
29
|
+
const commandName = parts[0]?.toLowerCase();
|
|
30
|
+
const args = parts.slice(1);
|
|
31
|
+
if (commandName && deps.handleTextCommand) {
|
|
32
|
+
try {
|
|
33
|
+
const handled = await deps.handleTextCommand(message, commandName, args);
|
|
34
|
+
if (handled)
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
39
|
+
logger_1.logger.error('[MessageHandler] Text command error:', errorMessage);
|
|
40
|
+
await message
|
|
41
|
+
.reply({
|
|
42
|
+
text: 'An error occurred while processing the command.',
|
|
43
|
+
})
|
|
44
|
+
.catch(() => { });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
// Route to workspace
|
|
50
|
+
const workspacePath = deps.getWorkspaceForChannel(message.channel.id);
|
|
51
|
+
if (!workspacePath) {
|
|
52
|
+
await message.reply({
|
|
53
|
+
text: 'No project is configured for this channel. Please create or select one with `/project`.',
|
|
54
|
+
});
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
const promptText = content ||
|
|
58
|
+
'Please review the attached images and respond accordingly.';
|
|
59
|
+
try {
|
|
60
|
+
await deps.sendPrompt(message, workspacePath, promptText);
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
64
|
+
logger_1.logger.error('[MessageHandler] Failed to send prompt:', errorMessage);
|
|
65
|
+
await message
|
|
66
|
+
.reply({ text: 'An error occurred while processing your message.' })
|
|
67
|
+
.catch(() => { });
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|