lazy-gravity 0.1.0 → 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 (34) hide show
  1. package/README.md +18 -6
  2. package/dist/bin/cli.js +18 -18
  3. package/dist/bin/commands/doctor.js +2 -1
  4. package/dist/bin/commands/start.js +25 -2
  5. package/dist/bot/index.js +346 -152
  6. package/dist/commands/joinCommandHandler.js +302 -0
  7. package/dist/commands/joinDetachCommandHandler.js +285 -0
  8. package/dist/commands/registerSlashCommands.js +35 -0
  9. package/dist/database/chatSessionRepository.js +10 -0
  10. package/dist/database/userPreferenceRepository.js +72 -0
  11. package/dist/events/interactionCreateHandler.js +58 -36
  12. package/dist/events/messageCreateHandler.js +158 -53
  13. package/dist/services/antigravityLauncher.js +4 -3
  14. package/dist/services/approvalDetector.js +6 -0
  15. package/dist/services/cdpBridgeManager.js +184 -84
  16. package/dist/services/cdpConnectionPool.js +79 -51
  17. package/dist/services/cdpService.js +149 -51
  18. package/dist/services/chatSessionService.js +229 -8
  19. package/dist/services/errorPopupDetector.js +6 -0
  20. package/dist/services/planningDetector.js +6 -0
  21. package/dist/services/responseMonitor.js +125 -24
  22. package/dist/services/updateCheckService.js +147 -0
  23. package/dist/services/userMessageDetector.js +221 -0
  24. package/dist/ui/modeUi.js +11 -1
  25. package/dist/ui/outputUi.js +30 -0
  26. package/dist/ui/sessionPickerUi.js +48 -0
  27. package/dist/utils/antigravityPaths.js +94 -0
  28. package/dist/utils/configLoader.js +10 -0
  29. package/dist/utils/discordButtonUtils.js +33 -0
  30. package/dist/utils/logBuffer.js +47 -0
  31. package/dist/utils/logger.js +80 -20
  32. package/dist/utils/pathUtils.js +57 -0
  33. package/dist/utils/plainTextFormatter.js +70 -0
  34. 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.
