lazy-gravity 0.2.0 → 0.4.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/README.md +77 -15
- package/dist/bin/cli.js +0 -0
- package/dist/bin/commands/doctor.js +19 -2
- package/dist/bin/commands/open.js +1 -1
- package/dist/bin/commands/setup.js +286 -70
- package/dist/bot/eventRouter.js +70 -0
- package/dist/bot/index.js +355 -147
- package/dist/bot/telegramCommands.js +478 -0
- package/dist/bot/telegramMessageHandler.js +308 -0
- package/dist/bot/telegramProjectCommand.js +137 -0
- package/dist/bot/workspaceQueue.js +61 -0
- package/dist/commands/joinCommandHandler.js +4 -1
- package/dist/database/telegramBindingRepository.js +97 -0
- package/dist/database/userPreferenceRepository.js +46 -1
- package/dist/events/interactionCreateHandler.js +36 -0
- package/dist/events/messageCreateHandler.js +11 -7
- package/dist/handlers/approvalButtonAction.js +99 -0
- package/dist/handlers/autoAcceptButtonAction.js +43 -0
- package/dist/handlers/buttonHandler.js +55 -0
- package/dist/handlers/commandHandler.js +44 -0
- package/dist/handlers/errorPopupButtonAction.js +137 -0
- package/dist/handlers/messageHandler.js +70 -0
- package/dist/handlers/modeSelectAction.js +63 -0
- package/dist/handlers/modelButtonAction.js +102 -0
- package/dist/handlers/planningButtonAction.js +118 -0
- package/dist/handlers/selectHandler.js +41 -0
- package/dist/handlers/templateButtonAction.js +54 -0
- package/dist/platform/adapter.js +8 -0
- package/dist/platform/discord/discordAdapter.js +99 -0
- package/dist/platform/discord/index.js +15 -0
- package/dist/platform/discord/wrappers.js +331 -0
- package/dist/platform/index.js +18 -0
- package/dist/platform/richContentBuilder.js +76 -0
- package/dist/platform/telegram/index.js +16 -0
- package/dist/platform/telegram/telegramAdapter.js +195 -0
- package/dist/platform/telegram/telegramFormatter.js +134 -0
- package/dist/platform/telegram/wrappers.js +333 -0
- package/dist/platform/types.js +28 -0
- package/dist/services/approvalDetector.js +15 -2
- package/dist/services/cdpBridgeManager.js +91 -146
- package/dist/services/cdpService.js +88 -2
- package/dist/services/chatSessionService.js +50 -10
- package/dist/services/defaultModelApplicator.js +54 -0
- package/dist/services/modeService.js +16 -1
- package/dist/services/modelService.js +57 -16
- package/dist/services/notificationSender.js +149 -0
- package/dist/services/responseMonitor.js +1 -2
- package/dist/services/screenshotService.js +2 -2
- package/dist/ui/autoAcceptUi.js +37 -0
- package/dist/ui/modeUi.js +38 -1
- package/dist/ui/modelsUi.js +96 -0
- package/dist/ui/outputUi.js +32 -0
- package/dist/ui/projectListUi.js +55 -0
- package/dist/ui/screenshotUi.js +26 -0
- package/dist/ui/sessionPickerUi.js +35 -1
- package/dist/ui/templateUi.js +41 -0
- package/dist/utils/configLoader.js +63 -12
- package/dist/utils/lockfile.js +5 -5
- package/dist/utils/logger.js +7 -0
- package/dist/utils/telegramImageHandler.js +127 -0
- package/package.json +6 -3
- package/dist/commands/joinDetachCommandHandler.js +0 -285
- package/dist/services/retryStore.js +0 -46
- package/dist/ui/buttonUtils.js +0 -33
- package/dist/utils/antigravityPaths.js +0 -94
- package/dist/utils/logFileTransport.js +0 -147
|
@@ -16,10 +16,9 @@ exports.ensureApprovalDetector = ensureApprovalDetector;
|
|
|
16
16
|
exports.ensurePlanningDetector = ensurePlanningDetector;
|
|
17
17
|
exports.ensureErrorPopupDetector = ensureErrorPopupDetector;
|
|
18
18
|
exports.ensureUserMessageDetector = ensureUserMessageDetector;
|
|
19
|
-
const discord_js_1 = require("discord.js");
|
|
20
19
|
const i18n_1 = require("../utils/i18n");
|
|
21
20
|
const logger_1 = require("../utils/logger");
|
|
22
|
-
const
|
|
21
|
+
const notificationSender_1 = require("./notificationSender");
|
|
23
22
|
const approvalDetector_1 = require("./approvalDetector");
|
|
24
23
|
const autoAcceptService_1 = require("./autoAcceptService");
|
|
25
24
|
const cdpConnectionPool_1 = require("./cdpConnectionPool");
|
|
@@ -230,89 +229,66 @@ function getCurrentCdp(bridge) {
|
|
|
230
229
|
* Helper to start an approval detector for each workspace.
|
|
231
230
|
* Does nothing if a detector for the same workspace is already running.
|
|
232
231
|
*/
|
|
233
|
-
function ensureApprovalDetector(bridge, cdp, projectName
|
|
232
|
+
function ensureApprovalDetector(bridge, cdp, projectName) {
|
|
234
233
|
const existing = bridge.pool.getApprovalDetector(projectName);
|
|
235
234
|
if (existing && existing.isActive())
|
|
236
235
|
return;
|
|
237
|
-
// Track the most recent
|
|
238
|
-
// Only the latest
|
|
239
|
-
// is resolved, the older
|
|
236
|
+
// Track the most recent notification for auto-disable on resolve.
|
|
237
|
+
// Only the latest is tracked; if a new detection fires before the previous
|
|
238
|
+
// is resolved, the older reference is overwritten. This is acceptable because
|
|
240
239
|
// the detector's lastDetectedKey deduplication prevents rapid successive notifications.
|
|
241
|
-
let
|
|
240
|
+
let lastNotification = null;
|
|
242
241
|
const detector = new approvalDetector_1.ApprovalDetector({
|
|
243
242
|
cdpService: cdp,
|
|
244
243
|
pollIntervalMs: 2000,
|
|
245
244
|
onResolved: () => {
|
|
246
|
-
if (!
|
|
245
|
+
if (!lastNotification)
|
|
247
246
|
return;
|
|
248
|
-
const
|
|
249
|
-
|
|
250
|
-
const
|
|
251
|
-
|
|
252
|
-
? discord_js_1.EmbedBuilder.from(originalEmbed)
|
|
253
|
-
: new discord_js_1.EmbedBuilder().setTitle((0, i18n_1.t)('Approval Required'));
|
|
254
|
-
updatedEmbed
|
|
255
|
-
.setColor(0x95A5A6)
|
|
256
|
-
.addFields({ name: (0, i18n_1.t)('Status'), value: (0, i18n_1.t)('Resolved in Antigravity'), inline: false });
|
|
257
|
-
msg.edit({
|
|
258
|
-
embeds: [updatedEmbed],
|
|
259
|
-
components: (0, discordButtonUtils_1.disableAllButtons)(msg.components),
|
|
260
|
-
}).catch(logger_1.logger.error);
|
|
247
|
+
const { sent, payload } = lastNotification;
|
|
248
|
+
lastNotification = null;
|
|
249
|
+
const resolved = (0, notificationSender_1.buildResolvedOverlay)(payload, (0, i18n_1.t)('Resolved in Antigravity'));
|
|
250
|
+
sent.edit(resolved).catch(logger_1.logger.error);
|
|
261
251
|
},
|
|
262
252
|
onApprovalRequired: async (info) => {
|
|
263
253
|
logger_1.logger.debug(`[ApprovalDetector:${projectName}] Approval button detected (allow="${info.approveText}", deny="${info.denyText}")`);
|
|
264
254
|
const currentChatTitle = await getCurrentChatTitle(cdp);
|
|
265
255
|
const targetChannel = resolveApprovalChannelForCurrentChat(bridge, projectName, currentChatTitle);
|
|
266
|
-
const targetChannelId = targetChannel
|
|
267
|
-
if (!targetChannel || !targetChannelId
|
|
268
|
-
logger_1.logger.warn(`[ApprovalDetector:${projectName}] Skipped approval notification because chat is not linked to a
|
|
256
|
+
const targetChannelId = targetChannel ? targetChannel.id : '';
|
|
257
|
+
if (!targetChannel || !targetChannelId) {
|
|
258
|
+
logger_1.logger.warn(`[ApprovalDetector:${projectName}] Skipped approval notification because chat is not linked to a session` +
|
|
269
259
|
`${currentChatTitle ? ` (title="${currentChatTitle}")` : ''}`);
|
|
270
260
|
return;
|
|
271
261
|
}
|
|
272
262
|
if (bridge.autoAccept.isEnabled()) {
|
|
273
263
|
const accepted = await detector.alwaysAllowButton() || await detector.approveButton();
|
|
274
|
-
const
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
.
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
}
|
|
282
|
-
if (info.approveText) {
|
|
283
|
-
autoEmbed.addFields({ name: (0, i18n_1.t)('Approved via'), value: info.approveText, inline: true });
|
|
284
|
-
}
|
|
285
|
-
autoEmbed.setTimestamp();
|
|
286
|
-
await targetChannel.send({ embeds: [autoEmbed] }).catch(logger_1.logger.error);
|
|
264
|
+
const autoPayload = (0, notificationSender_1.buildAutoApprovedNotification)({
|
|
265
|
+
accepted,
|
|
266
|
+
projectName,
|
|
267
|
+
description: info.description ?? undefined,
|
|
268
|
+
approveText: info.approveText ?? undefined,
|
|
269
|
+
});
|
|
270
|
+
await targetChannel.send(autoPayload).catch(logger_1.logger.error);
|
|
287
271
|
if (accepted) {
|
|
288
272
|
return;
|
|
289
273
|
}
|
|
290
274
|
}
|
|
291
|
-
const
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
.
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
.setCustomId(buildApprovalCustomId('deny', projectName, targetChannelId))
|
|
307
|
-
.setLabel((0, i18n_1.t)('Deny'))
|
|
308
|
-
.setStyle(discord_js_1.ButtonStyle.Danger);
|
|
309
|
-
const row = new discord_js_1.ActionRowBuilder().addComponents(approveBtn, alwaysAllowBtn, denyBtn);
|
|
310
|
-
const sent = await targetChannel.send({
|
|
311
|
-
embeds: [embed],
|
|
312
|
-
components: [row],
|
|
313
|
-
}).catch((err) => { logger_1.logger.error(err); return null; });
|
|
275
|
+
const payload = (0, notificationSender_1.buildApprovalNotification)({
|
|
276
|
+
title: (0, i18n_1.t)('Approval Required'),
|
|
277
|
+
description: info.description || (0, i18n_1.t)('Antigravity is requesting approval for an action'),
|
|
278
|
+
projectName,
|
|
279
|
+
channelId: targetChannelId,
|
|
280
|
+
extraFields: [
|
|
281
|
+
{ name: (0, i18n_1.t)('Allow button'), value: info.approveText, inline: true },
|
|
282
|
+
{ name: (0, i18n_1.t)('Allow Chat button'), value: info.alwaysAllowText || (0, i18n_1.t)('In Dropdown'), inline: true },
|
|
283
|
+
{ name: (0, i18n_1.t)('Deny button'), value: info.denyText || (0, i18n_1.t)('(None)'), inline: true },
|
|
284
|
+
],
|
|
285
|
+
});
|
|
286
|
+
const sent = await targetChannel.send(payload).catch((err) => {
|
|
287
|
+
logger_1.logger.error(err);
|
|
288
|
+
return null;
|
|
289
|
+
});
|
|
314
290
|
if (sent) {
|
|
315
|
-
|
|
291
|
+
lastNotification = { sent, payload };
|
|
316
292
|
}
|
|
317
293
|
},
|
|
318
294
|
});
|
|
@@ -324,68 +300,55 @@ function ensureApprovalDetector(bridge, cdp, projectName, client) {
|
|
|
324
300
|
* Helper to start a planning detector for each workspace.
|
|
325
301
|
* Does nothing if a detector for the same workspace is already running.
|
|
326
302
|
*/
|
|
327
|
-
function ensurePlanningDetector(bridge, cdp, projectName
|
|
303
|
+
function ensurePlanningDetector(bridge, cdp, projectName) {
|
|
328
304
|
const existing = bridge.pool.getPlanningDetector(projectName);
|
|
329
305
|
if (existing && existing.isActive())
|
|
330
306
|
return;
|
|
331
|
-
// Track the most recent planning
|
|
307
|
+
// Track the most recent planning notification for auto-disable on resolve.
|
|
332
308
|
// See ensureApprovalDetector comment for tracking limitation rationale.
|
|
333
|
-
let
|
|
309
|
+
let lastNotification = null;
|
|
334
310
|
const detector = new planningDetector_1.PlanningDetector({
|
|
335
311
|
cdpService: cdp,
|
|
336
312
|
pollIntervalMs: 2000,
|
|
337
313
|
onResolved: () => {
|
|
338
|
-
if (!
|
|
314
|
+
if (!lastNotification)
|
|
339
315
|
return;
|
|
340
|
-
const
|
|
341
|
-
|
|
342
|
-
const
|
|
343
|
-
|
|
344
|
-
? discord_js_1.EmbedBuilder.from(originalEmbed)
|
|
345
|
-
: new discord_js_1.EmbedBuilder().setTitle((0, i18n_1.t)('Planning Mode'));
|
|
346
|
-
updatedEmbed
|
|
347
|
-
.setColor(0x95A5A6)
|
|
348
|
-
.addFields({ name: (0, i18n_1.t)('Status'), value: (0, i18n_1.t)('Resolved in Antigravity'), inline: false });
|
|
349
|
-
msg.edit({
|
|
350
|
-
embeds: [updatedEmbed],
|
|
351
|
-
components: (0, discordButtonUtils_1.disableAllButtons)(msg.components),
|
|
352
|
-
}).catch(logger_1.logger.error);
|
|
316
|
+
const { sent, payload } = lastNotification;
|
|
317
|
+
lastNotification = null;
|
|
318
|
+
const resolved = (0, notificationSender_1.buildResolvedOverlay)(payload, (0, i18n_1.t)('Resolved in Antigravity'));
|
|
319
|
+
sent.edit(resolved).catch(logger_1.logger.error);
|
|
353
320
|
},
|
|
354
321
|
onPlanningRequired: async (info) => {
|
|
355
322
|
logger_1.logger.debug(`[PlanningDetector:${projectName}] Planning buttons detected (title="${info.planTitle}")`);
|
|
356
323
|
const currentChatTitle = await getCurrentChatTitle(cdp);
|
|
357
324
|
const targetChannel = resolveApprovalChannelForCurrentChat(bridge, projectName, currentChatTitle);
|
|
358
|
-
const targetChannelId = targetChannel
|
|
359
|
-
if (!targetChannel || !targetChannelId
|
|
360
|
-
logger_1.logger.warn(`[PlanningDetector:${projectName}] Skipped planning notification because chat is not linked to a
|
|
325
|
+
const targetChannelId = targetChannel ? targetChannel.id : '';
|
|
326
|
+
if (!targetChannel || !targetChannelId) {
|
|
327
|
+
logger_1.logger.warn(`[PlanningDetector:${projectName}] Skipped planning notification because chat is not linked to a session` +
|
|
361
328
|
`${currentChatTitle ? ` (title="${currentChatTitle}")` : ''}`);
|
|
362
329
|
return;
|
|
363
330
|
}
|
|
364
331
|
const descriptionText = info.description || info.planSummary || (0, i18n_1.t)('A plan has been generated and is awaiting your review.');
|
|
365
|
-
const
|
|
366
|
-
.
|
|
367
|
-
.
|
|
368
|
-
|
|
369
|
-
.addFields({ name: (0, i18n_1.t)('Plan'), value: info.planTitle || (0, i18n_1.t)('Implementation Plan'), inline: true }, { name: (0, i18n_1.t)('Workspace'), value: projectName, inline: true })
|
|
370
|
-
.setTimestamp();
|
|
332
|
+
const extraFields = [
|
|
333
|
+
{ name: (0, i18n_1.t)('Plan'), value: info.planTitle || (0, i18n_1.t)('Implementation Plan'), inline: true },
|
|
334
|
+
{ name: (0, i18n_1.t)('Workspace'), value: projectName, inline: true },
|
|
335
|
+
];
|
|
371
336
|
if (info.planSummary && info.description) {
|
|
372
|
-
|
|
337
|
+
extraFields.push({ name: (0, i18n_1.t)('Summary'), value: info.planSummary.substring(0, 1024), inline: false });
|
|
373
338
|
}
|
|
374
|
-
const
|
|
375
|
-
.
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
components: [row],
|
|
386
|
-
}).catch((err) => { logger_1.logger.error(err); return null; });
|
|
339
|
+
const payload = (0, notificationSender_1.buildPlanningNotification)({
|
|
340
|
+
title: (0, i18n_1.t)('Planning Mode'),
|
|
341
|
+
description: descriptionText,
|
|
342
|
+
projectName,
|
|
343
|
+
channelId: targetChannelId,
|
|
344
|
+
extraFields,
|
|
345
|
+
});
|
|
346
|
+
const sent = await targetChannel.send(payload).catch((err) => {
|
|
347
|
+
logger_1.logger.error(err);
|
|
348
|
+
return null;
|
|
349
|
+
});
|
|
387
350
|
if (sent) {
|
|
388
|
-
|
|
351
|
+
lastNotification = { sent, payload };
|
|
389
352
|
}
|
|
390
353
|
},
|
|
391
354
|
});
|
|
@@ -397,69 +360,51 @@ function ensurePlanningDetector(bridge, cdp, projectName, _client) {
|
|
|
397
360
|
* Helper to start an error popup detector for each workspace.
|
|
398
361
|
* Does nothing if a detector for the same workspace is already running.
|
|
399
362
|
*/
|
|
400
|
-
function ensureErrorPopupDetector(bridge, cdp, projectName
|
|
363
|
+
function ensureErrorPopupDetector(bridge, cdp, projectName) {
|
|
401
364
|
const existing = bridge.pool.getErrorPopupDetector(projectName);
|
|
402
365
|
if (existing && existing.isActive())
|
|
403
366
|
return;
|
|
404
|
-
// Track the most recent error
|
|
367
|
+
// Track the most recent error notification for auto-disable on resolve.
|
|
405
368
|
// See ensureApprovalDetector comment for tracking limitation rationale.
|
|
406
|
-
let
|
|
369
|
+
let lastNotification = null;
|
|
407
370
|
const detector = new errorPopupDetector_1.ErrorPopupDetector({
|
|
408
371
|
cdpService: cdp,
|
|
409
372
|
pollIntervalMs: 3000,
|
|
410
373
|
onResolved: () => {
|
|
411
|
-
if (!
|
|
374
|
+
if (!lastNotification)
|
|
412
375
|
return;
|
|
413
|
-
const
|
|
414
|
-
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
? discord_js_1.EmbedBuilder.from(originalEmbed)
|
|
418
|
-
: new discord_js_1.EmbedBuilder().setTitle((0, i18n_1.t)('Agent Error'));
|
|
419
|
-
updatedEmbed
|
|
420
|
-
.setColor(0x95A5A6)
|
|
421
|
-
.addFields({ name: (0, i18n_1.t)('Status'), value: (0, i18n_1.t)('Resolved in Antigravity'), inline: false });
|
|
422
|
-
msg.edit({
|
|
423
|
-
embeds: [updatedEmbed],
|
|
424
|
-
components: (0, discordButtonUtils_1.disableAllButtons)(msg.components),
|
|
425
|
-
}).catch(logger_1.logger.error);
|
|
376
|
+
const { sent, payload } = lastNotification;
|
|
377
|
+
lastNotification = null;
|
|
378
|
+
const resolved = (0, notificationSender_1.buildResolvedOverlay)(payload, (0, i18n_1.t)('Resolved in Antigravity'));
|
|
379
|
+
sent.edit(resolved).catch(logger_1.logger.error);
|
|
426
380
|
},
|
|
427
381
|
onErrorPopup: async (info) => {
|
|
428
382
|
logger_1.logger.debug(`[ErrorPopupDetector:${projectName}] Error popup detected (title="${info.title}")`);
|
|
429
383
|
const currentChatTitle = await getCurrentChatTitle(cdp);
|
|
430
384
|
const targetChannel = resolveApprovalChannelForCurrentChat(bridge, projectName, currentChatTitle);
|
|
431
|
-
const targetChannelId = targetChannel
|
|
432
|
-
if (!targetChannel || !targetChannelId
|
|
433
|
-
logger_1.logger.warn(`[ErrorPopupDetector:${projectName}] Skipped error popup notification because chat is not linked to a
|
|
385
|
+
const targetChannelId = targetChannel ? targetChannel.id : '';
|
|
386
|
+
if (!targetChannel || !targetChannelId) {
|
|
387
|
+
logger_1.logger.warn(`[ErrorPopupDetector:${projectName}] Skipped error popup notification because chat is not linked to a session` +
|
|
434
388
|
`${currentChatTitle ? ` (title="${currentChatTitle}")` : ''}`);
|
|
435
389
|
return;
|
|
436
390
|
}
|
|
437
391
|
const bodyText = info.body || (0, i18n_1.t)('An error occurred in the Antigravity agent.');
|
|
438
|
-
const
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
const
|
|
449
|
-
.
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
const retryBtn = new discord_js_1.ButtonBuilder()
|
|
453
|
-
.setCustomId(buildErrorPopupCustomId('retry', projectName, targetChannelId))
|
|
454
|
-
.setLabel((0, i18n_1.t)('Retry'))
|
|
455
|
-
.setStyle(discord_js_1.ButtonStyle.Success);
|
|
456
|
-
const row = new discord_js_1.ActionRowBuilder().addComponents(dismissBtn, copyDebugBtn, retryBtn);
|
|
457
|
-
const sent = await targetChannel.send({
|
|
458
|
-
embeds: [embed],
|
|
459
|
-
components: [row],
|
|
460
|
-
}).catch((err) => { logger_1.logger.error(err); return null; });
|
|
392
|
+
const payload = (0, notificationSender_1.buildErrorPopupNotification)({
|
|
393
|
+
title: info.title || (0, i18n_1.t)('Agent Error'),
|
|
394
|
+
errorMessage: bodyText.substring(0, 4096),
|
|
395
|
+
projectName,
|
|
396
|
+
channelId: targetChannelId,
|
|
397
|
+
extraFields: [
|
|
398
|
+
{ name: (0, i18n_1.t)('Buttons'), value: info.buttons.join(', ') || (0, i18n_1.t)('(None)'), inline: true },
|
|
399
|
+
{ name: (0, i18n_1.t)('Workspace'), value: projectName, inline: true },
|
|
400
|
+
],
|
|
401
|
+
});
|
|
402
|
+
const sent = await targetChannel.send(payload).catch((err) => {
|
|
403
|
+
logger_1.logger.error(err);
|
|
404
|
+
return null;
|
|
405
|
+
});
|
|
461
406
|
if (sent) {
|
|
462
|
-
|
|
407
|
+
lastNotification = { sent, payload };
|
|
463
408
|
}
|
|
464
409
|
},
|
|
465
410
|
});
|
|
@@ -225,6 +225,26 @@ class CdpService extends events_1.EventEmitter {
|
|
|
225
225
|
this.ws.send(JSON.stringify({ id, method, params }));
|
|
226
226
|
});
|
|
227
227
|
}
|
|
228
|
+
/**
|
|
229
|
+
* Try call(), and on WebSocket connection error,
|
|
230
|
+
* attempt a single on-demand reconnect then retry once.
|
|
231
|
+
* Non-connection errors (timeout, protocol) are NOT retried.
|
|
232
|
+
*/
|
|
233
|
+
async callWithRetry(method, params = {}, timeoutMs = 10000) {
|
|
234
|
+
try {
|
|
235
|
+
return await this.call(method, params);
|
|
236
|
+
}
|
|
237
|
+
catch (error) {
|
|
238
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
239
|
+
const isConnectionError = message === 'WebSocket is not connected' ||
|
|
240
|
+
message === 'WebSocket disconnected';
|
|
241
|
+
if (!isConnectionError) {
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
await this.reconnectOnDemand(timeoutMs);
|
|
245
|
+
return await this.call(method, params);
|
|
246
|
+
}
|
|
247
|
+
}
|
|
228
248
|
async disconnect() {
|
|
229
249
|
// Stop reconnection attempts
|
|
230
250
|
this.maxReconnectAttempts = 0;
|
|
@@ -644,6 +664,72 @@ class CdpService extends events_1.EventEmitter {
|
|
|
644
664
|
logger_1.logger.error('[CdpService]', finalError.message);
|
|
645
665
|
this.emit('reconnectFailed', finalError);
|
|
646
666
|
}
|
|
667
|
+
/**
|
|
668
|
+
* Wait for an in-progress reconnection to complete.
|
|
669
|
+
* Resolves when 'reconnected' fires, rejects on 'reconnectFailed' or timeout.
|
|
670
|
+
*/
|
|
671
|
+
waitForReconnection(timeoutMs = 15000) {
|
|
672
|
+
return new Promise((resolve, reject) => {
|
|
673
|
+
const timer = setTimeout(() => {
|
|
674
|
+
cleanup();
|
|
675
|
+
reject(new Error('WebSocket is not connected'));
|
|
676
|
+
}, timeoutMs);
|
|
677
|
+
const onReconnected = () => {
|
|
678
|
+
cleanup();
|
|
679
|
+
resolve();
|
|
680
|
+
};
|
|
681
|
+
const onFailed = (_err) => {
|
|
682
|
+
cleanup();
|
|
683
|
+
reject(new Error('WebSocket is not connected'));
|
|
684
|
+
};
|
|
685
|
+
const cleanup = () => {
|
|
686
|
+
clearTimeout(timer);
|
|
687
|
+
this.removeListener('reconnected', onReconnected);
|
|
688
|
+
this.removeListener('reconnectFailed', onFailed);
|
|
689
|
+
};
|
|
690
|
+
this.on('reconnected', onReconnected);
|
|
691
|
+
this.on('reconnectFailed', onFailed);
|
|
692
|
+
});
|
|
693
|
+
}
|
|
694
|
+
/** Shared promise to coalesce concurrent reconnectOnDemand() calls */
|
|
695
|
+
reconnectOnDemandPromise = null;
|
|
696
|
+
/**
|
|
697
|
+
* On-demand reconnect: if already reconnecting, wait; otherwise attempt once.
|
|
698
|
+
* Throws 'WebSocket is not connected' when no workspace path or reconnect fails.
|
|
699
|
+
*/
|
|
700
|
+
async reconnectOnDemand(timeoutMs = 15000) {
|
|
701
|
+
if (this.isReconnecting) {
|
|
702
|
+
return this.waitForReconnection(timeoutMs);
|
|
703
|
+
}
|
|
704
|
+
if (!this.currentWorkspacePath) {
|
|
705
|
+
throw new Error('WebSocket is not connected');
|
|
706
|
+
}
|
|
707
|
+
// Coalesce concurrent calls
|
|
708
|
+
if (!this.reconnectOnDemandPromise) {
|
|
709
|
+
this.reconnectOnDemandPromise = (async () => {
|
|
710
|
+
try {
|
|
711
|
+
await this.discoverAndConnectForWorkspace(this.currentWorkspacePath);
|
|
712
|
+
}
|
|
713
|
+
catch {
|
|
714
|
+
throw new Error('WebSocket is not connected');
|
|
715
|
+
}
|
|
716
|
+
finally {
|
|
717
|
+
this.reconnectOnDemandPromise = null;
|
|
718
|
+
}
|
|
719
|
+
})();
|
|
720
|
+
}
|
|
721
|
+
let timer;
|
|
722
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
723
|
+
timer = setTimeout(() => reject(new Error('WebSocket is not connected')), timeoutMs);
|
|
724
|
+
});
|
|
725
|
+
try {
|
|
726
|
+
await Promise.race([this.reconnectOnDemandPromise, timeoutPromise]);
|
|
727
|
+
}
|
|
728
|
+
finally {
|
|
729
|
+
if (timer)
|
|
730
|
+
clearTimeout(timer);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
647
733
|
isConnected() {
|
|
648
734
|
return this.isConnectedFlag;
|
|
649
735
|
}
|
|
@@ -1163,7 +1249,7 @@ class CdpService extends events_1.EventEmitter {
|
|
|
1163
1249
|
*/
|
|
1164
1250
|
async setUiMode(modeName) {
|
|
1165
1251
|
if (!this.isConnectedFlag || !this.ws) {
|
|
1166
|
-
|
|
1252
|
+
await this.reconnectOnDemand();
|
|
1167
1253
|
}
|
|
1168
1254
|
const safeMode = JSON.stringify(modeName);
|
|
1169
1255
|
// Internal mode name -> Antigravity UI display name mapping
|
|
@@ -1330,7 +1416,7 @@ class CdpService extends events_1.EventEmitter {
|
|
|
1330
1416
|
*/
|
|
1331
1417
|
async setUiModel(modelName) {
|
|
1332
1418
|
if (!this.isConnectedFlag || !this.ws) {
|
|
1333
|
-
|
|
1419
|
+
await this.reconnectOnDemand();
|
|
1334
1420
|
}
|
|
1335
1421
|
// DOM manipulation script: based on actual Antigravity UI DOM structure
|
|
1336
1422
|
// Model list uses div.cursor-pointer elements with class 'px-2 py-1 flex items-center justify-between'
|
|
@@ -78,12 +78,17 @@ const SCRAPE_PAST_CONVERSATIONS_SCRIPT = `(() => {
|
|
|
78
78
|
const isVisible = (el) => !!el && el instanceof HTMLElement && el.offsetParent !== null;
|
|
79
79
|
const normalize = (text) => (text || '').trim();
|
|
80
80
|
|
|
81
|
+
// Scope to the side panel to avoid picking up file tab names
|
|
82
|
+
const panel = document.querySelector('.antigravity-agent-side-panel');
|
|
83
|
+
if (!panel) return null;
|
|
84
|
+
|
|
81
85
|
const items = [];
|
|
82
86
|
const seen = new Set();
|
|
83
87
|
|
|
84
|
-
// Find the scrollable conversation list container
|
|
85
|
-
const containers = Array.from(
|
|
86
|
-
const container = containers.find((c) => isVisible(c) && c.querySelectorAll('div[class*="cursor-pointer"]').length > 0)
|
|
88
|
+
// Find the scrollable conversation list container within the side panel
|
|
89
|
+
const containers = Array.from(panel.querySelectorAll('div[class*="overflow-auto"], div[class*="overflow-y-scroll"]'));
|
|
90
|
+
const container = containers.find((c) => isVisible(c) && c.querySelectorAll('div[class*="cursor-pointer"]').length > 0);
|
|
91
|
+
if (!container) return null;
|
|
87
92
|
|
|
88
93
|
// Detect the "Other Conversations" section boundary.
|
|
89
94
|
// Sessions below this header belong to other projects and must be excluded.
|
|
@@ -436,6 +441,7 @@ class ChatSessionService {
|
|
|
436
441
|
* @returns Array of session list items (empty array on failure)
|
|
437
442
|
*/
|
|
438
443
|
async listAllSessions(cdpService) {
|
|
444
|
+
let panelOpened = false;
|
|
439
445
|
try {
|
|
440
446
|
// Step 1: Find Past Conversations button
|
|
441
447
|
const btnState = await this.evaluateOnAnyContext(cdpService, FIND_PAST_CONVERSATIONS_BUTTON_SCRIPT, false);
|
|
@@ -444,8 +450,30 @@ class ChatSessionService {
|
|
|
444
450
|
}
|
|
445
451
|
// Step 2: Click via CDP mouse events (reliable in Electron)
|
|
446
452
|
await this.cdpMouseClick(cdpService, btnState.x, btnState.y);
|
|
447
|
-
|
|
448
|
-
|
|
453
|
+
panelOpened = true;
|
|
454
|
+
// Step 3: Wait for panel to render (poll for content, up to 3s)
|
|
455
|
+
const PANEL_READY_CHECK = `(() => {
|
|
456
|
+
const isVisible = (el) => !!el && el instanceof HTMLElement && el.offsetParent !== null;
|
|
457
|
+
const panel = document.querySelector('.antigravity-agent-side-panel');
|
|
458
|
+
if (!panel) return false;
|
|
459
|
+
const containers = Array.from(
|
|
460
|
+
panel.querySelectorAll('div[class*="overflow-auto"], div[class*="overflow-y-scroll"]')
|
|
461
|
+
);
|
|
462
|
+
return containers.some((c) =>
|
|
463
|
+
isVisible(c) && c.querySelector('div[class*="cursor-pointer"]')
|
|
464
|
+
);
|
|
465
|
+
})()`;
|
|
466
|
+
let panelReady = false;
|
|
467
|
+
const deadline = Date.now() + 3000;
|
|
468
|
+
while (Date.now() < deadline) {
|
|
469
|
+
panelReady = Boolean(await this.evaluateOnAnyContext(cdpService, PANEL_READY_CHECK, false));
|
|
470
|
+
if (panelReady)
|
|
471
|
+
break;
|
|
472
|
+
await new Promise((r) => setTimeout(r, 200));
|
|
473
|
+
}
|
|
474
|
+
if (!panelReady) {
|
|
475
|
+
return [];
|
|
476
|
+
}
|
|
449
477
|
// Step 4: Scrape sessions
|
|
450
478
|
let scrapeResult = await this.evaluateOnAnyContext(cdpService, SCRAPE_PAST_CONVERSATIONS_SCRIPT, false);
|
|
451
479
|
let sessions = scrapeResult?.sessions ?? [];
|
|
@@ -460,7 +488,22 @@ class ChatSessionService {
|
|
|
460
488
|
sessions = scrapeResult?.sessions ?? [];
|
|
461
489
|
}
|
|
462
490
|
}
|
|
463
|
-
|
|
491
|
+
return sessions.slice(0, ChatSessionService.LIST_SESSIONS_TARGET);
|
|
492
|
+
}
|
|
493
|
+
catch (_) {
|
|
494
|
+
return [];
|
|
495
|
+
}
|
|
496
|
+
finally {
|
|
497
|
+
if (panelOpened) {
|
|
498
|
+
await this.closePanelWithEscape(cdpService);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
/**
|
|
503
|
+
* Close the Past Conversations panel by sending Escape key events.
|
|
504
|
+
*/
|
|
505
|
+
async closePanelWithEscape(cdpService) {
|
|
506
|
+
try {
|
|
464
507
|
await cdpService.call('Input.dispatchKeyEvent', {
|
|
465
508
|
type: 'keyDown', key: 'Escape', code: 'Escape',
|
|
466
509
|
windowsVirtualKeyCode: 27, nativeVirtualKeyCode: 27,
|
|
@@ -469,11 +512,8 @@ class ChatSessionService {
|
|
|
469
512
|
type: 'keyUp', key: 'Escape', code: 'Escape',
|
|
470
513
|
windowsVirtualKeyCode: 27, nativeVirtualKeyCode: 27,
|
|
471
514
|
});
|
|
472
|
-
return sessions.slice(0, ChatSessionService.LIST_SESSIONS_TARGET);
|
|
473
|
-
}
|
|
474
|
-
catch (_) {
|
|
475
|
-
return [];
|
|
476
515
|
}
|
|
516
|
+
catch (_) { /* best-effort cleanup */ }
|
|
477
517
|
}
|
|
478
518
|
/**
|
|
479
519
|
* Evaluate a script on the first context that returns a truthy value.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Single-responsibility module for applying the user's default model
|
|
4
|
+
* preference when a CDP session connects.
|
|
5
|
+
*
|
|
6
|
+
* Strategy: exact match only — no fuzzy matching to avoid selecting
|
|
7
|
+
* the wrong model after Antigravity renames a model.
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.applyDefaultModel = applyDefaultModel;
|
|
11
|
+
const logger_1 = require("../utils/logger");
|
|
12
|
+
/**
|
|
13
|
+
* Apply the user's default model preference to a CDP session.
|
|
14
|
+
*
|
|
15
|
+
* 1. Read modelService.getDefaultModel() — if null, skip
|
|
16
|
+
* 2. Read cdp.getCurrentModel() — if already matches, skip (mark synced)
|
|
17
|
+
* 3. Get available models via cdp.getUiModels()
|
|
18
|
+
* 4. Exact match → cdp.setUiModel() → mark synced
|
|
19
|
+
* 5. No match → return stale result with message + available model list
|
|
20
|
+
*/
|
|
21
|
+
async function applyDefaultModel(cdp, modelService) {
|
|
22
|
+
const defaultModel = modelService.getDefaultModel();
|
|
23
|
+
if (!defaultModel) {
|
|
24
|
+
return { applied: false, modelName: null, stale: false, staleMessage: null };
|
|
25
|
+
}
|
|
26
|
+
const currentModel = await cdp.getCurrentModel();
|
|
27
|
+
if (currentModel && currentModel.toLowerCase() === defaultModel.toLowerCase()) {
|
|
28
|
+
modelService.markSynced();
|
|
29
|
+
logger_1.logger.debug(`[DefaultModelApplicator] Already on default model: ${defaultModel}`);
|
|
30
|
+
return { applied: true, modelName: defaultModel, stale: false, staleMessage: null };
|
|
31
|
+
}
|
|
32
|
+
const availableModels = await cdp.getUiModels();
|
|
33
|
+
const exactMatch = availableModels.find(m => m.toLowerCase() === defaultModel.toLowerCase());
|
|
34
|
+
if (exactMatch) {
|
|
35
|
+
const result = await cdp.setUiModel(exactMatch);
|
|
36
|
+
if (result.ok) {
|
|
37
|
+
modelService.markSynced();
|
|
38
|
+
logger_1.logger.debug(`[DefaultModelApplicator] Applied default model: ${exactMatch}`);
|
|
39
|
+
return { applied: true, modelName: exactMatch, stale: false, staleMessage: null };
|
|
40
|
+
}
|
|
41
|
+
logger_1.logger.warn(`[DefaultModelApplicator] setUiModel failed: ${result.error}`);
|
|
42
|
+
return { applied: false, modelName: defaultModel, stale: false, staleMessage: null };
|
|
43
|
+
}
|
|
44
|
+
// No exact match — model is stale
|
|
45
|
+
const availableList = availableModels.join(', ');
|
|
46
|
+
const staleMessage = `Saved default model "${defaultModel}" is no longer available. Available models: ${availableList}`;
|
|
47
|
+
logger_1.logger.warn(`[DefaultModelApplicator] ${staleMessage}`);
|
|
48
|
+
return {
|
|
49
|
+
applied: false,
|
|
50
|
+
modelName: defaultModel,
|
|
51
|
+
stale: true,
|
|
52
|
+
staleMessage,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -33,17 +33,31 @@ exports.DEFAULT_MODE = 'fast';
|
|
|
33
33
|
*/
|
|
34
34
|
class ModeService {
|
|
35
35
|
currentMode = exports.DEFAULT_MODE;
|
|
36
|
+
pendingSync = false;
|
|
36
37
|
/**
|
|
37
38
|
* Get the current execution mode
|
|
38
39
|
*/
|
|
39
40
|
getCurrentMode() {
|
|
40
41
|
return this.currentMode;
|
|
41
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Check if the current mode is pending sync to Antigravity
|
|
45
|
+
*/
|
|
46
|
+
isPendingSync() {
|
|
47
|
+
return this.pendingSync;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Mark the pending mode as synced (clears pendingSync flag)
|
|
51
|
+
*/
|
|
52
|
+
markSynced() {
|
|
53
|
+
this.pendingSync = false;
|
|
54
|
+
}
|
|
42
55
|
/**
|
|
43
56
|
* Switch execution mode
|
|
44
57
|
* @param modeName Mode name to set (case-insensitive)
|
|
58
|
+
* @param synced Whether the mode has been synced to Antigravity (default: false)
|
|
45
59
|
*/
|
|
46
|
-
setMode(modeName) {
|
|
60
|
+
setMode(modeName, synced = false) {
|
|
47
61
|
if (!modeName || modeName.trim() === '') {
|
|
48
62
|
return {
|
|
49
63
|
success: false,
|
|
@@ -58,6 +72,7 @@ class ModeService {
|
|
|
58
72
|
};
|
|
59
73
|
}
|
|
60
74
|
this.currentMode = normalized;
|
|
75
|
+
this.pendingSync = !synced;
|
|
61
76
|
return {
|
|
62
77
|
success: true,
|
|
63
78
|
mode: this.currentMode,
|