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.
- package/dist/bot/index.js +233 -41
- package/dist/bot/telegramJoinCommand.js +9 -0
- package/dist/bot/telegramMessageHandler.js +7 -0
- package/dist/commands/chatCommandHandler.js +71 -1
- package/dist/commands/registerSlashCommands.js +13 -1
- package/dist/events/interactionCreateHandler.js +324 -26
- package/dist/events/messageCreateHandler.js +53 -4
- package/dist/handlers/__tests__/fileChangeButtonAction.test.js +85 -0
- package/dist/handlers/approvalButtonAction.js +2 -1
- package/dist/handlers/errorPopupButtonAction.js +2 -1
- package/dist/handlers/fileChangeButtonAction.js +69 -0
- package/dist/handlers/genericActionButtonAction.js +74 -0
- package/dist/handlers/planningButtonAction.js +24 -9
- package/dist/handlers/questionSelectAction.js +69 -0
- package/dist/handlers/questionSkipAction.js +67 -0
- package/dist/handlers/runCommandButtonAction.js +2 -1
- package/dist/platform/discord/discordResponseRenderer.js +164 -0
- package/dist/services/approvalDetector.js +70 -24
- package/dist/services/artifactService.js +57 -31
- package/dist/services/assistantDomExtractor.js +174 -3
- package/dist/services/cdpBridgeManager.js +114 -6
- package/dist/services/cdpConnectionPool.js +15 -0
- package/dist/services/cdpService.js +21 -7
- package/dist/services/chatSessionService.js +148 -27
- package/dist/services/errorPopupDetector.js +23 -11
- package/dist/services/notificationSender.js +70 -3
- package/dist/services/planningDetector.js +69 -25
- package/dist/services/promptDispatcher.js +7 -1
- package/dist/services/questionDetector.js +376 -0
- package/dist/services/responseMonitor.js +28 -2
- package/dist/services/runCommandDetector.js +14 -7
- package/dist/ui/artifactsUi.js +4 -3
- package/dist/utils/consecutiveEmptyPollGate.js +21 -0
- package/dist/utils/fileOpenCache.js +33 -0
- package/dist/utils/htmlToDiscordMarkdown.js +5 -2
- package/dist/utils/pathUtils.js +12 -0
- package/dist/utils/projectResolver.js +10 -0
- package/dist/utils/questionActionUtils.js +25 -0
- package/package.json +2 -2
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.QuestionDetector = void 0;
|
|
4
|
+
const events_1 = require("events");
|
|
5
|
+
const logger_1 = require("../utils/logger");
|
|
6
|
+
class QuestionDetector extends events_1.EventEmitter {
|
|
7
|
+
cdp;
|
|
8
|
+
pollIntervalMs;
|
|
9
|
+
intervalId = null;
|
|
10
|
+
logger = logger_1.logger;
|
|
11
|
+
lastQuestionDetected = false;
|
|
12
|
+
emptyPollCount = 0;
|
|
13
|
+
static REQUIRED_EMPTY_POLLS = 3;
|
|
14
|
+
_isStarted = false;
|
|
15
|
+
onQuestionRequired;
|
|
16
|
+
onResolved;
|
|
17
|
+
projectName = 'unknown';
|
|
18
|
+
constructor(options) {
|
|
19
|
+
super();
|
|
20
|
+
this.cdp = options.cdpService;
|
|
21
|
+
this.pollIntervalMs = options.pollIntervalMs || 2000;
|
|
22
|
+
this.onQuestionRequired = options.onQuestionRequired;
|
|
23
|
+
this.onResolved = options.onResolved;
|
|
24
|
+
// Listen to own events to call options callbacks (for compat)
|
|
25
|
+
this.on('question', (info) => this.onQuestionRequired(info));
|
|
26
|
+
this.on('resolved', () => {
|
|
27
|
+
if (this.onResolved)
|
|
28
|
+
this.onResolved();
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
setProjectName(name) {
|
|
32
|
+
this.projectName = name;
|
|
33
|
+
}
|
|
34
|
+
get isActive() {
|
|
35
|
+
return this._isStarted;
|
|
36
|
+
}
|
|
37
|
+
start() {
|
|
38
|
+
if (this._isStarted)
|
|
39
|
+
return;
|
|
40
|
+
this._isStarted = true;
|
|
41
|
+
this.logger.debug(`[QuestionDetector:${this.projectName}] Starting polling`);
|
|
42
|
+
this.lastQuestionDetected = false;
|
|
43
|
+
this.emptyPollCount = 0;
|
|
44
|
+
this.intervalId = setInterval(() => this.poll(), this.pollIntervalMs);
|
|
45
|
+
this.poll();
|
|
46
|
+
}
|
|
47
|
+
stop() {
|
|
48
|
+
if (!this._isStarted)
|
|
49
|
+
return;
|
|
50
|
+
this._isStarted = false;
|
|
51
|
+
if (this.intervalId) {
|
|
52
|
+
clearInterval(this.intervalId);
|
|
53
|
+
this.intervalId = null;
|
|
54
|
+
}
|
|
55
|
+
this.logger.debug(`[QuestionDetector:${this.projectName}] Stopped polling`);
|
|
56
|
+
}
|
|
57
|
+
async submitOption(index) {
|
|
58
|
+
this.logger.debug(`[QuestionDetector:${this.projectName}] Submitting option ${index}`);
|
|
59
|
+
try {
|
|
60
|
+
const contextId = this.cdp.getPrimaryContextId();
|
|
61
|
+
const callParams = {
|
|
62
|
+
expression: `
|
|
63
|
+
(() => {
|
|
64
|
+
const containers = Array.from(document.querySelectorAll('div, form, dialog')).reverse();
|
|
65
|
+
let targetList = null;
|
|
66
|
+
let submitBtn = null;
|
|
67
|
+
|
|
68
|
+
for (const container of containers) {
|
|
69
|
+
const buttons = Array.from(container.querySelectorAll('button'));
|
|
70
|
+
let possibleSubmitBtn = null;
|
|
71
|
+
for (const btn of buttons) {
|
|
72
|
+
const text = btn.textContent?.toLowerCase() || '';
|
|
73
|
+
if (text.includes('submit')) {
|
|
74
|
+
possibleSubmitBtn = btn;
|
|
75
|
+
break;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (possibleSubmitBtn) {
|
|
80
|
+
const items = Array.from(container.querySelectorAll('li, label, a, [role="radio"], [role="option"], [class*="cursor-pointer"]'))
|
|
81
|
+
.filter(el => el.tagName !== 'BUTTON' && !el.closest('button'));
|
|
82
|
+
|
|
83
|
+
if (items.length > 1) {
|
|
84
|
+
targetList = container;
|
|
85
|
+
submitBtn = possibleSubmitBtn;
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (!targetList || !submitBtn) return { found: false };
|
|
92
|
+
|
|
93
|
+
const items = Array.from(targetList.querySelectorAll('li, label, a, [role="radio"], [role="option"], [class*="cursor-pointer"]'))
|
|
94
|
+
.filter(el => el.tagName !== 'BUTTON' && !el.closest('button'));
|
|
95
|
+
if (items.length <= ${index}) return { found: false };
|
|
96
|
+
|
|
97
|
+
const targetOption = items[${index}];
|
|
98
|
+
|
|
99
|
+
const optionRect = targetOption.getBoundingClientRect();
|
|
100
|
+
const btnRect = submitBtn.getBoundingClientRect();
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
found: true,
|
|
104
|
+
option: {
|
|
105
|
+
x: Math.round(optionRect.left + optionRect.width / 2),
|
|
106
|
+
y: Math.round(optionRect.top + optionRect.height / 2)
|
|
107
|
+
},
|
|
108
|
+
button: {
|
|
109
|
+
x: Math.round(btnRect.left + btnRect.width / 2),
|
|
110
|
+
y: Math.round(btnRect.top + btnRect.height / 2)
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
})()
|
|
114
|
+
`,
|
|
115
|
+
returnByValue: true,
|
|
116
|
+
awaitPromise: false,
|
|
117
|
+
};
|
|
118
|
+
if (contextId !== null) {
|
|
119
|
+
callParams.contextId = contextId;
|
|
120
|
+
}
|
|
121
|
+
const response = await this.cdp.call('Runtime.evaluate', callParams);
|
|
122
|
+
const result = response?.result?.value;
|
|
123
|
+
if (!result || !result.found) {
|
|
124
|
+
this.logger.warn(`[QuestionDetector:${this.projectName}] Could not find question modal elements during submission.`);
|
|
125
|
+
return false;
|
|
126
|
+
}
|
|
127
|
+
// Click the option
|
|
128
|
+
await this.cdp.call('Input.dispatchMouseEvent', {
|
|
129
|
+
type: 'mousePressed',
|
|
130
|
+
x: result.option.x,
|
|
131
|
+
y: result.option.y,
|
|
132
|
+
button: 'left',
|
|
133
|
+
clickCount: 1,
|
|
134
|
+
});
|
|
135
|
+
await this.cdp.call('Input.dispatchMouseEvent', {
|
|
136
|
+
type: 'mouseReleased',
|
|
137
|
+
x: result.option.x,
|
|
138
|
+
y: result.option.y,
|
|
139
|
+
button: 'left',
|
|
140
|
+
clickCount: 1,
|
|
141
|
+
});
|
|
142
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
143
|
+
// Click the submit button
|
|
144
|
+
await this.cdp.call('Input.dispatchMouseEvent', {
|
|
145
|
+
type: 'mousePressed',
|
|
146
|
+
x: result.button.x,
|
|
147
|
+
y: result.button.y,
|
|
148
|
+
button: 'left',
|
|
149
|
+
clickCount: 1,
|
|
150
|
+
});
|
|
151
|
+
await this.cdp.call('Input.dispatchMouseEvent', {
|
|
152
|
+
type: 'mouseReleased',
|
|
153
|
+
x: result.button.x,
|
|
154
|
+
y: result.button.y,
|
|
155
|
+
button: 'left',
|
|
156
|
+
clickCount: 1,
|
|
157
|
+
});
|
|
158
|
+
this.lastQuestionDetected = false;
|
|
159
|
+
return true;
|
|
160
|
+
}
|
|
161
|
+
catch (e) {
|
|
162
|
+
this.logger.error(`[QuestionDetector:${this.projectName}] submitOption error:`, e.message);
|
|
163
|
+
return false;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
async skipQuestion() {
|
|
167
|
+
this.logger.debug(`[QuestionDetector:${this.projectName}] Skipping question`);
|
|
168
|
+
try {
|
|
169
|
+
const contextId = this.cdp.getPrimaryContextId();
|
|
170
|
+
const callParams = {
|
|
171
|
+
expression: `
|
|
172
|
+
(() => {
|
|
173
|
+
const containers = Array.from(document.querySelectorAll('div, form, dialog')).reverse();
|
|
174
|
+
let skipBtn = null;
|
|
175
|
+
|
|
176
|
+
for (const container of containers) {
|
|
177
|
+
const items = Array.from(container.querySelectorAll('li, [role="radio"], [role="option"], .cursor-pointer'))
|
|
178
|
+
.filter(el => el.tagName !== 'BUTTON' && !el.closest('button'));
|
|
179
|
+
const hasList = items.length > 1;
|
|
180
|
+
|
|
181
|
+
const buttons = Array.from(container.querySelectorAll('button'));
|
|
182
|
+
for (const btn of buttons) {
|
|
183
|
+
const text = btn.textContent?.toLowerCase() || '';
|
|
184
|
+
if (text.includes('skip') && hasList) {
|
|
185
|
+
skipBtn = btn;
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
if (skipBtn) break;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (!skipBtn) return { found: false };
|
|
193
|
+
|
|
194
|
+
const btnRect = skipBtn.getBoundingClientRect();
|
|
195
|
+
return {
|
|
196
|
+
found: true,
|
|
197
|
+
button: {
|
|
198
|
+
x: Math.round(btnRect.left + btnRect.width / 2),
|
|
199
|
+
y: Math.round(btnRect.top + btnRect.height / 2)
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
})()
|
|
203
|
+
`,
|
|
204
|
+
returnByValue: true,
|
|
205
|
+
awaitPromise: false,
|
|
206
|
+
};
|
|
207
|
+
if (contextId !== null)
|
|
208
|
+
callParams.contextId = contextId;
|
|
209
|
+
const response = await this.cdp.call('Runtime.evaluate', callParams);
|
|
210
|
+
const result = response?.result?.value;
|
|
211
|
+
if (!result || !result.found) {
|
|
212
|
+
this.logger.warn(`[QuestionDetector:${this.projectName}] Could not find skip button.`);
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
await this.cdp.call('Input.dispatchMouseEvent', {
|
|
216
|
+
type: 'mousePressed',
|
|
217
|
+
x: result.button.x,
|
|
218
|
+
y: result.button.y,
|
|
219
|
+
button: 'left',
|
|
220
|
+
clickCount: 1,
|
|
221
|
+
});
|
|
222
|
+
await this.cdp.call('Input.dispatchMouseEvent', {
|
|
223
|
+
type: 'mouseReleased',
|
|
224
|
+
x: result.button.x,
|
|
225
|
+
y: result.button.y,
|
|
226
|
+
button: 'left',
|
|
227
|
+
clickCount: 1,
|
|
228
|
+
});
|
|
229
|
+
this.lastQuestionDetected = false;
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
catch (e) {
|
|
233
|
+
this.logger.error(`[QuestionDetector:${this.projectName}] skipQuestion error:`, e.message);
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
async poll() {
|
|
238
|
+
if (!this.cdp.isConnected())
|
|
239
|
+
return;
|
|
240
|
+
try {
|
|
241
|
+
const callParams = {
|
|
242
|
+
expression: `
|
|
243
|
+
(() => {
|
|
244
|
+
const containers = Array.from(document.querySelectorAll('div, form, dialog')).reverse();
|
|
245
|
+
let targetList = null;
|
|
246
|
+
let submitBtn = null;
|
|
247
|
+
|
|
248
|
+
for (const container of containers) {
|
|
249
|
+
const buttons = Array.from(container.querySelectorAll('button'));
|
|
250
|
+
let possibleSubmitBtn = null;
|
|
251
|
+
for (const btn of buttons) {
|
|
252
|
+
const text = btn.textContent?.toLowerCase() || '';
|
|
253
|
+
if (text.includes('submit')) {
|
|
254
|
+
possibleSubmitBtn = btn;
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (possibleSubmitBtn) {
|
|
260
|
+
const items = Array.from(container.querySelectorAll('li, label, a, [role="radio"], [role="option"], [class*="cursor-pointer"]'))
|
|
261
|
+
.filter(el => el.tagName !== 'BUTTON' && !el.closest('button'));
|
|
262
|
+
|
|
263
|
+
if (items.length > 1) {
|
|
264
|
+
targetList = container;
|
|
265
|
+
submitBtn = possibleSubmitBtn;
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
if (!targetList || !submitBtn) return { detected: false, reason: "No targetList or submitBtn found" };
|
|
271
|
+
|
|
272
|
+
let titleEl = targetList.querySelector('h1, h2, h3, [role="heading"], .text-lg');
|
|
273
|
+
if (!titleEl) {
|
|
274
|
+
let p = targetList;
|
|
275
|
+
while (p && p.tagName !== 'BODY') {
|
|
276
|
+
titleEl = p.querySelector('h1, h2, h3, [role="heading"], .text-lg, p');
|
|
277
|
+
if (titleEl && titleEl !== p && (titleEl.innerText || titleEl.textContent || '').trim().length > 0) break;
|
|
278
|
+
p = p.parentElement;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const title = titleEl ? (titleEl.innerText || titleEl.textContent || '').trim() : 'Question';
|
|
282
|
+
|
|
283
|
+
const items = Array.from(targetList.querySelectorAll('li, label, a, [role="radio"], [role="option"], [class*="cursor-pointer"]'))
|
|
284
|
+
.filter(el => el.tagName !== 'BUTTON' && !el.closest('button'));
|
|
285
|
+
const options = items.map(n => {
|
|
286
|
+
const rect = n.getBoundingClientRect();
|
|
287
|
+
const finalLabel = n.innerText || n.textContent || 'Option';
|
|
288
|
+
|
|
289
|
+
return {
|
|
290
|
+
text: finalLabel.replace(/\\n/g, ' ').replace(/\\s+/g, ' ').trim().substring(0, 100),
|
|
291
|
+
x: Math.round(rect.left + rect.width / 2),
|
|
292
|
+
y: Math.round(rect.top + rect.height / 2)
|
|
293
|
+
};
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
if (options.length === 0) return { detected: false, reason: "options length 0" };
|
|
297
|
+
|
|
298
|
+
return {
|
|
299
|
+
detected: true,
|
|
300
|
+
title,
|
|
301
|
+
options
|
|
302
|
+
};
|
|
303
|
+
})()
|
|
304
|
+
`,
|
|
305
|
+
returnByValue: true,
|
|
306
|
+
awaitPromise: false,
|
|
307
|
+
};
|
|
308
|
+
const contexts = this.cdp.getContexts();
|
|
309
|
+
const contextIds = [
|
|
310
|
+
this.cdp.getPrimaryContextId(),
|
|
311
|
+
...contexts.map((ctx) => ctx.id),
|
|
312
|
+
].filter((value, index, arr) => typeof value === 'number' && arr.indexOf(value) === index);
|
|
313
|
+
const targets = contextIds.length > 0 ? contextIds : [null];
|
|
314
|
+
let detectedResult = null;
|
|
315
|
+
let lastReason = null;
|
|
316
|
+
for (const contextId of targets) {
|
|
317
|
+
if (contextId !== null) {
|
|
318
|
+
callParams.contextId = contextId;
|
|
319
|
+
}
|
|
320
|
+
const response = await this.cdp.call('Runtime.evaluate', callParams).catch(e => {
|
|
321
|
+
this.logger.debug(`[QuestionDetector] Error evaluating on ctx ${contextId}: ${e.message}`);
|
|
322
|
+
return null;
|
|
323
|
+
});
|
|
324
|
+
if (response?.exceptionDetails) {
|
|
325
|
+
this.logger.debug(`[QuestionDetector] Exception on ctx ${contextId}: ${response.exceptionDetails.exception?.description || response.exceptionDetails.text}`);
|
|
326
|
+
}
|
|
327
|
+
const result = response?.result?.value;
|
|
328
|
+
if (result) {
|
|
329
|
+
this.logger.debug(`[QuestionDetector] Context ${contextId} evaluated successfully, detected: ${result.detected}, reason: ${result.reason}`);
|
|
330
|
+
}
|
|
331
|
+
if (result && result.detected) {
|
|
332
|
+
detectedResult = result;
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
else if (result && result.reason) {
|
|
336
|
+
lastReason = result.reason;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
const result = detectedResult;
|
|
340
|
+
if (result && result.detected) {
|
|
341
|
+
this.emptyPollCount = 0;
|
|
342
|
+
if (!this.lastQuestionDetected) {
|
|
343
|
+
this.lastQuestionDetected = true;
|
|
344
|
+
this.logger.debug(`[QuestionDetector:${this.projectName}] Question modal detected`);
|
|
345
|
+
this.emit('question', {
|
|
346
|
+
title: result.title || 'Question',
|
|
347
|
+
description: 'Please answer the question below.',
|
|
348
|
+
options: result.options,
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
else {
|
|
353
|
+
if (!result && lastReason) {
|
|
354
|
+
this.logger.debug(`[QuestionDetector:${this.projectName}] Evaluate returned false: ${lastReason}`);
|
|
355
|
+
}
|
|
356
|
+
this.emptyPollCount++;
|
|
357
|
+
if (this.emptyPollCount >= QuestionDetector.REQUIRED_EMPTY_POLLS) {
|
|
358
|
+
if (this.lastQuestionDetected) {
|
|
359
|
+
this.logger.debug(`[QuestionDetector:${this.projectName}] Question modal disappeared`);
|
|
360
|
+
this.lastQuestionDetected = false;
|
|
361
|
+
this.emit('resolved');
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
catch (e) {
|
|
367
|
+
if (e.message?.includes('Target closed') || e.message?.includes('Session closed')) {
|
|
368
|
+
// Ignore disconnect errors
|
|
369
|
+
}
|
|
370
|
+
else {
|
|
371
|
+
this.logger.error(`[QuestionDetector:${this.projectName}] Error:`, e.message);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
exports.QuestionDetector = QuestionDetector;
|
|
@@ -601,6 +601,8 @@ class ResponseMonitor {
|
|
|
601
601
|
extractionMode;
|
|
602
602
|
onProgress;
|
|
603
603
|
onComplete;
|
|
604
|
+
onStructuredProgress;
|
|
605
|
+
onStructuredComplete;
|
|
604
606
|
onTimeout;
|
|
605
607
|
onPhaseChange;
|
|
606
608
|
onProcessLog;
|
|
@@ -609,12 +611,15 @@ class ResponseMonitor {
|
|
|
609
611
|
pollTimer = null;
|
|
610
612
|
isRunning = false;
|
|
611
613
|
lastText = null;
|
|
614
|
+
lastClassified = null;
|
|
612
615
|
baselineText = null;
|
|
613
616
|
generationStarted = false;
|
|
614
617
|
currentPhase = 'waiting';
|
|
615
618
|
stopGoneCount = 0;
|
|
616
619
|
quotaDetected = false;
|
|
617
620
|
seenProcessLogKeys = new Set();
|
|
621
|
+
currentCitedFiles = [];
|
|
622
|
+
currentFileChanges = [];
|
|
618
623
|
structuredDiagLogged = false;
|
|
619
624
|
lastContentContextId = null;
|
|
620
625
|
// CDP disconnect handling (#48)
|
|
@@ -632,12 +637,17 @@ class ResponseMonitor {
|
|
|
632
637
|
this.extractionMode = options.extractionMode ?? 'structured';
|
|
633
638
|
this.onProgress = options.onProgress;
|
|
634
639
|
this.onComplete = options.onComplete;
|
|
640
|
+
this.onStructuredProgress = options.onStructuredProgress;
|
|
641
|
+
this.onStructuredComplete = options.onStructuredComplete;
|
|
635
642
|
this.onTimeout = options.onTimeout;
|
|
636
643
|
this.onPhaseChange = options.onPhaseChange;
|
|
637
644
|
this.onProcessLog = options.onProcessLog;
|
|
638
645
|
this.initialBaselineText = options.initialBaselineText;
|
|
639
646
|
this.initialSeenProcessLogKeys = options.initialSeenProcessLogKeys;
|
|
640
647
|
}
|
|
648
|
+
getLastClassified() {
|
|
649
|
+
return this.lastClassified;
|
|
650
|
+
}
|
|
641
651
|
/** Start monitoring */
|
|
642
652
|
async start() {
|
|
643
653
|
return this.initMonitoring(false);
|
|
@@ -979,6 +989,7 @@ class ResponseMonitor {
|
|
|
979
989
|
if (classified.diagnostics.source === 'dom-structured') {
|
|
980
990
|
currentText = classified.finalOutputText.trim() || null;
|
|
981
991
|
structuredHandledLogs = true;
|
|
992
|
+
this.lastClassified = classified;
|
|
982
993
|
if (!this.structuredDiagLogged) {
|
|
983
994
|
this.structuredDiagLogged = true;
|
|
984
995
|
logger_1.logger.debug('[ResponseMonitor] Structured extraction OK — segments:', classified.diagnostics.segmentCounts);
|
|
@@ -987,6 +998,8 @@ class ResponseMonitor {
|
|
|
987
998
|
if (classified.activityLines.length > 0) {
|
|
988
999
|
this.emitNewProcessLogs(classified.activityLines);
|
|
989
1000
|
}
|
|
1001
|
+
this.currentCitedFiles = classified.citedFiles || [];
|
|
1002
|
+
this.currentFileChanges = classified.fileChangesTexts || [];
|
|
990
1003
|
}
|
|
991
1004
|
else if (!this.structuredDiagLogged) {
|
|
992
1005
|
this.structuredDiagLogged = true;
|
|
@@ -1037,6 +1050,8 @@ class ResponseMonitor {
|
|
|
1037
1050
|
await this.stop();
|
|
1038
1051
|
try {
|
|
1039
1052
|
await Promise.resolve(this.onComplete?.(''));
|
|
1053
|
+
if (this.lastClassified)
|
|
1054
|
+
await Promise.resolve(this.onStructuredComplete?.(this.lastClassified));
|
|
1040
1055
|
}
|
|
1041
1056
|
catch (error) {
|
|
1042
1057
|
logger_1.logger.error('[ResponseMonitor] complete callback failed:', error);
|
|
@@ -1061,7 +1076,16 @@ class ResponseMonitor {
|
|
|
1061
1076
|
this.generationStarted = true;
|
|
1062
1077
|
}
|
|
1063
1078
|
}
|
|
1064
|
-
this.onProgress?.(effectiveText);
|
|
1079
|
+
this.onProgress?.(effectiveText || '');
|
|
1080
|
+
if (this.lastClassified)
|
|
1081
|
+
this.onStructuredProgress?.(this.lastClassified);
|
|
1082
|
+
}
|
|
1083
|
+
else if (this.lastClassified && this.extractionMode === 'structured') {
|
|
1084
|
+
// If text didn't change but structured data did (e.g. new file changes), emit structured progress anyway
|
|
1085
|
+
// To keep it simple, we just emit on structured mode if generation is active.
|
|
1086
|
+
if (isGenerating && this.generationStarted) {
|
|
1087
|
+
this.onStructuredProgress?.(this.lastClassified);
|
|
1088
|
+
}
|
|
1065
1089
|
}
|
|
1066
1090
|
// Completion: stop button gone N consecutive times
|
|
1067
1091
|
if (!isGenerating && this.generationStarted) {
|
|
@@ -1071,7 +1095,9 @@ class ResponseMonitor {
|
|
|
1071
1095
|
this.setPhase('complete', finalText);
|
|
1072
1096
|
await this.stop();
|
|
1073
1097
|
try {
|
|
1074
|
-
await Promise.resolve(this.onComplete?.(finalText));
|
|
1098
|
+
await Promise.resolve(this.onComplete?.(finalText, this.currentCitedFiles, this.currentFileChanges));
|
|
1099
|
+
if (this.lastClassified)
|
|
1100
|
+
await Promise.resolve(this.onStructuredComplete?.(this.lastClassified));
|
|
1075
1101
|
}
|
|
1076
1102
|
catch (error) {
|
|
1077
1103
|
logger_1.logger.error('[ResponseMonitor] complete callback failed:', error);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.RunCommandDetector = 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
|
/**
|
|
@@ -25,7 +26,7 @@ const approvalDetector_1 = require("./approvalDetector");
|
|
|
25
26
|
*/
|
|
26
27
|
const DETECT_RUN_COMMAND_SCRIPT = `(() => {
|
|
27
28
|
const RUN_COMMAND_HEADER_PATTERNS = [
|
|
28
|
-
'run command?', 'run command', 'execute command',
|
|
29
|
+
'run command?', 'run command', 'execute command', 'command execution',
|
|
29
30
|
'コマンドを実行', 'コマンド実行'
|
|
30
31
|
];
|
|
31
32
|
const RUN_PATTERNS = ['run', 'accept', '実行', 'execute'];
|
|
@@ -111,8 +112,10 @@ class RunCommandDetector {
|
|
|
111
112
|
isRunning = false;
|
|
112
113
|
/** Key of the last detected dialog (for duplicate notification prevention) */
|
|
113
114
|
lastDetectedKey = null;
|
|
114
|
-
/** Full RunCommandInfo from the last detection
|
|
115
|
+
/** Full RunCommandInfo from the last detection */
|
|
115
116
|
lastDetectedInfo = null;
|
|
117
|
+
/** Gate for empty polls before reset */
|
|
118
|
+
emptyPollGate = new consecutiveEmptyPollGate_1.ConsecutiveEmptyPollGate(3);
|
|
116
119
|
constructor(options) {
|
|
117
120
|
this.cdpService = options.cdpService;
|
|
118
121
|
this.pollIntervalMs = options.pollIntervalMs ?? 1500;
|
|
@@ -126,6 +129,7 @@ class RunCommandDetector {
|
|
|
126
129
|
this.isRunning = true;
|
|
127
130
|
this.lastDetectedKey = null;
|
|
128
131
|
this.lastDetectedInfo = null;
|
|
132
|
+
this.emptyPollGate.reset();
|
|
129
133
|
this.schedulePoll();
|
|
130
134
|
}
|
|
131
135
|
/** Stop monitoring. */
|
|
@@ -171,6 +175,7 @@ class RunCommandDetector {
|
|
|
171
175
|
const result = await this.cdpService.call('Runtime.evaluate', callParams);
|
|
172
176
|
const info = result?.result?.value ?? null;
|
|
173
177
|
if (info) {
|
|
178
|
+
this.emptyPollGate.recordDetection();
|
|
174
179
|
// Duplicate prevention: use commandText as key
|
|
175
180
|
const key = `${info.commandText}::${info.workingDirectory}`;
|
|
176
181
|
if (key !== this.lastDetectedKey) {
|
|
@@ -180,11 +185,13 @@ class RunCommandDetector {
|
|
|
180
185
|
}
|
|
181
186
|
}
|
|
182
187
|
else {
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
this.onResolved
|
|
188
|
+
if (this.emptyPollGate.recordEmptyPoll()) {
|
|
189
|
+
const wasDetected = this.lastDetectedKey !== null;
|
|
190
|
+
this.lastDetectedKey = null;
|
|
191
|
+
this.lastDetectedInfo = null;
|
|
192
|
+
if (wasDetected && this.onResolved) {
|
|
193
|
+
this.onResolved();
|
|
194
|
+
}
|
|
188
195
|
}
|
|
189
196
|
}
|
|
190
197
|
}
|
package/dist/ui/artifactsUi.js
CHANGED
|
@@ -11,6 +11,7 @@ exports.sendArtifactsUI = sendArtifactsUI;
|
|
|
11
11
|
exports.sendArtifactPickerUI = sendArtifactPickerUI;
|
|
12
12
|
const discord_js_1 = require("discord.js");
|
|
13
13
|
const artifactService_1 = require("../services/artifactService");
|
|
14
|
+
const pathUtils_1 = require("../utils/pathUtils");
|
|
14
15
|
// ---------------------------------------------------------------------------
|
|
15
16
|
// Constants
|
|
16
17
|
// ---------------------------------------------------------------------------
|
|
@@ -143,7 +144,8 @@ async function sendArtifactPickerUI(interaction, deps, edit = false, resolvedCon
|
|
|
143
144
|
// Fallback to title matching if ID is not in DB or doesn't match a real folder
|
|
144
145
|
if (!conversationId || !artifactService.listArtifacts(conversationId).length) {
|
|
145
146
|
const sessionTitle = session.displayName?.trim() ?? '';
|
|
146
|
-
const
|
|
147
|
+
const workspaceDirName = (0, pathUtils_1.getWorkspaceDirName)(session);
|
|
148
|
+
const matchedId = sessionTitle ? artifactService.findConversationByTitle(sessionTitle, workspaceDirName) : null;
|
|
147
149
|
if (matchedId)
|
|
148
150
|
conversationId = matchedId;
|
|
149
151
|
}
|
|
@@ -151,8 +153,7 @@ async function sendArtifactPickerUI(interaction, deps, edit = false, resolvedCon
|
|
|
151
153
|
// to avoid showing artifacts from other projects/chats.
|
|
152
154
|
}
|
|
153
155
|
else {
|
|
154
|
-
|
|
155
|
-
conversationId = artifactService.getLatestConversationWithArtifacts();
|
|
156
|
+
conversationId = null;
|
|
156
157
|
}
|
|
157
158
|
}
|
|
158
159
|
if (!conversationId) {
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ConsecutiveEmptyPollGate = void 0;
|
|
4
|
+
class ConsecutiveEmptyPollGate {
|
|
5
|
+
requiredEmptyPolls;
|
|
6
|
+
count = 0;
|
|
7
|
+
constructor(requiredEmptyPolls = 3) {
|
|
8
|
+
this.requiredEmptyPolls = requiredEmptyPolls;
|
|
9
|
+
}
|
|
10
|
+
recordDetection() {
|
|
11
|
+
this.count = 0;
|
|
12
|
+
}
|
|
13
|
+
recordEmptyPoll() {
|
|
14
|
+
this.count++;
|
|
15
|
+
return this.count >= this.requiredEmptyPolls;
|
|
16
|
+
}
|
|
17
|
+
reset() {
|
|
18
|
+
this.count = 0;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
exports.ConsecutiveEmptyPollGate = ConsecutiveEmptyPollGate;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.fileOpenCache = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* In-memory cache to map button customIds to file URLs.
|
|
6
|
+
* Bounded to a maximum size to prevent memory leaks, and entries are removed once consumed.
|
|
7
|
+
*/
|
|
8
|
+
class BoundedCache {
|
|
9
|
+
max_size;
|
|
10
|
+
map;
|
|
11
|
+
constructor(max_size = 1000) {
|
|
12
|
+
this.max_size = max_size;
|
|
13
|
+
this.map = new Map();
|
|
14
|
+
}
|
|
15
|
+
set(key, value) {
|
|
16
|
+
if (this.map.size >= this.max_size) {
|
|
17
|
+
// Remove the oldest entry (first item in the map's insertion order)
|
|
18
|
+
const firstKey = this.map.keys().next().value;
|
|
19
|
+
if (firstKey !== undefined) {
|
|
20
|
+
this.map.delete(firstKey);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
this.map.set(key, value);
|
|
24
|
+
}
|
|
25
|
+
get(key) {
|
|
26
|
+
const value = this.map.get(key);
|
|
27
|
+
if (value !== undefined) {
|
|
28
|
+
this.map.delete(key); // Remove upon consumption
|
|
29
|
+
}
|
|
30
|
+
return value;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
exports.fileOpenCache = new BoundedCache(1000);
|
|
@@ -42,6 +42,9 @@ function htmlToDiscordMarkdown(html) {
|
|
|
42
42
|
// Extract language from class="language-xxx" if present.
|
|
43
43
|
// Do NOT decode entities here — let the final decodeEntities() handle them
|
|
44
44
|
// after stripTags() has run, to avoid decoded < > being stripped as tags.
|
|
45
|
+
// First, Antigravity 2.0 wrapped code blocks
|
|
46
|
+
result = result.replace(/<div[^>]*class="[^"]*code-block[^"]*"[^>]*>(?:(?!<div[^>]*class="[^"]*code-block[^"]*"[^>]*>)[\s\S])*?<div[^>]*class="[^"]*code-header[^"]*"[^>]*>([\s\S]*?)<\/div>(?:(?!<div[^>]*class="[^"]*code-block[^"]*"[^>]*>)[\s\S])*?<pre[^>]*>\s*<code[^>]*>([\s\S]*?)<\/code>\s*<\/pre>(?:(?!<div[^>]*class="[^"]*code-block[^"]*"[^>]*>)[\s\S])*?<\/div>/gi, (_m, lang, content) => `\n\`\`\`${stripTags(lang).trim()}\n${content}\n\`\`\`\n`);
|
|
47
|
+
// Standard <pre><code> blocks
|
|
45
48
|
result = result.replace(/<pre[^>]*>\s*<code(?:\s+class="language-([^"]*)")?[^>]*>([\s\S]*?)<\/code>\s*<\/pre>/gi, (_m, lang, content) => `\n\`\`\`${lang || ''}\n${content}\n\`\`\`\n`);
|
|
46
49
|
// Handle inline <code>
|
|
47
50
|
result = result.replace(/<code[^>]*>([\s\S]*?)<\/code>/gi, '`$1`');
|
|
@@ -49,8 +52,8 @@ function htmlToDiscordMarkdown(html) {
|
|
|
49
52
|
result = result.replace(/<(?:strong|b)(?:\s[^>]*)?>((?: |\s|[^<]|<(?!\/(?:strong|b)>))*)<\/(?:strong|b)>/gi, '**$1**');
|
|
50
53
|
// Handle <em> and <i>
|
|
51
54
|
result = result.replace(/<(?:em|i)(?:\s[^>]*)?>((?: |\s|[^<]|<(?!\/(?:em|i)>))*)<\/(?:em|i)>/gi, '*$1*');
|
|
52
|
-
// Handle <span class="context-scope-mention"> → `text`
|
|
53
|
-
result = result.replace(/<span[^>]*class="[^"]*context-scope-mention[^"]*"[^>]*>([\s\S]*?)<\/span>/gi, (_m, text) => `\`${stripTags(text).trim()}\``);
|
|
55
|
+
// Handle <span class="context-scope-mention"> or <span class="file-chip"> → `text`
|
|
56
|
+
result = result.replace(/<span[^>]*class="[^"]*(?:context-scope-mention|file-chip)[^"]*"[^>]*>([\s\S]*?)<\/span>/gi, (_m, text) => `\`${stripTags(text).trim()}\``);
|
|
54
57
|
// Handle elements with title attribute containing file paths
|
|
55
58
|
// e.g. <div title="src/bot/index.ts">:54</div> → src/bot/index.ts:54
|
|
56
59
|
result = result.replace(/<(?:div|span|a)[^>]*\btitle="([^"]*)"[^>]*>([\s\S]*?)<\/(?:div|span|a)>/gi, (_m, title, text) => {
|
package/dist/utils/pathUtils.js
CHANGED
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.getAntigravityCliPath = getAntigravityCliPath;
|
|
4
4
|
exports.extractProjectNameFromPath = extractProjectNameFromPath;
|
|
5
5
|
exports.getAntigravityCdpHint = getAntigravityCdpHint;
|
|
6
|
+
exports.getWorkspaceDirName = getWorkspaceDirName;
|
|
6
7
|
/**
|
|
7
8
|
* Helper to resolve the correct Antigravity CLI executable path based on the operating system
|
|
8
9
|
* and environment variables.
|
|
@@ -55,3 +56,14 @@ function getAntigravityCdpHint(port = 9222) {
|
|
|
55
56
|
return `${APP_NAME.toLowerCase()} --remote-debugging-port=${port}`;
|
|
56
57
|
}
|
|
57
58
|
}
|
|
59
|
+
/**
|
|
60
|
+
* Extracts the workspace directory name from a chat session.
|
|
61
|
+
* @param session The chat session object
|
|
62
|
+
* @returns The base directory name, or undefined
|
|
63
|
+
*/
|
|
64
|
+
function getWorkspaceDirName(session) {
|
|
65
|
+
if (session && session.workspacePath) {
|
|
66
|
+
return extractProjectNameFromPath(session.workspacePath);
|
|
67
|
+
}
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.resolveProjectName = resolveProjectName;
|
|
4
|
+
function resolveProjectName(deps, channelId, providedProjectName) {
|
|
5
|
+
if (providedProjectName) {
|
|
6
|
+
return providedProjectName;
|
|
7
|
+
}
|
|
8
|
+
const workspacePath = deps.wsHandler.getWorkspaceForChannel(channelId);
|
|
9
|
+
return workspacePath ? deps.bridge.pool.extractProjectName(workspacePath) : undefined;
|
|
10
|
+
}
|