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.
Files changed (34) hide show
  1. package/README.md +18 -6
  2. package/dist/bin/cli.js +18 -18
  3. package/dist/bin/commands/doctor.js +2 -1
  4. package/dist/bin/commands/start.js +25 -2
  5. package/dist/bot/index.js +346 -152
  6. package/dist/commands/joinCommandHandler.js +302 -0
  7. package/dist/commands/joinDetachCommandHandler.js +285 -0
  8. package/dist/commands/registerSlashCommands.js +35 -0
  9. package/dist/database/chatSessionRepository.js +10 -0
  10. package/dist/database/userPreferenceRepository.js +72 -0
  11. package/dist/events/interactionCreateHandler.js +58 -36
  12. package/dist/events/messageCreateHandler.js +158 -53
  13. package/dist/services/antigravityLauncher.js +4 -3
  14. package/dist/services/approvalDetector.js +6 -0
  15. package/dist/services/cdpBridgeManager.js +184 -84
  16. package/dist/services/cdpConnectionPool.js +79 -51
  17. package/dist/services/cdpService.js +149 -51
  18. package/dist/services/chatSessionService.js +229 -8
  19. package/dist/services/errorPopupDetector.js +6 -0
  20. package/dist/services/planningDetector.js +6 -0
  21. package/dist/services/responseMonitor.js +125 -24
  22. package/dist/services/updateCheckService.js +147 -0
  23. package/dist/services/userMessageDetector.js +221 -0
  24. package/dist/ui/modeUi.js +11 -1
  25. package/dist/ui/outputUi.js +30 -0
  26. package/dist/ui/sessionPickerUi.js +48 -0
  27. package/dist/utils/antigravityPaths.js +94 -0
  28. package/dist/utils/configLoader.js +10 -0
  29. package/dist/utils/discordButtonUtils.js +33 -0
  30. package/dist/utils/logBuffer.js +47 -0
  31. package/dist/utils/logger.js +80 -20
  32. package/dist/utils/pathUtils.js +57 -0
  33. package/dist/utils/plainTextFormatter.js +70 -0
  34. 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
- /** Disable all buttons in message component rows. */
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 workspaceDirName = approvalAction.workspaceDirName ?? deps.bridge.lastActiveWorkspace;
56
- const detector = workspaceDirName
57
- ? deps.bridge.pool.getApprovalDetector(workspaceDirName)
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.workspaceDirName ?? deps.bridge.lastActiveWorkspace;
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.workspaceDirName ?? deps.bridge.lastActiveWorkspace;
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
- await commandInteraction.deferReply();
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 embed = new discord_js_1.EmbedBuilder()
42
- .setTitle('🔧 Bot Status')
43
- .setColor(activeNames.length > 0 ? 0x00CC88 : 0x888888)
44
- .addFields({ name: 'CDP Connection', value: activeNames.length > 0 ? `🟢 ${activeNames.length} project(s) connected` : '⚪ Disconnected', inline: true }, { name: 'Mode', value: modeService_1.MODE_DISPLAY_NAMES[currentMode] || currentMode, inline: true }, { name: 'Auto Approve', value: deps.bridge.autoAccept.isEnabled() ? '🟢 ON' : '⚪ OFF', inline: true })
45
- .setFooter({ text: '💡 Use the slash command /status for more detailed information' })
46
- .setTimestamp();
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
- embed.setDescription(`**Connected Projects:**\n${lines.join('\n')}`);
72
+ statusDescription = `**Connected Projects:**\n${lines.join('\n')}`;
55
73
  }
56
74
  else {
57
- embed.setDescription('Send a message to auto-connect to a project.');
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
- try {
102
- const cdp = await deps.bridge.pool.getOrConnect(workspacePath);
103
- const dirName = deps.bridge.pool.extractDirName(workspacePath);
104
- deps.bridge.lastActiveWorkspace = dirName;
105
- deps.bridge.lastActiveChannel = message.channel;
106
- registerApprovalWorkspaceChannel(deps.bridge, dirName, message.channel);
107
- ensureApprovalDetector(deps.bridge, cdp, dirName, deps.client);
108
- ensureErrorPopupDetector(deps.bridge, cdp, dirName, deps.client);
109
- ensurePlanningDetector(deps.bridge, cdp, dirName, deps.client);
110
- const session = deps.chatSessionRepo.findByChannelId(message.channelId);
111
- if (session?.displayName) {
112
- registerApprovalSessionChannel(deps.bridge, dirName, session.displayName, message.channel);
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
- if (session?.isRenamed && session.displayName) {
115
- const activationResult = await deps.chatSessionService.activateSessionByTitle(cdp, session.displayName);
116
- if (!activationResult.ok) {
117
- const reason = activationResult.error ? ` (${activationResult.error})` : '';
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
- else if (session && !session.isRenamed) {
124
- try {
125
- const chatResult = await deps.chatSessionService.startNewChat(cdp);
126
- if (!chatResult.ok) {
127
- logger_1.logger.warn('[MessageCreate] Failed to start new chat in Antigravity:', chatResult.error);
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
- catch (err) {
132
- logger_1.logger.error('[MessageCreate] startNewChat error:', err);
133
- message.channel.send(`⚠️ Could not open a new chat in Antigravity. Sending to existing chat.`).catch(() => { });
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
- await deps.autoRenameChannel(message, deps.chatSessionRepo, deps.titleGenerator, deps.channelManager, cdp);
137
- // Re-register session channel after autoRenameChannel sets displayName
138
- const updatedSession = deps.chatSessionRepo.findByChannelId(message.channelId);
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
- await deps.sendPromptToAntigravity(deps.bridge, message, promptText, cdp, deps.modeService, deps.modelService, inboundImages, {
143
- chatSessionService: deps.chatSessionService,
144
- chatSessionRepo: deps.chatSessionRepo,
145
- channelManager: deps.channelManager,
146
- titleGenerator: deps.titleGenerator,
147
- });
148
- }
149
- catch (e) {
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.info('[AntigravityLauncher] Checking CDP ports...');
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.info(`[AntigravityLauncher] OK — Port ${port} responding`);
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(' open -a Antigravity --args --remote-debugging-port=9222');
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) {