lazy-gravity 0.9.2 → 0.11.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 (41) hide show
  1. package/dist/bin/commands/doctor.js +45 -7
  2. package/dist/bot/index.js +401 -41
  3. package/dist/bot/telegramMessageHandler.js +1 -0
  4. package/dist/commands/chatCommandHandler.js +43 -3
  5. package/dist/commands/registerSlashCommands.js +13 -1
  6. package/dist/events/interactionCreateHandler.js +456 -27
  7. package/dist/events/messageCreateHandler.js +66 -7
  8. package/dist/handlers/__tests__/fileChangeButtonAction.test.js +85 -0
  9. package/dist/handlers/approvalButtonAction.js +2 -1
  10. package/dist/handlers/errorPopupButtonAction.js +2 -1
  11. package/dist/handlers/fileChangeButtonAction.js +57 -0
  12. package/dist/handlers/genericActionButtonAction.js +72 -0
  13. package/dist/handlers/planningButtonAction.js +126 -23
  14. package/dist/handlers/planningModalSubmitAction.js +38 -0
  15. package/dist/handlers/questionSelectAction.js +70 -0
  16. package/dist/handlers/questionSkipAction.js +68 -0
  17. package/dist/handlers/runCommandButtonAction.js +2 -1
  18. package/dist/platform/discord/discordResponseRenderer.js +164 -0
  19. package/dist/platform/discord/wrappers.js +76 -0
  20. package/dist/services/approvalDetector.js +110 -35
  21. package/dist/services/artifactService.js +103 -37
  22. package/dist/services/assistantDomExtractor.js +194 -3
  23. package/dist/services/cdpBridgeManager.js +149 -8
  24. package/dist/services/cdpConnectionPool.js +22 -2
  25. package/dist/services/cdpService.js +134 -15
  26. package/dist/services/chatSessionService.js +147 -40
  27. package/dist/services/errorPopupDetector.js +23 -11
  28. package/dist/services/notificationSender.js +80 -3
  29. package/dist/services/planningDetector.js +69 -25
  30. package/dist/services/promptDispatcher.js +27 -1
  31. package/dist/services/questionDetector.js +428 -0
  32. package/dist/services/responseMonitor.js +45 -3
  33. package/dist/services/runCommandDetector.js +14 -7
  34. package/dist/ui/artifactsUi.js +4 -3
  35. package/dist/utils/consecutiveEmptyPollGate.js +21 -0
  36. package/dist/utils/fileOpenCache.js +29 -0
  37. package/dist/utils/htmlToDiscordMarkdown.js +5 -2
  38. package/dist/utils/pathUtils.js +12 -0
  39. package/dist/utils/projectResolver.js +10 -0
  40. package/dist/utils/questionActionUtils.js +47 -0
  41. package/package.json +1 -1
