lazy-gravity 0.0.4 → 0.2.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 (44) hide show
  1. package/README.md +22 -7
  2. package/dist/bin/cli.js +18 -18
  3. package/dist/bin/commands/doctor.js +25 -19
  4. package/dist/bin/commands/start.js +25 -2
  5. package/dist/bot/index.js +445 -126
  6. package/dist/commands/joinCommandHandler.js +302 -0
  7. package/dist/commands/joinDetachCommandHandler.js +285 -0
  8. package/dist/commands/registerSlashCommands.js +40 -0
  9. package/dist/commands/workspaceCommandHandler.js +17 -28
  10. package/dist/database/chatSessionRepository.js +10 -0
  11. package/dist/database/userPreferenceRepository.js +72 -0
  12. package/dist/events/interactionCreateHandler.js +338 -30
  13. package/dist/events/messageCreateHandler.js +161 -47
  14. package/dist/services/antigravityLauncher.js +4 -3
  15. package/dist/services/approvalDetector.js +7 -0
  16. package/dist/services/assistantDomExtractor.js +339 -0
  17. package/dist/services/cdpBridgeManager.js +323 -39
  18. package/dist/services/cdpConnectionPool.js +117 -33
  19. package/dist/services/cdpService.js +149 -53
  20. package/dist/services/chatSessionService.js +229 -8
  21. package/dist/services/errorPopupDetector.js +271 -0
  22. package/dist/services/planningDetector.js +318 -0
  23. package/dist/services/responseMonitor.js +308 -70
  24. package/dist/services/retryStore.js +46 -0
  25. package/dist/services/updateCheckService.js +147 -0
  26. package/dist/services/userMessageDetector.js +221 -0
  27. package/dist/ui/buttonUtils.js +33 -0
  28. package/dist/ui/modeUi.js +11 -1
  29. package/dist/ui/modelsUi.js +24 -13
  30. package/dist/ui/outputUi.js +30 -0
  31. package/dist/ui/projectListUi.js +83 -0
  32. package/dist/ui/sessionPickerUi.js +48 -0
  33. package/dist/utils/antigravityPaths.js +94 -0
  34. package/dist/utils/configLoader.js +18 -0
  35. package/dist/utils/discordButtonUtils.js +33 -0
  36. package/dist/utils/discordFormatter.js +149 -16
  37. package/dist/utils/htmlToDiscordMarkdown.js +184 -0
  38. package/dist/utils/logBuffer.js +47 -0
  39. package/dist/utils/logFileTransport.js +147 -0
  40. package/dist/utils/logger.js +86 -21
  41. package/dist/utils/pathUtils.js +57 -0
  42. package/dist/utils/plainTextFormatter.js +70 -0
  43. package/dist/utils/processLogBuffer.js +4 -0
  44. package/package.json +4 -4
@@ -2,6 +2,7 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.CdpConnectionPool = void 0;
4
4
  const logger_1 = require("../utils/logger");
5
+ const pathUtils_1 = require("../utils/pathUtils");
5
6
  const cdpService_1 = require("./cdpService");
6
7
  /**
7
8
  * Pool that manages independent CdpService instances per workspace.
@@ -13,6 +14,9 @@ const cdpService_1 = require("./cdpService");
13
14
  class CdpConnectionPool {
14
15
  connections = new Map();
15
16
  approvalDetectors = new Map();
17
+ errorPopupDetectors = new Map();
18
+ planningDetectors = new Map();
19
+ userMessageDetectors = new Map();
16
20
  connectingPromises = new Map();
17
21
  cdpOptions;
18
22
  constructor(cdpOptions = {}) {
@@ -27,36 +31,36 @@ class CdpConnectionPool {
27
31
  * @returns Connected CdpService
28
32
  */
29
33
  async getOrConnect(workspacePath) {
30
- const dirName = this.extractDirName(workspacePath);
34
+ const projectName = this.extractProjectName(workspacePath);
31
35
  // Return existing connection if available
32
- const existing = this.connections.get(dirName);
36
+ const existing = this.connections.get(projectName);
33
37
  if (existing && existing.isConnected()) {
34
38
  // Re-validate that the still-open window is actually bound to this workspace.
35
39
  await existing.discoverAndConnectForWorkspace(workspacePath);
36
40
  return existing;
37
41
  }
38
42
  // Wait for the pending connection promise if one exists (prevents concurrent connections)
39
- const pending = this.connectingPromises.get(dirName);
43
+ const pending = this.connectingPromises.get(projectName);
40
44
  if (pending) {
41
45
  return pending;
42
46
  }
43
47
  // Start a new connection
44
- const connectPromise = this.createAndConnect(workspacePath, dirName);
45
- this.connectingPromises.set(dirName, connectPromise);
48
+ const connectPromise = this.createAndConnect(workspacePath, projectName);
49
+ this.connectingPromises.set(projectName, connectPromise);
46
50
  try {
47
51
  const cdp = await connectPromise;
48
52
  return cdp;
49
53
  }
50
54
  finally {
51
- this.connectingPromises.delete(dirName);
55
+ this.connectingPromises.delete(projectName);
52
56
  }
53
57
  }
