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
|
@@ -102,6 +102,12 @@ const WORKSPACE_STATE_SCRIPT = `(() => {
|
|
|
102
102
|
if (isGenerating) break;
|
|
103
103
|
}
|
|
104
104
|
}
|
|
105
|
+
if (!isGenerating && panel) {
|
|
106
|
+
const panelText = (panel.textContent || '').trim();
|
|
107
|
+
if (/Working\.\s*$/i.test(panelText)) {
|
|
108
|
+
isGenerating = true;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
105
111
|
|
|
106
112
|
if (!panel) {
|
|
107
113
|
return { isGenerating, sessionTitle: '', hasActiveChat: false };
|
|
@@ -407,6 +413,9 @@ class CdpService extends events_1.EventEmitter {
|
|
|
407
413
|
getCurrentWorkspaceName() {
|
|
408
414
|
return this.currentWorkspaceName;
|
|
409
415
|
}
|
|
416
|
+
getCurrentWorkspacePath() {
|
|
417
|
+
return this.currentWorkspacePath;
|
|
418
|
+
}
|
|
410
419
|
/**
|
|
411
420
|
* Discover and connect to the workbench page for the specified workspace.
|
|
412
421
|
* Does nothing if already connected to the correct page.
|
|
@@ -459,7 +468,8 @@ class CdpService extends events_1.EventEmitter {
|
|
|
459
468
|
returnByValue: true,
|
|
460
469
|
});
|
|
461
470
|
const liveTitle = String(titleResult?.result?.value || '');
|
|
462
|
-
|
|
471
|
+
const titleParts = liveTitle.split(' - ');
|
|
472
|
+
if (titleParts[0].trim().toLowerCase() === projectName.toLowerCase()) {
|
|
463
473
|
this.currentWorkspaceName = projectName;
|
|
464
474
|
return true;
|
|
465
475
|
}
|
|
@@ -506,7 +516,12 @@ class CdpService extends events_1.EventEmitter {
|
|
|
506
516
|
logger_1.logger.debug(` - title="${p.title}" url=${p.url}`);
|
|
507
517
|
}
|
|
508
518
|
// 1. Title match (fast path)
|
|
509
|
-
const titleMatch = workbenchPages.find((t) =>
|
|
519
|
+
const titleMatch = workbenchPages.find((t) => {
|
|
520
|
+
if (!t.title)
|
|
521
|
+
return false;
|
|
522
|
+
const parts = t.title.split(' - ');
|
|
523
|
+
return parts[0].trim().toLowerCase() === projectName.toLowerCase();
|
|
524
|
+
});
|
|
510
525
|
if (titleMatch) {
|
|
511
526
|
return this.connectToPage(titleMatch, projectName);
|
|
512
527
|
}
|
|
@@ -559,9 +574,8 @@ class CdpService extends events_1.EventEmitter {
|
|
|
559
574
|
returnByValue: true,
|
|
560
575
|
});
|
|
561
576
|
const liveTitle = String(result?.result?.value || '');
|
|
562
|
-
const
|
|
563
|
-
|
|
564
|
-
if (normalizedLiveTitle.includes(normalizedProject)) {
|
|
577
|
+
const liveParts = liveTitle.split(' - ');
|
|
578
|
+
if (liveParts[0].trim().toLowerCase() === projectName.toLowerCase()) {
|
|
565
579
|
this.currentWorkspaceName = projectName;
|
|
566
580
|
logger_1.logger.debug(`[CdpService] Probe success: detected "${projectName}"`);
|
|
567
581
|
return true;
|
|
@@ -628,9 +642,11 @@ class CdpService extends events_1.EventEmitter {
|
|
|
628
642
|
const detectedValue = value.value;
|
|
629
643
|
const normalizedDetected = detectedValue.toLowerCase();
|
|
630
644
|
const normalizedProject = projectName.toLowerCase();
|
|
631
|
-
const normalizedWorkspace = workspacePath.toLowerCase();
|
|
632
|
-
|
|
633
|
-
|
|
645
|
+
const normalizedWorkspace = workspacePath.toLowerCase().replace(/\\/g, '/');
|
|
646
|
+
const detectedParts = normalizedDetected.split(',').map((s) => s.trim());
|
|
647
|
+
if (detectedParts.includes(normalizedProject) ||
|
|
648
|
+
normalizedDetected.includes(normalizedWorkspace) ||
|
|
649
|
+
normalizedDetected.replace(/\\/g, '/').includes(normalizedWorkspace)) {
|
|
634
650
|
this.currentWorkspaceName = projectName;
|
|
635
651
|
logger_1.logger.debug(`[CdpService] Folder path match success: "${projectName}"`);
|
|
636
652
|
return true;
|
|
@@ -641,11 +657,20 @@ class CdpService extends events_1.EventEmitter {
|
|
|
641
657
|
expression: 'window.location.href',
|
|
642
658
|
returnByValue: true,
|
|
643
659
|
});
|
|
644
|
-
const pageUrl = (urlResult?.result?.value || '').toLowerCase();
|
|
645
|
-
const normalizedWorkspaceUri =
|
|
646
|
-
if (pageUrl.includes(normalizedWorkspaceUri)
|
|
660
|
+
const pageUrl = decodeURIComponent(urlResult?.result?.value || '').toLowerCase().replace(/\\/g, '/');
|
|
661
|
+
const normalizedWorkspaceUri = workspacePath.toLowerCase().replace(/\\/g, '/');
|
|
662
|
+
if (pageUrl.includes(normalizedWorkspaceUri)) {
|
|
647
663
|
this.currentWorkspaceName = projectName;
|
|
648
|
-
logger_1.logger.debug(`[CdpService] URL parameter match success: "${projectName}"`);
|
|
664
|
+
logger_1.logger.debug(`[CdpService] URL parameter match success: "${projectName}" (workspace URI)`);
|
|
665
|
+
return true;
|
|
666
|
+
}
|
|
667
|
+
// If we only have the project name to go on, make sure it's a discrete boundary in the URL path
|
|
668
|
+
// e.g. "folder=.../test" or "workspace=.../test". We prevent false positives like "latest" matching "test".
|
|
669
|
+
const safeProjectName = projectName.toLowerCase().replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
670
|
+
const projectBoundaryRegex = new RegExp(`[=/]${safeProjectName}(?:$|&|\\?)`);
|
|
671
|
+
if (projectBoundaryRegex.test(pageUrl)) {
|
|
672
|
+
this.currentWorkspaceName = projectName;
|
|
673
|
+
logger_1.logger.debug(`[CdpService] URL parameter match success: "${projectName}" (boundary regex)`);
|
|
649
674
|
return true;
|
|
650
675
|
}
|
|
651
676
|
}
|
|
@@ -747,7 +772,12 @@ class CdpService extends events_1.EventEmitter {
|
|
|
747
772
|
!t.url?.includes('workbench-jetski-agent') &&
|
|
748
773
|
t.url?.includes('workbench'));
|
|
749
774
|
// Title match
|
|
750
|
-
const titleMatch = workbenchPages.find((t) =>
|
|
775
|
+
const titleMatch = workbenchPages.find((t) => {
|
|
776
|
+
if (!t.title)
|
|
777
|
+
return false;
|
|
778
|
+
const parts = t.title.split(' - ');
|
|
779
|
+
return parts[0].trim().toLowerCase() === projectName.toLowerCase();
|
|
780
|
+
});
|
|
751
781
|
if (titleMatch) {
|
|
752
782
|
return this.connectToPage(titleMatch, projectName);
|
|
753
783
|
}
|
|
@@ -1276,9 +1306,15 @@ class CdpService extends events_1.EventEmitter {
|
|
|
1276
1306
|
].some((fragment) => message.includes(fragment));
|
|
1277
1307
|
}
|
|
1278
1308
|
async injectMessageCore(text, imageFilePaths) {
|
|
1279
|
-
|
|
1309
|
+
// Check if chat input is already available
|
|
1310
|
+
let focusResult = await this.waitForChatInputReady(500);
|
|
1280
1311
|
if (!focusResult.ok) {
|
|
1281
|
-
|
|
1312
|
+
// Send Cmd+L / Ctrl+L to guarantee application-level focus on the chat panel (and open it if closed)
|
|
1313
|
+
await this.focusChatPanelViaShortcut();
|
|
1314
|
+
focusResult = await this.waitForChatInputReady();
|
|
1315
|
+
if (!focusResult.ok) {
|
|
1316
|
+
return { ok: false, error: focusResult.error || 'Chat input field not found' };
|
|
1317
|
+
}
|
|
1282
1318
|
}
|
|
1283
1319
|
// Clear any existing text in the input field before injecting.
|
|
1284
1320
|
await this.clearInputField();
|
|
@@ -1377,6 +1413,89 @@ class CdpService extends events_1.EventEmitter {
|
|
|
1377
1413
|
nativeVirtualKeyCode: 13,
|
|
1378
1414
|
});
|
|
1379
1415
|
}
|
|
1416
|
+
/**
|
|
1417
|
+
* Opens the chat panel and waits for the UI to be ready.
|
|
1418
|
+
* Can be called right after the IDE launches to ensure the panel is visible.
|
|
1419
|
+
*/
|
|
1420
|
+
async openChatPanel() {
|
|
1421
|
+
if (!await this.isConnected())
|
|
1422
|
+
return;
|
|
1423
|
+
// Ensure IDE UI is fully loaded before sending shortcut
|
|
1424
|
+
const deadline = Date.now() + 15000;
|
|
1425
|
+
let workbenchReady = false;
|
|
1426
|
+
while (Date.now() < deadline) {
|
|
1427
|
+
try {
|
|
1428
|
+
const res = await this.call('Runtime.evaluate', {
|
|
1429
|
+
expression: '!!document.querySelector(".monaco-workbench")',
|
|
1430
|
+
returnByValue: true
|
|
1431
|
+
});
|
|
1432
|
+
if (res?.result?.value) {
|
|
1433
|
+
workbenchReady = true;
|
|
1434
|
+
break;
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
catch {
|
|
1438
|
+
// ignore
|
|
1439
|
+
}
|
|
1440
|
+
await new Promise(r => setTimeout(r, 500));
|
|
1441
|
+
}
|
|
1442
|
+
if (workbenchReady) {
|
|
1443
|
+
// Check if chat panel is already open to avoid toggling it closed
|
|
1444
|
+
const focusResult = await this.waitForChatInputReady(1000);
|
|
1445
|
+
if (!focusResult.ok) {
|
|
1446
|
+
await this.focusChatPanelViaShortcut();
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
1450
|
+
/**
|
|
1451
|
+
* Send Cmd+L / Ctrl+L to focus the chat panel via shortcut.
|
|
1452
|
+
*/
|
|
1453
|
+
async focusChatPanelViaShortcut() {
|
|
1454
|
+
const modifiers = process.platform === 'darwin' ? 4 : 2; // Meta : Ctrl
|
|
1455
|
+
await this.call('Input.dispatchKeyEvent', {
|
|
1456
|
+
type: 'keyDown',
|
|
1457
|
+
key: 'l',
|
|
1458
|
+
code: 'KeyL',
|
|
1459
|
+
modifiers,
|
|
1460
|
+
windowsVirtualKeyCode: 76,
|
|
1461
|
+
nativeVirtualKeyCode: 76,
|
|
1462
|
+
});
|
|
1463
|
+
await this.call('Input.dispatchKeyEvent', {
|
|
1464
|
+
type: 'keyUp',
|
|
1465
|
+
key: 'l',
|
|
1466
|
+
code: 'KeyL',
|
|
1467
|
+
modifiers,
|
|
1468
|
+
windowsVirtualKeyCode: 76,
|
|
1469
|
+
nativeVirtualKeyCode: 76,
|
|
1470
|
+
});
|
|
1471
|
+
await new Promise(r => setTimeout(r, 100)); // wait for focus to shift
|
|
1472
|
+
}
|
|
1473
|
+
/**
|
|
1474
|
+
* Send Cmd+W / Ctrl+W to close the active editor.
|
|
1475
|
+
*/
|
|
1476
|
+
async closeActiveEditor() {
|
|
1477
|
+
if (!this.isConnectedFlag || !this.ws) {
|
|
1478
|
+
throw new Error('Not connected to CDP. Call connect() first.');
|
|
1479
|
+
}
|
|
1480
|
+
const modifiers = process.platform === 'darwin' ? 4 : 2; // Meta : Ctrl
|
|
1481
|
+
await this.call('Input.dispatchKeyEvent', {
|
|
1482
|
+
type: 'keyDown',
|
|
1483
|
+
key: 'w',
|
|
1484
|
+
code: 'KeyW',
|
|
1485
|
+
modifiers,
|
|
1486
|
+
windowsVirtualKeyCode: 87,
|
|
1487
|
+
nativeVirtualKeyCode: 87,
|
|
1488
|
+
});
|
|
1489
|
+
await this.call('Input.dispatchKeyEvent', {
|
|
1490
|
+
type: 'keyUp',
|
|
1491
|
+
key: 'w',
|
|
1492
|
+
code: 'KeyW',
|
|
1493
|
+
modifiers,
|
|
1494
|
+
windowsVirtualKeyCode: 87,
|
|
1495
|
+
nativeVirtualKeyCode: 87,
|
|
1496
|
+
});
|
|
1497
|
+
await new Promise(r => setTimeout(r, 100));
|
|
1498
|
+
}
|
|
1380
1499
|
/**
|
|
1381
1500
|
* Detect file input in the UI and attach the specified files.
|
|
1382
1501
|
*/
|
|
@@ -3,11 +3,29 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.ChatSessionService = void 0;
|
|
4
4
|
exports.buildActivateViaPastConversationsScript = buildActivateViaPastConversationsScript;
|
|
5
5
|
const logger_1 = require("../utils/logger");
|
|
6
|
+
/** Shared fuzzy-matching logic for in-page scripts */
|
|
7
|
+
const FUZZY_MATCH_HELPERS_SCRIPT = `
|
|
8
|
+
const getWords = (str) => {
|
|
9
|
+
return (str || '').toLowerCase()
|
|
10
|
+
.replace(/[^a-z0-9\\u3040-\\u30ff\\u4e00-\\u9faf\\s]/g, '')
|
|
11
|
+
.split(/\\s+/)
|
|
12
|
+
.filter(w => w.length > 1 && !/^(ago|wks?|days?|mins?|hours?|hrs?|secs?|weeks?|months?|years?)$/i.test(w));
|
|
13
|
+
};
|
|
14
|
+
const wordsMatch = (t, p) => {
|
|
15
|
+
const tWords = getWords(t);
|
|
16
|
+
const pWords = getWords(p);
|
|
17
|
+
if (pWords.length === 0) return false;
|
|
18
|
+
return pWords.every(pw => tWords.some(tw => tw === pw));
|
|
19
|
+
};
|
|
20
|
+
`;
|
|
6
21
|
/** Script to get the state of the new chat button */
|
|
7
22
|
const GET_NEW_CHAT_BUTTON_SCRIPT = `(() => {
|
|
8
23
|
const btn = document.querySelector('[data-tooltip-id="new-conversation-tooltip"]');
|
|
9
24
|
if (!btn) return { found: false };
|
|
10
25
|
const cursor = window.getComputedStyle(btn).cursor;
|
|
26
|
+
if (typeof btn.scrollIntoView === 'function') {
|
|
27
|
+
btn.scrollIntoView({ block: 'center', inline: 'nearest' });
|
|
28
|
+
}
|
|
11
29
|
const rect = btn.getBoundingClientRect();
|
|
12
30
|
return {
|
|
13
31
|
found: true,
|
|
@@ -22,7 +40,7 @@ const GET_NEW_CHAT_BUTTON_SCRIPT = `(() => {
|
|
|
22
40
|
* The title element is a div with the text-ellipsis class inside the header.
|
|
23
41
|
*/
|
|
24
42
|
const GET_CHAT_TITLE_SCRIPT = `(() => {
|
|
25
|
-
const panel = document.querySelector('.antigravity-agent-side-panel');
|
|
43
|
+
const panel = document.querySelector('.antigravity-agent-side-panel') || document.body;
|
|
26
44
|
if (!panel) return { title: '', hasActiveChat: false };
|
|
27
45
|
const header = panel.querySelector('div[class*="border-b"]');
|
|
28
46
|
if (!header) return { title: '', hasActiveChat: false };
|
|
@@ -40,7 +58,7 @@ const GET_CHAT_TITLE_SCRIPT = `(() => {
|
|
|
40
58
|
return { title: title || '(Untitled)', hasActiveChat };
|
|
41
59
|
})()`;
|
|
42
60
|
const GET_SESSION_VIEW_STATE_SCRIPT = `(() => {
|
|
43
|
-
const panel = document.querySelector('.antigravity-agent-side-panel');
|
|
61
|
+
const panel = document.querySelector('.antigravity-agent-side-panel') || document.body;
|
|
44
62
|
if (!panel) {
|
|
45
63
|
return {
|
|
46
64
|
panelFound: false,
|
|
@@ -106,6 +124,9 @@ const GET_SESSION_VIEW_STATE_SCRIPT = `(() => {
|
|
|
106
124
|
const FIND_PAST_CONVERSATIONS_BUTTON_SCRIPT = `(() => {
|
|
107
125
|
const isVisible = (el) => !!el && el instanceof HTMLElement && el.offsetParent !== null;
|
|
108
126
|
const getRect = (el) => {
|
|
127
|
+
if (typeof el.scrollIntoView === 'function') {
|
|
128
|
+
el.scrollIntoView({ block: 'center', inline: 'nearest' });
|
|
129
|
+
}
|
|
109
130
|
const rect = el.getBoundingClientRect();
|
|
110
131
|
return { found: true, x: Math.round(rect.x + rect.width / 2), y: Math.round(rect.y + rect.height / 2) };
|
|
111
132
|
};
|
|
@@ -148,7 +169,8 @@ const SCRAPE_PAST_CONVERSATIONS_SCRIPT = `(() => {
|
|
|
148
169
|
// Try the visible QuickInput dialog first, then fall back to the side panel.
|
|
149
170
|
const quickInputPanels = Array.from(document.querySelectorAll('div[class*="bg-quickinput-background"]'));
|
|
150
171
|
const panel = quickInputPanels.find((el) => isVisible(el))
|
|
151
|
-
|| document.querySelector('.antigravity-agent-side-panel')
|
|
172
|
+
|| document.querySelector('.antigravity-agent-side-panel')
|
|
173
|
+
|| document.body;
|
|
152
174
|
if (!panel) return null;
|
|
153
175
|
|
|
154
176
|
const items = [];
|
|
@@ -214,6 +236,9 @@ const FIND_SHOW_MORE_BUTTON_SCRIPT = `(() => {
|
|
|
214
236
|
if (!isVisible(el)) continue;
|
|
215
237
|
const text = (el.textContent || '').trim();
|
|
216
238
|
if (/^Show\\s+\\d+\\s+more/i.test(text)) {
|
|
239
|
+
if (typeof el.scrollIntoView === 'function') {
|
|
240
|
+
el.scrollIntoView({ block: 'center', inline: 'nearest' });
|
|
241
|
+
}
|
|
217
242
|
const rect = el.getBoundingClientRect();
|
|
218
243
|
return { found: true, x: Math.round(rect.x + rect.width / 2), y: Math.round(rect.y + rect.height / 2) };
|
|
219
244
|
}
|
|
@@ -241,18 +266,25 @@ function buildActivateChatByTitleScript(title) {
|
|
|
241
266
|
return true;
|
|
242
267
|
};
|
|
243
268
|
|
|
269
|
+
${FUZZY_MATCH_HELPERS_SCRIPT}
|
|
270
|
+
|
|
244
271
|
const nodes = Array.from(panel.querySelectorAll('button, [role="button"], a, li, div, span'))
|
|
245
272
|
.filter(isVisible);
|
|
246
273
|
|
|
247
274
|
const exact = [];
|
|
248
275
|
const includes = [];
|
|
276
|
+
const fuzzy = [];
|
|
249
277
|
for (const node of nodes) {
|
|
250
278
|
const text = normalize(node.textContent || '');
|
|
251
279
|
if (!text) continue;
|
|
280
|
+
|
|
281
|
+
const isShort = wanted.length < 5;
|
|
252
282
|
if (text === wanted) {
|
|
253
283
|
exact.push({ node, textLength: text.length });
|
|
254
|
-
} else if (text.includes(wanted)) {
|
|
284
|
+
} else if (!isShort && text.includes(wanted)) {
|
|
255
285
|
includes.push({ node, textLength: text.length });
|
|
286
|
+
} else if (wordsMatch(text, wantedRaw)) {
|
|
287
|
+
fuzzy.push({ node, textLength: text.length });
|
|
256
288
|
}
|
|
257
289
|
}
|
|
258
290
|
|
|
@@ -262,10 +294,18 @@ function buildActivateChatByTitleScript(title) {
|
|
|
262
294
|
return list[0].node;
|
|
263
295
|
};
|
|
264
296
|
|
|
265
|
-
const target = pick(exact) || pick(includes);
|
|
297
|
+
const target = pick(exact) || pick(includes) || pick(fuzzy);
|
|
266
298
|
if (!target) return { ok: false, error: 'Chat title not found in side panel' };
|
|
267
|
-
|
|
268
|
-
|
|
299
|
+
const clickable = target.closest('button, [role="button"], a, li, [data-testid*="conversation"]') || target;
|
|
300
|
+
if (typeof clickable.scrollIntoView === 'function') {
|
|
301
|
+
clickable.scrollIntoView({ block: 'center', inline: 'nearest' });
|
|
302
|
+
}
|
|
303
|
+
const rect = clickable.getBoundingClientRect();
|
|
304
|
+
return {
|
|
305
|
+
ok: true,
|
|
306
|
+
x: Math.round(rect.x + rect.width / 2),
|
|
307
|
+
y: Math.round(rect.y + rect.height / 2)
|
|
308
|
+
};
|
|
269
309
|
})()`;
|
|
270
310
|
}
|
|
271
311
|
/**
|
|
@@ -303,14 +343,6 @@ function buildActivateViaPastConversationsScript(title) {
|
|
|
303
343
|
];
|
|
304
344
|
};
|
|
305
345
|
const getLabelText = (el) => getLabelParts(el).filter(Boolean).join(' ');
|
|
306
|
-
// Verify the option that was actually selected really is the requested
|
|
307
|
-
// title. getLabelText concatenates several attributes, so a naive
|
|
308
|
-
// whole-string comparison can fail even for a correct pick (e.g. when
|
|
309
|
-
// textContent and aria-label duplicate the same value). Instead, check
|
|
310
|
-
// each label component individually for an exact (or loose-exact) match.
|
|
311
|
-
// If any component matches, report the requested title; otherwise report
|
|
312
|
-
// the actual visible text so the acceptance gate rejects a mere
|
|
313
|
-
// substring/loose selection.
|
|
314
346
|
const resolveMatchedTitle = (el) => {
|
|
315
347
|
const parts = getLabelParts(el);
|
|
316
348
|
for (const part of parts) {
|
|
@@ -327,6 +359,8 @@ function buildActivateViaPastConversationsScript(title) {
|
|
|
327
359
|
const clickable = el.closest('button, [role="button"], a, li, [role="option"], [data-testid*="conversation"]');
|
|
328
360
|
return clickable instanceof HTMLElement ? clickable : (el instanceof HTMLElement ? el : null);
|
|
329
361
|
};
|
|
362
|
+
${FUZZY_MATCH_HELPERS_SCRIPT}
|
|
363
|
+
|
|
330
364
|
const pickBest = (elements, patterns) => {
|
|
331
365
|
const matched = [];
|
|
332
366
|
for (const el of elements) {
|
|
@@ -338,13 +372,18 @@ function buildActivateViaPastConversationsScript(title) {
|
|
|
338
372
|
if (!pattern) continue;
|
|
339
373
|
const p = normalize(pattern);
|
|
340
374
|
const pLoose = normalizeLoose(pattern);
|
|
375
|
+
const isShort = p.length < 5;
|
|
376
|
+
|
|
341
377
|
if (
|
|
342
378
|
text === p ||
|
|
343
|
-
|
|
344
|
-
(
|
|
379
|
+
(pLoose && textLoose === pLoose) ||
|
|
380
|
+
(!isShort && (text.includes(p) || (pLoose && textLoose.includes(pLoose))))
|
|
345
381
|
) {
|
|
346
382
|
matched.push({ el, score: Math.abs(text.length - pattern.length) });
|
|
347
383
|
break;
|
|
384
|
+
} else if (wordsMatch(text, pattern)) {
|
|
385
|
+
matched.push({ el, score: Math.abs(text.length - pattern.length) + 1000 });
|
|
386
|
+
break;
|
|
348
387
|
}
|
|
349
388
|
}
|
|
350
389
|
}
|
|
@@ -366,7 +405,13 @@ function buildActivateViaPastConversationsScript(title) {
|
|
|
366
405
|
if (!el) return false;
|
|
367
406
|
if (el instanceof HTMLInputElement || el instanceof HTMLTextAreaElement) {
|
|
368
407
|
el.focus();
|
|
369
|
-
el.
|
|
408
|
+
const proto = el instanceof HTMLInputElement ? window.HTMLInputElement.prototype : window.HTMLTextAreaElement.prototype;
|
|
409
|
+
const nativeSetter = Object.getOwnPropertyDescriptor(proto, 'value')?.set;
|
|
410
|
+
if (nativeSetter) {
|
|
411
|
+
nativeSetter.call(el, value);
|
|
412
|
+
} else {
|
|
413
|
+
el.value = value;
|
|
414
|
+
}
|
|
370
415
|
el.dispatchEvent(new Event('input', { bubbles: true }));
|
|
371
416
|
el.dispatchEvent(new Event('change', { bubbles: true }));
|
|
372
417
|
return true;
|
|
@@ -489,29 +534,41 @@ function buildActivateViaPastConversationsScript(title) {
|
|
|
489
534
|
clickByPatterns(['select a conversation', 'select conversation', 'conversation'], '[role="button"], button, [aria-haspopup], [data-testid*="conversation"]');
|
|
490
535
|
await wait(220);
|
|
491
536
|
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
537
|
+
// First, try to find it in the default visible list (works for recent chats)
|
|
538
|
+
let selectedOption = pickBest(
|
|
539
|
+
asArray(document.querySelectorAll('[role="option"], li, button, [data-testid*="conversation"]')),
|
|
540
|
+
[wanted, wantedLoose],
|
|
541
|
+
);
|
|
542
|
+
|
|
543
|
+
// If not found, fall back to the search input but use a shorter prefix to avoid strict-match bugs
|
|
544
|
+
if (!selectedOption) {
|
|
545
|
+
const input = findSearchInput();
|
|
546
|
+
if (input) {
|
|
547
|
+
// Use first 2 words to bypass IDE renaming/truncation issues
|
|
548
|
+
const searchPrefix = (wantedRaw || '').split(/\\s+/).slice(0, 2).join(' ');
|
|
549
|
+
setInputValue(input, searchPrefix.length > 3 ? searchPrefix : wantedRaw);
|
|
550
|
+
await wait(350);
|
|
551
|
+
selectedOption = pickBest(
|
|
552
|
+
asArray(document.querySelectorAll('[role="option"], li, button, [data-testid*="conversation"]')),
|
|
553
|
+
[wanted, wantedLoose],
|
|
554
|
+
);
|
|
555
|
+
}
|
|
496
556
|
}
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
// on that same element so the click target and the verification
|
|
500
|
-
// target can never drift apart. Mirror clickByPatterns' scoped
|
|
501
|
-
// selector -> broad fallback behaviour.
|
|
502
|
-
const optionSelector = '[role="option"], li, button, [data-testid*="conversation"]';
|
|
503
|
-
const scopedOptions = asArray(document.querySelectorAll(optionSelector));
|
|
504
|
-
const broadOptions = asArray(document.querySelectorAll('button, [role="button"], a, li, div, span'));
|
|
505
|
-
const optionSource = scopedOptions.length > 0 ? scopedOptions : broadOptions;
|
|
506
|
-
const selectedOption = pickBest(optionSource, [wanted, wantedLoose]);
|
|
507
|
-
const selectedClickable = getClickable(selectedOption);
|
|
508
|
-
if (!selectedClickable) {
|
|
557
|
+
|
|
558
|
+
if (!selectedOption) {
|
|
509
559
|
return { ok: false, error: 'Conversation not found in Past Conversations' };
|
|
510
560
|
}
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
561
|
+
const clickable = getClickable(selectedOption) || selectedOption;
|
|
562
|
+
if (typeof clickable.scrollIntoView === 'function') {
|
|
563
|
+
clickable.scrollIntoView({ block: 'center', inline: 'nearest' });
|
|
564
|
+
}
|
|
565
|
+
const rect = clickable.getBoundingClientRect();
|
|
566
|
+
return {
|
|
567
|
+
ok: true,
|
|
568
|
+
x: Math.round(rect.x + rect.width / 2),
|
|
569
|
+
y: Math.round(rect.y + rect.height / 2),
|
|
570
|
+
matchedTitle: resolveMatchedTitle(selectedOption)
|
|
571
|
+
};
|
|
515
572
|
})();
|
|
516
573
|
})()`;
|
|
517
574
|
}
|
|
@@ -559,7 +616,8 @@ class ChatSessionService {
|
|
|
559
616
|
const isVisible = (el) => !!el && el instanceof HTMLElement && el.offsetParent !== null;
|
|
560
617
|
const quickInputPanels = Array.from(document.querySelectorAll('div[class*="bg-quickinput-background"]'));
|
|
561
618
|
const panel = quickInputPanels.find((el) => isVisible(el))
|
|
562
|
-
|| document.querySelector('.antigravity-agent-side-panel')
|
|
619
|
+
|| document.querySelector('.antigravity-agent-side-panel')
|
|
620
|
+
|| document.body;
|
|
563
621
|
if (!panel) return false;
|
|
564
622
|
const containers = Array.from(
|
|
565
623
|
panel.querySelectorAll('div[class*="overflow-auto"], div[class*="overflow-y-scroll"]')
|
|
@@ -893,7 +951,12 @@ class ChatSessionService {
|
|
|
893
951
|
if (!clicked) {
|
|
894
952
|
pastResult = await this.tryActivateByPastConversations(cdpService, title);
|
|
895
953
|
clicked = pastResult.ok;
|
|
896
|
-
|
|
954
|
+
// If we attempted past conversations, the panel is open (unless it clicked something)
|
|
955
|
+
usedPastConversations = true;
|
|
956
|
+
// If it failed to click anything, the panel is still open and blocking the UI.
|
|
957
|
+
if (!clicked) {
|
|
958
|
+
await this.closePanelWithEscape(cdpService);
|
|
959
|
+
}
|
|
897
960
|
}
|
|
898
961
|
if (clicked) {
|
|
899
962
|
break;
|
|
@@ -964,6 +1027,7 @@ class ChatSessionService {
|
|
|
964
1027
|
error: `Past Conversations selected a different chat (expected="${title}", actual="${afterPast.title}")`,
|
|
965
1028
|
};
|
|
966
1029
|
}
|
|
1030
|
+
await this.closePanelWithEscape(cdpService);
|
|
967
1031
|
return {
|
|
968
1032
|
ok: false,
|
|
969
1033
|
error: `Activated chat did not match target title (expected="${title}", actual="${after.title}") ` +
|
|
@@ -994,6 +1058,9 @@ class ChatSessionService {
|
|
|
994
1058
|
});
|
|
995
1059
|
const value = result?.result?.value;
|
|
996
1060
|
if (value?.ok) {
|
|
1061
|
+
if (typeof value.x === 'number' && typeof value.y === 'number') {
|
|
1062
|
+
await this.cdpMouseClick(cdpService, value.x, value.y);
|
|
1063
|
+
}
|
|
997
1064
|
return {
|
|
998
1065
|
ok: true,
|
|
999
1066
|
...(typeof value.matchedTitle === 'string'
|
|
@@ -1031,5 +1098,45 @@ class ChatSessionService {
|
|
|
1031
1098
|
}
|
|
1032
1099
|
return { found: false, enabled: false, x: 0, y: 0 };
|
|
1033
1100
|
}
|
|
1101
|
+
/**
|
|
1102
|
+
* Rename the current chat in the Antigravity UI directly by updating the DOM.
|
|
1103
|
+
* Note: This is a cosmetic change until Antigravity persists a rename.
|
|
1104
|
+
* IDE syncing for chat renaming is strictly "best-effort".
|
|
1105
|
+
*/
|
|
1106
|
+
async renameCurrentChatInUI(cdpService, newTitle) {
|
|
1107
|
+
try {
|
|
1108
|
+
const contextId = cdpService.getPrimaryContextId();
|
|
1109
|
+
const RENAME_SCRIPT = `(() => {
|
|
1110
|
+
const panel = document.querySelector('.antigravity-agent-side-panel') || document.body;
|
|
1111
|
+
if (!panel) return { ok: false, error: 'Panel not found' };
|
|
1112
|
+
const header = panel.querySelector('div[class*="border-b"]');
|
|
1113
|
+
const titleEl = header?.querySelector('div[class*="text-ellipsis"]');
|
|
1114
|
+
if (titleEl) {
|
|
1115
|
+
titleEl.textContent = ${JSON.stringify(newTitle)};
|
|
1116
|
+
return { ok: true };
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
const activeRow = Array.from(panel.querySelectorAll('div[class*="focusBackground"]'))
|
|
1120
|
+
.find((el) => el instanceof HTMLElement && el.offsetParent !== null);
|
|
1121
|
+
const activeTitle = activeRow?.querySelector('span.text-sm span, span.text-sm');
|
|
1122
|
+
if (activeTitle) {
|
|
1123
|
+
activeTitle.textContent = ${JSON.stringify(newTitle)};
|
|
1124
|
+
return { ok: true };
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
return { ok: false, error: 'Title element not found' };
|
|
1128
|
+
})()`;
|
|
1129
|
+
const result = await cdpService.call('Runtime.evaluate', {
|
|
1130
|
+
expression: RENAME_SCRIPT,
|
|
1131
|
+
returnByValue: true,
|
|
1132
|
+
...(contextId !== null ? { contextId } : {}),
|
|
1133
|
+
awaitPromise: true,
|
|
1134
|
+
});
|
|
1135
|
+
return result?.result?.value || { ok: false, error: 'Eval failed' };
|
|
1136
|
+
}
|
|
1137
|
+
catch (e) {
|
|
1138
|
+
return { ok: false, error: e.message };
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1034
1141
|
}
|
|
1035
1142
|
exports.ChatSessionService = ChatSessionService;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ErrorPopupDetector = void 0;
|
|
4
|
+
const consecutiveEmptyPollGate_1 = require("../utils/consecutiveEmptyPollGate");
|
|
4
5
|
const logger_1 = require("../utils/logger");
|
|
5
6
|
const approvalDetector_1 = require("./approvalDetector");
|
|
6
7
|
/**
|
|
@@ -107,8 +108,10 @@ class ErrorPopupDetector {
|
|
|
107
108
|
lastDetectedInfo = null;
|
|
108
109
|
/** Timestamp of last notification (for cooldown-based dedup) */
|
|
109
110
|
lastNotifiedAt = 0;
|
|
110
|
-
/** Cooldown period in ms to suppress duplicate notifications
|
|
111
|
-
static COOLDOWN_MS =
|
|
111
|
+
/** Cooldown period in ms to suppress duplicate notifications */
|
|
112
|
+
static COOLDOWN_MS = 5000;
|
|
113
|
+
/** Gate for empty polls before reset */
|
|
114
|
+
emptyPollGate = new consecutiveEmptyPollGate_1.ConsecutiveEmptyPollGate(3);
|
|
112
115
|
constructor(options) {
|
|
113
116
|
this.cdpService = options.cdpService;
|
|
114
117
|
this.pollIntervalMs = options.pollIntervalMs ?? 3000;
|
|
@@ -123,6 +126,7 @@ class ErrorPopupDetector {
|
|
|
123
126
|
this.lastDetectedKey = null;
|
|
124
127
|
this.lastDetectedInfo = null;
|
|
125
128
|
this.lastNotifiedAt = 0;
|
|
129
|
+
this.emptyPollGate.reset();
|
|
126
130
|
this.schedulePoll();
|
|
127
131
|
}
|
|
128
132
|
/** Stop monitoring. */
|
|
@@ -208,28 +212,36 @@ class ErrorPopupDetector {
|
|
|
208
212
|
const result = await this.cdpService.call('Runtime.evaluate', callParams);
|
|
209
213
|
const info = result?.result?.value ?? null;
|
|
210
214
|
if (info) {
|
|
215
|
+
this.emptyPollGate.recordDetection();
|
|
211
216
|
// Duplicate prevention: use title + body snippet as key
|
|
212
217
|
const key = `${info.title}::${info.body.slice(0, 100)}`;
|
|
213
218
|
const now = Date.now();
|
|
214
219
|
const withinCooldown = (now - this.lastNotifiedAt) < ErrorPopupDetector.COOLDOWN_MS;
|
|
215
|
-
if (key !== this.lastDetectedKey
|
|
220
|
+
if (key !== this.lastDetectedKey) {
|
|
221
|
+
this.lastNotifiedAt = now;
|
|
216
222
|
this.lastDetectedKey = key;
|
|
217
223
|
this.lastDetectedInfo = info;
|
|
224
|
+
this.onErrorPopup(info);
|
|
225
|
+
}
|
|
226
|
+
else if (!withinCooldown) {
|
|
218
227
|
this.lastNotifiedAt = now;
|
|
228
|
+
this.lastDetectedInfo = info;
|
|
219
229
|
this.onErrorPopup(info);
|
|
220
230
|
}
|
|
221
|
-
else
|
|
222
|
-
// Same key -- update stored info silently
|
|
231
|
+
else {
|
|
232
|
+
// Same key, within cooldown -- update stored info silently
|
|
223
233
|
this.lastDetectedInfo = info;
|
|
224
234
|
}
|
|
225
235
|
}
|
|
226
236
|
else {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
this.onResolved
|
|
237
|
+
if (this.emptyPollGate.recordEmptyPoll()) {
|
|
238
|
+
// Reset when popup disappears (prepare for next detection)
|
|
239
|
+
const wasDetected = this.lastDetectedKey !== null;
|
|
240
|
+
this.lastDetectedKey = null;
|
|
241
|
+
this.lastDetectedInfo = null;
|
|
242
|
+
if (wasDetected && this.onResolved) {
|
|
243
|
+
this.onResolved();
|
|
244
|
+
}
|
|
233
245
|
}
|
|
234
246
|
}
|
|
235
247
|
}
|