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
@@ -6,6 +6,7 @@
6
6
  * They return a `MessagePayload` that any platform adapter can render.
7
7
  */
8
8
  Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.QUESTION_SKIP_ACTION_PREFIX = exports.QUESTION_SELECT_ACTION_PREFIX = exports.FEEDBACK_ACTION_PREFIX = void 0;
9
10
  exports.buildApprovalNotification = buildApprovalNotification;
10
11
  exports.buildPlanningNotification = buildPlanningNotification;
11
12
  exports.buildErrorPopupNotification = buildErrorPopupNotification;
@@ -13,6 +14,7 @@ exports.buildRunCommandNotification = buildRunCommandNotification;
13
14
  exports.buildAutoApprovedNotification = buildAutoApprovedNotification;
14
15
  exports.buildResolvedOverlay = buildResolvedOverlay;
15
16
  exports.buildStatusNotification = buildStatusNotification;
17
+ exports.buildQuestionNotification = buildQuestionNotification;
16
18
  exports.buildProgressNotification = buildProgressNotification;
17
19
  const richContentBuilder_1 = require("../platform/richContentBuilder");
18
20
  // ---------------------------------------------------------------------------
@@ -28,6 +30,9 @@ const ERROR_POPUP_COPY_DEBUG_ACTION_PREFIX = 'error_popup_copy_debug_action';
28
30
  const ERROR_POPUP_RETRY_ACTION_PREFIX = 'error_popup_retry_action';
29
31
  const RUN_COMMAND_RUN_ACTION_PREFIX = 'run_command_run_action';
30
32
  const RUN_COMMAND_REJECT_ACTION_PREFIX = 'run_command_reject_action';
33
+ exports.FEEDBACK_ACTION_PREFIX = 'feedback_action';
34
+ exports.QUESTION_SELECT_ACTION_PREFIX = 'question_select_action';
35
+ exports.QUESTION_SKIP_ACTION_PREFIX = 'question_skip_action';
31
36
  // ---------------------------------------------------------------------------
32
37
  // Notification colours
33
38
  // ---------------------------------------------------------------------------