@@ -27,6 +27,12 @@ function classifyAssistantSegments(payload) {
27
27
  finalOutputText: '',
28
28
  activityLines: [],
29
29
  feedback: [],
30
+ planCards: [],
31
+ actionButtons: [],
32
+ fileChanges: [],
33
+ citations: [],
34
+ fileChangesTexts: [],
35
+ citedFiles: [],
30
36
  diagnostics: {
31
37
  source: 'legacy-fallback',
32
38
  segmentCounts: {},
@@ -41,8 +47,13 @@ function classifyAssistantSegments(payload) {
41
47
  const bodyTexts = [];
42
48
  const activityLines = [];
43
49
  const feedbackTexts = [];
50
+ const planCards = [];
51
+ const actionButtons = [];
52
+ const fileChanges = [];
53
+ const citations = [];
44
54
  const segmentCounts = {};
45
55
  const allFingerprints = [];
56
+ const fileChangesTexts = [];
46
57
  for (const seg of segments) {
47
58
  segmentCounts[seg.kind] = (segmentCounts[seg.kind] ?? 0) + 1;
48
59
  switch (seg.kind) {
@@ -65,16 +76,68 @@ function classifyAssistantSegments(payload) {
65
76
  feedbackTexts.push(seg.text.trim());
66
77
  }
67
78
  break;
79
+ case 'plan-card':
80
+ if (seg.text && seg.text.trim()) {
81
+ planCards.push(seg.text.trim());
82
+ }
83
+ break;
84
+ case 'action-button':
85
+ if (seg.text && seg.text.trim()) {
86
+ actionButtons.push(seg.text.trim());
87
+ }
88
+ break;
89
+ case 'file-change':
90
+ if (seg.text && seg.text.trim()) {
91
+ const parts = seg.text.split('|');
92
+ fileChanges.push({ path: parts[0]?.trim() || '', type: parts[1]?.trim() || '' });
93
+ }
94
+ break;
95
+ case 'citation':
96
+ if (seg.text && seg.text.trim()) {
97
+ citations.push(seg.text.trim());
98
+ }
99
+ break;
100
+ case 'file-changes':
101
+ if (seg.text && seg.text.trim()) {
102
+ fileChangesTexts.push(seg.text.trim());
103
+ }
104
+ break;
68
105
  }
69
106
  }
70
107
  // Concatenate all body segments — the CDP script may yield multiple nodes
71
108
  // (streaming partial + final, multi-block markdown, etc.)
72
109
  const lastBody = bodyTexts.join('\n\n') || '';
73
110
  const finalOutputText = (0, htmlToDiscordMarkdown_1.htmlToDiscordMarkdown)(lastBody);
111
+ // Extract file:/// links from finalOutputText and preserve citations from Pass 4
112
+ const citedFiles = Array.from(new Set([...citations]));
113
+ const fileRegex = /\[.*?\]\((file:\/\/\/[^\)]+)\)/g;
114
+ let match;
115
+ while ((match = fileRegex.exec(finalOutputText)) !== null) {
116
+ const fileUrl = match[1];
117
+ if (!citedFiles.includes(fileUrl)) {
118
+ citedFiles.push(fileUrl);
119
+ }
120
+ }
121
+ // Also extract backticked inline code that looks like a filename with common extensions
122
+ // e.g., `implementation_plan.md`
123
+ const inlineCodeRegex = /`([a-zA-Z0-9_\-\.]+\.(?:md|ts|js|json|html|css|txt|csv|py|java|go|cpp|c))`[^`]?/g;
124
+ let inlineMatch;
125
+ while ((inlineMatch = inlineCodeRegex.exec(finalOutputText)) !== null) {
126
+ const filename = inlineMatch[1];
127
+ if (!citedFiles.includes(filename)) {
128
+ citedFiles.push(filename);
129
+ }
130
+ }
74
131
  return {
75
132
  finalOutputText,
76
133
  activityLines,
77
134
  feedback: feedbackTexts,
135
+ planCards,
136
+ actionButtons,
137
+ fileChanges,
138
+ citations,
139
+ fileChangesTexts,
140
+ citedFiles,
78
141
  diagnostics: {
79
142
  source: 'dom-structured',
80
143
  segmentCounts,
@@ -145,10 +208,18 @@ function extractAssistantSegmentsPayloadScript() {
145
208
  if (node.closest('details')) return true;
146
209
  if (node.closest('[class*="feedback"], footer')) return true;
147
210
  if (node.closest('.notify-user-container')) return true;
148
- if (node.closest('[role="dialog"]')) return true;
149
- if (node.closest('form')) return true;
211
+ if (node.closest('dialog, [role="dialog"]')) return true;
212
+ // Guard against other approval forms leaking into the output
213
+ var formNode = node.closest('form');
214
+ if (formNode && formNode.querySelector('[data-testid="plan-card"], [class*="plan-summary"], .actions-container, .review-button')) {
215
+ return true;
216
+ }
217
+
218
+ // allow interactive tool forms (ask_question, ask_permission)
150
219
  if (node.closest('[data-message-author-role="user"], [data-message-role="user"]')) return true;
151
- if (node.querySelector('textarea') || node.closest('textarea')) return true;
220
+ // We do not exclude nodes just because they contain a textarea (e.g. ask_question "Other" option).
221
+ // The main chat input is excluded by the "ask anything" check below.
222
+ if (node.tagName && node.tagName.toLowerCase() === 'textarea') return true;
152
223
  var text = (node.innerText || '').toLowerCase();
153
224
  if (text.includes('ask anything, @ to mention')) return true;
154
225
  if (text.includes('0 files with changes')) return true;
@@ -158,6 +229,7 @@ function extractAssistantSegmentsPayloadScript() {
158
229
  var segments = [];
159
230
  var seen = new Set();
160
231
  var bodyFound = false;
232
+ var latestMessageContainer = null;
161
233
 
162
234
  // Pass 1: Find assistant body — last non-excluded content node (recency first)
163
235
  var combinedSelector = selectors.join(', ');
@@ -238,6 +310,7 @@ function extractAssistantSegmentsPayloadScript() {
238
310
  domPath: 'article:nth(' + i + ')'
239
311
  });
240
312
  bodyFound = true;
313
+ latestMessageContainer = node.closest('[data-message-id], .message-row, [class*="message-container"]') || node.parentNode || scope;
241
314
  break; // Only capture the single latest assistant message
242
315
  }
243
316
  }
@@ -362,6 +435,124 @@ function extractAssistantSegmentsPayloadScript() {
362
435
  }
363
436
  }
364
437
 
438
+ // Pass 4: Extract Antigravity 2.0 structured cards and artifacts
439
+ var artifactScope = latestMessageContainer || scope;
440
+ // 4a. Plan Cards
441
+ var planNodes = artifactScope.querySelectorAll('[data-testid="plan-card"], [class*="plan-summary"]');
442
+ for (var pi2 = 0; pi2 < planNodes.length; pi2++) {
443
+ var pnode = planNodes[pi2];
444
+ var ptext = (pnode.innerText || pnode.textContent || '').trim();
445
+ if (ptext) {
446
+ segments.push({
447
+ kind: 'plan-card',
448
+ text: ptext,
449
+ role: 'assistant',
450
+ messageIndex: 0,
451
+ domPath: 'plan-card:nth(' + pi2 + ')'
452
+ });
453
+ }
454
+ }
455
+
456
+ // 4b. Action Buttons (Open, Proceed, Review, etc.) - outside feedback footers
457
+ var actionBtns = artifactScope.querySelectorAll('.actions-container button, [class*="action-btn"], .review-button, [class*="review-btn"]');
458
+ for (var abi = 0; abi < actionBtns.length; abi++) {
459
+ var abtnText = (actionBtns[abi].textContent || '').trim();
460
+ if (!abtnText) continue;
461
+ var lower = abtnText.toLowerCase();
462
+ // Ignore approval buttons since they are handled by approvalDetector
463
+ if (lower.includes('accept all') || lower.includes('reject all')) continue;
464
+
465
+ segments.push({
466
+ kind: 'action-button',
467
+ text: abtnText,
468
+ role: 'assistant',
469
+ messageIndex: 0,
470
+ domPath: 'action-btn.' + abtnText.toLowerCase().replace(/\s+/g, '-')
471
+ });
472
+ }
473
+
474
+ // 4c. File Edits
475
+ var fileEdits = artifactScope.querySelectorAll('[class*="file-edit-item"], .file-edit-item, .bottom-full .cursor-pointer');
476
+ for (var fei = 0; fei < fileEdits.length; fei++) {
477
+ var editNode = fileEdits[fei];
478
+ if (seen.has(editNode)) continue;
479
+ seen.add(editNode);
480
+ var fpath = (editNode.querySelector('.file-path, [class*="path"]') || {}).textContent || '';
481
+ var ftype = (editNode.querySelector('.edit-type, [class*="type"]') || {}).textContent || 'Modified';
482
+
483
+ if (!fpath) {
484
+ var spans = editNode.querySelectorAll('span.whitespace-nowrap.text-sm');
485
+ if (spans.length > 0) {
486
+ fpath = spans[0].textContent || '';
487
+ }
488
+ }
489
+ if (!fpath) {
490
+ fpath = (editNode.textContent || '').trim();
491
+ if (fpath.endsWith(ftype)) {
492
+ fpath = fpath.slice(0, -ftype.length).trim();
493
+ }
494
+ }
495
+ if (fpath) {
496
+ segments.push({
497
+ kind: 'file-change',
498
+ text: fpath.trim() + '|' + ftype.trim(),
499
+ role: 'assistant',
500
+ messageIndex: 0,
501
+ domPath: 'file-edit:nth(' + fei + ')'
502
+ });
503
+ }
504
+ }
505
+
506
+ // 4d. Citations
507
+ var citationNodes = artifactScope.querySelectorAll('[class*="citation"], a[href^="file://"]');
508
+ for (var cti = 0; cti < citationNodes.length; cti++) {
509
+ var cnode = citationNodes[cti];
510
+ var ctext = (cnode.getAttribute('href') || cnode.textContent || '').trim();
511
+ if (ctext) {
512
+ segments.push({
513
+ kind: 'citation',
514
+ text: ctext,
515
+ role: 'assistant',
516
+ messageIndex: 0,
517
+ domPath: 'citation:nth(' + cti + ')'
518
+ });
519
+ }
520
+ }
521
+ // 4e. File Changes Text Blocks
522
+ var fcbNodes = artifactScope.querySelectorAll('.file-changes-block code, [class*="file-changes"] code');
523
+ for (var fci = 0; fci < fcbNodes.length; fci++) {
524
+ var fcNode = fcbNodes[fci];
525
+ var fcText = (fcNode.textContent || '').trim();
526
+ if (fcText) {
527
+ segments.push({
528
+ kind: 'file-changes',
529
+ text: fcText,
530
+ role: 'assistant',
531
+ messageIndex: 0,
532
+ domPath: 'file-changes:nth(' + fci + ')'
533
+ });
534
+ }
535
+ }
536
+ // 4f. Artifact Cards
537
+ var artifactCards = artifactScope.querySelectorAll('[data-testid="artifact-card"], [class*="artifact-card"]');
538
+ for (var aci = 0; aci < artifactCards.length; aci++) {
539
+ var card = artifactCards[aci];
540
+ var titleEl = card.querySelector('.title, [class*="title"], h3, h4, span');
541
+ if (titleEl) {
542
+ var titleText = (titleEl.textContent || '').trim();
543
+ if (titleText) {
544
+ segments.push({
545
+ kind: 'citation',
546
+ text: titleText + '.md',
547
+ role: 'assistant',
548
+ messageIndex: 0,
549
+ domPath: 'artifact-card:nth(' + aci + ')'
550
+ });
551
+ }
552
+ }
553
+ }
554
+
555
+
365
556
  if (!bodyFound && segments.length === 0) return null;
366
557
 
367
558
  return {
@@ -1,5 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.FILE_CHANGE_REJECT_ACTION_PREFIX = exports.FILE_CHANGE_ACCEPT_ACTION_PREFIX = exports.QUESTION_SELECT_ACTION_PREFIX = void 0;
3
4
  exports.getCurrentChatTitle = getCurrentChatTitle;
4
5
  exports.registerApprovalWorkspaceChannel = registerApprovalWorkspaceChannel;
5
6
  exports.registerApprovalSessionChannel = registerApprovalSessionChannel;
@@ -12,6 +13,8 @@ exports.buildErrorPopupCustomId = buildErrorPopupCustomId;
12
13
  exports.parseErrorPopupCustomId = parseErrorPopupCustomId;
13
14
  exports.buildRunCommandCustomId = buildRunCommandCustomId;
14
15
  exports.parseRunCommandCustomId = parseRunCommandCustomId;
16
+ exports.buildFileChangeCustomId = buildFileChangeCustomId;
17
+ exports.parseFileChangeCustomId = parseFileChangeCustomId;
15
18
  exports.initCdpBridge = initCdpBridge;
16
19
  exports.getCurrentCdp = getCurrentCdp;
17
20
  exports.ensureApprovalDetector = ensureApprovalDetector;
@@ -19,6 +22,7 @@ exports.ensurePlanningDetector = ensurePlanningDetector;
19
22
  exports.ensureErrorPopupDetector = ensureErrorPopupDetector;
20
23
  exports.ensureRunCommandDetector = ensureRunCommandDetector;
21
24
  exports.ensureUserMessageDetector = ensureUserMessageDetector;
25
+ exports.ensureQuestionDetector = ensureQuestionDetector;
22
26
  const i18n_1 = require("../utils/i18n");
23
27
  const logger_1 = require("../utils/logger");
24
28
  const notificationSender_1 = require("./notificationSender");
@@ -30,16 +34,21 @@ const planningDetector_1 = require("./planningDetector");
30
34
  const runCommandDetector_1 = require("./runCommandDetector");
31
35
  const quotaService_1 = require("./quotaService");
32
36
  const userMessageDetector_1 = require("./userMessageDetector");
37
+ const questionDetector_1 = require("./questionDetector");
33
38
  const APPROVE_ACTION_PREFIX = 'approve_action';
34
39
  const ALWAYS_ALLOW_ACTION_PREFIX = 'always_allow_action';
35
40
  const DENY_ACTION_PREFIX = 'deny_action';
36
41
  const PLANNING_OPEN_ACTION_PREFIX = 'planning_open_action';
37
42
  const PLANNING_PROCEED_ACTION_PREFIX = 'planning_proceed_action';
43
+ const PLANNING_REJECT_ACTION_PREFIX = 'planning_reject_action';
38
44
  const ERROR_POPUP_DISMISS_ACTION_PREFIX = 'error_popup_dismiss_action';
39
45
  const ERROR_POPUP_COPY_DEBUG_ACTION_PREFIX = 'error_popup_copy_debug_action';
40
46
  const ERROR_POPUP_RETRY_ACTION_PREFIX = 'error_popup_retry_action';
41
47
  const RUN_COMMAND_RUN_ACTION_PREFIX = 'run_command_run_action';
42
48
  const RUN_COMMAND_REJECT_ACTION_PREFIX = 'run_command_reject_action';
49
+ exports.QUESTION_SELECT_ACTION_PREFIX = 'question_select_action';
50
+ exports.FILE_CHANGE_ACCEPT_ACTION_PREFIX = 'ide_file_accept_all';
51
+ exports.FILE_CHANGE_REJECT_ACTION_PREFIX = 'ide_file_reject_all';
43
52
  function normalizeSessionTitle(title) {
44
53
  return title.trim().toLowerCase();
45
54
  }
@@ -47,7 +56,7 @@ function buildSessionRouteKey(projectName, sessionTitle) {
47
56
  return `${projectName}::${normalizeSessionTitle(sessionTitle)}`;
48
57
  }
49
58
  const GET_CURRENT_CHAT_TITLE_SCRIPT = `(() => {
50
- const panel = document.querySelector('.antigravity-agent-side-panel');
59
+ const panel = document.querySelector('.antigravity-agent-side-panel') || document.body;
51
60
  if (!panel) return '';
52
61
  const header = panel.querySelector('div[class*="border-b"]');
53
62
  if (!header) return '';
@@ -137,7 +146,9 @@ function parseApprovalCustomId(customId) {
137
146
  function buildPlanningCustomId(action, projectName, channelId) {
138
147
  const prefix = action === 'open'
139
148
  ? PLANNING_OPEN_ACTION_PREFIX
140
- : PLANNING_PROCEED_ACTION_PREFIX;
149
+ : action === 'proceed'
150
+ ? PLANNING_PROCEED_ACTION_PREFIX
151
+ : PLANNING_REJECT_ACTION_PREFIX;
141
152
  if (channelId && channelId.trim().length > 0) {
142
153
  return `${prefix}:${projectName}:${channelId}`;
143
154
  }
@@ -150,6 +161,9 @@ function parsePlanningCustomId(customId) {
150
161
  if (customId === PLANNING_PROCEED_ACTION_PREFIX) {
151
162
  return { action: 'proceed', projectName: null, channelId: null };
152
163
  }
164
+ if (customId === PLANNING_REJECT_ACTION_PREFIX) {
165
+ return { action: 'reject', projectName: null, channelId: null };
166
+ }
153
167
  if (customId.startsWith(`${PLANNING_OPEN_ACTION_PREFIX}:`)) {
154
168
  const rest = customId.substring(`${PLANNING_OPEN_ACTION_PREFIX}:`.length);
155
169
  const [projectName, channelId] = rest.split(':');
@@ -160,6 +174,11 @@ function parsePlanningCustomId(customId) {
160
174
  const [projectName, channelId] = rest.split(':');
161
175
  return { action: 'proceed', projectName: projectName || null, channelId: channelId || null };
162
176
  }
177
+ if (customId.startsWith(`${PLANNING_REJECT_ACTION_PREFIX}:`)) {
178
+ const rest = customId.substring(`${PLANNING_REJECT_ACTION_PREFIX}:`.length);
179
+ const [projectName, channelId] = rest.split(':');
180
+ return { action: 'reject', projectName: projectName || null, channelId: channelId || null };
181
+ }
163
182
  return null;
164
183
  }
165
184
  function buildErrorPopupCustomId(action, projectName, channelId) {
@@ -228,6 +247,34 @@ function parseRunCommandCustomId(customId) {
228
247
  }
229
248
  return null;
230
249
  }
250
+ function buildFileChangeCustomId(action, projectName, channelId) {
251
+ const prefix = action === 'accept'
252
+ ? exports.FILE_CHANGE_ACCEPT_ACTION_PREFIX
253
+ : exports.FILE_CHANGE_REJECT_ACTION_PREFIX;
254
+ if (channelId && channelId.trim().length > 0) {
255
+ return `${prefix}:${projectName}:${channelId}`;
256
+ }
257
+ return `${prefix}:${projectName}`;
258
+ }
259
+ function parseFileChangeCustomId(customId) {
260
+ if (customId === exports.FILE_CHANGE_ACCEPT_ACTION_PREFIX) {
261
+ return { action: 'accept', projectName: null, channelId: null };
262
+ }
263
+ if (customId === exports.FILE_CHANGE_REJECT_ACTION_PREFIX) {
264
+ return { action: 'reject', projectName: null, channelId: null };
265
+ }
266
+ if (customId.startsWith(`${exports.FILE_CHANGE_ACCEPT_ACTION_PREFIX}:`)) {
267
+ const rest = customId.substring(`${exports.FILE_CHANGE_ACCEPT_ACTION_PREFIX}:`.length);
268
+ const [projectName, channelId] = rest.split(':');
269
+ return { action: 'accept', projectName: projectName || null, channelId: channelId || null };
270
+ }
271
+ if (customId.startsWith(`${exports.FILE_CHANGE_REJECT_ACTION_PREFIX}:`)) {
272
+ const rest = customId.substring(`${exports.FILE_CHANGE_REJECT_ACTION_PREFIX}:`.length);
273
+ const [projectName, channelId] = rest.split(':');
274
+ return { action: 'reject', projectName: projectName || null, channelId: channelId || null };
275
+ }
276
+ return null;
277
+ }
231
278
  /** Initialize the CDP bridge (lazy connection: pool creation only) */
232
279
  function initCdpBridge(autoApproveDefault, accountPorts = {}, accountUserDataDirs = {}, cdpHost = '127.0.0.1') {
233
280
  const pool = new cdpConnectionPool_1.CdpConnectionPool({
@@ -290,6 +337,8 @@ function ensureApprovalDetector(bridge, cdp, projectName, accountName = 'default
290
337
  },
291
338
  onApprovalRequired: async (info) => {
292
339
  logger_1.logger.debug(`[ApprovalDetector:${projectName}] Approval button detected (allow="${info.approveText}", deny="${info.denyText}")`);
340
+ // Emit approval event for bot text updating
341
+ cdp.emit('approval_required', info);
293
342
  const currentChatTitle = await getCurrentChatTitle(cdp);
294
343
  const targetChannel = resolveApprovalChannelForCurrentChat(bridge, projectName, currentChatTitle);
295
344
  const targetChannelId = targetChannel ? targetChannel.id : '';
@@ -311,17 +360,47 @@ function ensureApprovalDetector(bridge, cdp, projectName, accountName = 'default
311
360
  return;
312
361
  }
313
362
  }
363
+ const extraFields = [
364
+ { name: (0, i18n_1.t)('Allow button'), value: info.approveText || (0, i18n_1.t)('(None)'), inline: true },
365
+ ];
366
+ if (info.alwaysAllowText) {
367
+ extraFields.push({ name: (0, i18n_1.t)('Allow Chat button'), value: info.alwaysAllowText, inline: true });
368
+ }
369
+ extraFields.push({ name: (0, i18n_1.t)('Deny button'), value: info.denyText || (0, i18n_1.t)('(None)'), inline: true });
370
+ // Resolve artifact review buttons if they exist
371
+ let walkthroughCustomId;
372
+ let taskCustomId;
373
+ if (bridge.chatSessionRepo && bridge.artifactService) {
374
+ const session = bridge.chatSessionRepo.findByChannelId(targetChannelId);
375
+ const activeConversationId = session?.conversationId;
376
+ if (activeConversationId) {
377
+ const artifacts = bridge.artifactService.listArtifacts(activeConversationId);
378
+ const walkthrough = artifacts.find((art) => art.filename.toLowerCase() === 'walkthrough.md');
379
+ if (walkthrough) {
380
+ walkthroughCustomId = `file_open:art:${activeConversationId}:walkthrough.md`;
381
+ }
382
+ const task = artifacts.find((art) => art.filename.toLowerCase() === 'task.md');
383
+ if (task) {
384
+ taskCustomId = `file_open:art:${activeConversationId}:task.md`;
385
+ }
386
+ }
387
+ }
314
388
  const payload = (0, notificationSender_1.buildApprovalNotification)({
315
389
  title: (0, i18n_1.t)('Approval Required'),
316
390
  description: info.description || (0, i18n_1.t)('Antigravity is requesting approval for an action'),
317
391
  projectName,
318
392
  channelId: targetChannelId,
319
- extraFields: [
320
- { name: (0, i18n_1.t)('Allow button'), value: info.approveText, inline: true },
321
- { name: (0, i18n_1.t)('Allow Chat button'), value: info.alwaysAllowText || (0, i18n_1.t)('In Dropdown'), inline: true },
322
- { name: (0, i18n_1.t)('Deny button'), value: info.denyText || (0, i18n_1.t)('(None)'), inline: true },
323
- ],
393
+ extraFields,
394
+ hasAlwaysAllow: !!info.alwaysAllowText,
395
+ alwaysAllowText: info.alwaysAllowText,
396
+ walkthroughCustomId,
397
+ taskCustomId,
324
398
  });
399
+ if (lastNotification) {
400
+ lastNotification.payload = payload;
401
+ lastNotification.sent.edit(payload).catch(logger_1.logger.error);
402
+ return;
403
+ }
325
404
  const sent = await targetChannel.send(payload).catch((err) => {
326
405
  logger_1.logger.error(err);
327
406
  return null;
@@ -381,7 +460,15 @@ function ensurePlanningDetector(bridge, cdp, projectName, accountName = 'default
381
460
  projectName,
382
461
  channelId: targetChannelId,
383
462
  extraFields,
463
+ hasOpenButton: info.hasOpenButton,
464
+ openText: info.openText,
465
+ proceedText: info.proceedText,
384
466
  });
467
+ if (lastNotification) {
468
+ lastNotification.payload = payload;
469
+ lastNotification.sent.edit(payload).catch(logger_1.logger.error);
470
+ return;
471
+ }
385
472
  const sent = await targetChannel.send(payload).catch((err) => {
386
473
  logger_1.logger.error(err);
387
474
  return null;
@@ -438,12 +525,23 @@ function ensureErrorPopupDetector(bridge, cdp, projectName, accountName = 'defau
438
525
  { name: (0, i18n_1.t)('Workspace'), value: projectName, inline: true },
439
526
  ],
440
527
  });
528
+ if (lastNotification && detector.lastDetectedKey && lastNotification.key === detector.lastDetectedKey) {
529
+ // Cooldown triggered for the same popup; edit existing message instead of sending new
530
+ const edited = await lastNotification.sent.edit(payload).catch((err) => {
531
+ logger_1.logger.error(err);
532
+ return null;
533
+ });
534
+ if (edited) {
535
+ lastNotification.payload = payload;
536
+ }
537
+ return;
538
+ }
441
539
  const sent = await targetChannel.send(payload).catch((err) => {
442
540
  logger_1.logger.error(err);
443
541
  return null;
444
542
  });
445
543
  if (sent) {
446
- lastNotification = { sent, payload };
544
+ lastNotification = { sent, payload, key: detector.lastDetectedKey };
447
545
  }
448
546
  },
449
547
  });
@@ -549,3 +647,46 @@ function ensureUserMessageDetector(bridge, cdp, projectName, onUserMessage, acco
549
647
  bridge.pool.registerUserMessageDetector(projectName, detector, accountName);
550
648
  logger_1.logger.debug(`[UserMessageDetector:${projectName}] Started user message detection`);
551
649
  }
650
+ function ensureQuestionDetector(bridge, cdp, projectName, accountName = 'default') {
651
+ const existing = bridge.pool.getQuestionDetector(projectName, accountName);
652
+ if (existing && existing.isActive)
653
+ return;
654
+ let lastNotification = null;
655
+ const detector = new questionDetector_1.QuestionDetector({
656
+ cdpService: cdp,
657
+ pollIntervalMs: 2000,
658
+ onResolved: () => {
659
+ if (!lastNotification)
660
+ return;
661
+ const { sent, payload } = lastNotification;
662
+ lastNotification = null;
663
+ const resolved = (0, notificationSender_1.buildResolvedOverlay)(payload, (0, i18n_1.t)('Question answered'));
664
+ sent.edit(resolved).catch(logger_1.logger.error);
665
+ },
666
+ onQuestionRequired: async (info) => {
667
+ const currentChatTitle = await getCurrentChatTitle(cdp);
668
+ const targetChannel = resolveApprovalChannelForCurrentChat(bridge, projectName, currentChatTitle);
669
+ const targetChannelId = targetChannel ? targetChannel.id : '';
670
+ if (!targetChannel || !targetChannelId)
671
+ return;
672
+ const payload = (0, notificationSender_1.buildQuestionNotification)({
673
+ title: info.title,
674
+ description: info.description,
675
+ projectName,
676
+ channelId: targetChannelId,
677
+ options: info.options,
678
+ });
679
+ const sent = await targetChannel.send(payload).catch((err) => {
680
+ logger_1.logger.error(err);
681
+ return null;
682
+ });
683
+ if (sent) {
684
+ lastNotification = { sent, payload };
685
+ }
686
+ },
687
+ });
688
+ detector.setProjectName(projectName);
689
+ detector.start();
690
+ bridge.pool.registerQuestionDetector(projectName, detector, accountName);
691
+ logger_1.logger.debug(`[QuestionDetector:${projectName}] Started question detection`);
692
+ }
@@ -11,6 +11,7 @@ function buildConnectionKey(projectName, accountName) {
11
11
  * Pool that manages independent CdpService instances per workspace/account pair.
12
12
  */
13
13
  class CdpConnectionPool {
14
+ lastActiveWorkspace = null;
14
15
  connections = new Map();
15
16
  workspaceToAccount = new Map();
16
17
  approvalDetectors = new Map();
@@ -18,6 +19,7 @@ class CdpConnectionPool {
18
19
  planningDetectors = new Map();
19
20
  runCommandDetectors = new Map();
20
21
  userMessageDetectors = new Map();
22
+ questionDetectors = new Map();
21
23
  connectingPromises = new Map();
22
24
  cdpOptions;
23
25
  constructor(cdpOptions = {}) {
@@ -39,6 +41,7 @@ class CdpConnectionPool {
39
41
  const key = buildConnectionKey(projectName, effectiveAccount);
40
42
  const existing = this.connections.get(key);
41
43
  if (existing && existing.isConnected()) {
44
+ this.lastActiveWorkspace = projectName;
42
45
  await existing.discoverAndConnectForWorkspace(workspacePath);
43
46
  return existing;
44
47
  }
@@ -49,7 +52,9 @@ class CdpConnectionPool {
49
52
  const connectPromise = this.createAndConnect(workspacePath, projectName, effectiveAccount);
50
53
  this.connectingPromises.set(key, connectPromise);
51
54
  try {
52
- return await connectPromise;
55
+ const result = await connectPromise;
56
+ this.lastActiveWorkspace = projectName;
57
+ return result;
53
58
  }
54
59
  finally {
55
60
  this.connectingPromises.delete(key);
@@ -58,8 +63,9 @@ class CdpConnectionPool {
58
63
  }
59
64
  getConnected(projectName, accountName = 'default') {
60
65
  const effectiveAccount = this.resolveAccountName(projectName, accountName);
61
- const cdp = this.connections.get(buildConnectionKey(projectName, effectiveAccount));
66
+ const cdp = this.connections.get(buildConnectionKey(projectName, effectiveAccount)) || null;
62
67
  if (cdp && cdp.isConnected()) {
68
+ this.lastActiveWorkspace = projectName;
63
69
  return cdp;
64
70
  }
65
71
  return null;
@@ -78,6 +84,8 @@ class CdpConnectionPool {
78
84
  this.approvalDetectors.delete(key);
79
85
  this.errorPopupDetectors.get(key)?.stop();
80
86
  this.errorPopupDetectors.delete(key);
87
+ this.questionDetectors.get(key)?.stop();
88
+ this.questionDetectors.delete(key);
81
89
  this.planningDetectors.get(key)?.stop();
82
90
  this.planningDetectors.delete(key);
83
91
  this.runCommandDetectors.get(key)?.stop();
@@ -117,6 +125,16 @@ class CdpConnectionPool {
117
125
  this.planningDetectors.get(key)?.stop();
118
126
  this.planningDetectors.set(key, detector);
119
127
  }
128
+ getQuestionDetector(projectName, accountName = 'default') {
129
+ const effectiveAccount = this.resolveAccountName(projectName, accountName);
130
+ return this.questionDetectors.get(buildConnectionKey(projectName, effectiveAccount));
131
+ }
132
+ registerQuestionDetector(projectName, detector, accountName = 'default') {
133
+ const effectiveAccount = this.resolveAccountName(projectName, accountName);
134
+ const key = buildConnectionKey(projectName, effectiveAccount);
135
+ this.questionDetectors.get(key)?.stop();
136
+ this.questionDetectors.set(key, detector);
137
+ }
120
138
  getPlanningDetector(projectName, accountName = 'default') {
121
139
  const effectiveAccount = this.resolveAccountName(projectName, accountName);
122
140
  return this.planningDetectors.get(buildConnectionKey(projectName, effectiveAccount));
@@ -182,6 +200,8 @@ class CdpConnectionPool {
182
200
  this.errorPopupDetectors.delete(key);
183
201
  this.planningDetectors.get(key)?.stop();
184
202
  this.planningDetectors.delete(key);
203
+ this.questionDetectors.get(key)?.stop();
204
+ this.questionDetectors.delete(key);
185
205
  this.runCommandDetectors.get(key)?.stop();
186
206
  this.runCommandDetectors.delete(key);
187
207
  this.userMessageDetectors.get(key)?.stop();