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
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getCurrentChatTitle = getCurrentChatTitle;
3
4
  exports.registerApprovalWorkspaceChannel = registerApprovalWorkspaceChannel;
4
5
  exports.registerApprovalSessionChannel = registerApprovalSessionChannel;
5
6
  exports.resolveApprovalChannelForCurrentChat = resolveApprovalChannelForCurrentChat;
@@ -14,15 +15,18 @@ exports.getCurrentCdp = getCurrentCdp;
14
15
  exports.ensureApprovalDetector = ensureApprovalDetector;
15
16
  exports.ensurePlanningDetector = ensurePlanningDetector;
16
17
  exports.ensureErrorPopupDetector = ensureErrorPopupDetector;
18
+ exports.ensureUserMessageDetector = ensureUserMessageDetector;
17
19
  const discord_js_1 = require("discord.js");
18
20
  const i18n_1 = require("../utils/i18n");
19
21
  const logger_1 = require("../utils/logger");
22
+ const discordButtonUtils_1 = require("../utils/discordButtonUtils");
20
23
  const approvalDetector_1 = require("./approvalDetector");
21
24
  const autoAcceptService_1 = require("./autoAcceptService");
22
25
  const cdpConnectionPool_1 = require("./cdpConnectionPool");
23
26
  const errorPopupDetector_1 = require("./errorPopupDetector");
24
27
  const planningDetector_1 = require("./planningDetector");
25
28
  const quotaService_1 = require("./quotaService");
29
+ const userMessageDetector_1 = require("./userMessageDetector");
26
30
  const APPROVE_ACTION_PREFIX = 'approve_action';
27
31
  const ALWAYS_ALLOW_ACTION_PREFIX = 'always_allow_action';
28
32
  const DENY_ACTION_PREFIX = 'deny_action';
@@ -34,8 +38,8 @@ const ERROR_POPUP_RETRY_ACTION_PREFIX = 'error_popup_retry_action';
34
38
  function normalizeSessionTitle(title) {
35
39
  return title.trim().toLowerCase();
36
40
  }
