chrometools-mcp 3.2.10 → 3.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,128 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [3.3.5] - 2026-01-30
6
+
7
+ ### Fixed
8
+ - **type/click: Intermittent timeout fix** — Added timeouts to internal operations
9
+ - `resolveSelector` now has 5s timeout (was unbounded)
10
+ - `getElementInfo` now has 5s timeout (was unbounded)
11
+ - `TextInputModel.setValue` operations have individual 5s timeouts
12
+ - Prevents indefinite hanging on unstable CDP sessions
13
+ - Error messages now show which specific operation timed out
14
+
15
+ ## [3.3.4] - 2026-01-30
16
+
17
+ ### Added
18
+ - **click: Form submit detection for non-SPA apps** — Detect classic form submissions with page reload
19
+ - Tracks URL before and after click to detect page navigation
20
+ - Reports "Page navigation detected (form submit)" when URL changes
21
+ - Solves issue where POST requests were lost during page reload
22
+ - Works for Django, Rails, PHP and other server-rendered apps
23
+ - Example output: `🔄 Page navigation detected (form submit): /form → /success`
24
+
25
+ ## [3.3.3] - 2026-01-30
26
+
27
+ ### Fixed
28
+ - **click: Post-click diagnostics timeout** — Fixed 30s timeout caused by network wait logic
29
+ - Now waits only for mutation requests (POST/PUT/PATCH) started within 200ms after click
30
+ - Ignores GET requests completely (were causing unnecessary waits)
31
+ - Hard 10s timeout limit enforced (never hangs indefinitely)
32
+ - Always returns success even if requests still pending after 10s
33
+ - Shows pending requests status instead of reporting timeout
34
+ - Changed output from "Form submission" to "Mutation requests"
35
+ - Example output:
36
+ ```
37
+ 📡 Mutation requests detected (2 POST/PUT/PATCH):
38
+ 1. ✓ POST /admin/tenant/.../change/ → 302 Found
39
+ 2. ⏳ POST /api/slow-endpoint/ → pending
40
+
41
+ ⏳ 1 request(s) still pending after 10000ms timeout
42
+ ```
43
+
44
+ ### Performance
45
+ - **click: Removed unnecessary delays** — Removed 100ms waits after scrollIntoView and retry clicks
46
+ - Click operations now complete instantly when no mutation requests detected
47
+ - Typical click time: <50ms (was 500-10000ms with old diagnostics)
48
+
49
+ ## [3.3.2] - 2026-01-30
50
+
51
+ ### Fixed
52
+ - **click/type: Viewport scrolling** — Always scrolls to element before interaction
53
+ - Prevents 30s timeout when elements are outside viewport
54
+ - Moves scrollIntoView BEFORE first click/type attempt (was in fallback)
55
+ - Fixes Django admin forms hanging after multiple navigations
56
+ - Pattern: scroll → wait 100ms → interact
57
+ - Applied to both click and type tools
58
+
59
+ ## [3.3.1] - 2026-01-30
60
+
61
+ ### Added
62
+ - **click: All network requests visibility** — Shows ALL requests started within 200ms after click
63
+ - AI agent now sees complete network activity picture after every click
64
+ - Shows: method, URL, and status code for each request
65
+ - Example output:
66
+ ```
67
+ 📡 Network requests started within 200ms after click (15 total):
68
+ 1. ✓ GET /api/auth/ws_token/ → 200 OK
69
+ 2. ✓ GET /static/css/app.css → 200 OK
70
+ 3. ✓ POST /admin/tenant/.../change/ → 302 Found
71
+ ```
72
+ - Helps AI understand what happened after click (form submission, navigation, polling, etc.)
73
+ - 200ms window captures both mutation requests and side-effect requests
74
+
75
+ ### Changed
76
+ - **Enhanced diagnostics output** — Restructured to show all requests first, then mutation tracking
77
+ - Section 1: All requests in 200ms window (complete visibility)
78
+ - Section 2: Form submission status (POST/PATCH/PUT tracking)
79
+ - Clear icons: ✓ (success), ✗ (error), ⏳ (pending)
80
+ - Agent can immediately see if any requests occurred or not
81
+
82
+ ## [3.3.0] - 2026-01-30
83
+
84
+ ### Added
85
+ - **click: skipNetworkWait parameter** — Skip network wait for forms with long-polling/WebSockets
86
+ - New parameter: `skipNetworkWait: boolean` (default: false)
87
+ - Use case: Pages with continuous long-polling to get instant response
88
+ - Example: `click({ selector: 'button[type="submit"]', skipNetworkWait: true })`
89
+ - **click: networkWaitTimeout parameter** — Custom network wait timeout
90
+ - New parameter: `networkWaitTimeout: number` (default: 10000ms)
91
+ - Configurable per-click timeout for network requests
92
+ - Example: `click({ selector: '.save-btn', networkWaitTimeout: 5000 })`
93
+ - **type: timeout parameter** — Explicit timeout for type operations
94
+ - New parameter: `timeout: number` (default: 30000ms)
95
+ - Prevents infinite hangs on input fields
96
+ - Example: `type({ selector: 'input[name="url"]', text: 'value', timeout: 10000 })`
97
+
98
+ ### Changed
99
+ - **Smart form submission tracking** — Only tracks POST/PATCH/PUT requests in 100ms window
100
+ - Filters mutation requests (POST/PATCH/PUT) that started within 100ms after click
101
+ - Waits ONLY for these mutation requests (up to 10s)
102
+ - Ignores all other requests (GET, polling, analytics, etc.)
103
+ - Shows form submission status: `✓ POST /admin/tenant/.../change/ → 302 Found`
104
+ - Example: Django form with 50 polling requests → only 1 POST tracked
105
+ - **Type operation timeout protection** — Wrapped in Promise.race with configurable timeout
106
+ - Prevents 120s hangs on problematic input fields
107
+ - Returns clear error message: "Type operation timed out after Xms"
108
+ - Django forms: type now fails fast instead of hanging
109
+
110
+ ### Fixed
111
+ - **Django form timeout issues** — Fixed 30s click timeout and 120s type timeout
112
+ - Root cause: Long-polling/WebSockets created noise in network tracking
113
+ - Solution: Track only POST/PATCH/PUT requests within 100ms detection window
114
+ - Type operations now have explicit timeout protection
115
+ - Example: Django Admin forms now show reliable form submission status
116
+
117
+ ## [3.2.11] - 2026-01-30
118
+
119
+ ### Fixed
120
+ - **Tailwind CSS selector escape** — Fixed `analyzePage` failing on pages with Tailwind CSS
121
+ - Tailwind classes with colons (e.g., `hover:text-gray-800`) broke `querySelectorAll`
122
+ - Added filtering for classes containing special characters (`:`, `/`, `[`, `]`)
123
+ - Applied `CSS.escape()` to class names when building CSS selectors
124
+ - Fixed in: APOM tree builder, Angular tools, hints generator, content script
125
+ - Example: Page with Tailwind → no more "not a valid selector" errors
126
+
5
127
  ## [3.2.10] - 2026-01-29
