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.
Files changed (200) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +297 -0
  3. package/dist/bin/cli.d.ts +2 -0
  4. package/dist/bin/cli.js +80 -0
  5. package/dist/bin/cli.js.map +1 -0
  6. package/dist/bin/commands/doctor.d.ts +1 -0
  7. package/dist/bin/commands/doctor.js +211 -0
  8. package/dist/bin/commands/doctor.js.map +1 -0
  9. package/dist/bin/commands/open.d.ts +1 -0
  10. package/dist/bin/commands/open.js +187 -0
  11. package/dist/bin/commands/open.js.map +1 -0
  12. package/dist/bin/commands/setup.d.ts +1 -0
  13. package/dist/bin/commands/setup.js +267 -0
  14. package/dist/bin/commands/setup.js.map +1 -0
  15. package/dist/bin/commands/start.d.ts +2 -0
  16. package/dist/bin/commands/start.js +39 -0
  17. package/dist/bin/commands/start.js.map +1 -0
  18. package/dist/bot/index.d.ts +2 -0
  19. package/dist/bot/index.js +1393 -0
  20. package/dist/bot/index.js.map +1 -0
  21. package/dist/commands/chatCommandHandler.d.ts +20 -0
  22. package/dist/commands/chatCommandHandler.js +30 -0
  23. package/dist/commands/chatCommandHandler.js.map +1 -0
  24. package/dist/commands/cleanupCommandHandler.d.ts +21 -0
  25. package/dist/commands/cleanupCommandHandler.js +40 -0
  26. package/dist/commands/cleanupCommandHandler.js.map +1 -0
  27. package/dist/commands/joinCommandHandler.d.ts +19 -0
  28. package/dist/commands/joinCommandHandler.js +27 -0
  29. package/dist/commands/joinCommandHandler.js.map +1 -0
  30. package/dist/commands/messageParser.d.ts +7 -0
  31. package/dist/commands/messageParser.js +29 -0
  32. package/dist/commands/messageParser.js.map +1 -0
  33. package/dist/commands/slashCommandHandler.d.ts +21 -0
  34. package/dist/commands/slashCommandHandler.js +105 -0
  35. package/dist/commands/slashCommandHandler.js.map +1 -0
  36. package/dist/commands/workspaceCommandHandler.d.ts +16 -0
  37. package/dist/commands/workspaceCommandHandler.js +29 -0
  38. package/dist/commands/workspaceCommandHandler.js.map +1 -0
  39. package/dist/database/chatSessionRepository.d.ts +59 -0
  40. package/dist/database/chatSessionRepository.js +110 -0
  41. package/dist/database/chatSessionRepository.js.map +1 -0
  42. package/dist/database/scheduleRepository.d.ts +60 -0
  43. package/dist/database/scheduleRepository.js +106 -0
  44. package/dist/database/scheduleRepository.js.map +1 -0
  45. package/dist/database/templateRepository.d.ts +51 -0
  46. package/dist/database/templateRepository.js +90 -0
  47. package/dist/database/templateRepository.js.map +1 -0
  48. package/dist/database/workspaceBindingRepository.d.ts +48 -0
  49. package/dist/database/workspaceBindingRepository.js +92 -0
  50. package/dist/database/workspaceBindingRepository.js.map +1 -0
  51. package/dist/index.d.ts +1 -0
  52. package/dist/index.js +11 -0
  53. package/dist/index.js.map +1 -0
  54. package/dist/middleware/auth.d.ts +5 -0
  55. package/dist/middleware/auth.js +14 -0
  56. package/dist/middleware/auth.js.map +1 -0
  57. package/dist/middleware/sanitize.d.ts +1 -0
  58. package/dist/middleware/sanitize.js +18 -0
  59. package/dist/middleware/sanitize.js.map +1 -0
  60. package/dist/services/antigravityLauncher.d.ts +7 -0
  61. package/dist/services/antigravityLauncher.js +94 -0
  62. package/dist/services/antigravityLauncher.js.map +1 -0
  63. package/dist/services/approvalDetector.d.ts +97 -0
  64. package/dist/services/approvalDetector.js +394 -0
  65. package/dist/services/approvalDetector.js.map +1 -0
  66. package/dist/services/assistantDomExtractor.d.ts +49 -0
  67. package/dist/services/assistantDomExtractor.js +340 -0
  68. package/dist/services/assistantDomExtractor.js.map +1 -0
  69. package/dist/services/autoAcceptService.d.ts +14 -0
  70. package/dist/services/autoAcceptService.js +81 -0
  71. package/dist/services/autoAcceptService.js.map +1 -0
  72. package/dist/services/cdpBridgeManager.d.ts +50 -0
  73. package/dist/services/cdpBridgeManager.js +355 -0
  74. package/dist/services/cdpBridgeManager.js.map +1 -0
  75. package/dist/services/cdpConnectionPool.d.ts +88 -0
  76. package/dist/services/cdpConnectionPool.js +235 -0
  77. package/dist/services/cdpConnectionPool.js.map +1 -0
  78. package/dist/services/cdpService.d.ts +214 -0
  79. package/dist/services/cdpService.js +1423 -0
  80. package/dist/services/cdpService.js.map +1 -0
  81. package/dist/services/chatSessionService.d.ts +89 -0
  82. package/dist/services/chatSessionService.js +738 -0
  83. package/dist/services/chatSessionService.js.map +1 -0
  84. package/dist/services/errorPopupDetector.d.ts +89 -0
  85. package/dist/services/errorPopupDetector.js +274 -0
  86. package/dist/services/errorPopupDetector.js.map +1 -0
  87. package/dist/services/modeService.d.ts +44 -0
  88. package/dist/services/modeService.js +74 -0
  89. package/dist/services/modeService.js.map +1 -0
  90. package/dist/services/modelService.d.ts +36 -0
  91. package/dist/services/modelService.js +64 -0
  92. package/dist/services/modelService.js.map +1 -0
  93. package/dist/services/planningDetector.d.ts +87 -0
  94. package/dist/services/planningDetector.js +321 -0
  95. package/dist/services/planningDetector.js.map +1 -0
  96. package/dist/services/processManager.d.ts +18 -0
  97. package/dist/services/processManager.js +62 -0
  98. package/dist/services/processManager.js.map +1 -0
  99. package/dist/services/progressSender.d.ts +20 -0
  100. package/dist/services/progressSender.js +65 -0
  101. package/dist/services/progressSender.js.map +1 -0
  102. package/dist/services/promptDispatcher.d.ts +38 -0
  103. package/dist/services/promptDispatcher.js +42 -0
  104. package/dist/services/promptDispatcher.js.map +1 -0
  105. package/dist/services/quotaService.d.ts +21 -0
  106. package/dist/services/quotaService.js +191 -0
  107. package/dist/services/quotaService.js.map +1 -0
  108. package/dist/services/responseMonitor.d.ts +129 -0
  109. package/dist/services/responseMonitor.js +996 -0
  110. package/dist/services/responseMonitor.js.map +1 -0
  111. package/dist/services/scheduleService.d.ts +58 -0
  112. package/dist/services/scheduleService.js +135 -0
  113. package/dist/services/scheduleService.js.map +1 -0
  114. package/dist/services/screenshotService.d.ts +55 -0
  115. package/dist/services/screenshotService.js +86 -0
  116. package/dist/services/screenshotService.js.map +1 -0
  117. package/dist/services/telegramTopicManager.d.ts +40 -0
  118. package/dist/services/telegramTopicManager.js +103 -0
  119. package/dist/services/telegramTopicManager.js.map +1 -0
  120. package/dist/services/titleGeneratorService.d.ts +32 -0
  121. package/dist/services/titleGeneratorService.js +114 -0
  122. package/dist/services/titleGeneratorService.js.map +1 -0
  123. package/dist/services/updateCheckService.d.ts +16 -0
  124. package/dist/services/updateCheckService.js +148 -0
  125. package/dist/services/updateCheckService.js.map +1 -0
  126. package/dist/services/userMessageDetector.d.ts +57 -0
  127. package/dist/services/userMessageDetector.js +222 -0
  128. package/dist/services/userMessageDetector.js.map +1 -0
  129. package/dist/services/workspaceService.d.ts +33 -0
  130. package/dist/services/workspaceService.js +65 -0
  131. package/dist/services/workspaceService.js.map +1 -0
  132. package/dist/ui/autoAcceptUi.d.ts +6 -0
  133. package/dist/ui/autoAcceptUi.js +22 -0
  134. package/dist/ui/autoAcceptUi.js.map +1 -0
  135. package/dist/ui/modeUi.d.ts +12 -0
  136. package/dist/ui/modeUi.js +40 -0
  137. package/dist/ui/modeUi.js.map +1 -0
  138. package/dist/ui/modelsUi.d.ts +12 -0
  139. package/dist/ui/modelsUi.js +101 -0
  140. package/dist/ui/modelsUi.js.map +1 -0
  141. package/dist/ui/projectListUi.d.ts +11 -0
  142. package/dist/ui/projectListUi.js +59 -0
  143. package/dist/ui/projectListUi.js.map +1 -0
  144. package/dist/ui/screenshotUi.d.ts +6 -0
  145. package/dist/ui/screenshotUi.js +28 -0
  146. package/dist/ui/screenshotUi.js.map +1 -0
  147. package/dist/ui/sessionPickerUi.d.ts +8 -0
  148. package/dist/ui/sessionPickerUi.js +32 -0
  149. package/dist/ui/sessionPickerUi.js.map +1 -0
  150. package/dist/ui/templateUi.d.ts +5 -0
  151. package/dist/ui/templateUi.js +44 -0
  152. package/dist/ui/templateUi.js.map +1 -0
  153. package/dist/utils/cdpPorts.d.ts +2 -0
  154. package/dist/utils/cdpPorts.js +6 -0
  155. package/dist/utils/cdpPorts.js.map +1 -0
  156. package/dist/utils/config.d.ts +14 -0
  157. package/dist/utils/config.js +12 -0
  158. package/dist/utils/config.js.map +1 -0
  159. package/dist/utils/configLoader.d.ts +23 -0
  160. package/dist/utils/configLoader.js +153 -0
  161. package/dist/utils/configLoader.js.map +1 -0
  162. package/dist/utils/htmlToTelegramMarkdown.d.ts +6 -0
  163. package/dist/utils/htmlToTelegramMarkdown.js +189 -0
  164. package/dist/utils/htmlToTelegramMarkdown.js.map +1 -0
  165. package/dist/utils/i18n.d.ts +3 -0
  166. package/dist/utils/i18n.js +78 -0
  167. package/dist/utils/i18n.js.map +1 -0
  168. package/dist/utils/imageHandler.d.ts +35 -0
  169. package/dist/utils/imageHandler.js +155 -0
  170. package/dist/utils/imageHandler.js.map +1 -0
  171. package/dist/utils/lockfile.d.ts +7 -0
  172. package/dist/utils/lockfile.js +117 -0
  173. package/dist/utils/lockfile.js.map +1 -0
  174. package/dist/utils/logger.d.ts +23 -0
  175. package/dist/utils/logger.js +85 -0
  176. package/dist/utils/logger.js.map +1 -0
  177. package/dist/utils/logo.d.ts +1 -0
  178. package/dist/utils/logo.js +14 -0
  179. package/dist/utils/logo.js.map +1 -0
  180. package/dist/utils/metadataExtractor.d.ts +5 -0
  181. package/dist/utils/metadataExtractor.js +16 -0
  182. package/dist/utils/metadataExtractor.js.map +1 -0
  183. package/dist/utils/pathUtils.d.ts +23 -0
  184. package/dist/utils/pathUtils.js +58 -0
  185. package/dist/utils/pathUtils.js.map +1 -0
  186. package/dist/utils/processLogBuffer.d.ts +17 -0
  187. package/dist/utils/processLogBuffer.js +108 -0
  188. package/dist/utils/processLogBuffer.js.map +1 -0
  189. package/dist/utils/streamMessageFormatter.d.ts +18 -0
  190. package/dist/utils/streamMessageFormatter.js +91 -0
  191. package/dist/utils/streamMessageFormatter.js.map +1 -0
  192. package/dist/utils/telegramFormatter.d.ts +37 -0
  193. package/dist/utils/telegramFormatter.js +445 -0
  194. package/dist/utils/telegramFormatter.js.map +1 -0
  195. package/dist/utils/voiceHandler.d.ts +23 -0
  196. package/dist/utils/voiceHandler.js +169 -0
  197. package/dist/utils/voiceHandler.js.map +1 -0
  198. package/locales/en.json +85 -0
  199. package/locales/ja.json +109 -0
  200. 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