lazy-gravity 0.0.4 → 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 (44) hide show
  1. package/README.md +22 -7
  2. package/dist/bin/cli.js +18 -18
  3. package/dist/bin/commands/doctor.js +25 -19
  4. package/dist/bin/commands/start.js +25 -2
  5. package/dist/bot/index.js +445 -126
  6. package/dist/commands/joinCommandHandler.js +302 -0
  7. package/dist/commands/joinDetachCommandHandler.js +285 -0
  8. package/dist/commands/registerSlashCommands.js +40 -0
  9. package/dist/commands/workspaceCommandHandler.js +17 -28
  10. package/dist/database/chatSessionRepository.js +10 -0
  11. package/dist/database/userPreferenceRepository.js +72 -0
  12. package/dist/events/interactionCreateHandler.js +338 -30
  13. package/dist/events/messageCreateHandler.js +161 -47
  14. package/dist/services/antigravityLauncher.js +4 -3
  15. package/dist/services/approvalDetector.js +7 -0
  16. package/dist/services/assistantDomExtractor.js +339 -0
  17. package/dist/services/cdpBridgeManager.js +323 -39
  18. package/dist/services/cdpConnectionPool.js +117 -33
  19. package/dist/services/cdpService.js +149 -53
  20. package/dist/services/chatSessionService.js +229 -8
  21. package/dist/services/errorPopupDetector.js +271 -0
  22. package/dist/services/planningDetector.js +318 -0
  23. package/dist/services/responseMonitor.js +308 -70
  24. package/dist/services/retryStore.js +46 -0
  25. package/dist/services/updateCheckService.js +147 -0
  26. package/dist/services/userMessageDetector.js +221 -0
  27. package/dist/ui/buttonUtils.js +33 -0
  28. package/dist/ui/modeUi.js +11 -1
  29. package/dist/ui/modelsUi.js +24 -13
  30. package/dist/ui/outputUi.js +30 -0
  31. package/dist/ui/projectListUi.js +83 -0
  32. package/dist/ui/sessionPickerUi.js +48 -0
  33. package/dist/utils/antigravityPaths.js +94 -0
  34. package/dist/utils/configLoader.js +18 -0
  35. package/dist/utils/discordButtonUtils.js +33 -0
  36. package/dist/utils/discordFormatter.js +149 -16
  37. package/dist/utils/htmlToDiscordMarkdown.js +184 -0
  38. package/dist/utils/logBuffer.js +47 -0
  39. package/dist/utils/logFileTransport.js +147 -0
  40. package/dist/utils/logger.js +86 -21
  41. package/dist/utils/pathUtils.js +57 -0
  42. package/dist/utils/plainTextFormatter.js +70 -0
  43. package/dist/utils/processLogBuffer.js +4 -0
  44. package/package.json +4 -4
@@ -1,28 +1,45 @@
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;
6
7
  exports.buildApprovalCustomId = buildApprovalCustomId;
7
8
  exports.parseApprovalCustomId = parseApprovalCustomId;
9
+ exports.buildPlanningCustomId = buildPlanningCustomId;
10
+ exports.parsePlanningCustomId = parsePlanningCustomId;
11
+ exports.buildErrorPopupCustomId = buildErrorPopupCustomId;
12
+ exports.parseErrorPopupCustomId = parseErrorPopupCustomId;
8
13
  exports.initCdpBridge = initCdpBridge;
9
14
  exports.getCurrentCdp = getCurrentCdp;
10
15
  exports.ensureApprovalDetector = ensureApprovalDetector;
16
+ exports.ensurePlanningDetector = ensurePlanningDetector;
17
+ exports.ensureErrorPopupDetector = ensureErrorPopupDetector;
18
+ exports.ensureUserMessageDetector = ensureUserMessageDetector;
11
19
  const discord_js_1 = require("discord.js");
12
20
  const i18n_1 = require("../utils/i18n");
13
21
  const logger_1 = require("../utils/logger");
22
+ const discordButtonUtils_1 = require("../utils/discordButtonUtils");
14
23
  const approvalDetector_1 = require("./approvalDetector");
15
24
  const autoAcceptService_1 = require("./autoAcceptService");
16
25
  const cdpConnectionPool_1 = require("./cdpConnectionPool");
26
+ const errorPopupDetector_1 = require("./errorPopupDetector");
27
+ const planningDetector_1 = require("./planningDetector");
17
28
  const quotaService_1 = require("./quotaService");
