reactoradar 1.6.8 → 1.6.11

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/AGENTS.md CHANGED
@@ -1,8 +1,10 @@
1
1
  # ReactoRadar — Architecture & Coding Rules
2
2
 
3
- > **Read this file before making ANY code changes.**
3
+ > **Read this file COMPLETELY before making ANY code changes.**
4
4
  > This document defines the architecture, panel ownership, state contracts, and rules
5
5
  > that must be followed to avoid breaking existing functionality.
6
+ >
7
+ > **NEVER release without passing the Pre-Release Checklist at the bottom.**
6
8
 
7
9
  ---
8
10
 
@@ -27,7 +29,7 @@
27
29
  | `panels/performance.js` | Performance + Memory panels — `perfState`, `handlePerfEvent`, `handleMemoryEvent`, `initMemoryPanel` |
28
30
  | `panels/native.js` | Native Logs panel — `_nativeState`, `initNativeLogsPanel`, `_appendNativeLog` |
29
31
  | `panels/react.js` | React Tree panel — just a connect button |
30
- | `panels/sources.js` | Sources panel — Metro source file browser |
32
+ | `panels/sources.js` | Sources panel — Metro source file browser (NOT initialized by default — no `panel-sources` in HTML) |
31
33
  | `init.js` | **Boot script** (loaded LAST) — IPC wiring, button handlers, memory monitor, settings apply, panel init calls |
32
34
 
33
35
  ### Other
@@ -40,7 +42,7 @@
40
42
 
41
43
  ### Script Load Order (critical)