37
- function buildSessionRouteKey(workspaceDirName, sessionTitle) {
38
- return `${workspaceDirName}::${normalizeSessionTitle(sessionTitle)}`;
41
+ function buildSessionRouteKey(projectName, sessionTitle) {
42
+ return `${projectName}::${normalizeSessionTitle(sessionTitle)}`;
39
43
  }
40
44
  const GET_CURRENT_CHAT_TITLE_SCRIPT = `(() => {
41
45
  const panel = document.querySelector('.antigravity-agent-side-panel');
@@ -67,127 +71,127 @@ async function getCurrentChatTitle(cdp) {
67
71
  }
68
72
  return null;
69
73
  }
70
- function registerApprovalWorkspaceChannel(bridge, workspaceDirName, channel) {
71
- bridge.approvalChannelByWorkspace.set(workspaceDirName, channel);
74
+ function registerApprovalWorkspaceChannel(bridge, projectName, channel) {
75
+ bridge.approvalChannelByWorkspace.set(projectName, channel);
72
76
  }
73
- function registerApprovalSessionChannel(bridge, workspaceDirName, sessionTitle, channel) {
77
+ function registerApprovalSessionChannel(bridge, projectName, sessionTitle, channel) {
74
78
  if (!sessionTitle || sessionTitle.trim().length === 0)
75
79
  return;
76
- bridge.approvalChannelBySession.set(buildSessionRouteKey(workspaceDirName, sessionTitle), channel);
77
- bridge.approvalChannelByWorkspace.set(workspaceDirName, channel);
80
+ bridge.approvalChannelBySession.set(buildSessionRouteKey(projectName, sessionTitle), channel);
81
+ bridge.approvalChannelByWorkspace.set(projectName, channel);
78
82
  }
79
- function resolveApprovalChannelForCurrentChat(bridge, workspaceDirName, currentChatTitle) {
83
+ function resolveApprovalChannelForCurrentChat(bridge, projectName, currentChatTitle) {
80
84
  // Try session-level match first (most precise routing)
81
85
  if (currentChatTitle && currentChatTitle.trim().length > 0) {
82
- const key = buildSessionRouteKey(workspaceDirName, currentChatTitle);
86
+ const key = buildSessionRouteKey(projectName, currentChatTitle);
83
87
  const sessionChannel = bridge.approvalChannelBySession.get(key);
84
88
  if (sessionChannel)
85
89
  return sessionChannel;
86
90
  }
87
91
  // Fall back to workspace-level routing
88
- return bridge.approvalChannelByWorkspace.get(workspaceDirName) ?? null;
92
+ return bridge.approvalChannelByWorkspace.get(projectName) ?? null;
89
93
  }
90
- function buildApprovalCustomId(action, workspaceDirName, channelId) {
94
+ function buildApprovalCustomId(action, projectName, channelId) {
91
95
  const prefix = action === 'approve'
92
96
  ? APPROVE_ACTION_PREFIX
93
97
  : action === 'always_allow'
94
98
  ? ALWAYS_ALLOW_ACTION_PREFIX
95
99
  : DENY_ACTION_PREFIX;
96
100
  if (channelId && channelId.trim().length > 0) {
97
- return `${prefix}:${workspaceDirName}:${channelId}`;
101
+ return `${prefix}:${projectName}:${channelId}`;
98
102
  }
99
- return `${prefix}:${workspaceDirName}`;
103
+ return `${prefix}:${projectName}`;
100
104
  }
101
105
  function parseApprovalCustomId(customId) {
102
106
  if (customId === APPROVE_ACTION_PREFIX) {
103
- return { action: 'approve', workspaceDirName: null, channelId: null };
107
+ return { action: 'approve', projectName: null, channelId: null };
104
108
  }
105
109
  if (customId === ALWAYS_ALLOW_ACTION_PREFIX) {
106
- return { action: 'always_allow', workspaceDirName: null, channelId: null };
110
+ return { action: 'always_allow', projectName: null, channelId: null };
107
111
  }
108
112
  if (customId === DENY_ACTION_PREFIX) {
109
- return { action: 'deny', workspaceDirName: null, channelId: null };
113
+ return { action: 'deny', projectName: null, channelId: null };
110
114
  }
111
115
  if (customId.startsWith(`${APPROVE_ACTION_PREFIX}:`)) {
112
116
  const rest = customId.substring(`${APPROVE_ACTION_PREFIX}:`.length);
113
- const [workspaceDirName, channelId] = rest.split(':');
114
- return { action: 'approve', workspaceDirName: workspaceDirName || null, channelId: channelId || null };
117
+ const [projectName, channelId] = rest.split(':');
118
+ return { action: 'approve', projectName: projectName || null, channelId: channelId || null };
115
119
  }
116
120
  if (customId.startsWith(`${ALWAYS_ALLOW_ACTION_PREFIX}:`)) {
117
121
  const rest = customId.substring(`${ALWAYS_ALLOW_ACTION_PREFIX}:`.length);
118
- const [workspaceDirName, channelId] = rest.split(':');
119
- return { action: 'always_allow', workspaceDirName: workspaceDirName || null, channelId: channelId || null };
122
+ const [projectName, channelId] = rest.split(':');
123
+ return { action: 'always_allow', projectName: projectName || null, channelId: channelId || null };
120
124
  }
121
125
  if (customId.startsWith(`${DENY_ACTION_PREFIX}:`)) {
122
126
  const rest = customId.substring(`${DENY_ACTION_PREFIX}:`.length);
123
- const [workspaceDirName, channelId] = rest.split(':');
124
- return { action: 'deny', workspaceDirName: workspaceDirName || null, channelId: channelId || null };
127
+ const [projectName, channelId] = rest.split(':');
128
+ return { action: 'deny', projectName: projectName || null, channelId: channelId || null };
125
129
  }
126
130
  return null;
127
131
  }
128
- function buildPlanningCustomId(action, workspaceDirName, channelId) {
132
+ function buildPlanningCustomId(action, projectName, channelId) {
129
133
  const prefix = action === 'open'
130
134
  ? PLANNING_OPEN_ACTION_PREFIX
131
135
  : PLANNING_PROCEED_ACTION_PREFIX;
132
136
  if (channelId && channelId.trim().length > 0) {
133
- return `${prefix}:${workspaceDirName}:${channelId}`;
137
+ return `${prefix}:${projectName}:${channelId}`;
134
138
  }
135
- return `${prefix}:${workspaceDirName}`;
139
+ return `${prefix}:${projectName}`;
136
140
  }
137
141
  function parsePlanningCustomId(customId) {
138
142
  if (customId === PLANNING_OPEN_ACTION_PREFIX) {
139
- return { action: 'open', workspaceDirName: null, channelId: null };
143
+ return { action: 'open', projectName: null, channelId: null };
140
144
  }
141
145
  if (customId === PLANNING_PROCEED_ACTION_PREFIX) {
142
- return { action: 'proceed', workspaceDirName: null, channelId: null };
146
+ return { action: 'proceed', projectName: null, channelId: null };
143
147
  }
144
148
  if (customId.startsWith(`${PLANNING_OPEN_ACTION_PREFIX}:`)) {
145
149
  const rest = customId.substring(`${PLANNING_OPEN_ACTION_PREFIX}:`.length);
146
- const [workspaceDirName, channelId] = rest.split(':');
147
- return { action: 'open', workspaceDirName: workspaceDirName || null, channelId: channelId || null };
150
+ const [projectName, channelId] = rest.split(':');
151
+ return { action: 'open', projectName: projectName || null, channelId: channelId || null };
148
152
  }
149
153
  if (customId.startsWith(`${PLANNING_PROCEED_ACTION_PREFIX}:`)) {
150
154
  const rest = customId.substring(`${PLANNING_PROCEED_ACTION_PREFIX}:`.length);
151
- const [workspaceDirName, channelId] = rest.split(':');
152
- return { action: 'proceed', workspaceDirName: workspaceDirName || null, channelId: channelId || null };
155
+ const [projectName, channelId] = rest.split(':');
156
+ return { action: 'proceed', projectName: projectName || null, channelId: channelId || null };
153
157
  }
154
158
  return null;
155
159
  }
156
- function buildErrorPopupCustomId(action, workspaceDirName, channelId) {
160
+ function buildErrorPopupCustomId(action, projectName, channelId) {
157
161
  const prefix = action === 'dismiss'
158
162
  ? ERROR_POPUP_DISMISS_ACTION_PREFIX
159
163
  : action === 'copy_debug'
160
164
  ? ERROR_POPUP_COPY_DEBUG_ACTION_PREFIX
161
165
  : ERROR_POPUP_RETRY_ACTION_PREFIX;
162
166
  if (channelId && channelId.trim().length > 0) {
163
- return `${prefix}:${workspaceDirName}:${channelId}`;
167
+ return `${prefix}:${projectName}:${channelId}`;
164
168
  }
165
- return `${prefix}:${workspaceDirName}`;
169
+ return `${prefix}:${projectName}`;
166
170
  }
167
171
  function parseErrorPopupCustomId(customId) {
168
172
  if (customId === ERROR_POPUP_DISMISS_ACTION_PREFIX) {
169
- return { action: 'dismiss', workspaceDirName: null, channelId: null };
173
+ return { action: 'dismiss', projectName: null, channelId: null };
170
174
  }
171
175
  if (customId === ERROR_POPUP_COPY_DEBUG_ACTION_PREFIX) {
172
- return { action: 'copy_debug', workspaceDirName: null, channelId: null };
176
+ return { action: 'copy_debug', projectName: null, channelId: null };
173
177
  }
174
178
  if (customId === ERROR_POPUP_RETRY_ACTION_PREFIX) {
175
- return { action: 'retry', workspaceDirName: null, channelId: null };
179
+ return { action: 'retry', projectName: null, channelId: null };
176
180
  }
177
181
  if (customId.startsWith(`${ERROR_POPUP_DISMISS_ACTION_PREFIX}:`)) {
178
182
  const rest = customId.substring(`${ERROR_POPUP_DISMISS_ACTION_PREFIX}:`.length);
179
- const [workspaceDirName, channelId] = rest.split(':');
180
- return { action: 'dismiss', workspaceDirName: workspaceDirName || null, channelId: channelId || null };
183
+ const [projectName, channelId] = rest.split(':');
184
+ return { action: 'dismiss', projectName: projectName || null, channelId: channelId || null };
181
185
  }
182
186
  if (customId.startsWith(`${ERROR_POPUP_COPY_DEBUG_ACTION_PREFIX}:`)) {
183
187
  const rest = customId.substring(`${ERROR_POPUP_COPY_DEBUG_ACTION_PREFIX}:`.length);
184
- const [workspaceDirName, channelId] = rest.split(':');
185
- return { action: 'copy_debug', workspaceDirName: workspaceDirName || null, channelId: channelId || null };
188
+ const [projectName, channelId] = rest.split(':');
189
+ return { action: 'copy_debug', projectName: projectName || null, channelId: channelId || null };
186
190
  }
187
191
  if (customId.startsWith(`${ERROR_POPUP_RETRY_ACTION_PREFIX}:`)) {
188
192
  const rest = customId.substring(`${ERROR_POPUP_RETRY_ACTION_PREFIX}:`.length);
189
- const [workspaceDirName, channelId] = rest.split(':');
190
- return { action: 'retry', workspaceDirName: workspaceDirName || null, channelId: channelId || null };
193
+ const [projectName, channelId] = rest.split(':');
194
+ return { action: 'retry', projectName: projectName || null, channelId: channelId || null };
191
195
  }
192
196
  return null;
193
197
  }
@@ -226,20 +230,42 @@ function getCurrentCdp(bridge) {
226
230
  * Helper to start an approval detector for each workspace.
227
231
  * Does nothing if a detector for the same workspace is already running.
228
232
  */
229
- function ensureApprovalDetector(bridge, cdp, workspaceDirName, client) {
230
- const existing = bridge.pool.getApprovalDetector(workspaceDirName);
233
+ function ensureApprovalDetector(bridge, cdp, projectName, client) {
234
+ const existing = bridge.pool.getApprovalDetector(projectName);
231
235
  if (existing && existing.isActive())
232
236
  return;
237
+ // Track the most recent button message for auto-disable on resolve.
238
+ // Only the latest message is tracked; if a new detection fires before the previous
239
+ // is resolved, the older message reference is overwritten. This is acceptable because
240
+ // the detector's lastDetectedKey deduplication prevents rapid successive notifications.
241
+ let lastButtonMessage = null;
233
242
  const detector = new approvalDetector_1.ApprovalDetector({
234
243
  cdpService: cdp,
235
244
  pollIntervalMs: 2000,
245
+ onResolved: () => {
246
+ if (!lastButtonMessage)
247
+ return;
248
+ const msg = lastButtonMessage;
249
+ lastButtonMessage = null;
250
+ const originalEmbed = msg.embeds[0];
251
+ const updatedEmbed = originalEmbed
252
+ ? discord_js_1.EmbedBuilder.from(originalEmbed)
253
+ : new discord_js_1.EmbedBuilder().setTitle((0, i18n_1.t)('Approval Required'));
254
+ updatedEmbed
255
+ .setColor(0x95A5A6)
256
+ .addFields({ name: (0, i18n_1.t)('Status'), value: (0, i18n_1.t)('Resolved in Antigravity'), inline: false });
257
+ msg.edit({
258
+ embeds: [updatedEmbed],
259
+ components: (0, discordButtonUtils_1.disableAllButtons)(msg.components),
260
+ }).catch(logger_1.logger.error);
261
+ },
236
262
  onApprovalRequired: async (info) => {
237
- logger_1.logger.info(`[ApprovalDetector:${workspaceDirName}] Approval button detected (allow="${info.approveText}", deny="${info.denyText}")`);
263
+ logger_1.logger.debug(`[ApprovalDetector:${projectName}] Approval button detected (allow="${info.approveText}", deny="${info.denyText}")`);
238
264
  const currentChatTitle = await getCurrentChatTitle(cdp);
239
- const targetChannel = resolveApprovalChannelForCurrentChat(bridge, workspaceDirName, currentChatTitle);
265
+ const targetChannel = resolveApprovalChannelForCurrentChat(bridge, projectName, currentChatTitle);
240
266
  const targetChannelId = targetChannel && 'id' in targetChannel ? String(targetChannel.id) : '';
241
267
  if (!targetChannel || !targetChannelId || !('send' in targetChannel)) {
242
- logger_1.logger.warn(`[ApprovalDetector:${workspaceDirName}] Skipped approval notification because chat is not linked to a Discord session` +
268
+ logger_1.logger.warn(`[ApprovalDetector:${projectName}] Skipped approval notification because chat is not linked to a Discord session` +
243
269
  `${currentChatTitle ? ` (title="${currentChatTitle}")` : ''}`);
244
270
  return;
245
271
  }
@@ -247,10 +273,16 @@ function ensureApprovalDetector(bridge, cdp, workspaceDirName, client) {
247
273
  const accepted = await detector.alwaysAllowButton() || await detector.approveButton();
248
274
  const autoEmbed = new discord_js_1.EmbedBuilder()
249
275
  .setTitle(accepted ? (0, i18n_1.t)('Auto-approved') : (0, i18n_1.t)('Auto-approve failed'))
250
- .setDescription(info.description || (0, i18n_1.t)('Antigravity is requesting approval for an action'))
276
+ .setDescription(accepted ? (0, i18n_1.t)('An action was automatically approved.') : (0, i18n_1.t)('Auto-approve attempted but failed. Manual approval required.'))
251
277
  .setColor(accepted ? 0x2ECC71 : 0xF39C12)
252
- .addFields({ name: (0, i18n_1.t)('Auto-approve mode'), value: (0, i18n_1.t)('ON'), inline: true }, { name: (0, i18n_1.t)('Workspace'), value: workspaceDirName, inline: true }, { name: (0, i18n_1.t)('Result'), value: accepted ? (0, i18n_1.t)('Executed Always Allow/Allow') : (0, i18n_1.t)('Manual approval required'), inline: true })
253
- .setTimestamp();
278
+ .addFields({ name: (0, i18n_1.t)('Auto-approve mode'), value: (0, i18n_1.t)('ON'), inline: true }, { name: (0, i18n_1.t)('Workspace'), value: projectName, inline: true }, { name: (0, i18n_1.t)('Result'), value: accepted ? (0, i18n_1.t)('Executed Always Allow/Allow') : (0, i18n_1.t)('Manual approval required'), inline: true });
279
+ if (info.description) {
280
+ autoEmbed.addFields({ name: (0, i18n_1.t)('Action Detail'), value: info.description.substring(0, 1024), inline: false });
281
+ }
282
+ if (info.approveText) {
283
+ autoEmbed.addFields({ name: (0, i18n_1.t)('Approved via'), value: info.approveText, inline: true });
284
+ }
285
+ autoEmbed.setTimestamp();
254
286
  await targetChannel.send({ embeds: [autoEmbed] }).catch(logger_1.logger.error);
255
287
  if (accepted) {
256
288
  return;
@@ -260,49 +292,72 @@ function ensureApprovalDetector(bridge, cdp, workspaceDirName, client) {
260
292
  .setTitle((0, i18n_1.t)('Approval Required'))
261
293
  .setDescription(info.description || (0, i18n_1.t)('Antigravity is requesting approval for an action'))
262
294
  .setColor(0xFFA500)
263
- .addFields({ name: (0, i18n_1.t)('Allow button'), value: info.approveText, inline: true }, { name: (0, i18n_1.t)('Allow Chat button'), value: info.alwaysAllowText || (0, i18n_1.t)('In Dropdown'), inline: true }, { name: (0, i18n_1.t)('Deny button'), value: info.denyText || (0, i18n_1.t)('(None)'), inline: true }, { name: (0, i18n_1.t)('Workspace'), value: workspaceDirName, inline: true })
295
+ .addFields({ name: (0, i18n_1.t)('Allow button'), value: info.approveText, inline: true }, { name: (0, i18n_1.t)('Allow Chat button'), value: info.alwaysAllowText || (0, i18n_1.t)('In Dropdown'), inline: true }, { name: (0, i18n_1.t)('Deny button'), value: info.denyText || (0, i18n_1.t)('(None)'), inline: true }, { name: (0, i18n_1.t)('Workspace'), value: projectName, inline: true })
264
296
  .setTimestamp();
265
297
  const approveBtn = new discord_js_1.ButtonBuilder()
266
- .setCustomId(buildApprovalCustomId('approve', workspaceDirName, targetChannelId))
298
+ .setCustomId(buildApprovalCustomId('approve', projectName, targetChannelId))
267
299
  .setLabel((0, i18n_1.t)('Allow'))
268
300
  .setStyle(discord_js_1.ButtonStyle.Success);
269
301
  const alwaysAllowBtn = new discord_js_1.ButtonBuilder()
270
- .setCustomId(buildApprovalCustomId('always_allow', workspaceDirName, targetChannelId))
302
+ .setCustomId(buildApprovalCustomId('always_allow', projectName, targetChannelId))
271
303
  .setLabel((0, i18n_1.t)('Allow Chat'))
272
304
  .setStyle(discord_js_1.ButtonStyle.Primary);
273
305
  const denyBtn = new discord_js_1.ButtonBuilder()
274
- .setCustomId(buildApprovalCustomId('deny', workspaceDirName, targetChannelId))
306
+ .setCustomId(buildApprovalCustomId('deny', projectName, targetChannelId))
275
307
  .setLabel((0, i18n_1.t)('Deny'))
276
308
  .setStyle(discord_js_1.ButtonStyle.Danger);
277
309
  const row = new discord_js_1.ActionRowBuilder().addComponents(approveBtn, alwaysAllowBtn, denyBtn);
278
- targetChannel.send({
310
+ const sent = await targetChannel.send({
279
311
  embeds: [embed],
280
312
  components: [row],
281
- }).catch(logger_1.logger.error);
313
+ }).catch((err) => { logger_1.logger.error(err); return null; });
314
+ if (sent) {
315
+ lastButtonMessage = sent;
316
+ }
282
317
  },
283
318
  });
284
319
  detector.start();
285
- bridge.pool.registerApprovalDetector(workspaceDirName, detector);
286
- logger_1.logger.info(`[ApprovalDetector:${workspaceDirName}] Started approval button detection`);
320
+ bridge.pool.registerApprovalDetector(projectName, detector);
321
+ logger_1.logger.debug(`[ApprovalDetector:${projectName}] Started approval button detection`);
287
322
  }
288
323
  /**
289
324
  * Helper to start a planning detector for each workspace.
290
325
  * Does nothing if a detector for the same workspace is already running.
291
326
  */
292
- function ensurePlanningDetector(bridge, cdp, workspaceDirName, _client) {
293
- const existing = bridge.pool.getPlanningDetector(workspaceDirName);
327
+ function ensurePlanningDetector(bridge, cdp, projectName, _client) {
328
+ const existing = bridge.pool.getPlanningDetector(projectName);
294
329
  if (existing && existing.isActive())
295
330
  return;
331
+ // Track the most recent planning message for auto-disable on resolve.
332
+ // See ensureApprovalDetector comment for tracking limitation rationale.
333
+ let lastPlanningMessage = null;
296
334
  const detector = new planningDetector_1.PlanningDetector({
297
335
  cdpService: cdp,
298
336
  pollIntervalMs: 2000,
337
+ onResolved: () => {
338
+ if (!lastPlanningMessage)
339
+ return;
340
+ const msg = lastPlanningMessage;
341
+ lastPlanningMessage = null;
342
+ const originalEmbed = msg.embeds[0];
343
+ const updatedEmbed = originalEmbed
344
+ ? discord_js_1.EmbedBuilder.from(originalEmbed)
345
+ : new discord_js_1.EmbedBuilder().setTitle((0, i18n_1.t)('Planning Mode'));
346
+ updatedEmbed
347
+ .setColor(0x95A5A6)
348
+ .addFields({ name: (0, i18n_1.t)('Status'), value: (0, i18n_1.t)('Resolved in Antigravity'), inline: false });
349
+ msg.edit({
350
+ embeds: [updatedEmbed],
351
+ components: (0, discordButtonUtils_1.disableAllButtons)(msg.components),
352
+ }).catch(logger_1.logger.error);
353
+ },
299
354
  onPlanningRequired: async (info) => {
300
- logger_1.logger.info(`[PlanningDetector:${workspaceDirName}] Planning buttons detected (title="${info.planTitle}")`);
355
+ logger_1.logger.debug(`[PlanningDetector:${projectName}] Planning buttons detected (title="${info.planTitle}")`);
301
356
  const currentChatTitle = await getCurrentChatTitle(cdp);
302
- const targetChannel = resolveApprovalChannelForCurrentChat(bridge, workspaceDirName, currentChatTitle);
357
+ const targetChannel = resolveApprovalChannelForCurrentChat(bridge, projectName, currentChatTitle);
303
358
  const targetChannelId = targetChannel && 'id' in targetChannel ? String(targetChannel.id) : '';
304
359
  if (!targetChannel || !targetChannelId || !('send' in targetChannel)) {
305
- logger_1.logger.warn(`[PlanningDetector:${workspaceDirName}] Skipped planning notification because chat is not linked to a Discord session` +
360
+ logger_1.logger.warn(`[PlanningDetector:${projectName}] Skipped planning notification because chat is not linked to a Discord session` +
306
361
  `${currentChatTitle ? ` (title="${currentChatTitle}")` : ''}`);
307
362
  return;
308
363
  }
@@ -311,48 +366,71 @@ function ensurePlanningDetector(bridge, cdp, workspaceDirName, _client) {
311
366
  .setTitle((0, i18n_1.t)('Planning Mode'))
312
367
  .setDescription(descriptionText)
313
368
  .setColor(0x3498DB)
314
- .addFields({ name: (0, i18n_1.t)('Plan'), value: info.planTitle || (0, i18n_1.t)('Implementation Plan'), inline: true }, { name: (0, i18n_1.t)('Workspace'), value: workspaceDirName, inline: true })
369
+ .addFields({ name: (0, i18n_1.t)('Plan'), value: info.planTitle || (0, i18n_1.t)('Implementation Plan'), inline: true }, { name: (0, i18n_1.t)('Workspace'), value: projectName, inline: true })
315
370
  .setTimestamp();
316
371
  if (info.planSummary && info.description) {
317
372
  embed.addFields({ name: (0, i18n_1.t)('Summary'), value: info.planSummary.substring(0, 1024), inline: false });
318
373
  }
319
374
  const openBtn = new discord_js_1.ButtonBuilder()
320
- .setCustomId(buildPlanningCustomId('open', workspaceDirName, targetChannelId))
375
+ .setCustomId(buildPlanningCustomId('open', projectName, targetChannelId))
321
376
  .setLabel((0, i18n_1.t)('Open'))
322
377
  .setStyle(discord_js_1.ButtonStyle.Secondary);
323
378
  const proceedBtn = new discord_js_1.ButtonBuilder()
324
- .setCustomId(buildPlanningCustomId('proceed', workspaceDirName, targetChannelId))
379
+ .setCustomId(buildPlanningCustomId('proceed', projectName, targetChannelId))
325
380
  .setLabel((0, i18n_1.t)('Proceed'))
326
381
  .setStyle(discord_js_1.ButtonStyle.Primary);
327
382
  const row = new discord_js_1.ActionRowBuilder().addComponents(openBtn, proceedBtn);
328
- targetChannel.send({
383
+ const sent = await targetChannel.send({
329
384
  embeds: [embed],
330
385
  components: [row],
331
- }).catch(logger_1.logger.error);
386
+ }).catch((err) => { logger_1.logger.error(err); return null; });
387
+ if (sent) {
388
+ lastPlanningMessage = sent;
389
+ }
332
390
  },
333
391
  });
334
392
  detector.start();
335
- bridge.pool.registerPlanningDetector(workspaceDirName, detector);
336
- logger_1.logger.info(`[PlanningDetector:${workspaceDirName}] Started planning button detection`);
393
+ bridge.pool.registerPlanningDetector(projectName, detector);
394
+ logger_1.logger.debug(`[PlanningDetector:${projectName}] Started planning button detection`);
337
395
  }
338
396
  /**
339
397
  * Helper to start an error popup detector for each workspace.
340
398
  * Does nothing if a detector for the same workspace is already running.
341
399
  */
342
- function ensureErrorPopupDetector(bridge, cdp, workspaceDirName, _client) {
343
- const existing = bridge.pool.getErrorPopupDetector(workspaceDirName);
400
+ function ensureErrorPopupDetector(bridge, cdp, projectName, _client) {
401
+ const existing = bridge.pool.getErrorPopupDetector(projectName);
344
402
  if (existing && existing.isActive())
345
403
  return;
404
+ // Track the most recent error message for auto-disable on resolve.
405
+ // See ensureApprovalDetector comment for tracking limitation rationale.
406
+ let lastErrorMessage = null;
346
407
  const detector = new errorPopupDetector_1.ErrorPopupDetector({
347
408
  cdpService: cdp,
348
409
  pollIntervalMs: 3000,
410
+ onResolved: () => {
411
+ if (!lastErrorMessage)
412
+ return;
413
+ const msg = lastErrorMessage;
414
+ lastErrorMessage = null;
415
+ const originalEmbed = msg.embeds[0];
416
+ const updatedEmbed = originalEmbed
417
+ ? discord_js_1.EmbedBuilder.from(originalEmbed)
418
+ : new discord_js_1.EmbedBuilder().setTitle((0, i18n_1.t)('Agent Error'));
419
+ updatedEmbed
420
+ .setColor(0x95A5A6)
421
+ .addFields({ name: (0, i18n_1.t)('Status'), value: (0, i18n_1.t)('Resolved in Antigravity'), inline: false });
422
+ msg.edit({
423
+ embeds: [updatedEmbed],
424
+ components: (0, discordButtonUtils_1.disableAllButtons)(msg.components),
425
+ }).catch(logger_1.logger.error);
426
+ },
349
427
  onErrorPopup: async (info) => {
350
- logger_1.logger.info(`[ErrorPopupDetector:${workspaceDirName}] Error popup detected (title="${info.title}")`);
428
+ logger_1.logger.debug(`[ErrorPopupDetector:${projectName}] Error popup detected (title="${info.title}")`);
351
429
  const currentChatTitle = await getCurrentChatTitle(cdp);
352
- const targetChannel = resolveApprovalChannelForCurrentChat(bridge, workspaceDirName, currentChatTitle);
430
+ const targetChannel = resolveApprovalChannelForCurrentChat(bridge, projectName, currentChatTitle);
353
431
  const targetChannelId = targetChannel && 'id' in targetChannel ? String(targetChannel.id) : '';
354
432
  if (!targetChannel || !targetChannelId || !('send' in targetChannel)) {
355
- logger_1.logger.warn(`[ErrorPopupDetector:${workspaceDirName}] Skipped error popup notification because chat is not linked to a Discord session` +
433
+ logger_1.logger.warn(`[ErrorPopupDetector:${projectName}] Skipped error popup notification because chat is not linked to a Discord session` +
356
434
  `${currentChatTitle ? ` (title="${currentChatTitle}")` : ''}`);
357
435
  return;
358
436
  }
@@ -361,28 +439,50 @@ function ensureErrorPopupDetector(bridge, cdp, workspaceDirName, _client) {
361
439
  .setTitle(info.title || (0, i18n_1.t)('Agent Error'))
362
440
  .setDescription(bodyText.substring(0, 4096))
363
441
  .setColor(0xE74C3C)
364
- .addFields({ name: (0, i18n_1.t)('Buttons'), value: info.buttons.join(', ') || (0, i18n_1.t)('(None)'), inline: true }, { name: (0, i18n_1.t)('Workspace'), value: workspaceDirName, inline: true })
442
+ .addFields({ name: (0, i18n_1.t)('Buttons'), value: info.buttons.join(', ') || (0, i18n_1.t)('(None)'), inline: true }, { name: (0, i18n_1.t)('Workspace'), value: projectName, inline: true })
365
443
  .setTimestamp();
366
444
  const dismissBtn = new discord_js_1.ButtonBuilder()
367
- .setCustomId(buildErrorPopupCustomId('dismiss', workspaceDirName, targetChannelId))
445
+ .setCustomId(buildErrorPopupCustomId('dismiss', projectName, targetChannelId))
368
446
  .setLabel((0, i18n_1.t)('Dismiss'))
369
447
  .setStyle(discord_js_1.ButtonStyle.Secondary);
370
448
  const copyDebugBtn = new discord_js_1.ButtonBuilder()
371
- .setCustomId(buildErrorPopupCustomId('copy_debug', workspaceDirName, targetChannelId))
449
+ .setCustomId(buildErrorPopupCustomId('copy_debug', projectName, targetChannelId))
372
450
  .setLabel((0, i18n_1.t)('Copy debug info'))
373
451
  .setStyle(discord_js_1.ButtonStyle.Primary);
374
452
  const retryBtn = new discord_js_1.ButtonBuilder()
375
- .setCustomId(buildErrorPopupCustomId('retry', workspaceDirName, targetChannelId))
453
+ .setCustomId(buildErrorPopupCustomId('retry', projectName, targetChannelId))
376
454
  .setLabel((0, i18n_1.t)('Retry'))
377
455
  .setStyle(discord_js_1.ButtonStyle.Success);
378
456
  const row = new discord_js_1.ActionRowBuilder().addComponents(dismissBtn, copyDebugBtn, retryBtn);
379
- targetChannel.send({
457
+ const sent = await targetChannel.send({
380
458
  embeds: [embed],
381
459
  components: [row],
382
- }).catch(logger_1.logger.error);
460
+ }).catch((err) => { logger_1.logger.error(err); return null; });
461
+ if (sent) {
462
+ lastErrorMessage = sent;
463
+ }
383
464
  },
384
465
  });
385
466
  detector.start();
386
- bridge.pool.registerErrorPopupDetector(workspaceDirName, detector);
387
- logger_1.logger.info(`[ErrorPopupDetector:${workspaceDirName}] Started error popup detection`);
467
+ bridge.pool.registerErrorPopupDetector(projectName, detector);
468
+ logger_1.logger.debug(`[ErrorPopupDetector:${projectName}] Started error popup detection`);
469
+ }
470
+ /**
471
+ * Helper to start a user message detector for a workspace.
472
+ * Detects messages typed directly in the Antigravity UI (e.g., from a PC)
473
+ * and mirrors them to a Discord channel.
474
+ * Does nothing if a detector for the same workspace is already running.
475
+ */
476
+ function ensureUserMessageDetector(bridge, cdp, projectName, onUserMessage) {
477
+ const existing = bridge.pool.getUserMessageDetector(projectName);
478
+ if (existing && existing.isActive())
479
+ return;
480
+ const detector = new userMessageDetector_1.UserMessageDetector({
481
+ cdpService: cdp,
482
+ pollIntervalMs: 2000,
483
+ onUserMessage,
484
+ });
485
+ detector.start();
486
+ bridge.pool.registerUserMessageDetector(projectName, detector);
487
+ logger_1.logger.debug(`[UserMessageDetector:${projectName}] Started user message detection`);
388
488
  }