29
+ const userMessageDetector_1 = require("./userMessageDetector");
18
30
  const APPROVE_ACTION_PREFIX = 'approve_action';
19
31
  const ALWAYS_ALLOW_ACTION_PREFIX = 'always_allow_action';
20
32
  const DENY_ACTION_PREFIX = 'deny_action';
33
+ const PLANNING_OPEN_ACTION_PREFIX = 'planning_open_action';
34
+ const PLANNING_PROCEED_ACTION_PREFIX = 'planning_proceed_action';
35
+ const ERROR_POPUP_DISMISS_ACTION_PREFIX = 'error_popup_dismiss_action';
36
+ const ERROR_POPUP_COPY_DEBUG_ACTION_PREFIX = 'error_popup_copy_debug_action';
37
+ const ERROR_POPUP_RETRY_ACTION_PREFIX = 'error_popup_retry_action';
21
38
  function normalizeSessionTitle(title) {
22
39
  return title.trim().toLowerCase();
23
40
  }
24
- function buildSessionRouteKey(workspaceDirName, sessionTitle) {
25
- return `${workspaceDirName}::${normalizeSessionTitle(sessionTitle)}`;
41
+ function buildSessionRouteKey(projectName, sessionTitle) {
42
+ return `${projectName}::${normalizeSessionTitle(sessionTitle)}`;
26
43
  }
27
44
  const GET_CURRENT_CHAT_TITLE_SCRIPT = `(() => {
28
45
  const panel = document.querySelector('.antigravity-agent-side-panel');
@@ -54,57 +71,127 @@ async function getCurrentChatTitle(cdp) {
54
71
  }
55
72
  return null;
56
73
  }
57
- function registerApprovalWorkspaceChannel(bridge, workspaceDirName, channel) {
58
- bridge.approvalChannelByWorkspace.set(workspaceDirName, channel);
74
+ function registerApprovalWorkspaceChannel(bridge, projectName, channel) {
75
+ bridge.approvalChannelByWorkspace.set(projectName, channel);
59
76
  }
60
- function registerApprovalSessionChannel(bridge, workspaceDirName, sessionTitle, channel) {
77
+ function registerApprovalSessionChannel(bridge, projectName, sessionTitle, channel) {
61
78
  if (!sessionTitle || sessionTitle.trim().length === 0)
62
79
  return;
63
- bridge.approvalChannelBySession.set(buildSessionRouteKey(workspaceDirName, sessionTitle), channel);
64
- bridge.approvalChannelByWorkspace.set(workspaceDirName, channel);
80
+ bridge.approvalChannelBySession.set(buildSessionRouteKey(projectName, sessionTitle), channel);
81
+ bridge.approvalChannelByWorkspace.set(projectName, channel);
65
82
  }
66
- function resolveApprovalChannelForCurrentChat(bridge, workspaceDirName, currentChatTitle) {
67
- if (!currentChatTitle || currentChatTitle.trim().length === 0) {
68
- return null;
83
+ function resolveApprovalChannelForCurrentChat(bridge, projectName, currentChatTitle) {
84
+ // Try session-level match first (most precise routing)
85
+ if (currentChatTitle && currentChatTitle.trim().length > 0) {
86
+ const key = buildSessionRouteKey(projectName, currentChatTitle);
87
+ const sessionChannel = bridge.approvalChannelBySession.get(key);
88
+ if (sessionChannel)
89
+ return sessionChannel;
69
90
  }
70
- const key = buildSessionRouteKey(workspaceDirName, currentChatTitle);
71
- return bridge.approvalChannelBySession.get(key) ?? null;
91
+ // Fall back to workspace-level routing
92
+ return bridge.approvalChannelByWorkspace.get(projectName) ?? null;
72
93
  }
73
- function buildApprovalCustomId(action, workspaceDirName, channelId) {
94
+ function buildApprovalCustomId(action, projectName, channelId) {
74
95
  const prefix = action === 'approve'
75
96
  ? APPROVE_ACTION_PREFIX
76
97
  : action === 'always_allow'
77
98
  ? ALWAYS_ALLOW_ACTION_PREFIX
78
99
  : DENY_ACTION_PREFIX;
79
100
  if (channelId && channelId.trim().length > 0) {
80
- return `${prefix}:${workspaceDirName}:${channelId}`;
101
+ return `${prefix}:${projectName}:${channelId}`;
81
102
  }
82
- return `${prefix}:${workspaceDirName}`;
103
+ return `${prefix}:${projectName}`;
83
104
  }
84
105
  function parseApprovalCustomId(customId) {
85
106
  if (customId === APPROVE_ACTION_PREFIX) {
86
- return { action: 'approve', workspaceDirName: null, channelId: null };
107
+ return { action: 'approve', projectName: null, channelId: null };
87
108
  }
88
109
  if (customId === ALWAYS_ALLOW_ACTION_PREFIX) {
89
- return { action: 'always_allow', workspaceDirName: null, channelId: null };
110
+ return { action: 'always_allow', projectName: null, channelId: null };
90
111
  }
91
112
  if (customId === DENY_ACTION_PREFIX) {
92
- return { action: 'deny', workspaceDirName: null, channelId: null };
113
+ return { action: 'deny', projectName: null, channelId: null };
93
114
  }
94
115
  if (customId.startsWith(`${APPROVE_ACTION_PREFIX}:`)) {
95
116
  const rest = customId.substring(`${APPROVE_ACTION_PREFIX}:`.length);
96
- const [workspaceDirName, channelId] = rest.split(':');
97
- 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 };
98
119
  }
99
120
  if (customId.startsWith(`${ALWAYS_ALLOW_ACTION_PREFIX}:`)) {
100
121
  const rest = customId.substring(`${ALWAYS_ALLOW_ACTION_PREFIX}:`.length);
101
- const [workspaceDirName, channelId] = rest.split(':');
102
- 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 };
103
124
  }
104
125
  if (customId.startsWith(`${DENY_ACTION_PREFIX}:`)) {
105
126
  const rest = customId.substring(`${DENY_ACTION_PREFIX}:`.length);
106
- const [workspaceDirName, channelId] = rest.split(':');
107
- 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 };
129
+ }
130
+ return null;
131
+ }
132
+ function buildPlanningCustomId(action, projectName, channelId) {
133
+ const prefix = action === 'open'
134
+ ? PLANNING_OPEN_ACTION_PREFIX
135
+ : PLANNING_PROCEED_ACTION_PREFIX;
136
+ if (channelId && channelId.trim().length > 0) {
137
+ return `${prefix}:${projectName}:${channelId}`;
138
+ }
139
+ return `${prefix}:${projectName}`;
140
+ }
141
+ function parsePlanningCustomId(customId) {
142
+ if (customId === PLANNING_OPEN_ACTION_PREFIX) {
143
+ return { action: 'open', projectName: null, channelId: null };
144
+ }
145
+ if (customId === PLANNING_PROCEED_ACTION_PREFIX) {
146
+ return { action: 'proceed', projectName: null, channelId: null };
147
+ }
148
+ if (customId.startsWith(`${PLANNING_OPEN_ACTION_PREFIX}:`)) {
149
+ const rest = customId.substring(`${PLANNING_OPEN_ACTION_PREFIX}:`.length);
150
+ const [projectName, channelId] = rest.split(':');
151
+ return { action: 'open', projectName: projectName || null, channelId: channelId || null };
152
+ }
153
+ if (customId.startsWith(`${PLANNING_PROCEED_ACTION_PREFIX}:`)) {
154
+ const rest = customId.substring(`${PLANNING_PROCEED_ACTION_PREFIX}:`.length);
155
+ const [projectName, channelId] = rest.split(':');
156
+ return { action: 'proceed', projectName: projectName || null, channelId: channelId || null };
157
+ }
158
+ return null;
159
+ }
160
+ function buildErrorPopupCustomId(action, projectName, channelId) {
161
+ const prefix = action === 'dismiss'
162
+ ? ERROR_POPUP_DISMISS_ACTION_PREFIX
163
+ : action === 'copy_debug'
164
+ ? ERROR_POPUP_COPY_DEBUG_ACTION_PREFIX
165
+ : ERROR_POPUP_RETRY_ACTION_PREFIX;
166
+ if (channelId && channelId.trim().length > 0) {
167
+ return `${prefix}:${projectName}:${channelId}`;
168
+ }
169
+ return `${prefix}:${projectName}`;
170
+ }
171
+ function parseErrorPopupCustomId(customId) {
172
+ if (customId === ERROR_POPUP_DISMISS_ACTION_PREFIX) {
173
+ return { action: 'dismiss', projectName: null, channelId: null };
174
+ }
175
+ if (customId === ERROR_POPUP_COPY_DEBUG_ACTION_PREFIX) {
176
+ return { action: 'copy_debug', projectName: null, channelId: null };
177
+ }
178
+ if (customId === ERROR_POPUP_RETRY_ACTION_PREFIX) {
179
+ return { action: 'retry', projectName: null, channelId: null };
180
+ }
181
+ if (customId.startsWith(`${ERROR_POPUP_DISMISS_ACTION_PREFIX}:`)) {
182
+ const rest = customId.substring(`${ERROR_POPUP_DISMISS_ACTION_PREFIX}:`.length);
183
+ const [projectName, channelId] = rest.split(':');
184
+ return { action: 'dismiss', projectName: projectName || null, channelId: channelId || null };
185
+ }
186
+ if (customId.startsWith(`${ERROR_POPUP_COPY_DEBUG_ACTION_PREFIX}:`)) {
187
+ const rest = customId.substring(`${ERROR_POPUP_COPY_DEBUG_ACTION_PREFIX}:`.length);
188
+ const [projectName, channelId] = rest.split(':');
189
+ return { action: 'copy_debug', projectName: projectName || null, channelId: channelId || null };
190
+ }
191
+ if (customId.startsWith(`${ERROR_POPUP_RETRY_ACTION_PREFIX}:`)) {
192
+ const rest = customId.substring(`${ERROR_POPUP_RETRY_ACTION_PREFIX}:`.length);
193
+ const [projectName, channelId] = rest.split(':');
194
+ return { action: 'retry', projectName: projectName || null, channelId: channelId || null };
108
195
  }
109
196
  return null;
110
197
  }
@@ -143,20 +230,42 @@ function getCurrentCdp(bridge) {
143
230
  * Helper to start an approval detector for each workspace.
144
231
  * Does nothing if a detector for the same workspace is already running.
145
232
  */
146
- function ensureApprovalDetector(bridge, cdp, workspaceDirName, client) {
147
- const existing = bridge.pool.getApprovalDetector(workspaceDirName);
233
+ function ensureApprovalDetector(bridge, cdp, projectName, client) {
234
+ const existing = bridge.pool.getApprovalDetector(projectName);
148
235
  if (existing && existing.isActive())
149
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;
150
242
  const detector = new approvalDetector_1.ApprovalDetector({
151
243
  cdpService: cdp,
152
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
+ },
153
262
  onApprovalRequired: async (info) => {
154
- 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}")`);
155
264
  const currentChatTitle = await getCurrentChatTitle(cdp);