@@ -15,6 +16,7 @@ class CdpConnectionPool {
15
16
  approvalDetectors = new Map();
16
17
  errorPopupDetectors = new Map();
17
18
  planningDetectors = new Map();
19
+ userMessageDetectors = new Map();
18
20
  connectingPromises = new Map();
19
21
  cdpOptions;
20
22
  constructor(cdpOptions = {}) {
@@ -29,36 +31,36 @@ class CdpConnectionPool {
29
31
  * @returns Connected CdpService
30
32
  */
31
33
  async getOrConnect(workspacePath) {
32
- const dirName = this.extractDirName(workspacePath);
34
+ const projectName = this.extractProjectName(workspacePath);
33
35
  // Return existing connection if available
34
- const existing = this.connections.get(dirName);
36
+ const existing = this.connections.get(projectName);
35
37
  if (existing && existing.isConnected()) {
36
38
  // Re-validate that the still-open window is actually bound to this workspace.
37
39
  await existing.discoverAndConnectForWorkspace(workspacePath);
38
40
  return existing;
39
41
  }
40
42
  // Wait for the pending connection promise if one exists (prevents concurrent connections)
41
- const pending = this.connectingPromises.get(dirName);
43
+ const pending = this.connectingPromises.get(projectName);
42
44
  if (pending) {
43
45
  return pending;
44
46
  }
45
47
  // Start a new connection
46
- const connectPromise = this.createAndConnect(workspacePath, dirName);
47
- this.connectingPromises.set(dirName, connectPromise);
48
+ const connectPromise = this.createAndConnect(workspacePath, projectName);
49
+ this.connectingPromises.set(projectName, connectPromise);
48
50
  try {
49
51
  const cdp = await connectPromise;
50
52
  return cdp;
51
53
  }
52
54
  finally {
53
- this.connectingPromises.delete(dirName);
55
+ this.connectingPromises.delete(projectName);
54
56
  }
55
57
  }
56
58
  /**
57
59
  * Get a connected CdpService (read-only).
58
60
  * Returns null if not connected.
59
61
  */
60
- getConnected(workspaceDirName) {
61
- const cdp = this.connections.get(workspaceDirName);
62
+ getConnected(projectName) {
63
+ const cdp = this.connections.get(projectName);
62
64
  if (cdp && cdp.isConnected()) {
63
65
  return cdp;
64
66
  }
@@ -67,88 +69,109 @@ class CdpConnectionPool {
67
69
  /**
68
70
  * Disconnect the specified workspace.
69
71
  */
70
- disconnectWorkspace(workspaceDirName) {
71
- const cdp = this.connections.get(workspaceDirName);
72
+ disconnectWorkspace(projectName) {
73
+ const cdp = this.connections.get(projectName);
72
74
  if (cdp) {
73
75
  cdp.disconnect().catch((err) => {
74
- logger_1.logger.error(`[CdpConnectionPool] Error while disconnecting ${workspaceDirName}:`, err);
76
+ logger_1.logger.error(`[CdpConnectionPool] Error while disconnecting ${projectName}:`, err);
75
77
  });
76
- this.connections.delete(workspaceDirName);
78
+ this.connections.delete(projectName);
77
79
  }
78
- const detector = this.approvalDetectors.get(workspaceDirName);
80
+ const detector = this.approvalDetectors.get(projectName);
79
81
  if (detector) {
80
82
  detector.stop();
81
- this.approvalDetectors.delete(workspaceDirName);
83
+ this.approvalDetectors.delete(projectName);
82
84
  }
83
- const errorPopupDetector = this.errorPopupDetectors.get(workspaceDirName);
85
+ const errorPopupDetector = this.errorPopupDetectors.get(projectName);
84
86
  if (errorPopupDetector) {
85
87
  errorPopupDetector.stop();
86
- this.errorPopupDetectors.delete(workspaceDirName);
88
+ this.errorPopupDetectors.delete(projectName);
87
89
  }
88
- const planningDetector = this.planningDetectors.get(workspaceDirName);
90
+ const planningDetector = this.planningDetectors.get(projectName);
89
91
  if (planningDetector) {
90
92
  planningDetector.stop();
91
- this.planningDetectors.delete(workspaceDirName);
93
+ this.planningDetectors.delete(projectName);
94
+ }
95
+ const userMsgDetector = this.userMessageDetectors.get(projectName);
96
+ if (userMsgDetector) {
97
+ userMsgDetector.stop();
98
+ this.userMessageDetectors.delete(projectName);
92
99
  }
93
100
  }
94
101
  /**
95
102
  * Disconnect all workspace connections.
96
103
  */
97
104
  disconnectAll() {
98
- for (const dirName of [...this.connections.keys()]) {
99
- this.disconnectWorkspace(dirName);
105
+ for (const projectName of [...this.connections.keys()]) {
106
+ this.disconnectWorkspace(projectName);
100
107
  }
101
108
  }
102
109
  /**
103
110
  * Register an approval detector for a workspace.
104
111
  */
105
- registerApprovalDetector(workspaceDirName, detector) {
112
+ registerApprovalDetector(projectName, detector) {
106
113
  // Stop existing detector
107
- const existing = this.approvalDetectors.get(workspaceDirName);
114
+ const existing = this.approvalDetectors.get(projectName);
108
115
  if (existing && existing.isActive()) {
109
116
  existing.stop();
110
117
  }
111
- this.approvalDetectors.set(workspaceDirName, detector);
118
+ this.approvalDetectors.set(projectName, detector);
112
119
  }
113
120
  /**
114
121
  * Get the approval detector for a workspace.
115
122
  */
116
- getApprovalDetector(workspaceDirName) {
117
- return this.approvalDetectors.get(workspaceDirName);
123
+ getApprovalDetector(projectName) {
124
+ return this.approvalDetectors.get(projectName);
118
125
  }
119
126
  /**
120
127
  * Register an error popup detector for a workspace.
121
128
  */
122
- registerErrorPopupDetector(workspaceDirName, detector) {
129
+ registerErrorPopupDetector(projectName, detector) {
123
130
  // Stop existing detector
124
- const existing = this.errorPopupDetectors.get(workspaceDirName);
131
+ const existing = this.errorPopupDetectors.get(projectName);
125
132
  if (existing && existing.isActive()) {
126
133
  existing.stop();
127
134
  }
128
- this.errorPopupDetectors.set(workspaceDirName, detector);
135
+ this.errorPopupDetectors.set(projectName, detector);
129
136
  }
130
137
  /**
131
138
  * Get the error popup detector for a workspace.
132
139
  */
133
- getErrorPopupDetector(workspaceDirName) {
134
- return this.errorPopupDetectors.get(workspaceDirName);
140
+ getErrorPopupDetector(projectName) {
141
+ return this.errorPopupDetectors.get(projectName);
135
142
  }
136
143
  /**
137
144
  * Register a planning detector for a workspace.
138
145
  */
139
- registerPlanningDetector(workspaceDirName, detector) {
146
+ registerPlanningDetector(projectName, detector) {
140
147
  // Stop existing detector
141
- const existing = this.planningDetectors.get(workspaceDirName);
148
+ const existing = this.planningDetectors.get(projectName);
142
149
  if (existing && existing.isActive()) {
143
150
  existing.stop();
144
151
  }
145
- this.planningDetectors.set(workspaceDirName, detector);
152
+ this.planningDetectors.set(projectName, detector);
146
153
  }
147
154
  /**
148
155
  * Get the planning detector for a workspace.
149
156
  */
150
- getPlanningDetector(workspaceDirName) {
151
- return this.planningDetectors.get(workspaceDirName);
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);
152
175
  }
153
176
  /**
154
177
  * Return a list of workspace names with active connections.
@@ -163,50 +186,55 @@ class CdpConnectionPool {
163
186
  return active;
164
187
  }
165
188
  /**
166
- * Extract the directory name from a workspace path.
189
+ * Extract the project name from a workspace path.
167
190
  */
168
- extractDirName(workspacePath) {
169
- return workspacePath.split('/').filter(Boolean).pop() || workspacePath;
191
+ extractProjectName(workspacePath) {
192
+ return (0, pathUtils_1.extractProjectNameFromPath)(workspacePath) || workspacePath;
170
193
  }
171
194
  /**
172
195
  * Create a new CdpService and connect to the workspace.
173
196
  */
174
- async createAndConnect(workspacePath, dirName) {
197
+ async createAndConnect(workspacePath, projectName) {
175
198
  // Disconnect old connection if exists
176
- const old = this.connections.get(dirName);
199
+ const old = this.connections.get(projectName);
177
200
  if (old) {
178
201
  await old.disconnect().catch(() => { });
179
- this.connections.delete(dirName);
202
+ this.connections.delete(projectName);
180
203
  }
181
204
  const cdp = new cdpService_1.CdpService(this.cdpOptions);
182
205
  // Auto-cleanup on disconnect
183
206
  cdp.on('disconnected', () => {
184
- logger_1.logger.error(`[CdpConnectionPool] Workspace "${dirName}" disconnected`);
207
+ logger_1.logger.error(`[CdpConnectionPool] Workspace "${projectName}" disconnected`);
185
208
  // Only remove from Map when reconnection fails
186
209
  // (CdpService attempts reconnection internally, so we don't remove here)
187
210
  });
188
211
  cdp.on('reconnectFailed', () => {
189
- logger_1.logger.error(`[CdpConnectionPool] Reconnection failed for workspace "${dirName}". Removing from pool`);
190
- this.connections.delete(dirName);
191
- 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);
192
215
  if (detector) {
193
216
  detector.stop();
194
- this.approvalDetectors.delete(dirName);
217
+ this.approvalDetectors.delete(projectName);
195
218
  }
196
- const errorDetector = this.errorPopupDetectors.get(dirName);
219
+ const errorDetector = this.errorPopupDetectors.get(projectName);
197
220
  if (errorDetector) {
198
221
  errorDetector.stop();
199
- this.errorPopupDetectors.delete(dirName);
222
+ this.errorPopupDetectors.delete(projectName);
200
223
  }
201
- const planDetector = this.planningDetectors.get(dirName);
224
+ const planDetector = this.planningDetectors.get(projectName);
202
225
  if (planDetector) {
203
226
  planDetector.stop();
204
- this.planningDetectors.delete(dirName);
227
+ this.planningDetectors.delete(projectName);
228
+ }
229
+ const userMsgDetector = this.userMessageDetectors.get(projectName);
230
+ if (userMsgDetector) {
231
+ userMsgDetector.stop();
232
+ this.userMessageDetectors.delete(projectName);
205
233
  }
206
234
  });
207
235
  // Connect to the workspace
208
236
  await cdp.discoverAndConnectForWorkspace(workspacePath);
209
- this.connections.set(dirName, cdp);
237
+ this.connections.set(projectName, cdp);
210
238
  return cdp;
211
239
  }
212
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
  */
@@ -846,6 +898,8 @@ class CdpService extends events_1.EventEmitter {
846
898
  if (!focusResult.ok) {
847
899
  return { ok: false, error: focusResult.error || 'Chat input field not found' };
848
900
  }
901
+ // Clear any existing text in the input field before injecting
902
+ await this.clearInputField();
849
903
  // 1. Input text via CDP Input.insertText
850
904
  await this.call('Input.insertText', { text });
851
905
  await new Promise(r => setTimeout(r, 200));
@@ -864,6 +918,8 @@ class CdpService extends events_1.EventEmitter {
864
918
  if (!focusResult.ok) {
865
919
  return { ok: false, error: focusResult.error || 'Chat input field not found' };
866
920
  }
921
+ // Clear any existing text in the input field before injecting
922
+ await this.clearInputField();
867
923
  const attachResult = await this.attachImageFiles(imageFilePaths, focusResult.contextId);
868
924
  if (!attachResult.ok) {
869
925
  return { ok: false, error: attachResult.error || 'Failed to attach images' };
@@ -1055,6 +1111,48 @@ class CdpService extends events_1.EventEmitter {
1055
1111
  return [];
1056
1112
  }
1057
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
+ }
1058
1156
  /**
1059
1157
  * Operate Antigravity UI mode dropdown to switch to the specified mode.
1060
1158
  * Two-step approach: