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
@@ -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
- if (this.maxDurationMs > 0 && !isGenerating && Date.now() - this.lastActivityTime >= this.maxDurationMs) {
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 (used for clicking) */
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
- const wasDetected = this.lastDetectedKey !== null;
184
- this.lastDetectedKey = null;
185
- this.lastDetectedInfo = null;
186
- if (wasDetected && this.onResolved) {
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
  }
@@ -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 matchedId = sessionTitle ? artifactService.findConversationByTitle(sessionTitle) : null;
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
- // Final fallback: latest overall (only for non-session channels)
155
- conversationId = artifactService.getLatestConversationWithArtifacts();
156
+ conversationId = null;
156
157
  }
157
158
  }
158
159
  if (!conversationId) {