54
58
  /**
55
59
  * Get a connected CdpService (read-only).
56
60
  * Returns null if not connected.
57
61
  */
58
- getConnected(workspaceDirName) {
59
- const cdp = this.connections.get(workspaceDirName);
62
+ getConnected(projectName) {
63
+ const cdp = this.connections.get(projectName);
60
64
  if (cdp && cdp.isConnected()) {
61
65
  return cdp;
62
66
  }
@@ -65,44 +69,109 @@ class CdpConnectionPool {
65
69
  /**
66
70
  * Disconnect the specified workspace.
67
71
  */
68
- disconnectWorkspace(workspaceDirName) {
69
- const cdp = this.connections.get(workspaceDirName);
72
+ disconnectWorkspace(projectName) {
73
+ const cdp = this.connections.get(projectName);
70
74
  if (cdp) {
71
75
  cdp.disconnect().catch((err) => {
72
- logger_1.logger.error(`[CdpConnectionPool] Error while disconnecting ${workspaceDirName}:`, err);
76
+ logger_1.logger.error(`[CdpConnectionPool] Error while disconnecting ${projectName}:`, err);
73
77
  });
74
- this.connections.delete(workspaceDirName);
78
+ this.connections.delete(projectName);
75
79
  }
76
- const detector = this.approvalDetectors.get(workspaceDirName);
80
+ const detector = this.approvalDetectors.get(projectName);
77
81
  if (detector) {
78
82
  detector.stop();
79
- this.approvalDetectors.delete(workspaceDirName);
83
+ this.approvalDetectors.delete(projectName);
84
+ }
85
+ const errorPopupDetector = this.errorPopupDetectors.get(projectName);
86
+ if (errorPopupDetector) {
87
+ errorPopupDetector.stop();
88
+ this.errorPopupDetectors.delete(projectName);
89
+ }
90
+ const planningDetector = this.planningDetectors.get(projectName);
91
+ if (planningDetector) {
92
+ planningDetector.stop();
93
+ this.planningDetectors.delete(projectName);
94
+ }
95
+ const userMsgDetector = this.userMessageDetectors.get(projectName);
96
+ if (userMsgDetector) {
97
+ userMsgDetector.stop();
98
+ this.userMessageDetectors.delete(projectName);
80
99
  }
81
100
  }
82
101
  /**
83
102
  * Disconnect all workspace connections.
84
103
  */
85
104
  disconnectAll() {
86
- for (const dirName of [...this.connections.keys()]) {
87
- this.disconnectWorkspace(dirName);
105
+ for (const projectName of [...this.connections.keys()]) {
106
+ this.disconnectWorkspace(projectName);
88
107
  }
89
108
  }
90
109
  /**
91
110
  * Register an approval detector for a workspace.
92
111
  */
93
- registerApprovalDetector(workspaceDirName, detector) {
112
+ registerApprovalDetector(projectName, detector) {
94
113
  // Stop existing detector
95
- const existing = this.approvalDetectors.get(workspaceDirName);
114
+ const existing = this.approvalDetectors.get(projectName);
96
115
  if (existing && existing.isActive()) {
97
116
  existing.stop();
98
117
  }
99
- this.approvalDetectors.set(workspaceDirName, detector);
118
+ this.approvalDetectors.set(projectName, detector);
100
119
  }
101
120
  /**
102
121
  * Get the approval detector for a workspace.
103
122
  */
104
- getApprovalDetector(workspaceDirName) {
105
- return this.approvalDetectors.get(workspaceDirName);
123
+ getApprovalDetector(projectName) {
124
+ return this.approvalDetectors.get(projectName);
125
+ }
126
+ /**
127
+ * Register an error popup detector for a workspace.
128
+ */
129
+ registerErrorPopupDetector(projectName, detector) {
130
+ // Stop existing detector
131
+ const existing = this.errorPopupDetectors.get(projectName);
132
+ if (existing && existing.isActive()) {
133
+ existing.stop();
134
+ }
135
+ this.errorPopupDetectors.set(projectName, detector);
136
+ }
137
+ /**
138
+ * Get the error popup detector for a workspace.
139
+ */
140
+ getErrorPopupDetector(projectName) {
141
+ return this.errorPopupDetectors.get(projectName);
142
+ }
143
+ /**
144
+ * Register a planning detector for a workspace.
145
+ */
146
+ registerPlanningDetector(projectName, detector) {
147
+ // Stop existing detector
148
+ const existing = this.planningDetectors.get(projectName);
149
+ if (existing && existing.isActive()) {
150
+ existing.stop();
151
+ }
152
+ this.planningDetectors.set(projectName, detector);
153
+ }
154
+ /**
155
+ * Get the planning detector for a workspace.
156
+ */
157
+ getPlanningDetector(projectName) {
158
+ return this.planningDetectors.get(projectName);
159
+ }
160
+ /**
161
+ * Register a user message detector for a workspace.
162
+ */
163
+ registerUserMessageDetector(projectName, detector) {
164
+ const existing = this.userMessageDetectors.get(projectName);
165
+ if (existing && existing.isActive()) {
166
+ existing.stop();
167
+ }
168
+ this.userMessageDetectors.set(projectName, detector);
169
+ }
170
+ /**
171
+ * Get the user message detector for a workspace.
172
+ */
173
+ getUserMessageDetector(projectName) {
174
+ return this.userMessageDetectors.get(projectName);
106
175
  }
107
176
  /**
108
177
  * Return a list of workspace names with active connections.
@@ -117,40 +186,55 @@ class CdpConnectionPool {
117
186
  return active;
118
187
  }
119
188
  /**
120
- * Extract the directory name from a workspace path.
189
+ * Extract the project name from a workspace path.
121
190
  */
122
- extractDirName(workspacePath) {
123
- return workspacePath.split('/').filter(Boolean).pop() || workspacePath;
191
+ extractProjectName(workspacePath) {
192
+ return (0, pathUtils_1.extractProjectNameFromPath)(workspacePath) || workspacePath;
124
193
  }
125
194
  /**
126
195
  * Create a new CdpService and connect to the workspace.
127
196
  */
128
- async createAndConnect(workspacePath, dirName) {
197
+ async createAndConnect(workspacePath, projectName) {
129
198
  // Disconnect old connection if exists
130
- const old = this.connections.get(dirName);
199
+ const old = this.connections.get(projectName);
131
200
  if (old) {
132
201
  await old.disconnect().catch(() => { });
133
- this.connections.delete(dirName);
202
+ this.connections.delete(projectName);
134
203
  }
135
204
  const cdp = new cdpService_1.CdpService(this.cdpOptions);
136
205
  // Auto-cleanup on disconnect
137
206
  cdp.on('disconnected', () => {
138
- logger_1.logger.error(`[CdpConnectionPool] Workspace "${dirName}" disconnected`);
207
+ logger_1.logger.error(`[CdpConnectionPool] Workspace "${projectName}" disconnected`);
139
208
  // Only remove from Map when reconnection fails
140
209
  // (CdpService attempts reconnection internally, so we don't remove here)
141
210
  });
142
211
  cdp.on('reconnectFailed', () => {
143
- logger_1.logger.error(`[CdpConnectionPool] Reconnection failed for workspace "${dirName}". Removing from pool`);
144
- this.connections.delete(dirName);
145
- const detector = this.approvalDetectors.get(dirName);
212
+ logger_1.logger.error(`[CdpConnectionPool] Reconnection failed for workspace "${projectName}". Removing from pool`);
213
+ this.connections.delete(projectName);
214
+ const detector = this.approvalDetectors.get(projectName);
146
215
  if (detector) {
147
216
  detector.stop();
148
- this.approvalDetectors.delete(dirName);
217
+ this.approvalDetectors.delete(projectName);
218
+ }
219
+ const errorDetector = this.errorPopupDetectors.get(projectName);
220
+ if (errorDetector) {
221
+ errorDetector.stop();
222
+ this.errorPopupDetectors.delete(projectName);
223
+ }
224
+ const planDetector = this.planningDetectors.get(projectName);
225
+ if (planDetector) {
226
+ planDetector.stop();
227
+ this.planningDetectors.delete(projectName);
228
+ }
229
+ const userMsgDetector = this.userMessageDetectors.get(projectName);
230
+ if (userMsgDetector) {
231
+ userMsgDetector.stop();
232
+ this.userMessageDetectors.delete(projectName);
149
233
  }
150
234
  });
151
235
  // Connect to the workspace
152
236
  await cdp.discoverAndConnectForWorkspace(workspacePath);
153
- this.connections.set(dirName, cdp);
237
+ this.connections.set(projectName, cdp);
154
238
  return cdp;
155
239
  }
156
240
  }
@@ -42,6 +42,7 @@ const cdpPorts_1 = require("../utils/cdpPorts");
42
42
  const events_1 = require("events");
43
43
  const http = __importStar(require("http"));
44
44
  const child_process_1 = require("child_process");
45
+ const pathUtils_1 = require("../utils/pathUtils");
45
46
  const ws_1 = __importDefault(require("ws"));
46
47
  /** Antigravity UI DOM selector constants */
47
48
  const SELECTORS = {
@@ -251,19 +252,19 @@ class CdpService extends events_1.EventEmitter {
251
252
  * @returns true on successful connection
252
253
  */
253
254
  async discoverAndConnectForWorkspace(workspacePath) {
254
- const workspaceDirName = workspacePath.split('/').filter(Boolean).pop() || '';
255
+ const projectName = (0, pathUtils_1.extractProjectNameFromPath)(workspacePath);
255
256
  this.currentWorkspacePath = workspacePath;
256
257
  // Re-validate existing connection before skipping reconnect.
257
- if (this.isConnectedFlag && this.currentWorkspaceName === workspaceDirName) {
258
- const stillMatched = await this.verifyCurrentWorkspace(workspaceDirName, workspacePath);
258
+ if (this.isConnectedFlag && this.currentWorkspaceName === projectName) {
259
+ const stillMatched = await this.verifyCurrentWorkspace(projectName, workspacePath);
259
260
  if (stillMatched) {
260
261
  return true;
261
262
  }
262
- logger_1.logger.warn(`[CdpService] Workspace mismatch detected while reusing connection (expected="${workspaceDirName}"). Reconnecting...`);
263
+ logger_1.logger.warn(`[CdpService] Workspace mismatch detected while reusing connection (expected="${projectName}"). Reconnecting...`);
263
264
  }
264
265
  this.isSwitchingWorkspace = true;
265
266
  try {
266
- return await this._discoverAndConnectForWorkspaceImpl(workspacePath, workspaceDirName);
267
+ return await this._discoverAndConnectForWorkspaceImpl(workspacePath, projectName);
267
268
  }
268
269
  finally {
269
270
  this.isSwitchingWorkspace = false;
@@ -272,7 +273,7 @@ class CdpService extends events_1.EventEmitter {
272
273
  /**
273
274
  * Verify whether the currently attached page still represents the expected workspace.
274
275
  */
275
- async verifyCurrentWorkspace(workspaceDirName, workspacePath) {
276
+ async verifyCurrentWorkspace(projectName, workspacePath) {
276
277
  if (!this.ws || this.ws.readyState !== ws_1.default.OPEN || !this.isConnectedFlag) {
277
278
  return false;
278
279
  }
@@ -282,17 +283,17 @@ class CdpService extends events_1.EventEmitter {
282
283
  returnByValue: true,
283
284
  });
284
285
  const liveTitle = String(titleResult?.result?.value || '');
285
- if (liveTitle.includes(workspaceDirName)) {
286
- this.currentWorkspaceName = workspaceDirName;
286
+ if (liveTitle.toLowerCase().includes(projectName.toLowerCase())) {
287
+ this.currentWorkspaceName = projectName;
287
288
  return true;
288
289
  }
289
290
  }
290
291
  catch {
291
292
  // Fall through to folder-path probe.
292
293
  }
293
- return this.probeWorkspaceFolderPath(workspaceDirName, workspacePath);
294
+ return this.probeWorkspaceFolderPath(projectName, workspacePath);
294
295
  }
295
- async _discoverAndConnectForWorkspaceImpl(workspacePath, workspaceDirName) {
296
+ async _discoverAndConnectForWorkspaceImpl(workspacePath, projectName) {
296
297
  // Scan all ports to collect workbench pages
297
298
  let pages = [];
298
299
  let respondingPort = null;
@@ -316,7 +317,7 @@ class CdpService extends events_1.EventEmitter {
316
317
  }
317
318
  if (respondingPort === null) {
318
319
  // Launch Antigravity if no port responds
319
- return this.launchAndConnectWorkspace(workspacePath, workspaceDirName);
320
+ return this.launchAndConnectWorkspace(workspacePath, projectName);
320
321
  }
321
322
  // Filter workbench pages only (exclude Launchpad, Manager, iframe, worker)
322
323
  const workbenchPages = pages.filter((t) => t.type === 'page' &&
@@ -324,38 +325,38 @@ class CdpService extends events_1.EventEmitter {
324
325
  !t.title?.includes('Launchpad') &&
325
326
  !t.url?.includes('workbench-jetski-agent') &&
326
327
  t.url?.includes('workbench'));
327
- logger_1.logger.info(`[CdpService] Searching for workspace "${workspaceDirName}" (port=${respondingPort})... ${workbenchPages.length} workbench pages:`);
328
+ logger_1.logger.debug(`[CdpService] Searching for workspace "${projectName}" (port=${respondingPort})... ${workbenchPages.length} workbench pages:`);
328
329
  for (const p of workbenchPages) {
329
- logger_1.logger.info(` - title="${p.title}" url=${p.url}`);
330
+ logger_1.logger.debug(` - title="${p.title}" url=${p.url}`);
330
331
  }
331
332
  // 1. Title match (fast path)
332
- const titleMatch = workbenchPages.find((t) => t.title?.includes(workspaceDirName));
333
+ const titleMatch = workbenchPages.find((t) => t.title?.includes(projectName));
333
334
  if (titleMatch) {
334
- return this.connectToPage(titleMatch, workspaceDirName);
335
+ return this.connectToPage(titleMatch, projectName);
335
336
  }
336
337
  // 2. Title match failed -> CDP probe (connect to each page and check document.title)
337
- logger_1.logger.info(`[CdpService] Title match failed. Searching via CDP probe...`);
338
- const probeResult = await this.probeWorkbenchPages(workbenchPages, workspaceDirName, workspacePath);
338
+ logger_1.logger.debug(`[CdpService] Title match failed. Searching via CDP probe...`);
339
+ const probeResult = await this.probeWorkbenchPages(workbenchPages, projectName, workspacePath);
339
340
  if (probeResult) {
340
341
  return true;
341
342
  }
342
343
  // 3. If not found by probe either, launch a new window
343
- return this.launchAndConnectWorkspace(workspacePath, workspaceDirName);
344
+ return this.launchAndConnectWorkspace(workspacePath, projectName);
344
345
  }
345
346
  /**
346
347
  * Connect to the specified page (skip if already connected).
347
348
  */
348
- async connectToPage(page, workspaceDirName) {
349
+ async connectToPage(page, projectName) {
349
350
  // No reconnection needed if already connected to the same URL
350
351
  if (this.isConnectedFlag && this.targetUrl === page.webSocketDebuggerUrl) {
351
- this.currentWorkspaceName = workspaceDirName;
352
+ this.currentWorkspaceName = projectName;
352
353
  return true;
353
354
  }
354
355
  this.disconnectQuietly();
355
356
  this.targetUrl = page.webSocketDebuggerUrl;
356
357
  await this.connect();
357
- this.currentWorkspaceName = workspaceDirName;
358
- logger_1.logger.info(`[CdpService] Connected to workspace "${workspaceDirName}"`);
358
+ this.currentWorkspaceName = projectName;
359
+ logger_1.logger.debug(`[CdpService] Connected to workspace "${projectName}"`);
359
360
  return true;
360
361
  }
361
362
  /**
@@ -365,10 +366,10 @@ class CdpService extends events_1.EventEmitter {
365
366
  * If the title is "Untitled (Workspace)", verify workspace folder path via CDP.
366
367
  *
367
368
  * @param workbenchPages List of workbench pages
368
- * @param workspaceDirName Workspace directory name
369
+ * @param projectName Workspace directory name
369
370
  * @param workspacePath Full workspace path (for folder path matching)
370
371
  */
371
- async probeWorkbenchPages(workbenchPages, workspaceDirName, workspacePath) {
372
+ async probeWorkbenchPages(workbenchPages, projectName, workspacePath) {
372
373
  for (const page of workbenchPages) {
373
374
  try {
374
375
  // Temporarily connect to retrieve document.title
@@ -379,15 +380,17 @@ class CdpService extends events_1.EventEmitter {
379
380
  expression: 'document.title',
380
381
  returnByValue: true,
381
382
  });
382
- const liveTitle = result?.result?.value || '';
383
- if (liveTitle.includes(workspaceDirName)) {
384
- this.currentWorkspaceName = workspaceDirName;
385
- logger_1.logger.info(`[CdpService] Probe success: detected "${workspaceDirName}"`);
383
+ const liveTitle = String(result?.result?.value || '');
384
+ const normalizedLiveTitle = liveTitle.toLowerCase();
385
+ const normalizedProject = projectName.toLowerCase();
386
+ if (normalizedLiveTitle.includes(normalizedProject)) {
387
+ this.currentWorkspaceName = projectName;
388
+ logger_1.logger.debug(`[CdpService] Probe success: detected "${projectName}"`);
386
389
  return true;
387
390
  }
388
391
  // If title is "Untitled (Workspace)", verify by folder path
389
- if (liveTitle.includes('Untitled') && workspacePath) {
390
- const folderMatch = await this.probeWorkspaceFolderPath(workspaceDirName, workspacePath);
392
+ if (normalizedLiveTitle.includes('untitled') && workspacePath) {
393
+ const folderMatch = await this.probeWorkspaceFolderPath(projectName, workspacePath);
391
394
  if (folderMatch) {
392
395
  return true;
393
396
  }
@@ -410,7 +413,7 @@ class CdpService extends events_1.EventEmitter {
410
413
  * 2. Check folder path display in DOM
411
414
  * 3. Get workspace info from window.location.hash, etc.
412
415
  */
413
- async probeWorkspaceFolderPath(workspaceDirName, workspacePath) {
416
+ async probeWorkspaceFolderPath(projectName, workspacePath) {
414
417
  try {
415
418
  // Instead of DOM/document.title, check folder parameter in page URL or
416
419
  // folder name in explorer view
@@ -445,10 +448,13 @@ class CdpService extends events_1.EventEmitter {
445
448
  const value = res?.result?.value;
446
449
  if (value?.found && value?.value) {
447
450
  const detectedValue = value.value;
448
- if (detectedValue.includes(workspaceDirName) ||
449
- detectedValue.includes(workspacePath)) {
450
- this.currentWorkspaceName = workspaceDirName;
451
- logger_1.logger.info(`[CdpService] Folder path match success: "${workspaceDirName}"`);
451
+ const normalizedDetected = detectedValue.toLowerCase();
452
+ const normalizedProject = projectName.toLowerCase();
453
+ const normalizedWorkspace = workspacePath.toLowerCase();
454
+ if (normalizedDetected.includes(normalizedProject) ||
455
+ normalizedDetected.includes(normalizedWorkspace)) {
456
+ this.currentWorkspaceName = projectName;
457
+ logger_1.logger.debug(`[CdpService] Folder path match success: "${projectName}"`);
452
458
  return true;
453
459
  }
454
460
  }
@@ -457,10 +463,11 @@ class CdpService extends events_1.EventEmitter {
457
463
  expression: 'window.location.href',
458
464
  returnByValue: true,
459
465
  });
460
- const pageUrl = urlResult?.result?.value || '';
461
- if (pageUrl.includes(encodeURIComponent(workspacePath)) || pageUrl.includes(workspaceDirName)) {
462
- this.currentWorkspaceName = workspaceDirName;
463
- logger_1.logger.info(`[CdpService] URL parameter match success: "${workspaceDirName}"`);
466
+ const pageUrl = (urlResult?.result?.value || '').toLowerCase();
467
+ const normalizedWorkspaceUri = encodeURIComponent(workspacePath).toLowerCase();
468
+ if (pageUrl.includes(normalizedWorkspaceUri) || pageUrl.includes(projectName.toLowerCase())) {
469
+ this.currentWorkspaceName = projectName;
470
+ logger_1.logger.debug(`[CdpService] URL parameter match success: "${projectName}"`);
464
471
  return true;
465
472
  }
466
473
  }
@@ -472,19 +479,24 @@ class CdpService extends events_1.EventEmitter {
472
479
  /**
473
480
  * Launch Antigravity and wait for a new workbench page to appear, then connect.
474
481
  */
475
- async launchAndConnectWorkspace(workspacePath, workspaceDirName) {
482
+ async launchAndConnectWorkspace(workspacePath, projectName) {
476
483
  // Open as folder using Antigravity CLI (not as workspace mode).
477
484
  // `open -a Antigravity` may open as workspace, resulting in title "Untitled (Workspace)".
478
485
  // CLI --new-window opens as folder, immediately reflecting directory name in title.
479
- const antigravityCli = '/Applications/Antigravity.app/Contents/Resources/app/bin/antigravity';
480
- logger_1.logger.info(`[CdpService] Launching Antigravity: ${antigravityCli} --new-window ${workspacePath}`);
486
+ const antigravityCli = (0, pathUtils_1.getAntigravityCliPath)();
487
+ logger_1.logger.debug(`[CdpService] Launching Antigravity: ${antigravityCli} --new-window ${workspacePath}`);
481
488
  try {
482
489
  await this.runCommand(antigravityCli, ['--new-window', workspacePath]);
483
490
  }
484
491
  catch (error) {
485
- // Fall back to open -a if CLI not found
486
- logger_1.logger.warn(`[CdpService] CLI launch failed, falling back to open -a: ${error?.message || String(error)}`);
487
- await this.runCommand('open', ['-a', 'Antigravity', workspacePath]);
492
+ // Fall back to open -a if CLI not found (macOS only)
493
+ logger_1.logger.warn(`[CdpService] CLI launch failed, falling back to open -a (if macOS): ${error?.message || String(error)}`);
494
+ if (process.platform === 'darwin') {
495
+ await this.runCommand('open', ['-a', 'Antigravity', workspacePath]);
496
+ }
497
+ else {
498
+ throw error;
499
+ }
488
500
  }
489
501
  // Poll until a new workbench page appears (max 30 seconds)
490
502
  const maxWaitMs = 30000;
@@ -524,12 +536,12 @@ class CdpService extends events_1.EventEmitter {
524
536
  !t.url?.includes('workbench-jetski-agent') &&
525
537
  t.url?.includes('workbench'));
526
538
  // Title match
527
- const titleMatch = workbenchPages.find((t) => t.title?.includes(workspaceDirName));
539
+ const titleMatch = workbenchPages.find((t) => t.title?.toLowerCase().includes(projectName.toLowerCase()));
528
540
  if (titleMatch) {
529
- return this.connectToPage(titleMatch, workspaceDirName);
541
+ return this.connectToPage(titleMatch, projectName);
530
542
  }
531
543
  // CDP probe (also check folder path if title is not updated)
532
- const probeResult = await this.probeWorkbenchPages(workbenchPages, workspaceDirName, workspacePath);
544
+ const probeResult = await this.probeWorkbenchPages(workbenchPages, projectName, workspacePath);
533
545
  if (probeResult) {
534
546
  return true;
535
547
  }
@@ -539,12 +551,12 @@ class CdpService extends events_1.EventEmitter {
539
551
  const newUntitledPages = workbenchPages.filter((t) => !knownPageIds.has(t.id) &&
540
552
  (t.title?.includes('Untitled') || t.title === ''));
541
553
  if (newUntitledPages.length === 1) {
542
- logger_1.logger.info(`[CdpService] New Untitled page detected. Connecting as "${workspaceDirName}" (page.id=${newUntitledPages[0].id})`);
543
- return this.connectToPage(newUntitledPages[0], workspaceDirName);
554
+ logger_1.logger.debug(`[CdpService] New Untitled page detected. Connecting as "${projectName}" (page.id=${newUntitledPages[0].id})`);
555
+ return this.connectToPage(newUntitledPages[0], projectName);
544
556
  }
545
557
  }
546
558
  }
547
- throw new Error(`Workbench page for workspace "${workspaceDirName}" not found within ${maxWaitMs / 1000} seconds`);
559
+ throw new Error(`Workbench page for workspace "${projectName}" not found within ${maxWaitMs / 1000} seconds`);
548
560
  }
549
561
  async runCommand(command, args) {
550
562
  await new Promise((resolve, reject) => {
@@ -698,6 +710,46 @@ class CdpService extends events_1.EventEmitter {
698
710
  }
699
711
  return { ok: false, error: 'Chat input field not found' };
700
712
  }
713
+ /**
714
+ * Select all text in the focused input and delete it to ensure a clean state.
715
+ * Uses Meta+A (select all) then Backspace (delete) via CDP key events.
716
+ */
717
+ async clearInputField() {
718
+ // Meta+A to select all content
719
+ await this.call('Input.dispatchKeyEvent', {
720
+ type: 'keyDown',
721
+ key: 'a',
722
+ code: 'KeyA',
723
+ modifiers: 4, // Meta (Cmd on macOS)
724
+ windowsVirtualKeyCode: 65,
725
+ nativeVirtualKeyCode: 65,
726
+ });
727
+ await this.call('Input.dispatchKeyEvent', {
728
+ type: 'keyUp',
729
+ key: 'a',
730
+ code: 'KeyA',
731
+ modifiers: 4,
732
+ windowsVirtualKeyCode: 65,
733
+ nativeVirtualKeyCode: 65,
734
+ });
735
+ // Backspace to delete selected content
736
+ await this.call('Input.dispatchKeyEvent', {
737
+ type: 'keyDown',
738
+ key: 'Backspace',
739
+ code: 'Backspace',
740
+ windowsVirtualKeyCode: 8,
741
+ nativeVirtualKeyCode: 8,
742
+ });
743
+ await this.call('Input.dispatchKeyEvent', {
744
+ type: 'keyUp',
745
+ key: 'Backspace',
746
+ code: 'Backspace',
747
+ windowsVirtualKeyCode: 8,
748
+ nativeVirtualKeyCode: 8,
749
+ });
750
+ // Wait for DOM to settle
751
+ await new Promise(r => setTimeout(r, 50));
752
+ }
701
753
  /**
702
754
  * Send Enter key to submit the message.
703
755
  */
@@ -815,8 +867,6 @@ class CdpService extends events_1.EventEmitter {
815
867
  const notifyScript = `(() => {
816
868
  const input = document.querySelector('${selector}');
817
869
  if (!input) return { ok: false, error: 'Image input not found' };
818
- input.dispatchEvent(new Event('input', { bubbles: true }));
819
- input.dispatchEvent(new Event('change', { bubbles: true }));
820
870
  input.removeAttribute('data-agclaw-upload-token');
821
871
  return { ok: true };
822
872
  })()`;
@@ -848,6 +898,8 @@ class CdpService extends events_1.EventEmitter {
848
898
  if (!focusResult.ok) {
849
899
  return { ok: false, error: focusResult.error || 'Chat input field not found' };
850
900
  }
901
+ // Clear any existing text in the input field before injecting
902
+ await this.clearInputField();
851
903
  // 1. Input text via CDP Input.insertText
852
904
  await this.call('Input.insertText', { text });
853
905
  await new Promise(r => setTimeout(r, 200));
@@ -866,6 +918,8 @@ class CdpService extends events_1.EventEmitter {
866
918
  if (!focusResult.ok) {
867
919
  return { ok: false, error: focusResult.error || 'Chat input field not found' };
868
920
  }
921
+ // Clear any existing text in the input field before injecting
922
+ await this.clearInputField();
869
923
  const attachResult = await this.attachImageFiles(imageFilePaths, focusResult.contextId);
870
924
  if (!attachResult.ok) {
871
925
  return { ok: false, error: attachResult.error || 'Failed to attach images' };
@@ -1057,6 +1111,48 @@ class CdpService extends events_1.EventEmitter {
1057
1111
  return [];
1058
1112
  }
1059
1113
  }
1114
+ /**
1115
+ * Get the currently selected mode from the Antigravity UI.
1116
+ * Reads the mode toggle button text and maps it back to internal mode name.
1117
+ *
1118
+ * @returns Internal mode name (e.g., 'fast', 'plan') or null if not found
1119
+ */
1120
+ async getCurrentMode() {
1121
+ if (!this.isConnectedFlag || !this.ws) {
1122
+ return null;
1123
+ }
1124
+ const expression = '(() => {'
1125
+ + ' const uiNameMap = { fast: "Fast", plan: "Planning" };'
1126
+ + ' const knownModes = Object.values(uiNameMap).map(n => n.toLowerCase());'
1127
+ + ' const reverseMap = {};'
1128
+ + ' Object.entries(uiNameMap).forEach(([k, v]) => { reverseMap[v.toLowerCase()] = k; });'
1129
+ + ' const allBtns = Array.from(document.querySelectorAll("button"));'
1130
+ + ' const visibleBtns = allBtns.filter(b => b.offsetParent !== null);'
1131
+ + ' const modeToggleBtn = visibleBtns.find(b => {'
1132
+ + ' const text = (b.textContent || "").trim().toLowerCase();'
1133
+ + ' const hasChevron = b.querySelector("svg[class*=\\"chevron\\"]");'
1134
+ + ' return knownModes.some(m => text === m) && hasChevron;'
1135
+ + ' });'
1136
+ + ' if (!modeToggleBtn) return null;'
1137
+ + ' const currentModeText = (modeToggleBtn.textContent || "").trim().toLowerCase();'
1138
+ + ' return reverseMap[currentModeText] || null;'
1139
+ + '})()';
1140
+ try {
1141
+ const contextId = this.getPrimaryContextId();
1142
+ const callParams = {
1143
+ expression,
1144
+ returnByValue: true,
1145
+ awaitPromise: false,
1146
+ };
1147
+ if (contextId !== null)
1148
+ callParams.contextId = contextId;
1149
+ const res = await this.call('Runtime.evaluate', callParams);
1150
+ return res?.result?.value || null;
1151
+ }
1152
+ catch {
1153
+ return null;
1154
+ }
1155
+ }
1060
1156
  /**
1061
1157
  * Operate Antigravity UI mode dropdown to switch to the specified mode.
1062
1158
  * Two-step approach: