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.
- package/README.md +18 -6
- package/dist/bin/cli.js +18 -18
- package/dist/bin/commands/doctor.js +2 -1
- package/dist/bin/commands/start.js +25 -2
- package/dist/bot/index.js +346 -152
- package/dist/commands/joinCommandHandler.js +302 -0
- package/dist/commands/joinDetachCommandHandler.js +285 -0
- package/dist/commands/registerSlashCommands.js +35 -0
- package/dist/database/chatSessionRepository.js +10 -0
- package/dist/database/userPreferenceRepository.js +72 -0
- package/dist/events/interactionCreateHandler.js +58 -36
- package/dist/events/messageCreateHandler.js +158 -53
- package/dist/services/antigravityLauncher.js +4 -3
- package/dist/services/approvalDetector.js +6 -0
- package/dist/services/cdpBridgeManager.js +184 -84
- package/dist/services/cdpConnectionPool.js +79 -51
- package/dist/services/cdpService.js +149 -51
- package/dist/services/chatSessionService.js +229 -8
- package/dist/services/errorPopupDetector.js +6 -0
- package/dist/services/planningDetector.js +6 -0
- package/dist/services/responseMonitor.js +125 -24
- package/dist/services/updateCheckService.js +147 -0
- package/dist/services/userMessageDetector.js +221 -0
- package/dist/ui/modeUi.js +11 -1
- package/dist/ui/outputUi.js +30 -0
- package/dist/ui/sessionPickerUi.js +48 -0
- package/dist/utils/antigravityPaths.js +94 -0
- package/dist/utils/configLoader.js +10 -0
- package/dist/utils/discordButtonUtils.js +33 -0
- package/dist/utils/logBuffer.js +47 -0
- package/dist/utils/logger.js +80 -20
- package/dist/utils/pathUtils.js +57 -0
- package/dist/utils/plainTextFormatter.js +70 -0
- 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
|
|
34
|
+
const projectName = this.extractProjectName(workspacePath);
|
|
33
35
|
// Return existing connection if available
|
|
34
|
-
const existing = this.connections.get(
|
|
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(
|
|
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,
|
|
47
|
-
this.connectingPromises.set(
|
|
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(
|
|
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(
|
|
61
|
-
const cdp = this.connections.get(
|
|
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(
|
|
71
|
-
const cdp = this.connections.get(
|
|
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 ${
|
|
76
|
+
logger_1.logger.error(`[CdpConnectionPool] Error while disconnecting ${projectName}:`, err);
|
|
75
77
|
});
|
|
76
|
-
this.connections.delete(
|
|
78
|
+
this.connections.delete(projectName);
|
|
77
79
|
}
|
|
78
|
-
const detector = this.approvalDetectors.get(
|
|
80
|
+
const detector = this.approvalDetectors.get(projectName);
|
|
79
81
|
if (detector) {
|
|
80
82
|
detector.stop();
|
|
81
|
-
this.approvalDetectors.delete(
|
|
83
|
+
this.approvalDetectors.delete(projectName);
|
|
82
84
|
}
|
|
83
|
-
const errorPopupDetector = this.errorPopupDetectors.get(
|
|
85
|
+
const errorPopupDetector = this.errorPopupDetectors.get(projectName);
|
|
84
86
|
if (errorPopupDetector) {
|
|
85
87
|
errorPopupDetector.stop();
|
|
86
|
-
this.errorPopupDetectors.delete(
|
|
88
|
+
this.errorPopupDetectors.delete(projectName);
|
|
87
89
|
}
|
|
88
|
-
const planningDetector = this.planningDetectors.get(
|
|
90
|
+
const planningDetector = this.planningDetectors.get(projectName);
|
|
89
91
|
if (planningDetector) {
|
|
90
92
|
planningDetector.stop();
|
|
91
|
-
this.planningDetectors.delete(
|
|
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
|
|
99
|
-
this.disconnectWorkspace(
|
|
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(
|
|
112
|
+
registerApprovalDetector(projectName, detector) {
|
|
106
113
|
// Stop existing detector
|
|
107
|
-
const existing = this.approvalDetectors.get(
|
|
114
|
+
const existing = this.approvalDetectors.get(projectName);
|
|
108
115
|
if (existing && existing.isActive()) {
|
|
109
116
|
existing.stop();
|
|
110
117
|
}
|
|
111
|
-
this.approvalDetectors.set(
|
|
118
|
+
this.approvalDetectors.set(projectName, detector);
|
|
112
119
|
}
|
|
113
120
|
/**
|
|
114
121
|
* Get the approval detector for a workspace.
|
|
115
122
|
*/
|
|
116
|
-
getApprovalDetector(
|
|
117
|
-
return this.approvalDetectors.get(
|
|
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(
|
|
129
|
+
registerErrorPopupDetector(projectName, detector) {
|
|
123
130
|
// Stop existing detector
|
|
124
|
-
const existing = this.errorPopupDetectors.get(
|
|
131
|
+
const existing = this.errorPopupDetectors.get(projectName);
|
|
125
132
|
if (existing && existing.isActive()) {
|
|
126
133
|
existing.stop();
|
|
127
134
|
}
|
|
128
|
-
this.errorPopupDetectors.set(
|
|
135
|
+
this.errorPopupDetectors.set(projectName, detector);
|
|
129
136
|
}
|
|
130
137
|
/**
|
|
131
138
|
* Get the error popup detector for a workspace.
|
|
132
139
|
*/
|
|
133
|
-
getErrorPopupDetector(
|
|
134
|
-
return this.errorPopupDetectors.get(
|
|
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(
|
|
146
|
+
registerPlanningDetector(projectName, detector) {
|
|
140
147
|
// Stop existing detector
|
|
141
|
-
const existing = this.planningDetectors.get(
|
|
148
|
+
const existing = this.planningDetectors.get(projectName);
|
|
142
149
|
if (existing && existing.isActive()) {
|
|
143
150
|
existing.stop();
|
|
144
151
|
}
|
|
145
|
-
this.planningDetectors.set(
|
|
152
|
+
this.planningDetectors.set(projectName, detector);
|
|
146
153
|
}
|
|
147
154
|
/**
|
|
148
155
|
* Get the planning detector for a workspace.
|
|
149
156
|
*/
|
|
150
|
-
getPlanningDetector(
|
|
151
|
-
return this.planningDetectors.get(
|
|
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
|
|
189
|
+
* Extract the project name from a workspace path.
|
|
167
190
|
*/
|
|
168
|
-
|
|
169
|
-
return
|
|
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,
|
|
197
|
+
async createAndConnect(workspacePath, projectName) {
|
|
175
198
|
// Disconnect old connection if exists
|
|
176
|
-
const old = this.connections.get(
|
|
199
|
+
const old = this.connections.get(projectName);
|
|
177
200
|
if (old) {
|
|
178
201
|
await old.disconnect().catch(() => { });
|
|
179
|
-
this.connections.delete(
|
|
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 "${
|
|
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 "${
|
|
190
|
-
this.connections.delete(
|
|
191
|
-
const detector = this.approvalDetectors.get(
|
|
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(
|
|
217
|
+
this.approvalDetectors.delete(projectName);
|
|
195
218
|
}
|
|
196
|
-
const errorDetector = this.errorPopupDetectors.get(
|
|
219
|
+
const errorDetector = this.errorPopupDetectors.get(projectName);
|
|
197
220
|
if (errorDetector) {
|
|
198
221
|
errorDetector.stop();
|
|
199
|
-
this.errorPopupDetectors.delete(
|
|
222
|
+
this.errorPopupDetectors.delete(projectName);
|
|
200
223
|
}
|
|
201
|
-
const planDetector = this.planningDetectors.get(
|
|
224
|
+
const planDetector = this.planningDetectors.get(projectName);
|
|
202
225
|
if (planDetector) {
|
|
203
226
|
planDetector.stop();
|
|
204
|
-
this.planningDetectors.delete(
|
|
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(
|
|
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
|
|
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 ===
|
|
258
|
-
const stillMatched = await this.verifyCurrentWorkspace(
|
|
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="${
|
|
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,
|
|
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(
|
|
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(
|
|
286
|
-
this.currentWorkspaceName =
|
|
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(
|
|
294
|
+
return this.probeWorkspaceFolderPath(projectName, workspacePath);
|
|
294
295
|
}
|
|
295
|
-
async _discoverAndConnectForWorkspaceImpl(workspacePath,
|
|
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,
|
|
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.
|
|
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.
|
|
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(
|
|
333
|
+
const titleMatch = workbenchPages.find((t) => t.title?.includes(projectName));
|
|
333
334
|
if (titleMatch) {
|
|
334
|
-
return this.connectToPage(titleMatch,
|
|
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.
|
|
338
|
-
const probeResult = await this.probeWorkbenchPages(workbenchPages,
|
|
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,
|
|
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,
|
|
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 =
|
|
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 =
|
|
358
|
-
logger_1.logger.
|
|
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
|
|
369
|
+
* @param projectName Workspace directory name
|
|
369
370
|
* @param workspacePath Full workspace path (for folder path matching)
|
|
370
371
|
*/
|
|
371
|
-
async probeWorkbenchPages(workbenchPages,
|
|
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
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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 (
|
|
390
|
-
const folderMatch = await this.probeWorkspaceFolderPath(
|
|
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(
|
|
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
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
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
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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,
|
|
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 =
|
|
480
|
-
logger_1.logger.
|
|
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
|
-
|
|
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(
|
|
539
|
+
const titleMatch = workbenchPages.find((t) => t.title?.toLowerCase().includes(projectName.toLowerCase()));
|
|
528
540
|
if (titleMatch) {
|
|
529
|
-
return this.connectToPage(titleMatch,
|
|
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,
|
|
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.
|
|
543
|
-
return this.connectToPage(newUntitledPages[0],
|
|
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 "${
|
|
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:
|