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.
- package/dist/bin/commands/doctor.js +45 -7
- package/dist/bot/index.js +401 -41
- package/dist/bot/telegramMessageHandler.js +1 -0
- package/dist/commands/chatCommandHandler.js +43 -3
- package/dist/commands/registerSlashCommands.js +13 -1
- package/dist/events/interactionCreateHandler.js +456 -27
- package/dist/events/messageCreateHandler.js +66 -7
- 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 +57 -0
- package/dist/handlers/genericActionButtonAction.js +72 -0
- package/dist/handlers/planningButtonAction.js +126 -23
- package/dist/handlers/planningModalSubmitAction.js +38 -0
- package/dist/handlers/questionSelectAction.js +70 -0
- package/dist/handlers/questionSkipAction.js +68 -0
- package/dist/handlers/runCommandButtonAction.js +2 -1
- package/dist/platform/discord/discordResponseRenderer.js +164 -0
- package/dist/platform/discord/wrappers.js +76 -0
- package/dist/services/approvalDetector.js +110 -35
- package/dist/services/artifactService.js +103 -37
- package/dist/services/assistantDomExtractor.js +194 -3
- package/dist/services/cdpBridgeManager.js +149 -8
- package/dist/services/cdpConnectionPool.js +22 -2
- package/dist/services/cdpService.js +134 -15
- package/dist/services/chatSessionService.js +147 -40
- package/dist/services/errorPopupDetector.js +23 -11
- package/dist/services/notificationSender.js +80 -3
- package/dist/services/planningDetector.js +69 -25
- package/dist/services/promptDispatcher.js +27 -1
- package/dist/services/questionDetector.js +428 -0
- package/dist/services/responseMonitor.js +45 -3
- 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 +29 -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 +47 -0
- package/package.json +1 -1
|
@@ -0,0 +1,428 @@
|
|
|
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 getInteractiveItems = (elContainer) => {
|
|
65
|
+
return Array.from(elContainer.querySelectorAll('li, label, a, [role="radio"], [role="option"], [class*="cursor-pointer"]'))
|
|
66
|
+
.filter(el => {
|
|
67
|
+
if (el.tagName === 'BUTTON' || el.closest('button')) return false;
|
|
68
|
+
const role = el.getAttribute('role');
|
|
69
|
+
if (['radio', 'option', 'checkbox', 'button', 'menuitem'].includes(role)) return true;
|
|
70
|
+
if (el.tagName === 'A' || el.tagName === 'LABEL') return true;
|
|
71
|
+
const style = window.getComputedStyle(el);
|
|
72
|
+
return style.cursor === 'pointer';
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
const containers = Array.from(document.querySelectorAll('div, form, dialog')).reverse();
|
|
76
|
+
let targetList = null;
|
|
77
|
+
let submitBtn = null;
|
|
78
|
+
|
|
79
|
+
for (const container of containers) {
|
|
80
|
+
const hasTextInput = container.querySelector('textarea, input:not([type="hidden"]):not([type="radio"]):not([type="checkbox"]), [contenteditable], [role="textbox"], .monaco-editor, .editor-container, .inputarea, vscode-text-field, vscode-text-area');
|
|
81
|
+
const hasExplicitQuestionItems = container.querySelector('[role="radio"], input[type="radio"], input[type="checkbox"], [role="option"], [role="checkbox"]');
|
|
82
|
+
if (hasTextInput && !hasExplicitQuestionItems) continue;
|
|
83
|
+
const buttons = Array.from(container.querySelectorAll('button'));
|
|
84
|
+
let possibleSubmitBtn = null;
|
|
85
|
+
for (const btn of buttons) {
|
|
86
|
+
const text = btn.textContent?.toLowerCase() || '';
|
|
87
|
+
if (text.includes('submit') || text.includes('continue')) {
|
|
88
|
+
possibleSubmitBtn = btn;
|
|
89
|
+
break;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (possibleSubmitBtn) {
|
|
94
|
+
const items = getInteractiveItems(container);
|
|
95
|
+
|
|
96
|
+
if (items.length > 1) {
|
|
97
|
+
targetList = container;
|
|
98
|
+
submitBtn = possibleSubmitBtn;
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (!targetList || !submitBtn) return { found: false };
|
|
105
|
+
|
|
106
|
+
const items = getInteractiveItems(targetList);
|
|
107
|
+
if (items.length <= ${index}) return { found: false };
|
|
108
|
+
|
|
109
|
+
const targetOption = items[${index}];
|
|
110
|
+
|
|
111
|
+
const clickElement = (el) => {
|
|
112
|
+
const rect = el.getBoundingClientRect();
|
|
113
|
+
const clickX = rect.left + rect.width / 2;
|
|
114
|
+
const clickY = rect.top + rect.height / 2;
|
|
115
|
+
const events = ['pointerdown', 'mousedown', 'mouseup', 'click'];
|
|
116
|
+
for (const type of events) {
|
|
117
|
+
el.dispatchEvent(new MouseEvent(type, {
|
|
118
|
+
bubbles: true,
|
|
119
|
+
cancelable: true,
|
|
120
|
+
view: window,
|
|
121
|
+
clientX: clickX,
|
|
122
|
+
clientY: clickY,
|
|
123
|
+
}));
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
return new Promise(resolve => {
|
|
128
|
+
clickElement(targetOption);
|
|
129
|
+
setTimeout(() => {
|
|
130
|
+
clickElement(submitBtn);
|
|
131
|
+
resolve({ found: true });
|
|
132
|
+
}, 50);
|
|
133
|
+
});
|
|
134
|
+
})()
|
|
135
|
+
`,
|
|
136
|
+
returnByValue: true,
|
|
137
|
+
awaitPromise: true,
|
|
138
|
+
};
|
|
139
|
+
if (contextId !== null) {
|
|
140
|
+
callParams.contextId = contextId;
|
|
141
|
+
}
|
|
142
|
+
const response = await this.cdp.call('Runtime.evaluate', callParams);
|
|
143
|
+
const result = response?.result?.value;
|
|
144
|
+
if (!result || !result.found) {
|
|
145
|
+
this.logger.warn(`[QuestionDetector:${this.projectName}] Could not find question modal elements during submission.`);
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
this.lastQuestionDetected = false;
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
catch (e) {
|
|
152
|
+
this.logger.error(`[QuestionDetector:${this.projectName}] submitOption error:`, e.message);
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async skipQuestion() {
|
|
157
|
+
this.logger.debug(`[QuestionDetector:${this.projectName}] Skipping question`);
|
|
158
|
+
try {
|
|
159
|
+
const contextId = this.cdp.getPrimaryContextId();
|
|
160
|
+
const callParams = {
|
|
161
|
+
expression: `
|
|
162
|
+
(() => {
|
|
163
|
+
const getInteractiveItems = (elContainer) => {
|
|
164
|
+
return Array.from(elContainer.querySelectorAll('li, label, a, [role="radio"], [role="option"], [class*="cursor-pointer"]'))
|
|
165
|
+
.filter(el => {
|
|
166
|
+
if (el.tagName === 'BUTTON' || el.closest('button')) return false;
|
|
167
|
+
const role = el.getAttribute('role');
|
|
168
|
+
if (['radio', 'option', 'checkbox', 'button', 'menuitem'].includes(role)) return true;
|
|
169
|
+
if (el.tagName === 'A' || el.tagName === 'LABEL') return true;
|
|
170
|
+
const style = window.getComputedStyle(el);
|
|
171
|
+
return style.cursor === 'pointer';
|
|
172
|
+
});
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
const containers = Array.from(document.querySelectorAll('div, form, dialog')).reverse();
|
|
176
|
+
let skipBtn = null;
|
|
177
|
+
|
|
178
|
+
for (const container of containers) {
|
|
179
|
+
const hasTextInput = container.querySelector('textarea, input:not([type="hidden"]):not([type="radio"]):not([type="checkbox"]), [contenteditable="true"], [contenteditable=""], [role="textbox"]');
|
|
180
|
+
const hasExplicitQuestionItems = container.querySelector('[role="radio"], input[type="radio"], input[type="checkbox"], [role="option"], [role="checkbox"]');
|
|
181
|
+
if (hasTextInput && !hasExplicitQuestionItems) continue;
|
|
182
|
+
const items = getInteractiveItems(container);
|
|
183
|
+
const hasList = items.length > 1;
|
|
184
|
+
|
|
185
|
+
const buttons = Array.from(container.querySelectorAll('button'));
|
|
186
|
+
for (const btn of buttons) {
|
|
187
|
+
const text = btn.textContent?.toLowerCase() || '';
|
|
188
|
+
if (text.includes('skip') && hasList) {
|
|
189
|
+
skipBtn = btn;
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (skipBtn) break;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (!skipBtn) return { found: false };
|
|
197
|
+
|
|
198
|
+
const clickElement = (el) => {
|
|
199
|
+
const rect = el.getBoundingClientRect();
|
|
200
|
+
const clickX = rect.left + rect.width / 2;
|
|
201
|
+
const clickY = rect.top + rect.height / 2;
|
|
202
|
+
const events = ['pointerdown', 'mousedown', 'mouseup', 'click'];
|
|
203
|
+
for (const type of events) {
|
|
204
|
+
el.dispatchEvent(new MouseEvent(type, {
|
|
205
|
+
bubbles: true,
|
|
206
|
+
cancelable: true,
|
|
207
|
+
view: window,
|
|
208
|
+
clientX: clickX,
|
|
209
|
+
clientY: clickY,
|
|
210
|
+
}));
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
clickElement(skipBtn);
|
|
215
|
+
return { found: true };
|
|
216
|
+
})()
|
|
217
|
+
`,
|
|
218
|
+
returnByValue: true,
|
|
219
|
+
awaitPromise: false,
|
|
220
|
+
};
|
|
221
|
+
if (contextId !== null)
|
|
222
|
+
callParams.contextId = contextId;
|
|
223
|
+
const response = await this.cdp.call('Runtime.evaluate', callParams);
|
|
224
|
+
const result = response?.result?.value;
|
|
225
|
+
if (!result || !result.found) {
|
|
226
|
+
this.logger.warn(`[QuestionDetector:${this.projectName}] Could not find skip button.`);
|
|
227
|
+
return false;
|
|
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 STOP_PATTERNS = [
|
|
245
|
+
/^stop$/,
|
|
246
|
+
/^stop generating$/,
|
|
247
|
+
/^stop response$/,
|
|
248
|
+
/^停止$/,
|
|
249
|
+
/^生成を停止$/,
|
|
250
|
+
/^応答を停止$/,
|
|
251
|
+
];
|
|
252
|
+
const normalize = (value) => (value || '').toLowerCase().replace(/\\s+/g, ' ').trim();
|
|
253
|
+
const isStopLabel = (value) => {
|
|
254
|
+
const normalized = normalize(value);
|
|
255
|
+
if (!normalized) return false;
|
|
256
|
+
return STOP_PATTERNS.some((re) => re.test(normalized));
|
|
257
|
+
};
|
|
258
|
+
const isGenerating = Array.from(document.querySelectorAll('button, [role="button"]')).some(btn => {
|
|
259
|
+
const labels = [
|
|
260
|
+
btn.textContent || '',
|
|
261
|
+
btn.getAttribute('aria-label') || '',
|
|
262
|
+
btn.getAttribute('title') || '',
|
|
263
|
+
];
|
|
264
|
+
return labels.some(isStopLabel);
|
|
265
|
+
}) || (() => {
|
|
266
|
+
const panel = document.querySelector('.antigravity-agent-side-panel');
|
|
267
|
+
if (panel) {
|
|
268
|
+
const panelText = (panel.textContent || '').trim();
|
|
269
|
+
return /Working\.\s*$/i.test(panelText);
|
|
270
|
+
}
|
|
271
|
+
return false;
|
|
272
|
+
})();
|
|
273
|
+
if (isGenerating) {
|
|
274
|
+
return { detected: false, reason: "IDE is generating" };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const getInteractiveItems = (elContainer) => {
|
|
278
|
+
return Array.from(elContainer.querySelectorAll('li, label, a, [role="radio"], [role="option"], [class*="cursor-pointer"]'))
|
|
279
|
+
.filter(el => {
|
|
280
|
+
if (el.tagName === 'BUTTON' || el.closest('button')) return false;
|
|
281
|
+
const text = (el.innerText || el.textContent || '').trim();
|
|
282
|
+
if (!text) return false;
|
|
283
|
+
const role = el.getAttribute('role');
|
|
284
|
+
if (['radio', 'option', 'checkbox', 'button', 'menuitem'].includes(role)) return true;
|
|
285
|
+
if (el.tagName === 'A' || el.tagName === 'LABEL') return true;
|
|
286
|
+
const style = window.getComputedStyle(el);
|
|
287
|
+
return style.cursor === 'pointer';
|
|
288
|
+
});
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const containers = Array.from(document.querySelectorAll('div, form, dialog')).reverse();
|
|
292
|
+
let targetList = null;
|
|
293
|
+
let submitBtn = null;
|
|
294
|
+
|
|
295
|
+
for (const container of containers) {
|
|
296
|
+
const hasTextInput = container.querySelector('textarea, input:not([type="hidden"]):not([type="radio"]):not([type="checkbox"]), [contenteditable], [role="textbox"], .monaco-editor, .editor-container, .inputarea, vscode-text-field, vscode-text-area');
|
|
297
|
+
const hasExplicitQuestionItems = container.querySelector('[role="radio"], input[type="radio"], input[type="checkbox"], [role="option"], [role="checkbox"]');
|
|
298
|
+
if (hasTextInput && !hasExplicitQuestionItems) continue;
|
|
299
|
+
const buttons = Array.from(container.querySelectorAll('button'));
|
|
300
|
+
let possibleSubmitBtn = null;
|
|
301
|
+
for (const btn of buttons) {
|
|
302
|
+
const text = btn.textContent?.toLowerCase() || '';
|
|
303
|
+
if (text.includes('submit') || text.includes('continue')) {
|
|
304
|
+
possibleSubmitBtn = btn;
|
|
305
|
+
break;
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
if (possibleSubmitBtn) {
|
|
310
|
+
const items = getInteractiveItems(container);
|
|
311
|
+
|
|
312
|
+
if (items.length > 1) {
|
|
313
|
+
targetList = container;
|
|
314
|
+
submitBtn = possibleSubmitBtn;
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
if (!targetList || !submitBtn) return { detected: false, reason: "No targetList or submitBtn found" };
|
|
320
|
+
|
|
321
|
+
let titleEl = targetList.querySelector('h1, h2, h3, [role="heading"], .text-lg');
|
|
322
|
+
if (!titleEl) {
|
|
323
|
+
let p = targetList;
|
|
324
|
+
while (p && p.tagName !== 'BODY') {
|
|
325
|
+
titleEl = p.querySelector('h1, h2, h3, [role="heading"], .text-lg, p');
|
|
326
|
+
if (titleEl && titleEl !== p && (titleEl.innerText || titleEl.textContent || '').trim().length > 0) break;
|
|
327
|
+
p = p.parentElement;
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
const title = titleEl ? (titleEl.innerText || titleEl.textContent || '').trim() : 'Question';
|
|
331
|
+
|
|
332
|
+
const items = getInteractiveItems(targetList);
|
|
333
|
+
const options = items.map(n => {
|
|
334
|
+
const rect = n.getBoundingClientRect();
|
|
335
|
+
const finalLabel = n.innerText || n.textContent || 'Option';
|
|
336
|
+
|
|
337
|
+
return {
|
|
338
|
+
text: finalLabel.replace(/\\n/g, ' ').replace(/\\s+/g, ' ').trim().substring(0, 100),
|
|
339
|
+
x: Math.round(rect.left + rect.width / 2),
|
|
340
|
+
y: Math.round(rect.top + rect.height / 2)
|
|
341
|
+
};
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
if (options.length === 0) return { detected: false, reason: "options length 0" };
|
|
345
|
+
|
|
346
|
+
return {
|
|
347
|
+
detected: true,
|
|
348
|
+
title,
|
|
349
|
+
options
|
|
350
|
+
};
|
|
351
|
+
})()
|
|
352
|
+
`,
|
|
353
|
+
returnByValue: true,
|
|
354
|
+
awaitPromise: false,
|
|
355
|
+
};
|
|
356
|
+
const contexts = this.cdp.getContexts();
|
|
357
|
+
const contextIds = [
|
|
358
|
+
this.cdp.getPrimaryContextId(),
|
|
359
|
+
...contexts.map((ctx) => ctx.id),
|
|
360
|
+
].filter((value, index, arr) => typeof value === 'number' && arr.indexOf(value) === index);
|
|
361
|
+
const targets = contextIds.length > 0 ? contextIds : [null];
|
|
362
|
+
let detectedResult = null;
|
|
363
|
+
let lastReason = null;
|
|
364
|
+
for (const contextId of targets) {
|
|
365
|
+
if (contextId !== null) {
|
|
366
|
+
callParams.contextId = contextId;
|
|
367
|
+
}
|
|
368
|
+
const response = await this.cdp.call('Runtime.evaluate', callParams).catch(e => {
|
|
369
|
+
this.logger.debug(`[QuestionDetector] Error evaluating on ctx ${contextId}: ${e.message}`);
|
|
370
|
+
return null;
|
|
371
|
+
});
|
|
372
|
+
if (!this._isStarted)
|
|
373
|
+
return;
|
|
374
|
+
if (response?.exceptionDetails) {
|
|
375
|
+
this.logger.debug(`[QuestionDetector] Exception on ctx ${contextId}: ${response.exceptionDetails.exception?.description || response.exceptionDetails.text}`);
|
|
376
|
+
}
|
|
377
|
+
const result = response?.result?.value;
|
|
378
|
+
if (result) {
|
|
379
|
+
this.logger.debug(`[QuestionDetector] Context ${contextId} evaluated successfully, detected: ${result.detected}, reason: ${result.reason}`);
|
|
380
|
+
}
|
|
381
|
+
if (result && result.detected) {
|
|
382
|
+
detectedResult = result;
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
else if (result && result.reason) {
|
|
386
|
+
lastReason = result.reason;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
if (!this._isStarted)
|
|
390
|
+
return;
|
|
391
|
+
const result = detectedResult;
|
|
392
|
+
if (result && result.detected) {
|
|
393
|
+
this.emptyPollCount = 0;
|
|
394
|
+
if (!this.lastQuestionDetected) {
|
|
395
|
+
this.lastQuestionDetected = true;
|
|
396
|
+
this.logger.debug(`[QuestionDetector:${this.projectName}] Question modal detected`);
|
|
397
|
+
this.emit('question', {
|
|
398
|
+
title: result.title || 'Question',
|
|
399
|
+
description: 'Please answer the question below.',
|
|
400
|
+
options: result.options,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
else {
|
|
405
|
+
if (!result && lastReason) {
|
|
406
|
+
this.logger.debug(`[QuestionDetector:${this.projectName}] Evaluate returned false: ${lastReason}`);
|
|
407
|
+
}
|
|
408
|
+
this.emptyPollCount++;
|
|
409
|
+
if (this.emptyPollCount >= QuestionDetector.REQUIRED_EMPTY_POLLS) {
|
|
410
|
+
if (this.lastQuestionDetected) {
|
|
411
|
+
this.logger.debug(`[QuestionDetector:${this.projectName}] Question modal disappeared`);
|
|
412
|
+
this.lastQuestionDetected = false;
|
|
413
|
+
this.emit('resolved');
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
catch (e) {
|
|
419
|
+
if (e.message?.includes('Target closed') || e.message?.includes('Session closed')) {
|
|
420
|
+
// Ignore disconnect errors
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
this.logger.error(`[QuestionDetector:${this.projectName}] Error:`, e.message);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
exports.QuestionDetector = QuestionDetector;
|
|
@@ -146,6 +146,13 @@ exports.RESPONSE_SELECTORS = {
|
|
|
146
146
|
}
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
if (panel) {
|
|
150
|
+
const panelText = (panel.textContent || '').trim();
|
|
151
|
+
if (/Working\.\s*$/i.test(panelText)) {
|
|
152
|
+
return { isGenerating: true };
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
149
156
|
return { isGenerating: false };
|
|
150
157
|
})()`,
|
|
151
158
|
/** Click stop button via tooltip-id + text fallback */
|
|
@@ -601,6 +608,8 @@ class ResponseMonitor {
|
|
|
601
608
|
extractionMode;
|
|
602
609
|
onProgress;
|
|
603
610
|
onComplete;
|
|
611
|
+
onStructuredProgress;
|
|
612
|
+
onStructuredComplete;
|
|
604
613
|
onTimeout;
|
|
605
614
|
onPhaseChange;
|
|
606
615
|
onProcessLog;
|
|
@@ -609,12 +618,15 @@ class ResponseMonitor {
|
|
|
609
618
|
pollTimer = null;
|
|
610
619
|
isRunning = false;
|
|
611
620
|
lastText = null;
|
|
621
|
+
lastClassified = null;
|
|
612
622
|
baselineText = null;
|
|
613
623
|
generationStarted = false;
|
|
614
624
|
currentPhase = 'waiting';
|
|
615
625
|
stopGoneCount = 0;
|
|
616
626
|
quotaDetected = false;
|
|
617
627
|
seenProcessLogKeys = new Set();
|
|
628
|
+
currentCitedFiles = [];
|
|
629
|
+
currentFileChanges = [];
|
|
618
630
|
structuredDiagLogged = false;
|
|
619
631
|
lastContentContextId = null;
|
|
620
632
|
// CDP disconnect handling (#48)
|
|
@@ -624,6 +636,7 @@ class ResponseMonitor {
|
|
|
624
636
|
onCdpReconnectFailed = null;
|
|
625
637
|
// Activity-based timeout (#49)
|
|
626
638
|
lastActivityTime = 0;
|
|
639
|
+
startTime = 0;
|
|
627
640
|
constructor(options) {
|
|
628
641
|
this.cdpService = options.cdpService;
|
|
629
642
|
this.pollIntervalMs = options.pollIntervalMs ?? 2000;
|
|
@@ -632,12 +645,17 @@ class ResponseMonitor {
|
|
|
632
645
|
this.extractionMode = options.extractionMode ?? 'structured';
|
|
633
646
|
this.onProgress = options.onProgress;
|
|
634
647
|
this.onComplete = options.onComplete;
|
|
648
|
+
this.onStructuredProgress = options.onStructuredProgress;
|
|
649
|
+
this.onStructuredComplete = options.onStructuredComplete;
|
|
635
650
|
this.onTimeout = options.onTimeout;
|
|
636
651
|
this.onPhaseChange = options.onPhaseChange;
|
|
637
652
|
this.onProcessLog = options.onProcessLog;
|
|
638
653
|
this.initialBaselineText = options.initialBaselineText;
|
|
639
654
|
this.initialSeenProcessLogKeys = options.initialSeenProcessLogKeys;
|
|
640
655
|
}
|
|
656
|
+
getLastClassified() {
|
|
657
|
+
return this.lastClassified;
|
|
658
|
+
}
|
|
641
659
|
/** Start monitoring */
|
|
642
660
|
async start() {
|
|
643
661
|
return this.initMonitoring(false);
|
|
@@ -721,6 +739,7 @@ class ResponseMonitor {
|
|
|
721
739
|
}
|
|
722
740
|
// Activity-based timeout: track last activity time instead of fixed timer (#49)
|
|
723
741
|
this.lastActivityTime = Date.now();
|
|
742
|
+
this.startTime = Date.now();
|
|
724
743
|
// Register CDP connection event listeners (#48)
|
|
725
744
|
this.registerCdpConnectionListeners();
|
|
726
745
|
const mode = passive ? 'Passive monitoring' : 'Monitoring';
|
|
@@ -979,6 +998,7 @@ class ResponseMonitor {
|
|
|
979
998
|
if (classified.diagnostics.source === 'dom-structured') {
|
|
980
999
|
currentText = classified.finalOutputText.trim() || null;
|
|
981
1000
|
structuredHandledLogs = true;
|
|
1001
|
+
this.lastClassified = classified;
|
|
982
1002
|
if (!this.structuredDiagLogged) {
|
|
983
1003
|
this.structuredDiagLogged = true;
|
|
984
1004
|
logger_1.logger.debug('[ResponseMonitor] Structured extraction OK — segments:', classified.diagnostics.segmentCounts);
|
|
@@ -987,6 +1007,8 @@ class ResponseMonitor {
|
|
|
987
1007
|
if (classified.activityLines.length > 0) {
|
|
988
1008
|
this.emitNewProcessLogs(classified.activityLines);
|
|
989
1009
|
}
|
|
1010
|
+
this.currentCitedFiles = classified.citedFiles || [];
|
|
1011
|
+
this.currentFileChanges = classified.fileChangesTexts || [];
|
|
990
1012
|
}
|
|
991
1013
|
else if (!this.structuredDiagLogged) {
|
|
992
1014
|
this.structuredDiagLogged = true;
|
|
@@ -1037,6 +1059,8 @@ class ResponseMonitor {
|
|
|
1037
1059
|
await this.stop();
|
|
1038
1060
|
try {
|
|
1039
1061
|
await Promise.resolve(this.onComplete?.(''));
|
|
1062
|
+
if (this.lastClassified)
|
|
1063
|
+
await Promise.resolve(this.onStructuredComplete?.(this.lastClassified));
|
|
1040
1064
|
}
|
|
1041
1065
|
catch (error) {
|
|
1042
1066
|
logger_1.logger.error('[ResponseMonitor] complete callback failed:', error);
|
|
@@ -1061,7 +1085,16 @@ class ResponseMonitor {
|
|
|
1061
1085
|
this.generationStarted = true;
|
|
1062
1086
|
}
|
|
1063
1087
|
}
|
|
1064
|
-
this.onProgress?.(effectiveText);
|
|
1088
|
+
this.onProgress?.(effectiveText || '');
|
|
1089
|
+
if (this.lastClassified)
|
|
1090
|
+
this.onStructuredProgress?.(this.lastClassified);
|
|
1091
|
+
}
|
|
1092
|
+
else if (this.lastClassified && this.extractionMode === 'structured') {
|
|
1093
|
+
// If text didn't change but structured data did (e.g. new file changes), emit structured progress anyway
|
|
1094
|
+
// To keep it simple, we just emit on structured mode if generation is active.
|
|
1095
|
+
if (isGenerating && this.generationStarted) {
|
|
1096
|
+
this.onStructuredProgress?.(this.lastClassified);
|
|
1097
|
+
}
|
|
1065
1098
|
}
|
|
1066
1099
|
// Completion: stop button gone N consecutive times
|
|
1067
1100
|
if (!isGenerating && this.generationStarted) {
|
|
@@ -1071,7 +1104,9 @@ class ResponseMonitor {
|
|
|
1071
1104
|
this.setPhase('complete', finalText);
|
|
1072
1105
|
await this.stop();
|
|
1073
1106
|
try {
|
|
1074
|
-
await Promise.resolve(this.onComplete?.(finalText));
|
|
1107
|
+
await Promise.resolve(this.onComplete?.(finalText, this.currentCitedFiles, this.currentFileChanges));
|
|
1108
|
+
if (this.lastClassified)
|
|
1109
|
+
await Promise.resolve(this.onStructuredComplete?.(this.lastClassified));
|
|
1075
1110
|
}
|
|
1076
1111
|
catch (error) {
|
|
1077
1112
|
logger_1.logger.error('[ResponseMonitor] complete callback failed:', error);
|
|
@@ -1083,7 +1118,14 @@ class ResponseMonitor {
|
|
|
1083
1118
|
// Guard: never timeout while the stop button is visible — it means
|
|
1084
1119
|
// Antigravity is still actively generating (extended thinking, long
|
|
1085
1120
|
// shell commands, large file operations, etc.).
|
|
1086
|
-
|
|
1121
|
+
// Hard timeout limit (20 minutes absolute hard timeout)
|
|
1122
|
+
const hardTimeoutLimit = 1200000;
|
|
1123
|
+
const totalElapsed = Date.now() - this.startTime;
|
|
1124
|
+
const isHardTimeout = totalElapsed >= hardTimeoutLimit;
|
|
1125
|
+
if (isHardTimeout || (this.maxDurationMs > 0 && !isGenerating && Date.now() - this.lastActivityTime >= this.maxDurationMs)) {
|
|
1126
|
+
if (isHardTimeout) {
|
|
1127
|
+
logger_1.logger.warn(`[ResponseMonitor] Hard timeout of 20 minutes reached.`);
|
|
1128
|
+
}
|
|
1087
1129
|
const lastText = this.lastText ?? '';
|
|
1088
1130
|
this.setPhase('timeout', lastText);
|
|
1089
1131
|
await this.stop();
|
|
@@ -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) {
|