chrometools-mcp 3.2.4 → 3.2.10

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,126 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [3.2.10] - 2026-01-29
6
+
7
+ ### Fixed
8
+ - **Network request deduplication** — Fixed duplicate pending requests in diagnostics
9
+ - Prevented same requestId from being added multiple times during redirects/retries
10
+ - Added deduplication check in Network.requestWillBeSent event handler
11
+ - Updates existing request instead of creating duplicate entry
12
+ - Example: example.com showed 4 pending (2 URLs × 2 duplicates) → now shows 2 pending (correct count)
13
+ - **Memory leak prevention** — Limited networkRequests array growth
14
+ - Keeps maximum 500 most recent network requests in memory
15
+ - Automatically removes oldest requests when limit exceeded
16
+ - Prevents unbounded memory growth during long browser sessions
17
+ - Example: After 100 page navigations, memory stays bounded
18
+
19
+ ## [3.2.9] - 2026-01-29
20
+
21
+ ### Added
22
+ - **navigateTo diagnostics** — Post-navigation diagnostics for navigateTo tool
23
+ - Detects chrome-error:// pages (unreachable servers, DNS failures)
24
+ - Waits 20s for slow page loads and network requests
25
+ - Reports JS console errors and network errors after navigation
26
+ - Shows pending requests if page loads slowly
27
+ - Same comprehensive diagnostics as click tool
28
+ - Example: Navigate to offline backend → instant error report instead of silent failure
29
+ - **openBrowser diagnostics** — Post-navigation diagnostics for openBrowser tool
30
+ - Same comprehensive diagnostics as navigateTo and click
31
+ - Critical for first action in session - shows errors immediately
32
+ - Detects chrome-error:// pages on initial load
33
+ - Reports network errors, console errors, pending requests
34
+ - Example: Open unreachable backend → instant error report with details
35
+
36
+ ### Changed
37
+ - **Diagnostics naming** — Renamed for clarity and universal use
38
+ - "POST-CLICK DIAGNOSTICS" → "POST-ACTION DIAGNOSTICS"
39
+ - Function parameters: beforeClickTimestamp → beforeActionTimestamp
40
+ - Comments updated to reflect use in both click and navigate actions
41
+ - File remains post-click-diagnostics.js for backward compatibility
42
+
43
+ ## [3.2.8] - 2026-01-29
44
+
45
+ ### Changed
46
+ - **Network wait timeout** — Increased from 5s to 20s for slow APIs
47
+ - Gives slow backend APIs time to complete before timeout
48
+ - AI gets complete success/error status instead of "pending unknown"
49
+ - Pending requests after 20s are reported with details (URL, method, runtime)
50
+ - Clear warning: "Status unknown - may complete successfully or fail"
51
+
52
+ ### Fixed
53
+ - **Click timeout on network errors** — No more 30s timeout when backend unreachable
54
+ - Detects chrome-error:// pages (ERR_CONNECTION_REFUSED, DNS_PROBE_FINISHED_NXDOMAIN, etc.)
55
+ - Returns error details immediately after 500ms diagnostic wait
56
+ - Shows error code and suggestion: "Backend likely not running or unreachable"
57
+ - Reduces diagnosis from 3 API calls to 1
58
+ - Example: Form submits to localhost:8001 (not running) → instant error report instead of 30s timeout
59
+ - **Network request tracking** — Now tracks ALL requests triggered by click, not just pending at 500ms
60
+ - Filters requests by timestamp (only those started AFTER click)
61
+ - Catches slow-starting requests that begin after initial 500ms wait
62
+ - Shows accurate count: completed/pending/total requests
63
+ - Prevents false "No network requests triggered" when requests start late
64
+ - **Delayed error collection** — Errors from requests that complete during maxWait are now captured
65
+ - Added 100ms delay after network wait before collecting errors
66
+ - Catches errors from requests that finish right as timeout expires
67
+ - Network summary shows: "⚠️ Network: 2 OK, 1 failed" when errors present
68
+ - Ensures AI sees errors even if request completes at edge of timeout window
69
+ - **Pending request reporting** — AI now sees details about slow/hanging requests
70
+ - Lists pending requests with URL, method, and elapsed time
71
+ - Suggests backend performance check or network connectivity issues
72
+ - Example: "POST /api/slow - Running for: 20145ms"
73
+
74
+ ## [3.2.7] - 2026-01-29
75
+
76
+ ### Added
77
+ - **Post-click diagnostics** — Click tool now automatically detects and reports errors
78
+ - Waits 500ms after click to capture async events
79
+ - Detects pending network requests and waits for completion (up to 5s)
80
+ - Collects JavaScript console errors and network errors
81
+ - **Error limit**: Max 15 console errors + 15 network errors to prevent spam
82
+ - Shows omitted error count if limit exceeded
83
+ - Returns diagnostics in click response for immediate AI feedback
84
+ - Prevents AI from making blind follow-up requests when errors occur
85
+ - New module: `utils/post-click-diagnostics.js`
86
+
87
+ ### Changed
88
+ - **Click behavior** — Enhanced UX for AI agents
89
+ - Click now includes network activity summary (requests completed, timing)
90
+ - Errors displayed immediately in click response
91
+ - AI can see what broke without additional tool calls
92
+ - Better error context: timestamp, location, status codes
93
+ - Smart error limiting prevents overwhelming AI with hundreds of errors
94
+
95
+ ## [3.2.6] - 2026-01-28
96
+
97
+ ### Removed
98
+ - **getAllInteractiveElements tool** — Removed redundant tool, fully replaced by analyzePage (54 → 53 tools)
99
+ - `analyzePage` provides superior functionality: hierarchical tree, element registration, APOM IDs, metadata
100
+ - `getAllInteractiveElements` only returned flat list with CSS selectors
101
+ - Affected files: `index.js`, `server/tool-definitions.js`, `server/tool-schemas.js`, `server/tool-groups.js`, `README.md`
102
+
103
+ ### Fixed
104
+ - **analyzePage visibility detection** — Fixed critical bug where analyzePage returned tree: null with interactiveCount: 0 on Angular Material pages
105
+ - Changed `isVisible()` check from `offsetParent` to `offsetWidth/offsetHeight > 0`
106
+ - Now correctly detects elements inside `position: fixed` containers (Angular Material overlays, dialogs, selects)
107
+ - Handles `position: sticky` elements properly
108
+ - Testing on my-autotests.segmento.ru: interactiveCount increased from 0 → 329 elements
109
+ - Affected file: `pom/apom-tree-converter.js`
110
+ - **type() text corruption** — Fixed text input corruption (duplicated/swapped characters)
111
+ - Changed default keystroke delay from 0ms to 30ms
112
+ - Prevents character corruption on fast-reacting inputs (Google Search, autocomplete fields)
113
+ - Example: "puppeteer automation" no longer becomes "ppuuppppeetteeeerr baruotwosmeart"
114
+ - Affected file: `index.js:454`
115
+
116
+ ## [3.2.5] - 2026-01-28
117
+
118
+ ### Fixed
119
+ - **CSS selector validation** — Fixed analyzePage crash when elements have numeric IDs
120
+ - Added validation to skip IDs starting with digits (e.g., `id="301178"`)
121
+ - CSS selectors don't support IDs starting with numbers (per CSS specification)
122
+ - Added try-catch for invalid selector edge cases
123
+ - Affected file: `pom/apom-tree-converter.js`
124
+
5
125
  ## [3.2.4] - 2026-01-27
