lazy-gravity 0.9.1 → 0.10.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 (39) hide show
  1. package/dist/bot/index.js +233 -41
  2. package/dist/bot/telegramJoinCommand.js +9 -0
  3. package/dist/bot/telegramMessageHandler.js +7 -0
  4. package/dist/commands/chatCommandHandler.js +71 -1
  5. package/dist/commands/registerSlashCommands.js +13 -1
  6. package/dist/events/interactionCreateHandler.js +324 -26
  7. package/dist/events/messageCreateHandler.js +53 -4
  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 +69 -0
  12. package/dist/handlers/genericActionButtonAction.js +74 -0
  13. package/dist/handlers/planningButtonAction.js +24 -9
  14. package/dist/handlers/questionSelectAction.js +69 -0
  15. package/dist/handlers/questionSkipAction.js +67 -0
  16. package/dist/handlers/runCommandButtonAction.js +2 -1
  17. package/dist/platform/discord/discordResponseRenderer.js +164 -0
  18. package/dist/services/approvalDetector.js +70 -24
  19. package/dist/services/artifactService.js +57 -31
  20. package/dist/services/assistantDomExtractor.js +174 -3
  21. package/dist/services/cdpBridgeManager.js +114 -6
  22. package/dist/services/cdpConnectionPool.js +15 -0
  23. package/dist/services/cdpService.js +21 -7
  24. package/dist/services/chatSessionService.js +148 -27
  25. package/dist/services/errorPopupDetector.js +23 -11
  26. package/dist/services/notificationSender.js +70 -3
  27. package/dist/services/planningDetector.js +69 -25
  28. package/dist/services/promptDispatcher.js +7 -1
  29. package/dist/services/questionDetector.js +376 -0
  30. package/dist/services/responseMonitor.js +28 -2
  31. package/dist/services/runCommandDetector.js +14 -7
  32. package/dist/ui/artifactsUi.js +4 -3
  33. package/dist/utils/consecutiveEmptyPollGate.js +21 -0
  34. package/dist/utils/fileOpenCache.js +33 -0
  35. package/dist/utils/htmlToDiscordMarkdown.js +5 -2
  36. package/dist/utils/pathUtils.js +12 -0
  37. package/dist/utils/projectResolver.js +10 -0
  38. package/dist/utils/questionActionUtils.js +25 -0
  39. package/package.json +2 -2
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ErrorPopupDetector = void 0;
4
+ const consecutiveEmptyPollGate_1 = require("../utils/consecutiveEmptyPollGate");
4
5
  const logger_1 = require("../utils/logger");
5
6
  const approvalDetector_1 = require("./approvalDetector");
6
7
  /**
@@ -107,8 +108,10 @@ class ErrorPopupDetector {
107
108
  lastDetectedInfo = null;
108
109
  /** Timestamp of last notification (for cooldown-based dedup) */
109
110
  lastNotifiedAt = 0;
110
- /** Cooldown period in ms to suppress duplicate notifications (10s for error popups) */
111
- static COOLDOWN_MS = 10000;
111
+ /** Cooldown period in ms to suppress duplicate notifications */
112
+ static COOLDOWN_MS = 5000;
113
+ /** Gate for empty polls before reset */
114
+ emptyPollGate = new consecutiveEmptyPollGate_1.ConsecutiveEmptyPollGate(3);
112
115
  constructor(options) {
113
116
  this.cdpService = options.cdpService;
114
117
  this.pollIntervalMs = options.pollIntervalMs ?? 3000;
@@ -123,6 +126,7 @@ class ErrorPopupDetector {
123
126
  this.lastDetectedKey = null;
124
127
  this.lastDetectedInfo = null;
125
128
  this.lastNotifiedAt = 0;
129
+ this.emptyPollGate.reset();
126
130
  this.schedulePoll();
127
131
  }
128
132
  /** Stop monitoring. */
@@ -208,28 +212,36 @@ class ErrorPopupDetector {
208
212
  const result = await this.cdpService.call('Runtime.evaluate', callParams);
209
213
  const info = result?.result?.value ?? null;
210
214
  if (info) {
215
+ this.emptyPollGate.recordDetection();
211
216
  // Duplicate prevention: use title + body snippet as key
212
217
  const key = `${info.title}::${info.body.slice(0, 100)}`;
213
218
  const now = Date.now();
214
219
  const withinCooldown = (now - this.lastNotifiedAt) < ErrorPopupDetector.COOLDOWN_MS;
215
- if (key !== this.lastDetectedKey && !withinCooldown) {
220
+ if (key !== this.lastDetectedKey) {
221
+ this.lastNotifiedAt = now;
216
222
  this.lastDetectedKey = key;
217
223
  this.lastDetectedInfo = info;
224
+ this.onErrorPopup(info);
225
+ }
226
+ else if (!withinCooldown) {
218
227
  this.lastNotifiedAt = now;
228
+ this.lastDetectedInfo = info;
219
229
  this.onErrorPopup(info);
220
230
  }
221
- else if (key === this.lastDetectedKey) {
222
- // Same key -- update stored info silently
231
+ else {
232
+ // Same key, within cooldown -- update stored info silently
223
233
  this.lastDetectedInfo = info;
224
234
  }
225
235
  }
226
236
  else {
227
- // Reset when popup disappears (prepare for next detection)
228
- const wasDetected = this.lastDetectedKey !== null;
229
- this.lastDetectedKey = null;
230
- this.lastDetectedInfo = null;
231
- if (wasDetected && this.onResolved) {
232
- this.onResolved();
237
+ if (this.emptyPollGate.recordEmptyPoll()) {
238
+ // Reset when popup disappears (prepare for next detection)
239
+ const wasDetected = this.lastDetectedKey !== null;
240
+ this.lastDetectedKey = null;
241
+ this.lastDetectedInfo = null;
242
+ if (wasDetected && this.onResolved) {
243
+ this.onResolved();
244
+ }
233
245
  }
234
246
  }
235
247
  }
@@ -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,14 +82,21 @@ 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 } = 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
  ];
89
101
  return { richContent, components };
90
102
  }
@@ -94,8 +106,15 @@ function buildPlanningNotification(opts) {
94
106
  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
107
  ? extraFields.reduce((acc, f) => (0, richContentBuilder_1.addField)(acc, f.name, f.value, f.inline), rc)
96
108
  : rc, (rc) => (0, richContentBuilder_1.withFooter)(rc, 'Planning mode detected'), (rc) => (0, richContentBuilder_1.withTimestamp)(rc));
109
+ const buttons = [];
110
+ const openLabel = opts.openText || 'Open';
111
+ if (opts.hasOpenButton !== false) {
112
+ buttons.push(button(customId(PLANNING_OPEN_ACTION_PREFIX, projectName, channelId), openLabel, 'primary'));
113
+ }
114
+ const buttonLabel = opts.proceedText || 'Proceed';
115
+ buttons.push(button(customId(PLANNING_PROCEED_ACTION_PREFIX, projectName, channelId), buttonLabel, 'success'));
97
116
  const components = [
98
- buttonRow(button(customId(PLANNING_OPEN_ACTION_PREFIX, projectName, channelId), 'Open', 'primary'), button(customId(PLANNING_PROCEED_ACTION_PREFIX, projectName, channelId), 'Proceed', 'success')),
117
+ buttonRow(...buttons),
99
118
  ];
100
119
  return { richContent, components };
101
120
  }
