reactoradar 1.5.2 → 1.5.4

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/app.js CHANGED
@@ -205,15 +205,19 @@ if (window.electronAPI) {
205
205
  window.electronAPI.on('ports', ports => { state.ports = ports; });
206
206
 
207
207
  window.electronAPI.on('cdp-targets', targets => {
208
- const hasCDP = targets?.length > 0;
209
- $('btnCDP').textContent = hasCDP
210
- ? `JS Debugger (${targets.length}) ↗`
211
- : 'JS Debugger ↗';
212
- $('btnCDP').style.opacity = hasCDP ? '1' : '0.5';
213
- if (hasCDP) {
214
- $('btnCDP').onclick = () => window.electronAPI.openCDPTarget(targets[0].webSocketDebuggerUrl);
208
+ state.cdpTargets = targets;
209
+ const btn = $('btnCDP');
210
+ if (btn) {
211
+ const hasCDP = targets?.length > 0;
212
+ const port = state.ports?.METRO || getStoredMetroPort();
213
+ btn.textContent = hasCDP
214
+ ? `JS Debugger (:${port}) [${targets.length}] ↗`
215
+ : `JS Debugger (:${port}) ↗`;
216
+ btn.style.opacity = hasCDP ? '1' : '0.5';
217
+ if (hasCDP) {
218
+ btn.onclick = () => window.electronAPI.openCDPTarget(targets[0].webSocketDebuggerUrl);
219
+ }
215
220
  }
216
-
217
221
  });
218
222
 
219
223
  window.electronAPI.on('redux-event', handleReduxEvent);
@@ -264,21 +268,7 @@ if (window.electronAPI) {
264
268
  window.electronAPI.on('update-available', ({ current, latest }) => {
265
269
  // Show in settings only, not as a banner
266
270
  state._updateAvailable = { current, latest };
267
- const el = $('aboutVersion');
268
- if (el) el.innerHTML = `v${current} <span style="color:var(--green);font-size:10px;margin-left:6px">v${latest} available</span>`;
269
- // Add update button in settings if not already there
270
- if (!$('updateBtn')) {
271
- const aboutEl = document.querySelector('.settings-about');
272
- if (aboutEl) {
273
- const btn = document.createElement('div');
274
- btn.style.cssText = 'margin-top:10px';
275
- btn.innerHTML = '<button id="updateBtn" class="tb-btn primary" style="font-size:11px">Download v' + latest + '</button>';
276
- aboutEl.appendChild(btn);
277
- $('updateBtn')?.addEventListener('click', () => {
278
- window.electronAPI?.openExternal('https://github.com/sharanagouda/react-native-debugger/releases');
279
- });
280
- }
281
- }
271
+ _applyUpdateBanner();
282
272
  });
283
273
 
284
274
  window.electronAPI.on('trigger-open-cdp', () => {
@@ -295,6 +285,31 @@ if (window.electronAPI) {
295
285
  }
296
286
 
297
287
  // ─── Device Connection Status (inline in titlebar) ───────────────────────────
288
+ // Reusable — called from IPC handler AND from initSettingsPanel
289
+ function _applyUpdateBanner() {
290
+ const info = state._updateAvailable;
291
+ if (!info) return;
292
+ const { current, latest } = info;
293
+ const el = $('aboutVersion');
294
+ if (el && !el.dataset.updateApplied) {
295
+ el.innerHTML = `v${current} <span style="color:var(--green);font-size:10px;margin-left:6px">v${latest} available</span>`;
296
+ el.dataset.updateApplied = '1';
297
+ }
298
+ // Add update button in settings if not already there
299
+ if (!$('updateBtn')) {
300
+ const aboutEl = document.querySelector('.settings-about');
301
+ if (aboutEl) {
302
+ const btn = document.createElement('div');
303
+ btn.style.cssText = 'margin-top:10px';
304
+ btn.innerHTML = '<button id="updateBtn" class="tb-btn primary" style="font-size:11px;padding:6px 16px">Download v' + latest + '</button>';
305
+ aboutEl.appendChild(btn);
306
+ $('updateBtn')?.addEventListener('click', () => {
307
+ window.electronAPI?.openExternal('https://github.com/sharanagouda/react-native-debugger/releases');
308
+ });
309
+ }
310
+ }
311
+ }
312
+
298
313
  function updateDeviceBanner(service, connected) {
299
314
  state.connections[service] = connected;
300
315
  const el = $('deviceStatus');
@@ -1269,9 +1284,13 @@ function renderNetwork() {
1269
1284
  rows.appendChild(frag);
1270
1285
  }
1271
1286
 
1287
+ function _isHttpError(r) {
1288
+ return r.phase === 'error' || (r.status && r.status >= 400);
1289
+ }
1290
+
1272
1291
  function buildNetRow(r, wfMin, wfRange) {
1273
1292
  const row = document.createElement('div');
1274
- row.className = 'net-row' + (r.id === state.network.selectedId ? ' selected' : '') + (r.phase === 'error' ? ' error' : '');
1293
+ row.className = 'net-row' + (r.id === state.network.selectedId ? ' selected' : '') + (_isHttpError(r) ? ' error' : '');
1275
1294
  row.dataset.id = r.id;
1276
1295
 
1277
1296
  const urlObj = tryURL(r.url);
@@ -1287,7 +1306,9 @@ function buildNetRow(r, wfMin, wfRange) {
1287
1306
  const method = r.method || '?';
1288
1307
  const mClass = ['GET','POST','PUT','PATCH','DELETE'].includes(method) ? `m-${method}` : 'm-other';
1289
1308
  const fullPath = urlObj ? urlObj.pathname + urlObj.search : r.url || '';
1290
- nameCell.innerHTML = `<span class="method-badge ${mClass}">${method}</span> <span class="net-path" title="${esc(r.url)}">${esc(fullPath)}</span><span class="net-host">${esc(host)}</span>`;
1309
+ const isErr = _isHttpError(r);
1310
+ const pathCls = isErr ? ' net-path-error' : '';
1311
+ nameCell.innerHTML = `<span class="method-badge ${mClass}">${method}</span> <span class="net-path${pathCls}" title="${esc(r.url)}">${esc(fullPath)}</span><span class="net-host">${esc(host)}</span>`;
1291
1312
  row.appendChild(nameCell);
1292
1313
 
1293
1314
  // Status
@@ -1297,7 +1318,13 @@ function buildNetRow(r, wfMin, wfRange) {
1297
1318
  statusCell.style.width = NET_COLS[1].width + 'px';
1298
1319
  let statusStr = '...', sCls = 's-pending';
1299
1320
  if (r.phase === 'error') { statusStr = 'ERR'; sCls = 's-err'; }
1300
- else if (r.status) { statusStr = String(r.status); sCls = `s-${Math.floor(r.status/100)}`; }
1321
+ else if (r.status) {
1322
+ statusStr = String(r.status);
1323
+ const group = Math.floor(r.status / 100);
1324
+ // 1xx info, 2xx success, 3xx redirect, 4xx client error, 5xx server error
1325
+ if (group >= 4) sCls = 's-err';
1326
+ else sCls = `s-${group}`;
1327
+ }
1301
1328
  statusCell.className += ` ${sCls}`;
1302
1329
  statusCell.textContent = statusStr;
1303
1330
  row.appendChild(statusCell);
@@ -1346,6 +1373,7 @@ function buildNetRow(r, wfMin, wfRange) {
1346
1373
  const width = Math.max(2, ((r.duration || 50) / wfRange) * 100);
1347
1374
  let barCls = 'pending';
1348
1375
  if (r.phase === 'error') barCls = 'err';
1376
+ else if (r.status && r.status >= 400) barCls = 'err';
1349
1377
  else if (r.status) barCls = `s${Math.floor(r.status/100)}`;
1350
1378
  wfCell.innerHTML = `<div class="wf-bar ${barCls}" style="left:${left}%;width:${width}%"></div>`;
1351
1379
  }
@@ -1412,8 +1440,12 @@ function renderNetDetailTabs(r) {
1412
1440
  }
1413
1441
 
1414
1442
  function renderNetDetailContent(r) {
1415
- const body = $('netDetailContent');
1443
+ let body = $('netDetailContent');
1416
1444
  if (!body) return;
1445
+ // Clone-replace to remove all stale event listeners (prevents contextmenu leak)
1446
+ const fresh = body.cloneNode(false);
1447
+ body.parentNode.replaceChild(fresh, body);
1448
+ body = fresh;
1417
1449
  const tab = r._tab || 'headers';
1418
1450
 
1419
1451
  if (tab === 'headers') {
@@ -1432,7 +1464,7 @@ function renderNetDetailContent(r) {
1432
1464
  <div class="kv-grid">
1433
1465
  <span class="kv-key">Request URL</span><span class="kv-val">${esc(r.url)}</span>
1434
1466
  <span class="kv-key">Method</span><span class="kv-val">${esc(r.method)}</span>
1435
- <span class="kv-key">Status</span><span class="kv-val ${r.status ? 's-' + Math.floor(r.status/100) : 's-pending'}">${r.status || 'Pending'} ${r.statusText || ''}</span>
1467
+ <span class="kv-key">Status</span><span class="kv-val ${r.phase === 'error' ? 's-err' : r.status ? (r.status >= 400 ? 's-err' : 's-' + Math.floor(r.status/100)) : 's-pending'}">${r.phase === 'error' ? (r.status || 'ERR') : (r.status || 'Pending')} ${r.statusText || (r.phase === 'error' ? r.error || 'Network Error' : '')}</span>
1436
1468
  </div>
1437
1469
  ${renderH('Response Headers', rsH)}
1438
1470
  ${renderH('Request Headers', rqH)}`;
@@ -1456,16 +1488,27 @@ function renderNetDetailContent(r) {
1456
1488
  }
1457
1489
  }
1458
1490
  } else if (tab === 'preview') {
1459
- if (r.phase === 'error') { body.innerHTML = `<span style="color:var(--red)">${esc(r.error || 'Request failed')}</span>`; return; }
1491
+ const isErrStatus = _isHttpError(r);
1492
+ if (r.phase === 'error' && !r.responseBody) { body.innerHTML = `<span style="color:var(--red)">${esc(r.error || 'Request failed')}</span>`; return; }
1460
1493
  if (!r.responseBody && r.phase !== 'response') { body.innerHTML = '<span style="color:var(--text-dim)">Pending...</span>'; return; }
1461
1494
  // Render as collapsible JSON tree with right-click copy
1462
1495
  const val = r.responseBody;
1463
1496
  let treeData = val;
1464
1497
  if (typeof val === 'string') {
1465
- try { treeData = JSON.parse(val); } catch { body.textContent = val; return; }
1498
+ try { treeData = JSON.parse(val); } catch {
1499
+ body.innerHTML = `<span style="color:${isErrStatus ? 'var(--red)' : 'inherit'}">${esc(val)}</span>`;
1500
+ return;
1501
+ }
1466
1502
  }
1467
1503
  if (treeData && typeof treeData === 'object') {
1468
1504
  body.innerHTML = '';
1505
+ // Show error status banner above the response body
1506
+ if (isErrStatus) {
1507
+ const errBanner = document.createElement('div');
1508
+ errBanner.style.cssText = 'color:var(--red);font-weight:600;padding:4px 0 8px;font-size:11px;border-bottom:1px solid rgba(255,94,114,.15);margin-bottom:8px';
1509
+ errBanner.textContent = `${r.status || 'ERR'} ${r.statusText || r.error || 'Error'}`;
1510
+ body.appendChild(errBanner);
1511
+ }
1469
1512
  body.appendChild(createTreeNode(null, treeData, false));
1470
1513
  // Right-click on preview to copy the whole object or clicked node value
1471
1514
  body.addEventListener('contextmenu', (e) => {
@@ -1473,12 +1516,27 @@ function renderNetDetailContent(r) {
1473
1516
  showPreviewCopyMenu(e, treeData);
1474
1517
  });
1475
1518
  } else {
1476
- body.innerHTML = '<span style="color:var(--text-dim)">No preview available</span>';
1519
+ body.innerHTML = isErrStatus
1520
+ ? `<span style="color:var(--red)">${esc(String(r.responseBody))}</span>`
1521
+ : '<span style="color:var(--text-dim)">No preview available</span>';
1477
1522
  }
1478
1523
  } else if (tab === 'response') {
1479
- if (r.phase === 'error') { body.innerHTML = `<span style="color:var(--red)">${esc(r.error || 'Request failed')}</span>`; return; }
1524
+ const isErrStatus = _isHttpError(r);
1525
+ if (r.phase === 'error' && !r.responseBody) { body.innerHTML = `<span style="color:var(--red)">${esc(r.error || 'Request failed')}</span>`; return; }
1480
1526
  if (!r.responseBody && r.phase !== 'response') { body.innerHTML = '<span style="color:var(--text-dim)">Pending...</span>'; return; }
1481
- body.innerHTML = renderJSON(r.responseBody);
1527
+ if (isErrStatus) {
1528
+ const errBanner = document.createElement('div');
1529
+ errBanner.style.cssText = 'color:var(--red);font-weight:600;padding:4px 0 8px;font-size:11px;border-bottom:1px solid rgba(255,94,114,.15);margin-bottom:8px';
1530
+ errBanner.textContent = `${r.status || 'ERR'} ${r.statusText || r.error || 'Error'}`;
1531
+ body.innerHTML = '';
1532
+ body.appendChild(errBanner);
1533
+ const raw = document.createElement('div');
1534
+ raw.style.color = 'var(--red)';
1535
+ raw.innerHTML = renderJSON(r.responseBody);
1536
+ body.appendChild(raw);
1537
+ } else {
1538
+ body.innerHTML = renderJSON(r.responseBody);
1539
+ }
1482
1540
  }
1483
1541
  }
1484
1542
 
@@ -2149,9 +2207,9 @@ function initReactPanel() {
2149
2207
  <div class="react-connect-hint" id="reactHint">
2150
2208
  <div class="icon" style="font-size:40px;opacity:.2">⚛️</div>
2151
2209
  <div class="label">React DevTools</div>
2152
- <div class="hint">Launches as a separate window connected to your app</div>
2153
- <div class="hint">React Native auto-connects on port <code>8097</code> in dev mode</div>
2154
- <button class="btn-launch" id="btnReactDT">Open React DevTools ↗</button>
2210
+ <div class="hint">Opens as a separate window connected to your app via port 8097</div>
2211
+ <div class="hint" style="margin-top:8px;color:var(--yellow)">Note: The RN inspector overlay won't work while React DevTools is connected. Close the DevTools window to use the built-in inspector.</div>
2212
+ <button class="btn-launch" id="btnReactDT" style="margin-top:12px">Open React DevTools ↗</button>
2155
2213
  </div>
2156
2214
  </div>`;
2157
2215
 
@@ -2182,6 +2240,12 @@ function getStoredAppName() {
2182
2240
  function setStoredAppName(n) {
2183
2241
  try { localStorage.setItem('rn-debug-appname', n); } catch {}
2184
2242
  }
2243
+ function getStoredMetroPort() {
2244
+ try { return parseInt(localStorage.getItem('rn-debug-metro-port')) || 8081; } catch { return 8081; }
2245
+ }
2246
+ function setStoredMetroPort(p) {
2247
+ try { localStorage.setItem('rn-debug-metro-port', String(p)); } catch {}
2248
+ }
2185
2249
  function applyAppName(name) {
2186
2250
  const logo = document.querySelector('.logo');
2187
2251
  if (logo) {
@@ -2280,10 +2344,11 @@ function initSettingsPanel() {
2280
2344
  </div>
2281
2345
  </div>
2282
2346
  <div class="settings-row">
2283
- <div>
2284
- <div class="settings-label">Metro Bundler</div>
2285
- <div class="settings-hint">CDP target discovery on :8081</div>
2347
+ <div style="display:flex;flex-direction:column;gap:2px">
2348
+ <div class="settings-label">Metro Bundler Port</div>
2349
+ <div class="settings-hint">Port for CDP target discovery (default: 8081)</div>
2286
2350
  </div>
2351
+ <input id="metroPortInput" type="number" class="net-search-input" style="width:70px;text-align:center" value="${getStoredMetroPort()}" />
2287
2352
  </div>
2288
2353
  </div>
2289
2354
  <div class="settings-section">
@@ -2334,6 +2399,7 @@ function initSettingsPanel() {
2334
2399
  <div class="about-links" style="display:flex;gap:16px;justify-content:center">
2335
2400
  <span class="about-link" id="linkGithub">GitHub</span>
2336
2401
  <span class="about-link" id="linkDocs">Documentation</span>
2402
+ <span class="about-link" id="linkLinkedIn">Developer LinkedIn</span>
2337
2403
  </div>
2338
2404
  </div>
2339
2405
  </div>
@@ -2383,6 +2449,9 @@ function initSettingsPanel() {
2383
2449
  $('linkDocs')?.addEventListener('click', () => {
2384
2450
  window.electronAPI?.openExternal('https://github.com/sharanagouda/react-native-debugger#readme');
2385
2451
  });
2452
+ $('linkLinkedIn')?.addEventListener('click', () => {
2453
+ window.electronAPI?.openExternal('https://www.linkedin.com/in/sharanagoudamk/');
2454
+ });
2386
2455
 
2387
2456
  // App name
2388
2457
  $('appNameInput').addEventListener('change', (e) => {
@@ -2396,6 +2465,15 @@ function initSettingsPanel() {
2396
2465
  applyAppName('ReactoRadar');
2397
2466
  });
2398
2467
 
2468
+ // Metro Port
2469
+ $('metroPortInput')?.addEventListener('change', (e) => {
2470
+ let port = parseInt(e.target.value.trim());
2471
+ if (isNaN(port) || port < 1024 || port > 65535) port = 8081;
2472
+ e.target.value = port;
2473
+ setStoredMetroPort(port);
2474
+ window.electronAPI?.setMetroPort(port);
2475
+ });
2476
+
2399
2477
  // Font size controls
2400
2478
  $('fontSizeDown').addEventListener('click', () => {
2401
2479
  let size = getStoredFontSize();
@@ -2409,6 +2487,9 @@ function initSettingsPanel() {
2409
2487
  setStoredFontSize(size);
2410
2488
  applyFontSize(size);
2411
2489
  });
2490
+
2491
+ // Apply update banner if update info arrived before settings panel was created
2492
+ _applyUpdateBanner();
2412
2493
  }
2413
2494
 
2414
2495
  // Apply saved theme + font size + app name on load
@@ -2416,6 +2497,9 @@ applyTheme(getStoredTheme());
2416
2497
  applyFontSize(getStoredFontSize());
2417
2498
  applyAppName(getStoredAppName());
2418
2499
 
2500
+ // Send stored metro port to backend
2501
+ window.electronAPI?.setMetroPort(getStoredMetroPort());
2502
+
2419
2503
  // ─────────────────────────────────────────────────────────────────────────────
2420
2504
  // SOURCES PANEL — CDP-based file browser + breakpoints
2421
2505
  // ─────────────────────────────────────────────────────────────────────────────
@@ -2667,7 +2751,8 @@ async function loadSourceFile(filepath) {
2667
2751
  // Strategy 2: Fetch from Metro
2668
2752
  if (!source) {
2669
2753
  try {
2670
- const resp = await fetch(`http://localhost:8081/${filepath}?platform=ios&dev=true`);
2754
+ const port = getStoredMetroPort();
2755
+ const resp = await fetch(`http://localhost:${port}/${filepath}?platform=ios&dev=true`);
2671
2756
  if (resp.ok) source = await resp.text();
2672
2757
  } catch {}
2673
2758
  }
package/main.js CHANGED
@@ -55,7 +55,8 @@ app.whenReady().then(async () => {
55
55
  // Check for updates (non-blocking)
56
56
  checkForUpdates();
57
57
  startBridgeServers();
58
- startReactDevToolsServer();
58
+ // React DevTools relay NOT started by default — it blocks RN's built-in inspector.
59
+ // Started on-demand when user clicks React tab or Cmd+R.
59
60
  setupMetroCDPProxy();
60
61
  setupIPC();
61
62
  buildMenu();
@@ -106,6 +107,18 @@ async function createMainWindow() {
106
107
  }
107
108
 
108
109
  // ─── Update Checker ──────────────────────────────────────────────────────────
110
+ function _semverCompare(a, b) {
111
+ // Returns 1 if a > b, -1 if a < b, 0 if equal
112
+ const pa = (a || '').split('.').map(Number);
113
+ const pb = (b || '').split('.').map(Number);
114
+ for (let i = 0; i < 3; i++) {
115
+ const va = pa[i] || 0, vb = pb[i] || 0;
116
+ if (va > vb) return 1;
117
+ if (va < vb) return -1;
118
+ }
119
+ return 0;
120
+ }
121
+
109
122
  function checkForUpdates() {
110
123
  const currentVersion = require('./package.json').version;
111
124
  https.get('https://registry.npmjs.org/reactoradar/latest', (res) => {
@@ -114,9 +127,16 @@ function checkForUpdates() {
114
127
  res.on('end', () => {
115
128
  try {
116
129
  const latest = JSON.parse(data).version;
117
- if (latest && latest !== currentVersion) {
118
- // Notify the renderer to show an update banner
119
- mainWindow?.webContents.send('update-available', { current: currentVersion, latest });
130
+ if (latest && _semverCompare(latest, currentVersion) > 0) {
131
+ // Send with retries to ensure renderer catches it after did-finish-load
132
+ const payload = { current: currentVersion, latest };
133
+ [500, 2000, 5000].forEach(delay => {
134
+ setTimeout(() => {
135
+ if (mainWindow && !mainWindow.isDestroyed()) {
136
+ mainWindow.webContents.send('update-available', payload);
137
+ }
138
+ }, delay);
139
+ });
120
140
  console.log(`[Update] New version available: ${latest} (current: ${currentVersion})`);
121
141
  }
122
142
  } catch {}
@@ -317,6 +337,8 @@ function setupIPC() {
317
337
  });
318
338
 
319
339
  ipcMain.on('open-react-devtools', () => {
340
+ // Start the relay server if not already running
341
+ if (!reactDTServer) startReactDevToolsServer();
320
342
  // Open standalone react-devtools window
321
343
  const rdtWin = new BrowserWindow({
322
344
  width: 1100,
@@ -332,6 +354,14 @@ function setupIPC() {
332
354
 
333
355
  // clear-all is handled by renderer via clear-all-ui IPC from menu
334
356
 
357
+ ipcMain.on('set-metro-port', (_, port) => {
358
+ const p = parseInt(port);
359
+ if (isNaN(p) || p < 1024 || p > 65535) return;
360
+ PORTS.METRO = p;
361
+ fetchCDPTargets();
362
+ mainWindow?.webContents.send('ports', PORTS);
363
+ });
364
+
335
365
  ipcMain.on('set-network-capture', (_, enabled) => {
336
366
  // Broadcast to connected RN apps so they can stop/start intercepting
337
367
  networkClients.forEach(ws => {
@@ -391,7 +421,7 @@ function setupIPC() {
391
421
  // Also try to detect from Metro's /json endpoint
392
422
  try {
393
423
  const result = require('child_process').execSync(
394
- "lsof -i :8081 -t 2>/dev/null | head -1 | xargs -I{} lsof -p {} -Fn 2>/dev/null | grep '^n/' | grep 'node_modules' | head -1 | sed 's|^n||;s|/node_modules.*||'",
424
+ `lsof -i :${PORTS.METRO} -t 2>/dev/null | head -1 | xargs -I{} lsof -p {} -Fn 2>/dev/null | grep '^n/' | grep 'node_modules' | head -1 | sed 's|^n||;s|/node_modules.*||'`,
395
425
  { encoding: 'utf8', timeout: 3000 }
396
426
  ).trim();
397
427
  if (result && fs.existsSync(result)) candidates.unshift(result);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "reactoradar",
3
3
  "productName": "ReactoRadar",
4
- "version": "1.5.2",
4
+ "version": "1.5.4",
5
5
  "description": "macOS debugger for React Native — Console, Sources, Network, Performance, Memory, Redux, AsyncStorage, React tree. Supports RN 0.74+ with Hermes and New Architecture.",
6
6
  "main": "main.js",
7
7
  "bin": {
package/preload.js CHANGED
@@ -26,6 +26,7 @@ contextBridge.exposeInMainWorld('electronAPI', {
26
26
  setNetworkCapture: (enabled) => ipcRenderer.send('set-network-capture', enabled),
27
27
  setStackTraceCapture: (enabled) => ipcRenderer.send('set-stack-trace-capture', enabled),
28
28
  setNetworkThrottle: (profile) => ipcRenderer.send('set-network-throttle', profile),
29
+ setMetroPort: (port) => ipcRenderer.send('set-metro-port', port),
29
30
  readSourceFile: (filepath) => ipcRenderer.invoke('read-source-file', filepath),
30
31
  openExternal: (url) => ipcRenderer.send('open-external', url),
31
32
  });
package/sdk/RNDebugSDK.js CHANGED
@@ -34,6 +34,49 @@ let _stackTraceEnabled = false; // Disabled by default for performance
34
34
  let _throttleProfile = 'none'; // 'none', 'fast3g', 'slow3g', 'offline'
35
35
  const THROTTLE_DELAYS = { none: 0, fast3g: 500, slow3g: 2000, offline: -1 };
36
36
 
37
+ // ─── SDK Pause/Resume (allows inspector to work without SDK interference) ────
38
+ // When paused, console/fetch/XHR interception is disabled so the RN inspector
39
+ // and CDP debugger can work without conflicts. Controlled via the debugger app.
40
+ let _sdkPaused = false;
41
+
42
+ function _isSDKActive() {
43
+ return !_sdkPaused;
44
+ }
45
+
46
+ // ─── Debugger Detection ──────────────────────────────────────────────────────
47
+ // Detect if a CDP debugger (Chrome DevTools / Hermes inspector) is attached.
48
+ // When detected, we back off our patches to avoid conflicts with the inspector.
49
+ let _debuggerDetected = false;
50
+ let _debuggerCheckInterval = null;
51
+
52
+ function _checkDebuggerAttached() {
53
+ // Method 1: Check if Hermes debugger globals are set
54
+ const hermesDebugger = !!(global.__DEBUGGER_CONNECTED__ || global.__HERMES_DEBUGGER_CONNECTED__);
55
+ // Method 2: Check React DevTools hook for debugger attachment
56
+ const rdtHook = global.__REACT_DEVTOOLS_GLOBAL_HOOK__;
57
+ const rdtDebugger = !!(rdtHook && rdtHook._debuggerAttached);
58
+ // Method 3: Check if CDP is connected via the inspector agent
59
+ const inspectorConnected = !!(global.__inspectorGlobalObject || global.__inspector);
60
+
61
+ const wasDetected = _debuggerDetected;
62
+ _debuggerDetected = hermesDebugger || rdtDebugger || inspectorConnected;
63
+
64
+ if (_debuggerDetected && !wasDetected) {
65
+ _console.log('[RNDebugSDK] Debugger detected — SDK interception paused to avoid inspector conflicts. Use the ReactoRadar app to resume.');
66
+ } else if (!_debuggerDetected && wasDetected) {
67
+ _console.log('[RNDebugSDK] Debugger disconnected — SDK interception resumed.');
68
+ }
69
+ }
70
+
71
+ // Check periodically (every 3s) — lightweight, no performance impact
72
+ _debuggerCheckInterval = setInterval(_checkDebuggerAttached, 3000);
73
+ // Also check once immediately after a short delay (debugger may attach during startup)
74
+ setTimeout(_checkDebuggerAttached, 1000);
75
+
76
+ function _shouldIntercept() {
77
+ return _isSDKActive() && !_debuggerDetected;
78
+ }
79
+
37
80
  // ─── WebSocket Factory ────────────────────────────────────────────────────────
38
81
  function makeChannel(port, name, onMessage) {
39
82
  let ws = null, queue = [], connected = false;
@@ -72,6 +115,21 @@ const mainCh = makeChannel(PORTS.NETWORK_AND_CONSOLE, 'main', (msg) => {
72
115
  if (msg.action === 'set-network-capture') _networkCaptureEnabled = !!msg.enabled;
73
116
  if (msg.action === 'set-throttle') _throttleProfile = msg.profile || 'none';
74
117
  if (msg.action === 'set-stack-trace') _stackTraceEnabled = !!msg.enabled;
118
+ // Pause/Resume SDK interception (allows inspector to work)
119
+ if (msg.action === 'pause-sdk') {
120
+ _sdkPaused = true;
121
+ _console.log('[RNDebugSDK] SDK paused — inspector/debugger can now inspect the app freely.');
122
+ mainCh.send({ type: 'control', action: 'sdk-status', paused: true });
123
+ }
124
+ if (msg.action === 'resume-sdk') {
125
+ _sdkPaused = false;
126
+ _console.log('[RNDebugSDK] SDK resumed — interception re-enabled.');
127
+ mainCh.send({ type: 'control', action: 'sdk-status', paused: false });
128
+ }
129
+ // Query current status
130
+ if (msg.action === 'query-sdk-status') {
131
+ mainCh.send({ type: 'control', action: 'sdk-status', paused: _sdkPaused, debuggerDetected: _debuggerDetected });
132
+ }
75
133
  }
76
134
  });
77
135
  const reduxCh = makeChannel(PORTS.REDUX, 'redux');
@@ -130,6 +188,9 @@ LEVELS.forEach(level => {
130
188
  _console[level] = console[level].bind(console);
131
189
  console[level] = (...args) => {
132
190
  _console[level](...args);
191
+ // Skip interception when SDK is paused or debugger is attached
192
+ // This prevents double-logging and message queue deadlocks with CDP
193
+ if (!_shouldIntercept()) return;
133
194
  const structuredArgs = args.map(serializeArg);
134
195
  const message = args.map(a => {
135
196
  if (typeof a === 'string') return a;
@@ -167,6 +228,10 @@ function _flattenHeaders(h) {
167
228
  // ─── Fetch Intercept ─────────────────────────────────────────────────────────
168
229
  const _fetch = global.fetch;
169
230
  global.fetch = async (input, init = {}) => {
231
+ // When SDK is paused or debugger is attached, pass through without interception
232
+ // This prevents racing with CDP's own Fetch.enable domain
233
+ if (!_shouldIntercept()) return _fetch(input, init);
234
+
170
235
  // Throttle: simulate slow network or offline
171
236
  const delay = THROTTLE_DELAYS[_throttleProfile] || 0;
172
237
  if (delay === -1) return Promise.reject(new TypeError('Network request failed (offline throttle)'));
@@ -236,24 +301,24 @@ global.fetch = async (input, init = {}) => {
236
301
  return _setHeader.apply(xhr, arguments);
237
302
  };
238
303
 
239
- // Wrap send
240
- const _send = xhr.send.bind(xhr);
241
- xhr.send = function(body) {
242
- if (_networkCaptureEnabled && !meta.sent) {
243
- meta.sent = true;
244
- let reqBody = null;
245
- if (body != null) {
246
- try { reqBody = typeof body === 'string' ? body : JSON.parse(JSON.stringify(body)); } catch { reqBody = String(body); }
247
- }
248
- mainCh.send({ type: 'network', phase: 'request', id: meta.id, url: meta.url,
249
- method: meta.method, requestHeaders: meta.headers, requestBody: reqBody });
250
- }
251
- return _send.apply(xhr, arguments);
252
- };
304
+ // Wrap send
305
+ const _send = xhr.send.bind(xhr);
306
+ xhr.send = function(body) {
307
+ if (_shouldIntercept() && _networkCaptureEnabled && !meta.sent) {
308
+ meta.sent = true;
309
+ let reqBody = null;
310
+ if (body != null) {
311
+ try { reqBody = typeof body === 'string' ? body : JSON.parse(JSON.stringify(body)); } catch { reqBody = String(body); }
312
+ }
313
+ mainCh.send({ type: 'network', phase: 'request', id: meta.id, url: meta.url,
314
+ method: meta.method, requestHeaders: meta.headers, requestBody: reqBody });
315
+ }
316
+ return _send.apply(xhr, arguments);
317
+ };
253
318
 
254
319
  // Listen for completion
255
- xhr.addEventListener('readystatechange', function() {
256
- if (xhr.readyState !== 4 || !meta.sent || !_networkCaptureEnabled) return;
320
+ xhr.addEventListener('readystatechange', function() {
321
+ if (xhr.readyState !== 4 || !meta.sent || !_shouldIntercept() || !_networkCaptureEnabled) return;
257
322
  try {
258
323
  const duration = Date.now() - meta.t0;
259
324
  if (xhr.status > 0) {
@@ -310,17 +375,8 @@ global.fetch = async (input, init = {}) => {
310
375
  _console.log('[RNDebugSDK] XHR constructor wrapped for network capture');
311
376
  }
312
377
 
313
- // Wrap immediately if available
314
- if (global.XMLHttpRequest) wrapXHR();
315
-
316
- // Also wrap after RN polyfills set up (they replace global.XMLHttpRequest)
317
- [0, 50, 200, 500].forEach(delay => {
318
- setTimeout(() => {
319
- if (global.XMLHttpRequest && !global.XMLHttpRequest.__dbgWrapped) {
320
- wrapXHR();
321
- }
322
- }, delay);
323
- });
378
+ // Wrap immediately if available
379
+ if (global.XMLHttpRequest) wrapXHR();
324
380
  })();
325
381
 
326
382
  // ─── Axios Interceptor (belt-and-suspenders with XHR patch) ──────────────────
@@ -334,8 +390,8 @@ setTimeout(() => {
334
390
  function addDbgInterceptors(instance) {
335
391
  if (!instance || !instance.interceptors || instance.__dbgInt) return;
336
392
  instance.__dbgInt = true;
337
- instance.interceptors.request.use(config => {
338
- if (!_networkCaptureEnabled) return config;
393
+ instance.interceptors.request.use(config => {
394
+ if (!_shouldIntercept() || !_networkCaptureEnabled) return config;
339
395
  const id = `ax-${Date.now()}-${Math.random().toString(36).slice(2,6)}`;
340
396
  config._dbgId = id;
341
397
  config._dbgT0 = Date.now();
@@ -348,9 +404,10 @@ setTimeout(() => {
348
404
  mainCh.send({ type:'network', phase:'request', id, url, method:(config.method||'GET').toUpperCase(), requestHeaders:h, requestBody:body });
349
405
  return config;
350
406
  }, e => Promise.reject(e));
351
- instance.interceptors.response.use(resp => {
352
- const c = resp.config || {};
353
- if (!c._dbgId) return resp;
407
+ instance.interceptors.response.use(resp => {
408
+ if (!_shouldIntercept() || !_networkCaptureEnabled) return resp;
409
+ const c = resp.config || {};
410
+ if (!c._dbgId) return resp;
354
411
  const url = c.baseURL ? c.baseURL.replace(/\/+$/,'') + '/' + (c.url||'').replace(/^\/+/,'') : (c.url||'');
355
412
  const dur = c._dbgT0 ? Date.now() - c._dbgT0 : 0;
356
413
  const rh = {};
@@ -361,9 +418,10 @@ setTimeout(() => {
361
418
  mainCh.send({ type:'network', phase:'response', id:c._dbgId, url, method:(c.method||'GET').toUpperCase(),
362
419
  status:resp.status, statusText:resp.statusText, duration:dur, responseHeaders:rh, responseBody:body });
363
420
  return resp;
364
- }, err => {
365
- const c = err?.config || {};
366
- if (c._dbgId) {
421
+ }, err => {
422
+ if (!_shouldIntercept() || !_networkCaptureEnabled) return Promise.reject(err);
423
+ const c = err?.config || {};
424
+ if (c._dbgId) {
367
425
  const url = c.baseURL ? c.baseURL.replace(/\/+$/,'') + '/' + (c.url||'').replace(/^\/+/,'') : (c.url||'');
368
426
  const dur = c._dbgT0 ? Date.now() - c._dbgT0 : 0;
369
427
  const r = err?.response;
package/styles.css CHANGED
@@ -741,7 +741,11 @@ body {
741
741
  }
742
742
  .net-row:hover { background: var(--bg3); }
743
743
  .net-row.selected { background: var(--bg4); }
744
- .net-row.error { background: rgba(255,94,114,.04); }
744
+ .net-row.error { background: rgba(255,94,114,.06); border-bottom-color: rgba(255,94,114,.10); }
745
+ .net-row.error:hover { background: rgba(255,94,114,.10); }
746
+ .net-row.error .net-path { color: var(--red); }
747
+ .net-row.error .net-host { color: rgba(255,94,114,.6); }
748
+ .net-path-error { color: var(--red) !important; }
745
749
  .net-cell {
746
750
  padding: 6px 8px;
747
751
  overflow: hidden;
@@ -757,12 +761,13 @@ body {
757
761
  .net-cell-name .method-badge { flex-shrink: 0; vertical-align: middle; }
758
762
 
759
763
  .net-status { font-weight: 700; }
760
- .s-2 { color: var(--green); }
761
- .s-3 { color: var(--yellow); }
762
- .s-4 { color: var(--orange); }
763
- .s-5 { color: var(--red); }
764
+ .s-1 { color: var(--text-mid); } /* 1xx informational */
765
+ .s-2 { color: var(--green); } /* 2xx success */
766
+ .s-3 { color: var(--yellow); } /* 3xx redirect */
767
+ .s-4 { color: var(--red); } /* 4xx client error */
768
+ .s-5 { color: var(--red); } /* 5xx server error */
764
769
  .s-pending { color: var(--text-dim); }
765
- .s-err { color: var(--red); }
770
+ .s-err { color: var(--red); font-weight: 700; }
766
771
 
767
772
  .net-type { color: var(--text-dim); font-size: 10px; }
768
773
  .net-initiator { color: var(--text-dim); font-size: 10px; }