6
128
 
7
129
  ### Fixed
package/README.md CHANGED
@@ -494,9 +494,11 @@ Click an element with optional result screenshot. **PREFERRED**: Use APOM ID fro
494
494
  - `waitAfter` (optional): Wait time in ms (default: 1500)
495
495
  - `screenshot` (optional): Capture screenshot (default: false for performance) ⚡
496
496
  - `timeout` (optional): Max operation time in ms (default: 30000)
497
- - **Use case**: Buttons, links, form submissions
498
- - **Returns**: Confirmation text + optional screenshot
499
- - **Performance**: 2-10x faster without screenshot
497
+ - `skipNetworkWait` (optional): Skip waiting for network requests (default: false). **Use for pages with continuous long-polling to get instant response.**
498
+ - `networkWaitTimeout` (optional): Custom network wait timeout in ms (default: 10000). Only used if skipNetworkWait is false.
499
+ - **Use case**: Buttons, links, form submissions, Django admin forms
500
+ - **Returns**: Confirmation text + optional screenshot + network diagnostics
501
+ - **Performance**: 2-10x faster without screenshot, instant with skipNetworkWait
500
502
  - **Example**:
501
503
  ```javascript
502
504
  // PREFERRED: Using APOM ID
@@ -504,6 +506,12 @@ Click an element with optional result screenshot. **PREFERRED**: Use APOM ID fro
504
506
 
505
507
  // Alternative: Using CSS selector
506
508
  click({ selector: "button[type='submit']" })
509
+
510
+ // Django forms with WebSockets (prevents timeout)
511
+ click({ selector: ".submit-row input[type='submit']", skipNetworkWait: true })
512
+
513
+ // Custom network timeout for slow APIs
514
+ click({ id: "save_btn", networkWaitTimeout: 10000 })
507
515
  ```
508
516
 
509
517
  #### type
@@ -513,9 +521,10 @@ Type text into input fields with optional clearing and typing delay. **PREFERRED
513
521
  - `selector` (optional): CSS selector. Use when APOM ID is not available.
