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
|
@@ -3,26 +3,28 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.ApprovalDetector = void 0;
|
|
4
4
|
exports.buildClickScript = buildClickScript;
|
|
5
5
|
const logger_1 = require("../utils/logger");
|
|
6
|
+
const consecutiveEmptyPollGate_1 = require("../utils/consecutiveEmptyPollGate");
|
|
6
7
|
/**
|
|
7
8
|
* Approval button detection script for the Antigravity UI
|
|
8
9
|
*
|
|
9
10
|
* Detects allow/deny button pairs and extracts descriptions with fallbacks.
|
|
10
11
|
*/
|
|
11
12
|
const DETECT_APPROVAL_SCRIPT = `(() => {
|
|
12
|
-
const ALLOW_ONCE_PATTERNS = ['allow once', 'allow one time', '今回のみ許可', '1回のみ許可', '一度許可'];
|
|
13
|
+
const ALLOW_ONCE_PATTERNS = ['allow once', 'allow one time', 'yes, allow this time', '今回のみ許可', '1回のみ許可', '一度許可'];
|
|
13
14
|
const ALWAYS_ALLOW_PATTERNS = [
|
|
14
15
|
'allow this conversation',
|
|
15
16
|
'allow this chat',
|
|
16
17
|
'always allow',
|
|
18
|
+
'yes, and always allow',
|
|
17
19
|
'常に許可',
|
|
18
20
|
'この会話を許可',
|
|
19
21
|
];
|
|
20
|
-
const ALLOW_PATTERNS = ['allow', 'permit', '許可', '承認', '確認'];
|
|
21
|
-
const DENY_PATTERNS = ['deny', '拒否', 'decline'];
|
|
22
|
+
const ALLOW_PATTERNS = ['allow', 'permit', 'accept', 'approve', '許可', '承認', '確認'];
|
|
23
|
+
const DENY_PATTERNS = ['deny', 'reject', '拒否', 'decline', 'no (tell the agent'];
|
|
22
24
|
|
|
23
25
|
const normalize = (text) => (text || '').toLowerCase().replace(/\\s+/g, ' ').trim();
|
|
24
26
|
|
|
25
|
-
const allButtons = Array.from(document.querySelectorAll('button'))
|
|
27
|
+
const allButtons = Array.from(document.querySelectorAll('button, [role="button"], span.cursor-pointer, div.cursor-pointer'))
|
|
26
28
|
.filter(btn => btn.offsetParent !== null);
|
|
27
29
|
|
|
28
30
|
let approveBtn = allButtons.find(btn => {
|
|
@@ -45,7 +47,7 @@ const DETECT_APPROVAL_SCRIPT = `(() => {
|
|
|
45
47
|
|| approveBtn.parentElement
|
|
46
48
|
|| document.body;
|
|
47
49
|
|
|
48
|
-
const containerButtons = Array.from(container.querySelectorAll('button'))
|
|
50
|
+
const containerButtons = Array.from(container.querySelectorAll('button, [role="button"], span.cursor-pointer, div.cursor-pointer'))
|
|
49
51
|
.filter(btn => btn.offsetParent !== null);
|
|
50
52
|
|
|
51
53
|
const denyBtn = containerButtons.find(btn => {
|
|
@@ -78,14 +80,48 @@ const DETECT_APPROVAL_SCRIPT = `(() => {
|
|
|
78
80
|
|
|
79
81
|
// 2. Parent element text (excluding button text)
|
|
80
82
|
if (!description) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
83
|
+
let modal = approveBtn.closest('.notify-user-container, [role="dialog"], .modal, .dialog, .approval-container, .permission-dialog');
|
|
84
|
+
|
|
85
|
+
if (!modal) {
|
|
86
|
+
// New Antigravity IDE uses a sticky footer with no identifying modal classes.
|
|
87
|
+
// Traverse up until we find a container that includes the file list popup (.bottom-full)
|
|
88
|
+
let p = approveBtn.parentElement;
|
|
89
|
+
while (p && p.tagName !== 'BODY') {
|
|
90
|
+
if (p.querySelector('.bottom-full')) {
|
|
91
|
+
modal = p;
|
|
92
|
+
break;
|
|
93
|
+
}
|
|
94
|
+
p = p.parentElement;
|
|
95
|
+
}
|
|
96
|
+
if (!modal) {
|
|
97
|
+
modal = approveBtn.parentElement?.parentElement?.parentElement || approveBtn.parentElement?.parentElement;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (modal) {
|
|
102
|
+
const parts = [];
|
|
103
|
+
const walk = (node) => {
|
|
104
|
+
if (node.nodeType === 1) {
|
|
105
|
+
// Skip buttons entirely
|
|
106
|
+
if (node.tagName === 'BUTTON' || node.getAttribute('role') === 'button' || node.classList.contains('cursor-pointer')) return;
|
|
107
|
+
|
|
108
|
+
const display = window.getComputedStyle(node).display;
|
|
109
|
+
if (display === 'none') return;
|
|
110
|
+
|
|
111
|
+
const isBlock = display === 'block' || display === 'flex' || node.tagName === 'DIV' || node.tagName === 'LI';
|
|
112
|
+
if (isBlock && parts.length > 0 && parts[parts.length - 1] !== '\\n') parts.push('\\n');
|
|
113
|
+
for (const child of node.childNodes) walk(child);
|
|
114
|
+
if (isBlock && parts.length > 0 && parts[parts.length - 1] !== '\\n') parts.push('\\n');
|
|
115
|
+
} else if (node.nodeType === 3) {
|
|
116
|
+
const t = node.textContent || '';
|
|
117
|
+
if (t.trim()) parts.push(t.trim());
|
|
118
|
+
}
|
|
119
|
+
};
|
|
120
|
+
walk(modal);
|
|
121
|
+
|
|
122
|
+
const parentText = parts.join(' ').replace(/\\n\\s*/g, '\\n').replace(/\\n{3,}/g, '\\n\\n').trim();
|
|
123
|
+
if (parentText.length > 5) {
|
|
124
|
+
description = parentText.length > 2000 ? parentText.substring(0, 2000) + '...\\n(truncated)' : parentText;
|
|
89
125
|
}
|
|
90
126
|
}
|
|
91
127
|
}
|
|
@@ -102,11 +138,12 @@ const DETECT_APPROVAL_SCRIPT = `(() => {
|
|
|
102
138
|
* Press the toggle on the right side of Allow Once to expand the Always Allow dropdown.
|
|
103
139
|
*/
|
|
104
140
|
const EXPAND_ALWAYS_ALLOW_MENU_SCRIPT = `(() => {
|
|
105
|
-
const ALLOW_ONCE_PATTERNS = ['allow once', 'allow one time', '今回のみ許可', '1回のみ許可', '一度許可'];
|
|
141
|
+
const ALLOW_ONCE_PATTERNS = ['allow once', 'allow one time', 'yes, allow this time', '今回のみ許可', '1回のみ許可', '一度許可'];
|
|
106
142
|
const ALWAYS_ALLOW_PATTERNS = [
|
|
107
143
|
'allow this conversation',
|
|
108
144
|
'allow this chat',
|
|
109
145
|
'always allow',
|
|
146
|
+
'yes, and always allow',
|
|
110
147
|
'常に許可',
|
|
111
148
|
'この会話を許可',
|
|
112
149
|
];
|
|
@@ -181,17 +218,19 @@ function buildClickScript(buttonText) {
|
|
|
181
218
|
const normalize = (text) => (text || '').toLowerCase().replace(/\\s+/g, ' ').trim();
|
|
182
219
|
const text = ${safeText};
|
|
183
220
|
const wanted = normalize(text);
|
|
184
|
-
const allButtons = Array.from(document.querySelectorAll('button'));
|
|
221
|
+
const allButtons = Array.from(document.querySelectorAll('button, [role="button"], a, [class*="btn"], [class*="button"], [class*="action"]')).reverse();
|
|
185
222
|
const target = allButtons.find(btn => {
|
|
186
|
-
|
|
223
|
+
const style = window.getComputedStyle(btn);
|
|
224
|
+
if (style.display === 'none' || style.visibility === 'hidden' || btn.disabled) return false;
|
|
187
225
|
const buttonText = normalize(btn.textContent || '');
|
|
188
226
|
const ariaLabel = normalize(btn.getAttribute('aria-label') || '');
|
|
189
227
|
return buttonText === wanted ||
|
|
190
228
|
ariaLabel === wanted ||
|
|
191
|
-
buttonText.includes(wanted) ||
|
|
192
|
-
ariaLabel.includes(wanted);
|
|
229
|
+
(buttonText.includes(wanted) && buttonText.length < wanted.length + 10) ||
|
|
230
|
+
(ariaLabel.includes(wanted) && ariaLabel.length < wanted.length + 10);
|
|
193
231
|
});
|
|
194
232
|
if (!target) return { ok: false, error: 'Button not found: ' + text };
|
|
233
|
+
target.scrollIntoView({ block: 'center' });
|
|
195
234
|
const rect = target.getBoundingClientRect();
|
|
196
235
|
const x = rect.left + rect.width / 2;
|
|
197
236
|
const y = rect.top + rect.height / 2;
|
|
@@ -199,6 +238,7 @@ function buildClickScript(buttonText) {
|
|
|
199
238
|
for (const type of ['pointerdown', 'mousedown', 'pointerup', 'mouseup', 'click']) {
|
|
200
239
|
target.dispatchEvent(new PointerEvent(type, { ...eventInit, pointerId: 1 }));
|
|
201
240
|
}
|
|
241
|
+
if (typeof target.click === 'function') target.click();
|
|
202
242
|
return { ok: true };
|
|
203
243
|
})()`;
|
|
204
244
|
}
|
|
@@ -219,6 +259,8 @@ class ApprovalDetector {
|
|
|
219
259
|
lastDetectedKey = null;
|
|
220
260
|
/** Full ApprovalInfo from the last detection (used for clicking) */
|
|
221
261
|
lastDetectedInfo = null;
|
|
262
|
+
/** Gate for empty polls before reset */
|
|
263
|
+
emptyPollGate = new consecutiveEmptyPollGate_1.ConsecutiveEmptyPollGate(3);
|
|
222
264
|
constructor(options) {
|
|
223
265
|
this.cdpService = options.cdpService;
|
|
224
266
|
this.pollIntervalMs = options.pollIntervalMs ?? 1500;
|
|
@@ -234,6 +276,7 @@ class ApprovalDetector {
|
|
|
234
276
|
this.isRunning = true;
|
|
235
277
|
this.lastDetectedKey = null;
|
|
236
278
|
this.lastDetectedInfo = null;
|
|
279
|
+
this.emptyPollGate.reset();
|
|
237
280
|
this.schedulePoll();
|
|
238
281
|
}
|
|
239
282
|
/**
|
|
@@ -284,6 +327,7 @@ class ApprovalDetector {
|
|
|
284
327
|
const result = await this.cdpService.call('Runtime.evaluate', callParams);
|
|
285
328
|
const info = result?.result?.value ?? null;
|
|
286
329
|
if (info) {
|
|
330
|
+
this.emptyPollGate.recordDetection();
|
|
287
331
|
// Duplicate prevention: use approveText + description combination as key
|
|
288
332
|
const key = `${info.approveText}::${info.description}`;
|
|
289
333
|
if (key !== this.lastDetectedKey) {
|
|
@@ -293,12 +337,14 @@ class ApprovalDetector {
|
|
|
293
337
|
}
|
|
294
338
|
}
|
|
295
339
|
else {
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
this.onResolved
|
|
340
|
+
if (this.emptyPollGate.recordEmptyPoll()) {
|
|
341
|
+
// Reset when buttons disappear for consecutive polls
|
|
342
|
+
const wasDetected = this.lastDetectedKey !== null;
|
|
343
|
+
this.lastDetectedKey = null;
|
|
344
|
+
this.lastDetectedInfo = null;
|
|
345
|
+
if (wasDetected && this.onResolved) {
|
|
346
|
+
this.onResolved();
|
|
347
|
+
}
|
|
302
348
|
}
|
|
303
349
|
}
|
|
304
350
|
}
|
|
@@ -148,11 +148,12 @@ class ArtifactService {
|
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
150
|
/**
|
|
151
|
-
* Try to find a conversation UUID whose overview
|
|
151
|
+
* Try to find a conversation UUID whose transcript or overview contains the given session title.
|
|
152
152
|
* Uses an exact match first, falling back to keyword overlap scoring.
|
|
153
|
+
* If workspaceFilter is provided, restricts matching exclusively to conversations belonging to that workspace.
|
|
153
154
|
* Returns the UUID or null if not found.
|
|
154
155
|
*/
|
|
155
|
-
findConversationByTitle(title) {
|
|
156
|
+
findConversationByTitle(title, workspaceFilter) {
|
|
156
157
|
if (!title || !title.trim())
|
|
157
158
|
return null;
|
|
158
159
|
const needle = title.trim().toLowerCase();
|
|
@@ -186,40 +187,58 @@ class ArtifactService {
|
|
|
186
187
|
const uniqueNeedleWords = Array.from(new Set(cleanNeedleWords));
|
|
187
188
|
// Require a stronger minimum score of 2 to prevent weak one-word matches.
|
|
188
189
|
const minScore = 2;
|
|
190
|
+
const filterStr = workspaceFilter ? workspaceFilter.toLowerCase() : null;
|
|
189
191
|
for (const id of sortedIds) {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
// Only read the first 4KB to avoid huge files
|
|
195
|
-
let fd = null;
|
|
192
|
+
let score = 0;
|
|
193
|
+
let exactMatch = false;
|
|
194
|
+
let belongsToWorkspace = !filterStr; // True if no filter, or if we prove it belongs
|
|
195
|
+
const scanFile = (filePath, readSize) => {
|
|
196
196
|
try {
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
if (
|
|
209
|
-
|
|
197
|
+
if (!fs.existsSync(filePath))
|
|
198
|
+
return;
|
|
199
|
+
let fd = null;
|
|
200
|
+
try {
|
|
201
|
+
fd = fs.openSync(filePath, 'r');
|
|
202
|
+
const buf = Buffer.alloc(readSize);
|
|
203
|
+
const bytesRead = fs.readSync(fd, buf, 0, buf.length, 0);
|
|
204
|
+
const content = buf.slice(0, bytesRead).toString('utf-8').toLowerCase();
|
|
205
|
+
if (filterStr && content.includes(filterStr)) {
|
|
206
|
+
belongsToWorkspace = true;
|
|
207
|
+
}
|
|
208
|
+
if (belongsToWorkspace) {
|
|
209
|
+
if (content.includes(needle)) {
|
|
210
|
+
exactMatch = true;
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
const contentTokens = new Set(content.replace(/[^\p{L}\p{N}]+/gu, ' ').split(/\s+/));
|
|
214
|
+
let currentScore = 0;
|
|
215
|
+
for (const word of uniqueNeedleWords) {
|
|
216
|
+
if (contentTokens.has(word))
|
|
217
|
+
currentScore++;
|
|
218
|
+
}
|
|
219
|
+
if (currentScore > score)
|
|
220
|
+
score = currentScore;
|
|
221
|
+
}
|
|
222
|
+
}
|
|
210
223
|
}
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
224
|
+
finally {
|
|
225
|
+
if (fd !== null)
|
|
226
|
+
fs.closeSync(fd);
|
|
214
227
|
}
|
|
215
228
|
}
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
229
|
+
catch { /* ignore */ }
|
|
230
|
+
};
|
|
231
|
+
// 1. Check transcript.jsonl (modern)
|
|
232
|
+
scanFile(path.join(this.brainBasePath, id, '.system_generated', 'logs', 'transcript.jsonl'), 16384);
|
|
233
|
+
// 2. Check overview.txt (legacy)
|
|
234
|
+
scanFile(path.join(this.brainBasePath, id, '.system_generated', 'logs', 'overview.txt'), 4096);
|
|
235
|
+
if (!belongsToWorkspace)
|
|
236
|
+
continue;
|
|
237
|
+
if (exactMatch)
|
|
238
|
+
return id; // Exact match in the right workspace takes precedence immediately
|
|
239
|
+
if (score > bestScore) {
|
|
240
|
+
bestScore = score;
|
|
241
|
+
bestId = id;
|
|
223
242
|
}
|
|
224
243
|
}
|
|
225
244
|
if (bestScore >= minScore) {
|
|
@@ -256,6 +275,13 @@ class ArtifactService {
|
|
|
256
275
|
// Nothing has artifacts — return most recent anyway (caller handles empty list)
|
|
257
276
|
return sorted[0]?.id ?? null;
|
|
258
277
|
}
|
|
278
|
+
/**
|
|
279
|
+
* Build the filesystem path for a specific artifact file.
|
|
280
|
+
*/
|
|
281
|
+
getArtifactPath(conversationId, filename) {
|
|
282
|
+
const safe = path.basename(filename);
|
|
283
|
+
return path.join(this.brainBasePath, conversationId, safe);
|
|
284
|
+
}
|
|
259
285
|
/**
|
|
260
286
|
* Read the markdown content of a specific artifact file.
|
|
261
287
|
* Returns null if the file cannot be read.
|
|
@@ -27,6 +27,12 @@ function classifyAssistantSegments(payload) {
|
|
|
27
27
|
finalOutputText: '',
|
|
28
28
|
activityLines: [],
|
|
29
29
|
feedback: [],
|
|
30
|
+
planCards: [],
|
|
31
|
+
actionButtons: [],
|
|
32
|
+
fileChanges: [],
|
|
33
|
+
citations: [],
|
|
34
|
+
fileChangesTexts: [],
|
|
35
|
+
citedFiles: [],
|
|
30
36
|
diagnostics: {
|
|
31
37
|
source: 'legacy-fallback',
|
|
32
38
|
segmentCounts: {},
|
|
@@ -41,8 +47,13 @@ function classifyAssistantSegments(payload) {
|
|
|
41
47
|
const bodyTexts = [];
|
|
42
48
|
const activityLines = [];
|
|
43
49
|
const feedbackTexts = [];
|
|
50
|
+
const planCards = [];
|
|
51
|
+
const actionButtons = [];
|
|
52
|
+
const fileChanges = [];
|
|
53
|
+
const citations = [];
|
|
44
54
|
const segmentCounts = {};
|
|
45
55
|
const allFingerprints = [];
|
|
56
|
+
const fileChangesTexts = [];
|
|
46
57
|
for (const seg of segments) {
|
|
47
58
|
segmentCounts[seg.kind] = (segmentCounts[seg.kind] ?? 0) + 1;
|
|
48
59
|
switch (seg.kind) {
|
|
@@ -65,16 +76,68 @@ function classifyAssistantSegments(payload) {
|
|
|
65
76
|
feedbackTexts.push(seg.text.trim());
|
|
66
77
|
}
|
|
67
78
|
break;
|
|
79
|
+
case 'plan-card':
|
|
80
|
+
if (seg.text && seg.text.trim()) {
|
|
81
|
+
planCards.push(seg.text.trim());
|
|
82
|
+
}
|
|
83
|
+
break;
|
|
84
|
+
case 'action-button':
|
|
85
|
+
if (seg.text && seg.text.trim()) {
|
|
86
|
+
actionButtons.push(seg.text.trim());
|
|
87
|
+
}
|
|
88
|
+
break;
|
|
89
|
+
case 'file-change':
|
|
90
|
+
if (seg.text && seg.text.trim()) {
|
|
91
|
+
const parts = seg.text.split('|');
|
|
92
|
+
fileChanges.push({ path: parts[0]?.trim() || '', type: parts[1]?.trim() || '' });
|
|
93
|
+
}
|
|
94
|
+
break;
|
|
95
|
+
case 'citation':
|
|
96
|
+
if (seg.text && seg.text.trim()) {
|
|
97
|
+
citations.push(seg.text.trim());
|
|
98
|
+
}
|
|
99
|
+
break;
|
|
100
|
+
case 'file-changes':
|
|
101
|
+
if (seg.text && seg.text.trim()) {
|
|
102
|
+
fileChangesTexts.push(seg.text.trim());
|
|
103
|
+
}
|
|
104
|
+
break;
|
|
68
105
|
}
|
|
69
106
|
}
|
|
70
107
|
// Concatenate all body segments — the CDP script may yield multiple nodes
|
|
71
108
|
// (streaming partial + final, multi-block markdown, etc.)
|
|
72
109
|
const lastBody = bodyTexts.join('\n\n') || '';
|
|
73
110
|
const finalOutputText = (0, htmlToDiscordMarkdown_1.htmlToDiscordMarkdown)(lastBody);
|
|
111
|
+
// Extract file:/// links from finalOutputText and preserve citations from Pass 4
|
|
112
|
+
const citedFiles = Array.from(new Set([...citations]));
|
|
113
|
+
const fileRegex = /\[.*?\]\((file:\/\/\/[^\)]+)\)/g;
|
|
114
|
+
let match;
|
|
115
|
+
while ((match = fileRegex.exec(finalOutputText)) !== null) {
|
|
116
|
+
const fileUrl = match[1];
|
|
117
|
+
if (!citedFiles.includes(fileUrl)) {
|
|
118
|
+
citedFiles.push(fileUrl);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// Also extract backticked inline code that looks like a filename with common extensions
|
|
122
|
+
// e.g., `implementation_plan.md`
|
|
123
|
+
const inlineCodeRegex = /`([a-zA-Z0-9_\-\.]+\.(?:md|ts|js|json|html|css|txt|csv|py|java|go|cpp|c))`[^`]?/g;
|
|
124
|
+
let inlineMatch;
|
|
125
|
+
while ((inlineMatch = inlineCodeRegex.exec(finalOutputText)) !== null) {
|
|
126
|
+
const filename = inlineMatch[1];
|
|
127
|
+
if (!citedFiles.includes(filename)) {
|
|
128
|
+
citedFiles.push(filename);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
74
131
|
return {
|
|
75
132
|
finalOutputText,
|
|
76
133
|
activityLines,
|
|
77
134
|
feedback: feedbackTexts,
|
|
135
|
+
planCards,
|
|
136
|
+
actionButtons,
|
|
137
|
+
fileChanges,
|
|
138
|
+
citations,
|
|
139
|
+
fileChangesTexts,
|
|
140
|
+
citedFiles,
|
|
78
141
|
diagnostics: {
|
|
79
142
|
source: 'dom-structured',
|
|
80
143
|
segmentCounts,
|
|
@@ -145,10 +208,17 @@ function extractAssistantSegmentsPayloadScript() {
|
|
|
145
208
|
if (node.closest('details')) return true;
|
|
146
209
|
if (node.closest('[class*="feedback"], footer')) return true;
|
|
147
210
|
if (node.closest('.notify-user-container')) return true;
|
|
148
|
-
|
|
149
|
-
|
|
211
|
+
// Guard against approval dialogs leaking into the output (they contain plan cards or review buttons)
|
|
212
|
+
var dialogNode = node.closest('dialog, [role="dialog"], form');
|
|
213
|
+
if (dialogNode && dialogNode.querySelector('[data-testid="plan-card"], [class*="plan-summary"], .actions-container, .review-button')) {
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// allow interactive tool forms (ask_question, ask_permission)
|
|
150
218
|
if (node.closest('[data-message-author-role="user"], [data-message-role="user"]')) return true;
|
|
151
|
-
|
|
219
|
+
// We do not exclude nodes just because they contain a textarea (e.g. ask_question "Other" option).
|
|
220
|
+
// The main chat input is excluded by the "ask anything" check below.
|
|
221
|
+
if (node.tagName && node.tagName.toLowerCase() === 'textarea') return true;
|
|
152
222
|
var text = (node.innerText || '').toLowerCase();
|
|
153
223
|
if (text.includes('ask anything, @ to mention')) return true;
|
|
154
224
|
if (text.includes('0 files with changes')) return true;
|
|
@@ -158,6 +228,7 @@ function extractAssistantSegmentsPayloadScript() {
|
|
|
158
228
|
var segments = [];
|
|
159
229
|
var seen = new Set();
|
|
160
230
|
var bodyFound = false;
|
|
231
|
+
var latestMessageContainer = null;
|
|
161
232
|
|
|
162
233
|
// Pass 1: Find assistant body — last non-excluded content node (recency first)
|
|
163
234
|
var combinedSelector = selectors.join(', ');
|
|
@@ -238,6 +309,7 @@ function extractAssistantSegmentsPayloadScript() {
|
|
|
238
309
|
domPath: 'article:nth(' + i + ')'
|
|
239
310
|
});
|
|
240
311
|
bodyFound = true;
|
|
312
|
+
latestMessageContainer = node.closest('[data-message-id], .message-row, [class*="message-container"]') || node.parentNode || scope;
|
|
241
313
|
break; // Only capture the single latest assistant message
|
|
242
314
|
}
|
|
243
315
|
}
|
|
@@ -362,6 +434,105 @@ function extractAssistantSegmentsPayloadScript() {
|
|
|
362
434
|
}
|
|
363
435
|
}
|
|
364
436
|
|
|
437
|
+
// Pass 4: Extract Antigravity 2.0 structured cards and artifacts
|
|
438
|
+
var artifactScope = latestMessageContainer || scope;
|
|
439
|
+
// 4a. Plan Cards
|
|
440
|
+
var planNodes = artifactScope.querySelectorAll('[data-testid="plan-card"], [class*="plan-summary"]');
|
|
441
|
+
for (var pi2 = 0; pi2 < planNodes.length; pi2++) {
|
|
442
|
+
var pnode = planNodes[pi2];
|
|
443
|
+
var ptext = (pnode.innerText || pnode.textContent || '').trim();
|
|
444
|
+
if (ptext) {
|
|
445
|
+
segments.push({
|
|
446
|
+
kind: 'plan-card',
|
|
447
|
+
text: ptext,
|
|
448
|
+
role: 'assistant',
|
|
449
|
+
messageIndex: 0,
|
|
450
|
+
domPath: 'plan-card:nth(' + pi2 + ')'
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
// 4b. Action Buttons (Open, Proceed, Review, etc.) - outside feedback footers
|
|
456
|
+
var actionBtns = artifactScope.querySelectorAll('.actions-container button, [class*="action-btn"], .review-button, [class*="review-btn"]');
|
|
457
|
+
for (var abi = 0; abi < actionBtns.length; abi++) {
|
|
458
|
+
var abtnText = (actionBtns[abi].textContent || '').trim();
|
|
459
|
+
if (!abtnText) continue;
|
|
460
|
+
var lower = abtnText.toLowerCase();
|
|
461
|
+
// Ignore approval buttons since they are handled by approvalDetector
|
|
462
|
+
if (lower.includes('accept all') || lower.includes('reject all')) continue;
|
|
463
|
+
|
|
464
|
+
segments.push({
|
|
465
|
+
kind: 'action-button',
|
|
466
|
+
text: abtnText,
|
|
467
|
+
role: 'assistant',
|
|
468
|
+
messageIndex: 0,
|
|
469
|
+
domPath: 'action-btn.' + abtnText.toLowerCase().replace(/\s+/g, '-')
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// 4c. File Edits
|
|
474
|
+
var fileEdits = artifactScope.querySelectorAll('[class*="file-edit-item"], .file-edit-item, .bottom-full .cursor-pointer');
|
|
475
|
+
for (var fei = 0; fei < fileEdits.length; fei++) {
|
|
476
|
+
var editNode = fileEdits[fei];
|
|
477
|
+
if (seen.has(editNode)) continue;
|
|
478
|
+
seen.add(editNode);
|
|
479
|
+
var fpath = (editNode.querySelector('.file-path, [class*="path"]') || {}).textContent || '';
|
|
480
|
+
var ftype = (editNode.querySelector('.edit-type, [class*="type"]') || {}).textContent || 'Modified';
|
|
481
|
+
|
|
482
|
+
if (!fpath) {
|
|
483
|
+
var spans = editNode.querySelectorAll('span.whitespace-nowrap.text-sm');
|
|
484
|
+
if (spans.length > 0) {
|
|
485
|
+
fpath = spans[0].textContent || '';
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
if (!fpath) {
|
|
489
|
+
fpath = (editNode.textContent || '').trim();
|
|
490
|
+
if (fpath.endsWith(ftype)) {
|
|
491
|
+
fpath = fpath.slice(0, -ftype.length).trim();
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
if (fpath) {
|
|
495
|
+
segments.push({
|
|
496
|
+
kind: 'file-change',
|
|
497
|
+
text: fpath.trim() + '|' + ftype.trim(),
|
|
498
|
+
role: 'assistant',
|
|
499
|
+
messageIndex: 0,
|
|
500
|
+
domPath: 'file-edit:nth(' + fei + ')'
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// 4d. Citations
|
|
506
|
+
var citationNodes = artifactScope.querySelectorAll('[class*="citation"], a[href^="file://"]');
|
|
507
|
+
for (var cti = 0; cti < citationNodes.length; cti++) {
|
|
508
|
+
var cnode = citationNodes[cti];
|
|
509
|
+
var ctext = (cnode.getAttribute('href') || cnode.textContent || '').trim();
|
|
510
|
+
if (ctext) {
|
|
511
|
+
segments.push({
|
|
512
|
+
kind: 'citation',
|
|
513
|
+
text: ctext,
|
|
514
|
+
role: 'assistant',
|
|
515
|
+
messageIndex: 0,
|
|
516
|
+
domPath: 'citation:nth(' + cti + ')'
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
// 4e. File Changes Text Blocks
|
|
521
|
+
var fcbNodes = artifactScope.querySelectorAll('.file-changes-block code, [class*="file-changes"] code');
|
|
522
|
+
for (var fci = 0; fci < fcbNodes.length; fci++) {
|
|
523
|
+
var fcNode = fcbNodes[fci];
|
|
524
|
+
var fcText = (fcNode.textContent || '').trim();
|
|
525
|
+
if (fcText) {
|
|
526
|
+
segments.push({
|
|
527
|
+
kind: 'file-changes',
|
|
528
|
+
text: fcText,
|
|
529
|
+
role: 'assistant',
|
|
530
|
+
messageIndex: 0,
|
|
531
|
+
domPath: 'file-changes:nth(' + fci + ')'
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
|
|
365
536
|
if (!bodyFound && segments.length === 0) return null;
|
|
366
537
|
|
|
367
538
|
return {
|