@@ -77,15 +82,32 @@ function customId(prefix, projectName, channelId) {
77
82
  // ---------------------------------------------------------------------------
78
83
  /** Build the approval notification message. */
79
84
  function buildApprovalNotification(opts) {
80
- const { title, description, projectName, channelId, toolNames, extraFields } = opts;
85
+ const { title, description, projectName, channelId, toolNames, extraFields, hasAlwaysAllow, alwaysAllowText, walkthroughCustomId, taskCustomId } = opts;
81
86
  const richContent = (0, richContentBuilder_1.pipe)((0, richContentBuilder_1.createRichContent)(), (rc) => (0, richContentBuilder_1.withTitle)(rc, title), (rc) => (0, richContentBuilder_1.withDescription)(rc, description), (rc) => (0, richContentBuilder_1.withColor)(rc, COLOR_APPROVAL), (rc) => (0, richContentBuilder_1.addField)(rc, 'Project', projectName, true), (rc) => toolNames && toolNames.length > 0
82
87
  ? (0, richContentBuilder_1.addField)(rc, 'Tools', toolNames.join(', '), true)
83
88
  : rc, (rc) => extraFields
84
89
  ? extraFields.reduce((acc, f) => (0, richContentBuilder_1.addField)(acc, f.name, f.value, f.inline), rc)
85
90
  : rc, (rc) => (0, richContentBuilder_1.withFooter)(rc, 'Approval required'), (rc) => (0, richContentBuilder_1.withTimestamp)(rc));
91
+ const buttons = [
92
+ button(customId(APPROVE_ACTION_PREFIX, projectName, channelId), 'Allow', 'success'),
93
+ ];
94
+ if (hasAlwaysAllow) {
95
+ buttons.push(button(customId(ALWAYS_ALLOW_ACTION_PREFIX, projectName, channelId), alwaysAllowText || 'Allow Chat', 'primary'));
96
+ }
97
+ buttons.push(button(customId(DENY_ACTION_PREFIX, projectName, channelId), 'Deny', 'danger'));
86
98
  const components = [
87
- buttonRow(button(customId(APPROVE_ACTION_PREFIX, projectName, channelId), 'Allow', 'success'), button(customId(ALWAYS_ALLOW_ACTION_PREFIX, projectName, channelId), 'Allow Chat', 'primary'), button(customId(DENY_ACTION_PREFIX, projectName, channelId), 'Deny', 'danger')),
99
+ buttonRow(...buttons),
88
100
  ];
101
+ const artifactButtons = [];
102
+ if (walkthroughCustomId) {
103
+ artifactButtons.push(button(walkthroughCustomId, 'Review walkthrough.md', 'success'));
104
+ }
105
+ if (taskCustomId) {
106
+ artifactButtons.push(button(taskCustomId, 'Review task.md', 'primary'));
107
+ }
108
+ if (artifactButtons.length > 0) {
109
+ components.push(buttonRow(...artifactButtons));
110
+ }
89
111
  return { richContent, components };
90
112
  }
91
113
  /** Build the planning mode notification message. */
@@ -94,8 +116,15 @@ function buildPlanningNotification(opts) {
94
116
  const richContent = (0, richContentBuilder_1.pipe)((0, richContentBuilder_1.createRichContent)(), (rc) => (0, richContentBuilder_1.withTitle)(rc, title), (rc) => (0, richContentBuilder_1.withDescription)(rc, description), (rc) => (0, richContentBuilder_1.withColor)(rc, COLOR_PLANNING), (rc) => extraFields
95
117
  ? extraFields.reduce((acc, f) => (0, richContentBuilder_1.addField)(acc, f.name, f.value, f.inline), rc)
96
118
  : rc, (rc) => (0, richContentBuilder_1.withFooter)(rc, 'Planning mode detected'), (rc) => (0, richContentBuilder_1.withTimestamp)(rc));
119
+ const buttons = [];
120
+ if (opts.hasOpenButton !== false) {
121
+ const openLabel = opts.openText || 'Open Plan';
122
+ buttons.push(button(customId(PLANNING_OPEN_ACTION_PREFIX, projectName, channelId), openLabel, 'primary'));
123
+ }
124
+ const buttonLabel = opts.proceedText || 'Proceed';
125
+ buttons.push(button(customId(PLANNING_PROCEED_ACTION_PREFIX, projectName, channelId), buttonLabel, 'success'));
97
126
  const components = [
98
- buttonRow(button(customId(PLANNING_OPEN_ACTION_PREFIX, projectName, channelId), 'Open', 'primary'), button(customId(PLANNING_PROCEED_ACTION_PREFIX, projectName, channelId), 'Proceed', 'success')),
127
+ buttonRow(...buttons),
99
128
  ];
100
129
  return { richContent, components };
101
130
  }
@@ -158,6 +187,54 @@ function buildStatusNotification(opts) {
158
187
  : rc);
159
188
  return { richContent };
160
189
  }