514
522
  - ⚠️ Either `id` OR `selector` required (mutually exclusive)
515
523
  - `text` (required): Text to type
516
- - `delay` (optional): Delay between keystrokes in ms
524
+ - `delay` (optional): Delay between keystrokes in ms (default: 30)
517
525
  - `clearFirst` (optional): Clear field first (default: true)
518
- - **Use case**: Filling forms, search boxes, text inputs
526
+ - `timeout` (optional): Max operation time in ms (default: 30000). **Prevents infinite hangs on Django forms.**
527
+ - **Use case**: Filling forms, search boxes, text inputs, Django admin forms
519
528
  - **Returns**: Confirmation text
520
529
  - **Example**:
521
530
  ```javascript
package/angular-tools.js CHANGED
@@ -29,10 +29,16 @@ export async function listAngularComponents(page, includeHidden = false) {
29
29
 
30
30
  const component = ng.getComponent(el);
31
31
  if (component && component.constructor && component.constructor.name !== 'Object') {
32
- // Get selector
32
+ // Get selector (filter out Tailwind classes with special characters)
33
33
  const tagName = el.tagName.toLowerCase();
34
- const id = el.id ? `#${el.id}` : '';
35
- const classes = el.className ? `.${el.className.split(' ').join('.')}` : '';
34
+ const id = el.id ? `#${CSS.escape(el.id)}` : '';
35
+ const stableClasses = el.className
36
+ ? el.className.split(' ')
37
+ .filter(c => c && !/[:\/\[\]]/.test(c))
38
+ .slice(0, 3)
39
+ .map(c => CSS.escape(c))
40
+ : [];
41
+ const classes = stableClasses.length > 0 ? `.${stableClasses.join('.')}` : '';
36
42
  const selector = id || `${tagName}${classes}` || tagName;
37
43
 
38
44
  // Get methods (public only by default)
@@ -12,8 +12,17 @@
12
12
  */
13
13
 
14
14
  import WebSocket from 'ws';
15
+ import { appendFileSync } from 'fs';
16
+ import { homedir } from 'os';
17
+ import { join } from 'path';
15
18
 
16
19
  const BRIDGE_PORT = 9223;
20
+ const LOG_FILE = join(homedir(), 'chrometools-bridge.log');
21
+
22
+ function logToFile(message) {
23
+ const timestamp = new Date().toISOString();
24
+ appendFileSync(LOG_FILE, `${timestamp} ${message}\n`);
25
+ }
17
26
  const RECONNECT_DELAY = 2000;
18
27
  const MAX_RECONNECT_ATTEMPTS = 5;
19
28
 
