chrometools-mcp 3.2.10 → 3.3.8

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.
@@ -21,6 +21,10 @@ let nativePort = null;
21
21
  let isConnected = false;
22
22
  const tabsState = new Map(); // tabId -> {url, title, active, windowId}
23
23
 
24
+ // Network requests storage (persists across page navigations)
25
+ const networkRequests = new Map(); // requestId -> request info
26
+ const MAX_NETWORK_REQUESTS = 500;
27
+
24
28
  // Recorder state (persisted in storage)
25
29
  let recorderState = {
26
30
  isRecording: false,
@@ -152,6 +156,24 @@ function handleBridgeMessage(message) {
152
156
  sendToBridge({ type: 'pong', requestId: message.requestId });
153
157
  break;
154
158
 
159
+ case 'get_network_requests':
160
+ // Return recent network requests
161
+ const requests = Array.from(networkRequests.values());
162
+ sendToBridge({
163
+ type: 'network_requests',
164
+ payload: { requests },
165
+ requestId: message.requestId
166
+ });
167
+ break;
168
+
169
+ case 'clear_network_requests':
170
+ networkRequests.clear();
171
+ sendToBridge({
172
+ type: 'network_requests_cleared',
173
+ requestId: message.requestId
174
+ });
175
+ break;
176
+
155
177
  default:
156
178
  console.log('[ChromeTools] Unknown message from Bridge:', message.type);
157
179
  }
@@ -293,6 +315,101 @@ chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
293
315
  }
294
316
  });
295
317
 
318
+ // ============================================
319
+ // Network Request Tracking (via webRequest API)
320
+ // Persists across page navigations!
321
+ // ============================================
322
+
323
+ /**
324
+ * Clean old network requests to prevent memory leak
325
+ */
326
+ function cleanOldNetworkRequests() {
327
+ if (networkRequests.size > MAX_NETWORK_REQUESTS) {
328
+ const entries = Array.from(networkRequests.entries());
329
+ const removeCount = entries.length - MAX_NETWORK_REQUESTS;
330
+ for (let i = 0; i < removeCount; i++) {
331
+ networkRequests.delete(entries[i][0]);
332
+ }
333
+ }
334
+ }
335
+
336
+ // Track when request starts (captures POST/PUT/PATCH before navigation)
337
+ chrome.webRequest.onBeforeRequest.addListener(
338
+ (details) => {
339
+ // Only track mutation requests (POST, PUT, PATCH, DELETE)
340
+ if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(details.method)) {
341
+ const requestInfo = {
342
+ requestId: details.requestId,
343
+ url: details.url,
344
+ method: details.method,
345
+ type: details.type, // main_frame, xmlhttprequest, etc.
346
+ tabId: details.tabId,
347
+ timestamp: Date.now(),
348
+ status: 'pending',
349
+ initiator: details.initiator || null
350
+ };
351
+
352
+ networkRequests.set(details.requestId, requestInfo);
353
+ cleanOldNetworkRequests();
354
+
355
+ // Send to Bridge
356
+ sendToBridge({
357
+ type: 'network_request_started',
358
+ payload: requestInfo
359
+ });
360
+
361
+ console.log(`[ChromeTools] Network: ${details.method} ${details.url}`);
362
+ }
363
+ },
364
+ { urls: ['<all_urls>'] }
365
+ );
366
+
367
+ // Track when request completes
368
+ chrome.webRequest.onCompleted.addListener(
369
+ (details) => {
370
+ const request = networkRequests.get(details.requestId);
371
+ if (request) {
372
+ request.status = details.statusCode;
373
+ request.statusText = details.statusLine;
374
+ request.completedAt = Date.now();
375
+
376
+ // Send update to Bridge
377
+ sendToBridge({
378
+ type: 'network_request_completed',
379
+ payload: {
380
+ requestId: details.requestId,
381
+ status: details.statusCode,
382
+ statusText: details.statusLine
383
+ }
384
+ });
385
+
386
+ console.log(`[ChromeTools] Network completed: ${request.method} ${request.url} -> ${details.statusCode}`);
387
+ }
388
+ },
389
+ { urls: ['<all_urls>'] }
390
+ );
391
+
392
+ // Track request errors
393
+ chrome.webRequest.onErrorOccurred.addListener(
394
+ (details) => {
395
+ const request = networkRequests.get(details.requestId);
396
+ if (request) {
397
+ request.status = 'failed';
398
+ request.error = details.error;
399
+ request.completedAt = Date.now();
400
+
401
+ sendToBridge({
402
+ type: 'network_request_failed',
403
+ payload: {
404
+ requestId: details.requestId,
405
+ error: details.error
406
+ }
407
+ });
408
+ }
409
+ },
410
+ { urls: ['<all_urls>'] }
411
+ );
412
+
296
413
  // ============================================