6
126
 
7
127
  ### Performance
package/README.md CHANGED
@@ -1,6 +1,29 @@
1
1
  # chrometools-mcp
2
2
 
3
- MCP server for Chrome automation using Puppeteer with persistent browser sessions.
3
+ > 🌐 [Русская версия README](./README.ru.md)
4
+
5
+ **AI-powered Chrome automation through natural language.** No more fighting with CSS selectors, XPath expressions, or brittle test scripts. Just tell your AI assistant what you want to do on a web page, and ChromeTools MCP makes it happen.
6
+
7
+ ## Why ChromeTools MCP?
8
+
9
+ **For AI Agents & Developers:**
10
+ - 🎯 **54 specialized tools** for browser automation - from simple clicks to Figma comparisons
11
+ - 🧠 **APOM (Agent Page Object Model)** - AI-friendly page representation (~8-10k tokens vs 15-25k for screenshots)
12
+ - 🔄 **Persistent browser sessions** - pages stay open between commands for iterative workflows
13
+ - ⚡ **Framework-aware** - handles React, Vue, Angular events and state updates automatically
14
+ - 📸 **Visual testing** - compare designs pixel-by-pixel with Figma integration
15
+ - 🎬 **Scenario recording** - record browser actions, replay them, or export as Playwright/Selenium tests
16
+ - 🌍 **Cross-platform** - works seamlessly on Windows, WSL, Linux, and macOS
17
+
18
+ **Perfect for:**
19
+ - 🤖 Building AI agents that interact with web applications
20
+ - 🧪 Automated testing without writing code - let AI generate tests from scenarios
21
+ - 🔍 Web scraping and data extraction with natural language instructions
22
+ - 🎨 Design validation - compare implemented UI with Figma designs
23
+ - 🚀 Rapid prototyping - test user flows by describing them to AI
24
+ - 📊 Monitoring and health checks for web applications
25
+
26
+ Stop writing brittle automation scripts. Start describing what you want in plain English.
4
27
 
5
28
  ## Installation
6
29
 
@@ -152,7 +175,7 @@ The Chrome Extension is **required** for scenario recording and other advanced f
152
175
  **Step 3:** Download and Extract the Extension
153
176
 
154
177
  **Option A - Download from GitHub (Recommended):**