@@ -64,19 +73,24 @@ export function setActiveTabSyncHandler(handler) {
64
73
  */
65
74
  export async function connectToBridge() {
66
75
  return new Promise((resolve) => {
76
+ logToFile(`connectToBridge called, isConnected=${isConnected}, wsState=${ws?.readyState}`);
77
+
67
78
  if (isConnected && ws?.readyState === WebSocket.OPEN) {
68
79
  debugLog('Already connected to Bridge');
80
+ logToFile('Already connected, skipping');
69
81
  resolve(true);
70
82
  return;
71
83
  }
72
84
 
73
85
  debugLog(`Connecting to Bridge on port ${BRIDGE_PORT}...`);
86
+ logToFile(`Attempting connection to ws://127.0.0.1:${BRIDGE_PORT}`);
74
87
 
75
88
  try {
76
89
  ws = new WebSocket(`ws://127.0.0.1:${BRIDGE_PORT}`);
77
90
 
78
91
  const connectTimeout = setTimeout(() => {
79
- debugLog('Connection timeout');
92
+ console.error('[chrometools-mcp] Bridge connection timeout (5s)');
93
+ logToFile('TIMEOUT: Connection timeout after 5s');
80
94
  ws?.close();
81
95
  resolve(false);
82
96
  }, 5000);
@@ -86,7 +100,8 @@ export async function connectToBridge() {
86
100
  isConnected = true;
87
101
  reconnectAttempts = 0;
88
102
  debugLog('Connected to Bridge');
89
- console.error('[chrometools-mcp] Connected to Bridge Service');
103
+ console.error('[chrometools-mcp] Connected to Bridge Service (isConnected=true)');
104
+ logToFile('SUCCESS: WebSocket connected, isConnected=true');
90
105
  resolve(true);
91
106
  });
92
107
 
@@ -99,8 +114,9 @@ export async function connectToBridge() {
99
114
  }
100
115
  });
101
116
 
102
- ws.on('close', () => {
103
- debugLog('Disconnected from Bridge');
117
+ ws.on('close', (code, reason) => {
118
+ console.error(`[chrometools-mcp] Bridge disconnected (code=${code}, reason=${reason || 'none'})`);
119
+ logToFile(`CLOSE: WebSocket closed, code=${code}, reason=${reason || 'none'}`);
104
120
  isConnected = false;
105
121
  ws = null;
106
122
  scheduleReconnect();
@@ -108,12 +124,14 @@ export async function connectToBridge() {
108
124
 
109
125
  ws.on('error', (error) => {
110
126
  clearTimeout(connectTimeout);
111
- debugLog('Connection error:', error.message);
127
+ console.error(`[chrometools-mcp] Bridge connection error: ${error.message}`);
128
+ logToFile(`ERROR: WebSocket error: ${error.message}`);
112
129
  resolve(false);
113
130
  });
114
131
 
115
132
  } catch (error) {
116
- debugLog('Failed to create WebSocket:', error.message);
133
+ console.error(`[chrometools-mcp] Failed to create WebSocket: ${error.message}`);
134
+ logToFile(`EXCEPTION: Failed to create WebSocket: ${error.message}`);
117
135
  resolve(false);
118
136
  }
119
137
  });
@@ -125,7 +143,7 @@ export async function connectToBridge() {
125
143
  function scheduleReconnect() {
126
144
  if (reconnectTimer) return;
127
145
  if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
128
- debugLog(`Max reconnect attempts (${MAX_RECONNECT_ATTEMPTS}) reached`);
146
+ console.error(`[chrometools-mcp] Bridge: max reconnect attempts (${MAX_RECONNECT_ATTEMPTS}) reached, giving up`);
129
147
  return;
130
148
  }
131
149
 
@@ -360,6 +378,7 @@ export function isExtensionConnected() {
360
378
  * Check if connected to Bridge
361
379
  */
362
380
  export function isBridgeConnected() {
381
+ logToFile(`isBridgeConnected called, returning ${isConnected}`);
363
382
  return isConnected;
364
383
  }
365
384
 
@@ -470,3 +489,39 @@ export function getRecorderState() {
470
489
  export function getRecordings() {
471
490
  return bridgeState.recordings;
472
491
  }
492
+
493
+ /**
494
+ * Get network requests from Bridge (tracked via Extension webRequest API)
495
+ * These persist across page navigations unlike CDP-based tracking
496
+ */
497
+ export async function getNetworkRequestsFromBridge(options = {}) {
498
+ if (!isConnected) {
499
+ debugLog('Cannot get network requests: not connected to Bridge');
500
+ return [];
501
+ }
502
+
503
+ try {
504
+ const response = await sendCommand('get_network_requests', {}, options.timeout || 5000);
505
+ return response.payload?.requests || [];
506
+ } catch (error) {
507
+ debugLog('Failed to get network requests:', error.message);
508
+ return [];
509
+ }
510
+ }
511
+
512
+ /**
513
+ * Clear network requests in Bridge
514
+ */
515
+ export async function clearNetworkRequestsFromBridge() {
516
+ if (!isConnected) {
517
+ return false;
518
+ }
519
+
520
+ try {
521
+ await sendCommand('clear_network_requests', {}, 5000);
522
+ return true;
523
+ } catch (error) {
524
+ debugLog('Failed to clear network requests:', error.message);
525
+ return false;
526
+ }
527
+ }
@@ -7,7 +7,7 @@
7
7
  *
8
8
  * - Launched by Chrome when Extension starts
9
9
  * - Maintains state (tabs, recordings, events)
10
- * - Accepts 0-8 WebSocket clients
10
+ * - Accepts up to 20 WebSocket clients (with dead connection cleanup)
11
11
  * - Clients get full state on connect + real-time updates
12
12
  */
13
13
 
@@ -24,7 +24,7 @@ const require = createRequire(import.meta.url);
24
24
  // ============================================
25
25
 
26
26
  const WS_PORT = 9223;
27
- const MAX_CLIENTS = 8;
27
+ const MAX_CLIENTS = 20; // Increased from 8 to handle multiple MCP restarts
28
28
  const HOST_NAME = 'com.chrometools.bridge';
29
29
 
30
30
  // ============================================
@@ -40,11 +40,13 @@ const state = {
40
40
  metadata: null,
41
41
  secrets: {}
42
42
  },
43
+ networkRequests: new Map(), // requestId -> request info (from Extension webRequest API)
43
44
  eventLog: [], // Ring buffer of recent events (max 1000)
44
45
  extensionConnected: false
45
46
  };
46
47
 
47
48
  const MAX_EVENT_LOG = 1000;
49
+ const MAX_NETWORK_REQUESTS = 500;
48
50
 
49
51
  // ============================================
50
52
  // WebSocket Server for Claude/MCP Clients
@@ -61,6 +63,14 @@ function startWebSocketServer() {
61
63
  });
62
64
 
63
65
  wss.on('connection', (ws) => {
66
+ // Clean up dead connections before checking limit
67
+ for (const client of clients) {
68
+ if (client.readyState !== 1) { // Not OPEN
69
+ clients.delete(client);
70
+ log(`Cleaned up dead client (readyState=${client.readyState})`);
71
+ }
72
+ }
73
+
64
74
  if (clients.size >= MAX_CLIENTS) {
65
75
  log(`Max clients (${MAX_CLIENTS}) reached, rejecting connection`);
66
76
  ws.close(1013, 'Max clients reached');
@@ -105,6 +115,7 @@ function getFullState() {
105
115
  tabs: Array.from(state.tabs.values()),
106
116
  recordings: state.recordings,
107
117
  recorderState: state.recorderState,
118
+ networkRequests: Array.from(state.networkRequests.values()),
108
119
  extensionConnected: state.extensionConnected,
109
120
  timestamp: Date.now()
110
121
  };
@@ -153,6 +164,25 @@ function handleClientMessage(ws, message) {
153
164
  });
154
165
  break;
155
166
 
167
+ case 'get_network_requests':
168
+ // Return network requests from state (collected via Extension webRequest API)
169
+ sendToClient(ws, {
170
+ type: 'network_requests',
171
+ payload: {
172
+ requests: Array.from(state.networkRequests.values())
173
+ },
174
+ requestId: message.requestId
175
+ });
176
+ break;
177
+
178
+ case 'clear_network_requests':
179
+ state.networkRequests.clear();
180
+ sendToClient(ws, {
181
+ type: 'network_requests_cleared',
182
+ requestId: message.requestId
183
+ });
184
+ break;
185
+
156
186
  // Commands to forward to extension
157
187
  case 'start_recording':
158
188
  case 'stop_recording':
@@ -295,6 +325,54 @@ function handleExtensionMessage(message) {
295
325
  broadcastToClients({ type: 'pong' });
296
326
  break;
297
327
 
328
+ // Network request events (from Extension webRequest API)
329
+ case 'network_request_started':
330
+ state.networkRequests.set(message.payload.requestId, message.payload);
331
+ // Clean old requests
332
+ if (state.networkRequests.size > MAX_NETWORK_REQUESTS) {
333
+ const entries = Array.from(state.networkRequests.entries());
334
+ const removeCount = entries.length - MAX_NETWORK_REQUESTS;
335
+ for (let i = 0; i < removeCount; i++) {
336
+ state.networkRequests.delete(entries[i][0]);
337
+ }
338
+ }
339
+ broadcastToClients({ type: 'network_request_started', payload: message.payload });
340
+ break;
341
+
342
+ case 'network_request_completed':
343
+ if (state.networkRequests.has(message.payload.requestId)) {
344
+ const req = state.networkRequests.get(message.payload.requestId);
345
+ req.status = message.payload.status;
346
+ req.statusText = message.payload.statusText;
347
+ req.completedAt = Date.now();
348
+ }
349
+ broadcastToClients({ type: 'network_request_completed', payload: message.payload });
350
+ break;
351
+
352
+ case 'network_request_failed':
353
+ if (state.networkRequests.has(message.payload.requestId)) {
354
+ const req = state.networkRequests.get(message.payload.requestId);
355
+ req.status = 'failed';
356
+ req.error = message.payload.error;
357
+ req.completedAt = Date.now();
358
+ }
359
+ broadcastToClients({ type: 'network_request_failed', payload: message.payload });
360
+ break;
361
+
362
+ case 'network_requests':
363
+ // Response from extension with all network requests
364
+ broadcastToClients({
365
+ type: 'network_requests',
366
+ payload: message.payload,
367
+ requestId: message.requestId
368
+ });
369
+ break;
370
+
371
+ case 'network_requests_cleared':
372
+ state.networkRequests.clear();
373
+ broadcastToClients({ type: 'network_requests_cleared' });
374
+ break;
375
+
298
376
  default:
299
377
  // Forward unknown messages to clients
300
378
  broadcastToClients(message);
@@ -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