297
414
  // Icon Management
298
415
  // ============================================
@@ -149,8 +149,10 @@
149
149
 
150
150
  return Array.from(element.classList).filter(cls => {
151
151
  if (cls.length < 2) return false;
152
+ // Filter out Tailwind classes with special characters (colons, slashes, brackets)
153
+ if (/[:\/\[\]]/.test(cls)) return false;
152
154
  return !unstablePatterns.some(p => p.test(cls));
153
- }).slice(0, 3);
155
+ }).slice(0, 3).map(cls => CSS.escape(cls));
154
156
  }
155
157
 
156
158
  function getNthChildPath(element, maxDepth = 5) {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "ChromeTools MCP",
4
- "version": "3.1.2",
4
+ "version": "3.3.6",
5
5
  "description": "Tab tracking and scenario recording for chrometools-mcp",
6
6
 
7
7
  "key": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAxLLqg7Nu1h9ogRVgQoVMRPv8Jp7uRJugZSGUh++Niq0xm3khJefBuJ3L0dSG6xb9tkjTdgqUyg81VUgJBDVw9Bxu6iz1uL17VnEGHDZKe5wpsEpG8o6ZsTWtKRDeoxmkCGSOSDsh/ihlJe8mFaqpBYz6RBaO28R89TNobVhSobTQPB1ptyEND7W7JnsnMOiMcTo9l6j9HrIHLoHj7tO42DHNI4tEyLxI7C6R3i5dLIdwwxJMj0Hhrx4Ncmh24AzPyZypxVvpa1V7HP3sAXGBoUjLd/SEaY8j50lnaIQI3AkYv86pS9l6EZ6y3XCuW7C7W9guTTL/7ZNawYoE2bJ1HwIDAQAB",
@@ -12,6 +12,7 @@
12
12
  "scripting",
13
13
  "storage",
14
14
  "webNavigation",
15
+ "webRequest",
15
16
  "nativeMessaging"
16
17
  ],
17
18
 
package/index.js CHANGED
@@ -1,4 +1,4 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
 
3
3
  import {Server} from "@modelcontextprotocol/sdk/server/index.js";
4
4
  import {StdioServerTransport} from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -360,12 +360,65 @@ async function executeToolInternal(name, args) {
360
360
  };
361
361
  }
362
362
 
363
+ /**
364
+ * Check if identifier looks like an APOM ID (e.g., button_8, input_4, form_1)
365
+ */
366
+ function isApomIdPattern(identifier) {
367
+ return /^[a-z]+_\d+$/.test(identifier);
368
+ }
369
+
370
+ /**
371
+ * Quick element registration - runs APOM analysis and registers elements
372
+ */
373
+ async function quickRegisterElements(page) {
374
+ await page.evaluate((apomTreeConverterCode, selectorResolverCode) => {
375
+ // Inject utilities
376
+ if (typeof buildAPOMTree === 'undefined') {
377
+ eval(apomTreeConverterCode);
378
+ }
379
+ if (typeof registerElements === 'undefined') {
380
+ eval(selectorResolverCode);
381
+ }
382
+
383
+ // Build APOM tree (interactive only for speed)
384
+ const apomData = buildAPOMTree(true);
385
+
386
+ // Flatten and register elements
387
+ const elementsArray = [];
388
+ function collectElements(node) {
389
+ if (!node) return;
390
+ if (node.id && node.selector) {
391
+ elementsArray.push({
392
+ id: node.id,
393
+ selector: node.selector,
394
+ metadata: { type: node.type, tag: node.tag }
395
+ });
396
+ }
397
+ if (node.children) {
398
+ node.children.forEach(child => collectElements(child));
399
+ }
400
+ // Also check for container keys (div_container_0, etc.)
401
+ Object.keys(node).forEach(key => {
402
+ if (Array.isArray(node[key])) {
403
+ node[key].forEach(child => collectElements(child));
404
+ }
405
+ });
406
+ }
407
+ collectElements(apomData.tree);
408
+
409
+ if (typeof registerElements !== 'undefined') {
410
+ registerElements(elementsArray);
411
+ }
412
+ }, apomTreeConverter, selectorResolver);
413
+ }
414
+
363
415
  /**
364
416
  * Helper: Resolve selector (ID or CSS selector)
365
417
  * Injects selector-resolver and resolves element identifier
418
+ * Auto-refreshes element registry if APOM ID not found
366
419
  */