@@ -158,6 +177,54 @@ function buildStatusNotification(opts) {
158
177
  : rc);
159
178
  return { richContent };
160
179
  }
180
+ function buildQuestionNotification(params) {
181
+ const { title, description, projectName, channelId, options } = params;
182
+ 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));
183
+ const embed = description ? (0, richContentBuilder_1.withDescription)(baseContent, description) : baseContent;
184
+ const selectMenuOptions = options.map((opt, i) => ({
185
+ label: opt.text.substring(0, 100), // Discord max length for label is 100
186
+ value: i.toString(),
187
+ })).slice(0, 25); // Discord max options in a select menu is 25
188
+ // Ensure customIds fit within 100 chars without losing channelId
189
+ const encodeSafe = (str, maxLen) => {
190
+ let encoded = encodeURIComponent(str);
191
+ if (encoded.length <= maxLen)
192
+ return encoded;
193
+ return encoded.substring(0, maxLen).replace(/(%[0-9A-F]?)$/i, '');
194
+ };
195
+ const encodedChannel = encodeURIComponent(channelId);
196
+ const selectOverhead = exports.QUESTION_SELECT_ACTION_PREFIX.length + 2 + encodedChannel.length;
197
+ const safeSelectProjectName = encodeSafe(projectName, 100 - selectOverhead);
198
+ const encodedCustomId = `${exports.QUESTION_SELECT_ACTION_PREFIX}:${safeSelectProjectName}:${encodedChannel}`;
199
+ const skipOverhead = exports.QUESTION_SKIP_ACTION_PREFIX.length + 2 + encodedChannel.length;
200
+ const safeSkipProjectName = encodeSafe(projectName, 100 - skipOverhead);
201
+ const encodedSkipCustomId = `${exports.QUESTION_SKIP_ACTION_PREFIX}:${safeSkipProjectName}:${encodedChannel}`;
202
+ return {
203
+ richContent: embed,
204
+ components: [
205
+ {
206
+ components: [
207
+ {
208
+ type: 'selectMenu',
209
+ customId: encodedCustomId,
210
+ placeholder: 'Select an option...',
211
+ options: selectMenuOptions,
212
+ },
213
+ ],
214
+ },
215
+ {
216
+ components: [
217
+ {
218
+ type: 'button',
219
+ customId: encodedSkipCustomId,
220
+ label: 'Skip',
221
+ style: 'secondary',
222
+ },
223
+ ],
224
+ },
225
+ ],
226
+ };
227
+ }
161
228
  /** Build a progress / phase notification (e.g. "Thinking...", "Generating..."). */
162
229
  function buildProgressNotification(opts) {
163
230
  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
  }
@@ -11,7 +11,13 @@ class PromptDispatcher {
11
11
  this.deps = deps;
12
12
  }
13
13
  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);
14
+ await this._dispatch(req, req.options);
15
+ }
16
+ async resume(req) {
17
+ await this._dispatch(req, { ...req.options, resumeOnly: true });
18
+ }
19
+ async _dispatch(req, options) {
20
+ await this.deps.sendPromptImpl(this.deps.bridge, req.message, req.prompt, req.cdp, this.deps.modeService, this.deps.modelService, req.inboundImages ?? [], options);
15
21
  }
16
22
  }
17
23
  exports.PromptDispatcher = PromptDispatcher;