remoat 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/LICENSE +21 -0
- package/README.md +297 -0
- package/dist/bin/cli.d.ts +2 -0
- package/dist/bin/cli.js +80 -0
- package/dist/bin/cli.js.map +1 -0
- package/dist/bin/commands/doctor.d.ts +1 -0
- package/dist/bin/commands/doctor.js +211 -0
- package/dist/bin/commands/doctor.js.map +1 -0
- package/dist/bin/commands/open.d.ts +1 -0
- package/dist/bin/commands/open.js +187 -0
- package/dist/bin/commands/open.js.map +1 -0
- package/dist/bin/commands/setup.d.ts +1 -0
- package/dist/bin/commands/setup.js +267 -0
- package/dist/bin/commands/setup.js.map +1 -0
- package/dist/bin/commands/start.d.ts +2 -0
- package/dist/bin/commands/start.js +39 -0
- package/dist/bin/commands/start.js.map +1 -0
- package/dist/bot/index.d.ts +2 -0
- package/dist/bot/index.js +1393 -0
- package/dist/bot/index.js.map +1 -0
- package/dist/commands/chatCommandHandler.d.ts +20 -0
- package/dist/commands/chatCommandHandler.js +30 -0
- package/dist/commands/chatCommandHandler.js.map +1 -0
- package/dist/commands/cleanupCommandHandler.d.ts +21 -0
- package/dist/commands/cleanupCommandHandler.js +40 -0
- package/dist/commands/cleanupCommandHandler.js.map +1 -0
- package/dist/commands/joinCommandHandler.d.ts +19 -0
- package/dist/commands/joinCommandHandler.js +27 -0
- package/dist/commands/joinCommandHandler.js.map +1 -0
- package/dist/commands/messageParser.d.ts +7 -0
- package/dist/commands/messageParser.js +29 -0
- package/dist/commands/messageParser.js.map +1 -0
- package/dist/commands/slashCommandHandler.d.ts +21 -0
- package/dist/commands/slashCommandHandler.js +105 -0
- package/dist/commands/slashCommandHandler.js.map +1 -0
- package/dist/commands/workspaceCommandHandler.d.ts +16 -0
- package/dist/commands/workspaceCommandHandler.js +29 -0
- package/dist/commands/workspaceCommandHandler.js.map +1 -0
- package/dist/database/chatSessionRepository.d.ts +59 -0
- package/dist/database/chatSessionRepository.js +110 -0
- package/dist/database/chatSessionRepository.js.map +1 -0
- package/dist/database/scheduleRepository.d.ts +60 -0
- package/dist/database/scheduleRepository.js +106 -0
- package/dist/database/scheduleRepository.js.map +1 -0
- package/dist/database/templateRepository.d.ts +51 -0
- package/dist/database/templateRepository.js +90 -0
- package/dist/database/templateRepository.js.map +1 -0
- package/dist/database/workspaceBindingRepository.d.ts +48 -0
- package/dist/database/workspaceBindingRepository.js +92 -0
- package/dist/database/workspaceBindingRepository.js.map +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/auth.d.ts +5 -0
- package/dist/middleware/auth.js +14 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware/sanitize.d.ts +1 -0
- package/dist/middleware/sanitize.js +18 -0
- package/dist/middleware/sanitize.js.map +1 -0
- package/dist/services/antigravityLauncher.d.ts +7 -0
- package/dist/services/antigravityLauncher.js +94 -0
- package/dist/services/antigravityLauncher.js.map +1 -0
- package/dist/services/approvalDetector.d.ts +97 -0
- package/dist/services/approvalDetector.js +394 -0
- package/dist/services/approvalDetector.js.map +1 -0
- package/dist/services/assistantDomExtractor.d.ts +49 -0
- package/dist/services/assistantDomExtractor.js +340 -0
- package/dist/services/assistantDomExtractor.js.map +1 -0
- package/dist/services/autoAcceptService.d.ts +14 -0
- package/dist/services/autoAcceptService.js +81 -0
- package/dist/services/autoAcceptService.js.map +1 -0
- package/dist/services/cdpBridgeManager.d.ts +50 -0
- package/dist/services/cdpBridgeManager.js +355 -0
- package/dist/services/cdpBridgeManager.js.map +1 -0
- package/dist/services/cdpConnectionPool.d.ts +88 -0
- package/dist/services/cdpConnectionPool.js +235 -0
- package/dist/services/cdpConnectionPool.js.map +1 -0
- package/dist/services/cdpService.d.ts +214 -0
- package/dist/services/cdpService.js +1423 -0
- package/dist/services/cdpService.js.map +1 -0
- package/dist/services/chatSessionService.d.ts +89 -0
- package/dist/services/chatSessionService.js +738 -0
- package/dist/services/chatSessionService.js.map +1 -0
- package/dist/services/errorPopupDetector.d.ts +89 -0
- package/dist/services/errorPopupDetector.js +274 -0
- package/dist/services/errorPopupDetector.js.map +1 -0
- package/dist/services/modeService.d.ts +44 -0
- package/dist/services/modeService.js +74 -0
- package/dist/services/modeService.js.map +1 -0
- package/dist/services/modelService.d.ts +36 -0
- package/dist/services/modelService.js +64 -0
- package/dist/services/modelService.js.map +1 -0
- package/dist/services/planningDetector.d.ts +87 -0
- package/dist/services/planningDetector.js +321 -0
- package/dist/services/planningDetector.js.map +1 -0
- package/dist/services/processManager.d.ts +18 -0
- package/dist/services/processManager.js +62 -0
- package/dist/services/processManager.js.map +1 -0
- package/dist/services/progressSender.d.ts +20 -0
- package/dist/services/progressSender.js +65 -0
- package/dist/services/progressSender.js.map +1 -0
- package/dist/services/promptDispatcher.d.ts +38 -0
- package/dist/services/promptDispatcher.js +42 -0
- package/dist/services/promptDispatcher.js.map +1 -0
- package/dist/services/quotaService.d.ts +21 -0
- package/dist/services/quotaService.js +191 -0
- package/dist/services/quotaService.js.map +1 -0
- package/dist/services/responseMonitor.d.ts +129 -0
- package/dist/services/responseMonitor.js +996 -0
- package/dist/services/responseMonitor.js.map +1 -0
- package/dist/services/scheduleService.d.ts +58 -0
- package/dist/services/scheduleService.js +135 -0
- package/dist/services/scheduleService.js.map +1 -0
- package/dist/services/screenshotService.d.ts +55 -0
- package/dist/services/screenshotService.js +86 -0
- package/dist/services/screenshotService.js.map +1 -0
- package/dist/services/telegramTopicManager.d.ts +40 -0
- package/dist/services/telegramTopicManager.js +103 -0
- package/dist/services/telegramTopicManager.js.map +1 -0
- package/dist/services/titleGeneratorService.d.ts +32 -0
- package/dist/services/titleGeneratorService.js +114 -0
- package/dist/services/titleGeneratorService.js.map +1 -0
- package/dist/services/updateCheckService.d.ts +16 -0
- package/dist/services/updateCheckService.js +148 -0
- package/dist/services/updateCheckService.js.map +1 -0
- package/dist/services/userMessageDetector.d.ts +57 -0
- package/dist/services/userMessageDetector.js +222 -0
- package/dist/services/userMessageDetector.js.map +1 -0
- package/dist/services/workspaceService.d.ts +33 -0
- package/dist/services/workspaceService.js +65 -0
- package/dist/services/workspaceService.js.map +1 -0
- package/dist/ui/autoAcceptUi.d.ts +6 -0
- package/dist/ui/autoAcceptUi.js +22 -0
- package/dist/ui/autoAcceptUi.js.map +1 -0
- package/dist/ui/modeUi.d.ts +12 -0
- package/dist/ui/modeUi.js +40 -0
- package/dist/ui/modeUi.js.map +1 -0
- package/dist/ui/modelsUi.d.ts +12 -0
- package/dist/ui/modelsUi.js +101 -0
- package/dist/ui/modelsUi.js.map +1 -0
- package/dist/ui/projectListUi.d.ts +11 -0
- package/dist/ui/projectListUi.js +59 -0
- package/dist/ui/projectListUi.js.map +1 -0
- package/dist/ui/screenshotUi.d.ts +6 -0
- package/dist/ui/screenshotUi.js +28 -0
- package/dist/ui/screenshotUi.js.map +1 -0
- package/dist/ui/sessionPickerUi.d.ts +8 -0
- package/dist/ui/sessionPickerUi.js +32 -0
- package/dist/ui/sessionPickerUi.js.map +1 -0
- package/dist/ui/templateUi.d.ts +5 -0
- package/dist/ui/templateUi.js +44 -0
- package/dist/ui/templateUi.js.map +1 -0
- package/dist/utils/cdpPorts.d.ts +2 -0
- package/dist/utils/cdpPorts.js +6 -0
- package/dist/utils/cdpPorts.js.map +1 -0
- package/dist/utils/config.d.ts +14 -0
- package/dist/utils/config.js +12 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/configLoader.d.ts +23 -0
- package/dist/utils/configLoader.js +153 -0
- package/dist/utils/configLoader.js.map +1 -0
- package/dist/utils/htmlToTelegramMarkdown.d.ts +6 -0
- package/dist/utils/htmlToTelegramMarkdown.js +189 -0
- package/dist/utils/htmlToTelegramMarkdown.js.map +1 -0
- package/dist/utils/i18n.d.ts +3 -0
- package/dist/utils/i18n.js +78 -0
- package/dist/utils/i18n.js.map +1 -0
- package/dist/utils/imageHandler.d.ts +35 -0
- package/dist/utils/imageHandler.js +155 -0
- package/dist/utils/imageHandler.js.map +1 -0
- package/dist/utils/lockfile.d.ts +7 -0
- package/dist/utils/lockfile.js +117 -0
- package/dist/utils/lockfile.js.map +1 -0
- package/dist/utils/logger.d.ts +23 -0
- package/dist/utils/logger.js +85 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/logo.d.ts +1 -0
- package/dist/utils/logo.js +14 -0
- package/dist/utils/logo.js.map +1 -0
- package/dist/utils/metadataExtractor.d.ts +5 -0
- package/dist/utils/metadataExtractor.js +16 -0
- package/dist/utils/metadataExtractor.js.map +1 -0
- package/dist/utils/pathUtils.d.ts +23 -0
- package/dist/utils/pathUtils.js +58 -0
- package/dist/utils/pathUtils.js.map +1 -0
- package/dist/utils/processLogBuffer.d.ts +17 -0
- package/dist/utils/processLogBuffer.js +108 -0
- package/dist/utils/processLogBuffer.js.map +1 -0
- package/dist/utils/streamMessageFormatter.d.ts +18 -0
- package/dist/utils/streamMessageFormatter.js +91 -0
- package/dist/utils/streamMessageFormatter.js.map +1 -0
- package/dist/utils/telegramFormatter.d.ts +37 -0
- package/dist/utils/telegramFormatter.js +445 -0
- package/dist/utils/telegramFormatter.js.map +1 -0
- package/dist/utils/voiceHandler.d.ts +23 -0
- package/dist/utils/voiceHandler.js +169 -0
- package/dist/utils/voiceHandler.js.map +1 -0
- package/locales/en.json +85 -0
- package/locales/ja.json +109 -0
- package/package.json +84 -0
|
@@ -0,0 +1,1423 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.CdpService = void 0;
|
|
40
|
+
const logger_1 = require("../utils/logger");
|
|
41
|
+
const cdpPorts_1 = require("../utils/cdpPorts");
|
|
42
|
+
const events_1 = require("events");
|
|
43
|
+
const http = __importStar(require("http"));
|
|
44
|
+
const child_process_1 = require("child_process");
|
|
45
|
+
const pathUtils_1 = require("../utils/pathUtils");
|
|
46
|
+
const ws_1 = __importDefault(require("ws"));
|
|
47
|
+
/** Antigravity UI DOM selector constants */
|
|
48
|
+
const SELECTORS = {
|
|
49
|
+
/** Chat input box: textbox excluding xterm */
|
|
50
|
+
CHAT_INPUT: 'div[role="textbox"]:not(.xterm-helper-textarea)',
|
|
51
|
+
/** Submit button search target tag */
|
|
52
|
+
SUBMIT_BUTTON_CONTAINER: 'button',
|
|
53
|
+
/** Submit icon SVG class candidates */
|
|
54
|
+
SUBMIT_BUTTON_SVG_CLASSES: ['lucide-arrow-right', 'lucide-arrow-up', 'lucide-send'],
|
|
55
|
+
/** Keyword to identify message injection target context */
|
|
56
|
+
CONTEXT_URL_KEYWORD: 'cascade-panel',
|
|
57
|
+
};
|
|
58
|
+
class CdpService extends events_1.EventEmitter {
|
|
59
|
+
ports;
|
|
60
|
+
isConnectedFlag = false;
|
|
61
|
+
ws = null;
|
|
62
|
+
contexts = [];
|
|
63
|
+
pendingCalls = new Map();
|
|
64
|
+
idCounter = 1;
|
|
65
|
+
cdpCallTimeout = 30000;
|
|
66
|
+
targetUrl = null;
|
|
67
|
+
/** Number of auto-reconnect attempts on disconnect */
|
|
68
|
+
maxReconnectAttempts;
|
|
69
|
+
/** Delay between reconnect attempts (ms) */
|
|
70
|
+
reconnectDelayMs;
|
|
71
|
+
/** Current reconnect attempt count */
|
|
72
|
+
reconnectAttemptCount = 0;
|
|
73
|
+
/** Reconnecting flag (prevents double connections) */
|
|
74
|
+
isReconnecting = false;
|
|
75
|
+
/** Currently connected workspace name */
|
|
76
|
+
currentWorkspaceName = null;
|
|
77
|
+
/** Last requested workspace path (used for deterministic reconnect) */
|
|
78
|
+
currentWorkspacePath = null;
|
|
79
|
+
/** Workspace switching flag (suppresses disconnected event) */
|
|
80
|
+
isSwitchingWorkspace = false;
|
|
81
|
+
constructor(options = {}) {
|
|
82
|
+
super();
|
|
83
|
+
this.ports = options.portsToScan || [...cdpPorts_1.CDP_PORTS];
|
|
84
|
+
if (options.cdpCallTimeout)
|
|
85
|
+
this.cdpCallTimeout = options.cdpCallTimeout;
|
|
86
|
+
this.maxReconnectAttempts = options.maxReconnectAttempts ?? 3;
|
|
87
|
+
this.reconnectDelayMs = options.reconnectDelayMs ?? 2000;
|
|
88
|
+
}
|
|
89
|
+
async getJson(url) {
|
|
90
|
+
return new Promise((resolve, reject) => {
|
|
91
|
+
const req = http.get(url, (res) => {
|
|
92
|
+
if (res.statusCode && (res.statusCode < 200 || res.statusCode >= 300)) {
|
|
93
|
+
res.resume(); // drain response
|
|
94
|
+
reject(new Error(`HTTP ${res.statusCode} from ${url}`));
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
let data = '';
|
|
98
|
+
res.on('data', chunk => data += chunk);
|
|
99
|
+
res.on('end', () => {
|
|
100
|
+
try {
|
|
101
|
+
resolve(JSON.parse(data));
|
|
102
|
+
}
|
|
103
|
+
catch (e) {
|
|
104
|
+
reject(e);
|
|
105
|
+
}
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
req.on('error', reject);
|
|
109
|
+
req.setTimeout(5000, () => {
|
|
110
|
+
req.destroy();
|
|
111
|
+
reject(new Error(`Timeout fetching ${url}`));
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
async discoverTarget() {
|
|
116
|
+
let allPages = [];
|
|
117
|
+
for (const port of this.ports) {
|
|
118
|
+
try {
|
|
119
|
+
const list = await this.getJson(`http://127.0.0.1:${port}/json/list`);
|
|
120
|
+
allPages.push(...list);
|
|
121
|
+
}
|
|
122
|
+
catch (e) {
|
|
123
|
+
// Ignore port not found
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
let target = allPages.find(t => t.type === 'page' &&
|
|
127
|
+
t.webSocketDebuggerUrl &&
|
|
128
|
+
!t.title?.includes('Launchpad') &&
|
|
129
|
+
!t.url?.includes('workbench-jetski-agent') &&
|
|
130
|
+
(t.url?.includes('workbench') || t.title?.includes('Antigravity') || t.title?.includes('Cascade')));
|
|
131
|
+
if (!target) {
|
|
132
|
+
target = allPages.find(t => t.webSocketDebuggerUrl &&
|
|
133
|
+
(t.url?.includes('workbench') || t.title?.includes('Antigravity') || t.title?.includes('Cascade')) &&
|
|
134
|
+
!t.title?.includes('Launchpad'));
|
|
135
|
+
}
|
|
136
|
+
if (!target) {
|
|
137
|
+
target = allPages.find(t => t.webSocketDebuggerUrl &&
|
|
138
|
+
(t.url?.includes('workbench') || t.title?.includes('Antigravity') || t.title?.includes('Cascade') || t.title?.includes('Launchpad')));
|
|
139
|
+
}
|
|
140
|
+
if (target && target.webSocketDebuggerUrl) {
|
|
141
|
+
this.targetUrl = target.webSocketDebuggerUrl;
|
|
142
|
+
// Extract workspace name from title (e.g., "ProjectName — Antigravity")
|
|
143
|
+
if (target.title && !this.currentWorkspaceName) {
|
|
144
|
+
const titleParts = target.title.split(/\s[—–-]\s/);
|
|
145
|
+
if (titleParts.length > 0) {
|
|
146
|
+
this.currentWorkspaceName = titleParts[0].trim();
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return target.webSocketDebuggerUrl;
|
|
150
|
+
}
|
|
151
|
+
throw new Error('CDP target not found on any port.');
|
|
152
|
+
}
|
|
153
|
+
async connect() {
|
|
154
|
+
if (!this.targetUrl) {
|
|
155
|
+
await this.discoverTarget();
|
|
156
|
+
}
|
|
157
|
+
if (!this.targetUrl)
|
|
158
|
+
throw new Error('Target URL not established.');
|
|
159
|
+
this.ws = new ws_1.default(this.targetUrl);
|
|
160
|
+
await new Promise((resolve, reject) => {
|
|
161
|
+
if (!this.ws)
|
|
162
|
+
return reject(new Error('WebSocket not initialized'));
|
|
163
|
+
this.ws.on('open', () => {
|
|
164
|
+
this.isConnectedFlag = true;
|
|
165
|
+
resolve();
|
|
166
|
+
});
|
|
167
|
+
this.ws.on('error', reject);
|
|
168
|
+
});
|
|
169
|
+
this.ws.on('message', (msg) => {
|
|
170
|
+
try {
|
|
171
|
+
const data = JSON.parse(msg.toString());
|
|
172
|
+
if (data.id !== undefined && this.pendingCalls.has(data.id)) {
|
|
173
|
+
const { resolve, reject, timeoutId } = this.pendingCalls.get(data.id);
|
|
174
|
+
clearTimeout(timeoutId);
|
|
175
|
+
this.pendingCalls.delete(data.id);
|
|
176
|
+
if (data.error)
|
|
177
|
+
reject(new Error(data.error.message || JSON.stringify(data.error)));
|
|
178
|
+
else
|
|
179
|
+
resolve(data.result);
|
|
180
|
+
}
|
|
181
|
+
if (data.method === 'Runtime.executionContextCreated') {
|
|
182
|
+
this.contexts.push(data.params.context);
|
|
183
|
+
}
|
|
184
|
+
if (data.method === 'Runtime.executionContextDestroyed') {
|
|
185
|
+
const idx = this.contexts.findIndex(c => c.id === data.params.executionContextId);
|
|
186
|
+
if (idx !== -1)
|
|
187
|
+
this.contexts.splice(idx, 1);
|
|
188
|
+
}
|
|
189
|
+
// Forward CDP events via EventEmitter (Network.*, Runtime.*, etc.)
|
|
190
|
+
if (data.method) {
|
|
191
|
+
this.emit(data.method, data.params);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
catch (e) {
|
|
195
|
+
logger_1.logger.warn('[CdpService] Failed to parse WebSocket message:', e);
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
this.ws.on('close', () => {
|
|
199
|
+
this.isConnectedFlag = false;
|
|
200
|
+
// Reject all unresolved pending calls to prevent memory leaks
|
|
201
|
+
this.clearPendingCalls(new Error('WebSocket disconnected'));
|
|
202
|
+
this.ws = null;
|
|
203
|
+
this.targetUrl = null;
|
|
204
|
+
// Suppress disconnected event and auto-reconnect during workspace switching
|
|
205
|
+
if (this.isSwitchingWorkspace)
|
|
206
|
+
return;
|
|
207
|
+
this.emit('disconnected');
|
|
208
|
+
// Attempt auto-reconnect (when maxReconnectAttempts > 0)
|
|
209
|
+
if (this.maxReconnectAttempts > 0 && !this.isReconnecting) {
|
|
210
|
+
this.tryReconnect();
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
// Initialize Runtime to get execution contexts
|
|
214
|
+
await this.call('Runtime.enable', {});
|
|
215
|
+
// Enable Network domain for event-based completion detection
|
|
216
|
+
try {
|
|
217
|
+
await this.call('Network.enable', {});
|
|
218
|
+
}
|
|
219
|
+
catch {
|
|
220
|
+
// Network.enable failure is non-fatal; polling fallback still works
|
|
221
|
+
logger_1.logger.warn('[CdpService] Network.enable failed — network event detection disabled');
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
async call(method, params = {}) {
|
|
225
|
+
if (!this.ws || this.ws.readyState !== ws_1.default.OPEN) {
|
|
226
|
+
throw new Error('WebSocket is not connected');
|
|
227
|
+
}
|
|
228
|
+
return new Promise((resolve, reject) => {
|
|
229
|
+
const id = this.idCounter++;
|
|
230
|
+
const timeoutId = setTimeout(() => {
|
|
231
|
+
if (this.pendingCalls.has(id)) {
|
|
232
|
+
this.pendingCalls.delete(id);
|
|
233
|
+
reject(new Error(`Timeout calling CDP method ${method}`));
|
|
234
|
+
}
|
|
235
|
+
}, this.cdpCallTimeout);
|
|
236
|
+
this.pendingCalls.set(id, { resolve, reject, timeoutId });
|
|
237
|
+
this.ws.send(JSON.stringify({ id, method, params }));
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
async disconnect() {
|
|
241
|
+
// Suppress reconnection during intentional disconnect
|
|
242
|
+
const savedMaxReconnectAttempts = this.maxReconnectAttempts;
|
|
243
|
+
this.maxReconnectAttempts = 0;
|
|
244
|
+
if (this.ws) {
|
|
245
|
+
this.ws.close();
|
|
246
|
+
this.ws = null;
|
|
247
|
+
}
|
|
248
|
+
this.isConnectedFlag = false;
|
|
249
|
+
this.contexts = [];
|
|
250
|
+
this.currentWorkspacePath = null;
|
|
251
|
+
this.currentWorkspaceName = null;
|
|
252
|
+
this.clearPendingCalls(new Error('disconnect() was called'));
|
|
253
|
+
// Restore so future connect() calls can still reconnect
|
|
254
|
+
this.maxReconnectAttempts = savedMaxReconnectAttempts;
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Return the currently connected workspace name.
|
|
258
|
+
*/
|
|
259
|
+
getCurrentWorkspaceName() {
|
|
260
|
+
return this.currentWorkspaceName;
|
|
261
|
+
}
|
|
262
|
+
/**
|
|
263
|
+
* Discover and connect to the workbench page for the specified workspace.
|
|
264
|
+
* Does nothing if already connected to the correct page.
|
|
265
|
+
*
|
|
266
|
+
* @param workspacePath Full workspace path (e.g., /home/user/Code/MyProject)
|
|
267
|
+
* @returns true on successful connection
|
|
268
|
+
*/
|
|
269
|
+
async discoverAndConnectForWorkspace(workspacePath) {
|
|
270
|
+
const projectName = (0, pathUtils_1.extractProjectNameFromPath)(workspacePath);
|
|
271
|
+
this.currentWorkspacePath = workspacePath;
|
|
272
|
+
// Re-validate existing connection before skipping reconnect.
|
|
273
|
+
if (this.isConnectedFlag && this.currentWorkspaceName === projectName) {
|
|
274
|
+
const stillMatched = await this.verifyCurrentWorkspace(projectName, workspacePath);
|
|
275
|
+
if (stillMatched) {
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
278
|
+
logger_1.logger.warn(`[CdpService] Workspace mismatch detected while reusing connection (expected="${projectName}"). Reconnecting...`);
|
|
279
|
+
}
|
|
280
|
+
this.isSwitchingWorkspace = true;
|
|
281
|
+
try {
|
|
282
|
+
return await this._discoverAndConnectForWorkspaceImpl(workspacePath, projectName);
|
|
283
|
+
}
|
|
284
|
+
finally {
|
|
285
|
+
this.isSwitchingWorkspace = false;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Verify whether the currently attached page still represents the expected workspace.
|
|
290
|
+
*/
|
|
291
|
+
async verifyCurrentWorkspace(projectName, workspacePath) {
|
|
292
|
+
if (!this.ws || this.ws.readyState !== ws_1.default.OPEN || !this.isConnectedFlag) {
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
try {
|
|
296
|
+
const titleResult = await this.call('Runtime.evaluate', {
|
|
297
|
+
expression: 'document.title',
|
|
298
|
+
returnByValue: true,
|
|
299
|
+
});
|
|
300
|
+
const liveTitle = String(titleResult?.result?.value || '');
|
|
301
|
+
if (liveTitle.toLowerCase().includes(projectName.toLowerCase())) {
|
|
302
|
+
this.currentWorkspaceName = projectName;
|
|
303
|
+
return true;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
// Fall through to folder-path probe.
|
|
308
|
+
}
|
|
309
|
+
return this.probeWorkspaceFolderPath(projectName, workspacePath);
|
|
310
|
+
}
|
|
311
|
+
async _discoverAndConnectForWorkspaceImpl(workspacePath, projectName) {
|
|
312
|
+
// Scan all ports to collect workbench pages
|
|
313
|
+
let pages = [];
|
|
314
|
+
let respondingPort = null;
|
|
315
|
+
for (const port of this.ports) {
|
|
316
|
+
try {
|
|
317
|
+
const list = await this.getJson(`http://127.0.0.1:${port}/json/list`);
|
|
318
|
+
pages.push(...list);
|
|
319
|
+
// Prioritize recording ports that contain workbench pages
|
|
320
|
+
const hasWorkbench = list.some((t) => t.url?.includes('workbench'));
|
|
321
|
+
if (hasWorkbench && respondingPort === null) {
|
|
322
|
+
respondingPort = port;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
catch {
|
|
326
|
+
// No response from this port, next
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (respondingPort === null && pages.length > 0) {
|
|
330
|
+
// No workbench found but ports responded
|
|
331
|
+
respondingPort = this.ports[0]; // logging purposes
|
|
332
|
+
}
|
|
333
|
+
if (respondingPort === null) {
|
|
334
|
+
// Launch Antigravity if no port responds
|
|
335
|
+
return this.launchAndConnectWorkspace(workspacePath, projectName);
|
|
336
|
+
}
|
|
337
|
+
// Filter workbench pages only (exclude Launchpad, Manager, iframe, worker)
|
|
338
|
+
const workbenchPages = pages.filter((t) => t.type === 'page' &&
|
|
339
|
+
t.webSocketDebuggerUrl &&
|
|
340
|
+
!t.title?.includes('Launchpad') &&
|
|
341
|
+
!t.url?.includes('workbench-jetski-agent') &&
|
|
342
|
+
t.url?.includes('workbench'));
|
|
343
|
+
logger_1.logger.debug(`[CdpService] Searching for workspace "${projectName}" (port=${respondingPort})... ${workbenchPages.length} workbench pages:`);
|
|
344
|
+
for (const p of workbenchPages) {
|
|
345
|
+
logger_1.logger.debug(` - title="${p.title}" url=${p.url}`);
|
|
346
|
+
}
|
|
347
|
+
// 1. Title match (fast path)
|
|
348
|
+
const titleMatch = workbenchPages.find((t) => t.title?.includes(projectName));
|
|
349
|
+
if (titleMatch) {
|
|
350
|
+
return this.connectToPage(titleMatch, projectName);
|
|
351
|
+
}
|
|
352
|
+
// 2. Title match failed -> CDP probe (connect to each page and check document.title)
|
|
353
|
+
logger_1.logger.debug(`[CdpService] Title match failed. Searching via CDP probe...`);
|
|
354
|
+
const probeResult = await this.probeWorkbenchPages(workbenchPages, projectName, workspacePath);
|
|
355
|
+
if (probeResult) {
|
|
356
|
+
return true;
|
|
357
|
+
}
|
|
358
|
+
// 3. If not found by probe either, launch a new window
|
|
359
|
+
return this.launchAndConnectWorkspace(workspacePath, projectName);
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Connect to the specified page (skip if already connected).
|
|
363
|
+
*/
|
|
364
|
+
async connectToPage(page, projectName) {
|
|
365
|
+
// No reconnection needed if already connected to the same URL
|
|
366
|
+
if (this.isConnectedFlag && this.targetUrl === page.webSocketDebuggerUrl) {
|
|
367
|
+
this.currentWorkspaceName = projectName;
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
370
|
+
this.disconnectQuietly();
|
|
371
|
+
this.targetUrl = page.webSocketDebuggerUrl;
|
|
372
|
+
await this.connect();
|
|
373
|
+
this.currentWorkspaceName = projectName;
|
|
374
|
+
logger_1.logger.debug(`[CdpService] Connected to workspace "${projectName}"`);
|
|
375
|
+
return true;
|
|
376
|
+
}
|
|
377
|
+
/**
|
|
378
|
+
* Connect to each workbench page via CDP to get document.title and detect workspace name.
|
|
379
|
+
* Fallback when /json/list titles are stale or incomplete.
|
|
380
|
+
*
|
|
381
|
+
* If the title is "Untitled (Workspace)", verify workspace folder path via CDP.
|
|
382
|
+
*
|
|
383
|
+
* @param workbenchPages List of workbench pages
|
|
384
|
+
* @param projectName Workspace directory name
|
|
385
|
+
* @param workspacePath Full workspace path (for folder path matching)
|
|
386
|
+
*/
|
|
387
|
+
async probeWorkbenchPages(workbenchPages, projectName, workspacePath) {
|
|
388
|
+
for (const page of workbenchPages) {
|
|
389
|
+
try {
|
|
390
|
+
// Temporarily connect to retrieve document.title
|
|
391
|
+
this.disconnectQuietly();
|
|
392
|
+
this.targetUrl = page.webSocketDebuggerUrl;
|
|
393
|
+
await this.connect();
|
|
394
|
+
const result = await this.call('Runtime.evaluate', {
|
|
395
|
+
expression: 'document.title',
|
|
396
|
+
returnByValue: true,
|
|
397
|
+
});
|
|
398
|
+
const liveTitle = String(result?.result?.value || '');
|
|
399
|
+
const normalizedLiveTitle = liveTitle.toLowerCase();
|
|
400
|
+
const normalizedProject = projectName.toLowerCase();
|
|
401
|
+
if (normalizedLiveTitle.includes(normalizedProject)) {
|
|
402
|
+
this.currentWorkspaceName = projectName;
|
|
403
|
+
logger_1.logger.debug(`[CdpService] Probe success: detected "${projectName}"`);
|
|
404
|
+
return true;
|
|
405
|
+
}
|
|
406
|
+
// If title is "Untitled (Workspace)", verify by folder path
|
|
407
|
+
if (normalizedLiveTitle.includes('untitled') && workspacePath) {
|
|
408
|
+
const folderMatch = await this.probeWorkspaceFolderPath(projectName, workspacePath);
|
|
409
|
+
if (folderMatch) {
|
|
410
|
+
return true;
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
catch (e) {
|
|
415
|
+
logger_1.logger.warn(`[CdpService] Probe failed (page.id=${page.id}):`, e);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
// Probe complete, not found -> return to disconnected state
|
|
419
|
+
this.disconnectQuietly();
|
|
420
|
+
return false;
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Check if the currently connected page has the specified workspace folder open.
|
|
424
|
+
* In Antigravity (VS Code-based), info may be available from explorer views or APIs.
|
|
425
|
+
*
|
|
426
|
+
* Detects folder path via multiple approaches:
|
|
427
|
+
* 1. Check vscode.workspace.workspaceFolders via VS Code API
|
|
428
|
+
* 2. Check folder path display in DOM
|
|
429
|
+
* 3. Get workspace info from window.location.hash, etc.
|
|
430
|
+
*/
|
|
431
|
+
async probeWorkspaceFolderPath(projectName, workspacePath) {
|
|
432
|
+
try {
|
|
433
|
+
// Instead of DOM/document.title, check folder parameter in page URL or
|
|
434
|
+
// folder name in explorer view
|
|
435
|
+
const expression = `(() => {
|
|
436
|
+
// Method 1: Check window title data attribute
|
|
437
|
+
const titleEl = document.querySelector('title');
|
|
438
|
+
if (titleEl && titleEl.textContent) {
|
|
439
|
+
const t = titleEl.textContent;
|
|
440
|
+
if (t !== document.title) return { found: true, source: 'title-element', value: t };
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Method 2: Check folder name in explorer view
|
|
444
|
+
const explorerItems = document.querySelectorAll('.explorer-item-label, .monaco-icon-label .label-name');
|
|
445
|
+
const folderNames = Array.from(explorerItems).map(e => (e.textContent || '').trim()).filter(Boolean);
|
|
446
|
+
if (folderNames.length > 0) return { found: true, source: 'explorer', value: folderNames.join(',') };
|
|
447
|
+
|
|
448
|
+
// Method 3: Get path from tab titles or breadcrumbs
|
|
449
|
+
const breadcrumbs = document.querySelectorAll('.breadcrumbs-view .folder-icon, .tabs-breadcrumbs .label-name');
|
|
450
|
+
const crumbs = Array.from(breadcrumbs).map(e => (e.textContent || '').trim()).filter(Boolean);
|
|
451
|
+
if (crumbs.length > 0) return { found: true, source: 'breadcrumbs', value: crumbs.join(',') };
|
|
452
|
+
|
|
453
|
+
// Method 4: Check body data-uri attribute, etc.
|
|
454
|
+
const bodyUri = document.body?.getAttribute('data-uri') || '';
|
|
455
|
+
if (bodyUri) return { found: true, source: 'data-uri', value: bodyUri };
|
|
456
|
+
|
|
457
|
+
return { found: false };
|
|
458
|
+
})()`;
|
|
459
|
+
const res = await this.call('Runtime.evaluate', {
|
|
460
|
+
expression,
|
|
461
|
+
returnByValue: true,
|
|
462
|
+
});
|
|
463
|
+
const value = res?.result?.value;
|
|
464
|
+
if (value?.found && value?.value) {
|
|
465
|
+
const detectedValue = value.value;
|
|
466
|
+
const normalizedDetected = detectedValue.toLowerCase();
|
|
467
|
+
const normalizedProject = projectName.toLowerCase();
|
|
468
|
+
const normalizedWorkspace = workspacePath.toLowerCase();
|
|
469
|
+
if (normalizedDetected.includes(normalizedProject) ||
|
|
470
|
+
normalizedDetected.includes(normalizedWorkspace)) {
|
|
471
|
+
this.currentWorkspaceName = projectName;
|
|
472
|
+
logger_1.logger.debug(`[CdpService] Folder path match success: "${projectName}"`);
|
|
473
|
+
return true;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
// Additional fallback: check URL params (VS Code-based editors may have folder parameter)
|
|
477
|
+
const urlResult = await this.call('Runtime.evaluate', {
|
|
478
|
+
expression: 'window.location.href',
|
|
479
|
+
returnByValue: true,
|
|
480
|
+
});
|
|
481
|
+
const pageUrl = (urlResult?.result?.value || '').toLowerCase();
|
|
482
|
+
const normalizedWorkspaceUri = encodeURIComponent(workspacePath).toLowerCase();
|
|
483
|
+
if (pageUrl.includes(normalizedWorkspaceUri) || pageUrl.includes(projectName.toLowerCase())) {
|
|
484
|
+
this.currentWorkspaceName = projectName;
|
|
485
|
+
logger_1.logger.debug(`[CdpService] URL parameter match success: "${projectName}"`);
|
|
486
|
+
return true;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
catch (e) {
|
|
490
|
+
logger_1.logger.warn(`[CdpService] Folder path probe failed:`, e);
|
|
491
|
+
}
|
|
492
|
+
return false;
|
|
493
|
+
}
|
|
494
|
+
/**
|
|
495
|
+
* Launch Antigravity and wait for a new workbench page to appear, then connect.
|
|
496
|
+
*/
|
|
497
|
+
async launchAndConnectWorkspace(workspacePath, projectName) {
|
|
498
|
+
// Open as folder using Antigravity CLI (not as workspace mode).
|
|
499
|
+
// `open -a Antigravity` may open as workspace, resulting in title "Untitled (Workspace)".
|
|
500
|
+
// CLI --new-window opens as folder, immediately reflecting directory name in title.
|
|
501
|
+
const antigravityCli = (0, pathUtils_1.getAntigravityCliPath)();
|
|
502
|
+
logger_1.logger.debug(`[CdpService] Launching Antigravity: ${antigravityCli} --new-window ${workspacePath}`);
|
|
503
|
+
try {
|
|
504
|
+
await this.runCommand(antigravityCli, ['--new-window', workspacePath]);
|
|
505
|
+
}
|
|
506
|
+
catch (error) {
|
|
507
|
+
// Fall back to open -a if CLI not found (macOS only)
|
|
508
|
+
logger_1.logger.warn(`[CdpService] CLI launch failed, falling back to open -a (if macOS): ${error?.message || String(error)}`);
|
|
509
|
+
if (process.platform === 'darwin') {
|
|
510
|
+
await this.runCommand('open', ['-a', 'Antigravity', workspacePath]);
|
|
511
|
+
}
|
|
512
|
+
else {
|
|
513
|
+
throw error;
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
// Poll until a new workbench page appears (max 30 seconds)
|
|
517
|
+
const maxWaitMs = 30000;
|
|
518
|
+
const pollIntervalMs = 1000;
|
|
519
|
+
const startTime = Date.now();
|
|
520
|
+
/** Pre-launch workbench page IDs (for detecting new pages) */
|
|
521
|
+
let knownPageIds = new Set();
|
|
522
|
+
for (const port of this.ports) {
|
|
523
|
+
try {
|
|
524
|
+
const preLaunchPages = await this.getJson(`http://127.0.0.1:${port}/json/list`);
|
|
525
|
+
preLaunchPages.forEach((p) => {
|
|
526
|
+
if (p.id)
|
|
527
|
+
knownPageIds.add(p.id);
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
catch {
|
|
531
|
+
// No response from this port
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
while (Date.now() - startTime < maxWaitMs) {
|
|
535
|
+
await new Promise(r => setTimeout(r, pollIntervalMs));
|
|
536
|
+
let pages = [];
|
|
537
|
+
for (const port of this.ports) {
|
|
538
|
+
try {
|
|
539
|
+
const list = await this.getJson(`http://127.0.0.1:${port}/json/list`);
|
|
540
|
+
pages.push(...list);
|
|
541
|
+
}
|
|
542
|
+
catch {
|
|
543
|
+
// Next port
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
if (pages.length === 0)
|
|
547
|
+
continue;
|
|
548
|
+
const workbenchPages = pages.filter((t) => t.type === 'page' &&
|
|
549
|
+
t.webSocketDebuggerUrl &&
|
|
550
|
+
!t.title?.includes('Launchpad') &&
|
|
551
|
+
!t.url?.includes('workbench-jetski-agent') &&
|
|
552
|
+
t.url?.includes('workbench'));
|
|
553
|
+
// Title match
|
|
554
|
+
const titleMatch = workbenchPages.find((t) => t.title?.toLowerCase().includes(projectName.toLowerCase()));
|
|
555
|
+
if (titleMatch) {
|
|
556
|
+
return this.connectToPage(titleMatch, projectName);
|
|
557
|
+
}
|
|
558
|
+
// CDP probe (also check folder path if title is not updated)
|
|
559
|
+
const probeResult = await this.probeWorkbenchPages(workbenchPages, projectName, workspacePath);
|
|
560
|
+
if (probeResult) {
|
|
561
|
+
return true;
|
|
562
|
+
}
|
|
563
|
+
// Fallback: connect to newly appeared "Untitled (Workspace)" page after launch
|
|
564
|
+
// If title update and folder path both fail, treat new page as target
|
|
565
|
+
if (Date.now() - startTime > 10000) {
|
|
566
|
+
const newUntitledPages = workbenchPages.filter((t) => !knownPageIds.has(t.id) &&
|
|
567
|
+
(t.title?.includes('Untitled') || t.title === ''));
|
|
568
|
+
if (newUntitledPages.length === 1) {
|
|
569
|
+
logger_1.logger.debug(`[CdpService] New Untitled page detected. Connecting as "${projectName}" (page.id=${newUntitledPages[0].id})`);
|
|
570
|
+
return this.connectToPage(newUntitledPages[0], projectName);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
throw new Error(`Workbench page for workspace "${projectName}" not found within ${maxWaitMs / 1000} seconds`);
|
|
575
|
+
}
|
|
576
|
+
async runCommand(command, args) {
|
|
577
|
+
await new Promise((resolve, reject) => {
|
|
578
|
+
const child = (0, child_process_1.spawn)(command, args, { stdio: 'ignore' });
|
|
579
|
+
child.once('error', (error) => {
|
|
580
|
+
reject(error);
|
|
581
|
+
});
|
|
582
|
+
child.once('close', (code) => {
|
|
583
|
+
if (code === 0) {
|
|
584
|
+
resolve();
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
reject(new Error(`${command} exited with code ${code ?? 'unknown'}`));
|
|
588
|
+
});
|
|
589
|
+
});
|
|
590
|
+
}
|
|
591
|
+
/**
|
|
592
|
+
* Quietly disconnect the existing connection (no reconnect attempts).
|
|
593
|
+
* Used during workspace switching.
|
|
594
|
+
*
|
|
595
|
+
* Important: ws.close() fires close event asynchronously, so all listeners
|
|
596
|
+
* must be removed first to prevent targetUrl reset and tryReconnect()
|
|
597
|
+
* from reconnecting to a different workbench.
|
|
598
|
+
*/
|
|
599
|
+
disconnectQuietly() {
|
|
600
|
+
if (this.ws) {
|
|
601
|
+
// Remove all listeners including close event handlers to prevent side effects
|
|
602
|
+
this.ws.removeAllListeners();
|
|
603
|
+
this.ws.close();
|
|
604
|
+
this.ws = null;
|
|
605
|
+
this.isConnectedFlag = false;
|
|
606
|
+
this.contexts = [];
|
|
607
|
+
this.clearPendingCalls(new Error('Disconnected for workspace switch'));
|
|
608
|
+
this.targetUrl = null;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Reject all unresolved pending calls to prevent memory leaks.
|
|
613
|
+
* (Step 12: Error handling)
|
|
614
|
+
* @param error Error to pass to reject
|
|
615
|
+
*/
|
|
616
|
+
clearPendingCalls(error) {
|
|
617
|
+
for (const [, { reject, timeoutId }] of this.pendingCalls.entries()) {
|
|
618
|
+
clearTimeout(timeoutId);
|
|
619
|
+
reject(error);
|
|
620
|
+
}
|
|
621
|
+
this.pendingCalls.clear();
|
|
622
|
+
}
|
|
623
|
+
/**
|
|
624
|
+
* Attempt auto-reconnect after CDP disconnection.
|
|
625
|
+
* Fires 'reconnectFailed' event after maxReconnectAttempts failures.
|
|
626
|
+
* (Step 12: Error handling and timeout management)
|
|
627
|
+
*/
|
|
628
|
+
async tryReconnect() {
|
|
629
|
+
if (this.isReconnecting)
|
|
630
|
+
return;
|
|
631
|
+
this.isReconnecting = true;
|
|
632
|
+
this.reconnectAttemptCount = 0;
|
|
633
|
+
while (this.reconnectAttemptCount < this.maxReconnectAttempts) {
|
|
634
|
+
this.reconnectAttemptCount++;
|
|
635
|
+
logger_1.logger.error(`[CdpService] Reconnect attempt ${this.reconnectAttemptCount}/${this.maxReconnectAttempts}...`);
|
|
636
|
+
// Add delay between attempts
|
|
637
|
+
await new Promise(r => setTimeout(r, this.reconnectDelayMs));
|
|
638
|
+
try {
|
|
639
|
+
this.contexts = [];
|
|
640
|
+
if (this.currentWorkspacePath) {
|
|
641
|
+
await this.discoverAndConnectForWorkspace(this.currentWorkspacePath);
|
|
642
|
+
}
|
|
643
|
+
else {
|
|
644
|
+
await this.discoverTarget();
|
|
645
|
+
await this.connect();
|
|
646
|
+
}
|
|
647
|
+
logger_1.logger.error('[CdpService] Reconnect succeeded.');
|
|
648
|
+
this.reconnectAttemptCount = 0;
|
|
649
|
+
this.isReconnecting = false;
|
|
650
|
+
this.emit('reconnected');
|
|
651
|
+
return;
|
|
652
|
+
}
|
|
653
|
+
catch (err) {
|
|
654
|
+
logger_1.logger.error('[CdpService] Reconnect failed:', err);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
this.isReconnecting = false;
|
|
658
|
+
const finalError = new Error(`CDP reconnection failed ${this.maxReconnectAttempts} times. Manual restart required.`);
|
|
659
|
+
logger_1.logger.error('[CdpService]', finalError.message);
|
|
660
|
+
this.emit('reconnectFailed', finalError);
|
|
661
|
+
}
|
|
662
|
+
isConnected() {
|
|
663
|
+
return this.isConnectedFlag;
|
|
664
|
+
}
|
|
665
|
+
getContexts() {
|
|
666
|
+
return [...this.contexts];
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Wait by polling until cascade-panel context becomes available.
|
|
670
|
+
* Right after Antigravity launch, contexts are created asynchronously even after Runtime.enable,
|
|
671
|
+
* so use this method to confirm readiness before DOM operations.
|
|
672
|
+
*
|
|
673
|
+
* @param timeoutMs Maximum wait time (ms). Default: 10000
|
|
674
|
+
* @param pollIntervalMs Polling interval (ms). Default: 500
|
|
675
|
+
* @returns true if cascade-panel context was found
|
|
676
|
+
*/
|
|
677
|
+
async waitForCascadePanelReady(timeoutMs = 10000, pollIntervalMs = 500) {
|
|
678
|
+
const start = Date.now();
|
|
679
|
+
while (Date.now() - start < timeoutMs) {
|
|
680
|
+
const cascadeCtx = this.contexts.find(c => c.url && c.url.includes(SELECTORS.CONTEXT_URL_KEYWORD));
|
|
681
|
+
if (cascadeCtx) {
|
|
682
|
+
return true;
|
|
683
|
+
}
|
|
684
|
+
await new Promise(r => setTimeout(r, pollIntervalMs));
|
|
685
|
+
}
|
|
686
|
+
return false;
|
|
687
|
+
}
|
|
688
|
+
getPrimaryContextId() {
|
|
689
|
+
// Find cascade-panel context
|
|
690
|
+
const context = this.contexts.find(c => c.url && c.url.includes('cascade-panel'));
|
|
691
|
+
if (context)
|
|
692
|
+
return context.id;
|
|
693
|
+
// Fallback to Extension context or first one
|
|
694
|
+
const extContext = this.contexts.find(c => c.name && c.name.includes('Extension'));
|
|
695
|
+
if (extContext)
|
|
696
|
+
return extContext.id;
|
|
697
|
+
return this.contexts.length > 0 ? this.contexts[0].id : null;
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Focus the chat input field.
|
|
701
|
+
*/
|
|
702
|
+
async focusChatInput() {
|
|
703
|
+
const focusScript = `(() => {
|
|
704
|
+
const editors = Array.from(document.querySelectorAll('${SELECTORS.CHAT_INPUT}'));
|
|
705
|
+
const visible = editors.filter(el => el.offsetParent !== null);
|
|
706
|
+
const editor = visible[visible.length - 1];
|
|
707
|
+
if (!editor) return { ok: false, error: 'No editor found' };
|
|
708
|
+
editor.focus();
|
|
709
|
+
return { ok: true };
|
|
710
|
+
})()`;
|
|
711
|
+
for (const ctx of this.contexts) {
|
|
712
|
+
try {
|
|
713
|
+
const res = await this.call('Runtime.evaluate', {
|
|
714
|
+
expression: focusScript,
|
|
715
|
+
returnByValue: true,
|
|
716
|
+
contextId: ctx.id,
|
|
717
|
+
});
|
|
718
|
+
if (res?.result?.value?.ok) {
|
|
719
|
+
return { ok: true, contextId: ctx.id };
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
catch {
|
|
723
|
+
// Try next context
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
return { ok: false, error: 'Chat input field not found' };
|
|
727
|
+
}
|
|
728
|
+
/**
|
|
729
|
+
* Select all text in the focused input and delete it to ensure a clean state.
|
|
730
|
+
* Uses Meta+A (select all) then Backspace (delete) via CDP key events.
|
|
731
|
+
*/
|
|
732
|
+
async clearInputField() {
|
|
733
|
+
// Meta+A to select all content
|
|
734
|
+
await this.call('Input.dispatchKeyEvent', {
|
|
735
|
+
type: 'keyDown',
|
|
736
|
+
key: 'a',
|
|
737
|
+
code: 'KeyA',
|
|
738
|
+
modifiers: process.platform === 'darwin' ? 4 : 2, // Meta on macOS, Ctrl on Windows/Linux
|
|
739
|
+
windowsVirtualKeyCode: 65,
|
|
740
|
+
nativeVirtualKeyCode: 65,
|
|
741
|
+
});
|
|
742
|
+
await this.call('Input.dispatchKeyEvent', {
|
|
743
|
+
type: 'keyUp',
|
|
744
|
+
key: 'a',
|
|
745
|
+
code: 'KeyA',
|
|
746
|
+
modifiers: process.platform === 'darwin' ? 4 : 2,
|
|
747
|
+
windowsVirtualKeyCode: 65,
|
|
748
|
+
nativeVirtualKeyCode: 65,
|
|
749
|
+
});
|
|
750
|
+
// Backspace to delete selected content
|
|
751
|
+
await this.call('Input.dispatchKeyEvent', {
|
|
752
|
+
type: 'keyDown',
|
|
753
|
+
key: 'Backspace',
|
|
754
|
+
code: 'Backspace',
|
|
755
|
+
windowsVirtualKeyCode: 8,
|
|
756
|
+
nativeVirtualKeyCode: 8,
|
|
757
|
+
});
|
|
758
|
+
await this.call('Input.dispatchKeyEvent', {
|
|
759
|
+
type: 'keyUp',
|
|
760
|
+
key: 'Backspace',
|
|
761
|
+
code: 'Backspace',
|
|
762
|
+
windowsVirtualKeyCode: 8,
|
|
763
|
+
nativeVirtualKeyCode: 8,
|
|
764
|
+
});
|
|
765
|
+
// Wait for DOM to settle
|
|
766
|
+
await new Promise(r => setTimeout(r, 50));
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Send Enter key to submit the message.
|
|
770
|
+
*/
|
|
771
|
+
async pressEnterToSend() {
|
|
772
|
+
await this.call('Input.dispatchKeyEvent', {
|
|
773
|
+
type: 'keyDown',
|
|
774
|
+
key: 'Enter',
|
|
775
|
+
code: 'Enter',
|
|
776
|
+
windowsVirtualKeyCode: 13,
|
|
777
|
+
nativeVirtualKeyCode: 13,
|
|
778
|
+
});
|
|
779
|
+
await this.call('Input.dispatchKeyEvent', {
|
|
780
|
+
type: 'keyUp',
|
|
781
|
+
key: 'Enter',
|
|
782
|
+
code: 'Enter',
|
|
783
|
+
windowsVirtualKeyCode: 13,
|
|
784
|
+
nativeVirtualKeyCode: 13,
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
/**
|
|
788
|
+
* Detect file input in the UI and attach the specified files.
|
|
789
|
+
*/
|
|
790
|
+
async attachImageFiles(filePaths, contextId) {
|
|
791
|
+
if (filePaths.length === 0)
|
|
792
|
+
return { ok: true };
|
|
793
|
+
await this.call('DOM.enable', {});
|
|
794
|
+
const locateInputScript = `(async () => {
|
|
795
|
+
const wait = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
796
|
+
const visible = (el) => {
|
|
797
|
+
if (!el) return false;
|
|
798
|
+
if (el.offsetParent !== null) return true;
|
|
799
|
+
const style = window.getComputedStyle(el);
|
|
800
|
+
if (!style) return false;
|
|
801
|
+
if (style.display === 'none' || style.visibility === 'hidden' || style.opacity === '0') return false;
|
|
802
|
+
const rect = typeof el.getBoundingClientRect === 'function' ? el.getBoundingClientRect() : null;
|
|
803
|
+
return !!rect && rect.width > 0 && rect.height > 0;
|
|
804
|
+
};
|
|
805
|
+
const normalize = (v) => (v || '').toLowerCase();
|
|
806
|
+
const hasImageAccept = (input) => {
|
|
807
|
+
const accept = normalize(input.getAttribute('accept'));
|
|
808
|
+
return !accept || accept.includes('image') || accept.includes('*/*');
|
|
809
|
+
};
|
|
810
|
+
const findInput = () => {
|
|
811
|
+
const inputs = Array.from(document.querySelectorAll('input[type="file"]'));
|
|
812
|
+
const visibleInput = inputs.find(i => visible(i) && hasImageAccept(i));
|
|
813
|
+
if (visibleInput) return visibleInput;
|
|
814
|
+
return inputs.find(hasImageAccept) || null;
|
|
815
|
+
};
|
|
816
|
+
|
|
817
|
+
let input = findInput();
|
|
818
|
+
if (!input) {
|
|
819
|
+
const triggerKeywords = ['attach', 'upload', 'image', 'file', 'paperclip', 'plus'];
|
|
820
|
+
const triggers = Array.from(document.querySelectorAll('button, [role="button"]'))
|
|
821
|
+
.filter(visible)
|
|
822
|
+
.filter((el) => {
|
|
823
|
+
const text = normalize(el.textContent);
|
|
824
|
+
const aria = normalize(el.getAttribute('aria-label'));
|
|
825
|
+
const title = normalize(el.getAttribute('title'));
|
|
826
|
+
const cls = normalize(el.getAttribute('class'));
|
|
827
|
+
const all = [text, aria, title, cls].join(' ');
|
|
828
|
+
return triggerKeywords.some(k => all.includes(k));
|
|
829
|
+
})
|
|
830
|
+
.slice(-8);
|
|
831
|
+
|
|
832
|
+
for (const trigger of triggers) {
|
|
833
|
+
if (typeof trigger.click === 'function') {
|
|
834
|
+
trigger.click();
|
|
835
|
+
await wait(150);
|
|
836
|
+
input = findInput();
|
|
837
|
+
if (input) break;
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
if (!input) {
|
|
843
|
+
return { ok: false, error: 'Image upload input not found' };
|
|
844
|
+
}
|
|
845
|
+
|
|
846
|
+
const token = 'agclaw-upload-' + Math.random().toString(36).slice(2, 10);
|
|
847
|
+
input.setAttribute('data-agclaw-upload-token', token);
|
|
848
|
+
return { ok: true, token };
|
|
849
|
+
})()`;
|
|
850
|
+
const callParams = {
|
|
851
|
+
expression: locateInputScript,
|
|
852
|
+
returnByValue: true,
|
|
853
|
+
awaitPromise: true,
|
|
854
|
+
};
|
|
855
|
+
if (contextId !== undefined) {
|
|
856
|
+
callParams.contextId = contextId;
|
|
857
|
+
}
|
|
858
|
+
const locateResult = await this.call('Runtime.evaluate', callParams);
|
|
859
|
+
const locateValue = locateResult?.result?.value;
|
|
860
|
+
if (!locateValue?.ok || !locateValue?.token) {
|
|
861
|
+
return { ok: false, error: locateValue?.error || 'Failed to locate file input' };
|
|
862
|
+
}
|
|
863
|
+
const token = String(locateValue.token);
|
|
864
|
+
const documentResult = await this.call('DOM.getDocument', { depth: 1, pierce: true });
|
|
865
|
+
const rootNodeId = documentResult?.root?.nodeId;
|
|
866
|
+
if (!rootNodeId) {
|
|
867
|
+
return { ok: false, error: 'Failed to get DOM root' };
|
|
868
|
+
}
|
|
869
|
+
const selector = `input[data-agclaw-upload-token="${token}"]`;
|
|
870
|
+
const nodeResult = await this.call('DOM.querySelector', {
|
|
871
|
+
nodeId: rootNodeId,
|
|
872
|
+
selector,
|
|
873
|
+
});
|
|
874
|
+
const nodeId = nodeResult?.nodeId;
|
|
875
|
+
if (!nodeId) {
|
|
876
|
+
return { ok: false, error: 'Failed to get upload input node' };
|
|
877
|
+
}
|
|
878
|
+
await this.call('DOM.setFileInputFiles', {
|
|
879
|
+
nodeId,
|
|
880
|
+
files: filePaths,
|
|
881
|
+
});
|
|
882
|
+
const notifyScript = `(() => {
|
|
883
|
+
const input = document.querySelector('${selector}');
|
|
884
|
+
if (!input) return { ok: false, error: 'Image input not found' };
|
|
885
|
+
input.removeAttribute('data-agclaw-upload-token');
|
|
886
|
+
return { ok: true };
|
|
887
|
+
})()`;
|
|
888
|
+
await this.call('Runtime.evaluate', {
|
|
889
|
+
expression: notifyScript,
|
|
890
|
+
returnByValue: true,
|
|
891
|
+
awaitPromise: true,
|
|
892
|
+
...(contextId !== undefined ? { contextId } : {}),
|
|
893
|
+
});
|
|
894
|
+
await new Promise(r => setTimeout(r, 250));
|
|
895
|
+
return { ok: true };
|
|
896
|
+
}
|
|
897
|
+
/**
|
|
898
|
+
* Inject and send the specified text into Antigravity's chat input field.
|
|
899
|
+
*
|
|
900
|
+
* Strategy:
|
|
901
|
+
* 1. Focus editor via Runtime.evaluate
|
|
902
|
+
* 2. Input text via CDP Input.insertText
|
|
903
|
+
* 3. Send via CDP Input.dispatchKeyEvent(Enter)
|
|
904
|
+
*
|
|
905
|
+
* Using CDP Input API instead of DOM manipulation ensures reliable
|
|
906
|
+
* delivery to Cascade panel's React/framework event handlers.
|
|
907
|
+
*/
|
|
908
|
+
async injectMessage(text) {
|
|
909
|
+
if (!this.isConnectedFlag || !this.ws) {
|
|
910
|
+
throw new Error('Not connected to CDP. Call connect() first.');
|
|
911
|
+
}
|
|
912
|
+
const focusResult = await this.focusChatInput();
|
|
913
|
+
if (!focusResult.ok) {
|
|
914
|
+
return { ok: false, error: focusResult.error || 'Chat input field not found' };
|
|
915
|
+
}
|
|
916
|
+
// Clear any existing text in the input field before injecting
|
|
917
|
+
await this.clearInputField();
|
|
918
|
+
// 1. Input text via CDP Input.insertText
|
|
919
|
+
await this.call('Input.insertText', { text });
|
|
920
|
+
await new Promise(r => setTimeout(r, 200));
|
|
921
|
+
// 2. Send via Enter key
|
|
922
|
+
await this.pressEnterToSend();
|
|
923
|
+
return { ok: true, method: 'enter', contextId: focusResult.contextId };
|
|
924
|
+
}
|
|
925
|
+
/**
|
|
926
|
+
* Attach image files to the UI and send the specified text.
|
|
927
|
+
*/
|
|
928
|
+
async injectMessageWithImageFiles(text, imageFilePaths) {
|
|
929
|
+
if (!this.isConnectedFlag || !this.ws) {
|
|
930
|
+
throw new Error('Not connected to CDP. Call connect() first.');
|
|
931
|
+
}
|
|
932
|
+
const focusResult = await this.focusChatInput();
|
|
933
|
+
if (!focusResult.ok) {
|
|
934
|
+
return { ok: false, error: focusResult.error || 'Chat input field not found' };
|
|
935
|
+
}
|
|
936
|
+
// Clear any existing text in the input field before injecting
|
|
937
|
+
await this.clearInputField();
|
|
938
|
+
const attachResult = await this.attachImageFiles(imageFilePaths, focusResult.contextId);
|
|
939
|
+
if (!attachResult.ok) {
|
|
940
|
+
return { ok: false, error: attachResult.error || 'Failed to attach images' };
|
|
941
|
+
}
|
|
942
|
+
await this.call('Input.insertText', { text });
|
|
943
|
+
await new Promise(r => setTimeout(r, 200));
|
|
944
|
+
await this.pressEnterToSend();
|
|
945
|
+
return { ok: true, method: 'enter', contextId: focusResult.contextId };
|
|
946
|
+
}
|
|
947
|
+
/**
|
|
948
|
+
* Extract images from the latest AI response.
|
|
949
|
+
*/
|
|
950
|
+
async extractLatestResponseImages(maxImages = 4) {
|
|
951
|
+
if (!this.isConnectedFlag || !this.ws) {
|
|
952
|
+
return [];
|
|
953
|
+
}
|
|
954
|
+
const safeMaxImages = Math.max(1, Math.min(8, Math.floor(maxImages)));
|
|
955
|
+
const expression = `(async () => {
|
|
956
|
+
const maxImages = ${safeMaxImages};
|
|
957
|
+
const panel = document.querySelector('.antigravity-agent-side-panel');
|
|
958
|
+
const scope = panel || document;
|
|
959
|
+
|
|
960
|
+
const candidateSelectors = [
|
|
961
|
+
'.rendered-markdown',
|
|
962
|
+
'.leading-relaxed.select-text',
|
|
963
|
+
'.flex.flex-col.gap-y-3',
|
|
964
|
+
'[data-message-author-role="assistant"]',
|
|
965
|
+
'[data-message-role="assistant"]',
|
|
966
|
+
'[class*="assistant-message"]',
|
|
967
|
+
'[class*="message-content"]',
|
|
968
|
+
'[class*="markdown-body"]',
|
|
969
|
+
'.prose',
|
|
970
|
+
];
|
|
971
|
+
|
|
972
|
+
const responseNodes = [];
|
|
973
|
+
const seenNodes = new Set();
|
|
974
|
+
for (const selector of candidateSelectors) {
|
|
975
|
+
const nodes = scope.querySelectorAll(selector);
|
|
976
|
+
for (const node of nodes) {
|
|
977
|
+
if (!node || seenNodes.has(node)) continue;
|
|
978
|
+
seenNodes.add(node);
|
|
979
|
+
responseNodes.push(node);
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
|
|
983
|
+
// Skip image extraction when no response nodes found (prevent UI icon false positives)
|
|
984
|
+
if (responseNodes.length === 0) return [];
|
|
985
|
+
|
|
986
|
+
const normalize = (value) => (value || '').toLowerCase();
|
|
987
|
+
const isLikelyUiImage = (img) => {
|
|
988
|
+
if (!img) return true;
|
|
989
|
+
const src = normalize(img.currentSrc || img.src || img.getAttribute('src') || '');
|
|
990
|
+
const alt = normalize(img.getAttribute('alt') || '');
|
|
991
|
+
const title = normalize(img.getAttribute('title') || '');
|
|
992
|
+
const cls = normalize(img.getAttribute('class') || '');
|
|
993
|
+
const blob = [src, alt, title, cls].join(' ');
|
|
994
|
+
|
|
995
|
+
if (blob.includes('icon') || blob.includes('avatar') || blob.includes('emoji')) return true;
|
|
996
|
+
if (blob.includes('thumb') || blob.includes('good') || blob.includes('bad')) return true;
|
|
997
|
+
if (src.startsWith('data:image/svg+xml')) return true;
|
|
998
|
+
if (img.closest('button, [role="button"], nav, header, footer, [class*="toolbar"], [class*="reaction"]')) return true;
|
|
999
|
+
|
|
1000
|
+
const rect = typeof img.getBoundingClientRect === 'function' ? img.getBoundingClientRect() : null;
|
|
1001
|
+
const w = Number(img.naturalWidth || img.width || rect?.width || 0);
|
|
1002
|
+
const h = Number(img.naturalHeight || img.height || rect?.height || 0);
|
|
1003
|
+
if (w < 96 || h < 96) return true;
|
|
1004
|
+
if ((w * h) < 12000) return true;
|
|
1005
|
+
|
|
1006
|
+
return false;
|
|
1007
|
+
};
|
|
1008
|
+
|
|
1009
|
+
const dedup = new Set();
|
|
1010
|
+
const images = [];
|
|
1011
|
+
for (let i = responseNodes.length - 1; i >= 0; i--) {
|
|
1012
|
+
const node = responseNodes[i];
|
|
1013
|
+
const nodeImages = Array.from(node.querySelectorAll('img'));
|
|
1014
|
+
for (const img of nodeImages) {
|
|
1015
|
+
if (isLikelyUiImage(img)) continue;
|
|
1016
|
+
const key = (img.currentSrc || img.src || img.getAttribute('src') || '') + '|' + (img.getAttribute('alt') || '');
|
|
1017
|
+
if (!key || dedup.has(key)) continue;
|
|
1018
|
+
dedup.add(key);
|
|
1019
|
+
images.push(img);
|
|
1020
|
+
}
|
|
1021
|
+
if (images.length >= maxImages) break;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
if (images.length === 0) return [];
|
|
1025
|
+
const picked = images.slice(-maxImages);
|
|
1026
|
+
|
|
1027
|
+
const normalizeFileName = (value, idx) => {
|
|
1028
|
+
const raw = (value || '').trim();
|
|
1029
|
+
const safe = raw.replace(/[^a-zA-Z0-9._-]/g, '-').replace(/-+/g, '-').replace(/^-+|-+$/g, '');
|
|
1030
|
+
return safe || ('generated-image-' + (idx + 1));
|
|
1031
|
+
};
|
|
1032
|
+
|
|
1033
|
+
const guessMimeType = (src) => {
|
|
1034
|
+
if (!src) return 'image/png';
|
|
1035
|
+
if (src.startsWith('data:')) {
|
|
1036
|
+
const match = src.match(/^data:([^;]+);/);
|
|
1037
|
+
return (match && match[1]) || 'image/png';
|
|
1038
|
+
}
|
|
1039
|
+
const lower = src.toLowerCase();
|
|
1040
|
+
if (lower.includes('.jpg') || lower.includes('.jpeg')) return 'image/jpeg';
|
|
1041
|
+
if (lower.includes('.webp')) return 'image/webp';
|
|
1042
|
+
if (lower.includes('.gif')) return 'image/gif';
|
|
1043
|
+
return 'image/png';
|
|
1044
|
+
};
|
|
1045
|
+
|
|
1046
|
+
const blobToBase64 = (blob) => new Promise((resolve, reject) => {
|
|
1047
|
+
const reader = new FileReader();
|
|
1048
|
+
reader.onload = () => {
|
|
1049
|
+
const value = typeof reader.result === 'string' ? reader.result : '';
|
|
1050
|
+
const commaIndex = value.indexOf(',');
|
|
1051
|
+
resolve(commaIndex >= 0 ? value.slice(commaIndex + 1) : value);
|
|
1052
|
+
};
|
|
1053
|
+
reader.onerror = () => reject(reader.error || new Error('read failed'));
|
|
1054
|
+
reader.readAsDataURL(blob);
|
|
1055
|
+
});
|
|
1056
|
+
|
|
1057
|
+
const result = [];
|
|
1058
|
+
for (let i = 0; i < picked.length; i++) {
|
|
1059
|
+
const img = picked[i];
|
|
1060
|
+
const src = img.currentSrc || img.src || img.getAttribute('src') || '';
|
|
1061
|
+
if (!src) continue;
|
|
1062
|
+
|
|
1063
|
+
const baseName = normalizeFileName(img.getAttribute('alt') || img.getAttribute('title'), i);
|
|
1064
|
+
const mimeType = guessMimeType(src);
|
|
1065
|
+
const extensionMap = { 'image/png': 'png', 'image/jpeg': 'jpg', 'image/webp': 'webp', 'image/gif': 'gif' };
|
|
1066
|
+
const ext = extensionMap[mimeType] || 'png';
|
|
1067
|
+
const name = baseName.includes('.') ? baseName : (baseName + '.' + ext);
|
|
1068
|
+
|
|
1069
|
+
if (src.startsWith('data:')) {
|
|
1070
|
+
const commaIndex = src.indexOf(',');
|
|
1071
|
+
if (commaIndex > 0) {
|
|
1072
|
+
result.push({
|
|
1073
|
+
name,
|
|
1074
|
+
mimeType,
|
|
1075
|
+
base64Data: src.slice(commaIndex + 1),
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
continue;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
try {
|
|
1082
|
+
const response = await fetch(src);
|
|
1083
|
+
if (!response.ok) throw new Error('fetch failed');
|
|
1084
|
+
const blob = await response.blob();
|
|
1085
|
+
const base64Data = await blobToBase64(blob);
|
|
1086
|
+
result.push({
|
|
1087
|
+
name,
|
|
1088
|
+
mimeType: blob.type || mimeType,
|
|
1089
|
+
base64Data,
|
|
1090
|
+
});
|
|
1091
|
+
} catch {
|
|
1092
|
+
result.push({
|
|
1093
|
+
name,
|
|
1094
|
+
mimeType,
|
|
1095
|
+
url: src,
|
|
1096
|
+
});
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
return result;
|
|
1101
|
+
})()`;
|
|
1102
|
+
try {
|
|
1103
|
+
const contextId = this.getPrimaryContextId();
|
|
1104
|
+
const callParams = {
|
|
1105
|
+
expression,
|
|
1106
|
+
returnByValue: true,
|
|
1107
|
+
awaitPromise: true,
|
|
1108
|
+
};
|
|
1109
|
+
if (contextId !== null) {
|
|
1110
|
+
callParams.contextId = contextId;
|
|
1111
|
+
}
|
|
1112
|
+
const response = await this.call('Runtime.evaluate', callParams);
|
|
1113
|
+
const value = response?.result?.value;
|
|
1114
|
+
if (!Array.isArray(value))
|
|
1115
|
+
return [];
|
|
1116
|
+
return value
|
|
1117
|
+
.filter((item) => item && typeof item === 'object' && typeof item.name === 'string')
|
|
1118
|
+
.map((item) => ({
|
|
1119
|
+
name: item.name,
|
|
1120
|
+
mimeType: typeof item.mimeType === 'string' ? item.mimeType : 'image/png',
|
|
1121
|
+
base64Data: typeof item.base64Data === 'string' ? item.base64Data : undefined,
|
|
1122
|
+
url: typeof item.url === 'string' ? item.url : undefined,
|
|
1123
|
+
}));
|
|
1124
|
+
}
|
|
1125
|
+
catch {
|
|
1126
|
+
return [];
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
/**
|
|
1130
|
+
* Get the currently selected mode from the Antigravity UI.
|
|
1131
|
+
* Reads the mode toggle button text and maps it back to internal mode name.
|
|
1132
|
+
*
|
|
1133
|
+
* @returns Internal mode name (e.g., 'fast', 'plan') or null if not found
|
|
1134
|
+
*/
|
|
1135
|
+
async getCurrentMode() {
|
|
1136
|
+
if (!this.isConnectedFlag || !this.ws) {
|
|
1137
|
+
return null;
|
|
1138
|
+
}
|
|
1139
|
+
const expression = '(() => {'
|
|
1140
|
+
+ ' const uiNameMap = { fast: "Fast", plan: "Planning" };'
|
|
1141
|
+
+ ' const knownModes = Object.values(uiNameMap).map(n => n.toLowerCase());'
|
|
1142
|
+
+ ' const reverseMap = {};'
|
|
1143
|
+
+ ' Object.entries(uiNameMap).forEach(([k, v]) => { reverseMap[v.toLowerCase()] = k; });'
|
|
1144
|
+
+ ' const allBtns = Array.from(document.querySelectorAll("button"));'
|
|
1145
|
+
+ ' const visibleBtns = allBtns.filter(b => b.offsetParent !== null);'
|
|
1146
|
+
+ ' const modeToggleBtn = visibleBtns.find(b => {'
|
|
1147
|
+
+ ' const text = (b.textContent || "").trim().toLowerCase();'
|
|
1148
|
+
+ ' const hasChevron = b.querySelector("svg[class*=\\"chevron\\"]");'
|
|
1149
|
+
+ ' return knownModes.some(m => text === m) && hasChevron;'
|
|
1150
|
+
+ ' });'
|
|
1151
|
+
+ ' if (!modeToggleBtn) return null;'
|
|
1152
|
+
+ ' const currentModeText = (modeToggleBtn.textContent || "").trim().toLowerCase();'
|
|
1153
|
+
+ ' return reverseMap[currentModeText] || null;'
|
|
1154
|
+
+ '})()';
|
|
1155
|
+
try {
|
|
1156
|
+
const contextId = this.getPrimaryContextId();
|
|
1157
|
+
const callParams = {
|
|
1158
|
+
expression,
|
|
1159
|
+
returnByValue: true,
|
|
1160
|
+
awaitPromise: false,
|
|
1161
|
+
};
|
|
1162
|
+
if (contextId !== null)
|
|
1163
|
+
callParams.contextId = contextId;
|
|
1164
|
+
const res = await this.call('Runtime.evaluate', callParams);
|
|
1165
|
+
return res?.result?.value || null;
|
|
1166
|
+
}
|
|
1167
|
+
catch {
|
|
1168
|
+
return null;
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
/**
|
|
1172
|
+
* Operate Antigravity UI mode dropdown to switch to the specified mode.
|
|
1173
|
+
* Two-step approach:
|
|
1174
|
+
* Step 1: Click mode toggle button ("Fast"/"Plan" + chevron icon) to open dropdown
|
|
1175
|
+
* Step 2: Select the target mode option from dropdown
|
|
1176
|
+
*
|
|
1177
|
+
* @param modeName Mode name to set (e.g., 'fast', 'plan')
|
|
1178
|
+
*/
|
|
1179
|
+
async setUiMode(modeName) {
|
|
1180
|
+
if (!this.isConnectedFlag || !this.ws) {
|
|
1181
|
+
throw new Error('Not connected to CDP. Call connect() first.');
|
|
1182
|
+
}
|
|
1183
|
+
const safeMode = JSON.stringify(modeName);
|
|
1184
|
+
// Internal mode name -> Antigravity UI display name mapping
|
|
1185
|
+
const uiNameMap = JSON.stringify({ fast: 'Fast', plan: 'Planning' });
|
|
1186
|
+
// Build DOM manipulation script avoiding backticks in template literals
|
|
1187
|
+
const expression = '(async () => {'
|
|
1188
|
+
+ ' const targetMode = ' + safeMode + ';'
|
|
1189
|
+
+ ' const targetModeLower = targetMode.toLowerCase();'
|
|
1190
|
+
+ ' const uiNameMap = ' + uiNameMap + ';'
|
|
1191
|
+
+ ' const targetUiName = uiNameMap[targetModeLower] || targetMode;'
|
|
1192
|
+
+ ' const targetUiNameLower = targetUiName.toLowerCase();'
|
|
1193
|
+
+ ' const allBtns = Array.from(document.querySelectorAll("button"));'
|
|
1194
|
+
+ ' const visibleBtns = allBtns.filter(b => b.offsetParent !== null);'
|
|
1195
|
+
// Step 1: Search for mode toggle button ("Fast"/"Planning" + chevron icon)
|
|
1196
|
+
+ ' const knownModes = Object.values(uiNameMap).map(n => n.toLowerCase());'
|
|
1197
|
+
+ ' const modeToggleBtn = visibleBtns.find(b => {'
|
|
1198
|
+
+ ' const text = (b.textContent || "").trim().toLowerCase();'
|
|
1199
|
+
+ ' const hasChevron = b.querySelector("svg[class*=\\"chevron\\"]");'
|
|
1200
|
+
+ ' return knownModes.some(m => text === m) && hasChevron;'
|
|
1201
|
+
+ ' });'
|
|
1202
|
+
+ ' if (!modeToggleBtn) {'
|
|
1203
|
+
+ ' return { ok: false, error: "Mode toggle button not found" };'
|
|
1204
|
+
+ ' }'
|
|
1205
|
+
+ ' const currentModeText = (modeToggleBtn.textContent || "").trim().toLowerCase();'
|
|
1206
|
+
// Do nothing if already on the target mode
|
|
1207
|
+
+ ' if (currentModeText === targetUiNameLower) {'
|
|
1208
|
+
+ ' return { ok: true, mode: targetUiName, alreadySelected: true };'
|
|
1209
|
+
+ ' }'
|
|
1210
|
+
// Open dropdown
|
|
1211
|
+
+ ' modeToggleBtn.click();'
|
|
1212
|
+
+ ' await new Promise(r => setTimeout(r, 500));'
|
|
1213
|
+
// Step 2: Search for option by .font-medium text inside role="dialog"
|
|
1214
|
+
+ ' const dialogs = Array.from(document.querySelectorAll("[role=\\"dialog\\"]"));'
|
|
1215
|
+
+ ' const visibleDialog = dialogs.find(d => {'
|
|
1216
|
+
+ ' const style = window.getComputedStyle(d);'
|
|
1217
|
+
+ ' return style.visibility !== "hidden" && style.display !== "none";'
|
|
1218
|
+
+ ' });'
|
|
1219
|
+
+ ' let modeOption = null;'
|
|
1220
|
+
+ ' if (visibleDialog) {'
|
|
1221
|
+
+ ' const fontMediumEls = Array.from(visibleDialog.querySelectorAll(".font-medium"));'
|
|
1222
|
+
+ ' const matchEl = fontMediumEls.find(el => {'
|
|
1223
|
+
+ ' const text = (el.textContent || "").trim().toLowerCase();'
|
|
1224
|
+
+ ' return text === targetUiNameLower;'
|
|
1225
|
+
+ ' });'
|
|
1226
|
+
+ ' if (matchEl) {'
|
|
1227
|
+
// Target the parent element of .font-medium (div with cursor-pointer) for clicking
|
|
1228
|
+
+ ' modeOption = matchEl.closest("div.cursor-pointer") || matchEl.parentElement;'
|
|
1229
|
+
+ ' }'
|
|
1230
|
+
+ ' }'
|
|
1231
|
+
// Fallback when dialog not found: legacy selectors
|
|
1232
|
+
+ ' if (!modeOption) {'
|
|
1233
|
+
+ ' const fallbackEls = Array.from(document.querySelectorAll('
|
|
1234
|
+
+ ' "div[class*=\\"cursor-pointer\\"]"'
|
|
1235
|
+
+ ' )).filter(el => el.offsetParent !== null);'
|
|
1236
|
+
+ ' modeOption = fallbackEls.find(el => {'
|
|
1237
|
+
+ ' if (el === modeToggleBtn) return false;'
|
|
1238
|
+
+ ' const fm = el.querySelector(".font-medium");'
|
|
1239
|
+
+ ' if (fm) {'
|
|
1240
|
+
+ ' const text = (fm.textContent || "").trim().toLowerCase();'
|
|
1241
|
+
+ ' return text === targetUiNameLower;'
|
|
1242
|
+
+ ' }'
|
|
1243
|
+
+ ' return false;'
|
|
1244
|
+
+ ' });'
|
|
1245
|
+
+ ' }'
|
|
1246
|
+
+ ' if (modeOption) {'
|
|
1247
|
+
+ ' modeOption.click();'
|
|
1248
|
+
+ ' await new Promise(r => setTimeout(r, 500));'
|
|
1249
|
+
// Verify: check if mode button text has changed
|
|
1250
|
+
+ ' const updBtn = Array.from(document.querySelectorAll("button"))'
|
|
1251
|
+
+ ' .filter(b => b.offsetParent !== null)'
|
|
1252
|
+
+ ' .find(b => b.querySelector("svg[class*=\\"chevron\\"]") && knownModes.some(m => (b.textContent || "").trim().toLowerCase() === m));'
|
|
1253
|
+
+ ' const newMode = updBtn ? (updBtn.textContent || "").trim() : "unknown";'
|
|
1254
|
+
+ ' return { ok: true, mode: newMode };'
|
|
1255
|
+
+ ' }'
|
|
1256
|
+
// Failed -> close dropdown
|
|
1257
|
+
+ ' document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape", bubbles: true }));'
|
|
1258
|
+
+ ' await new Promise(r => setTimeout(r, 200));'
|
|
1259
|
+
+ ' return { ok: false, error: "Mode option " + targetUiName + " not found in dropdown" };'
|
|
1260
|
+
+ '})()';
|
|
1261
|
+
try {
|
|
1262
|
+
const contextId = this.getPrimaryContextId();
|
|
1263
|
+
const callParams = {
|
|
1264
|
+
expression,
|
|
1265
|
+
returnByValue: true,
|
|
1266
|
+
awaitPromise: true,
|
|
1267
|
+
};
|
|
1268
|
+
if (contextId !== null)
|
|
1269
|
+
callParams.contextId = contextId;
|
|
1270
|
+
const res = await this.call('Runtime.evaluate', callParams);
|
|
1271
|
+
const value = res?.result?.value;
|
|
1272
|
+
if (value?.ok) {
|
|
1273
|
+
return { ok: true, mode: value.mode };
|
|
1274
|
+
}
|
|
1275
|
+
return { ok: false, error: value?.error || 'UI operation failed (setUiMode)' };
|
|
1276
|
+
}
|
|
1277
|
+
catch (error) {
|
|
1278
|
+
return { ok: false, error: error?.message || String(error) };
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
/**
|
|
1282
|
+
* Dynamically retrieve the list of available models from the Antigravity UI.
|
|
1283
|
+
*/
|
|
1284
|
+
async getUiModels() {
|
|
1285
|
+
if (!this.isConnectedFlag || !this.ws) {
|
|
1286
|
+
throw new Error('Not connected to CDP.');
|
|
1287
|
+
}
|
|
1288
|
+
const expression = `(async () => {
|
|
1289
|
+
return Array.from(document.querySelectorAll('div.cursor-pointer'))
|
|
1290
|
+
.map(e => ({text: (e.textContent || '').trim().replace(/New$/, ''), class: e.className}))
|
|
1291
|
+
.filter(e => e.class.includes('px-2 py-1 flex items-center justify-between') || e.text.includes('Gemini') || e.text.includes('GPT') || e.text.includes('Claude'))
|
|
1292
|
+
.map(e => e.text);
|
|
1293
|
+
})()`;
|
|
1294
|
+
try {
|
|
1295
|
+
const contextId = this.getPrimaryContextId();
|
|
1296
|
+
const callParams = {
|
|
1297
|
+
expression,
|
|
1298
|
+
returnByValue: true,
|
|
1299
|
+
awaitPromise: true,
|
|
1300
|
+
};
|
|
1301
|
+
if (contextId !== null)
|
|
1302
|
+
callParams.contextId = contextId;
|
|
1303
|
+
const res = await this.call('Runtime.evaluate', callParams);
|
|
1304
|
+
const value = res?.result?.value;
|
|
1305
|
+
if (Array.isArray(value) && value.length > 0) {
|
|
1306
|
+
// remove duplicates
|
|
1307
|
+
return Array.from(new Set(value));
|
|
1308
|
+
}
|
|
1309
|
+
return [];
|
|
1310
|
+
}
|
|
1311
|
+
catch (error) {
|
|
1312
|
+
logger_1.logger.error('Failed to get UI models:', error);
|
|
1313
|
+
return [];
|
|
1314
|
+
}
|
|
1315
|
+
}
|
|
1316
|
+
/**
|
|
1317
|
+
* Get the currently selected model from the Antigravity UI.
|
|
1318
|
+
*/
|
|
1319
|
+
async getCurrentModel() {
|
|
1320
|
+
if (!this.isConnectedFlag || !this.ws) {
|
|
1321
|
+
return null;
|
|
1322
|
+
}
|
|
1323
|
+
const expression = `(() => {
|
|
1324
|
+
return Array.from(document.querySelectorAll('div.cursor-pointer'))
|
|
1325
|
+
.find(e => e.className.includes('px-2 py-1 flex items-center justify-between') && e.className.includes('bg-gray-500/20'))
|
|
1326
|
+
?.textContent?.trim().replace(/New$/, '') || null;
|
|
1327
|
+
})()`;
|
|
1328
|
+
try {
|
|
1329
|
+
const contextId = this.getPrimaryContextId();
|
|
1330
|
+
const res = await this.call('Runtime.evaluate', {
|
|
1331
|
+
expression, returnByValue: true, awaitPromise: true,
|
|
1332
|
+
contextId: contextId || undefined
|
|
1333
|
+
});
|
|
1334
|
+
return res?.result?.value || null;
|
|
1335
|
+
}
|
|
1336
|
+
catch (e) {
|
|
1337
|
+
return null;
|
|
1338
|
+
}
|
|
1339
|
+
}
|
|
1340
|
+
/**
|
|
1341
|
+
* Operate Antigravity UI model dropdown to switch to the specified model.
|
|
1342
|
+
* (Step 9: Model/mode switching UI sync)
|
|
1343
|
+
*
|
|
1344
|
+
* @param modelName Model name to set (e.g., 'gpt-4o', 'claude-3-opus')
|
|
1345
|
+
*/
|
|
1346
|
+
async setUiModel(modelName) {
|
|
1347
|
+
if (!this.isConnectedFlag || !this.ws) {
|
|
1348
|
+
throw new Error('Not connected to CDP. Call connect() first.');
|
|
1349
|
+
}
|
|
1350
|
+
// DOM manipulation script: based on actual Antigravity UI DOM structure
|
|
1351
|
+
// Model list uses div.cursor-pointer elements with class 'px-2 py-1 flex items-center justify-between'
|
|
1352
|
+
// Currently selected has 'bg-gray-500/20', others have 'hover:bg-gray-500/10'
|
|
1353
|
+
// textContent may have "New" suffix
|
|
1354
|
+
const safeModel = JSON.stringify(modelName);
|
|
1355
|
+
const expression = `(async () => {
|
|
1356
|
+
const targetModel = ${safeModel};
|
|
1357
|
+
|
|
1358
|
+
// Get all items in the model list
|
|
1359
|
+
const modelItems = Array.from(document.querySelectorAll('div.cursor-pointer'))
|
|
1360
|
+
.filter(e => e.className.includes('px-2 py-1 flex items-center justify-between'));
|
|
1361
|
+
|
|
1362
|
+
if (modelItems.length === 0) {
|
|
1363
|
+
return { ok: false, error: 'Model list not found. The dropdown may not be open.' };
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
// Match target model by name (compare after removing New suffix)
|
|
1367
|
+
const targetItem = modelItems.find(el => {
|
|
1368
|
+
const text = (el.textContent || '').trim().replace(/New$/, '').trim();
|
|
1369
|
+
return text === targetModel || text.toLowerCase() === targetModel.toLowerCase();
|
|
1370
|
+
});
|
|
1371
|
+
|
|
1372
|
+
if (!targetItem) {
|
|
1373
|
+
const available = modelItems.map(el => (el.textContent || '').trim().replace(/New$/, '').trim()).join(', ');
|
|
1374
|
+
return { ok: false, error: 'Model "' + targetModel + '" not found. Available: ' + available };
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
// Check if already selected
|
|
1378
|
+
if (targetItem.className.includes('bg-gray-500/20') && !targetItem.className.includes('hover:bg-gray-500/20')) {
|
|
1379
|
+
return { ok: true, model: targetModel, alreadySelected: true };
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
// Click to select model
|
|
1383
|
+
targetItem.click();
|
|
1384
|
+
await new Promise(r => setTimeout(r, 500));
|
|
1385
|
+
|
|
1386
|
+
// Verify selection was applied
|
|
1387
|
+
const updatedItems = Array.from(document.querySelectorAll('div.cursor-pointer'))
|
|
1388
|
+
.filter(e => e.className.includes('px-2 py-1 flex items-center justify-between'));
|
|
1389
|
+
const selectedItem = updatedItems.find(el => {
|
|
1390
|
+
const text = (el.textContent || '').trim().replace(/New$/, '').trim();
|
|
1391
|
+
return text === targetModel || text.toLowerCase() === targetModel.toLowerCase();
|
|
1392
|
+
});
|
|
1393
|
+
|
|
1394
|
+
if (selectedItem && selectedItem.className.includes('bg-gray-500/20') && !selectedItem.className.includes('hover:bg-gray-500/20')) {
|
|
1395
|
+
return { ok: true, model: targetModel, verified: true };
|
|
1396
|
+
}
|
|
1397
|
+
|
|
1398
|
+
// Click succeeded but verification failed
|
|
1399
|
+
return { ok: true, model: targetModel, verified: false };
|
|
1400
|
+
})()`;
|
|
1401
|
+
try {
|
|
1402
|
+
const contextId = this.getPrimaryContextId();
|
|
1403
|
+
const callParams = {
|
|
1404
|
+
expression,
|
|
1405
|
+
returnByValue: true,
|
|
1406
|
+
awaitPromise: true,
|
|
1407
|
+
};
|
|
1408
|
+
if (contextId !== null)
|
|
1409
|
+
callParams.contextId = contextId;
|
|
1410
|
+
const res = await this.call('Runtime.evaluate', callParams);
|
|
1411
|
+
const value = res?.result?.value;
|
|
1412
|
+
if (value?.ok) {
|
|
1413
|
+
return { ok: true, model: value.model };
|
|
1414
|
+
}
|
|
1415
|
+
return { ok: false, error: value?.error || 'UI operation failed (setUiModel)' };
|
|
1416
|
+
}
|
|
1417
|
+
catch (error) {
|
|
1418
|
+
return { ok: false, error: error?.message || String(error) };
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
exports.CdpService = CdpService;
|
|
1423
|
+
//# sourceMappingURL=cdpService.js.map
|