367
- async function resolveSelector(page, identifier) {
368
- return await page.evaluate((id, selectorResolverCode) => {
420
+ async function resolveSelector(page, identifier, timeoutMs = 5000) {
421
+ const tryResolve = () => page.evaluate((id, selectorResolverCode) => {
369
422
  // Inject selector resolver if not already loaded
370
423
  if (typeof resolveSelector === 'undefined') {
371
424
  eval(selectorResolverCode);
@@ -378,6 +431,20 @@ async function executeToolInternal(name, args) {
378
431
  found: document.querySelector(resolved.selector) !== null
379
432
  };
380
433
  }, identifier, selectorResolver);
434
+
435
+ const timeoutPromise = new Promise((_, reject) =>
436
+ setTimeout(() => reject(new Error(`resolveSelector timed out after ${timeoutMs}ms`)), timeoutMs)
437
+ );
438
+
439
+ let resolved = await Promise.race([tryResolve(), timeoutPromise]);
440
+
441
+ // Auto-refresh: if looks like APOM ID but not found, re-register elements and retry
442
+ if (!resolved.found && isApomIdPattern(identifier)) {
443
+ await quickRegisterElements(page);
444
+ resolved = await Promise.race([tryResolve(), timeoutPromise]);
445
+ }
446
+
447
+ return resolved;
381
448
  }
382
449
 
383
450
  if (name === "click") {
@@ -401,31 +468,56 @@ async function executeToolInternal(name, args) {
401
468
  throw new Error(`Element not found: ${identifier}`);
402
469
  }
403
470
 
404
- // Capture timestamp BEFORE click for error filtering
471
+ // Capture timestamp and URL BEFORE click for diagnostics
405
472
  const beforeClickTimestamp = Date.now();
473
+ const urlBeforeClick = page.url();
474
+
475
+ // ALWAYS scroll to element first to ensure it's in viewport
476
+ await element.evaluate(el => el.scrollIntoView({ behavior: 'instant', block: 'center' }));
477
+
478
+ // Click with timeout to prevent hanging on navigation
479
+ const clickWithTimeout = async (timeoutMs = 5000) => {
480
+ const clickPromise = element.click().catch(() => {
481
+ // If Puppeteer click fails, fallback to JS click
482
+ return element.evaluate(el => el.click());
483
+ });
484
+ const timeoutPromise = new Promise((_, reject) =>
485
+ setTimeout(() => reject(new Error('click timeout')), timeoutMs)
486
+ );
487
+ return Promise.race([clickPromise, timeoutPromise]).catch(() => {
488
+ // If click times out, try JS click as last resort
489
+ return element.evaluate(el => el.click());
490
+ });
491
+ };
406
492
 
407
- // Try multiple click methods for better reliability
408
- try {
409
- // Method 1: Puppeteer click (most reliable for most cases)
410
- await element.click();
411
- } catch (clickError) {
412
- // Method 2: Scroll into view and try again
413
- try {
414
- await element.evaluate(el => el.scrollIntoView({ behavior: 'instant', block: 'center' }));
415
- await new Promise(resolve => setTimeout(resolve, 100));
416
- await element.click();
417
- } catch (scrollClickError) {
418
- // Method 3: JavaScript click (works for hidden/overlapping elements)
419
- await element.evaluate(el => el.click());
420
- }
421
- }
493
+ await clickWithTimeout();
422
494
 
423
495
  // NEW POST-CLICK PATTERN:
424
- // 1. Run post-click diagnostics (waits 500ms, checks pending requests, collects errors)
425
- const diagnostics = await runPostClickDiagnostics(page, beforeClickTimestamp);
496
+ // 1. Run post-click diagnostics (waits for network requests within 200ms, max 10s timeout)
497
+ let diagnostics;
498
+ try {
499
+ diagnostics = await runPostClickDiagnostics(page, beforeClickTimestamp, {
500
+ skipNetworkWait: validatedArgs.skipNetworkWait,
501
+ networkWaitTimeout: validatedArgs.networkWaitTimeout,
502
+ urlBeforeAction: urlBeforeClick
503
+ });
504
+ } catch (diagError) {
505
+ // Diagnostics may fail if page navigated - create minimal diagnostics
506
+ diagnostics = {
507
+ networkActivity: { trackedRequests: [], bridgeRequests: [], stillPending: 0, pendingRequests: [] },
508
+ navigation: { from: urlBeforeClick, to: page.url(), likelyFormSubmit: true },
509
+ errors: { consoleErrors: [], networkErrors: [], totalErrors: 0 },
510
+ hasErrors: false
511
+ };
512
+ }
426
513
 
427
514
  // 2. Generate AI hints after click
428
- const hints = await generateClickHints(page, identifier);
515
+ let hints;
516
+ try {
517
+ hints = await generateClickHints(page, identifier);
518
+ } catch (hintsError) {
519
+ hints = { modalOpened: false, newElements: [], suggestedNext: [] };
520
+ }
429
521
 
430
522
  // 3. Format output with hints and diagnostics
431
523
  let hintsText = '\n\n** AI HINTS **';
@@ -464,36 +556,60 @@ async function executeToolInternal(name, args) {
464
556
  if (name === "type") {
465
557
  const validatedArgs = schemas.TypeSchema.parse(args);
466
558
  const page = await getLastOpenPage();
559
+ const timeout = validatedArgs.timeout || 30000;
467
560
 
468
- // Get identifier (id or selector)
469
- const identifier = validatedArgs.id || validatedArgs.selector;
561
+ // Wrap operation in timeout
562
+ const typeOperation = async () => {
563
+ // Get identifier (id or selector)
564
+ const identifier = validatedArgs.id || validatedArgs.selector;
470
565
 
471
- // Resolve selector (supports both APOM ID and CSS selector)
472
- const resolved = await resolveSelector(page, identifier);
473
- if (!resolved.found) {
474
- throw new Error(`Element not found: ${identifier}${resolved.isPageObjectId ? ' (APOM ID)' : ' (CSS selector)'}`);
475
- }
566
+ // Resolve selector (supports both APOM ID and CSS selector)
567
+ const resolved = await resolveSelector(page, identifier);
568
+ if (!resolved.found) {
569
+ throw new Error(`Element not found: ${identifier}${resolved.isPageObjectId ? ' (APOM ID)' : ' (CSS selector)'}`);
570
+ }
476
571
 
477
- const element = await page.$(resolved.selector);
478
- if (!element) {
479
- throw new Error(`Element not found: ${identifier}`);
480
- }
572
+ const element = await page.$(resolved.selector);
573
+ if (!element) {
574
+ throw new Error(`Element not found: ${identifier}`);
575
+ }
481
576
 
482
- // Use input model to handle the element appropriately
483
- const model = await getInputModel(element, page);
484
- const options = {
485
- delay: validatedArgs.delay !== undefined ? validatedArgs.delay : 30,
486
- clearFirst: validatedArgs.clearFirst !== undefined ? validatedArgs.clearFirst : true,
487
- };
577
+ // ALWAYS scroll to element first to ensure it's in viewport
578
+ await element.evaluate(el => el.scrollIntoView({ behavior: 'instant', block: 'center' }));
579
+ await new Promise(resolve => setTimeout(resolve, 100));
488
580
 
489
- await model.setValue(validatedArgs.text, options);
490
- const description = model.getActionDescription(validatedArgs.text, identifier);
581
+ // Use input model to handle the element appropriately
582
+ const model = await getInputModel(element, page);
583
+ const options = {
584
+ delay: validatedArgs.delay !== undefined ? validatedArgs.delay : 30,
585
+ clearFirst: validatedArgs.clearFirst !== undefined ? validatedArgs.clearFirst : true,
586
+ };
491
587
 
492
- return {
493
- content: [
494
- { type: "text", text: description }
495
- ],
588
+ await model.setValue(validatedArgs.text, options);
589
+ const description = model.getActionDescription(validatedArgs.text, identifier);
590
+
591
+ // Capture timestamp AFTER typing finishes - requests should start within 200ms of this
592
+ const afterTypeTimestamp = Date.now();
593
+
594
+ // Run diagnostics with 3s timeout for type (shorter than click's 10s)
595
+ const diagnostics = await runPostClickDiagnostics(page, afterTypeTimestamp, {
596
+ networkWaitTimeout: 3000
597
+ });
598
+ const diagnosticsText = formatDiagnosticsForAI(diagnostics);
599
+
600
+ return {
601
+ content: [
602
+ { type: "text", text: `${description}${diagnosticsText}` }
603
+ ],
604
+ };
496
605
  };
606
+
607
+ // Execute with timeout
608
+ const timeoutPromise = new Promise((_, reject) =>
609
+ setTimeout(() => reject(new Error(`Type operation timed out after ${timeout}ms`)), timeout)
610
+ );
611
+
612
+ return Promise.race([typeOperation(), timeoutPromise]);
497
613
  }
498
614
 
499
615
  if (name === "getComputedCss") {
@@ -2096,20 +2212,25 @@ Start coding now.`;
2096
2212
  };
2097
2213
  }
2098
2214
 
2215
+ // Store previous analysis for diff calculation
2216
+ if (!global.previousApomAnalysis) {
2217
+ global.previousApomAnalysis = new Map(); // pageUrl -> analysis data
2218
+ }
2219
+
2099
2220
  if (name === "analyzePage") {
2100
2221
  const validatedArgs = schemas.AnalyzePageSchema.parse(args);
2101
2222
  const page = await getLastOpenPage();
2102
2223
  const pageUrl = page.url();
2103
2224
 
2104
2225
  // APOM Tree format (default) - v2 with tree structure and positioning
2105
- const apomResult = await page.evaluate((apomTreeConverterCode, selectorResolverCode, shouldRegister, includeAll) => {
2226
+ const apomResult = await page.evaluate((apomTreeConverterCode, selectorResolverCode, shouldRegister, includeAll, viewportOnly) => {
2106
2227
  // Inject utilities
2107
2228
  eval(apomTreeConverterCode);
2108
2229
  eval(selectorResolverCode);
2109
2230
 
2110
2231
  // Build APOM tree
2111
2232
  // interactiveOnly = !includeAll (if includeAll is true, we want ALL elements)
2112
- const apomData = buildAPOMTree(!includeAll);
2233
+ const apomData = buildAPOMTree(!includeAll, viewportOnly);
2113
2234
 
2114
2235
  // Register elements in selector resolver if requested
2115
2236
  if (shouldRegister) {
@@ -2142,7 +2263,43 @@ Start coding now.`;
2142
2263
  }
2143
2264
 
2144
2265
  return apomData;
2145
- }, apomTreeConverter, selectorResolver, validatedArgs.registerElements !== false, validatedArgs.includeAll || false);
2266
+ }, apomTreeConverter, selectorResolver, validatedArgs.registerElements !== false, validatedArgs.includeAll || false, validatedArgs.viewportOnly || false);
2267
+
2268
+ // Handle diff mode
2269
+ if (validatedArgs.diff) {
2270
+ const previousAnalysis = global.previousApomAnalysis.get(pageUrl);
2271
+
2272
+ if (previousAnalysis) {
2273
+ // Calculate diff
2274
+ const diff = calculateApomDiff(previousAnalysis, apomResult);
2275
+
2276
+ // Store current analysis for next diff
2277
+ global.previousApomAnalysis.set(pageUrl, apomResult);
2278
+
2279
+ return {
2280
+ content: [{
2281
+ type: 'text',
2282
+ text: JSON.stringify({
2283
+ mode: 'diff',
2284
+ pageId: apomResult.pageId,
2285
+ url: apomResult.url,
2286
+ timestamp: apomResult.timestamp,
2287
+ previousTimestamp: previousAnalysis.timestamp,
2288
+ diff,
2289
+ metadata: apomResult.metadata,
2290
+ alerts: apomResult.alerts
2291
+ })
2292
+ }]
2293
+ };
2294
+ } else {
2295
+ // No previous analysis, return full result with note
2296
+ global.previousApomAnalysis.set(pageUrl, apomResult);
2297
+ apomResult._note = 'First analysis for this page, no diff available';
2298
+ }
2299
+ } else {
2300
+ // Store for future diff
2301
+ global.previousApomAnalysis.set(pageUrl, apomResult);
2302
+ }
2146
2303
 
2147
2304
  return {
2148
2305
  content: [{
@@ -2152,6 +2309,82 @@ Start coding now.`;
2152
2309
  };
2153
2310
  }
2154
2311
 
2312
+ /**
2313
+ * Calculate diff between two APOM analyses
2314
+ */
2315
+ function calculateApomDiff(previous, current) {
2316
+ const previousElements = flattenApomTree(previous.tree);
2317
+ const currentElements = flattenApomTree(current.tree);
2318
+
2319
+ const previousIds = new Set(previousElements.map(e => e.id));
2320
+ const currentIds = new Set(currentElements.map(e => e.id));
2321
+
2322
+ const added = currentElements.filter(e => !previousIds.has(e.id));
2323
+ const removed = previousElements.filter(e => !currentIds.has(e.id));
2324
+
2325
+ // Find changed elements (same ID but different content)
2326
+ const changed = [];
2327
+ for (const curr of currentElements) {
2328
+ if (previousIds.has(curr.id)) {
2329
+ const prev = previousElements.find(e => e.id === curr.id);
2330
+ if (prev && JSON.stringify(prev.metadata) !== JSON.stringify(curr.metadata)) {
2331
+ changed.push({
2332
+ id: curr.id,
2333
+ type: curr.type,
2334
+ before: prev.metadata,
2335
+ after: curr.metadata
2336
+ });
2337
+ }
2338
+ }
2339
+ }
2340
+
2341
+ return {
2342
+ added: added.length > 0 ? added : undefined,
2343
+ removed: removed.length > 0 ? removed : undefined,
2344
+ changed: changed.length > 0 ? changed : undefined,
2345
+ summary: {
2346
+ addedCount: added.length,
2347
+ removedCount: removed.length,
2348
+ changedCount: changed.length
2349
+ }
2350
+ };
2351
+ }
2352
+
2353
+ /**
2354
+ * Flatten APOM tree to array of elements
2355
+ */
2356
+ function flattenApomTree(node, result = []) {
2357
+ if (!node) return result;
2358
+
2359
+ // Handle compact format: { "tag_id": [children] }
2360
+ if (typeof node === 'object' && !node.id && !node.tag) {
2361
+ const keys = Object.keys(node);
2362
+ for (const key of keys) {
2363
+ if (Array.isArray(node[key])) {
2364
+ node[key].forEach(child => flattenApomTree(child, result));
2365
+ }
2366
+ }
2367
+ return result;
2368
+ }
2369
+
2370
+ // Interactive element with id
2371
+ if (node.id) {
2372
+ result.push({
2373
+ id: node.id,
2374
+ tag: node.tag,
2375
+ type: node.type,
2376
+ metadata: node.metadata
2377
+ });
2378
+ }
2379
+
2380
+ // Process children
2381
+ if (node.children) {
2382
+ node.children.forEach(child => flattenApomTree(child, result));
2383
+ }
2384
+
2385
+ return result;
2386
+ }
2387
+
2155
2388
  if (name === "getElementDetails") {
2156
2389
  const validatedArgs = schemas.GetElementDetailsSchema.parse(args);
2157
2390
  const page = await getLastOpenPage();
@@ -2462,6 +2695,9 @@ Start coding now.`;
2462
2695
  const extensionConnected = isExtensionConnected();
2463
2696
  const debugInfo = getWsDebugInfo();
2464
2697
 
2698
+ // Always log connection state for debugging
2699
+ console.error(`[chrometools-mcp] enableRecorder check: bridgeConnected=${debugInfo.bridgeConnected}, extensionConnected=${extensionConnected}, wsState=${debugInfo.readyState}`);
2700
+
2465
2701
  if (extensionConnected) {
2466
2702
  return {
2467
2703
  content: [{
@@ -7,25 +7,76 @@
7
7
 
8
8
  import { BaseInputModel } from './BaseInputModel.js';
9
9
 
10
+ /**
11
+ * Wrap operation with timeout to prevent hanging
12
+ */
13
+ async function withTimeout(operation, timeoutMs, operationName) {
14
+ const timeoutPromise = new Promise((_, reject) =>
15
+ setTimeout(() => reject(new Error(`${operationName} timed out after ${timeoutMs}ms`)), timeoutMs)
16
+ );
17
+ return Promise.race([operation(), timeoutPromise]);
18
+ }
19
+
10
20
  export class TextInputModel extends BaseInputModel {
11
21
  static get inputTypes() {
12
22
  return ['text', 'email', 'tel', 'password', 'search', 'url', 'number', null];
13
23
  }
14
24
 
15
25
  /**
16
- * Type text into the input using keyboard simulation
26
+ * Type text into the input using keyboard simulation with JS fallback
17
27
  * @param {string} value - Text to type
18
28
  * @param {object} options - { delay, clearFirst }
19
29
  */
20
30
  async setValue(value, options = {}) {
21
31
  const { delay = 0, clearFirst = true } = options;
32
+ const opTimeout = 5000; // 5s timeout per operation
33
+
34
+ // Method 1: Try Puppeteer typing (works for most cases)
35
+ try {
36
+ // Focus and clear using JS (most reliable)
37
+ await withTimeout(
38
+ () => this.element.evaluate((el, shouldClear) => {
39
+ el.focus();
40
+ el.click();
41
+ if (shouldClear) {
42
+ el.select(); // Select all text
43
+ }
44
+ }, clearFirst),
45
+ opTimeout,
46
+ 'focus-and-select'
47
+ );
48
+
49
+ // Small delay to ensure focus is established
50
+ await new Promise(resolve => setTimeout(resolve, 50));
51
+
52
+ // Type the new value
53
+ const typeTimeout = Math.max(opTimeout, value.length * delay + 5000);
54
+ await withTimeout(
55
+ () => this.element.type(value, { delay }),
56
+ typeTimeout,
57
+ 'type'
58
+ );
22
59
 
23
- if (clearFirst) {
24
- await this.element.click({ clickCount: 3 });
25
- await this.page.keyboard.press('Backspace');
60
+ // Verify the value was set
61
+ const actualValue = await this.element.evaluate(el => el.value);
62
+ if (actualValue.includes(value)) {
63
+ return; // Success
64
+ }
65
+ } catch (e) {
66
+ // Fall through to JS method
26
67
  }
27
68
 
28
- await this.element.type(value, { delay });
69
+ // Method 2: Fallback to direct JS value setting
70
+ await withTimeout(
71
+ () => this.element.evaluate((el, newValue) => {
72
+ el.focus();
73
+ el.value = newValue;
74
+ el.dispatchEvent(new Event('input', { bubbles: true }));
75
+ el.dispatchEvent(new Event('change', { bubbles: true }));
76
+ }, value),
77
+ opTimeout,
78
+ 'js-set-value'
79
+ );
29
80
  }
30
81
 
31
82
  getActionDescription(value, identifier) {