42
44
  ```
43
- 1. app.js — state, $, esc, ts, clearAll, freeMemory (no panel dependencies)
45
+ 1. app.js — state, $, esc, ts, clearAll, freeMemory (no panel dependencies)
44
46
  2. panels/settings.js — TAB_CONFIG, isTabEnabled, localStorage helpers (used by all panels)
45
47
  3. panels/console.js — createTreeNode, showContextMenu, addConsoleLog (used by other panels)
46
48
  4. panels/network.js — depends on: console.js (showToast, showContextMenu, createTreeNode)
@@ -56,120 +58,119 @@
56
58
 
57
59
  ---
58
60
 
59
- ## Critical Rule: Do NOT Break Other Panels
61
+ ## Current Working State (v1.6.10)
60
62
 
61
- **Every panel is currently in a single `app.js` file.** Changes to one panel can silently
62
- break another. Before making any change, check:
63
+ > **This section documents exactly what works. Any change MUST preserve all of this.**
63
64
 
64
- 1. **Shared state** — Is the variable you're touching read/written by other panels?
65
- 2. **Shared functions** — Is the function you're modifying called from other panels?
66
- 3. **IPC listeners** The preload `on()` method calls `removeAllListeners(channel)`.
67
- Registering the same channel twice **kills the first listener**.
68
- 4. **Array sync** `state.redux.actions` and `state.redux.states` MUST have the same length.
69
- Never empty one without emptying the other.
65
+ ### Panel Status
66
+
67
+ | Panel | Status | Init Function | File | DOM ID |
68
+ |-------|--------|---------------|------|--------|
69
+ | Console | WORKING | `initConsolePanel()` | `panels/console.js` | `panel-console` |
70
+ | Network | WORKING | `initNetworkPanel()` | `panels/network.js` | `panel-network` |
71
+ | Redux | WORKING | `initReduxPanel()` | `panels/redux.js` | `panel-redux` |
72
+ | GA4 Events | WORKING | `initGA4Panel()` | `panels/ga4.js` | `panel-ga4` |
73
+ | AsyncStorage | WORKING | `initStoragePanel()` | `panels/storage.js` | `panel-storage` |
74
+ | Performance | WORKING | `initPerformancePanel()` | `panels/performance.js` | `panel-performance` |
75
+ | Memory | WORKING | `initMemoryPanel()` | `panels/performance.js` | `panel-memory` |
76
+ | Native Logs | WORKING | `initNativeLogsPanel()` | `panels/native.js` | `panel-native` |
77
+ | React Tree | WORKING | `initReactPanel()` | `panels/react.js` | `panel-react` |
78
+ | Settings | WORKING | `initSettingsPanel()` | `panels/settings.js` | `panel-settings` |
79
+ | Sources | NOT INITIALIZED | `initSourcesPanel()` | `panels/sources.js` | NO DOM element — `panel-sources` does NOT exist in `index.html` |
80
+
81
+ ### Init Sequence (in `init.js`)
82
+ ```
83
+ initConsolePanel();
84
+ initNetworkPanel();
85
+ initGA4Panel();
86
+ initPerformancePanel();
87
+ initMemoryPanel();
88
+ initReduxPanel();
89
+ initStoragePanel();
90
+ initReactPanel();
91
+ initNativeLogsPanel();
92
+ initSettingsPanel();
93
+ ```
94
+ **`initSourcesPanel()` is NOT called — intentional. No DOM element exists for it.**
95
+
96
+ ### IPC Channels Currently Registered (in `init.js`)
97
+ All inside `if (window.electronAPI) { }` block:
98
+ ```
99
+ ports, cdp-targets, redux-event, network-event, storage-event,
100
+ console-event, ga4-event, perf-event, clear-all-ui,
101
+ device-all-disconnected, redux-connected, network-connected,
102
+ storage-connected, react-dt-status, focus-search, app-version,
103
+ update-available, update-downloaded, trigger-open-cdp, theme-changed
104
+ ```
105
+ Native panel registers `native-log` and `native-status` inside `initNativeLogsPanel()`.
106
+
107
+ ### SDK Platform Detection
108
+ The SDK (`sdk/RNDebugSDK.js`) auto-detects the platform at runtime:
109
+ - **Android emulator** → `10.0.2.2` (requires `adb reverse` on ports 9090, 9091, 9092, 8097)
110
+ - **iOS simulator** → `127.0.0.1`
111
+ - **Real device** → Set `HOST_OVERRIDE` in SDK to Mac's LAN IP
112
+ - Detection uses `require('react-native').Platform.OS`
113
+
114
+ ### Setup Script (`bin/setup.js`) Behavior
115
+ 1. Copies `sdk/RNDebugSDK.js` → user's `src/debug/RNDebugSDK.js`
116
+ 2. Patches `index.js` to import SDK
117
+ 3. Store file detection order:
118
+ - Step 1: Common directories (`src/store/`, `src/redux/`, etc.) with filenames `store.*`, `index.*`
119
+ - Step 2: Root app files (`src/App.tsx`, `src/App.js`, `App.tsx`, `App.js`)
120
+ - Step 3: Deep recursive scan for files with `createStore(` or `configureStore(` call syntax
121
+ 4. Auto-patches RTK `configureStore` (adds middleware field)
122
+ 5. Auto-patches legacy `createStore` if `const middleware = [...]` pattern found
123
+ 6. Falls back to manual instructions if auto-patch fails
124
+ 7. Runs `adb reverse` for Android ports
125
+ 8. Only sets `HOST_OVERRIDE` for real device LAN IP; emulator/simulator uses auto-detect
126
+
127
+ ### Redux Integration Requirements
128
+ - Connection to port 9090 ("RN app connected") does NOT mean events are flowing
129
+ - `reduxMiddleware` or `reduxEnhancer` MUST be wired into the store
130
+ - Events only flow when `store.dispatch()` goes through the middleware/enhancer
131
+ - Thunk/function actions are safely serialized as `{type: "[Function: thunk]"}`
132
+ - State > 1MB is truncated to `{__truncated: true, sizeBytes, keys}`
70
133
 
71
134
  ---
72
135
 
73
- ## PanelsOwnership & State
136
+ ## Critical Rules DO NOT VIOLATE
74
137
 
75
- ### Console Panel
76
- - **Init:** `initConsolePanel()` (app.js)
77
- - **State owned:**
78
- - `state.console.logs` array of log entries
79
- - `state.console.levelFilters``{log, info, warn, error, debug}` booleans
80
- - `state.console.searchFilter` string
81
- - `state.console.showRedux` — boolean (show redux actions in console)
82
- - `_consolePending` — batch queue for rAF rendering
83
- - `_consoleRAF` — pending requestAnimationFrame ID
84
- - `_lastLogMsg`, `_lastLogRow`, `_lastLogCount` — log grouping state
85
- - **IPC channels:** `console-event` (line 921)
86
- - **Key functions:** `addConsoleLog()`, `flushConsoleBatch()`, `renderConsole()`, `buildLogRow()`, `buildLogBody()`
87
- - **Cross-panel dependency:** Called by Redux (`handleReduxEvent` → `addConsoleLog`)
88
- - **Constants:** `MAX_CONSOLE_LOGS = 5000`
138
+ ### 1. Every init function MUST have a null guard
139
+ ```js
140
+ function initXxxPanel() {
141
+ const panel = $('panel-xxx');
142
+ if (!panel) return; // ← REQUIRED prevents crash if DOM element is missing
143
+ panel.innerHTML = `...`;
144
+ }
145
+ ```
89
146
 
90
- ### Network Panel
91
- - **Init:** `initNetworkPanel()` (app.js)
92
- - **State owned:**
93
- - `state.network.requests` — `{id: requestObj}` map
94
- - `state.network.order` — array of request IDs
95
- - `state.network.selectedId`, `statusFilter`, `typeFilter`, `searchFilter`, `throttle`, `enabled`, `sortCol`, `sortDir`
96
- - `_netRAF` — pending requestAnimationFrame ID
97
- - **IPC channels:** `network-event` (line 357)
98
- - **Key functions:** `handleNetworkEvent()`, `renderNetwork()`, `buildNetRow()`, `selectNetRequest()`, `closeNetDetail()`
99
- - **Cross-panel dependency:** Calls `addConsoleLog()` for error toasts; calls `showToast()`
100
- - **Constants:** `NET_COLS` (column definitions)
101
-
102
- ### Redux Panel
103
- - **Init:** `initReduxPanel()` (app.js)
104
- - **State owned:**
105
- - `state.redux.actions` — array of action entries
106
- - `state.redux.states` — array of full state snapshots (**MUST be same length as actions**)
107
- - `state.redux.selected` — selected action index (-1 = none)
108
- - `state.redux.searchFilter`, `sortDir`
109
- - `_reduxCatColors`, `_reduxColorIdx` — action category color cache
110
- - **IPC channels:** `redux-event` (line 356)
111
- - **Key functions:** `handleReduxEvent()`, `renderRedux()`, `_createHighlightedTree()`, `_deepEqual()`, `_findLeafChanges()`
112
- - **Cross-panel dependency:** Calls `addConsoleLog()` to mirror actions in console
113
- - **CRITICAL:** `actions` and `states` arrays must always be the same length. Never clear one without the other.
114
- - **Constants:** `MAX_REDUX_HISTORY = 500`
147
+ ### 2. Redux arrays MUST stay in sync
148
+ `state.redux.actions` and `state.redux.states` MUST have the same length.
149
+ Never empty one without emptying the other. In `freeMemory()`, always trim both together.
115
150
 
116
- ### GA4 Events Panel
117
- - **Init:** `initGA4Panel()` (app.js)
118
- - **State owned:**
119
- - `ga4State` — standalone object: `{events, selected, searchFilter, sortDir, _raf}`
120
- - `_ga4EventColors`, `_ga4ColorIdx` — event color cache
121
- - **IPC channels:** `ga4-event` (line 360)
122
- - **Key functions:** `handleGA4Event()`, `renderGA4List()`, `renderGA4Detail()`, `renderGA4Summary()`
123
-
124
- ### Storage Panel
125
- - **Init:** `initStoragePanel()` (app.js)
126
- - **State owned:**
127
- - `state.storage.entries` — `{key: value}` map
128
- - `state.storage.keys` — ordered key array
129
- - `state.storage.selected`, `searchFilter`
130
- - `_storageRAF` — pending requestAnimationFrame ID
131
- - **IPC channels:** `storage-event` (line 358)
132
- - **Key functions:** `handleStorageEvent()`, `renderStorage()`, `renderStorageValue()`
133
-
134
- ### Performance Panel
135
- - **Init:** `initPerformancePanel()` (app.js)
136
- - **State owned:**
137
- - `perfState` — standalone object: `{fps, jsThread, uiThread, recording, data}`
138
- - **IPC channels:** `perf-event` (line 362, shared with Memory)
139
- - **Key functions:** `handlePerfEvent()`, `drawPerfGraph()`, `clearPerfCanvas()`
140
-
141
- ### Memory Panel
142
- - **Init:** `initMemoryPanel()` (app.js)
143
- - **State owned:** None (displays live values from perf events)
144
- - **IPC channels:** `perf-event` (line 362, shared with Performance)
145
- - **Key functions:** `handleMemoryEvent()`
146
-
147
- ### Native Logs Panel
148
- - **Init:** `initNativeLogsPanel()` (app.js)
149
- - **State owned:**
150
- - `_nativeState` — standalone object: `{logs, connected, platform, levelFilter, searchFilter}`
151
- - **IPC channels:** `native-log`, `native-status` (registered inside init, lines 3429, 3440)
152
- - **Key functions:** `_clearNativeLogs()`, `_appendNativeLog()`, `_renderNativeLogs()`, `_autoDetectNative()`
153
- - **Constants:** `MAX_NATIVE_LOGS`
154
-
155
- ### Settings Panel
156
- - **Init:** `initSettingsPanel()` (app.js)
157
- - **State owned:** All localStorage accessors (theme, font, app name, metro port, tab visibility, tab order, hidden URLs)
158
- - **Key functions:** `applyTheme()`, `applyFontSize()`, `applyFontFamily()`, `applyAppName()`, `applyTabVisibility()`, `_buildTabVisGrid()`, `_loadVersionHistory()`
159
- - **Constants:** `TAB_CONFIG`, `FONT_FAMILIES`
160
-
161
- ### Sources Panel
162
- - **Init:** `initSourcesPanel()` (app.js)
163
- - **Key functions:** `fetchSourceFileList()`, `renderSourceFileList()`, `buildSourceTreeNode()`, `loadSourceFile()`
164
-
165
- ### React Tree Panel
166
- - **Init:** `initReactPanel()` (app.js)
167
- - **Minimal** — just a connect button for React DevTools
151
+ ### 3. IPC channels MUST be in preload allowlist
152
+ Every new IPC channel MUST be added to the `allowed` array in `preload.js`.
153
+ If a channel is not in this array, the listener is **silently dropped** — no error, no warning.
168
154
 
169
- ---
155
+ ### 4. IPC listeners MUST NOT be registered twice
156
+ The preload `on()` method calls `removeAllListeners(channel)` before adding.
157
+ Registering the same channel twice **kills the first listener**.
158
+ ALL IPC listeners go in `init.js` inside the `if (window.electronAPI) {}` block — nowhere else.
170
159
 
171
- ## Shared UtilitiesDO NOT MODIFY without checking all callers
160
+ ### 5. CSS variables use ONLY defined variables
161
+ Themes define: `--bg`, `--bg2`, `--bg3`, `--bg4` (NOT `--bg1`)
162
+ `--text`, `--text-mid`, `--text-dim`, `--text-bright`
163
+ `--accent`, `--accent2`, `--border`, `--border2`
164
+ `--green`, `--yellow`, `--red`
165
+ **Never use `--bg1`** — it resolves to transparent/empty.
172
166
 
167
+ ### 6. clearAll() and freeMemory() — check ALL panels
168
+ - `clearAll()` wipes everything + re-renders — used for Cmd+K
169
+ - `freeMemory()` trims heavy data without clearing UI — used on disconnect/quit
170
+ - After modifying either, verify ALL panels still render correctly
171
+ - Cancel pending `requestAnimationFrame` IDs before clearing data
172
+
173
+ ### 7. Shared functions — check ALL callers before modifying
173
174
  | Function | Used By |
174
175
  |----------|---------|
175
176
  | `$(id)` | Every panel |
@@ -178,75 +179,76 @@ break another. Before making any change, check:
178
179
  | `collectEntries(val)` | `objPreview()`, `createTreeNode()` |
179
180
  | `objPreview(val)` | `createTreeNode()` |
180
181
  | `createTreeNode(key, val, collapsed)` | Console, Network, Redux, Storage, GA4 |
181
- | `createPrimitiveSpan(val)` | `createTreeNode()`, `renderConsoleArg()` |
182
182
  | `showContextMenu(e, items)` | Console, Network, Redux, Storage, GA4, Native |
183
- | `clearAll()` | IPC `clear-all-ui`, memory warning |
184
- | `freeMemory()` | IPC `device-all-disconnected` (debounced) |
185
- | `clearActiveTab()` | Keyboard shortcut Cmd+K |
183
+ | `addConsoleLog(entry)` | Console IPC, Redux (`handleReduxEvent`), Network (error toasts) |
186
184
  | `isTabEnabled(tabId)` | Redux, GA4, Storage, Native, Performance |
187
185
  | `formatSize(bytes)` | Storage, Network, Memory |
188
- | `switchPanel(panel)` | Navigation, tab visibility, toasts |
189
- | `updateDeviceBanner(service, on)` | IPC connection handlers |
190
- | `showToast(msg, type, panel)` | Network |
186
+
187
+ ### 8. package.json `files` field MUST include all runtime files
188
+ Current required entries:
189
+ ```json
190
+ "files": ["main.js", "preload.js", "index.html", "app.js", "init.js", "panels/", "styles.css", "sdk/", "bin/", "assets/", "AGENTS.md"]
191
+ ```
192
+ **If you add a new file, add it here or it won't be published to npm.**
191
193
 
192
194
  ---
193
195
 
194
- ## IPC Channel Registry
195
-
196
- ### Renderer listens (app.js → via preload allowlist)
197
-
198
- | Channel | Handler | Panel |
199
- |---------|---------|-------|
200
- | `ports` | Sets `state.ports` | Global |
201
- | `cdp-targets` | Updates CDP button | Global |
202
- | `redux-event` | `handleReduxEvent` | Redux |
203
- | `network-event` | `handleNetworkEvent` | Network |
204
- | `storage-event` | `handleStorageEvent` | Storage |
205
- | `console-event` | `addConsoleLog` | Console |
206
- | `ga4-event` | `handleGA4Event` | GA4 |
207
- | `perf-event` | `handlePerfEvent` + `handleMemoryEvent` | Perf + Memory |
208
- | `redux-connected` | `updateDeviceBanner` + cancel disconnect timer | Global |
209
- | `network-connected` | `updateDeviceBanner` + cancel disconnect timer | Global |
210
- | `storage-connected` | `updateDeviceBanner` + cancel disconnect timer | Global |
211
- | `react-dt-status` | `updateDeviceBanner` | Global |
212
- | `clear-all-ui` | `clearAll()` | Global |
213
- | `device-all-disconnected` | debounced `freeMemory()` | Global |
214
- | `app-version` | Sets `state._appVersion`, `state._isPackaged` | Settings |
215
- | `update-available` | `_applyUpdateBanner()` | Settings |
216
- | `update-downloaded` | `_applyUpdateBanner()` | Settings |
217
- | `trigger-open-cdp` | Opens CDP | Global |
218
- | `theme-changed` | Applies theme | Settings |
219
- | `focus-search` | Focuses active panel search | Global |
220
- | `native-log` | Appends native log | Native (registered inside init) |
221
- | `native-status` | Updates native connection status | Native (registered inside init) |
222
-
223
- ### Preload allowlist (preload.js)
224
-
225
- **Every new IPC channel MUST be added to the `allowed` array in `preload.js` line 10-14.**
226
- If a channel is not in this array, the listener is silently dropped — no error, no warning.
227
-
228
- ### Main process sends (main.js)
229
-
230
- | Channel | Sent from |
231
- |---------|-----------|
232
- | `redux-event` | `startBridge` callback for Redux bridge (port 9090) |
233
- | `network-event` | `startBridge` callback for Network bridge (port 9092) |
234
- | `storage-event` | `startBridge` callback for Storage bridge (port 9091) |
235
- | `console-event` | `startBridge` callback for Network bridge (type=console) |
236
- | `perf-event` | `startBridge` callback for Network bridge (type=perf) |
237
- | `ga4-event` | `startBridge` callback for Network bridge (type=ga4) |
238
- | `*-connected` | `startBridge` on WS connect/disconnect |
239
- | `device-all-disconnected` | `startBridge` when all 3 bridges have 0 clients |
240
- | `clear-all-ui` | Menu Cmd+K handler |
241
- | `app-version` | `createMainWindow` on `did-finish-load` |
242
- | `native-log` | Native log process stdout parser |
243
- | `native-status` | Native log start/stop/error |
196
+ ## Panels Ownership & State
244
197
 
245
- ---
198
+ ### Console Panel (`panels/console.js`)
199
+ - **State:** `state.console.logs`, `state.console.levelFilters`, `state.console.searchFilter`, `state.console.showRedux`
200
+ - **Private state:** `_consolePending`, `_consoleRAF`, `_lastLogMsg`, `_lastLogRow`, `_lastLogCount`
201
+ - **IPC:** `console-event` → `addConsoleLog()`
202
+ - **Cross-panel:** Called by Redux (`handleReduxEvent` → `addConsoleLog`)
203
+ - **Constants:** `MAX_CONSOLE_LOGS = 5000`
246
204
 
247
- ## Main Process (main.js) — Structure
205
+ ### Network Panel (`panels/network.js`)
206
+ - **State:** `state.network.requests`, `state.network.order`, `state.network.selectedId`, `statusFilter`, `typeFilter`, `searchFilter`, `sortCol`, `sortDir`
207
+ - **Private state:** `_netRAF`
208
+ - **IPC:** `network-event` → `handleNetworkEvent()`
209
+ - **Cross-panel:** Calls `addConsoleLog()`, `showToast()`, `showContextMenu()`
248
210
 
249
- ### WebSocket Bridges
211
+ ### Redux Panel (`panels/redux.js`)
212
+ - **State:** `state.redux.actions`, `state.redux.states` (MUST be same length), `state.redux.selected`
213
+ - **IPC:** `redux-event` → `handleReduxEvent()`
214
+ - **Cross-panel:** Calls `addConsoleLog()` to mirror actions in console
215
+ - **Constants:** `MAX_REDUX_HISTORY = 500`
216
+
217
+ ### GA4 Events Panel (`panels/ga4.js`)
218
+ - **State:** `ga4State` standalone object
219
+ - **IPC:** `ga4-event` → `handleGA4Event()`
220
+
221
+ ### AsyncStorage Panel (`panels/storage.js`)
222
+ - **State:** `state.storage.entries`, `state.storage.keys`, `state.storage.selected`
223
+ - **Private state:** `_storageRAF`
224
+ - **IPC:** `storage-event` → `handleStorageEvent()`
225
+
226
+ ### Performance Panel (`panels/performance.js`)
227
+ - **State:** `perfState` standalone object
228
+ - **IPC:** `perf-event` → `handlePerfEvent()`
229
+
230
+ ### Memory Panel (`panels/performance.js`)
231
+ - **IPC:** `perf-event` → `handleMemoryEvent()` (shared channel with Performance)
232
+
233
+ ### Native Logs Panel (`panels/native.js`)
234
+ - **State:** `_nativeState` standalone object
235
+ - **IPC:** `native-log`, `native-status` (registered inside `initNativeLogsPanel`, NOT in init.js)
236
+ - **Constants:** `MAX_NATIVE_LOGS = 2000`
237
+
238
+ ### Settings Panel (`panels/settings.js`)
239
+ - **State:** All localStorage accessors
240
+ - **Shared utilities:** `TAB_CONFIG`, `isTabEnabled()`, `applyTheme()`, `getTabVisibility()`, `applyTabVisibility()`
241
+
242
+ ### React Tree Panel (`panels/react.js`)
243
+ - Minimal — just a connect button for React DevTools
244
+
245
+ ### Sources Panel (`panels/sources.js`)
246
+ - **NOT INITIALIZED** — `panel-sources` does not exist in `index.html`
247
+ - `initSourcesPanel()` has a null guard and returns early
248
+
249
+ ---
250
+
251
+ ## WebSocket Bridges (main.js)
250
252
 
251
253
  | Port | Name | Client Set | Events Carried |
252
254
  |------|------|------------|----------------|
@@ -260,82 +262,103 @@ If a channel is not in this array, the listener is silently dropped — no error
260
262
  2. Destroy `devtoolsWindow`
261
263
  3. Close `reactDTServer` + clients
262
264
  4. Close all bridge servers + clients
263
- 5. Kill `_nativeLogProcess` (second `before-quit` handler inside `setupIPC`)
265
+ 5. Kill `_nativeLogProcess`
264
266
 
265
267
  ---
266
268
 
267
269
  ## Rules for Making Changes
268
270
 
269
- ### 1. Adding a new panel
270
- - Add init function: `initXxxPanel()`
271
- - Add to `TAB_CONFIG` array
272
- - Add to init sequence at bottom of app.js
273
- - Add state to `clearAll()` and `freeMemory()` if the panel stores data
274
- - Add case to `clearActiveTab()` if the panel has clearable data
275
-
276
- ### 2. Adding a new IPC channel
277
- - Add `ipcMain.on/handle` in `main.js` `setupIPC()`
278
- - Add channel name to `preload.js` allowed array (line 10-14)
279
- - Add `window.electronAPI.on()` in app.js or expose send method in preload
280
- - **Test:** Verify the listener fires by adding a `console.log` in the handler
281
-
282
- ### 3. Modifying shared state
283
- - Check all panels that read/write the field (see tables above)
284
- - **Redux arrays:** `actions` and `states` must stay in sync
285
- - **rAF IDs:** Cancel with `cancelAnimationFrame()` before clearing data
286
- - **freeMemory():** Only trim, never wipe arrays that other panels index into
287
-
288
- ### 4. Modifying clearAll() or freeMemory()
289
- - `clearAll()` wipes everything + re-renders — used for explicit user action (Cmd+K)
290
- - `freeMemory()` trims heavy data without clearing UI — used on disconnect/quit
291
- - After modifying either, verify ALL panels still render correctly
292
- - Test: Cmd+K clears all panels, device disconnect doesn't break subsequent events
271
+ ### Adding a new panel
272
+ 1. Create `panels/newpanel.js` with `function initNewPanel() { const panel = $('panel-new'); if (!panel) return; ... }`
273
+ 2. Add `<div id="panel-new" class="panel"></div>` to `index.html`
274
+ 3. Add `<script src="panels/newpanel.js"></script>` to `index.html` BEFORE `init.js`
275
+ 4. Add `initNewPanel();` to `init.js` init sequence
276
+ 5. Add to `TAB_CONFIG` array in `panels/settings.js`
277
+ 6. Add state to `clearAll()` and `freeMemory()` in `app.js`
278
+ 7. Add case to `clearActiveTab()` in `app.js`
279
+
280
+ ### Adding a new IPC channel
281
+ 1. Add `ipcMain.on/handle` in `main.js` `setupIPC()`
282
+ 2. Add channel name to `preload.js` allowed array
283
+ 3. Add `window.electronAPI.on()` in `init.js` inside the `if (window.electronAPI)` block
284
+ 4. **Test:** Verify the listener fires
285
+
286
+ ### Modifying the SDK
287
+ 1. Test on BOTH Android emulator AND iOS simulator
288
+ 2. Verify `adb reverse` works for Android
289
+ 3. Test hot reload — ensure no timer/WebSocket leaks
290
+ 4. Test with Redux Thunk actions (function dispatches)
291
+ 5. Test with large Redux state (>1MB)
292
+ 6. Test with binary network responses (images, videos)
293
293
 
294
- ### 5. Modifying the object tree renderer
295
- - `collectEntries()`, `objPreview()`, `createTreeNode()` are used by Console, Network, Redux, Storage, GA4
296
- - **Any change to these functions affects ALL panels** that render object trees
297
- - `_createHighlightedTree()` in Redux is a SEPARATE tree renderer — changes to `createTreeNode` do NOT automatically apply to Redux diff trees
294
+ ---
298
295
 
299
- ### 6. CSS variable naming
300
- - Themes define: `--bg`, `--bg2`, `--bg3`, `--bg4` (NOT `--bg1`)
301
- - `--text`, `--text-mid`, `--text-dim`, `--text-bright`
302
- - `--accent`, `--accent2`, `--border`, `--border2`
303
- - `--green`, `--yellow`, `--red`
304
- - **Never use undefined variables** — the value resolves to transparent/empty
296
+ ## Pre-Release Checklist
305
297
 
306
- ---
298
+ > **MANDATORY — Do NOT publish without passing ALL checks.**
307
299
 
308
- ## SDK Integration (in user's React Native app)
300
+ ### Automated Checks (run these commands)
301
+ ```bash
302
+ # 1. Syntax check all files
303
+ for f in app.js init.js main.js preload.js panels/*.js sdk/RNDebugSDK.js bin/setup.js; do
304
+ node -c "$f" || echo "FAIL: $f"
305
+ done
309
306
 
310
- The SDK (`RNDebugSDK.js`) has two parts:
311
- 1. **Auto-connect** WebSocket connections to ports 9090/9091/9092 open on import
312
- 2. **Manual wiring** — Redux requires adding `reduxMiddleware` or `reduxEnhancer` to the store
307
+ # 2. No duplicate functions
308
+ grep -rhn "^function [a-zA-Z_]" app.js init.js panels/*.js | sed 's/(.*//' | sort | uniq -c | sort -rn | awk '$1 > 1'
313
309
 
314
- `npx reactoradar setup` handles:
315
- - Copying SDK to `src/debug/RNDebugSDK.js`
316
- - Patching entry file (`index.js`) to import SDK
317
- - Auto-patching RTK `configureStore` with middleware
318
- - **Legacy `createStore`**: Only prints manual instructions (does NOT auto-patch)
310
+ # 3. No duplicate top-level variables
311
+ grep -rhn "^const [a-zA-Z_]\|^let [a-zA-Z_]" app.js init.js panels/*.js | sed 's/ =.*//' | sort | uniq -c | sort -rn | awk '$1 > 1'
319
312
 
320
- If Redux is not working:
321
- 1. Check if `reduxMiddleware` or `reduxEnhancer` is wired into the store
322
- 2. Connection to port 9090 ("RN app connected") does NOT mean events are flowing
323
- 3. Events only flow when `store.dispatch()` goes through the middleware/enhancer
313
+ # 4. All critical functions exist
314
+ for fn in handleReduxEvent handleNetworkEvent handleStorageEvent handleGA4Event handlePerfEvent handleMemoryEvent clearAll freeMemory updateDeviceBanner initConsolePanel initNetworkPanel initGA4Panel initPerformancePanel initMemoryPanel initReduxPanel initStoragePanel initReactPanel initNativeLogsPanel initSettingsPanel addConsoleLog renderConsole renderNetwork renderRedux renderStorage renderGA4List closeNetDetail clearPerfCanvas showContextMenu createTreeNode isTabEnabled formatSize showToast takeScreenshot collectEntries _applyUpdateBanner _showChangelog; do
315
+ found=$(grep -rln "function $fn\b" app.js init.js panels/*.js 2>/dev/null | head -1)
316
+ [ -z "$found" ] && echo "MISSING: $fn"
317
+ done
318
+
319
+ # 5. npm pack includes all files
320
+ npm pack --dry-run 2>&1 | grep -c "panels/\|init.js\|sdk/\|bin/"
321
+ # Expected: 15+ files
322
+
323
+ # 6. IPC channels all in preload allowlist
324
+ # Every channel registered in init.js must be in preload.js allowed array
325
+ ```
326
+
327
+ ### Manual Checks (test in the running app)
328
+ - [ ] **App launches** — `npm start` opens the window, no blank screen
329
+ - [ ] **Console** — logs appear, level filters work, search works, Cmd+K clears
330
+ - [ ] **Network** — requests appear, detail panel opens/closes, search works, Cmd+K clears
331
+ - [ ] **Redux** — actions appear, state diff shows correctly, Cmd+K clears, works after disconnect/reconnect
332
+ - [ ] **GA4** — events appear, summary tab works, color toggle works
333
+ - [ ] **Storage** — keys appear, values render, search works
334
+ - [ ] **Performance** — Record button toggles, graphs render when data arrives
335
+ - [ ] **Memory** — heap values display when device connected
336
+ - [ ] **Native Logs** — Connect button works for Android/iOS, logs stream, Cmd+K clears
337
+ - [ ] **Settings** — theme switch works, font size changes, tab visibility toggles, version history loads
338
+ - [ ] **React Tree** — connect button shows
339
+ - [ ] **Device disconnect** — `freeMemory()` fires after 3s, panels still work after reconnect
340
+ - [ ] **Device reconnect** — cancel disconnect timer, new events flow immediately
341
+ - [ ] **Cmd+K** — clears active tab data (test on each tab)
342
+ - [ ] **All panels render** — switch through every tab, none show blank/white screen
343
+
344
+ ### SDK Checks (test in RN app)
345
+ - [ ] **iOS simulator** — SDK connects with `127.0.0.1`, console/network/storage data flows
346
+ - [ ] **Android emulator** — SDK connects with `10.0.2.2` (after `adb reverse`), all data flows
347
+ - [ ] **Redux** — actions appear in Redux tab AND console (if showRedux enabled)
348
+ - [ ] **Hot reload** — reload RN app, SDK reconnects, no duplicate data/timers
349
+
350
+ ### Setup Script Checks
351
+ - [ ] `npx reactoradar setup` — copies SDK, patches index.js, detects store file
352
+ - [ ] Redux auto-patch — adds `reduxMiddleware` to middleware array
353
+ - [ ] `npx reactoradar remove` — cleans up SDK and patches
354
+ - [ ] `adb reverse` — ports forwarded for Android
324
355
 
325
356
  ---
326
357
 
327
- ## Testing Checklist (after any change)
328
-
329
- - [ ] Console: logs appear, level filters work, search works, Cmd+K clears
330
- - [ ] Network: requests appear, detail panel opens, HAR export works
331
- - [ ] Redux: actions appear, state diff shows, Cmd+K clears
332
- - [ ] GA4: events appear, summary works, color toggle works
333
- - [ ] Storage: keys appear, values render, search works
334
- - [ ] Performance: FPS/JS/UI graphs render when recording
335
- - [ ] Memory: heap values appear
336
- - [ ] Native Logs: connect to adb/xcrun, logs stream, Cmd+K clears
337
- - [ ] Settings: theme switch, font size, tab visibility, version history loads
338
- - [ ] Device disconnect: `freeMemory()` fires after 3s, panels still work after reconnect
339
- - [ ] Device reconnect: cancel disconnect timer, new events flow immediately
340
- - [ ] Cmd+K: clears active tab (all tabs including native)
341
- - [ ] App quit: `devtoolsWindow` closed, bridges closed, no crash
358
+ ## Known Limitations
359
+
360
+ 1. **Sources panel** — `panel-sources` div does not exist in `index.html`. `initSourcesPanel()` is not called. Panel is dormant.
361
+ 2. **`npx reactoradar setup` store detection** if store is in an unusual file (not `App.tsx`, not `store.*`), setup may not find it
362
+ 3. **iOS real device** requires manually setting `HOST_OVERRIDE` in the SDK
363
+ 4. **`adb reverse` drops** if Android emulator restarts, `adb reverse` must be re-run
364
+ 5. **Performance `jsThread` metric** currently uses `performance.now() % 16.67` which is approximate
package/README.md CHANGED
@@ -53,6 +53,17 @@
53
53
  | **React** | Component tree and props inspector via `react-devtools-core` relay |
54
54
  | **Settings** | 9 color themes, font family/size, configurable panel visibility with drag-to-reorder, Metro port config, keyboard shortcuts, auto-update, support link |
55
55
 
56
+ ### What's New in v1.6.11
57
+
58
+ - **Auto-clear on reconnect** — All tabs reset automatically when the RN app relaunches (fresh session, no stale data)
59
+ - **No more `[object Object]`** — Safe string serialization everywhere in Console, Network, and Redux panels
60
+ - **SDK auto-detects platform** — Android emulator (`10.0.2.2`) and iOS simulator (`127.0.0.1`) detected at runtime. No manual HOST editing.
61
+ - **Setup auto-patches legacy `createStore`** — Detects `const middleware = []` pattern and wires `reduxMiddleware` automatically
62
+ - **SDK hardened** — BigInt/circular-safe JSON, Redux state >1MB truncated, binary response guard, reconnect backoff, console try/catch
63
+ - **Version rollback** — Settings > Version History shows all releases with download/install buttons
64
+ - **Panel-per-file architecture** — Each panel in its own file under `panels/`. Safer, easier to maintain.
65
+ - **Null guards everywhere** — All panel init functions, badge updates, and DOM access null-safe
66
+
56
67
  ### What's New in v1.6.0
57
68
 
58
69
  - **Auto-Update** — `.dmg` builds auto-download updates from GitHub Releases. Settings shows "Restart & Update" when ready.
@@ -134,7 +145,7 @@ Console, Network, Redux, GA4, AsyncStorage data flows automatically. No config n
134
145
  | Android real device (USB) | `10.0.2.2` | `adb reverse` tunnels over USB (auto-configured) |
135
146
  | iOS real device (USB/WiFi) | Mac's LAN IP | Auto-detected. Device must be on same WiFi as Mac. |
136
147
 
137
- `npx reactoradar setup` auto-detects your platform and sets the correct HOST.
148
+ The SDK auto-detects the platform at runtime. For iOS real devices, set `HOST_OVERRIDE` in `src/debug/RNDebugSDK.js` to your Mac's LAN IP. `npx reactoradar setup` handles this automatically.
138
149
 
139
150
  ### Uninstall
140
151
 
@@ -146,7 +157,7 @@ npx reactoradar remove
146
157
 
147
158
  | ReactoRadar | React Native | Engine | Architecture |
148
159
  |---|---|---|---|
149
- | v1.6+ | 0.74 — 0.81+ | Hermes | Old & New Architecture |
160
+ | v1.6.11+ | 0.74 — 0.81+ | Hermes | Old & New Architecture |
150
161
 
151
162
  ## Network Inspector
152
163
 
@@ -203,13 +214,26 @@ export const store = configureStore({
203
214
  });
204
215
  ```
205
216
 
206
- **Legacy Redux (createStore):**
217
+ **Legacy Redux (createStore with middleware array):**
218
+ ```js
219
+ const middleware = [];
220
+ // ... your existing middleware (saga, thunk, etc.)
221
+ if (__DEV__) {
222
+ try {
223
+ const { reduxMiddleware } = require('./debug/RNDebugSDK');
224
+ if (reduxMiddleware) middleware.push(reduxMiddleware);
225
+ } catch {}
226
+ }
227
+ const store = createStore(rootReducer, applyMiddleware(...middleware));
228
+ ```
229
+
230
+ **Legacy Redux (createStore without middleware):**
207
231
  ```js
208
232
  import { reduxEnhancer } from '../debug/RNDebugSDK';
209
233
  const store = createStore(reducer, __DEV__ ? reduxEnhancer : undefined);
210
234
  ```
211
235
 
212
- > **Note:** The import path is relative from your store file to `src/debug/RNDebugSDK`. Run `npx reactoradar setup` to auto-detect the correct path.
236
+ > **Note:** `npx reactoradar setup` auto-detects your store file and patches it. If it can't, follow the examples above. The import path is relative from your store file to `src/debug/RNDebugSDK`.
213
237
 
214
238
  ## Settings
215
239
 
@@ -293,7 +317,8 @@ const store = createStore(reducer, __DEV__ ? reduxEnhancer : undefined);
293
317
  | Network tab empty | Run Metro with `--reset-cache` |
294
318
  | Blank screen after long use | Click "Clear All Data" on the memory warning banner, or restart the app |
295
319
  | Redux shows "No actions dispatched" | Verify `reduxMiddleware` is wired in your store. Run `npx reactoradar setup` to auto-detect. |
296
- | Real device not connecting | Ensure HOST in `src/debug/RNDebugSDK.js` matches your Mac's LAN IP. Re-run `npx reactoradar setup`. |
320
+ | Android emulator not connecting | Run `adb reverse tcp:9090 tcp:9090 && adb reverse tcp:9091 tcp:9091 && adb reverse tcp:9092 tcp:9092`. Re-run after emulator restart. |
321
+ | Real device not connecting | Set `HOST_OVERRIDE` in `src/debug/RNDebugSDK.js` to your Mac's LAN IP. Re-run `npx reactoradar setup`. |
297
322
  | `XHRInterceptor.js` warning | Set `networking: false` in ReactotronConfig.js |
298
323
  | GA4 events not showing | Restart Metro with `--reset-cache` after setup |
299
324
  | Port conflict | Run `kill $(lsof -ti :9092)` to free the port, then restart |
package/app.js CHANGED
@@ -46,6 +46,14 @@ const esc = s => s == null ? '' : String(s)
46
46
  .replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
47
47
  const ts = ms => new Date(ms).toLocaleTimeString('en',{hour12:false,hour:'2-digit',minute:'2-digit',second:'2-digit'});
48
48
 
49
+ // Safe string conversion — never returns [object Object]
50
+ function safeStr(val) {
51
+ if (val == null) return '';
52
+ if (typeof val === 'string') return val;
53
+ if (typeof val === 'number' || typeof val === 'boolean') return String(val);
54
+ try { return JSON.stringify(val); } catch { return '[Complex object]'; }
55
+ }
56
+
49
57
 
50
58
  function pretty(val) {
51
59
  if (val == null) return '';
@@ -64,10 +72,12 @@ function syntaxHighlight(json) {
64
72
  }
65
73
 
66
74
  function renderJSON(val) {
75
+ if (val == null) return '<span style="color:var(--text-dim)">Empty response</span>';
67
76
  try {
68
77
  const str = typeof val === 'string' ? val : JSON.stringify(val, null, 2);
78
+ if (!str || str === '{}' || str === '""') return '<span style="color:var(--text-dim)">Empty response body</span>';
69
79
  return syntaxHighlight(esc(str));
70
- } catch { return esc(typeof val === 'object' ? JSON.stringify(val) : String(val)); }
80
+ } catch { try { return esc(JSON.stringify(val)); } catch { return esc('[Unserializable data]'); } }
71
81
  }
72
82
 
73
83
  function tryURL(url) { try { return new URL(url); } catch { return null; } }
package/bin/setup.js CHANGED
@@ -181,6 +181,7 @@ function findStoreFile(projectDir) {
181
181
  ];
182
182
  const storeNames = ['store.ts', 'store.js', 'store.tsx', 'store.jsx', 'index.ts', 'index.js', 'index.tsx'];
183
183
 
184
+ // 1. Check common store file locations
184
185
  for (const dir of searchDirs) {
185
186
  for (const name of storeNames) {
186
187
  const p = path.join(projectDir, dir, name);
@@ -193,18 +194,32 @@ function findStoreFile(projectDir) {
193
194
  }
194
195
  }
195
196
 
196
- // Deep search: recursively find any file with configureStore/createStore
197
+ // 2. Check App.tsx / App.js (common pattern: store created in root component)
198
+ const rootAppFiles = ['src/App.tsx', 'src/App.js', 'App.tsx', 'App.js', 'app/App.tsx', 'app/App.js'];
199
+ for (const f of rootAppFiles) {
200
+ const p = path.join(projectDir, f);
201
+ if (fileExists(p)) {
202
+ const content = fs.readFileSync(p, 'utf8');
203
+ if (content.includes('configureStore') || content.includes('createStore')) {
204
+ return f;
205
+ }
206
+ }
207
+ }
208
+
209
+ // 3. Deep search: recursively find files with actual createStore()/configureStore() calls
210
+ // Skip files that merely reference configureStore as a parameter/type name
197
211
  try {
198
212
  const glob = (dir, depth) => {
199
213
  if (depth > 4) return null;
200
214
  let entries;
201
215
  try { entries = fs.readdirSync(path.join(projectDir, dir), { withFileTypes: true }); } catch { return null; }
202
216
  for (const e of entries) {
203
- if (e.name === 'node_modules' || e.name.startsWith('.')) continue;
217
+ if (e.name === 'node_modules' || e.name.startsWith('.') || e.name === 'debug') continue;
204
218
  const rel = path.join(dir, e.name);
205
- if (e.isFile() && /\.(ts|js|tsx|jsx)$/.test(e.name) && /store/i.test(e.name)) {
219
+ if (e.isFile() && /\.(ts|js|tsx|jsx)$/.test(e.name)) {
206
220
  const content = fs.readFileSync(path.join(projectDir, rel), 'utf8');
207
- if (content.includes('configureStore') || content.includes('createStore')) {
221
+ // Must contain actual call: configureStore({ or createStore( — not just the word as a type/param
222
+ if (/configureStore\s*\(/.test(content) || /createStore\s*\(/.test(content)) {
208
223
  return rel;
209
224
  }
210
225
  }
@@ -265,12 +280,20 @@ async function install(projectDir) {
265
280
  process.exit(1);
266
281
  }
267
282
 
268
- // Read SDK, patch HOST
283
+ // Read SDK, patch HOST_OVERRIDE for real device support
284
+ // The SDK auto-detects Android emulator vs iOS simulator at runtime.
285
+ // For real devices, we set HOST_OVERRIDE to the Mac's LAN IP.
269
286
  let sdkContent = fs.readFileSync(sdkSrc, 'utf8');
270
- sdkContent = sdkContent.replace(
271
- /const HOST = '[^']+';/,
272
- `const HOST = '${host}';`
273
- );
287
+ const isRealDevice = reason.includes('device') || reason.includes('LAN IP');
288
+ if (isRealDevice && host !== '127.0.0.1' && host !== '10.0.2.2') {
289
+ sdkContent = sdkContent.replace(
290
+ /const HOST_OVERRIDE = null;/,
291
+ `const HOST_OVERRIDE = '${host}';`
292
+ );
293
+ log('HOST_OVERRIDE set to', C.bold + host + C.reset, '(real device LAN IP)');
294
+ } else {
295
+ log('HOST auto-detect enabled', C.dim + '(Android: 10.0.2.2, iOS: 127.0.0.1)' + C.reset);
296
+ }
274
297
  try {
275
298
  fs.writeFileSync(sdkDest, sdkContent);
276
299
  } catch (e) {
@@ -360,36 +383,75 @@ ${SDK_MARKER_END}
360
383
  } else {
361
384
  log('Redux store already has RNDebugSDK wired correctly — skipping');
362
385
  }
363
- } else if (storeContent.includes('configureStore')) {
364
- // RTK configureStore
365
- // Try to add middleware to configureStore
366
- if (storeContent.includes('middleware:') || storeContent.includes('middleware :')) {
367
- warn('Redux store found at', C.bold + storeFile + C.reset, '— has custom middleware');
368
- console.log(C.dim + ' Add manually to your middleware:' + C.reset);
369
- console.log(C.dim + ` import { reduxMiddleware } from '${relSDK}';` + C.reset);
370
- console.log(C.dim + ' middleware: (getDefault) => __DEV__' + C.reset);
371
- console.log(C.dim + ' ? getDefault().concat(reduxMiddleware)' + C.reset);
372
- console.log(C.dim + ' : getDefault(),' + C.reset);
373
- } else {
374
- // Add middleware field to configureStore
375
- const patched = storeContent.replace(
376
- /(configureStore\s*\(\s*\{)/,
377
- `$1\n middleware: (getDefaultMiddleware) =>\n __DEV__\n ? getDefaultMiddleware().concat(require('${relSDK}').reduxMiddleware)\n : getDefaultMiddleware(),`
378
- );
379
- if (patched !== storeContent) {
380
- fs.writeFileSync(storePath, patched);
381
- log('Patched', C.bold + storeFile + C.reset, '— Redux middleware wired');
382
- } else {
383
- warn('Could not auto-patch', storeFile, 'wire Redux manually');
386
+ } else if (/createStore\s*\(/.test(storeContent)) {
387
+ // Legacy createStore (check this BEFORE configureStore — a file may have both
388
+ // if the user named their wrapper function "configureStore" but uses Redux's createStore inside)
389
+ // Legacy createStore — try to auto-patch by adding reduxMiddleware to middleware array
390
+ let patched = storeContent;
391
+ let didPatch = false;
392
+
393
+ // Pattern 1: middleware array exists push reduxMiddleware into it
394
+ // e.g. const middleware = []; or const middleware = [sagaMiddleware];
395
+ if (/(?:const|let|var)\s+middleware\s*=\s*\[/.test(storeContent) && !storeContent.includes('reduxMiddleware') && !storeContent.includes('RNDebugSDK')) {
396
+ // Add the require + push after the middleware array declaration
397
+ patched = patched.replace(
398
+ /((?:const|let|var)\s+middleware\s*=\s*\[[^\]]*\];?)/,
399
+ `$1\n if (__DEV__) { try { const { reduxMiddleware } = require('${relSDK}'); if (reduxMiddleware) middleware.push(reduxMiddleware); } catch {} }`
400
+ );
401
+ if (patched !== storeContent) {
402
+ didPatch = true;
403
+ }
404
+ }
405
+
406
+ // Pattern 2: applyMiddleware(...middleware) exists but no middleware array inject inline
407
+ if (!didPatch && storeContent.includes('applyMiddleware') && !storeContent.includes('reduxMiddleware') && !storeContent.includes('RNDebugSDK')) {
408
+ // Find the if (__DEV__) block near middleware setup and add there
409
+ // Or add before the createStore call
410
+ const createStoreMatch = storeContent.match(/createStore\s*\(/);
411
+ if (createStoreMatch) {
412
+ const idx = storeContent.indexOf(createStoreMatch[0]);
413
+ const insertPoint = storeContent.lastIndexOf('\n', idx);
414
+ if (insertPoint > 0) {
415
+ patched = storeContent.slice(0, insertPoint) +
416
+ `\n if (__DEV__) { try { const { reduxMiddleware } = require('${relSDK}'); if (reduxMiddleware) middleware.push(reduxMiddleware); } catch {} }` +
417
+ storeContent.slice(insertPoint);
418
+ if (patched !== storeContent) didPatch = true;
419
+ }
420
+ }
421
+ }
422
+
423
+ if (didPatch) {
424
+ fs.writeFileSync(storePath, patched);
425
+ log('Patched', C.bold + storeFile + C.reset, '— Redux middleware wired (legacy createStore)');
426
+ } else {
427
+ warn('Legacy createStore found at', C.bold + storeFile + C.reset);
428
+ console.log(C.dim + ' Could not auto-patch. Add manually:' + C.reset);
429
+ console.log(C.dim + ` if (__DEV__) { try { const { reduxMiddleware } = require('${relSDK}'); middleware.push(reduxMiddleware); } catch {} }` + C.reset);
430
+ console.log(C.dim + ' Add this BEFORE the createStore() call in your middleware setup.' + C.reset);
384
431
  }
432
+ } else if (/configureStore\s*\(\s*\{/.test(storeContent)) {
433
+ // RTK configureStore({ ... }) — actual Redux Toolkit usage
434
+ if (storeContent.includes('middleware:') || storeContent.includes('middleware :')) {
435
+ warn('Redux store found at', C.bold + storeFile + C.reset, '— has custom middleware');
436
+ console.log(C.dim + ' Add manually to your middleware:' + C.reset);
437
+ console.log(C.dim + ` import { reduxMiddleware } from '${relSDK}';` + C.reset);
438
+ console.log(C.dim + ' middleware: (getDefault) => __DEV__' + C.reset);
439
+ console.log(C.dim + ' ? getDefault().concat(reduxMiddleware)' + C.reset);
440
+ console.log(C.dim + ' : getDefault(),' + C.reset);
441
+ } else {
442
+ const patched = storeContent.replace(
443
+ /(configureStore\s*\(\s*\{)/,
444
+ `$1\n middleware: (getDefaultMiddleware) =>\n __DEV__\n ? getDefaultMiddleware().concat(require('${relSDK}').reduxMiddleware)\n : getDefaultMiddleware(),`
445
+ );
446
+ if (patched !== storeContent) {
447
+ fs.writeFileSync(storePath, patched);
448
+ log('Patched', C.bold + storeFile + C.reset, '— Redux middleware wired (RTK)');
449
+ } else {
450
+ warn('Could not auto-patch', storeFile, '— wire Redux manually');
451
+ }
452
+ }
385
453
  }
386
- } else if (storeContent.includes('createStore')) {
387
- warn('Legacy createStore found at', C.bold + storeFile + C.reset);
388
- console.log(C.dim + ' Add manually:' + C.reset);
389
- console.log(C.dim + ` import { reduxEnhancer } from '${relSDK}';` + C.reset);
390
- console.log(C.dim + ' const store = createStore(reducer, __DEV__ ? reduxEnhancer : undefined);' + C.reset);
391
- }
392
- } else {
454
+ } else {
393
455
  warn('Redux detected but store file not found automatically');
394
456
  console.log(C.dim + ' Add to your store setup:' + C.reset);
395
457
  console.log(C.dim + ' import { reduxMiddleware } from \'./src/debug/RNDebugSDK\';' + C.reset);
package/init.js CHANGED
@@ -47,21 +47,32 @@ if (window.electronAPI) {
47
47
 
48
48
  window.electronAPI.on('clear-all-ui', clearAll);
49
49
 
50
- // When all device bridges disconnect, release heavy memory but keep logs visible.
51
- // Debounced to avoid data loss during hot reloads or flaky connections.
50
+ // Device disconnect debounced freeMemory. Reconnect clearAll (fresh session).
52
51
  let _disconnectTimer = null;
52
+ let _wasDisconnected = false;
53
+
53
54
  window.electronAPI.on('device-all-disconnected', () => {
55
+ _wasDisconnected = true;
54
56
  clearTimeout(_disconnectTimer);
55
57
  _disconnectTimer = setTimeout(() => {
56
58
  console.log('[App] All devices disconnected — freeing memory');
57
59
  freeMemory();
58
60
  }, 3000);
59
61
  });
60
- // Cancel pending free if a device reconnects
61
- const _cancelDisconnectTimer = () => { clearTimeout(_disconnectTimer); _disconnectTimer = null; };
62
- window.electronAPI.on('redux-connected', on => { if (on) _cancelDisconnectTimer(); updateDeviceBanner('redux', on); });
63
- window.electronAPI.on('network-connected', on => { if (on) _cancelDisconnectTimer(); updateDeviceBanner('network', on); });
64
- window.electronAPI.on('storage-connected', on => { if (on) _cancelDisconnectTimer(); updateDeviceBanner('storage', on); });
62
+
63
+ const _handleReconnect = () => {
64
+ clearTimeout(_disconnectTimer);
65
+ _disconnectTimer = null;
66
+ if (_wasDisconnected) {
67
+ _wasDisconnected = false;
68
+ console.log('[App] Device reconnected — clearing old session data');
69
+ clearAll();
70
+ }
71
+ };
72
+
73
+ window.electronAPI.on('redux-connected', on => { if (on) _handleReconnect(); updateDeviceBanner('redux', on); });
74
+ window.electronAPI.on('network-connected', on => { if (on) _handleReconnect(); updateDeviceBanner('network', on); });
75
+ window.electronAPI.on('storage-connected', on => { if (on) _handleReconnect(); updateDeviceBanner('storage', on); });
65
76
  window.electronAPI.on('react-dt-status', on => { updateDeviceBanner('reactDT', on); });
66
77
 
67
78
  // Cmd+F — focus the search input for the active panel
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "reactoradar",
3
3
  "productName": "ReactoRadar",
4
- "version": "1.6.8",
4
+ "version": "1.6.11",
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/panels/console.js CHANGED
@@ -401,7 +401,8 @@ function primitivePreview(val) {
401
401
  if (typeof val === 'number' || typeof val === 'boolean') return String(val);
402
402
  if (Array.isArray(val)) return `Array(${val.length})`;
403
403
  if (typeof val === 'object') return `{...}`;
404
- return String(val);
404
+ if (typeof val === 'function') return `[Function: ${val.name || 'anonymous'}]`;
405
+ return safeStr(val);
405
406
  }
406
407
 
407
408
  function createTreeNode(key, val, startCollapsed) {
@@ -505,7 +506,7 @@ function _safeStr(val) {
505
506
  if (val === undefined) return 'undefined';
506
507
  if (typeof val === 'string') return val;
507
508
  if (typeof val === 'number' || typeof val === 'boolean') return String(val);
508
- try { return JSON.stringify(val, null, 2); } catch { return String(val); }
509
+ try { return JSON.stringify(val, null, 2); } catch { return '[Complex object]'; }
509
510
  }
510
511
 
511
512
  function createPrimitiveSpan(val) {
@@ -561,7 +562,7 @@ function buildLogBody(logEntry) {
561
562
  });
562
563
  } else if (logEntry.message != null) {
563
564
  // Legacy / flat message — try to parse JSON objects out of it
564
- const msg = String(logEntry.message);
565
+ const msg = safeStr(logEntry.message);
565
566
  // Try parsing the whole message as JSON
566
567
  try {
567
568
  const parsed = JSON.parse(msg);
@@ -691,7 +692,7 @@ function buildLogRow(l) {
691
692
  items.push({ label: 'Copy as JSON', action: () => {
692
693
  const json = l.args.map(a => {
693
694
  if (a.t === 'object' || a.t === 'array') return JSON.stringify(a.v, null, 2);
694
- return String(a.v);
695
+ return safeStr(a.v);
695
696
  }).join(' ');
696
697
  navigator.clipboard.writeText(json);
697
698
  }});
package/panels/network.js CHANGED
@@ -203,13 +203,13 @@ function initNetworkPanel() {
203
203
  method: r.method || 'GET',
204
204
  url: r.url || '',
205
205
  headers: Object.entries(r.requestHeaders || {}).map(([n, v]) => ({ name: n, value: v })),
206
- postData: r.requestBody ? { mimeType: 'application/json', text: typeof r.requestBody === 'object' ? JSON.stringify(r.requestBody) : String(r.requestBody) } : undefined,
206
+ postData: r.requestBody ? { mimeType: 'application/json', text: typeof r.requestBody === 'object' ? JSON.stringify(r.requestBody) : safeStr(r.requestBody) } : undefined,
207
207
  },
208
208
  response: {
209
209
  status: r.status || 0,
210
210
  statusText: r.statusText || '',
211
211
  headers: Object.entries(r.responseHeaders || {}).map(([n, v]) => ({ name: n, value: v })),
212
- content: { size: -1, mimeType: 'application/json', text: r.responseBody ? (typeof r.responseBody === 'object' ? JSON.stringify(r.responseBody) : String(r.responseBody)) : '' },
212
+ content: { size: -1, mimeType: 'application/json', text: r.responseBody ? (typeof r.responseBody === 'object' ? JSON.stringify(r.responseBody) : safeStr(r.responseBody)) : '' },
213
213
  },
214
214
  timings: { send: 0, wait: r.duration || 0, receive: 0 },
215
215
  };
@@ -835,7 +835,7 @@ function renderNetDetailContent(r) {
835
835
  if (!keys.length) return `<div class="section-label">${title}</div><span style="color:var(--text-dim)">none</span>`;
836
836
  return `<div class="section-label">${title}</div><div class="kv-grid">${keys.map(k => {
837
837
  let val = h[k];
838
- if (val && typeof val === 'object') { try { val = JSON.stringify(val); } catch { val = String(val); } }
838
+ if (val && typeof val === 'object') { try { val = JSON.stringify(val); } catch { val = '[Complex object]'; } }
839
839
  return `<span class="kv-key">${esc(k)}</span><span class="kv-val">${esc(val)}</span>`;
840
840
  }).join('')}</div>`;
841
841
  };
@@ -870,6 +870,7 @@ function renderNetDetailContent(r) {
870
870
  const isErrStatus = _isHttpError(r);
871
871
  if (r.phase === 'error' && !r.responseBody) { body.innerHTML = `<span style="color:var(--red)">${esc(r.error || 'Request failed')}</span>`; return; }
872
872
  if (!r.responseBody && r.phase !== 'response') { body.innerHTML = '<span style="color:var(--text-dim)">Pending...</span>'; return; }
873
+ if (r.responseBody == null || r.responseBody === '') { body.innerHTML = '<span style="color:var(--text-dim)">Empty response body</span>'; return; }
873
874
  // Render as collapsible JSON tree with right-click copy
874
875
  const val = r.responseBody;
875
876
  let treeData = val;
@@ -896,13 +897,14 @@ function renderNetDetailContent(r) {
896
897
  });
897
898
  } else {
898
899
  body.innerHTML = isErrStatus
899
- ? `<span style="color:var(--red)">${esc(String(r.responseBody))}</span>`
900
+ ? `<span style="color:var(--red)">${esc(safeStr(r.responseBody))}</span>`
900
901
  : '<span style="color:var(--text-dim)">No preview available</span>';
901
902
  }
902
903
  } else if (tab === 'response') {
903
904
  const isErrStatus = _isHttpError(r);
904
905
  if (r.phase === 'error' && !r.responseBody) { body.innerHTML = `<span style="color:var(--red)">${esc(r.error || 'Request failed')}</span>`; return; }
905
906
  if (!r.responseBody && r.phase !== 'response') { body.innerHTML = '<span style="color:var(--text-dim)">Pending...</span>'; return; }
907
+ if (r.responseBody == null || r.responseBody === '') { body.innerHTML = '<span style="color:var(--text-dim)">Empty response body</span>'; return; }
906
908
  if (isErrStatus) {
907
909
  const errBanner = document.createElement('div');
908
910
  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';
package/sdk/RNDebugSDK.js CHANGED
@@ -19,8 +19,20 @@ if (typeof __DEV__ === 'undefined' || !__DEV__) {
19
19
  } else {
20
20
 
21
21
  // ─── Config ───────────────────────────────────────────────────────────────────
22
- // Android emulator → 10.0.2.2 | iOS sim → 127.0.0.1 | Device → your LAN IP
23
- const HOST = '10.0.2.2';
22
+ // Auto-detect platform: Android emulator → 10.0.2.2 | iOS sim → 127.0.0.1
23
+ // For real devices: Android uses adb reverse (so 10.0.2.2 works via port forwarding),
24
+ // iOS real device needs the Mac's LAN IP — override HOST_OVERRIDE below if needed.
25
+ const HOST_OVERRIDE = null; // Set to your Mac's LAN IP for iOS real device, e.g. '192.168.1.100'
26
+
27
+ function _detectHost() {
28
+ if (HOST_OVERRIDE) return HOST_OVERRIDE;
29
+ try {
30
+ const { Platform } = require('react-native');
31
+ if (Platform.OS === 'android') return '10.0.2.2';
32
+ return '127.0.0.1'; // iOS simulator
33
+ } catch { return '127.0.0.1'; }
34
+ }
35
+ const HOST = _detectHost();
24
36
 
25
37
  const PORTS = {
26
38
  NETWORK_AND_CONSOLE: 9092, // unified feed for network + console