lazy-gravity 0.4.0 → 0.5.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 CHANGED
@@ -92,6 +92,7 @@ const selectHandler_1 = require("../handlers/selectHandler");
92
92
  const approvalButtonAction_1 = require("../handlers/approvalButtonAction");
93
93
  const planningButtonAction_1 = require("../handlers/planningButtonAction");
94
94
  const errorPopupButtonAction_1 = require("../handlers/errorPopupButtonAction");
95
+ const runCommandButtonAction_1 = require("../handlers/runCommandButtonAction");
95
96
  const modelButtonAction_1 = require("../handlers/modelButtonAction");
96
97
  const autoAcceptButtonAction_1 = require("../handlers/autoAcceptButtonAction");
97
98
  const templateButtonAction_1 = require("../handlers/templateButtonAction");
@@ -831,6 +832,7 @@ const startBot = async (cliLogLevel) => {
831
832
  parseApprovalCustomId: cdpBridgeManager_1.parseApprovalCustomId,
832
833
  parseErrorPopupCustomId: cdpBridgeManager_1.parseErrorPopupCustomId,
833
834
  parsePlanningCustomId: cdpBridgeManager_1.parsePlanningCustomId,
835
+ parseRunCommandCustomId: cdpBridgeManager_1.parseRunCommandCustomId,
834
836
  joinHandler,
835
837
  userPrefRepo,
836
838
  handleSlashInteraction: async (interaction, handler, bridgeArg, wsHandlerArg, chatHandlerArg, cleanupHandlerArg, modeServiceArg, modelServiceArg, autoAcceptServiceArg, clientArg) => handleSlashInteraction(interaction, handler, bridgeArg, wsHandlerArg, chatHandlerArg, cleanupHandlerArg, modeServiceArg, modelServiceArg, autoAcceptServiceArg, clientArg, promptDispatcher, templateRepo, joinHandler, userPrefRepo),
@@ -862,6 +864,7 @@ const startBot = async (cliLogLevel) => {
862
864
  (0, cdpBridgeManager_1.ensureApprovalDetector)(bridge, cdp, projectName);
863
865
  (0, cdpBridgeManager_1.ensureErrorPopupDetector)(bridge, cdp, projectName);
864
866
  (0, cdpBridgeManager_1.ensurePlanningDetector)(bridge, cdp, projectName);
867
+ (0, cdpBridgeManager_1.ensureRunCommandDetector)(bridge, cdp, projectName);
865
868
  }
866
869
  catch (e) {
867
870
  await interaction.followUp({
@@ -998,6 +1001,7 @@ const startBot = async (cliLogLevel) => {
998
1001
  (0, approvalButtonAction_1.createApprovalButtonAction)({ bridge }),
999
1002
  (0, planningButtonAction_1.createPlanningButtonAction)({ bridge }),
1000
1003
  (0, errorPopupButtonAction_1.createErrorPopupButtonAction)({ bridge }),
1004
+ (0, runCommandButtonAction_1.createRunCommandButtonAction)({ bridge }),
1001
1005
  (0, modelButtonAction_1.createModelButtonAction)({ bridge, fetchQuota: () => bridge.quota.fetchQuota(), modelService, userPrefRepo }),
1002
1006
  (0, autoAcceptButtonAction_1.createAutoAcceptButtonAction)({ autoAcceptService: bridge.autoAccept }),
1003
1007
  (0, templateButtonAction_1.createTemplateButtonAction)({ bridge, templateRepo }),
@@ -134,6 +134,7 @@ function createTelegramMessageHandler(deps) {
134
134
  (0, cdpBridgeManager_1.ensureApprovalDetector)(deps.bridge, cdp, projectName);
135
135
  (0, cdpBridgeManager_1.ensureErrorPopupDetector)(deps.bridge, cdp, projectName);
136
136
  (0, cdpBridgeManager_1.ensurePlanningDetector)(deps.bridge, cdp, projectName);
137
+ (0, cdpBridgeManager_1.ensureRunCommandDetector)(deps.bridge, cdp, projectName);
137
138
  // Acknowledge receipt
138
139
  await message.react('\u{1F440}').catch(() => { });
139
140
  // Download image attachments if present
@@ -366,6 +366,72 @@ function createInteractionCreateHandler(deps) {
366
366
  }
367
367
  return;
368
368
  }
369
+ const runCommandAction = deps.parseRunCommandCustomId(interaction.customId);
370
+ if (runCommandAction) {
371
+ if (runCommandAction.channelId && runCommandAction.channelId !== interaction.channelId) {
372
+ await interaction.reply({
373
+ content: (0, i18n_1.t)('This run command action is linked to a different session channel.'),
374
+ flags: discord_js_1.MessageFlags.Ephemeral,
375
+ }).catch(logger_1.logger.error);
376
+ return;
377
+ }
378
+ const runCmdWorkspace = runCommandAction.projectName ?? deps.bridge.lastActiveWorkspace;
379
+ const runCmdDetector = runCmdWorkspace
380
+ ? deps.bridge.pool.getRunCommandDetector(runCmdWorkspace)
381
+ : undefined;
382
+ if (!runCmdDetector) {
383
+ try {
384
+ await interaction.reply({ content: (0, i18n_1.t)('Run command detector not found.'), flags: discord_js_1.MessageFlags.Ephemeral });
385
+ }
386
+ catch { /* ignore */ }
387
+ return;
388
+ }
389
+ let success = false;
390
+ let actionLabel = '';
391
+ if (runCommandAction.action === 'run') {
392
+ success = await runCmdDetector.runButton();
393
+ actionLabel = (0, i18n_1.t)('Run');
394
+ }
395
+ else {
396
+ success = await runCmdDetector.rejectButton();
397
+ actionLabel = (0, i18n_1.t)('Reject');
398
+ }
399
+ try {
400
+ if (success) {
401
+ const originalEmbed = interaction.message.embeds[0];
402
+ const updatedEmbed = originalEmbed
403
+ ? discord_js_1.EmbedBuilder.from(originalEmbed)
404
+ : new discord_js_1.EmbedBuilder().setTitle('Run Command');
405
+ const historyText = `${actionLabel} by <@${interaction.user.id}> (${new Date().toLocaleString('ja-JP')})`;
406
+ updatedEmbed
407
+ .setColor(runCommandAction.action === 'reject' ? 0xE74C3C : 0x2ECC71)
408
+ .addFields({ name: 'Action History', value: historyText, inline: false })
409
+ .setTimestamp();
410
+ await interaction.update({
411
+ embeds: [updatedEmbed],
412
+ components: (0, discordButtonUtils_1.disableAllButtons)(interaction.message.components),
413
+ });
414
+ }
415
+ else {
416
+ await interaction.reply({ content: (0, i18n_1.t)('Run command button not found.'), flags: discord_js_1.MessageFlags.Ephemeral });
417
+ }
418
+ }
419
+ catch (interactionError) {
420
+ if (interactionError?.code === 10062 || interactionError?.code === 40060) {
421
+ logger_1.logger.warn('[RunCommand] Interaction expired. Responding directly in the channel.');
422
+ if (interaction.channel && 'send' in interaction.channel) {
423
+ const fallbackMessage = success
424
+ ? `${actionLabel} completed.`
425
+ : (0, i18n_1.t)('Run command button not found.');
426
+ await interaction.channel.send(fallbackMessage).catch(logger_1.logger.error);
427
+ }
428
+ }
429
+ else {
430
+ throw interactionError;
431
+ }
432
+ }
433
+ return;
434
+ }
369
435
  if (interaction.customId === cleanupCommandHandler_1.CLEANUP_ARCHIVE_BTN) {
370
436
  await deps.cleanupHandler.handleArchive(interaction);
371
437
  return;
@@ -14,6 +14,7 @@ function createMessageCreateHandler(deps) {
14
14
  const ensureApprovalDetector = deps.ensureApprovalDetector ?? cdpBridgeManager_1.ensureApprovalDetector;
15
15
  const ensureErrorPopupDetector = deps.ensureErrorPopupDetector ?? cdpBridgeManager_1.ensureErrorPopupDetector;
16
16
  const ensurePlanningDetector = deps.ensurePlanningDetector ?? cdpBridgeManager_1.ensurePlanningDetector;
17
+ const ensureRunCommandDetector = deps.ensureRunCommandDetector ?? cdpBridgeManager_1.ensureRunCommandDetector;
17
18
  const registerApprovalWorkspaceChannel = deps.registerApprovalWorkspaceChannel ?? cdpBridgeManager_1.registerApprovalWorkspaceChannel;
18
19
  const registerApprovalSessionChannel = deps.registerApprovalSessionChannel ?? cdpBridgeManager_1.registerApprovalSessionChannel;
19
20
  const downloadInboundImageAttachments = deps.downloadInboundImageAttachments ?? imageHandler_1.downloadInboundImageAttachments;
@@ -170,6 +171,7 @@ function createMessageCreateHandler(deps) {
170
171
  ensureApprovalDetector(deps.bridge, cdp, projectName);
171
172
  ensureErrorPopupDetector(deps.bridge, cdp, projectName);
172
173
  ensurePlanningDetector(deps.bridge, cdp, projectName);
174
+ ensureRunCommandDetector(deps.bridge, cdp, projectName);
173
175
  const session = deps.chatSessionRepo.findByChannelId(message.channelId);
174
176
  if (session?.displayName) {
175
177
  registerApprovalSessionChannel(deps.bridge, projectName, session.displayName, platformChannel);
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ /**
3
+ * Platform-agnostic run command button action.
4
+ *
5
+ * Handles Run / Reject button presses for the "Run command?"
6
+ * dialog from both Discord and Telegram using the ButtonAction interface.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.createRunCommandButtonAction = createRunCommandButtonAction;
10
+ const cdpBridgeManager_1 = require("../services/cdpBridgeManager");
11
+ const logger_1 = require("../utils/logger");
12
+ function createRunCommandButtonAction(deps) {
13
+ return {
14
+ match(customId) {
15
+ const parsed = (0, cdpBridgeManager_1.parseRunCommandCustomId)(customId);
16
+ if (!parsed)
17
+ return null;
18
+ return {
19
+ action: parsed.action,
20
+ projectName: parsed.projectName ?? '',
21
+ channelId: parsed.channelId ?? '',
22
+ };
23
+ },
24
+ async execute(interaction, params) {
25
+ const { action, channelId } = params;
26
+ // Acknowledge immediately so Telegram doesn't time out
27
+ await interaction.deferUpdate().catch(() => { });
28
+ if (channelId && channelId !== interaction.channel.id) {
29
+ await interaction
30
+ .reply({ text: 'This run command action is linked to a different session channel.' })
31
+ .catch(() => { });
32
+ return;
33
+ }
34
+ const projectName = params.projectName || deps.bridge.lastActiveWorkspace;
35
+ const detector = projectName
36
+ ? deps.bridge.pool.getRunCommandDetector(projectName)
37
+ : undefined;
38
+ if (!detector) {
39
+ logger_1.logger.warn(`[RunCommandAction] No detector for project=${projectName}`);
40
+ await interaction
41
+ .reply({ text: 'Run command detector not found.' })
42
+ .catch(() => { });
43
+ return;
44
+ }
45
+ let success = false;
46
+ let actionLabel = '';
47
+ try {
48
+ if (action === 'run') {
49
+ success = await detector.runButton();
50
+ actionLabel = 'Run';
51
+ }
52
+ else {
53
+ success = await detector.rejectButton();
54
+ actionLabel = 'Reject';
55
+ }
56
+ }
57
+ catch (err) {
58
+ const msg = err instanceof Error ? err.message : String(err);
59
+ logger_1.logger.error(`[RunCommandAction] CDP click failed: ${msg}`);
60
+ await interaction
61
+ .reply({ text: `Run command action failed: ${msg}` })
62
+ .catch(() => { });
63
+ return;
64
+ }
65
+ if (success) {
66
+ await interaction
67
+ .update({ text: `${action === 'run' ? '▶️' : '⛔'} ${actionLabel} completed`, components: [] })
68
+ .catch((err) => {
69
+ logger_1.logger.warn('[RunCommandAction] update failed, trying editReply:', err);
70
+ interaction.editReply({ text: `${action === 'run' ? '▶️' : '⛔'} ${actionLabel} completed`, components: [] })
71
+ .catch((editErr) => {
72
+ logger_1.logger.warn('[RunCommandAction] editReply failed, sending followUp:', editErr);
73
+ interaction.followUp({ text: `${action === 'run' ? '▶️' : '⛔'} ${actionLabel} completed` }).catch(() => { });
74
+ });
75
+ });
76
+ }
77
+ else {
78
+ await interaction
79
+ .reply({ text: 'Run command button not found.' })
80
+ .catch(() => { });
81
+ }
82
+ },
83
+ };
84
+ }
@@ -10,11 +10,14 @@ exports.buildPlanningCustomId = buildPlanningCustomId;
10
10
  exports.parsePlanningCustomId = parsePlanningCustomId;
11
11
  exports.buildErrorPopupCustomId = buildErrorPopupCustomId;
12
12
  exports.parseErrorPopupCustomId = parseErrorPopupCustomId;
13
+ exports.buildRunCommandCustomId = buildRunCommandCustomId;
14
+ exports.parseRunCommandCustomId = parseRunCommandCustomId;
13
15
  exports.initCdpBridge = initCdpBridge;
14
16
  exports.getCurrentCdp = getCurrentCdp;
15
17
  exports.ensureApprovalDetector = ensureApprovalDetector;
16
18
  exports.ensurePlanningDetector = ensurePlanningDetector;
17
19
  exports.ensureErrorPopupDetector = ensureErrorPopupDetector;
20
+ exports.ensureRunCommandDetector = ensureRunCommandDetector;
18
21
  exports.ensureUserMessageDetector = ensureUserMessageDetector;
19
22
  const i18n_1 = require("../utils/i18n");
20
23
  const logger_1 = require("../utils/logger");
@@ -24,6 +27,7 @@ const autoAcceptService_1 = require("./autoAcceptService");
24
27
  const cdpConnectionPool_1 = require("./cdpConnectionPool");
25
28
  const errorPopupDetector_1 = require("./errorPopupDetector");
26
29
  const planningDetector_1 = require("./planningDetector");
30
+ const runCommandDetector_1 = require("./runCommandDetector");
27
31
  const quotaService_1 = require("./quotaService");
28
32
  const userMessageDetector_1 = require("./userMessageDetector");
29
33
  const APPROVE_ACTION_PREFIX = 'approve_action';
@@ -34,6 +38,8 @@ const PLANNING_PROCEED_ACTION_PREFIX = 'planning_proceed_action';
34
38
  const ERROR_POPUP_DISMISS_ACTION_PREFIX = 'error_popup_dismiss_action';
35
39
  const ERROR_POPUP_COPY_DEBUG_ACTION_PREFIX = 'error_popup_copy_debug_action';
36
40
  const ERROR_POPUP_RETRY_ACTION_PREFIX = 'error_popup_retry_action';
41
+ const RUN_COMMAND_RUN_ACTION_PREFIX = 'run_command_run_action';
42
+ const RUN_COMMAND_REJECT_ACTION_PREFIX = 'run_command_reject_action';
37
43
  function normalizeSessionTitle(title) {
38
44
  return title.trim().toLowerCase();
39
45
  }
@@ -194,6 +200,34 @@ function parseErrorPopupCustomId(customId) {
194
200
  }
195
201
  return null;
196
202
  }
203
+ function buildRunCommandCustomId(action, projectName, channelId) {
204
+ const prefix = action === 'run'
205
+ ? RUN_COMMAND_RUN_ACTION_PREFIX
206
+ : RUN_COMMAND_REJECT_ACTION_PREFIX;
207
+ if (channelId && channelId.trim().length > 0) {
208
+ return `${prefix}:${projectName}:${channelId}`;
209
+ }
210
+ return `${prefix}:${projectName}`;
211
+ }
212
+ function parseRunCommandCustomId(customId) {
213
+ if (customId === RUN_COMMAND_RUN_ACTION_PREFIX) {
214
+ return { action: 'run', projectName: null, channelId: null };
215
+ }
216
+ if (customId === RUN_COMMAND_REJECT_ACTION_PREFIX) {
217
+ return { action: 'reject', projectName: null, channelId: null };
218
+ }
219
+ if (customId.startsWith(`${RUN_COMMAND_RUN_ACTION_PREFIX}:`)) {
220
+ const rest = customId.substring(`${RUN_COMMAND_RUN_ACTION_PREFIX}:`.length);
221
+ const [projectName, channelId] = rest.split(':');
222
+ return { action: 'run', projectName: projectName || null, channelId: channelId || null };
223
+ }
224
+ if (customId.startsWith(`${RUN_COMMAND_REJECT_ACTION_PREFIX}:`)) {
225
+ const rest = customId.substring(`${RUN_COMMAND_REJECT_ACTION_PREFIX}:`.length);
226
+ const [projectName, channelId] = rest.split(':');
227
+ return { action: 'reject', projectName: projectName || null, channelId: channelId || null };
228
+ }
229
+ return null;
230
+ }
197
231
  /** Initialize the CDP bridge (lazy connection: pool creation only) */
198
232
  function initCdpBridge(autoApproveDefault) {
199
233
  const pool = new cdpConnectionPool_1.CdpConnectionPool({
@@ -412,6 +446,74 @@ function ensureErrorPopupDetector(bridge, cdp, projectName) {
412
446
  bridge.pool.registerErrorPopupDetector(projectName, detector);
413
447
  logger_1.logger.debug(`[ErrorPopupDetector:${projectName}] Started error popup detection`);
414
448
  }
449
+ /**
450
+ * Helper to start a run command detector for each workspace.
451
+ * Detects "Run command?" confirmation dialogs and forwards them to Discord.
452
+ * Does nothing if a detector for the same workspace is already running.
453
+ */
454
+ function ensureRunCommandDetector(bridge, cdp, projectName) {
455
+ const existing = bridge.pool.getRunCommandDetector(projectName);
456
+ if (existing && existing.isActive())
457
+ return;
458
+ let lastNotification = null;
459
+ const detector = new runCommandDetector_1.RunCommandDetector({
460
+ cdpService: cdp,
461
+ pollIntervalMs: 2000,
462
+ onResolved: () => {
463
+ if (!lastNotification)
464
+ return;
465
+ const { sent, payload } = lastNotification;
466
+ lastNotification = null;
467
+ const resolved = (0, notificationSender_1.buildResolvedOverlay)(payload, (0, i18n_1.t)('Resolved in Antigravity'));
468
+ sent.edit(resolved).catch(logger_1.logger.error);
469
+ },
470
+ onRunCommandRequired: async (info) => {
471
+ logger_1.logger.debug(`[RunCommandDetector:${projectName}] Run command detected`);
472
+ const currentChatTitle = await getCurrentChatTitle(cdp);
473
+ const targetChannel = resolveApprovalChannelForCurrentChat(bridge, projectName, currentChatTitle);
474
+ const targetChannelId = targetChannel ? targetChannel.id : '';
475
+ if (!targetChannel || !targetChannelId) {
476
+ logger_1.logger.warn(`[RunCommandDetector:${projectName}] Skipped run command notification because chat is not linked to a session` +
477
+ `${currentChatTitle ? ` (title="${currentChatTitle}")` : ''}`);
478
+ return;
479
+ }
480
+ if (bridge.autoAccept.isEnabled()) {
481
+ const accepted = await detector.runButton();
482
+ const autoPayload = (0, notificationSender_1.buildAutoApprovedNotification)({
483
+ accepted,
484
+ projectName,
485
+ description: `Run: ${info.commandText}`,
486
+ approveText: info.runText ?? 'Run',
487
+ });
488
+ await targetChannel.send(autoPayload).catch(logger_1.logger.error);
489
+ if (accepted) {
490
+ return;
491
+ }
492
+ }
493
+ const payload = (0, notificationSender_1.buildRunCommandNotification)({
494
+ title: (0, i18n_1.t)('Run Command?'),
495
+ commandText: info.commandText,
496
+ workingDirectory: info.workingDirectory,
497
+ projectName,
498
+ channelId: targetChannelId,
499
+ extraFields: [
500
+ { name: (0, i18n_1.t)('Run button'), value: info.runText, inline: true },
501
+ { name: (0, i18n_1.t)('Reject button'), value: info.rejectText, inline: true },
502
+ ],
503
+ });
504
+ const sent = await targetChannel.send(payload).catch((err) => {
505
+ logger_1.logger.error(err);
506
+ return null;
507
+ });
508
+ if (sent) {
509
+ lastNotification = { sent, payload };
510
+ }
511
+ },
512
+ });
513
+ detector.start();
514
+ bridge.pool.registerRunCommandDetector(projectName, detector);
515
+ logger_1.logger.debug(`[RunCommandDetector:${projectName}] Started run command detection`);
516
+ }
415
517
  /**
416
518
  * Helper to start a user message detector for a workspace.
417
519
  * Detects messages typed directly in the Antigravity UI (e.g., from a PC)
@@ -16,6 +16,7 @@ class CdpConnectionPool {
16
16
  approvalDetectors = new Map();
17
17
  errorPopupDetectors = new Map();
18
18
  planningDetectors = new Map();
19
+ runCommandDetectors = new Map();
19
20
  userMessageDetectors = new Map();
20
21
  connectingPromises = new Map();
21
22
  cdpOptions;
@@ -92,6 +93,11 @@ class CdpConnectionPool {
92
93
  planningDetector.stop();
93
94
  this.planningDetectors.delete(projectName);
94
95
  }
96
+ const runCmdDetector = this.runCommandDetectors.get(projectName);
97
+ if (runCmdDetector) {
98
+ runCmdDetector.stop();
99
+ this.runCommandDetectors.delete(projectName);
100
+ }
95
101
  const userMsgDetector = this.userMessageDetectors.get(projectName);
96
102
  if (userMsgDetector) {
97
103
  userMsgDetector.stop();
@@ -157,6 +163,22 @@ class CdpConnectionPool {
157
163
  getPlanningDetector(projectName) {
158
164
  return this.planningDetectors.get(projectName);
159
165
  }
166
+ /**
167
+ * Register a run command detector for a workspace.
168
+ */
169
+ registerRunCommandDetector(projectName, detector) {
170
+ const existing = this.runCommandDetectors.get(projectName);
171
+ if (existing && existing.isActive()) {
172
+ existing.stop();
173
+ }
174
+ this.runCommandDetectors.set(projectName, detector);
175
+ }
176
+ /**
177
+ * Get the run command detector for a workspace.
178
+ */
179
+ getRunCommandDetector(projectName) {
180
+ return this.runCommandDetectors.get(projectName);
181
+ }
160
182
  /**
161
183
  * Register a user message detector for a workspace.
162
184
  */
@@ -226,6 +248,11 @@ class CdpConnectionPool {
226
248
  planDetector.stop();
227
249
  this.planningDetectors.delete(projectName);
228
250
  }
251
+ const runCmdDetector = this.runCommandDetectors.get(projectName);
252
+ if (runCmdDetector) {
253
+ runCmdDetector.stop();
254
+ this.runCommandDetectors.delete(projectName);
255
+ }
229
256
  const userMsgDetector = this.userMessageDetectors.get(projectName);
230
257
  if (userMsgDetector) {
231
258
  userMsgDetector.stop();
@@ -78,8 +78,11 @@ 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');
81
+ // Past Conversations opens as a floating QuickInput dialog, not inside the side panel.
82
+ // Try the visible QuickInput dialog first, then fall back to the side panel.
83
+ const quickInputPanels = Array.from(document.querySelectorAll('div[class*="bg-quickinput-background"]'));
84
+ const panel = quickInputPanels.find((el) => isVisible(el))
85
+ || document.querySelector('.antigravity-agent-side-panel');
83
86
  if (!panel) return null;
84
87
 
85
88
  const items = [];
@@ -136,7 +139,11 @@ const SCRAPE_PAST_CONVERSATIONS_SCRIPT = `(() => {
136
139
  */
137
140
  const FIND_SHOW_MORE_BUTTON_SCRIPT = `(() => {
138
141
  const isVisible = (el) => !!el && el instanceof HTMLElement && el.offsetParent !== null;
139
- const els = Array.from(document.querySelectorAll('div, span'));
142
+ const quickInputPanels = Array.from(document.querySelectorAll('div[class*="bg-quickinput-background"]'));
143
+ const root = quickInputPanels.find((el) => isVisible(el))
144
+ || document.querySelector('.antigravity-agent-side-panel')
145
+ || document;
146
+ const els = Array.from(root.querySelectorAll('div, span'));
140
147
  for (const el of els) {
141
148
  if (!isVisible(el)) continue;
142
149
  const text = (el.textContent || '').trim();
@@ -454,7 +461,9 @@ class ChatSessionService {
454
461
  // Step 3: Wait for panel to render (poll for content, up to 3s)
455
462
  const PANEL_READY_CHECK = `(() => {
456
463
  const isVisible = (el) => !!el && el instanceof HTMLElement && el.offsetParent !== null;
457
- const panel = document.querySelector('.antigravity-agent-side-panel');
464
+ const quickInputPanels = Array.from(document.querySelectorAll('div[class*="bg-quickinput-background"]'));
465
+ const panel = quickInputPanels.find((el) => isVisible(el))
466
+ || document.querySelector('.antigravity-agent-side-panel');
458
467
  if (!panel) return false;
459
468
  const containers = Array.from(
460
469
  panel.querySelectorAll('div[class*="overflow-auto"], div[class*="overflow-y-scroll"]')
@@ -9,6 +9,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
9
9
  exports.buildApprovalNotification = buildApprovalNotification;
10
10
  exports.buildPlanningNotification = buildPlanningNotification;
11
11
  exports.buildErrorPopupNotification = buildErrorPopupNotification;
12
+ exports.buildRunCommandNotification = buildRunCommandNotification;
12
13
  exports.buildAutoApprovedNotification = buildAutoApprovedNotification;
13
14
  exports.buildResolvedOverlay = buildResolvedOverlay;
14
15
  exports.buildStatusNotification = buildStatusNotification;
@@ -25,6 +26,8 @@ const PLANNING_PROCEED_ACTION_PREFIX = 'planning_proceed_action';
25
26
  const ERROR_POPUP_DISMISS_ACTION_PREFIX = 'error_popup_dismiss_action';
26
27
  const ERROR_POPUP_COPY_DEBUG_ACTION_PREFIX = 'error_popup_copy_debug_action';
27
28
  const ERROR_POPUP_RETRY_ACTION_PREFIX = 'error_popup_retry_action';
29
+ const RUN_COMMAND_RUN_ACTION_PREFIX = 'run_command_run_action';
30
+ const RUN_COMMAND_REJECT_ACTION_PREFIX = 'run_command_reject_action';
28
31
  // ---------------------------------------------------------------------------
29
32
  // Notification colours
30
33
  // ---------------------------------------------------------------------------
@@ -107,6 +110,21 @@ function buildErrorPopupNotification(opts) {
107
110
  ];
108
111
  return { richContent, components };
109
112
  }
113
+ /** Build the run command notification message. */
114
+ function buildRunCommandNotification(opts) {
115
+ const { title, commandText, workingDirectory, projectName, channelId, extraFields } = opts;
116
+ const safeCommandText = (commandText || '')
117
+ .replace(/```/g, '`\u200b``')
118
+ .slice(0, 3800);
119
+ const safeWorkingDirectory = (workingDirectory || '(unknown)').slice(0, 1024);
120
+ const richContent = (0, richContentBuilder_1.pipe)((0, richContentBuilder_1.createRichContent)(), (rc) => (0, richContentBuilder_1.withTitle)(rc, title), (rc) => (0, richContentBuilder_1.withDescription)(rc, `\`\`\`\n${safeCommandText}\n\`\`\``), (rc) => (0, richContentBuilder_1.withColor)(rc, COLOR_APPROVAL), (rc) => (0, richContentBuilder_1.addField)(rc, 'Directory', safeWorkingDirectory, true), (rc) => (0, richContentBuilder_1.addField)(rc, 'Project', projectName, true), (rc) => extraFields
121
+ ? extraFields.reduce((acc, f) => (0, richContentBuilder_1.addField)(acc, f.name, f.value, f.inline), rc)
122
+ : rc, (rc) => (0, richContentBuilder_1.withFooter)(rc, 'Run command approval required'), (rc) => (0, richContentBuilder_1.withTimestamp)(rc));
123
+ const components = [
124
+ buttonRow(button(customId(RUN_COMMAND_RUN_ACTION_PREFIX, projectName, channelId), 'Run', 'success'), button(customId(RUN_COMMAND_REJECT_ACTION_PREFIX, projectName, channelId), 'Reject', 'danger')),
125
+ ];
126
+ return { richContent, components };
127
+ }
110
128
  /** Build an auto-approved notification (shown when auto-accept fires). */
111
129
  function buildAutoApprovedNotification(opts) {
112
130
  const { accepted, projectName, description, approveText } = opts;
@@ -0,0 +1,258 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RunCommandDetector = void 0;
4
+ const logger_1 = require("../utils/logger");
5
+ const approvalDetector_1 = require("./approvalDetector");
6
+ /**
7
+ * CDP detection script for the "Run command?" dialog in Claude Code / Antigravity.
8
+ *
9
+ * DOM structure (from user-provided inspection):
10
+ * <div class="flex flex-col gap-2 border-gray-500/25 border rounded-lg my-1">
11
+ * <div>
12
+ * <div class="... border-b ..."><span class="opacity-60">Run command?</span></div>
13
+ * <div class="...">
14
+ * <pre class="whitespace-pre-wrap break-all font-mono text-sm">
15
+ * <span class="... opacity-50">~/Code/login</span>
16
+ * <span class="opacity-50"> $ </span>python3 -m http.server 8000
17
+ * </pre>
18
+ * </div>
19
+ * <div class="... border-t ...">
20
+ * <button>Reject</button>
21
+ * <button>Run</button> (split button with chevron for options)
22
+ * </div>
23
+ * </div>
24
+ * </div>
25
+ */
26
+ const DETECT_RUN_COMMAND_SCRIPT = `(() => {
27
+ const RUN_COMMAND_HEADER_PATTERNS = [
28
+ 'run command?', 'run command', 'execute command',
29
+ 'コマンドを実行', 'コマンド実行'
30
+ ];
31
+ const RUN_PATTERNS = ['run', '実行', 'execute'];
32
+ const REJECT_PATTERNS = ['reject', 'cancel', '拒否', 'キャンセル'];
33
+
34
+ const normalize = (text) => (text || '').toLowerCase().replace(/\\s+/g, ' ').trim();
35
+
36
+ // Find the "Run command?" header span (reverse order to prefer newest card)
37
+ const allSpans = Array.from(document.querySelectorAll('span')).reverse();
38
+ const headerSpan = allSpans.find(span => {
39
+ if (!span.offsetParent && span.offsetParent !== document.body) {
40
+ const rect = span.getBoundingClientRect();
41
+ if (rect.width === 0 && rect.height === 0) return false;
42
+ }
43
+ const t = normalize(span.textContent || '');
44
+ return RUN_COMMAND_HEADER_PATTERNS.some(p => t.includes(p));
45
+ });
46
+ if (!headerSpan) return null;
47
+
48
+ // Navigate up to the rounded-lg container
49
+ const container = headerSpan.closest('div[class*="rounded-lg"][class*="border"]')
50
+ || headerSpan.closest('div[class*="gap-2"]')
51
+ || headerSpan.parentElement?.parentElement?.parentElement;
52
+ if (!container) return null;
53
+
54
+ // Extract command text from <pre> element
55
+ const pre = container.querySelector('pre');
56
+ if (!pre) return null;
57
+
58
+ const preText = (pre.textContent || '').trim();
59
+ // Format: "~/Code/login $ python3 -m http.server 8000"
60
+ // Split on " $ " to separate working directory from command
61
+ const dollarIdx = preText.indexOf(' $ ');
62
+ let commandText = preText;
63
+ let workingDirectory = '';
64
+ if (dollarIdx >= 0) {
65
+ workingDirectory = preText.substring(0, dollarIdx).trim();
66
+ commandText = preText.substring(dollarIdx + 3).trim();
67
+ }
68
+
69
+ // Find Run and Reject buttons within the container
70
+ const containerButtons = Array.from(container.querySelectorAll('button'))
71
+ .filter(btn => {
72
+ if (btn.offsetParent !== null) return true;
73
+ const rect = btn.getBoundingClientRect();
74
+ return rect.width > 0 && rect.height > 0;
75
+ });
76
+
77
+ const runBtn = containerButtons.find(btn => {
78
+ const t = normalize(btn.textContent || '');
79
+ // Exclude buttons that are clearly not the Run button (dropdowns, copy, etc.)
80
+ if (t === '' || t.length > 30) return false;
81
+ return RUN_PATTERNS.some(p => t === p || t.startsWith(p));
82
+ });
83
+
84
+ const rejectBtn = containerButtons.find(btn => {
85
+ const t = normalize(btn.textContent || '');
86
+ if (t === '' || t.length > 30) return false;
87
+ return REJECT_PATTERNS.some(p => t === p || t.startsWith(p));
88
+ });
89
+
90
+ if (!runBtn || !rejectBtn) return null;
91
+
92
+ return {
93
+ commandText,
94
+ workingDirectory,
95
+ runText: (runBtn.textContent || '').trim(),
96
+ rejectText: (rejectBtn.textContent || '').trim(),
97
+ };
98
+ })()`;
99
+ /**
100
+ * Class that detects "Run command?" dialogs in the Antigravity UI via polling.
101
+ *
102
+ * Notifies detected dialog info through the onRunCommandRequired callback,
103
+ * and performs the actual click operations via runButton() / rejectButton() methods.
104
+ */
105
+ class RunCommandDetector {
106
+ cdpService;
107
+ pollIntervalMs;
108
+ onRunCommandRequired;
109
+ onResolved;
110
+ pollTimer = null;
111
+ isRunning = false;
112
+ /** Key of the last detected dialog (for duplicate notification prevention) */
113
+ lastDetectedKey = null;
114
+ /** Full RunCommandInfo from the last detection (used for clicking) */
115
+ lastDetectedInfo = null;
116
+ constructor(options) {
117
+ this.cdpService = options.cdpService;
118
+ this.pollIntervalMs = options.pollIntervalMs ?? 1500;
119
+ this.onRunCommandRequired = options.onRunCommandRequired;
120
+ this.onResolved = options.onResolved;
121
+ }
122
+ /** Start monitoring. */
123
+ start() {
124
+ if (this.isRunning)
125
+ return;
126
+ this.isRunning = true;
127
+ this.lastDetectedKey = null;
128
+ this.lastDetectedInfo = null;
129
+ this.schedulePoll();
130
+ }
131
+ /** Stop monitoring. */
132
+ async stop() {
133
+ this.isRunning = false;
134
+ if (this.pollTimer) {
135
+ clearTimeout(this.pollTimer);
136
+ this.pollTimer = null;
137
+ }
138
+ }
139
+ /** Return the last detected run command info. */
140
+ getLastDetectedInfo() {
141
+ return this.lastDetectedInfo;
142
+ }
143
+ /** Schedule the next poll */
144
+ schedulePoll() {
145
+ if (!this.isRunning)
146
+ return;
147
+ this.pollTimer = setTimeout(async () => {
148
+ await this.poll();
149
+ if (this.isRunning) {
150
+ this.schedulePoll();
151
+ }
152
+ }, this.pollIntervalMs);
153
+ }
154
+ /**
155
+ * Single poll iteration:
156
+ * 1. Detect run command dialog in DOM (with contextId)
157
+ * 2. Notify via callback only on new detection (prevent duplicates)
158
+ * 3. Reset when dialog disappears
159
+ */
160
+ async poll() {
161
+ try {
162
+ const contextId = this.cdpService.getPrimaryContextId();
163
+ const callParams = {
164
+ expression: DETECT_RUN_COMMAND_SCRIPT,
165
+ returnByValue: true,
166
+ awaitPromise: false,
167
+ };
168
+ if (contextId !== null) {
169
+ callParams.contextId = contextId;
170
+ }
171
+ const result = await this.cdpService.call('Runtime.evaluate', callParams);
172
+ const info = result?.result?.value ?? null;
173
+ if (info) {
174
+ // Duplicate prevention: use commandText as key
175
+ const key = `${info.commandText}::${info.workingDirectory}`;
176
+ if (key !== this.lastDetectedKey) {
177
+ this.lastDetectedKey = key;
178
+ this.lastDetectedInfo = info;
179
+ this.onRunCommandRequired(info);
180
+ }
181
+ }
182
+ else {
183
+ const wasDetected = this.lastDetectedKey !== null;
184
+ this.lastDetectedKey = null;
185
+ this.lastDetectedInfo = null;
186
+ if (wasDetected && this.onResolved) {
187
+ this.onResolved();
188
+ }
189
+ }
190
+ }
191
+ catch (error) {
192
+ const message = error instanceof Error ? error.message : String(error);
193
+ if (message.includes('WebSocket is not connected')) {
194
+ return;
195
+ }
196
+ logger_1.logger.error('[RunCommandDetector] Error during polling:', error);
197
+ }
198
+ }
199
+ /**
200
+ * Click the Run button via CDP.
201
+ * @param buttonText Text of the button to click (default: detected runText or "Run")
202
+ * @returns true if click succeeded
203
+ */
204
+ async runButton(buttonText) {
205
+ const text = buttonText ?? this.lastDetectedInfo?.runText ?? 'Run';
206
+ return this.clickButton(text);
207
+ }
208
+ /**
209
+ * Click the Reject button via CDP.
210
+ * @param buttonText Text of the button to click (default: detected rejectText or "Reject")
211
+ * @returns true if click succeeded
212
+ */
213
+ async rejectButton(buttonText) {
214
+ const text = buttonText ?? this.lastDetectedInfo?.rejectText ?? 'Reject';
215
+ return this.clickButton(text);
216
+ }
217
+ /**
218
+ * Internal click handler (shared implementation for runButton / rejectButton).
219
+ */
220
+ async clickButton(buttonText) {
221
+ try {
222
+ const script = (0, approvalDetector_1.buildClickScript)(buttonText);
223
+ const result = await this.runEvaluateScript(script);
224
+ if (result?.ok !== true) {
225
+ logger_1.logger.warn(`[RunCommandDetector] Click failed for "${buttonText}":`, result?.error ?? 'unknown');
226
+ }
227
+ else {
228
+ logger_1.logger.debug(`[RunCommandDetector] Click OK for "${buttonText}"`);
229
+ }
230
+ return result?.ok === true;
231
+ }
232
+ catch (error) {
233
+ logger_1.logger.error('[RunCommandDetector] Error while clicking button:', error);
234
+ return false;
235
+ }
236
+ }
237
+ /**
238
+ * Execute Runtime.evaluate with contextId and return result.value.
239
+ */
240
+ async runEvaluateScript(expression) {
241
+ const contextId = this.cdpService.getPrimaryContextId();
242
+ const callParams = {
243
+ expression,
244
+ returnByValue: true,
245
+ awaitPromise: false,
246
+ };
247
+ if (contextId !== null) {
248
+ callParams.contextId = contextId;
249
+ }
250
+ const result = await this.cdpService.call('Runtime.evaluate', callParams);
251
+ return result?.result?.value;
252
+ }
253
+ /** Returns whether monitoring is currently active */
254
+ isActive() {
255
+ return this.isRunning;
256
+ }
257
+ }
258
+ exports.RunCommandDetector = RunCommandDetector;
@@ -36,6 +36,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.COOLDOWN_MS = exports.UPDATE_CHECK_FILE = void 0;
37
37
  exports.shouldCheckForUpdates = shouldCheckForUpdates;
38
38
  exports.fetchLatestVersion = fetchLatestVersion;
39
+ exports.isGlobalInstall = isGlobalInstall;
39
40
  exports.checkForUpdates = checkForUpdates;
40
41
  const https = __importStar(require("https"));
41
42
  const fs = __importStar(require("fs"));
@@ -127,11 +128,25 @@ function compareSemver(a, b) {
127
128
  }
128
129
  return 0;
129
130
  }
131
+ /**
132
+ * Detect whether the process is running from a global npm install
133
+ * (as opposed to a local dev checkout via `ts-node`, `tsx`, etc.).
134
+ */
135
+ function isGlobalInstall() {
136
+ const execPath = process.argv[1] || '';
137
+ // Global installs run from a path containing node_modules
138
+ // Local dev runs from the source tree (no node_modules/.bin in argv[1])
139
+ const globalIndicators = ['/lib/node_modules/', '\\node_modules\\lazy-gravity\\'];
140
+ return globalIndicators.some((indicator) => execPath.includes(indicator));
141
+ }
130
142
  /**
131
143
  * Non-blocking update check. Call at startup (fire-and-forget).
132
144
  * Respects a 24-hour cooldown via a local cache file.
145
+ * Skipped when running from source (dev/local checkout).
133
146
  */
134
147
  async function checkForUpdates(currentVersion) {
148
+ if (!isGlobalInstall())
149
+ return;
135
150
  if (!shouldCheckForUpdates())
136
151
  return;
137
152
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lazy-gravity",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Control Antigravity from anywhere — a local, secure bot (Discord + Telegram) that lets you remotely operate Antigravity on your home PC from your smartphone.",
5
5
  "main": "dist/index.js",
6
6
  "bin": {