chrometools-mcp 3.2.6 → 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.
@@ -711,12 +711,17 @@ function buildAPOMTree(interactiveOnly = true) {
711
711
  // Try to find stable class name (excluding framework-specific dynamic classes)
712
712
  const stableClass = getStableClassName(element);
713
713
  if (stableClass) {
714
- const classSelector = `.${stableClass}`;
714
+ const escapedClass = CSS.escape(stableClass);
715
+ const classSelector = `.${escapedClass}`;
715
716
  // Verify it's unique within parent context
716
717
  if (element.parentElement) {
717
- const matches = element.parentElement.querySelectorAll(classSelector);
718
- if (matches.length === 1 && matches[0] === element) {
719
- return classSelector;
718
+ try {
719
+ const matches = element.parentElement.querySelectorAll(classSelector);
720
+ if (matches.length === 1 && matches[0] === element) {
721
+ return classSelector;
722
+ }
723
+ } catch (e) {
724
+ // Invalid selector, continue to path-based approach
720
725
  }
721
726
  }
722
727
  }
@@ -728,10 +733,10 @@ function buildAPOMTree(interactiveOnly = true) {
728
733
  while (current && current !== document.body) {
729
734
  let selector = current.tagName.toLowerCase();
730
735
 
731
- // Add stable class if available
736
+ // Add stable class if available (escaped for CSS selector safety)
732
737
  const stableClass = getStableClassName(current);
733
738
  if (stableClass) {
734
- selector += `.${stableClass}`;
739
+ selector += `.${CSS.escape(stableClass)}`;
735
740
  }
736
741
 
737
742
  // Add nth-of-type if needed
@@ -754,6 +759,7 @@ function buildAPOMTree(interactiveOnly = true) {
754
759
 
755
760
  /**
756
761
  * Get stable class name excluding framework-specific dynamic classes
762
+ * and Tailwind CSS utility classes with special characters
757
763
  * Returns first stable class or null
758
764
  */
759
765
  function getStableClassName(element) {
@@ -763,8 +769,14 @@ function buildAPOMTree(interactiveOnly = true) {
763
769
 
764
770
  const classes = element.className.split(/\s+/).filter(c => c);
765
771
 
766
- // Filter out framework-specific classes
772
+ // Filter out framework-specific classes and Tailwind utilities
767
773
  const stableClasses = classes.filter(className => {
774
+ // Tailwind CSS: classes with special characters that break CSS selectors
775
+ // Colons for variants (hover:, focus:, md:, etc.)
776
+ // Slashes for fractions (w-1/2)
777
+ // Brackets for arbitrary values (bg-[#1da1f2])
778
+ if (/[:\/\[\]]/.test(className)) return false;
779
+
768
780
  // React: CSS Modules, Styled Components, Emotion
769
781
  if (/^[a-zA-Z0-9_-]+-[a-zA-Z0-9_-]{5,}$/.test(className)) return false;
770
782
  if (/^css-[a-z0-9]+(-[a-z0-9]+)?$/i.test(className)) return false;
@@ -21,6 +21,8 @@ export const ClickSchema = z.object({
21
21
  waitAfter: z.number().optional().describe("Milliseconds to wait after click (default: 1500)"),
22
22
  screenshot: z.boolean().optional().describe("Capture screenshot after click (default: false for performance)"),
23
23
  timeout: z.number().optional().describe("Maximum time to wait for operation in ms (default: 30000)"),
24
+ skipNetworkWait: z.boolean().optional().describe("Skip waiting for network requests (default: false). Use for forms with long-polling/WebSockets to avoid timeouts."),
25
+ networkWaitTimeout: z.number().optional().describe("Maximum time to wait for network requests in ms (default: 3000). Only used if skipNetworkWait is false."),
24
26
  }).refine(data => (data.id && !data.selector) || (!data.id && data.selector), {
25
27
  message: "Either 'id' or 'selector' must be provided, but not both"
26
28
  });
@@ -31,6 +33,7 @@ export const TypeSchema = z.object({
31
33
  text: z.string().describe("Text to type"),
32
34
  delay: z.number().optional().describe("Delay between keystrokes in ms (default: 30)"),
33
35
  clearFirst: z.boolean().optional().describe("Clear field before typing (default: true)"),
36
+ timeout: z.number().optional().describe("Maximum time to wait for operation in ms (default: 30000)"),
34
37
  }).refine(data => (data.id && !data.selector) || (!data.id && data.selector), {
35
38
  message: "Either 'id' or 'selector' must be provided, but not both"
36
39
  });
@@ -8,6 +8,20 @@
8
8
  */
9
9
  export function generateNavigationHints(page, url) {
10
10
  return page.evaluate(() => {
11
+ // Helper to get safe class selector (filters Tailwind special chars)
12
+ function getSafeClassSelector(element) {
13
+ if (!element.className || typeof element.className !== 'string') return null;
14
+ const classes = element.className.split(' ')
15
+ .filter(c => c && !/[:\/\[\]]/.test(c))
16
+ .slice(0, 1);
17
+ if (classes.length === 0) return null;
18
+ try {
19
+ return `.${CSS.escape(classes[0])}`;
20
+ } catch (e) {
21
+ return null;
22
+ }
23
+ }
24
+
11
25
  const hints = {
12
26
  pageType: 'unknown',
13
27
  availableActions: [],
@@ -64,7 +78,7 @@ export function generateNavigationHints(page, url) {
64
78
  hints.keyElements.push({
65
79
  type: 'primary-button',
66
80
  text: mainButton.textContent.trim(),
67
- selector: mainButton.id ? `#${mainButton.id}` : `.${mainButton.className.split(' ')[0]}`,
81
+ selector: mainButton.id ? `#${CSS.escape(mainButton.id)}` : (getSafeClassSelector(mainButton) || 'button'),
68
82
  });
69
83
  }
70
84
 
@@ -74,7 +88,7 @@ export function generateNavigationHints(page, url) {
74
88
  hints.keyElements.push({
75
89
  type: 'notification',
76
90
  text: alert.textContent.trim().substring(0, 100),
77
- selector: alert.className ? `.${alert.className.split(' ')[0]}` : 'notification',
91
+ selector: getSafeClassSelector(alert) || '[role="alert"]',
78
92
  });
79
93
  }
80
94
  });
@@ -91,6 +105,20 @@ export async function generateClickHints(page, selector) {
91
105
  await new Promise(resolve => setTimeout(resolve, 100));
92
106
 
93
107
  return page.evaluate((clickedSelector) => {
108
+ // Helper to get safe class selector (filters Tailwind special chars)
109
+ function getSafeClassSelector(element) {
110
+ if (!element.className || typeof element.className !== 'string') return null;
111
+ const classes = element.className.split(' ')
112
+ .filter(c => c && !/[:\/\[\]]/.test(c))
113
+ .slice(0, 1);
114
+ if (classes.length === 0) return null;
115
+ try {
116
+ return `.${CSS.escape(classes[0])}`;
117
+ } catch (e) {
118
+ return null;
119
+ }
120
+ }
121
+
94
122
  const hints = {
95
123
  pageChanged: false,
96
124
  newElements: [],
@@ -105,7 +133,7 @@ export async function generateClickHints(page, selector) {
105
133
  hints.modalOpened = true;
106
134
  hints.newElements.push({
107
135
  type: 'modal',
108
- selector: modal.className ? `.${modal.className.split(' ')[0]}` : '[role="dialog"]',
136
+ selector: getSafeClassSelector(modal) || '[role="dialog"]',
109
137
  });
110
138
  hints.suggestedNext.push('Interact with modal or close it');
111
139
  }
@@ -145,6 +173,20 @@ export async function generateFormSubmitHints(page) {
145
173
  await new Promise(resolve => setTimeout(resolve, 500));
146
174
 
147
175
  return page.evaluate(() => {
176
+ // Helper to get safe class selector (filters Tailwind special chars)
177
+ function getSafeClassSelector(element) {
178
+ if (!element.className || typeof element.className !== 'string') return null;
179
+ const classes = element.className.split(' ')
180
+ .filter(c => c && !/[:\/\[\]]/.test(c))
181
+ .slice(0, 1);
182
+ if (classes.length === 0) return null;
183
+ try {
184
+ return `.${CSS.escape(classes[0])}`;
185
+ } catch (e) {
186
+ return null;
187
+ }
188
+ }
189
+
148
190
  const hints = {
149
191
  success: false,
150
192
  errors: [],
@@ -173,7 +215,7 @@ export async function generateFormSubmitHints(page) {
173
215
  if (el.offsetWidth > 0) {
174
216
  hints.errors.push({
175
217
  text: el.textContent.trim().substring(0, 100),
176
- selector: el.className ? `.${el.className.split(' ')[0]}` : 'error-element',
218
+ selector: getSafeClassSelector(el) || '[aria-invalid="true"]',
177
219
  });
178
220
  }
179
221
  });
@@ -0,0 +1,376 @@
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
+ import { getNetworkRequestsFromBridge, isBridgeConnected } from '../bridge/bridge-client.js';
8
+
9
+ /**
10
+ * Wait for network requests to complete
11
+ * Tracks all requests (GET, POST, PUT, PATCH, DELETE) that started within detection window
12
+ * @param {number} beforeActionTimestamp - Timestamp before action to track new requests
13
+ * @param {number} detectionWindowMs - Time window to detect requests (default: 200ms)
14
+ * @param {number} maxWaitMs - Maximum time to wait for requests (default: 10000ms)
15
+ * @returns {Promise<{pendingFound: boolean, waitedMs: number, completedRequests: number, totalRequests: number}>}
16
+ */
17
+ export async function waitForPendingRequests(beforeActionTimestamp, detectionWindowMs = 200, maxWaitMs = 10000) {
18
+ const startTime = Date.now();
19
+
20
+ // Step 1: Wait for detection window to let requests start
21
+ await new Promise(resolve => setTimeout(resolve, detectionWindowMs));
22
+
23
+ // Step 2: Find all requests (GET, POST, PUT, PATCH, DELETE) that started within detection window
24
+ const cutoffStart = new Date(beforeActionTimestamp).toISOString();
25
+ const cutoffEnd = new Date(beforeActionTimestamp + detectionWindowMs).toISOString();
26
+
27
+ const trackedRequests = networkRequests.filter(req => {
28
+ // Track all HTTP methods
29
+ if (!['GET', 'POST', 'PUT', 'PATCH', 'DELETE'].includes(req.method)) {
30
+ return false;
31
+ }
32
+ // Only requests in detection window [T0, T0+200ms]
33
+ return req.timestamp >= cutoffStart && req.timestamp <= cutoffEnd;
34
+ });
35
+
36
+ // If no requests found, return immediately
37
+ if (trackedRequests.length === 0) {
38
+ return {
39
+ pendingFound: false,
40
+ waitedMs: Date.now() - startTime,
41
+ completedRequests: 0,
42
+ stillPending: 0,
43
+ pendingRequests: [],
44
+ totalRequests: 0,
45
+ trackedRequests: []
46
+ };
47
+ }
48
+
49
+ // Step 3: Wait for these specific requests to complete
50
+ const trackedRequestIds = new Set(trackedRequests.map(req => req.requestId));
51
+
52
+ const checkPending = () => {
53
+ return networkRequests.filter(req =>
54
+ trackedRequestIds.has(req.requestId) && req.status === 'pending'
55
+ );
56
+ };
57
+
58
+ let pending = checkPending();
59
+ const initialPendingCount = pending.length;
60
+
61
+ // Wait for requests to complete (with configurable timeout)
62
+ while (pending.length > 0 && (Date.now() - startTime) < maxWaitMs) {
63
+ await new Promise(resolve => setTimeout(resolve, 100)); // Check every 100ms
64
+ pending = checkPending();
65
+ }
66
+
67
+ // Collect final results for tracked requests
68
+ const finalRequests = networkRequests.filter(req => trackedRequestIds.has(req.requestId));
69
+ const completedRequests = finalRequests.filter(req => req.status === 'completed' || (typeof req.status === 'number'));
70
+ const stillPendingRequests = pending.map(req => ({
71
+ url: req.url,
72
+ method: req.method,
73
+ timestamp: req.timestamp,
74
+ status: 'pending' // Still waiting after timeout
75
+ }));
76
+
77
+ return {
78
+ pendingFound: initialPendingCount > 0,
79
+ waitedMs: Date.now() - startTime,
80
+ completedRequests: completedRequests.length,
81
+ stillPending: pending.length,
82
+ pendingRequests: stillPendingRequests,
83
+ totalRequests: finalRequests.length,
84
+ trackedRequests: finalRequests.map(req => ({
85
+ method: req.method,
86
+ url: req.url,
87
+ status: req.status,
88
+ statusText: req.statusText
89
+ }))
90
+ };
91
+ }
92
+
93
+ /**
94
+ * Collect errors from console logs and network requests
95
+ * @param {number} sinceTimestamp - Only collect errors after this timestamp (default: collect recent errors)
96
+ * @param {number} maxConsoleErrors - Maximum console errors to return (default: 15)
97
+ * @param {number} maxNetworkErrors - Maximum network errors to return (default: 15)
98
+ * @returns {Object} Object with consoleErrors and networkErrors arrays
99
+ */
100
+ export function collectErrors(sinceTimestamp = null, maxConsoleErrors = 15, maxNetworkErrors = 15) {
101
+ const errors = {
102
+ consoleErrors: [],
103
+ networkErrors: [],
104
+ jsExceptions: [],
105
+ consoleErrorsOmitted: 0,
106
+ networkErrorsOmitted: 0
107
+ };
108
+
109
+ // If no timestamp provided, look back 10 seconds
110
+ const cutoffTime = sinceTimestamp || (Date.now() - 10000);
111
+ const cutoffDate = new Date(cutoffTime).toISOString();
112
+
113
+ // Collect console errors (with limit)
114
+ let consoleErrorCount = 0;
115
+ consoleLogs.forEach(log => {
116
+ if (log.type === 'error') {
117
+ // Check if error is recent
118
+ const logTime = new Date(log.timestamp || 0).toISOString();
119
+ if (!sinceTimestamp || logTime >= cutoffDate) {
120
+ if (consoleErrorCount < maxConsoleErrors) {
121
+ errors.consoleErrors.push({
122
+ message: log.text,
123
+ timestamp: log.timestamp,
124
+ location: log.location || 'unknown'
125
+ });
126
+ } else {
127
+ errors.consoleErrorsOmitted++;
128
+ }
129
+ consoleErrorCount++;
130
+ }
131
+ }
132
+ });
133
+
134
+ // Collect network errors (failed requests, with limit)
135
+ let networkErrorCount = 0;
136
+ networkRequests.forEach(req => {
137
+ if (req.status === 'failed' || (typeof req.status === 'number' && req.status >= 400)) {
138
+ // Check if error is recent
139
+ const reqTime = req.timestamp;
140
+ if (!sinceTimestamp || reqTime >= cutoffDate) {
141
+ if (networkErrorCount < maxNetworkErrors) {
142
+ errors.networkErrors.push({
143
+ url: req.url,
144
+ method: req.method,
145
+ status: req.status,
146
+ statusText: req.statusText,
147
+ errorText: req.errorText,
148
+ timestamp: req.timestamp
149
+ });
150
+ } else {
151
+ errors.networkErrorsOmitted++;
152
+ }
153
+ networkErrorCount++;
154
+ }
155
+ }
156
+ });
157
+
158
+ return errors;
159
+ }
160
+
161
+ /**
162
+ * Full post-action diagnostics: wait for requests and collect errors
163
+ * @param {Page} page - Puppeteer page instance
164
+ * @param {number} beforeActionTimestamp - Timestamp before action (to filter errors)
165
+ * @param {Object} options - Options for diagnostics
166
+ * @param {boolean} options.skipNetworkWait - Skip waiting for network requests (default: false)
167
+ * @param {number} options.networkWaitTimeout - Custom timeout for network wait in ms (default: 10000)
168
+ * @param {string} options.urlBeforeAction - URL before action (to detect navigation/form submit)
169
+ * @returns {Promise<Object>} Diagnostics result with errors and network info
170
+ */
171
+ export async function runPostClickDiagnostics(page, beforeActionTimestamp, options = {}) {
172
+ const { skipNetworkWait = false, networkWaitTimeout = 10000, urlBeforeAction = null } = options;
173
+
174
+ // Wait for network requests (all methods within 200ms detection window)
175
+ // Default maxWait = 10s for click, configurable via networkWaitTimeout parameter
176
+ const networkInfo = skipNetworkWait
177
+ ? { pendingFound: false, waitedMs: 0, completedRequests: 0, stillPending: 0, pendingRequests: [], totalRequests: 0, trackedRequests: [], allRecentRequests: [] }
178
+ : await waitForPendingRequests(beforeActionTimestamp, 200, networkWaitTimeout);
179
+
180
+ // Small delay to let pending requests update their error status
181
+ // (handles case where request completes with error right after maxWait expires)
182
+ await new Promise(resolve => setTimeout(resolve, 100));
183
+
184
+ // Check for page navigation (indicates form submit in non-SPA apps)
185
+ const currentUrl = page.url();
186
+ let navigationDetected = null;
187
+ if (urlBeforeAction && currentUrl !== urlBeforeAction) {
188
+ navigationDetected = {
189
+ from: urlBeforeAction,
190
+ to: currentUrl,
191
+ likelyFormSubmit: true // Page URL changed after click - likely form POST with redirect
192
+ };
193
+ }
194
+
195
+ // Fetch network requests from Bridge (Extension webRequest API)
196
+ // These persist across page navigations, unlike CDP requests
197
+ let bridgeRequests = [];
198
+ if (isBridgeConnected()) {
199
+ try {
200
+ const allBridgeRequests = await getNetworkRequestsFromBridge({ timeout: 2000 });
201
+ // Filter to requests after beforeActionTimestamp
202
+ const cutoffTime = beforeActionTimestamp - 1000; // 1s buffer
203
+ bridgeRequests = allBridgeRequests.filter(req =>
204
+ req.timestamp >= cutoffTime
205
+ );
206
+ } catch (e) {
207
+ // Bridge not available, continue without
208
+ }
209
+ }
210
+
211
+ // Check for chrome error page (ERR_CONNECTION_REFUSED, etc.)
212
+ const url = currentUrl;
213
+ let chromeErrorInfo = null;
214
+ if (url.startsWith('chrome-error://')) {
215
+ chromeErrorInfo = await page.evaluate(() => {
216
+ const errorCode = document.querySelector('#error-code');
217
+ const suggestionText = document.querySelector('.suggestions');
218
+ return {
219
+ errorCode: errorCode?.textContent || 'UNKNOWN_ERROR',
220
+ suggestion: suggestionText?.textContent?.trim() || 'Connection failed'
221
+ };
222
+ }).catch(() => ({ errorCode: 'PAGE_LOAD_ERROR', suggestion: 'Navigation failed' }));
223
+ }
224
+
225
+ // Collect errors that occurred after the action (including errors from just-completed requests)
226
+ const errors = collectErrors(beforeActionTimestamp);
227
+
228
+ // Combine into diagnostics report
229
+ const diagnostics = {
230
+ networkActivity: {
231
+ hadPendingRequests: networkInfo.pendingFound,
232
+ completedRequests: networkInfo.completedRequests,
233
+ stillPending: networkInfo.stillPending,
234
+ pendingRequests: networkInfo.pendingRequests,
235
+ totalRequests: networkInfo.totalRequests,
236
+ waitedMs: networkInfo.waitedMs,
237
+ trackedRequests: networkInfo.trackedRequests || [],
238
+ allRecentRequests: networkInfo.allRecentRequests || [],
239
+ // Bridge requests (from Extension webRequest API) - persist across page navigations
240
+ bridgeRequests: bridgeRequests.map(req => ({
241
+ method: req.method,
242
+ url: req.url,
243
+ type: req.type,
244
+ status: req.status,
245
+ timestamp: req.timestamp
246
+ }))
247
+ },
248
+ navigation: navigationDetected,
249
+ chromeError: chromeErrorInfo,
250
+ errors: {
251
+ consoleErrors: errors.consoleErrors,
252
+ networkErrors: errors.networkErrors,
253
+ consoleErrorsOmitted: errors.consoleErrorsOmitted,
254
+ networkErrorsOmitted: errors.networkErrorsOmitted,
255
+ totalErrors: errors.consoleErrors.length + errors.networkErrors.length
256
+ },
257
+ hasErrors: (errors.consoleErrors.length + errors.networkErrors.length) > 0 || chromeErrorInfo !== null
258
+ };
259
+
260
+ return diagnostics;
261
+ }
262
+
263
+ /**
264
+ * Format diagnostics for AI-friendly output
265
+ * @param {Object} diagnostics - Diagnostics object from runPostClickDiagnostics
266
+ * @returns {string} Formatted text for AI
267
+ */
268
+ export function formatDiagnosticsForAI(diagnostics) {
269
+ let output = '\n\n** POST-ACTION DIAGNOSTICS **';
270
+
271
+ // Chrome error page (connection refused, DNS failed, etc.)
272
+ if (diagnostics.chromeError) {
273
+ output += `\n\nšŸ”“ CRITICAL: Navigation Failed`;
274
+ output += `\n Error: ${diagnostics.chromeError.errorCode}`;
275
+ output += `\n Suggestion: ${diagnostics.chromeError.suggestion}`;
276
+ output += `\n → Backend likely not running or unreachable`;
277
+ }
278
+
279
+ // Page navigation detection (form submit in non-SPA apps)
280
+ if (diagnostics.navigation) {
281
+ output += `\n\nšŸ”„ Page navigation detected (form submit):`;
282
+ output += `\n From: ${diagnostics.navigation.from}`;
283
+ output += `\n To: ${diagnostics.navigation.to}`;
284
+ output += `\n → This indicates a successful form POST with page reload`;
285
+ }
286
+
287
+ // Network activity - show all tracked requests (GET/POST/PUT/PATCH/DELETE)
288
+ const netActivity = diagnostics.networkActivity;
289
+ const trackedRequests = netActivity.trackedRequests || [];
290
+
291
+ // Show requests detected within 200ms after action
292
+ if (trackedRequests.length > 0) {
293
+ output += `\n\nšŸ“” Network requests (${trackedRequests.length}):`;
294
+ trackedRequests.forEach((req, idx) => {
295
+ const statusIcon = req.status === 'pending' ? 'ā³' :
296
+ (req.status === 'completed' || (typeof req.status === 'number' && req.status < 400) ? 'āœ“' : 'āœ—');
297
+ const statusText = req.statusText || req.status || 'pending';
298
+ output += `\n ${idx + 1}. ${statusIcon} ${req.method} ${req.url}`;
299
+ output += `\n → Status: ${statusText}`;
300
+ });
301
+
302
+ // Show if some requests are still pending after timeout
303
+ if (netActivity.stillPending > 0) {
304
+ output += `\n\nā³ ${netActivity.stillPending} request(s) still pending after ${Math.round(netActivity.waitedMs)}ms timeout`;
305
+ }
306
+ } else {
307
+ output += '\n\nšŸ“” No network requests detected within 200ms';
308
+ }
309
+
310
+ // Bridge requests (from Extension - persist across page reloads)
311
+ const bridgeRequests = netActivity.bridgeRequests || [];
312
+ if (bridgeRequests.length > 0) {
313
+ output += `\n\nšŸ“” Browser-level requests (via Extension):`;
314
+ bridgeRequests.forEach((req, idx) => {
315
+ const statusIcon = req.status === 'pending' ? 'ā³' :
316
+ (req.status === 'completed' || (typeof req.status === 'number' && req.status < 400) ? 'āœ“' : 'āœ—');
317
+ output += `\n ${idx + 1}. ${statusIcon} ${req.method} ${req.url}`;
318
+ if (req.status !== 'pending') {
319
+ output += ` → ${req.status}`;
320
+ }
321
+ });
322
+ }
323
+
324
+ // Errors
325
+ if (diagnostics.errors.totalErrors > 0) {
326
+ output += `\n\nāš ļø ERRORS DETECTED (${diagnostics.errors.totalErrors} total):`;
327
+
328
+ // Console errors
329
+ if (diagnostics.errors.consoleErrors.length > 0) {
330
+ output += `\n\nJavaScript Console Errors (${diagnostics.errors.consoleErrors.length}):`;
331
+ diagnostics.errors.consoleErrors.forEach((err, idx) => {
332
+ output += `\n ${idx + 1}. ${err.message}`;
333
+ if (err.location && err.location !== 'unknown') {
334
+ output += ` [${err.location}]`;
335
+ }
336
+ });
337
+ // Show if some errors were omitted
338
+ if (diagnostics.errors.consoleErrorsOmitted > 0) {
339
+ output += `\n ... and ${diagnostics.errors.consoleErrorsOmitted} more console error(s) (omitted to prevent spam)`;
340
+ }
341
+ }
342
+
343
+ // Network errors
344
+ if (diagnostics.errors.networkErrors.length > 0) {
345
+ output += `\n\nNetwork Errors (${diagnostics.errors.networkErrors.length}):`;
346
+ diagnostics.errors.networkErrors.forEach((err, idx) => {
347
+ output += `\n ${idx + 1}. ${err.method} ${err.url}`;
348
+ output += `\n Status: ${err.status}${err.statusText ? ' ' + err.statusText : ''}`;
349
+ if (err.errorText) {
350
+ output += `\n Error: ${err.errorText}`;
351
+ }
352
+ });
353
+ // Show if some errors were omitted
354
+ if (diagnostics.errors.networkErrorsOmitted > 0) {
355
+ output += `\n ... and ${diagnostics.errors.networkErrorsOmitted} more network error(s) (omitted to prevent spam)`;
356
+ }
357
+ }
358
+ } else {
359
+ output += '\nāœ“ No errors detected';
360
+ }
361
+
362
+ // Pending requests (if any still running after timeout)
363
+ if (netActivity.stillPending > 0 && netActivity.pendingRequests.length > 0) {
364
+ output += `\n\nā³ PENDING REQUESTS (${netActivity.stillPending} still running):`;
365
+ netActivity.pendingRequests.forEach((req, idx) => {
366
+ output += `\n ${idx + 1}. ${req.method} ${req.url}`;
367
+ const elapsed = Date.now() - new Date(req.timestamp).getTime();
368
+ output += `\n Running for: ${elapsed}ms`;
369
+ });
370
+ output += `\n\nšŸ’” Suggestion: These requests may be slow or hanging`;
371
+ output += `\n → Check backend performance or network connectivity`;
372
+ output += `\n → Consider using getNetworkRequest() to monitor progress`;
373
+ }
374
+
375
+ return output;
376
+ }
File without changes