190
+ function buildQuestionNotification(params) {
191
+ const { title, description, projectName, channelId, options } = params;
192
+ const baseContent = (0, richContentBuilder_1.pipe)((0, richContentBuilder_1.createRichContent)(), (rc) => (0, richContentBuilder_1.withTitle)(rc, title), (rc) => (0, richContentBuilder_1.withColor)(rc, COLOR_APPROVAL));
193
+ const embed = description ? (0, richContentBuilder_1.withDescription)(baseContent, description) : baseContent;
194
+ const selectMenuOptions = options.map((opt, i) => ({
195
+ label: opt.text.substring(0, 100), // Discord max length for label is 100
196
+ value: i.toString(),
197
+ })).slice(0, 25); // Discord max options in a select menu is 25
198
+ // Ensure customIds fit within 100 chars without losing channelId
199
+ const encodeSafe = (str, maxLen) => {
200
+ let encoded = encodeURIComponent(str);
201
+ if (encoded.length <= maxLen)
202
+ return encoded;
203
+ return encoded.substring(0, maxLen).replace(/(%[0-9A-F]?)$/i, '');
204
+ };
205
+ const encodedChannel = encodeURIComponent(channelId);
206
+ const selectOverhead = exports.QUESTION_SELECT_ACTION_PREFIX.length + 2 + encodedChannel.length;
207
+ const safeSelectProjectName = encodeSafe(projectName, 100 - selectOverhead);
208
+ const encodedCustomId = `${exports.QUESTION_SELECT_ACTION_PREFIX}:${safeSelectProjectName}:${encodedChannel}`;
209
+ const skipOverhead = exports.QUESTION_SKIP_ACTION_PREFIX.length + 2 + encodedChannel.length;
210
+ const safeSkipProjectName = encodeSafe(projectName, 100 - skipOverhead);
211
+ const encodedSkipCustomId = `${exports.QUESTION_SKIP_ACTION_PREFIX}:${safeSkipProjectName}:${encodedChannel}`;
212
+ return {
213
+ richContent: embed,
214
+ components: [
215
+ {
216
+ components: [
217
+ {
218
+ type: 'selectMenu',
219
+ customId: encodedCustomId,
220
+ placeholder: 'Select an option...',
221
+ options: selectMenuOptions,
222
+ },
223
+ ],
224
+ },
225
+ {
226
+ components: [
227
+ {
228
+ type: 'button',
229
+ customId: encodedSkipCustomId,
230
+ label: 'Skip',
231
+ style: 'secondary',
232
+ },
233
+ ],
234
+ },
235
+ ],
236
+ };
237
+ }
161
238
  /** Build a progress / phase notification (e.g. "Thinking...", "Generating..."). */
