chrometools-mcp 3.2.6 → 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,96 @@
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
+
5
95
  ## [3.2.6] - 2026-01-28
6
96
 
7
97
  ### Removed
@@ -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
@@ -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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chrometools-mcp",
3
- "version": "3.2.6",
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",
@@ -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
+ }
File without changes