156
- const targetChannel = resolveApprovalChannelForCurrentChat(bridge, workspaceDirName, currentChatTitle);
265
+ const targetChannel = resolveApprovalChannelForCurrentChat(bridge, projectName, currentChatTitle);
157
266
  const targetChannelId = targetChannel && 'id' in targetChannel ? String(targetChannel.id) : '';
158
267
  if (!targetChannel || !targetChannelId || !('send' in targetChannel)) {
159
- 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` +
160
269
  `${currentChatTitle ? ` (title="${currentChatTitle}")` : ''}`);
161
270
  return;
162
271
  }
@@ -164,10 +273,16 @@ function ensureApprovalDetector(bridge, cdp, workspaceDirName, client) {
164
273
  const accepted = await detector.alwaysAllowButton() || await detector.approveButton();
165
274
  const autoEmbed = new discord_js_1.EmbedBuilder()
166
275
  .setTitle(accepted ? (0, i18n_1.t)('Auto-approved') : (0, i18n_1.t)('Auto-approve failed'))
167
- .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.'))
168
277
  .setColor(accepted ? 0x2ECC71 : 0xF39C12)
169
- .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 })
170
- .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();
171
286
  await targetChannel.send({ embeds: [autoEmbed] }).catch(logger_1.logger.error);
172
287
  if (accepted) {
173
288
  return;
@@ -177,28 +292,197 @@ function ensureApprovalDetector(bridge, cdp, workspaceDirName, client) {
177
292
  .setTitle((0, i18n_1.t)('Approval Required'))
178
293
  .setDescription(info.description || (0, i18n_1.t)('Antigravity is requesting approval for an action'))
179
294
  .setColor(0xFFA500)
180
- .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 })
181
296
  .setTimestamp();
182
297
  const approveBtn = new discord_js_1.ButtonBuilder()
183
- .setCustomId(buildApprovalCustomId('approve', workspaceDirName, targetChannelId))
298
+ .setCustomId(buildApprovalCustomId('approve', projectName, targetChannelId))
184
299
  .setLabel((0, i18n_1.t)('Allow'))
185
300
  .setStyle(discord_js_1.ButtonStyle.Success);
186
301
  const alwaysAllowBtn = new discord_js_1.ButtonBuilder()
187
- .setCustomId(buildApprovalCustomId('always_allow', workspaceDirName, targetChannelId))
302
+ .setCustomId(buildApprovalCustomId('always_allow', projectName, targetChannelId))
188
303
  .setLabel((0, i18n_1.t)('Allow Chat'))
189
304
  .setStyle(discord_js_1.ButtonStyle.Primary);
190
305
  const denyBtn = new discord_js_1.ButtonBuilder()
191
- .setCustomId(buildApprovalCustomId('deny', workspaceDirName, targetChannelId))
306
+ .setCustomId(buildApprovalCustomId('deny', projectName, targetChannelId))
192
307
  .setLabel((0, i18n_1.t)('Deny'))
193
308
  .setStyle(discord_js_1.ButtonStyle.Danger);
194
309
  const row = new discord_js_1.ActionRowBuilder().addComponents(approveBtn, alwaysAllowBtn, denyBtn);
195
- targetChannel.send({
310
+ const sent = await targetChannel.send({
196
311
  embeds: [embed],
197
312
  components: [row],
313
+ }).catch((err) => { logger_1.logger.error(err); return null; });
314
+ if (sent) {
315
+ lastButtonMessage = sent;
316
+ }
317
+ },
318
+ });
319
+ detector.start();
320
+ bridge.pool.registerApprovalDetector(projectName, detector);
321
+ logger_1.logger.debug(`[ApprovalDetector:${projectName}] Started approval button detection`);
322
+ }
323
+ /**
324
+ * Helper to start a planning detector for each workspace.
325
+ * Does nothing if a detector for the same workspace is already running.
326
+ */
327
+ function ensurePlanningDetector(bridge, cdp, projectName, _client) {
328
+ const existing = bridge.pool.getPlanningDetector(projectName);
329
+ if (existing && existing.isActive())
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;
334
+ const detector = new planningDetector_1.PlanningDetector({
335
+ cdpService: cdp,
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),
198
352
  }).catch(logger_1.logger.error);
199
353
  },
354
+ onPlanningRequired: async (info) => {
355
+ logger_1.logger.debug(`[PlanningDetector:${projectName}] Planning buttons detected (title="${info.planTitle}")`);
356
+ const currentChatTitle = await getCurrentChatTitle(cdp);
357
+ const targetChannel = resolveApprovalChannelForCurrentChat(bridge, projectName, currentChatTitle);
358
+ const targetChannelId = targetChannel && 'id' in targetChannel ? String(targetChannel.id) : '';
359
+ if (!targetChannel || !targetChannelId || !('send' in targetChannel)) {
360
+ logger_1.logger.warn(`[PlanningDetector:${projectName}] Skipped planning notification because chat is not linked to a Discord session` +
361
+ `${currentChatTitle ? ` (title="${currentChatTitle}")` : ''}`);
362
+ return;
363
+ }
364
+ const descriptionText = info.description || info.planSummary || (0, i18n_1.t)('A plan has been generated and is awaiting your review.');
365
+ const embed = new discord_js_1.EmbedBuilder()
366
+ .setTitle((0, i18n_1.t)('Planning Mode'))
367
+ .setDescription(descriptionText)
368
+ .setColor(0x3498DB)
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 })
370
+ .setTimestamp();
371
+ if (info.planSummary && info.description) {
372
+ embed.addFields({ name: (0, i18n_1.t)('Summary'), value: info.planSummary.substring(0, 1024), inline: false });
373
+ }
374
+ const openBtn = new discord_js_1.ButtonBuilder()
375
+ .setCustomId(buildPlanningCustomId('open', projectName, targetChannelId))
376
+ .setLabel((0, i18n_1.t)('Open'))
377
+ .setStyle(discord_js_1.ButtonStyle.Secondary);
378
+ const proceedBtn = new discord_js_1.ButtonBuilder()
379
+ .setCustomId(buildPlanningCustomId('proceed', projectName, targetChannelId))
380
+ .setLabel((0, i18n_1.t)('Proceed'))
381
+ .setStyle(discord_js_1.ButtonStyle.Primary);
382
+ const row = new discord_js_1.ActionRowBuilder().addComponents(openBtn, proceedBtn);
383
+ const sent = await targetChannel.send({
384
+ embeds: [embed],
385
+ components: [row],
386
+ }).catch((err) => { logger_1.logger.error(err); return null; });
387
+ if (sent) {
388
+ lastPlanningMessage = sent;
389
+ }
390
+ },
391
+ });
392
+ detector.start();
393
+ bridge.pool.registerPlanningDetector(projectName, detector);
394
+ logger_1.logger.debug(`[PlanningDetector:${projectName}] Started planning button detection`);
395
+ }
396
+ /**
397
+ * Helper to start an error popup detector for each workspace.
398
+ * Does nothing if a detector for the same workspace is already running.
399
+ */
400
+ function ensureErrorPopupDetector(bridge, cdp, projectName, _client) {
401
+ const existing = bridge.pool.getErrorPopupDetector(projectName);
402
+ if (existing && existing.isActive())
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;
407
+ const detector = new errorPopupDetector_1.ErrorPopupDetector({
408
+ cdpService: cdp,
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
+ },
427
+ onErrorPopup: async (info) => {
428
+ logger_1.logger.debug(`[ErrorPopupDetector:${projectName}] Error popup detected (title="${info.title}")`);
429
+ const currentChatTitle = await getCurrentChatTitle(cdp);
430
+ const targetChannel = resolveApprovalChannelForCurrentChat(bridge, projectName, currentChatTitle);
431
+ const targetChannelId = targetChannel && 'id' in targetChannel ? String(targetChannel.id) : '';
432
+ if (!targetChannel || !targetChannelId || !('send' in targetChannel)) {
433
+ logger_1.logger.warn(`[ErrorPopupDetector:${projectName}] Skipped error popup notification because chat is not linked to a Discord session` +
434
+ `${currentChatTitle ? ` (title="${currentChatTitle}")` : ''}`);
435
+ return;
436
+ }
437
+ const bodyText = info.body || (0, i18n_1.t)('An error occurred in the Antigravity agent.');
438
+ const embed = new discord_js_1.EmbedBuilder()
439
+ .setTitle(info.title || (0, i18n_1.t)('Agent Error'))
440
+ .setDescription(bodyText.substring(0, 4096))
441
+ .setColor(0xE74C3C)
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 })
443
+ .setTimestamp();
444
+ const dismissBtn = new discord_js_1.ButtonBuilder()
445
+ .setCustomId(buildErrorPopupCustomId('dismiss', projectName, targetChannelId))
446
+ .setLabel((0, i18n_1.t)('Dismiss'))
447
+ .setStyle(discord_js_1.ButtonStyle.Secondary);
448
+ const copyDebugBtn = new discord_js_1.ButtonBuilder()
449
+ .setCustomId(buildErrorPopupCustomId('copy_debug', projectName, targetChannelId))
450
+ .setLabel((0, i18n_1.t)('Copy debug info'))
451
+ .setStyle(discord_js_1.ButtonStyle.Primary);
452
+ const retryBtn = new discord_js_1.ButtonBuilder()
453
+ .setCustomId(buildErrorPopupCustomId('retry', projectName, targetChannelId))
454
+ .setLabel((0, i18n_1.t)('Retry'))
455
+ .setStyle(discord_js_1.ButtonStyle.Success);
456
+ const row = new discord_js_1.ActionRowBuilder().addComponents(dismissBtn, copyDebugBtn, retryBtn);
457
+ const sent = await targetChannel.send({
458
+ embeds: [embed],
459
+ components: [row],
460
+ }).catch((err) => { logger_1.logger.error(err); return null; });
461
+ if (sent) {
462
+ lastErrorMessage = sent;
463
+ }
464
+ },
465
+ });
466
+ detector.start();
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,
200
484
  });
201
485
  detector.start();
202
- bridge.pool.registerApprovalDetector(workspaceDirName, detector);
203
- logger_1.logger.info(`[ApprovalDetector:${workspaceDirName}] Started approval button detection`);
486
+ bridge.pool.registerUserMessageDetector(projectName, detector);
487
+ logger_1.logger.debug(`[UserMessageDetector:${projectName}] Started user message detection`);
204
488
  }