162
239
  function buildProgressNotification(opts) {
163
240
  const { phase, projectName, detail } = opts;
@@ -10,33 +10,59 @@ const approvalDetector_1 = require("./approvalDetector");
10
10
  * and extracts plan metadata from the surrounding DOM elements.
11
11
  */
12
12
  const DETECT_PLANNING_SCRIPT = `(() => {
13
- const OPEN_PATTERNS = ['open'];
13
+ const OPEN_PATTERNS = ['open', 'review'];
14
14
  const PROCEED_PATTERNS = ['proceed'];
15
+ const REJECT_PATTERNS = ['reject'];
16
+ const ACCEPT_PATTERNS = ['accept'];
15
17
 
16
18
  const normalize = (text) => (text || '').toLowerCase().replace(/\\s+/g, ' ').trim();
17
19
 
18
- // Find the notify container that holds planning UI
19
- const container = document.querySelector('.notify-user-container');
20
- if (!container) return null;
20
+ const allButtons = Array.from(document.querySelectorAll('button, a'))
21
+ .filter(btn => btn.offsetParent !== null)
22
+ .reverse();
21
23
 
22
- const allButtons = Array.from(container.querySelectorAll('button'))
23
- .filter(btn => btn.offsetParent !== null);
24
+ let proceedBtn = null;
25
+ let openBtn = null;
26
+ let rejectBtn = null;
24
27
 
25
- const openBtn = allButtons.find(btn => {
28
+ for (const btn of allButtons) {
26
29
  const t = normalize(btn.textContent || '');
27
- return OPEN_PATTERNS.some(p => t === p || t.includes(p));
28
- }) || null;
30
+ if (t.includes('review changes')) continue; // Ignore file change review button
29
31
 
30
- const proceedBtn = allButtons.find(btn => {
31
- const t = normalize(btn.textContent || '');
32
- return PROCEED_PATTERNS.some(p => t === p || t.includes(p));
33
- }) || null;
32
+ // If we hit an accept or reject button before finding a proceed button,
33
+ // it means the most recent state is an Approval, not Planning.
34
+ if (!proceedBtn && (ACCEPT_PATTERNS.some(p => t === p || t.includes(p)) || REJECT_PATTERNS.some(p => t === p || t.includes(p)))) {
35
+ return null;
36
+ }
37
+
38
+ if (!proceedBtn && PROCEED_PATTERNS.some(p => t === p || t.includes(p))) {
39
+ proceedBtn = btn;
40
+ continue;
41
+ }
42
+
43
+ if (!openBtn && OPEN_PATTERNS.some(p => t === p || t.includes(p))) {
44
+ openBtn = btn;
45
+ }
46
+
47
+ if (!rejectBtn && REJECT_PATTERNS.some(p => t === p || t.includes(p))) {
48
+ rejectBtn = btn;
49
+ }
50
+
51
+ if (proceedBtn && openBtn) break;
52
+ }
34
53
 
35
- // Both buttons must exist for this to be a planning UI
36
- if (!openBtn || !proceedBtn) return null;
54
+ // Proceed button must exist for this to be a planning UI
55
+ if (!proceedBtn) return null;
56
+
57
+ const container = proceedBtn.closest('.notify-user-container') || proceedBtn.closest('div[class*="rounded-lg"]') || proceedBtn.parentElement?.parentElement;
58
+ if (!container) return null;
37
59
 
38
- const openText = (openBtn.textContent || '').trim();
60
+ // openBtn was already extracted in the loop above
61
+
62
+ const hasOpenButton = openBtn !== null;
63
+ const openText = openBtn ? (openBtn.textContent || '').trim() : 'Open';
39
64
  const proceedText = (proceedBtn.textContent || '').trim();
65
+ const rejectText = rejectBtn ? (rejectBtn.textContent || '').trim() : '';
40
66
 
41
67
  // Extract plan title from .inline-flex.break-all
42
68
  const titleEl = container.querySelector('span.inline-flex.break-all, .inline-flex.break-all');
@@ -67,7 +93,7 @@ const DETECT_PLANNING_SCRIPT = `(() => {
67
93
  description = parts.join(' ').slice(0, 500);
68
94
  }
69
95
 
70
- return { openText, proceedText, planTitle, planSummary, description };
96
+ return { openText, proceedText, planTitle, planSummary, description, rejectText, hasOpenButton };
71
97
  })()`;
72
98
  /**
73
99
  * Extract plan content displayed after clicking Open.
@@ -158,8 +184,12 @@ class PlanningDetector {
158
184
  lastDetectedInfo = null;
159
185
  /** Timestamp of last notification (for cooldown-based dedup) */
160
186
  lastNotifiedAt = 0;
187
+ /** Number of consecutive polls without detecting buttons */
188
+ emptyPollCount = 0;
161
189
  /** Cooldown period in ms to suppress duplicate notifications */
162
190
  static COOLDOWN_MS = 5000;
191
+ /** Number of consecutive empty polls required before resetting state */
192
+ static REQUIRED_EMPTY_POLLS = 3;
163
193
  constructor(options) {
164
194
  this.cdpService = options.cdpService;
165
195
  this.pollIntervalMs = options.pollIntervalMs ?? 2000;
@@ -174,6 +204,7 @@ class PlanningDetector {
174
204
  this.lastDetectedKey = null;
175
205
  this.lastDetectedInfo = null;
176
206
  this.lastNotifiedAt = 0;
207
+ this.emptyPollCount = 0;
177
208
  this.schedulePoll();
178
209
  }
179
210
  /** Stop monitoring. */
@@ -210,6 +241,15 @@ class PlanningDetector {
210
241
  const text = buttonText ?? this.lastDetectedInfo?.proceedText ?? 'Proceed';
211
242
  return this.clickButton(text);
212
243
  }
244
+ /**
245
+ * Click the Reject button via CDP.
246
+ * @param buttonText Text of the button to click (default: detected rejectText or "Reject All")
247
+ * @returns true if click succeeded
248
+ */
249
+ async clickRejectButton(buttonText) {
250
+ const text = buttonText || this.lastDetectedInfo?.rejectText || 'Reject All';
251
+ return this.clickButton(text);
252
+ }
213
253
  /**
214
254
  * Extract plan content from the DOM after Open has been clicked.
215
255
  * @returns Plan content text or null if not found
@@ -255,8 +295,9 @@ class PlanningDetector {
255
295
  const result = await this.cdpService.call('Runtime.evaluate', callParams);
256
296
  const info = result?.result?.value ?? null;
257
297
  if (info) {
258
- // Duplicate prevention: use button text pair as key (stable across DOM redraws)
259
- const key = `${info.openText}::${info.proceedText}`;
298
+ this.emptyPollCount = 0;
299
+ // Duplicate prevention: use plan title and content as key (stable across DOM redraws and button text updates)
300
+ const key = `${info.planTitle}::${info.planSummary}::${(info.description || '').substring(0, 50)}`;
260
301
  const now = Date.now();
261
302
  const withinCooldown = (now - this.lastNotifiedAt) < PlanningDetector.COOLDOWN_MS;
262
303
  if (key !== this.lastDetectedKey && !withinCooldown) {
@@ -271,12 +312,15 @@ class PlanningDetector {
271
312
  }
272
313
  }
273
314
  else {
274
- // Reset when buttons disappear (prepare for next planning detection)
275
- const wasDetected = this.lastDetectedKey !== null;
276
- this.lastDetectedKey = null;
277
- this.lastDetectedInfo = null;
278
- if (wasDetected && this.onResolved) {
279
- this.onResolved();
315
+ this.emptyPollCount++;
316
+ if (this.emptyPollCount >= PlanningDetector.REQUIRED_EMPTY_POLLS) {
317
+ // Reset when buttons disappear for consecutive polls
318
+ const wasDetected = this.lastDetectedKey !== null;
319
+ this.lastDetectedKey = null;
320
+ this.lastDetectedInfo = null;
321
+ if (wasDetected && this.onResolved) {
322
+ this.onResolved();
323
+ }
280
324
  }
281
325
  }
282
326
  }
@@ -1,17 +1,43 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PromptDispatcher = void 0;
4
+ const logger_1 = require("../utils/logger");
4
5
  /**
5
6
  * Dispatcher that calls the existing sendPromptToAntigravity.
6
7
  * Unifies dependency injection on the caller side and simplifies event handlers.
7
8
  */
8
9
  class PromptDispatcher {
9
10
  deps;
11
+ activeMonitors = new Map();
10
12
  constructor(deps) {
11
13
  this.deps = deps;
12
14
  }
13
15
  async send(req) {
14
- await this.deps.sendPromptImpl(this.deps.bridge, req.message, req.prompt, req.cdp, this.deps.modeService, this.deps.modelService, req.inboundImages ?? [], req.options);
16
+ await this._dispatch(req, req.options);
17
+ }
18
+ async resume(req) {
19
+ await this._dispatch(req, { ...req.options, resumeOnly: true });
20
+ }
21
+ async _dispatch(req, options) {
22
+ const channelId = req.message.channelId;
23
+ const existing = this.activeMonitors.get(channelId);
24
+ if (existing) {
25
+ logger_1.logger.info(`[PromptDispatcher] Aborting previous active monitor for channel ${channelId}`);
26
+ await existing.stop().catch((err) => logger_1.logger.error('[PromptDispatcher] Error stopping monitor:', err));
27
+ this.activeMonitors.delete(channelId);
28
+ }
29
+ const wrappedOptions = {
30
+ ...(options || {}),
31
+ onMonitorCreated: (monitor) => {
32
+ this.activeMonitors.set(channelId, monitor);
33
+ options?.onMonitorCreated?.(monitor);
34
+ },
35
+ onFullCompletion: () => {
36
+ this.activeMonitors.delete(channelId);
37
+ options?.onFullCompletion?.();
38
+ }
39
+ };
40
+ await this.deps.sendPromptImpl(this.deps.bridge, req.message, req.prompt, req.cdp, this.deps.modeService, this.deps.modelService, req.inboundImages ?? [], wrappedOptions);
15
41
  }
16
42
  }
17
43
  exports.PromptDispatcher = PromptDispatcher;