lazy-gravity 0.10.0 → 0.12.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.
Files changed (33) hide show
  1. package/README.md +7 -0
  2. package/dist/bin/commands/doctor.js +45 -7
  3. package/dist/bot/index.js +526 -24
  4. package/dist/commands/chatCommandHandler.js +11 -41
  5. package/dist/commands/registerSlashCommands.js +62 -0
  6. package/dist/database/scheduleRepository.js +50 -6
  7. package/dist/events/interactionCreateHandler.js +163 -28
  8. package/dist/events/messageCreateHandler.js +24 -28
  9. package/dist/handlers/fileChangeButtonAction.js +12 -24
  10. package/dist/handlers/genericActionButtonAction.js +16 -18
  11. package/dist/handlers/planningButtonAction.js +105 -17
  12. package/dist/handlers/planningModalSubmitAction.js +38 -0
  13. package/dist/handlers/questionSelectAction.js +8 -7
  14. package/dist/handlers/questionSkipAction.js +7 -6
  15. package/dist/platform/discord/wrappers.js +76 -0
  16. package/dist/services/approvalDetector.js +43 -14
  17. package/dist/services/artifactService.js +61 -21
  18. package/dist/services/assistantDomExtractor.js +23 -3
  19. package/dist/services/cdpBridgeManager.js +35 -2
  20. package/dist/services/cdpConnectionPool.js +7 -2
  21. package/dist/services/cdpService.js +113 -8
  22. package/dist/services/chatSessionService.js +15 -8
  23. package/dist/services/heartbeatService.js +261 -0
  24. package/dist/services/notificationSender.js +12 -2
  25. package/dist/services/planningDetector.js +1 -1
  26. package/dist/services/promptDispatcher.js +21 -1
  27. package/dist/services/questionDetector.js +128 -76
  28. package/dist/services/responseMonitor.js +17 -1
  29. package/dist/services/scheduleService.js +101 -4
  30. package/dist/utils/configLoader.js +8 -0
  31. package/dist/utils/fileOpenCache.js +2 -6
  32. package/dist/utils/questionActionUtils.js +22 -0
  33. package/package.json +2 -1
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ApprovalDetector = void 0;
3
+ exports.ApprovalDetector = exports.DETECT_APPROVAL_SCRIPT = void 0;
4
4
  exports.buildClickScript = buildClickScript;
5
5
  const logger_1 = require("../utils/logger");
6
6
  const consecutiveEmptyPollGate_1 = require("../utils/consecutiveEmptyPollGate");
@@ -9,7 +9,7 @@ const consecutiveEmptyPollGate_1 = require("../utils/consecutiveEmptyPollGate");
9
9
  *
10
10
  * Detects allow/deny button pairs and extracts descriptions with fallbacks.
11
11
  */
