lazy-gravity 0.10.0 → 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.
@@ -11,6 +11,7 @@ function buildConnectionKey(projectName, accountName) {
11
11
  * Pool that manages independent CdpService instances per workspace/account pair.
12
12
  */
13
13
  class CdpConnectionPool {
14
+ lastActiveWorkspace = null;
14
15
  connections = new Map();
15
16
  workspaceToAccount = new Map();
16
17
  approvalDetectors = new Map();
@@ -40,6 +41,7 @@ class CdpConnectionPool {
40
41
  const key = buildConnectionKey(projectName, effectiveAccount);
41
42
  const existing = this.connections.get(key);
42
43
  if (existing && existing.isConnected()) {
44
+ this.lastActiveWorkspace = projectName;
43
45
  await existing.discoverAndConnectForWorkspace(workspacePath);
44
46
  return existing;
45
47
  }
@@ -50,7 +52,9 @@ class CdpConnectionPool {
50
52
  const connectPromise = this.createAndConnect(workspacePath, projectName, effectiveAccount);
51
53
  this.connectingPromises.set(key, connectPromise);
52
54
  try {
53
- return await connectPromise;
55
+ const result = await connectPromise;
56
+ this.lastActiveWorkspace = projectName;
57
+ return result;
54
58
  }
55
59
  finally {
56
60
  this.connectingPromises.delete(key);
@@ -59,8 +63,9 @@ class CdpConnectionPool {
59
63
  }
60
64
  getConnected(projectName, accountName = 'default') {
61
65
  const effectiveAccount = this.resolveAccountName(projectName, accountName);
62
- const cdp = this.connections.get(buildConnectionKey(projectName, effectiveAccount));
66
+ const cdp = this.connections.get(buildConnectionKey(projectName, effectiveAccount)) || null;
63
67
  if (cdp && cdp.isConnected()) {
68
+ this.lastActiveWorkspace = projectName;
64
69
  return cdp;
65
70
  }
66
71
  return null;
@@ -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 };
@@ -462,7 +468,8 @@ class CdpService extends events_1.EventEmitter {
462
468
  returnByValue: true,
463
469
  });
464
470
  const liveTitle = String(titleResult?.result?.value || '');
465
- if (liveTitle.toLowerCase().includes(projectName.toLowerCase())) {
471
+ const titleParts = liveTitle.split(' - ');
472
+ if (titleParts[0].trim().toLowerCase() === projectName.toLowerCase()) {
466
473
  this.currentWorkspaceName = projectName;
467
474
  return true;
468
475
  }
@@ -509,7 +516,12 @@ class CdpService extends events_1.EventEmitter {
509
516
  logger_1.logger.debug(` - title="${p.title}" url=${p.url}`);
510
517
  }
511
518
  // 1. Title match (fast path)
512
- const titleMatch = workbenchPages.find((t) => t.title?.includes(projectName));
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
+ });
513
525
  if (titleMatch) {
514
526
  return this.connectToPage(titleMatch, projectName);
515
527
  }
@@ -562,9 +574,8 @@ class CdpService extends events_1.EventEmitter {
562
574
  returnByValue: true,
563
575
  });
564
576
  const liveTitle = String(result?.result?.value || '');
565
- const normalizedLiveTitle = liveTitle.toLowerCase();
566
- const normalizedProject = projectName.toLowerCase();
567
- if (normalizedLiveTitle.includes(normalizedProject)) {
577
+ const liveParts = liveTitle.split(' - ');
578
+ if (liveParts[0].trim().toLowerCase() === projectName.toLowerCase()) {
568
579
  this.currentWorkspaceName = projectName;
569
580
  logger_1.logger.debug(`[CdpService] Probe success: detected "${projectName}"`);
570
581
  return true;
@@ -761,7 +772,12 @@ class CdpService extends events_1.EventEmitter {
761
772
  !t.url?.includes('workbench-jetski-agent') &&
762
773
  t.url?.includes('workbench'));
763
774
  // Title match
764
- const titleMatch = workbenchPages.find((t) => t.title?.toLowerCase().includes(projectName.toLowerCase()));
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
+ });
765
781
  if (titleMatch) {
766
782
  return this.connectToPage(titleMatch, projectName);
767
783
  }
@@ -1290,9 +1306,15 @@ class CdpService extends events_1.EventEmitter {
1290
1306
  ].some((fragment) => message.includes(fragment));
1291
1307
  }
1292
1308
  async injectMessageCore(text, imageFilePaths) {
1293
- const focusResult = await this.waitForChatInputReady();
1309
+ // Check if chat input is already available
1310
+ let focusResult = await this.waitForChatInputReady(500);
1294
1311
  if (!focusResult.ok) {
1295
- return { ok: false, error: focusResult.error || 'Chat input field not found' };
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
+ }
1296
1318
  }
1297
1319
  // Clear any existing text in the input field before injecting.
1298
1320
  await this.clearInputField();
@@ -1391,6 +1413,89 @@ class CdpService extends events_1.EventEmitter {
1391
1413
  nativeVirtualKeyCode: 13,
1392
1414
  });
