lazy-gravity 0.1.0 → 0.2.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 +18 -6
- package/dist/bin/cli.js +18 -18
- package/dist/bin/commands/doctor.js +2 -1
- package/dist/bin/commands/start.js +25 -2
- package/dist/bot/index.js +346 -152
- package/dist/commands/joinCommandHandler.js +302 -0
- package/dist/commands/joinDetachCommandHandler.js +285 -0
- package/dist/commands/registerSlashCommands.js +35 -0
- package/dist/database/chatSessionRepository.js +10 -0
- package/dist/database/userPreferenceRepository.js +72 -0
- package/dist/events/interactionCreateHandler.js +58 -36
- package/dist/events/messageCreateHandler.js +158 -53
- package/dist/services/antigravityLauncher.js +4 -3
- package/dist/services/approvalDetector.js +6 -0
- package/dist/services/cdpBridgeManager.js +184 -84
- package/dist/services/cdpConnectionPool.js +79 -51
- package/dist/services/cdpService.js +149 -51
- package/dist/services/chatSessionService.js +229 -8
- package/dist/services/errorPopupDetector.js +6 -0
- package/dist/services/planningDetector.js +6 -0
- package/dist/services/responseMonitor.js +125 -24
- package/dist/services/updateCheckService.js +147 -0
- package/dist/services/userMessageDetector.js +221 -0
- package/dist/ui/modeUi.js +11 -1
- package/dist/ui/outputUi.js +30 -0
- package/dist/ui/sessionPickerUi.js +48 -0
- package/dist/utils/antigravityPaths.js +94 -0
- package/dist/utils/configLoader.js +10 -0
- package/dist/utils/discordButtonUtils.js +33 -0
- package/dist/utils/logBuffer.js +47 -0
- package/dist/utils/logger.js +80 -20
- package/dist/utils/pathUtils.js +57 -0
- package/dist/utils/plainTextFormatter.js +70 -0
- package/package.json +4 -4
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.UserPreferenceRepository = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Repository class for SQLite persistence of per-user preferences.
|
|
6
|
+
* Currently stores output format preference (embed vs plain text).
|
|
7
|
+
*/
|
|
8
|
+
class UserPreferenceRepository {
|
|
9
|
+
db;
|
|
10
|
+
constructor(db) {
|
|
11
|
+
this.db = db;
|
|
12
|
+
this.initialize();
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Initialize table (create if not exists)
|
|
16
|
+
*/
|
|
17
|
+
initialize() {
|
|
18
|
+
this.db.exec(`
|
|
19
|
+
CREATE TABLE IF NOT EXISTS user_preferences (
|
|
20
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
21
|
+
user_id TEXT NOT NULL UNIQUE,
|
|
22
|
+
output_format TEXT NOT NULL DEFAULT 'embed',
|
|
23
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
24
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
25
|
+
)
|
|
26
|
+
`);
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Get the output format preference for a user.
|
|
30
|
+
* Returns 'embed' as default if no preference is stored.
|
|
31
|
+
*/
|
|
32
|
+
getOutputFormat(userId) {
|
|
33
|
+
const row = this.db.prepare('SELECT output_format FROM user_preferences WHERE user_id = ?').get(userId);
|
|
34
|
+
if (!row)
|
|
35
|
+
return 'embed';
|
|
36
|
+
return row.output_format === 'plain' ? 'plain' : 'embed';
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Set the output format preference for a user (upsert).
|
|
40
|
+
*/
|
|
41
|
+
setOutputFormat(userId, format) {
|
|
42
|
+
this.db.prepare(`
|
|
43
|
+
INSERT INTO user_preferences (user_id, output_format)
|
|
44
|
+
VALUES (?, ?)
|
|
45
|
+
ON CONFLICT(user_id)
|
|
46
|
+
DO UPDATE SET output_format = excluded.output_format,
|
|
47
|
+
updated_at = datetime('now')
|
|
48
|
+
`).run(userId, format);
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Get full preference record for a user
|
|
52
|
+
*/
|
|
53
|
+
findByUserId(userId) {
|
|
54
|
+
const row = this.db.prepare('SELECT * FROM user_preferences WHERE user_id = ?').get(userId);
|
|
55
|
+
if (!row)
|
|
56
|
+
return undefined;
|
|
57
|
+
return this.mapRow(row);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Map a DB row to UserPreferenceRecord
|
|
61
|
+
*/
|
|
62
|
+
mapRow(row) {
|
|
63
|
+
return {
|
|
64
|
+
id: row.id,
|
|
65
|
+
userId: row.user_id,
|
|
66
|
+
outputFormat: row.output_format,
|
|
67
|
+
createdAt: row.created_at,
|
|
68
|
+
updatedAt: row.updated_at,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
exports.UserPreferenceRepository = UserPreferenceRepository;
|
|
@@ -4,37 +4,14 @@ exports.createInteractionCreateHandler = createInteractionCreateHandler;
|
|
|
4
4
|
const discord_js_1 = require("discord.js");
|
|
5
5
|
const i18n_1 = require("../utils/i18n");
|
|
6
6
|
const logger_1 = require("../utils/logger");
|
|
7
|
+
const discordButtonUtils_1 = require("../utils/discordButtonUtils");
|
|
7
8
|
const templateUi_1 = require("../ui/templateUi");
|
|
8
9
|
const autoAcceptUi_1 = require("../ui/autoAcceptUi");
|
|
10
|
+
const outputUi_1 = require("../ui/outputUi");
|
|
9
11
|
const cleanupCommandHandler_1 = require("../commands/cleanupCommandHandler");
|
|
10
12
|
const projectListUi_1 = require("../ui/projectListUi");
|
|
11
13
|
const modeService_1 = require("../services/modeService");
|
|
12
|
-
|
|
13
|
-
function disableAllButtons(components) {
|
|
14
|
-
return components
|
|
15
|
-
.map((row) => {
|
|
16
|
-
const rowAny = row;
|
|
17
|
-
if (!Array.isArray(rowAny.components))
|
|
18
|
-
return null;
|
|
19
|
-
const nextRow = new discord_js_1.ActionRowBuilder();
|
|
20
|
-
const disabledButtons = rowAny.components
|
|
21
|
-
.map((component) => {
|
|
22
|
-
const componentType = component?.type ?? component?.data?.type;
|
|
23
|
-
if (componentType !== 2)
|
|
24
|
-
return null;
|
|
25
|
-
const payload = typeof component?.toJSON === 'function'
|
|
26
|
-
? component.toJSON()
|
|
27
|
-
: component;
|
|
28
|
-
return discord_js_1.ButtonBuilder.from(payload).setDisabled(true);
|
|
29
|
-
})
|
|
30
|
-
.filter((button) => button !== null);
|
|
31
|
-
if (disabledButtons.length === 0)
|
|
32
|
-
return null;
|
|
33
|
-
nextRow.addComponents(...disabledButtons);
|
|
34
|
-
return nextRow;
|
|
35
|
-
})
|
|
36
|
-
.filter((row) => row !== null);
|
|
37
|
-
}
|
|
14
|
+
const sessionPickerUi_1 = require("../ui/sessionPickerUi");
|
|
38
15
|
function createInteractionCreateHandler(deps) {
|
|
39
16
|
return async (interaction) => {
|
|
40
17
|
if (interaction.isButton()) {
|
|
@@ -52,9 +29,9 @@ function createInteractionCreateHandler(deps) {
|
|
|
52
29
|
}).catch(logger_1.logger.error);
|
|
53
30
|
return;
|
|
54
31
|
}
|
|
55
|
-
const
|
|
56
|
-
const detector =
|
|
57
|
-
? deps.bridge.pool.getApprovalDetector(
|
|
32
|
+
const projectName = approvalAction.projectName ?? deps.bridge.lastActiveWorkspace;
|
|
33
|
+
const detector = projectName
|
|
34
|
+
? deps.bridge.pool.getApprovalDetector(projectName)
|
|
58
35
|
: undefined;
|
|
59
36
|
if (!detector) {
|
|
60
37
|
try {
|
|
@@ -90,7 +67,7 @@ function createInteractionCreateHandler(deps) {
|
|
|
90
67
|
.setTimestamp();
|
|
91
68
|
await interaction.update({
|
|
92
69
|
embeds: [updatedEmbed],
|
|
93
|
-
components: disableAllButtons(interaction.message.components),
|
|
70
|
+
components: (0, discordButtonUtils_1.disableAllButtons)(interaction.message.components),
|
|
94
71
|
});
|
|
95
72
|
}
|
|
96
73
|
else {
|
|
@@ -122,7 +99,7 @@ function createInteractionCreateHandler(deps) {
|
|
|
122
99
|
}).catch(logger_1.logger.error);
|
|
123
100
|
return;
|
|
124
101
|
}
|
|
125
|
-
const planWorkspaceDirName = planningAction.
|
|
102
|
+
const planWorkspaceDirName = planningAction.projectName ?? deps.bridge.lastActiveWorkspace;
|
|
126
103
|
const planDetector = planWorkspaceDirName
|
|
127
104
|
? deps.bridge.pool.getPlanningDetector(planWorkspaceDirName)
|
|
128
105
|
: undefined;
|
|
@@ -201,7 +178,7 @@ function createInteractionCreateHandler(deps) {
|
|
|
201
178
|
try {
|
|
202
179
|
await interaction.update({
|
|
203
180
|
embeds: [updatedEmbed],
|
|
204
|
-
components: disableAllButtons(interaction.message.components),
|
|
181
|
+
components: (0, discordButtonUtils_1.disableAllButtons)(interaction.message.components),
|
|
205
182
|
});
|
|
206
183
|
}
|
|
207
184
|
catch (interactionError) {
|
|
@@ -248,7 +225,7 @@ function createInteractionCreateHandler(deps) {
|
|
|
248
225
|
}).catch(logger_1.logger.error);
|
|
249
226
|
return;
|
|
250
227
|
}
|
|
251
|
-
const errorWorkspaceDirName = errorPopupAction.
|
|
228
|
+
const errorWorkspaceDirName = errorPopupAction.projectName ?? deps.bridge.lastActiveWorkspace;
|
|
252
229
|
const errorDetector = errorWorkspaceDirName
|
|
253
230
|
? deps.bridge.pool.getErrorPopupDetector(errorWorkspaceDirName)
|
|
254
231
|
: undefined;
|
|
@@ -274,7 +251,7 @@ function createInteractionCreateHandler(deps) {
|
|
|
274
251
|
try {
|
|
275
252
|
await interaction.update({
|
|
276
253
|
embeds: [updatedEmbed],
|
|
277
|
-
components: disableAllButtons(interaction.message.components),
|
|
254
|
+
components: (0, discordButtonUtils_1.disableAllButtons)(interaction.message.components),
|
|
278
255
|
});
|
|
279
256
|
}
|
|
280
257
|
catch (interactionError) {
|
|
@@ -351,7 +328,7 @@ function createInteractionCreateHandler(deps) {
|
|
|
351
328
|
try {
|
|
352
329
|
await interaction.update({
|
|
353
330
|
embeds: [updatedEmbed],
|
|
354
|
-
components: disableAllButtons(interaction.message.components),
|
|
331
|
+
components: (0, discordButtonUtils_1.disableAllButtons)(interaction.message.components),
|
|
355
332
|
});
|
|
356
333
|
}
|
|
357
334
|
catch (interactionError) {
|
|
@@ -446,6 +423,20 @@ function createInteractionCreateHandler(deps) {
|
|
|
446
423
|
});
|
|
447
424
|
return;
|
|
448
425
|
}
|
|
426
|
+
if (interaction.customId === outputUi_1.OUTPUT_BTN_EMBED || interaction.customId === outputUi_1.OUTPUT_BTN_PLAIN) {
|
|
427
|
+
if (deps.userPrefRepo) {
|
|
428
|
+
await interaction.deferUpdate();
|
|
429
|
+
const format = interaction.customId === outputUi_1.OUTPUT_BTN_PLAIN ? 'plain' : 'embed';
|
|
430
|
+
deps.userPrefRepo.setOutputFormat(interaction.user.id, format);
|
|
431
|
+
await (0, outputUi_1.sendOutputUI)({ editReply: async (data) => await interaction.editReply(data) }, format);
|
|
432
|
+
const label = format === 'plain' ? 'Plain Text' : 'Embed';
|
|
433
|
+
await interaction.followUp({
|
|
434
|
+
content: `Output format changed to **${label}**.`,
|
|
435
|
+
flags: discord_js_1.MessageFlags.Ephemeral,
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
449
440
|
if (interaction.customId.startsWith(`${projectListUi_1.PROJECT_PAGE_PREFIX}:`)) {
|
|
450
441
|
const page = (0, projectListUi_1.parseProjectPageId)(interaction.customId);
|
|
451
442
|
if (!isNaN(page) && page >= 0) {
|
|
@@ -519,6 +510,32 @@ function createInteractionCreateHandler(deps) {
|
|
|
519
510
|
}
|
|
520
511
|
return;
|
|
521
512
|
}
|
|
513
|
+
if (interaction.isStringSelectMenu() && (0, sessionPickerUi_1.isSessionSelectId)(interaction.customId)) {
|
|
514
|
+
if (!deps.config.allowedUserIds.includes(interaction.user.id)) {
|
|
515
|
+
await interaction.reply({ content: (0, i18n_1.t)('You do not have permission.'), flags: discord_js_1.MessageFlags.Ephemeral }).catch(logger_1.logger.error);
|
|
516
|
+
return;
|
|
517
|
+
}
|
|
518
|
+
try {
|
|
519
|
+
await interaction.deferUpdate();
|
|
520
|
+
}
|
|
521
|
+
catch (deferError) {
|
|
522
|
+
if (deferError?.code === 10062 || deferError?.code === 40060) {
|
|
523
|
+
logger_1.logger.warn('[SessionSelect] deferUpdate expired. Skipping.');
|
|
524
|
+
return;
|
|
525
|
+
}
|
|
526
|
+
logger_1.logger.error('[SessionSelect] deferUpdate failed:', deferError);
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
try {
|
|
530
|
+
if (deps.joinHandler) {
|
|
531
|
+
await deps.joinHandler.handleJoinSelect(interaction, deps.bridge);
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
catch (error) {
|
|
535
|
+
logger_1.logger.error('Session selection error:', error);
|
|
536
|
+
}
|
|
537
|
+
return;
|
|
538
|
+
}
|
|
522
539
|
if (interaction.isStringSelectMenu() && (0, projectListUi_1.isProjectSelectId)(interaction.customId)) {
|
|
523
540
|
if (!deps.config.allowedUserIds.includes(interaction.user.id)) {
|
|
524
541
|
await interaction.reply({ content: (0, i18n_1.t)('You do not have permission.'), flags: discord_js_1.MessageFlags.Ephemeral }).catch(logger_1.logger.error);
|
|
@@ -547,7 +564,12 @@ function createInteractionCreateHandler(deps) {
|
|
|
547
564
|
return;
|
|
548
565
|
}
|
|
549
566
|
try {
|
|
550
|
-
|
|
567
|
+
if (commandInteraction.commandName === 'logs') {
|
|
568
|
+
await commandInteraction.deferReply({ flags: discord_js_1.MessageFlags.Ephemeral });
|
|
569
|
+
}
|
|
570
|
+
else {
|
|
571
|
+
await commandInteraction.deferReply();
|
|
572
|
+
}
|
|
551
573
|
}
|
|
552
574
|
catch (deferError) {
|
|
553
575
|
if (deferError?.code === 10062) {
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.createMessageCreateHandler = createMessageCreateHandler;
|
|
4
4
|
const discord_js_1 = require("discord.js");
|
|
5
5
|
const messageParser_1 = require("../commands/messageParser");
|
|
6
|
+
const plainTextFormatter_1 = require("../utils/plainTextFormatter");
|
|
6
7
|
const cdpBridgeManager_1 = require("../services/cdpBridgeManager");
|
|
7
8
|
const modeService_1 = require("../services/modeService");
|
|
8
9
|
const imageHandler_1 = require("../utils/imageHandler");
|
|
@@ -17,6 +18,23 @@ function createMessageCreateHandler(deps) {
|
|
|
17
18
|
const downloadInboundImageAttachments = deps.downloadInboundImageAttachments ?? imageHandler_1.downloadInboundImageAttachments;
|
|
18
19
|
const cleanupInboundImageAttachments = deps.cleanupInboundImageAttachments ?? imageHandler_1.cleanupInboundImageAttachments;
|
|
19
20
|
const isImageAttachment = deps.isImageAttachment ?? imageHandler_1.isImageAttachment;
|
|
21
|
+
// Per-workspace prompt queue: serializes send→response cycles
|
|
22
|
+
const workspaceQueues = new Map();
|
|
23
|
+
const workspaceQueueDepths = new Map();
|
|
24
|
+
function enqueueForWorkspace(workspacePath, task) {
|
|
25
|
+
// .catch: ensure a prior rejection never stalls the chain
|
|
26
|
+
const current = (workspaceQueues.get(workspacePath) ?? Promise.resolve()).catch(() => { });
|
|
27
|
+
const next = current.then(async () => {
|
|
28
|
+
try {
|
|
29
|
+
await task();
|
|
30
|
+
}
|
|
31
|
+
catch (err) {
|
|
32
|
+
logger_1.logger.error('[WorkspaceQueue] task error:', err?.message || err);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
workspaceQueues.set(workspacePath, next);
|
|
36
|
+
return next;
|
|
37
|
+
}
|
|
20
38
|
return async (message) => {
|
|
21
39
|
if (message.author.bot)
|
|
22
40
|
return;
|
|
@@ -38,12 +56,12 @@ function createMessageCreateHandler(deps) {
|
|
|
38
56
|
if (parsed.commandName === 'status') {
|
|
39
57
|
const activeNames = deps.bridge.pool.getActiveWorkspaceNames();
|
|
40
58
|
const currentMode = deps.modeService.getCurrentMode();
|
|
41
|
-
const
|
|
42
|
-
.
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
59
|
+
const statusFields = [
|
|
60
|
+
{ name: 'CDP Connection', value: activeNames.length > 0 ? `🟢 ${activeNames.length} project(s) connected` : '⚪ Disconnected', inline: true },
|
|
61
|
+
{ name: 'Mode', value: modeService_1.MODE_DISPLAY_NAMES[currentMode] || currentMode, inline: true },
|
|
62
|
+
{ name: 'Auto Approve', value: deps.bridge.autoAccept.isEnabled() ? '🟢 ON' : '⚪ OFF', inline: true },
|
|
63
|
+
];
|
|
64
|
+
let statusDescription = '';
|
|
47
65
|
if (activeNames.length > 0) {
|
|
48
66
|
const lines = activeNames.map((name) => {
|
|
49
67
|
const cdp = deps.bridge.pool.getConnected(name);
|
|
@@ -51,15 +69,33 @@ function createMessageCreateHandler(deps) {
|
|
|
51
69
|
const detectorActive = deps.bridge.pool.getApprovalDetector(name)?.isActive() ? ' [Detecting]' : '';
|
|
52
70
|
return `• **${name}** — Contexts: ${contexts}${detectorActive}`;
|
|
53
71
|
});
|
|
54
|
-
|
|
72
|
+
statusDescription = `**Connected Projects:**\n${lines.join('\n')}`;
|
|
55
73
|
}
|
|
56
74
|
else {
|
|
57
|
-
|
|
75
|
+
statusDescription = 'Send a message to auto-connect to a project.';
|
|
76
|
+
}
|
|
77
|
+
const statusOutputFormat = deps.userPrefRepo?.getOutputFormat(message.author.id) ?? 'embed';
|
|
78
|
+
if (statusOutputFormat === 'plain') {
|
|
79
|
+
const chunks = (0, plainTextFormatter_1.formatAsPlainText)({
|
|
80
|
+
title: '🔧 Bot Status',
|
|
81
|
+
description: statusDescription,
|
|
82
|
+
fields: statusFields,
|
|
83
|
+
footerText: 'Use the slash command /status for more detailed information',
|
|
84
|
+
});
|
|
85
|
+
await message.reply({ content: chunks[0] });
|
|
86
|
+
return;
|
|
58
87
|
}
|
|
88
|
+
const embed = new discord_js_1.EmbedBuilder()
|
|
89
|
+
.setTitle('🔧 Bot Status')
|
|
90
|
+
.setColor(activeNames.length > 0 ? 0x00CC88 : 0x888888)
|
|
91
|
+
.addFields(...statusFields)
|
|
92
|
+
.setDescription(statusDescription)
|
|
93
|
+
.setFooter({ text: '💡 Use the slash command /status for more detailed information' })
|
|
94
|
+
.setTimestamp();
|
|
59
95
|
await message.reply({ embeds: [embed] });
|
|
60
96
|
return;
|
|
61
97
|
}
|
|
62
|
-
const slashOnlyCommands = ['help', 'stop', 'model', 'mode', 'project', 'chat', 'new', 'cleanup'];
|
|
98
|
+
const slashOnlyCommands = ['help', 'stop', 'model', 'mode', 'project', 'chat', 'new', 'cleanup', 'join', 'mirror', 'output'];
|
|
63
99
|
if (slashOnlyCommands.includes(parsed.commandName)) {
|
|
64
100
|
await message.reply({
|
|
65
101
|
content: `💡 Please use \`/${parsed.commandName}\` as a slash command.\nType \`/${parsed.commandName}\` in the Discord input field to see suggestions.`,
|
|
@@ -78,6 +114,7 @@ function createMessageCreateHandler(deps) {
|
|
|
78
114
|
chatSessionRepo: deps.chatSessionRepo,
|
|
79
115
|
channelManager: deps.channelManager,
|
|
80
116
|
titleGenerator: deps.titleGenerator,
|
|
117
|
+
userPrefRepo: deps.userPrefRepo,
|
|
81
118
|
});
|
|
82
119
|
}
|
|
83
120
|
else {
|
|
@@ -98,58 +135,126 @@ function createMessageCreateHandler(deps) {
|
|
|
98
135
|
const workspacePath = deps.wsHandler.getWorkspaceForChannel(message.channelId);
|
|
99
136
|
try {
|
|
100
137
|
if (workspacePath) {
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
138
|
+
const projectLabel = deps.bridge.pool.extractProjectName(workspacePath);
|
|
139
|
+
// Track queue depth for hourglass reactions
|
|
140
|
+
const currentDepth = workspaceQueueDepths.get(workspacePath) ?? 0;
|
|
141
|
+
workspaceQueueDepths.set(workspacePath, currentDepth + 1);
|
|
142
|
+
const newDepth = currentDepth + 1;
|
|
143
|
+
if (currentDepth > 0) {
|
|
144
|
+
logger_1.logger.info(`[Queue:${projectLabel}] Enqueued (depth: ${newDepth}, channel: ${message.channelId})`);
|
|
145
|
+
await message.react('⏳').catch(() => { });
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
logger_1.logger.info(`[Queue:${projectLabel}] Processing immediately (depth: ${newDepth}, channel: ${message.channelId})`);
|
|
149
|
+
}
|
|
150
|
+
const queueStartTime = Date.now();
|
|
151
|
+
await enqueueForWorkspace(workspacePath, async () => {
|
|
152
|
+
const waitMs = Date.now() - queueStartTime;
|
|
153
|
+
if (waitMs > 100) {
|
|
154
|
+
logger_1.logger.info(`[Queue:${projectLabel}] Task started after ${Math.round(waitMs / 1000)}s wait (channel: ${message.channelId})`);
|
|
113
155
|
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
await message.reply(`⚠️ Could not route this message to the bound session (${session.displayName}). ` +
|
|
119
|
-
`Please open /chat and verify the session${reason}.`).catch(() => { });
|
|
120
|
-
return;
|
|
121
|
-
}
|
|
156
|
+
// Remove hourglass when task starts processing
|
|
157
|
+
const botId = message.client.user?.id;
|
|
158
|
+
if (botId) {
|
|
159
|
+
await message.reactions.resolve('⏳')?.users.remove(botId).catch(() => { });
|
|
122
160
|
}
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
161
|
+
try {
|
|
162
|
+
const cdp = await deps.bridge.pool.getOrConnect(workspacePath);
|
|
163
|
+
const projectName = deps.bridge.pool.extractProjectName(workspacePath);
|
|
164
|
+
deps.bridge.lastActiveWorkspace = projectName;
|
|
165
|
+
deps.bridge.lastActiveChannel = message.channel;
|
|
166
|
+
registerApprovalWorkspaceChannel(deps.bridge, projectName, message.channel);
|
|
167
|
+
ensureApprovalDetector(deps.bridge, cdp, projectName, deps.client);
|
|
168
|
+
ensureErrorPopupDetector(deps.bridge, cdp, projectName, deps.client);
|
|
169
|
+
ensurePlanningDetector(deps.bridge, cdp, projectName, deps.client);
|
|
170
|
+
const session = deps.chatSessionRepo.findByChannelId(message.channelId);
|
|
171
|
+
if (session?.displayName) {
|
|
172
|
+
registerApprovalSessionChannel(deps.bridge, projectName, session.displayName, message.channel);
|
|
173
|
+
}
|
|
174
|
+
if (session?.isRenamed && session.displayName) {
|
|
175
|
+
const activationResult = await deps.chatSessionService.activateSessionByTitle(cdp, session.displayName);
|
|
176
|
+
if (!activationResult.ok) {
|
|
177
|
+
const reason = activationResult.error ? ` (${activationResult.error})` : '';
|
|
178
|
+
await message.reply(`⚠️ Could not route this message to the bound session (${session.displayName}). ` +
|
|
179
|
+
`Please open /chat and verify the session${reason}.`).catch(() => { });
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
else if (session && !session.isRenamed) {
|
|
184
|
+
try {
|
|
185
|
+
const chatResult = await deps.chatSessionService.startNewChat(cdp);
|
|
186
|
+
if (!chatResult.ok) {
|
|
187
|
+
logger_1.logger.warn('[MessageCreate] Failed to start new chat in Antigravity:', chatResult.error);
|
|
188
|
+
message.channel.send(`⚠️ Could not open a new chat in Antigravity. Sending to existing chat.`).catch(() => { });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
catch (err) {
|
|
192
|
+
logger_1.logger.error('[MessageCreate] startNewChat error:', err);
|
|
128
193
|
message.channel.send(`⚠️ Could not open a new chat in Antigravity. Sending to existing chat.`).catch(() => { });
|
|
129
194
|
}
|
|
130
195
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
196
|
+
await deps.autoRenameChannel(message, deps.chatSessionRepo, deps.titleGenerator, deps.channelManager, cdp);
|
|
197
|
+
// Re-register session channel after autoRenameChannel sets displayName
|
|
198
|
+
const updatedSession = deps.chatSessionRepo.findByChannelId(message.channelId);
|
|
199
|
+
if (updatedSession?.displayName) {
|
|
200
|
+
registerApprovalSessionChannel(deps.bridge, projectName, updatedSession.displayName, message.channel);
|
|
201
|
+
}
|
|
202
|
+
// Register echo hash so UserMessageDetector skips this message
|
|
203
|
+
const userMsgDetector = deps.bridge.pool.getUserMessageDetector?.(projectName);
|
|
204
|
+
if (userMsgDetector) {
|
|
205
|
+
userMsgDetector.addEchoHash(promptText);
|
|
134
206
|
}
|
|
207
|
+
// Wait for full response cycle (onComplete/onTimeout) before releasing the queue.
|
|
208
|
+
// Safety timeout (360s) prevents permanent queue deadlock if onFullCompletion
|
|
209
|
+
// is never called due to a bug.
|
|
210
|
+
const QUEUE_SAFETY_TIMEOUT_MS = 360_000;
|
|
211
|
+
const promptStartTime = Date.now();
|
|
212
|
+
await new Promise((resolve) => {
|
|
213
|
+
const safetyTimer = setTimeout(() => {
|
|
214
|
+
logger_1.logger.warn(`[Queue:${projectName}] Safety timeout — releasing queue after 360s ` +
|
|
215
|
+
`(channel: ${message.channelId})`);
|
|
216
|
+
resolve();
|
|
217
|
+
}, QUEUE_SAFETY_TIMEOUT_MS);
|
|
218
|
+
let settled = false;
|
|
219
|
+
const settle = () => {
|
|
220
|
+
if (settled)
|
|
221
|
+
return;
|
|
222
|
+
settled = true;
|
|
223
|
+
clearTimeout(safetyTimer);
|
|
224
|
+
const elapsed = Math.round((Date.now() - promptStartTime) / 1000);
|
|
225
|
+
logger_1.logger.info(`[Queue:${projectName}] Prompt completed in ${elapsed}s ` +
|
|
226
|
+
`(channel: ${message.channelId})`);
|
|
227
|
+
resolve();
|
|
228
|
+
};
|
|
229
|
+
deps.sendPromptToAntigravity(deps.bridge, message, promptText, cdp, deps.modeService, deps.modelService, inboundImages, {
|
|
230
|
+
chatSessionService: deps.chatSessionService,
|
|
231
|
+
chatSessionRepo: deps.chatSessionRepo,
|
|
232
|
+
channelManager: deps.channelManager,
|
|
233
|
+
titleGenerator: deps.titleGenerator,
|
|
234
|
+
userPrefRepo: deps.userPrefRepo,
|
|
235
|
+
onFullCompletion: settle,
|
|
236
|
+
}).catch((err) => {
|
|
237
|
+
// sendPromptToAntigravity rejected before onFullCompletion fired
|
|
238
|
+
// (e.g. setup code threw before top-level try/catch).
|
|
239
|
+
// Release the queue immediately instead of waiting for safety timeout.
|
|
240
|
+
logger_1.logger.error(`[Queue:${projectName}] sendPromptToAntigravity rejected early ` +
|
|
241
|
+
`(channel: ${message.channelId}):`, err?.message || err);
|
|
242
|
+
settle();
|
|
243
|
+
});
|
|
244
|
+
});
|
|
135
245
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
if (updatedSession?.displayName) {
|
|
140
|
-
registerApprovalSessionChannel(deps.bridge, dirName, updatedSession.displayName, message.channel);
|
|
246
|
+
catch (e) {
|
|
247
|
+
logger_1.logger.error(`[Queue:${projectLabel}] Task failed (channel: ${message.channelId}):`, e.message);
|
|
248
|
+
await message.reply(`Failed to connect to workspace: ${e.message}`);
|
|
141
249
|
}
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
await message.reply(`Failed to connect to workspace: ${e.message}`);
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
250
|
+
finally {
|
|
251
|
+
const remainingDepth = (workspaceQueueDepths.get(workspacePath) ?? 1) - 1;
|
|
252
|
+
workspaceQueueDepths.set(workspacePath, remainingDepth);
|
|
253
|
+
if (remainingDepth > 0) {
|
|
254
|
+
logger_1.logger.info(`[Queue:${projectLabel}] Task done, ${remainingDepth} remaining`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
});
|
|
153
258
|
}
|
|
154
259
|
else {
|
|
155
260
|
await message.reply('No project is configured for this channel. Please create or select one with `/project`.');
|
|
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.ensureAntigravityRunning = ensureAntigravityRunning;
|
|
37
37
|
const logger_1 = require("../utils/logger");
|
|
38
38
|
const cdpPorts_1 = require("../utils/cdpPorts");
|
|
39
|
+
const pathUtils_1 = require("../utils/pathUtils");
|
|
39
40
|
const http = __importStar(require("http"));
|
|
40
41
|
/**
|
|
41
42
|
* Check if CDP responds on the specified port.
|
|
@@ -69,10 +70,10 @@ function checkPort(port) {
|
|
|
69
70
|
* Called during Bot initialization.
|
|
70
71
|
*/
|
|
71
72
|
async function ensureAntigravityRunning() {
|
|
72
|
-
logger_1.logger.
|
|
73
|
+
logger_1.logger.debug('[AntigravityLauncher] Checking CDP ports...');
|
|
73
74
|
for (const port of cdpPorts_1.CDP_PORTS) {
|
|
74
75
|
if (await checkPort(port)) {
|
|
75
|
-
logger_1.logger.
|
|
76
|
+
logger_1.logger.debug(`[AntigravityLauncher] OK — Port ${port} responding`);
|
|
76
77
|
return;
|
|
77
78
|
}
|
|
78
79
|
}
|
|
@@ -83,7 +84,7 @@ async function ensureAntigravityRunning() {
|
|
|
83
84
|
logger_1.logger.warn(' Please run AntigravityDebug.command before starting the Bot');
|
|
84
85
|
logger_1.logger.warn('');
|
|
85
86
|
logger_1.logger.warn(' Or manually:');
|
|
86
|
-
logger_1.logger.warn(
|
|
87
|
+
logger_1.logger.warn(` ${(0, pathUtils_1.getAntigravityCdpHint)(9222)}`);
|
|
87
88
|
logger_1.logger.warn('='.repeat(70));
|
|
88
89
|
logger_1.logger.warn('');
|
|
89
90
|
}
|
|
@@ -206,6 +206,7 @@ class ApprovalDetector {
|
|
|
206
206
|
cdpService;
|
|
207
207
|
pollIntervalMs;
|
|
208
208
|
onApprovalRequired;
|
|
209
|
+
onResolved;
|
|
209
210
|
pollTimer = null;
|
|
210
211
|
isRunning = false;
|
|
211
212
|
/** Key of the last detected button info (for duplicate notification prevention) */
|
|
@@ -216,6 +217,7 @@ class ApprovalDetector {
|
|
|
216
217
|
this.cdpService = options.cdpService;
|
|
217
218
|
this.pollIntervalMs = options.pollIntervalMs ?? 1500;
|
|
218
219
|
this.onApprovalRequired = options.onApprovalRequired;
|
|
220
|
+
this.onResolved = options.onResolved;
|
|
219
221
|
}
|
|
220
222
|
/**
|
|
221
223
|
* Start monitoring.
|
|
@@ -286,8 +288,12 @@ class ApprovalDetector {
|
|
|
286
288
|
}
|
|
287
289
|
else {
|
|
288
290
|
// Reset when buttons disappear (prepare for next approval detection)
|
|
291
|
+
const wasDetected = this.lastDetectedKey !== null;
|
|
289
292
|
this.lastDetectedKey = null;
|
|
290
293
|
this.lastDetectedInfo = null;
|
|
294
|
+
if (wasDetected && this.onResolved) {
|
|
295
|
+
this.onResolved();
|
|
296
|
+
}
|
|
291
297
|
}
|
|
292
298
|
}
|
|
293
299
|
catch (error) {
|