12
- const DETECT_APPROVAL_SCRIPT = `(() => {
12
+ exports.DETECT_APPROVAL_SCRIPT = `(() => {
13
13
  const ALLOW_ONCE_PATTERNS = ['allow once', 'allow one time', 'yes, allow this time', '今回のみ許可', '1回のみ許可', '一度許可'];
14
14
  const ALWAYS_ALLOW_PATTERNS = [
15
15
  'allow this conversation',
@@ -25,7 +25,21 @@ const DETECT_APPROVAL_SCRIPT = `(() => {
25
25
  const normalize = (text) => (text || '').toLowerCase().replace(/\\s+/g, ' ').trim();
26
26
 
27
27
  const allButtons = Array.from(document.querySelectorAll('button, [role="button"], span.cursor-pointer, div.cursor-pointer'))
28
- .filter(btn => btn.offsetParent !== null);
28
+ .filter(btn => btn.offsetParent !== null)
29
+ .reverse();
30
+
31
+ const STOP_PATTERNS_GEN = [/^stop$/, /^stop generating$/, /^stop response$/, /^停止$/, /^生成を停止$/, /^応答を停止$/];
32
+ const isGenerating = allButtons.some(btn => {
33
+ const tooltipId = btn.getAttribute('data-tooltip-id');
34
+ if (tooltipId === 'input-send-button-cancel-tooltip') return true;
35
+ const labels = [btn.textContent || '', btn.getAttribute('aria-label') || '', btn.getAttribute('title') || ''];
36
+ return labels.some(val => {
37
+ const normalized = (val || '').toLowerCase().replace(/\\s+/g, ' ').trim();
38
+ return normalized && STOP_PATTERNS_GEN.some(re => re.test(normalized));
39
+ });
40
+ });
41
+
42
+ if (isGenerating) return null; // Wait for generation to finish!
29
43
 
30
44
  let approveBtn = allButtons.find(btn => {
31
45
  const t = normalize(btn.textContent || '');
@@ -48,7 +62,8 @@ const DETECT_APPROVAL_SCRIPT = `(() => {
48
62
  || document.body;
49
63
 
50
64
  const containerButtons = Array.from(container.querySelectorAll('button, [role="button"], span.cursor-pointer, div.cursor-pointer'))
51
- .filter(btn => btn.offsetParent !== null);
65
+ .filter(btn => btn.offsetParent !== null)
66
+ .reverse();
52
67
 
53
68
  const denyBtn = containerButtons.find(btn => {
54
69
  const t = normalize(btn.textContent || '');
@@ -62,9 +77,9 @@ const DETECT_APPROVAL_SCRIPT = `(() => {
62
77
  return ALWAYS_ALLOW_PATTERNS.some(p => t.includes(p));
63
78
  }) || null;
64
79
 
65
- const approveText = (approveBtn.textContent || '').trim();
66
- const alwaysAllowText = alwaysAllowBtn ? (alwaysAllowBtn.textContent || '').trim() : '';
67
- const denyText = (denyBtn.textContent || '').trim();
80
+ const approveText = (approveBtn.innerText || approveBtn.textContent || '').trim();
81
+ const alwaysAllowText = alwaysAllowBtn ? (alwaysAllowBtn.innerText || alwaysAllowBtn.textContent || '').trim() : '';
82
+ const denyText = (denyBtn.innerText || denyBtn.textContent || '').trim();
68
83
 
69
84
  // Description extraction (multiple fallbacks)
70
85
  let description = '';
@@ -95,6 +110,7 @@ const DETECT_APPROVAL_SCRIPT = `(() => {
95
110
  }
96
111
  if (!modal) {
97
112
  modal = approveBtn.parentElement?.parentElement?.parentElement || approveBtn.parentElement?.parentElement;
113
+ if (modal === document.body || modal?.id === 'root') modal = null;
98
114
  }
99
115
  }
100
116
 
@@ -103,8 +119,11 @@ const DETECT_APPROVAL_SCRIPT = `(() => {
103
119
  const walk = (node) => {
104
120
  if (node.nodeType === 1) {
105
121
  // Skip buttons entirely
106
- if (node.tagName === 'BUTTON' || node.getAttribute('role') === 'button' || node.classList.contains('cursor-pointer')) return;
122
+ if (node.tagName === 'BUTTON' || node.getAttribute('role') === 'button') return;
107
123
 
124
+ // Skip menu bars and sidebars
125
+ if (node.tagName === 'NAV' || node.getAttribute('role') === 'menubar' || node.closest('nav') || node.closest('.monaco-menu') || node.closest('.sidebar') || node.classList.contains('sidebar')) return;
126
+
108
127
  const display = window.getComputedStyle(node).display;
109
128
  if (display === 'none') return;
110
129
 
@@ -121,7 +140,11 @@ const DETECT_APPROVAL_SCRIPT = `(() => {
121
140
 
122
141
  const parentText = parts.join(' ').replace(/\\n\\s*/g, '\\n').replace(/\\n{3,}/g, '\\n\\n').trim();
123
142
  if (parentText.length > 5) {
124
- description = parentText.length > 2000 ? parentText.substring(0, 2000) + '...\\n(truncated)' : parentText;
143
+ if (parentText.length > 800 || parentText.includes('F ile\\nE dit\\nS election')) {
144
+ description = 'Code changes require your approval.';
145
+ } else {
146
+ description = parentText;
147
+ }
125
148
  }
126
149
  }
127
150
  }
@@ -149,7 +172,7 @@ const EXPAND_ALWAYS_ALLOW_MENU_SCRIPT = `(() => {
149
172
  ];
150
173
 
151
174
  const normalize = (text) => (text || '').toLowerCase().replace(/\\s+/g, ' ').trim();
152
- const visibleButtons = Array.from(document.querySelectorAll('button'))
175
+ const visibleButtons = Array.from(document.querySelectorAll('button, [role="button"], span.cursor-pointer, div.cursor-pointer'))
153
176
  .filter(btn => btn.offsetParent !== null);
154
177
 
155
178
  const directAlways = visibleButtons.find(btn => {
@@ -169,7 +192,7 @@ const EXPAND_ALWAYS_ALLOW_MENU_SCRIPT = `(() => {
169
192
  || allowOnceBtn.parentElement
170
193
  || document.body;
171
194
 
172
- const containerButtons = Array.from(container.querySelectorAll('button'))
195
+ const containerButtons = Array.from(container.querySelectorAll('button, [role="button"], span.cursor-pointer, div.cursor-pointer'))
173
196
  .filter(btn => btn.offsetParent !== null);
174
197
 
175
198
  const toggleBtn = containerButtons.find(btn => {
@@ -218,12 +241,18 @@ function buildClickScript(buttonText) {
218
241
  const normalize = (text) => (text || '').toLowerCase().replace(/\\s+/g, ' ').trim();
219
242
  const text = ${safeText};
220
243
  const wanted = normalize(text);
221
- const allButtons = Array.from(document.querySelectorAll('button, [role="button"], a, [class*="btn"], [class*="button"], [class*="action"]')).reverse();
244
+ const allButtons = Array.from(document.querySelectorAll('button, [role="button"], a.action-btn, a[class*="btn"], span.cursor-pointer, div.cursor-pointer')).reverse();
222
245
  const target = allButtons.find(btn => {
223
246
  const style = window.getComputedStyle(btn);
224
247
  if (style.display === 'none' || style.visibility === 'hidden' || btn.disabled) return false;
225
- const buttonText = normalize(btn.textContent || '');
248
+ const buttonText = normalize(btn.innerText || btn.textContent || '');
226
249
  const ariaLabel = normalize(btn.getAttribute('aria-label') || '');
250
+
251
+ const isShort = wanted.length < 5;
252
+ if (isShort) {
253
+ return buttonText === wanted || ariaLabel === wanted;
254
+ }
255
+
227
256
  return buttonText === wanted ||
228
257
  ariaLabel === wanted ||
229
258
  (buttonText.includes(wanted) && buttonText.length < wanted.length + 10) ||
@@ -317,7 +346,7 @@ class ApprovalDetector {
317
346
  try {
318
347
  const contextId = this.cdpService.getPrimaryContextId();
319
348
  const callParams = {
320
- expression: DETECT_APPROVAL_SCRIPT,
349
+ expression: exports.DETECT_APPROVAL_SCRIPT,
321
350
  returnByValue: true,
322
351
  awaitPromise: false,
323
352
  };
@@ -69,9 +69,17 @@ const COMMON_WORDS = new Set(['the', 'and', 'for', 'with', 'from', 'this', 'that
69
69
  class ArtifactService {
70
70
  brainBasePath;
71
71
  constructor(brainBasePath) {
72
- this.brainBasePath =
73
- brainBasePath ??
74
- path.join(os.homedir(), '.gemini', 'antigravity', 'brain');
72
+ if (brainBasePath) {
73
+ this.brainBasePath = brainBasePath;
74
+ }
75
+ else {
76
+ const idePath = path.join(os.homedir(), '.gemini', 'antigravity-ide', 'brain');
77
+ const defaultPath = path.join(os.homedir(), '.gemini', 'antigravity', 'brain');
78
+ this.brainBasePath = fs.existsSync(idePath) ? idePath : defaultPath;
79
+ }
80
+ }
81
+ getBrainBasePath() {
82
+ return this.brainBasePath;
75
83
  }
76
84
  /**
77
85
  * List all conversations in the brain directory (UUIDs).
@@ -202,23 +210,25 @@ class ArtifactService {
202
210
  const buf = Buffer.alloc(readSize);
203
211
  const bytesRead = fs.readSync(fd, buf, 0, buf.length, 0);
204
212
  const content = buf.slice(0, bytesRead).toString('utf-8').toLowerCase();
205
- if (filterStr && content.includes(filterStr)) {
206
- belongsToWorkspace = true;
207
- }
208
- if (belongsToWorkspace) {
209
- if (content.includes(needle)) {
210
- exactMatch = true;
213
+ if (filterStr) {
214
+ const escapedFilter = filterStr.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
215
+ const regex = new RegExp(`(?:file:\\/\\/\\/[^"\\)]*?\\/|[a-zA-Z]:[\\\\/](?!.*(?:\\.gemini|brain|antigravity))[^"\\)]*?[\\\\/])${escapedFilter}\\b`, 'i');
216
+ if (regex.test(content)) {
217
+ belongsToWorkspace = true;
211
218
  }
212
- else {
213
- const contentTokens = new Set(content.replace(/[^\p{L}\p{N}]+/gu, ' ').split(/\s+/));
214
- let currentScore = 0;
215
- for (const word of uniqueNeedleWords) {
216
- if (contentTokens.has(word))
217
- currentScore++;
218
- }
219
- if (currentScore > score)
220
- score = currentScore;
219
+ }
220
+ if (content.includes(needle)) {
221
+ exactMatch = true;
222
+ }
223
+ else {
224
+ const contentTokens = new Set(content.replace(/[^\p{L}\p{N}]+/gu, ' ').split(/\s+/));
225
+ let currentScore = 0;
226
+ for (const word of uniqueNeedleWords) {
227
+ if (contentTokens.has(word))
228
+ currentScore++;
221
229
  }
230
+ if (currentScore > score)
231
+ score = currentScore;
222
232
  }
223
233
  }
224
234
  finally {
@@ -251,10 +261,11 @@ class ArtifactService {
251
261
  * that has at least one artifact. Falls back to the most recent conversation
252
262
  * overall if none have artifacts.
253
263
  */
254
- getLatestConversationWithArtifacts() {
264
+ getLatestConversationWithArtifacts(workspaceFilter) {
255
265
  const ids = this.listConversationIds();
256
266
  if (ids.length === 0)
257
267
  return null;
268
+ const filterStr = workspaceFilter ? workspaceFilter.toLowerCase() : null;
258
269
  // Sort by directory mtime descending
259
270
  const sorted = ids
260
271
  .map((id) => {
@@ -267,12 +278,41 @@ class ArtifactService {
267
278
  }
268
279
  })
269
280
  .sort((a, b) => b.mtime - a.mtime);
270
- // Find the first one that has artifacts
281
+ if (filterStr) {
282
+ const escapedFilter = filterStr.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
283
+ const workspaceIds = sorted.filter(({ id }) => {
284
+ let belongs = false;
285
+ const regex = new RegExp(`(?:file:\\/\\/\\/[^"\\)]*?\\/|[a-zA-Z]:[\\\\/](?!.*(?:\\.gemini|brain|antigravity))[^"\\)]*?[\\\\/])${escapedFilter}\\b`, 'i');
286
+ const checkFile = (filePath) => {
287
+ try {
288
+ if (fs.existsSync(filePath)) {
289
+ const content = fs.readFileSync(filePath, 'utf-8').toLowerCase();
290
+ if (regex.test(content)) {
291
+ belongs = true;
292
+ }
293
+ }
294
+ }
295
+ catch { /* ignore */ }
296
+ };
297
+ checkFile(path.join(this.brainBasePath, id, '.system_generated', 'logs', 'transcript.jsonl'));
298
+ if (!belongs) {
299
+ checkFile(path.join(this.brainBasePath, id, 'implementation_plan.md'));
300
+ }
301
+ return belongs;
302
+ });
303
+ if (workspaceIds.length === 0)
304
+ return null;
305
+ const latestId = workspaceIds[0].id;
306
+ if (this.listArtifacts(latestId).length > 0) {
307
+ return latestId;
308
+ }
309
+ return null;
310
+ }
311
+ // Original fallback behavior for no filter
271
312
  for (const { id } of sorted) {
272
313
  if (this.listArtifacts(id).length > 0)
273
314
  return id;
274
315
  }
275
- // Nothing has artifacts — return most recent anyway (caller handles empty list)
276
316
  return sorted[0]?.id ?? null;
277
317
  }
278
318
  /**
@@ -208,9 +208,10 @@ function extractAssistantSegmentsPayloadScript() {
208
208
  if (node.closest('details')) return true;
209
209
  if (node.closest('[class*="feedback"], footer')) return true;
210
210
  if (node.closest('.notify-user-container')) return true;
211
- // Guard against approval dialogs leaking into the output (they contain plan cards or review buttons)
212
- var dialogNode = node.closest('dialog, [role="dialog"], form');
213
- if (dialogNode && dialogNode.querySelector('[data-testid="plan-card"], [class*="plan-summary"], .actions-container, .review-button')) {
211
+ if (node.closest('dialog, [role="dialog"]')) return true;
212
+ // Guard against other approval forms leaking into the output
213
+ var formNode = node.closest('form');
214
+ if (formNode && formNode.querySelector('[data-testid="plan-card"], [class*="plan-summary"], .actions-container, .review-button')) {
214
215
  return true;
215
216
  }
216
217
 
@@ -532,6 +533,25 @@ function extractAssistantSegmentsPayloadScript() {
532
533
  });
533
534
  }
534
535
  }
536
+ // 4f. Artifact Cards
537
+ var artifactCards = artifactScope.querySelectorAll('[data-testid="artifact-card"], [class*="artifact-card"]');
538
+ for (var aci = 0; aci < artifactCards.length; aci++) {
539
+ var card = artifactCards[aci];
540
+ var titleEl = card.querySelector('.title, [class*="title"], h3, h4, span');
541
+ if (titleEl) {
542
+ var titleText = (titleEl.textContent || '').trim();
543
+ if (titleText) {
544
+ segments.push({
545
+ kind: 'citation',
546
+ text: titleText + '.md',
547
+ role: 'assistant',
548
+ messageIndex: 0,
549
+ domPath: 'artifact-card:nth(' + aci + ')'
550
+ });
551
+ }
552
+ }
553
+ }
554
+
535
555
 
536
556
  if (!bodyFound && segments.length === 0) return null;
537
557
 
@@ -56,7 +56,7 @@ function buildSessionRouteKey(projectName, sessionTitle) {
56
56
  return `${projectName}::${normalizeSessionTitle(sessionTitle)}`;
57
57
  }
58
58
  const GET_CURRENT_CHAT_TITLE_SCRIPT = `(() => {
59
- const panel = document.querySelector('.antigravity-agent-side-panel');
59
+ const panel = document.querySelector('.antigravity-agent-side-panel') || document.body;
60
60
  if (!panel) return '';
61
61
  const header = panel.querySelector('div[class*="border-b"]');
62
62
  if (!header) return '';
@@ -337,6 +337,8 @@ function ensureApprovalDetector(bridge, cdp, projectName, accountName = 'default
337
337
  },
338
338
  onApprovalRequired: async (info) => {
339
339
  logger_1.logger.debug(`[ApprovalDetector:${projectName}] Approval button detected (allow="${info.approveText}", deny="${info.denyText}")`);
340
+ // Emit approval event for bot text updating
341
+ cdp.emit('approval_required', info);
340
342
  const currentChatTitle = await getCurrentChatTitle(cdp);
341
343
  const targetChannel = resolveApprovalChannelForCurrentChat(bridge, projectName, currentChatTitle);
342
344
  const targetChannelId = targetChannel ? targetChannel.id : '';
@@ -365,6 +367,24 @@ function ensureApprovalDetector(bridge, cdp, projectName, accountName = 'default
365
367
  extraFields.push({ name: (0, i18n_1.t)('Allow Chat button'), value: info.alwaysAllowText, inline: true });
366
368
  }
367
369
  extraFields.push({ name: (0, i18n_1.t)('Deny button'), value: info.denyText || (0, i18n_1.t)('(None)'), inline: true });
370
+ // Resolve artifact review buttons if they exist
371
+ let walkthroughCustomId;
372
+ let taskCustomId;
373
+ if (bridge.chatSessionRepo && bridge.artifactService) {
374
+ const session = bridge.chatSessionRepo.findByChannelId(targetChannelId);
375
+ const activeConversationId = session?.conversationId;
376
+ if (activeConversationId) {
377
+ const artifacts = bridge.artifactService.listArtifacts(activeConversationId);
378
+ const walkthrough = artifacts.find((art) => art.filename.toLowerCase() === 'walkthrough.md');
379
+ if (walkthrough) {
380
+ walkthroughCustomId = `file_open:art:${activeConversationId}:walkthrough.md`;
381
+ }
382
+ const task = artifacts.find((art) => art.filename.toLowerCase() === 'task.md');
383
+ if (task) {
384
+ taskCustomId = `file_open:art:${activeConversationId}:task.md`;
385
+ }
386
+ }
387
+ }
368
388
  const payload = (0, notificationSender_1.buildApprovalNotification)({
369
389
  title: (0, i18n_1.t)('Approval Required'),
370
390
  description: info.description || (0, i18n_1.t)('Antigravity is requesting approval for an action'),
@@ -373,6 +393,8 @@ function ensureApprovalDetector(bridge, cdp, projectName, accountName = 'default
373
393
  extraFields,
374
394
  hasAlwaysAllow: !!info.alwaysAllowText,
375
395
  alwaysAllowText: info.alwaysAllowText,
396
+ walkthroughCustomId,
397
+ taskCustomId,
376
398
  });
377
399
  if (lastNotification) {
378
400
  lastNotification.payload = payload;
@@ -503,12 +525,23 @@ function ensureErrorPopupDetector(bridge, cdp, projectName, accountName = 'defau
503
525
  { name: (0, i18n_1.t)('Workspace'), value: projectName, inline: true },
504
526
  ],
505
527
  });
528
+ if (lastNotification && detector.lastDetectedKey && lastNotification.key === detector.lastDetectedKey) {
529
+ // Cooldown triggered for the same popup; edit existing message instead of sending new
530
+ const edited = await lastNotification.sent.edit(payload).catch((err) => {
531
+ logger_1.logger.error(err);
532
+ return null;
533
+ });
534
+ if (edited) {
535
+ lastNotification.payload = payload;
536
+ }
537
+ return;
538
+ }
506
539
  const sent = await targetChannel.send(payload).catch((err) => {
507
540
  logger_1.logger.error(err);
508
541
  return null;
509
542
  });
510
543
  if (sent) {
511
- lastNotification = { sent, payload };
544
+ lastNotification = { sent, payload, key: detector.lastDetectedKey };
512
545
  }
513
546
  },
514
547
  });
@@ -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
  */