1393
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
+ }
1394
1499
  /**
1395
1500
  * Detect file input in the UI and attach the specified files.
1396
1501
  */
@@ -40,7 +40,7 @@ const GET_NEW_CHAT_BUTTON_SCRIPT = `(() => {
40
40
  * The title element is a div with the text-ellipsis class inside the header.
41
41
  */
42
42
  const GET_CHAT_TITLE_SCRIPT = `(() => {
43
- const panel = document.querySelector('.antigravity-agent-side-panel');
43
+ const panel = document.querySelector('.antigravity-agent-side-panel') || document.body;
44
44
  if (!panel) return { title: '', hasActiveChat: false };
45
45
  const header = panel.querySelector('div[class*="border-b"]');
46
46
  if (!header) return { title: '', hasActiveChat: false };
@@ -58,7 +58,7 @@ const GET_CHAT_TITLE_SCRIPT = `(() => {
58
58
  return { title: title || '(Untitled)', hasActiveChat };
59
59
  })()`;
60
60
  const GET_SESSION_VIEW_STATE_SCRIPT = `(() => {
61
- const panel = document.querySelector('.antigravity-agent-side-panel');
61
+ const panel = document.querySelector('.antigravity-agent-side-panel') || document.body;
62
62
  if (!panel) {
63
63
  return {
64
64
  panelFound: false,
@@ -169,7 +169,8 @@ const SCRAPE_PAST_CONVERSATIONS_SCRIPT = `(() => {
169
169
  // Try the visible QuickInput dialog first, then fall back to the side panel.
170
170
  const quickInputPanels = Array.from(document.querySelectorAll('div[class*="bg-quickinput-background"]'));
171
171
  const panel = quickInputPanels.find((el) => isVisible(el))
172
- || document.querySelector('.antigravity-agent-side-panel');
172
+ || document.querySelector('.antigravity-agent-side-panel')
173
+ || document.body;
173
174
  if (!panel) return null;
174
175
 
175
176
  const items = [];
@@ -276,9 +277,11 @@ function buildActivateChatByTitleScript(title) {
276
277
  for (const node of nodes) {
277
278
  const text = normalize(node.textContent || '');
278
279
  if (!text) continue;
280
+
281
+ const isShort = wanted.length < 5;
279
282
  if (text === wanted) {
280
283
  exact.push({ node, textLength: text.length });
281
- } else if (text.includes(wanted)) {
284
+ } else if (!isShort && text.includes(wanted)) {
282
285
  includes.push({ node, textLength: text.length });
283
286
  } else if (wordsMatch(text, wantedRaw)) {
284
287
  fuzzy.push({ node, textLength: text.length });
@@ -369,10 +372,12 @@ function buildActivateViaPastConversationsScript(title) {
369
372
  if (!pattern) continue;
370
373
  const p = normalize(pattern);
371
374
  const pLoose = normalizeLoose(pattern);
375
+ const isShort = p.length < 5;
376
+
372
377
  if (
373
378
  text === p ||
374
- text.includes(p) ||
375
- (pLoose && (textLoose === pLoose || textLoose.includes(pLoose)))
379
+ (pLoose && textLoose === pLoose) ||
380
+ (!isShort && (text.includes(p) || (pLoose && textLoose.includes(pLoose))))
376
381
  ) {
377
382
  matched.push({ el, score: Math.abs(text.length - pattern.length) });
378
383
  break;
@@ -611,7 +616,8 @@ class ChatSessionService {
611
616
  const isVisible = (el) => !!el && el instanceof HTMLElement && el.offsetParent !== null;
612
617
  const quickInputPanels = Array.from(document.querySelectorAll('div[class*="bg-quickinput-background"]'));
613
618
  const panel = quickInputPanels.find((el) => isVisible(el))
614
- || document.querySelector('.antigravity-agent-side-panel');
619
+ || document.querySelector('.antigravity-agent-side-panel')
620
+ || document.body;
615
621
  if (!panel) return false;
616
622
  const containers = Array.from(
617
623
  panel.querySelectorAll('div[class*="overflow-auto"], div[class*="overflow-y-scroll"]')
@@ -1095,12 +1101,13 @@ class ChatSessionService {
1095
1101
  /**
1096
1102
  * Rename the current chat in the Antigravity UI directly by updating the DOM.
1097
1103
  * Note: This is a cosmetic change until Antigravity persists a rename.
1104
+ * IDE syncing for chat renaming is strictly "best-effort".
1098
1105
  */
1099
1106
  async renameCurrentChatInUI(cdpService, newTitle) {
1100
1107
  try {
1101
1108
  const contextId = cdpService.getPrimaryContextId();
1102
1109
  const RENAME_SCRIPT = `(() => {
1103
- const panel = document.querySelector('.antigravity-agent-side-panel');
1110
+ const panel = document.querySelector('.antigravity-agent-side-panel') || document.body;
1104
1111
  if (!panel) return { ok: false, error: 'Panel not found' };
1105
1112
  const header = panel.querySelector('div[class*="border-b"]');
1106
1113
  const titleEl = header?.querySelector('div[class*="text-ellipsis"]');
@@ -82,7 +82,7 @@ function customId(prefix, projectName, channelId) {
82
82
  // ---------------------------------------------------------------------------
83
83
  /** Build the approval notification message. */
84
84
  function buildApprovalNotification(opts) {
85
- const { title, description, projectName, channelId, toolNames, extraFields, hasAlwaysAllow, alwaysAllowText } = opts;
85
+ const { title, description, projectName, channelId, toolNames, extraFields, hasAlwaysAllow, alwaysAllowText, walkthroughCustomId, taskCustomId } = opts;
86
86
  const richContent = (0, richContentBuilder_1.pipe)((0, richContentBuilder_1.createRichContent)(), (rc) => (0, richContentBuilder_1.withTitle)(rc, title), (rc) => (0, richContentBuilder_1.withDescription)(rc, description), (rc) => (0, richContentBuilder_1.withColor)(rc, COLOR_APPROVAL), (rc) => (0, richContentBuilder_1.addField)(rc, 'Project', projectName, true), (rc) => toolNames && toolNames.length > 0
87
87
  ? (0, richContentBuilder_1.addField)(rc, 'Tools', toolNames.join(', '), true)
88
88
  : rc, (rc) => extraFields
@@ -98,6 +98,16 @@ function buildApprovalNotification(opts) {
98
98
  const components = [
99
99
  buttonRow(...buttons),
100
100
  ];
101
+ const artifactButtons = [];
102
+ if (walkthroughCustomId) {
103
+ artifactButtons.push(button(walkthroughCustomId, 'Review walkthrough.md', 'success'));
104
+ }
105
+ if (taskCustomId) {
106
+ artifactButtons.push(button(taskCustomId, 'Review task.md', 'primary'));
107
+ }
108
+ if (artifactButtons.length > 0) {
109
+ components.push(buttonRow(...artifactButtons));
110
+ }
101
111
  return { richContent, components };
102
112
  }
103
113
  /** Build the planning mode notification message. */
@@ -107,8 +117,8 @@ function buildPlanningNotification(opts) {
107
117
  ? extraFields.reduce((acc, f) => (0, richContentBuilder_1.addField)(acc, f.name, f.value, f.inline), rc)
108
118
  : rc, (rc) => (0, richContentBuilder_1.withFooter)(rc, 'Planning mode detected'), (rc) => (0, richContentBuilder_1.withTimestamp)(rc));
109
119
  const buttons = [];
110
- const openLabel = opts.openText || 'Open';
111
120
  if (opts.hasOpenButton !== false) {
121
+ const openLabel = opts.openText || 'Open Plan';
112
122
  buttons.push(button(customId(PLANNING_OPEN_ACTION_PREFIX, projectName, channelId), openLabel, 'primary'));
113
123
  }
114
124
  const buttonLabel = opts.proceedText || 'Proceed';
@@ -247,7 +247,7 @@ class PlanningDetector {
247
247
  * @returns true if click succeeded
248
248
  */
249
249
  async clickRejectButton(buttonText) {
250
- const text = buttonText ?? this.lastDetectedInfo?.rejectText ?? 'Reject All';
250
+ const text = buttonText || this.lastDetectedInfo?.rejectText || 'Reject All';
251
251
  return this.clickButton(text);
252
252
  }
253
253
  /**
@@ -1,12 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.PromptDispatcher = void 0;
4
+ const logger_1 = require("../utils/logger");
4
5
  /**
5
6
  * Dispatcher that calls the existing sendPromptToAntigravity.
6
7
  * Unifies dependency injection on the caller side and simplifies event handlers.
7
8
  */
8
9
  class PromptDispatcher {
9
10
  deps;
11
+ activeMonitors = new Map();
10
12
  constructor(deps) {
11
13
  this.deps = deps;
12
14
  }
@@ -17,7 +19,25 @@ class PromptDispatcher {
17
19
  await this._dispatch(req, { ...req.options, resumeOnly: true });
18
20
  }
19
21
  async _dispatch(req, options) {
20
- await this.deps.sendPromptImpl(this.deps.bridge, req.message, req.prompt, req.cdp, this.deps.modeService, this.deps.modelService, req.inboundImages ?? [], options);
22
+ const channelId = req.message.channelId;
23
+ const existing = this.activeMonitors.get(channelId);
24
+ if (existing) {
25
+ logger_1.logger.info(`[PromptDispatcher] Aborting previous active monitor for channel ${channelId}`);
26
+ await existing.stop().catch((err) => logger_1.logger.error('[PromptDispatcher] Error stopping monitor:', err));
27
+ this.activeMonitors.delete(channelId);
28
+ }
29
+ const wrappedOptions = {
30
+ ...(options || {}),
31
+ onMonitorCreated: (monitor) => {
32
+ this.activeMonitors.set(channelId, monitor);
33
+ options?.onMonitorCreated?.(monitor);
34
+ },
35
+ onFullCompletion: () => {
36
+ this.activeMonitors.delete(channelId);
37
+ options?.onFullCompletion?.();
38
+ }
39
+ };
40
+ await this.deps.sendPromptImpl(this.deps.bridge, req.message, req.prompt, req.cdp, this.deps.modeService, this.deps.modelService, req.inboundImages ?? [], wrappedOptions);
21
41
  }
22
42
  }
23
43
  exports.PromptDispatcher = PromptDispatcher;