155
- 1. Download the extension archive: [chrome-extension.zip](https://github.com/modelcontextprotocol/servers/raw/main/src/chrometools/chrome-extension.zip)
178
+ 1. Download the extension archive: [chrome-extension.zip](https://github.com/docentovich/chrometools-mcp/raw/main/chrome-extension.zip)
156
179
  2. Extract the ZIP file to a folder on your computer
157
180
  3. Remember the extraction path (you'll need it in the next step)
158
181
 
@@ -197,7 +220,7 @@ The Chrome Extension is **required** for scenario recording and other advanced f
197
220
  - [Chrome Extension Setup](#chrome-extension-setup)
198
221
  - [AI Optimization Features](#ai-optimization-features)- [Scenario Recorder](#scenario-recorder) - Visual UI-based recording with smart optimization
199
222
  - [Available Tools](#available-tools) - **46+ Tools Total**
200
- - [AI-Powered Tools](#ai-powered-tools) - smartFindElement, analyzePage, getElementDetails, getAllInteractiveElements, findElementsByText
223
+ - [AI-Powered Tools](#ai-powered-tools) - smartFindElement, analyzePage, getElementDetails, findElementsByText
201
224
  - [Core Tools](#1-core-tools) - ping, openBrowser
202
225
  - [Interaction Tools](#2-interaction-tools) - click, type, scrollTo, selectOption, selectFromGroup, drag, scrollHorizontal
203
226
  - [Inspection Tools](#3-inspection-tools) - getElement, getComputedCss, getBoxModel, screenshot
@@ -238,7 +261,7 @@ AI: smartFindElement("login button")
238
261
  1. **`analyzePage`** - 🔥 **USE FREQUENTLY** - Get current page state after loads, clicks, submissions (cached, use refresh:true)
239
262
  2. **`smartFindElement`** - Natural language element search with multilingual support
240
263
  3. **AI Hints** - Automatic context in all tools (page type, available actions, suggestions)
241
- 4. **Batch helpers** - `getAllInteractiveElements`, `findElementsByText`
264
+ 4. **Text search** - `findElementsByText` for finding elements by visible text
242
265
 
243
266
  **Performance:** 3-5x faster, 5-10x fewer requests
244
267
 
@@ -438,12 +461,6 @@ executeScenario({ name: "login_flow", parameters: { email: "user@test.com" } })
438
461
  getElementDetails({ id: "container_123", analyzeChildren: true, refresh: true }) // Analyze modal contents with children tree
439
462
  ```
440
463
 
441
- #### getAllInteractiveElements
442
- Get all clickable/fillable elements with their selectors.
443
- - **Parameters**:
444
- - `includeHidden` (optional): Include hidden elements (default: false)
445
- - **Returns**: Array of all interactive elements with selectors and metadata
446
-
447
464
  #### findElementsByText
448
465
  Find elements by their visible text content.
449
466
  - **Parameters**:
@@ -1431,11 +1448,11 @@ Each tool definition is sent to the AI in every request, consuming context token
1431
1448
  | `interaction` | User interaction | `click`, `type`, `scrollTo`, `waitForElement`, `hover` (5) |
1432
1449
  | `inspection` | Page inspection | `getComputedCss`, `getBoxModel`, `screenshot`, `saveScreenshot` (4) |
1433
1450
  | `debug` | Debugging & network | `getConsoleLogs`, `listNetworkRequests`, `getNetworkRequest`, `filterNetworkRequests` (4) |
1434
- | `advanced` | Advanced automation & AI | `executeScript`, `setStyles`, `setViewport`, `getViewport`, `navigateTo`, `smartFindElement`, `analyzePage`, `getAllInteractiveElements`, `findElementsByText` (9) |
1451
+ | `advanced` | Advanced automation & AI | `executeScript`, `setStyles`, `setViewport`, `getViewport`, `navigateTo`, `smartFindElement`, `analyzePage`, `findElementsByText` (8) |
1435
1452
  | `recorder` | Scenario recording | `enableRecorder`, `executeScenario`, `listScenarios`, `searchScenarios`, `getScenarioInfo`, `deleteScenario`, `exportScenarioAsCode`, `appendScenarioToFile`, `generatePageObject` (9) |
1436
1453
  | `figma` | Figma integration | `getFigmaFrame`, `compareFigmaToElement`, `getFigmaSpecs`, `parseFigmaUrl`, `listFigmaPages`, `searchFigmaFrames`, `getFigmaComponents`, `getFigmaStyles`, `getFigmaColorPalette`, `convertFigmaToCode` (10) |
1437
1454
 
1438
- **Total:** 43 tools across 7 groups
1455
+ **Total:** 42 tools across 7 groups
1439
1456
 
1440
1457
  **Configuration:**
1441
1458
 
@@ -1603,7 +1620,7 @@ npx @modelcontextprotocol/inspector node index.js
1603
1620
  - Interaction: click, type, scrollTo, selectOption, selectFromGroup, drag, scrollHorizontal
1604
1621
  - Inspection: getElement, getComputedCss, getBoxModel, screenshot, saveScreenshot
1605
1622
  - Advanced: executeScript, getConsoleLogs, listNetworkRequests, getNetworkRequest, filterNetworkRequests, hover, setStyles, setViewport, getViewport, navigateTo, waitForElement
1606
- - AI-Powered: smartFindElement, analyzePage, getElementDetails (with children analysis), getAllInteractiveElements, findElementsByText - Recorder: enableRecorder, executeScenario, listScenarios, searchScenarios, getScenarioInfo, deleteScenario, exportScenarioAsCode, appendScenarioToFile, generatePageObject
1623
+ - AI-Powered: smartFindElement, analyzePage, getElementDetails (with children analysis), findElementsByText - Recorder: enableRecorder, executeScenario, listScenarios, searchScenarios, getScenarioInfo, deleteScenario, exportScenarioAsCode, appendScenarioToFile, generatePageObject
1607
1624
  - Figma: getFigmaFrame, compareFigmaToElement, getFigmaSpecs, parseFigmaUrl, listFigmaPages, searchFigmaFrames, getFigmaComponents, getFigmaStyles, getFigmaColorPalette, convertFigmaToCode
1608
1625
  - **UI Framework Detection**: Automatic detection of MUI, Ant Design, Chakra UI, Bootstrap, Vuetify, Semantic UI- **Smart Dropdown Handling**: Extracts options from both native `<select>` and custom UI framework components- **APOM (Agent Page Object Model)**: Automatic element ID assignment for reliable interaction - `analyzePage()` returns elements with unique IDs (e.g., `input_20`, `button_45`)
1609
1626
  - Use `id` parameter in click/type/hover/selectOption for stable targeting
package/README.ru.md CHANGED
@@ -1,8 +1,29 @@
1
1
  # chrometools-mcp
2
2
 
3
- MCP сервер для автоматизации Chrome с использованием Puppeteer и постоянными сессиями браузера.
3
+ > 🌐 [English version](./README.md)
4
4
 
5
- [English version](README.md)
5
+ **Автоматизация Chrome через естественный язык для ИИ.** Забудьте о борьбе с CSS селекторами, XPath выражениями и хрупкими тестовыми скриптами. Просто скажите своему ИИ-помощнику, что вы хотите сделать на веб-странице, и ChromeTools MCP сделает это.
6
+
7
+ ## Зачем нужен ChromeTools MCP?
8
+
9
+ **Для ИИ-агентов и разработчиков:**
10
+ - 🎯 **54 специализированных инструмента** для автоматизации браузера — от простых кликов до сравнения с Figma
11
+ - 🧠 **APOM (Agent Page Object Model)** — представление страницы для ИИ (~8-10k токенов против 15-25k для скриншотов)
12
+ - 🔄 **Постоянные сессии браузера** — страницы остаются открытыми между командами для итеративной работы
13
+ - ⚡ **Поддержка фреймворков** — автоматически обрабатывает события и состояние React, Vue, Angular
14
+ - 📸 **Визуальное тестирование** — попиксельное сравнение дизайна с макетами Figma
15
+ - 🎬 **Запись сценариев** — записывайте действия в браузере, воспроизводите их или экспортируйте в Playwright/Selenium
16
+ - 🌍 **Кросс-платформенность** — работает на Windows, WSL, Linux и macOS
17
+
18
+ **Идеально для:**
19
+ - 🤖 Создания ИИ-агентов, взаимодействующих с веб-приложениями
20
+ - 🧪 Автоматизированного тестирования без написания кода — пусть ИИ генерирует тесты из сценариев
21
+ - 🔍 Парсинга веб-страниц и извлечения данных с помощью естественного языка
22
+ - 🎨 Валидации дизайна — сравнение реализованного UI с дизайном в Figma
23
+ - 🚀 Быстрого прототипирования — тестирование пользовательских сценариев через их описание
24
+ - 📊 Мониторинга и проверки работоспособности веб-приложений
25
+
26
+ Перестаньте писать хрупкие скрипты автоматизации. Начните описывать желаемое на обычном языке.
6
27
 
7
28
  ## Установка
8
29
 
@@ -91,7 +112,7 @@ npx chrometools-mcp
91
112
  **Шаг 3:** Скачайте и распакуйте расширение
92
113
 
93
114
  **Вариант A - Скачать с GitHub (Рекомендуется):**
94
- 1. Скачайте архив расширения: [chrome-extension.zip](https://github.com/modelcontextprotocol/servers/raw/main/src/chrometools/chrome-extension.zip)
115
+ 1. Скачайте архив расширения: [chrome-extension.zip](https://github.com/docentovich/chrometools-mcp/raw/main/chrome-extension.zip)
95
116
  2. Распакуйте ZIP файл в папку на вашем компьютере
96
117
  3. Запомните путь распаковки (он понадобится на следующем шаге)
97
118
 
@@ -30,6 +30,21 @@ export const consoleLogs = [];
30
30
  // Network requests storage
31
31
  export const networkRequests = [];
32
32
 
33
+ // Maximum number of network requests to keep in memory (prevent unbounded growth)
34
+ const MAX_NETWORK_REQUESTS = 500;
35
+
36
+ /**
37
+ * Clean old network requests to prevent memory leak
38
+ * Keeps only the most recent MAX_NETWORK_REQUESTS requests
39
+ */
40
+ function cleanOldNetworkRequests() {
41
+ if (networkRequests.length > MAX_NETWORK_REQUESTS) {
42
+ // Remove oldest requests (keep most recent)
43
+ const removeCount = networkRequests.length - MAX_NETWORK_REQUESTS;
44
+ networkRequests.splice(0, removeCount);
45
+ }
46
+ }
47
+
33
48
  // Page analysis cache
34
49
  export const pageAnalysisCache = new Map();
35
50
 
@@ -52,6 +67,19 @@ export async function setupNetworkMonitoring(page) {
52
67
 
53
68
  client.on('Network.requestWillBeSent', (event) => {
54
69
  const timestamp = new Date().toISOString();
70
+
71
+ // Check if request already exists (prevent duplicates from redirects/retries)
72
+ const existingReq = networkRequests.find(r => r.requestId === event.requestId);
73
+ if (existingReq) {
74
+ // Update existing request instead of creating duplicate
75
+ existingReq.url = event.request.url;
76
+ existingReq.method = event.request.method;
77
+ existingReq.headers = event.request.headers;
78
+ existingReq.postData = event.request.postData;
79
+ existingReq.timestamp = timestamp;
80
+ return;
81
+ }
82
+
55
83
  networkRequests.push({
56
84
  requestId: event.requestId,
57
85
  url: event.request.url,
@@ -64,6 +92,9 @@ export async function setupNetworkMonitoring(page) {
64
92
  status: 'pending',
65
93
  documentURL: event.documentURL
66
94
  });
95
+
96
+ // Clean old requests to prevent unbounded memory growth
97
+ cleanOldNetworkRequests();
67
98
  });
68
99
 
69
100
  client.on('Network.responseReceived', (event) => {
package/index.js CHANGED
@@ -54,6 +54,8 @@ import {getToolsFromGroups, getAllGroupNames} from './server/tool-groups.js';
54
54
  import {executeElementAction} from './utils/element-actions.js';
55
55
  // Import hints generator
56
56
  import {generateClickHints, generateNavigationHints} from './utils/hints-generator.js';
57
+ // Import post-click diagnostics
58
+ import {runPostClickDiagnostics, formatDiagnosticsForAI} from './utils/post-click-diagnostics.js';
57
59
 
58
60
  // Import Recorder modules
59
61
  // Note: injectRecorder removed - now using Chrome Extension
@@ -313,12 +315,22 @@ async function executeToolInternal(name, args) {
313
315
 
314
316
  if (name === "openBrowser") {
315
317
  const validatedArgs = schemas.OpenBrowserSchema.parse(args);
318
+
319
+ // Capture timestamp BEFORE opening for diagnostics
320
+ const beforeOpenTimestamp = Date.now();
321
+
316
322
  const page = await getOrCreatePage(validatedArgs.url);
317
323
  const title = await page.title();
318
324
 
325
+ // Run post-navigation diagnostics (same as navigateTo)
326
+ const diagnostics = await runPostClickDiagnostics(page, beforeOpenTimestamp);
327
+
319
328
  // Generate AI hints
320
329
  const hints = await generateNavigationHints(page, validatedArgs.url);
321
330
 
331
+ // Format diagnostics for output
332
+ const diagnosticsText = formatDiagnosticsForAI(diagnostics);
333
+
322
334
  // Check if extension is connected
323
335
  const extensionConnected = isExtensionConnected();
324
336
  const usedExistingChrome = isConnectedToExistingChrome();
@@ -329,11 +341,20 @@ async function executeToolInternal(name, args) {
329
341
  extensionNote = `\n\n⚠️ EXTENSION NOT CONNECTED\nConnected to existing Chrome - extension needs manual installation.\n${instructions.installSteps.join('\n')}\n\nAlternative: ${instructions.alternativeFix}`;
330
342
  }
331
343
 
344
+ let hintsText = '\n\n** AI HINTS **';
345
+ hintsText += `\nPage type: ${hints.pageType}`;
346
+ if (hints.availableActions.length > 0) {
347
+ hintsText += `\nAvailable actions: ${hints.availableActions.join(', ')}`;
348
+ }
349
+ if (hints.suggestedNext.length > 0) {
350
+ hintsText += `\nSuggested next: ${hints.suggestedNext.join('; ')}`;
351
+ }
352
+
332
353
  return {
333
354
  content: [
334
355
  {
335
356
  type: "text",
336
- text: `Browser opened successfully!\nURL: ${validatedArgs.url}\nPage title: ${title}\n\nBrowser remains open for interaction.\n\n** AI HINTS **\nPage type: ${hints.pageType}\nAvailable actions: ${hints.availableActions.join(', ')}\nSuggested next: ${hints.suggestedNext.join('; ')}${extensionNote}`,
357
+ text: `Browser opened successfully!\nURL: ${validatedArgs.url}\nPage title: ${title}\n\nBrowser remains open for interaction.${hintsText}${diagnosticsText}${extensionNote}`,
337
358
  },
338
359
  ],
339
360
  };
@@ -380,6 +401,9 @@ async function executeToolInternal(name, args) {
380
401
  throw new Error(`Element not found: ${identifier}`);
381
402
  }
382
403
 
404
+ // Capture timestamp BEFORE click for error filtering
405
+ const beforeClickTimestamp = Date.now();
406
+
383
407
  // Try multiple click methods for better reliability
384
408
  try {
385
409
  // Method 1: Puppeteer click (most reliable for most cases)
@@ -395,11 +419,15 @@ async function executeToolInternal(name, args) {
395
419
  await element.evaluate(el => el.click());
396
420
  }
397
421
  }
398
- await new Promise(resolve => setTimeout(resolve, validatedArgs.waitAfter || 1500));
399
422
 
400
- // Generate AI hints after click
423
+ // NEW POST-CLICK PATTERN:
424
+ // 1. Run post-click diagnostics (waits 500ms, checks pending requests, collects errors)
425
+ const diagnostics = await runPostClickDiagnostics(page, beforeClickTimestamp);
426
+
427
+ // 2. Generate AI hints after click
401
428
  const hints = await generateClickHints(page, identifier);
402
429
 
430
+ // 3. Format output with hints and diagnostics
403
431
  let hintsText = '\n\n** AI HINTS **';
404
432
  if (hints.modalOpened) hintsText += '\nModal opened - interact with it or close';
405
433
  if (hints.newElements.length > 0) {
@@ -409,8 +437,11 @@ async function executeToolInternal(name, args) {
409
437
  hintsText += `\nSuggested next: ${hints.suggestedNext.join('; ')}`;
410
438
  }
411
439
 
440
+ // 4. Add diagnostics to output
441
+ const diagnosticsText = formatDiagnosticsForAI(diagnostics);
442
+
412
443
  const content = [
413
- { type: "text", text: `Clicked: ${identifier}${hintsText}` }
444
+ { type: "text", text: `Clicked: ${identifier}${hintsText}${diagnosticsText}` }
414
445
  ];
415
446
 
416
447
  // Only add screenshot if requested
@@ -451,7 +482,7 @@ async function executeToolInternal(name, args) {
451
482
  // Use input model to handle the element appropriately
452
483
  const model = await getInputModel(element, page);
453
484
  const options = {
454
- delay: validatedArgs.delay || 0,
485
+ delay: validatedArgs.delay !== undefined ? validatedArgs.delay : 30,
455
486
  clearFirst: validatedArgs.clearFirst !== undefined ? validatedArgs.clearFirst : true,
456
487
  };
457
488
 
@@ -1343,6 +1374,9 @@ async function executeToolInternal(name, args) {
1343
1374
  browserOpened = true;
1344
1375
  }
1345
1376
 
1377
+ // Capture timestamp BEFORE navigation for diagnostics
1378
+ const beforeNavTimestamp = Date.now();
1379
+
1346
1380
  // Navigate to the new URL (skip if we just created page with this URL)
1347
1381
  if (!browserOpened) {
1348
1382
  await page.goto(validatedArgs.url, { waitUntil: validatedArgs.waitUntil || 'networkidle2' });
@@ -1350,17 +1384,32 @@ async function executeToolInternal(name, args) {
1350
1384
 
1351
1385
  const title = await page.title();
1352
1386
 
1387
+ // Run post-navigation diagnostics (same as post-click)
1388
+ const diagnostics = await runPostClickDiagnostics(page, beforeNavTimestamp);
1389
+
1353
1390
  // Generate AI hints
1354
1391
  const hints = await generateNavigationHints(page, validatedArgs.url);
1355
1392
 
1393
+ // Format diagnostics for output
1394
+ const diagnosticsText = formatDiagnosticsForAI(diagnostics);
1395
+
1356
1396
  const message = browserOpened
1357
1397
  ? `Browser opened and navigated to: ${validatedArgs.url}`
1358
1398
  : `Navigated to: ${validatedArgs.url}`;
1359
1399
 
1400
+ let hintsText = '\n\n** AI HINTS **';
1401
+ hintsText += `\nPage type: ${hints.pageType}`;
1402
+ if (hints.availableActions.length > 0) {
1403
+ hintsText += `\nAvailable actions: ${hints.availableActions.join(', ')}`;
1404
+ }
1405
+ if (hints.suggestedNext.length > 0) {
1406
+ hintsText += `\nSuggested next: ${hints.suggestedNext.join('; ')}`;
1407
+ }
1408
+
1360
1409
  return {
1361
1410
  content: [{
1362
1411
  type: "text",
1363
- text: `${message}\nPage title: ${title}\n\n** AI HINTS **\nPage type: ${hints.pageType}\nAvailable actions: ${hints.availableActions.join(', ')}\nSuggested next: ${hints.suggestedNext.join('; ')}`
1412
+ text: `${message}\nPage title: ${title}${hintsText}${diagnosticsText}`
1364
1413
  }],
1365
1414
  };
1366
1415
  }
@@ -2250,54 +2299,6 @@ Start coding now.`;
2250
2299
  };
2251
2300
  }
2252
2301
 
2253
- if (name === "getAllInteractiveElements") {
2254
- const validatedArgs = schemas.GetAllInteractiveElementsSchema.parse(args);
2255
- const page = await getLastOpenPage();
2256
-
2257
- const elements = await page.evaluate((includeHidden, utilsCode) => {
2258
- eval(utilsCode);
2259
-
2260
- const results = [];
2261
- const selector = 'button, a[href], input, select, textarea, [onclick], [role="button"], [tabindex]:not([tabindex="-1"])';
2262
-
2263
- document.querySelectorAll(selector).forEach(el => {
2264
- const isVisible = el.offsetWidth > 0 && el.offsetHeight > 0;
2265
-
2266
- if (!includeHidden && !isVisible) return;
2267
-
2268
- const text = (el.textContent || el.value || el.getAttribute('aria-label') || el.placeholder || '').trim();
2269
-
2270
- results.push({
2271
- selector: getUniqueSelectorInPage(el),
2272
- type: el.tagName.toLowerCase(),
2273
- text: text.substring(0, 100),
2274
- visible: isVisible,
2275
- attributes: {
2276
- id: el.id || null,
2277
- class: el.className || null,
2278
- role: el.getAttribute('role') || null,
2279
- type: el.type || null,
2280
- }
2281
- });
2282
- });
2283
-
2284
- return results;
2285
- }, validatedArgs.includeHidden || false, elementFinderUtils);
2286
-
2287
- return {
2288
- content: [{
2289
- type: 'text',
2290
- text: JSON.stringify({
2291
- count: elements.length,
2292
- elements,
2293
- hints: {
2294
- suggestion: 'Use these selectors directly with click, type, or other tools'
2295
- }
2296
- }, null, 2)
2297
- }]
2298
- };
2299
- }
2300
-
2301
2302
  if (name === "findElementsByText") {
2302
2303
  const validatedArgs = schemas.FindElementsByTextSchema.parse(args);
2303
2304
  const page = await getLastOpenPage();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chrometools-mcp",
3
- "version": "3.2.4",
3
+ "version": "3.2.10",
4
4
  "description": "MCP (Model Context Protocol) server for Chrome automation using Puppeteer. Persistent browser sessions, UI framework detection (MUI, Ant Design, etc.), Page Object support, visual testing, Figma comparison. Works seamlessly in WSL, Linux, macOS, and Windows.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -231,13 +231,26 @@ function buildAPOMTree(interactiveOnly = true) {
231
231
 
232
232
  /**
233
233
  * Check if element is visible
234
+ * More reliable check that works with position:fixed elements (Angular Material, etc.)
234
235
  */
235
236
  function isVisible(el) {
236
- if (!el.offsetParent && el !== document.body) return false;
237
+ // Check dimensions first (works for fixed position elements)
238
+ if (el.offsetWidth === 0 || el.offsetHeight === 0) return false;
239
+
240
+ // Check computed styles
237
241
  const style = window.getComputedStyle(el);
238
- return style.display !== 'none' &&
239
- style.visibility !== 'hidden' &&
240
- style.opacity !== '0';
242
+ if (style.display === 'none' ||
243
+ style.visibility === 'hidden' ||
244
+ style.opacity === '0') {
245
+ return false;
246
+ }
247
+
248
+ // For body element, always consider visible if dimensions > 0
249
+ if (el === document.body) return true;
250
+
251
+ // Additional check: element should be in viewport or have offsetParent
252
+ // This handles elements inside position:fixed containers (Angular Material)
253
+ return el.offsetParent !== null || style.position === 'fixed' || style.position === 'sticky';
241
254
  }
242
255
 
243
256
  /**
@@ -682,9 +695,17 @@ function buildAPOMTree(interactiveOnly = true) {
682
695
  * Excludes framework-specific dynamic attributes (React, Vue, Angular)
683
696
  */
684
697
  function generateSelector(element) {
685
- // Use ID if available and unique
686
- if (element.id && document.querySelectorAll(`#${element.id}`).length === 1) {
687
- return `#${element.id}`;
698
+ // Use ID if available, valid (not starting with digit), and unique
699
+ // CSS selectors don't support IDs starting with digits (e.g., #301178 is invalid)
700
+ if (element.id && !/^[0-9]/.test(element.id)) {
701
+ try {
702
+ const selector = `#${CSS.escape(element.id)}`;
703
+ if (document.querySelectorAll(selector).length === 1) {
704
+ return selector;
705
+ }
706
+ } catch (e) {
707
+ // Invalid selector, continue to other strategies
708
+ }
688
709
  }
689
710
 
690
711
  // Try to find stable class name (excluding framework-specific dynamic classes)
@@ -49,7 +49,7 @@ export const toolDefinitions = [
49
49
  id: { type: "string", description: "APOM element ID from analyzePage (e.g., 'input_20'). Either id or selector required." },
50
50
  selector: { type: "string", description: "CSS selector (e.g., '#email'). Either id or selector required." },
51
51
  text: { type: "string", description: "Text to type" },
52
- delay: { type: "number", description: "Keystroke delay ms (default: 0)" },
52
+ delay: { type: "number", description: "Keystroke delay ms (default: 30)" },
53
53
  clearFirst: { type: "boolean", description: "Clear first (default: true)" },
54
54
  },
55
55
  required: ["text"],
@@ -503,16 +503,6 @@ export const toolDefinitions = [
503
503
  required: ["id"],
504
504
  },
505
505
  },
506
- {
507
- name: "getAllInteractiveElements",
508
- description: "Get all interactive elements with selectors. For understanding available actions.",
509
- inputSchema: {
510
- type: "object",
511
- properties: {
512
- includeHidden: { type: "boolean", description: "Include hidden (default: false)" },
513
- },
514
- },
515
- },
516
506
  {
517
507
  name: "findElementsByText",
518
508
  description: "Find elements by visible text content and get their selectors. Use this INSTEAD of executeScript when you need to find elements. Returns working selectors that can be used with click/type tools. Can optionally perform actions directly.",
@@ -24,7 +24,6 @@ export const toolGroups = {
24
24
  'getViewport',
25
25
  'smartFindElement',
26
26
  'analyzePage',
27
- 'getAllInteractiveElements',
28
27
  'findElementsByText'
29
28
  ],
30
29
 
@@ -29,7 +29,7 @@ export const TypeSchema = z.object({
29
29
  id: z.string().optional().describe("APOM element ID from analyzePage (e.g., 'input_20', 'input_33'). Mutually exclusive with selector."),
30
30
  selector: z.string().optional().describe("CSS selector for input element. Mutually exclusive with id."),
31
31
  text: z.string().describe("Text to type"),
32
- delay: z.number().optional().describe("Delay between keystrokes in ms (default: 0)"),
32
+ delay: z.number().optional().describe("Delay between keystrokes in ms (default: 30)"),
33
33
  clearFirst: z.boolean().optional().describe("Clear field before typing (default: true)"),
34
34
  }).refine(data => (data.id && !data.selector) || (!data.id && data.selector), {
35
35
  message: "Either 'id' or 'selector' must be provided, but not both"
@@ -269,10 +269,6 @@ export const GetElementDetailsSchema = z.object({
269
269
  refresh: z.boolean().optional().describe("Force refresh of cached analysis (default: false)"),
270
270
  });
271
271
 
272
- export const GetAllInteractiveElementsSchema = z.object({
273
- includeHidden: z.boolean().optional().describe("Include hidden elements (default: false)"),
274
- });
275
-
276
272
  export const FindElementsByTextSchema = z.object({
277
273
  text: z.string().describe("Text to search for in elements"),
278
274
  exact: z.boolean().optional().describe("Exact match only (default: false)"),
@@ -0,0 +1,277 @@
1
+ /**
2
+ * Post-Action Diagnostics
3
+ * Collects errors and waits for network requests after user actions (click, navigation, etc.)
4
+ */
5
+
6
+ import { consoleLogs, networkRequests } from '../browser/page-manager.js';
7
+
8
+ /**
9
+ * Wait for pending network requests to complete
10
+ * @param {number} beforeActionTimestamp - Timestamp before action to track new requests
11
+ * @param {number} initialWaitMs - Initial wait time before checking (default: 500ms)
12
+ * @param {number} maxWaitMs - Maximum time to wait for requests (default: 5000ms)
13
+ * @returns {Promise<{pendingFound: boolean, waitedMs: number, completedRequests: number, totalRequests: number}>}
14
+ */
15
+ export async function waitForPendingRequests(beforeActionTimestamp, initialWaitMs = 500, maxWaitMs = 5000) {
16
+ const startTime = Date.now();
17
+
18
+ // Step 1: Wait initial period to let requests start
19
+ await new Promise(resolve => setTimeout(resolve, initialWaitMs));
20
+
21
+ // Step 2: Get requests that started AFTER action
22
+ const getPostActionRequests = () => {
23
+ const cutoffDate = new Date(beforeActionTimestamp).toISOString();
24
+ return networkRequests.filter(req => req.timestamp >= cutoffDate);
25
+ };
26
+
27
+ // Step 3: Check for pending requests (from post-action requests)
28
+ const checkPending = () => {
29
+ return getPostActionRequests().filter(req => req.status === 'pending');
30
+ };
31
+
32
+ let pending = checkPending();
33
+ let allPostActionRequests = getPostActionRequests();
34
+ const initialPendingCount = pending.length;
35
+
36
+ // Step 4: If there are pending requests OR new requests appeared, wait for completion
37
+ if (pending.length > 0 || allPostActionRequests.length > 0) {
38
+ // Wait for pending requests to complete (with timeout)
39
+ while (pending.length > 0 && (Date.now() - startTime) < maxWaitMs) {
40
+ await new Promise(resolve => setTimeout(resolve, 100)); // Check every 100ms
41
+ pending = checkPending();
42
+ allPostActionRequests = getPostActionRequests(); // Update total count
43
+ }
44
+ }
45
+
46
+ const finalRequests = getPostActionRequests();
47
+ const completedRequests = finalRequests.filter(req => req.status === 'completed' || (typeof req.status === 'number'));
48
+ const pendingRequests = pending.map(req => ({
49
+ url: req.url,
50
+ method: req.method,
51
+ timestamp: req.timestamp
52
+ }));
53
+
54
+ return {
55
+ pendingFound: initialPendingCount > 0,
56
+ waitedMs: Date.now() - startTime,
57
+ completedRequests: completedRequests.length,
58
+ stillPending: pending.length,
59
+ pendingRequests: pendingRequests,
60
+ totalRequests: finalRequests.length
61
+ };
62
+ }
63
+
64
+ /**
65
+ * Collect errors from console logs and network requests
66
+ * @param {number} sinceTimestamp - Only collect errors after this timestamp (default: collect recent errors)
67
+ * @param {number} maxConsoleErrors - Maximum console errors to return (default: 15)
68
+ * @param {number} maxNetworkErrors - Maximum network errors to return (default: 15)
69
+ * @returns {Object} Object with consoleErrors and networkErrors arrays
70
+ */
71
+ export function collectErrors(sinceTimestamp = null, maxConsoleErrors = 15, maxNetworkErrors = 15) {
72
+ const errors = {
73
+ consoleErrors: [],
74
+ networkErrors: [],
75
+ jsExceptions: [],
76
+ consoleErrorsOmitted: 0,
77
+ networkErrorsOmitted: 0
78
+ };
79
+
80
+ // If no timestamp provided, look back 10 seconds
81
+ const cutoffTime = sinceTimestamp || (Date.now() - 10000);
82
+ const cutoffDate = new Date(cutoffTime).toISOString();
83
+
84
+ // Collect console errors (with limit)
85
+ let consoleErrorCount = 0;
86
+ consoleLogs.forEach(log => {
87
+ if (log.type === 'error') {
88
+ // Check if error is recent
89
+ const logTime = new Date(log.timestamp || 0).toISOString();
90
+ if (!sinceTimestamp || logTime >= cutoffDate) {
91
+ if (consoleErrorCount < maxConsoleErrors) {
92
+ errors.consoleErrors.push({
93
+ message: log.text,
94
+ timestamp: log.timestamp,
95
+ location: log.location || 'unknown'
96
+ });
97
+ } else {
98
+ errors.consoleErrorsOmitted++;
99
+ }
100
+ consoleErrorCount++;
101
+ }
102
+ }
103
+ });
104
+
105
+ // Collect network errors (failed requests, with limit)
106
+ let networkErrorCount = 0;
107
+ networkRequests.forEach(req => {
108
+ if (req.status === 'failed' || (typeof req.status === 'number' && req.status >= 400)) {
109
+ // Check if error is recent
110
+ const reqTime = req.timestamp;
111
+ if (!sinceTimestamp || reqTime >= cutoffDate) {
112
+ if (networkErrorCount < maxNetworkErrors) {
113
+ errors.networkErrors.push({
114
+ url: req.url,
115
+ method: req.method,
116
+ status: req.status,
117
+ statusText: req.statusText,
118
+ errorText: req.errorText,
119
+ timestamp: req.timestamp
120
+ });
121
+ } else {
122
+ errors.networkErrorsOmitted++;
123
+ }
124
+ networkErrorCount++;
125
+ }
126
+ }
127
+ });
128
+
129
+ return errors;
130
+ }
131
+
132
+ /**
133
+ * Full post-action diagnostics: wait for requests and collect errors
134
+ * @param {Page} page - Puppeteer page instance
135
+ * @param {number} beforeActionTimestamp - Timestamp before action (to filter errors)
136
+ * @returns {Promise<Object>} Diagnostics result with errors and network info
137
+ */
138
+ export async function runPostClickDiagnostics(page, beforeActionTimestamp) {
139
+ // Wait for network requests (passing timestamp to track post-action requests)
140
+ // maxWait = 20s to give slow APIs time to complete
141
+ const networkInfo = await waitForPendingRequests(beforeActionTimestamp, 500, 20000);
142
+
143
+ // Small delay to let pending requests update their error status
144
+ // (handles case where request completes with error right after maxWait expires)
145
+ await new Promise(resolve => setTimeout(resolve, 100));
146
+
147
+ // Check for chrome error page (ERR_CONNECTION_REFUSED, etc.)
148
+ const url = page.url();
149
+ let chromeErrorInfo = null;
150
+ if (url.startsWith('chrome-error://')) {
151
+ chromeErrorInfo = await page.evaluate(() => {
152
+ const errorCode = document.querySelector('#error-code');
153
+ const suggestionText = document.querySelector('.suggestions');
154
+ return {
155
+ errorCode: errorCode?.textContent || 'UNKNOWN_ERROR',
156
+ suggestion: suggestionText?.textContent?.trim() || 'Connection failed'
157
+ };
158
+ }).catch(() => ({ errorCode: 'PAGE_LOAD_ERROR', suggestion: 'Navigation failed' }));
159
+ }
160
+
161
+ // Collect errors that occurred after the action (including errors from just-completed requests)
162
+ const errors = collectErrors(beforeActionTimestamp);
163
+
164
+ // Combine into diagnostics report
165
+ const diagnostics = {
166
+ networkActivity: {
167
+ hadPendingRequests: networkInfo.pendingFound,
168
+ completedRequests: networkInfo.completedRequests,
169
+ stillPending: networkInfo.stillPending,
170
+ pendingRequests: networkInfo.pendingRequests,
171
+ totalRequests: networkInfo.totalRequests,
172
+ waitedMs: networkInfo.waitedMs
173
+ },
174
+ chromeError: chromeErrorInfo,
175
+ errors: {
176
+ consoleErrors: errors.consoleErrors,
177
+ networkErrors: errors.networkErrors,
178
+ consoleErrorsOmitted: errors.consoleErrorsOmitted,
179
+ networkErrorsOmitted: errors.networkErrorsOmitted,
180
+ totalErrors: errors.consoleErrors.length + errors.networkErrors.length
181
+ },
182
+ hasErrors: (errors.consoleErrors.length + errors.networkErrors.length) > 0 || chromeErrorInfo !== null
183
+ };
184
+
185
+ return diagnostics;
186
+ }
187
+
188
+ /**
189
+ * Format diagnostics for AI-friendly output
190
+ * @param {Object} diagnostics - Diagnostics object from runPostClickDiagnostics
191
+ * @returns {string} Formatted text for AI
192
+ */
193
+ export function formatDiagnosticsForAI(diagnostics) {
194
+ let output = '\n\n** POST-ACTION DIAGNOSTICS **';
195
+
196
+ // Chrome error page (connection refused, DNS failed, etc.)
197
+ if (diagnostics.chromeError) {
198
+ output += `\n\n🔴 CRITICAL: Navigation Failed`;
199
+ output += `\n Error: ${diagnostics.chromeError.errorCode}`;
200
+ output += `\n Suggestion: ${diagnostics.chromeError.suggestion}`;
201
+ output += `\n → Backend likely not running or unreachable`;
202
+ }
203
+
204
+ // Network activity
205
+ const netActivity = diagnostics.networkActivity;
206
+ if (netActivity.totalRequests > 0) {
207
+ const errorCount = diagnostics.errors.networkErrors.length;
208
+ const successCount = netActivity.completedRequests - errorCount;
209
+
210
+ // Show warning if there are pending requests after timeout
211
+ if (netActivity.stillPending > 0) {
212
+ output += `\n⚠️ Network: ${successCount} OK, ${errorCount} failed, ${netActivity.stillPending} PENDING`;
213
+ output += `\n ⏱️ Timeout: Stopped waiting after ${netActivity.waitedMs}ms`;
214
+ output += `\n → ${netActivity.stillPending} request(s) still running - status unknown`;
215
+ output += `\n → May complete successfully or fail - cannot determine outcome`;
216
+ } else if (errorCount > 0) {
217
+ output += `\n⚠️ Network: ${successCount} OK, ${errorCount} failed (${netActivity.waitedMs}ms)`;
218
+ } else {
219
+ output += `\n✓ Network: ${netActivity.completedRequests} completed (${netActivity.waitedMs}ms)`;
220
+ }
221
+ } else {
222
+ output += '\n✓ No network requests triggered';
223
+ }
224
+
225
+ // Errors
226
+ if (diagnostics.errors.totalErrors > 0) {
227
+ output += `\n\n⚠️ ERRORS DETECTED (${diagnostics.errors.totalErrors} total):`;
228
+
229
+ // Console errors
230
+ if (diagnostics.errors.consoleErrors.length > 0) {
231
+ output += `\n\nJavaScript Console Errors (${diagnostics.errors.consoleErrors.length}):`;
232
+ diagnostics.errors.consoleErrors.forEach((err, idx) => {
233
+ output += `\n ${idx + 1}. ${err.message}`;
234
+ if (err.location && err.location !== 'unknown') {
235
+ output += ` [${err.location}]`;
236
+ }
237
+ });
238
+ // Show if some errors were omitted
239
+ if (diagnostics.errors.consoleErrorsOmitted > 0) {
240
+ output += `\n ... and ${diagnostics.errors.consoleErrorsOmitted} more console error(s) (omitted to prevent spam)`;
241
+ }
242
+ }
243
+
244
+ // Network errors
245
+ if (diagnostics.errors.networkErrors.length > 0) {
246
+ output += `\n\nNetwork Errors (${diagnostics.errors.networkErrors.length}):`;
247
+ diagnostics.errors.networkErrors.forEach((err, idx) => {
248
+ output += `\n ${idx + 1}. ${err.method} ${err.url}`;
249
+ output += `\n Status: ${err.status}${err.statusText ? ' ' + err.statusText : ''}`;
250
+ if (err.errorText) {
251
+ output += `\n Error: ${err.errorText}`;
252
+ }
253
+ });
254
+ // Show if some errors were omitted
255
+ if (diagnostics.errors.networkErrorsOmitted > 0) {
256
+ output += `\n ... and ${diagnostics.errors.networkErrorsOmitted} more network error(s) (omitted to prevent spam)`;
257
+ }
258
+ }
259
+ } else {
260
+ output += '\n✓ No errors detected';
261
+ }
262
+
263
+ // Pending requests (if any still running after timeout)
264
+ if (netActivity.stillPending > 0 && netActivity.pendingRequests.length > 0) {
265
+ output += `\n\n⏳ PENDING REQUESTS (${netActivity.stillPending} still running):`;
266
+ netActivity.pendingRequests.forEach((req, idx) => {
267
+ output += `\n ${idx + 1}. ${req.method} ${req.url}`;
268
+ const elapsed = Date.now() - new Date(req.timestamp).getTime();
269
+ output += `\n Running for: ${elapsed}ms`;
270
+ });
271
+ output += `\n\n💡 Suggestion: These requests may be slow or hanging`;
272
+ output += `\n → Check backend performance or network connectivity`;
273
+ output += `\n → Consider using getNetworkRequest() to monitor progress`;
274
+ }
275
+
276
+ return output;
277
+ }