@buoy-gg/redux 7.0.4 → 7.0.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.
Files changed (46) hide show
  1. package/README.md +21 -2
  2. package/lib/commonjs/index.js +44 -0
  3. package/lib/commonjs/redux/components/ReduxAutoInstrument.js +20 -0
  4. package/lib/commonjs/redux/components/ReduxModal.js +123 -68
  5. package/lib/commonjs/redux/components/index.js +8 -1
  6. package/lib/commonjs/redux/hooks/useAutoInstrumentRedux.js +32 -42
  7. package/lib/commonjs/redux/sync/reduxSyncAdapter.js +27 -8
  8. package/lib/commonjs/redux/utils/activeStoreRegistry.js +73 -0
  9. package/lib/commonjs/redux/utils/autoInstrument.js +88 -114
  10. package/lib/commonjs/redux/utils/buoyReduxMiddleware.js +20 -15
  11. package/lib/commonjs/redux/utils/devtoolsHook.js +137 -0
  12. package/lib/commonjs/redux/utils/index.js +32 -1
  13. package/lib/module/index.js +29 -4
  14. package/lib/module/redux/components/ReduxAutoInstrument.js +17 -0
  15. package/lib/module/redux/components/ReduxModal.js +122 -68
  16. package/lib/module/redux/components/index.js +2 -1
  17. package/lib/module/redux/hooks/useAutoInstrumentRedux.js +33 -43
  18. package/lib/module/redux/sync/reduxSyncAdapter.js +28 -9
  19. package/lib/module/redux/utils/activeStoreRegistry.js +63 -0
  20. package/lib/module/redux/utils/autoInstrument.js +82 -113
  21. package/lib/module/redux/utils/buoyReduxMiddleware.js +20 -15
  22. package/lib/module/redux/utils/devtoolsHook.js +130 -0
  23. package/lib/module/redux/utils/index.js +3 -1
  24. package/lib/typescript/index.d.ts +19 -5
  25. package/lib/typescript/index.d.ts.map +1 -1
  26. package/lib/typescript/redux/components/ReduxAutoInstrument.d.ts +11 -0
  27. package/lib/typescript/redux/components/ReduxAutoInstrument.d.ts.map +1 -0
  28. package/lib/typescript/redux/components/ReduxModal.d.ts +15 -1
  29. package/lib/typescript/redux/components/ReduxModal.d.ts.map +1 -1
  30. package/lib/typescript/redux/components/index.d.ts +1 -0
  31. package/lib/typescript/redux/components/index.d.ts.map +1 -1
  32. package/lib/typescript/redux/hooks/useAutoInstrumentRedux.d.ts +16 -28
  33. package/lib/typescript/redux/hooks/useAutoInstrumentRedux.d.ts.map +1 -1
  34. package/lib/typescript/redux/sync/reduxSyncAdapter.d.ts +18 -4
  35. package/lib/typescript/redux/sync/reduxSyncAdapter.d.ts.map +1 -1
  36. package/lib/typescript/redux/utils/activeStoreRegistry.d.ts +47 -0
  37. package/lib/typescript/redux/utils/activeStoreRegistry.d.ts.map +1 -0
  38. package/lib/typescript/redux/utils/autoInstrument.d.ts +46 -23
  39. package/lib/typescript/redux/utils/autoInstrument.d.ts.map +1 -1
  40. package/lib/typescript/redux/utils/buoyReduxMiddleware.d.ts +5 -1
  41. package/lib/typescript/redux/utils/buoyReduxMiddleware.d.ts.map +1 -1
  42. package/lib/typescript/redux/utils/devtoolsHook.d.ts +45 -0
  43. package/lib/typescript/redux/utils/devtoolsHook.d.ts.map +1 -0
  44. package/lib/typescript/redux/utils/index.d.ts +3 -2
  45. package/lib/typescript/redux/utils/index.d.ts.map +1 -1
  46. package/package.json +8 -5
package/README.md CHANGED
@@ -14,7 +14,7 @@ npm install @buoy-gg/core @buoy-gg/redux
14
14
 
15
15
  ## Quick start
16
16
 
17
- Zero config. Your existing store works as-is — no middleware, no wrapper. The tool auto-instruments the store when `FloatingDevTools` mounts.
17
+ Zero config. Your existing store works as-is — no middleware, no wrapper. Buoy hooks the store at creation via the official Redux DevTools integration point (which Redux Toolkit enables by default), so actions are captured from your app's very first dispatch — including everything dispatched inside thunks and RTK Query.
18
18
 
19
19
  ```tsx
20
20
  import { configureStore } from '@reduxjs/toolkit';
@@ -33,9 +33,28 @@ export default function App() {
33
33
  }
34
34
  ```
35
35
 
36
+ ### How capture works (and how to guarantee full capture)
37
+
38
+ Buoy binds to your store the earliest way available, in this order:
39
+
40
+ 1. **Store-creation hook (default, full capture)** — importing `@buoy-gg/redux` claims the Redux DevTools global that RTK checks by default. If the package loads before your store module, Buoy is inside the store from creation: every action (thunk-internal, RTK Query) is captured and time travel is real. To **guarantee** this ordering, make it the first import of your app entry:
41
+
42
+ ```tsx
43
+ // index.js — first line
44
+ import '@buoy-gg/redux';
45
+ ```
46
+
47
+ 2. **Middleware (explicit full capture)** — one line if you prefer being explicit:
48
+
49
+ ```tsx
50
+ middleware: (getDefault) => getDefault().concat(buoyReduxMiddleware),
51
+ ```
52
+
53
+ 3. **Startup fallback** — if your store was created before Buoy loaded, `FloatingDevTools` binds it automatically at app mount. Top-level dispatches are captured from startup; actions dispatched *inside* thunks/middleware aren't visible to this mode (the tool tells you when it's in it).
54
+
36
55
  ### Optional: full time travel
37
56
 
38
- To jump back to past states (not just view them), wrap your reducer:
57
+ Time travel works out of the box on path 1. On the middleware path, wrap your reducer:
39
58
 
40
59
  ```tsx
41
60
  import { withBuoyDevTools } from '@buoy-gg/redux';
@@ -45,6 +45,12 @@ Object.defineProperty(exports, "ReduxActionItem", {
45
45
  return _ReduxActionItem.ReduxActionItem;
46
46
  }
47
47
  });
48
+ Object.defineProperty(exports, "ReduxAutoInstrument", {
49
+ enumerable: true,
50
+ get: function () {
51
+ return _ReduxAutoInstrument.ReduxAutoInstrument;
52
+ }
53
+ });
48
54
  Object.defineProperty(exports, "ReduxIcon", {
49
55
  enumerable: true,
50
56
  get: function () {
@@ -57,12 +63,30 @@ Object.defineProperty(exports, "ReduxModal", {
57
63
  return _ReduxModal.ReduxModal;
58
64
  }
59
65
  });
66
+ Object.defineProperty(exports, "ReduxToolView", {
67
+ enumerable: true,
68
+ get: function () {
69
+ return _ReduxModal.ReduxToolView;
70
+ }
71
+ });
72
+ Object.defineProperty(exports, "buoyDevtoolsEnhancer", {
73
+ enumerable: true,
74
+ get: function () {
75
+ return _devtoolsHook.buoyDevtoolsEnhancer;
76
+ }
77
+ });
60
78
  Object.defineProperty(exports, "buoyReduxMiddleware", {
61
79
  enumerable: true,
62
80
  get: function () {
63
81
  return _buoyReduxMiddleware.buoyReduxMiddleware;
64
82
  }
65
83
  });
84
+ Object.defineProperty(exports, "claimDevtoolsHook", {
85
+ enumerable: true,
86
+ get: function () {
87
+ return _devtoolsHook.claimDevtoolsHook;
88
+ }
89
+ });
66
90
  Object.defineProperty(exports, "createBuoyReduxMiddleware", {
67
91
  enumerable: true,
68
92
  get: function () {
@@ -87,6 +111,12 @@ Object.defineProperty(exports, "getActiveStore", {
87
111
  return _autoInstrument.getActiveStore;
88
112
  }
89
113
  });
114
+ Object.defineProperty(exports, "getInstrumentationStatus", {
115
+ enumerable: true,
116
+ get: function () {
117
+ return _autoInstrument.getInstrumentationStatus;
118
+ }
119
+ });
90
120
  Object.defineProperty(exports, "instrumentStore", {
91
121
  enumerable: true,
92
122
  get: function () {
@@ -99,6 +129,12 @@ Object.defineProperty(exports, "isAutoInstrumentActive", {
99
129
  return _autoInstrument.isAutoInstrumentActive;
100
130
  }
101
131
  });
132
+ Object.defineProperty(exports, "isDevtoolsHookClaimed", {
133
+ enumerable: true,
134
+ get: function () {
135
+ return _devtoolsHook.isDevtoolsHookClaimed;
136
+ }
137
+ });
102
138
  Object.defineProperty(exports, "isStoreInstrumented", {
103
139
  enumerable: true,
104
140
  get: function () {
@@ -135,6 +171,12 @@ Object.defineProperty(exports, "reduxToolPreset", {
135
171
  return _preset.reduxToolPreset;
136
172
  }
137
173
  });
174
+ Object.defineProperty(exports, "registerReduxStore", {
175
+ enumerable: true,
176
+ get: function () {
177
+ return _autoInstrument.registerReduxStore;
178
+ }
179
+ });
138
180
  Object.defineProperty(exports, "replayAction", {
139
181
  enumerable: true,
140
182
  get: function () {
@@ -171,6 +213,7 @@ Object.defineProperty(exports, "withBuoyDevTools", {
171
213
  return _buoyReduxMiddleware.withBuoyDevTools;
172
214
  }
173
215
  });
216
+ var _devtoolsHook = require("./redux/utils/devtoolsHook");
174
217
  var _preset = require("./preset");
175
218
  var _buoyReduxMiddleware = require("./redux/utils/buoyReduxMiddleware");
176
219
  var _autoInstrument = require("./redux/utils/autoInstrument");
@@ -183,4 +226,5 @@ var _ReduxActionItem = require("./redux/components/ReduxActionItem");
183
226
  var _ReduxActionDetailView = require("./redux/components/ReduxActionDetailView");
184
227
  var _ReduxActionDetailContent = require("./redux/components/ReduxActionDetailContent");
185
228
  var _ReduxIcon = require("./redux/components/ReduxIcon");
229
+ var _ReduxAutoInstrument = require("./redux/components/ReduxAutoInstrument");
186
230
  var _reduxActionStore = require("./redux/utils/reduxActionStore");
@@ -0,0 +1,20 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.ReduxAutoInstrument = ReduxAutoInstrument;
7
+ var _useAutoInstrumentRedux = require("../hooks/useAutoInstrumentRedux");
8
+ /**
9
+ * Headless startup instrumentation. Auto-rendered by FloatingDevTools (same
10
+ * pattern as RouteTracker & the overlay tools) so the Redux store is bound
11
+ * from app mount — action log, MCP getState/dispatch, and desktop sync all
12
+ * work WITHOUT the user ever opening the Redux tool. Renders nothing.
13
+ *
14
+ * Safe anywhere: if react-redux is missing or there's no Provider above, the
15
+ * hook records the diagnostic and fails gracefully.
16
+ */
17
+ function ReduxAutoInstrument() {
18
+ (0, _useAutoInstrumentRedux.useAutoInstrumentRedux)();
19
+ return null;
20
+ }
@@ -5,6 +5,7 @@ Object.defineProperty(exports, "__esModule", {
5
5
  });
6
6
  exports.FREE_TIER_ACTION_LIMIT = void 0;
7
7
  exports.ReduxModal = ReduxModal;
8
+ exports.ReduxToolView = ReduxToolView;
8
9
  var _react = require("react");
9
10
  var _reactNative = require("react-native");
10
11
  var _sharedUi = require("@buoy-gg/shared-ui");
@@ -46,7 +47,8 @@ function ReduxModal({
46
47
  onClose,
47
48
  onBack,
48
49
  onMinimize,
49
- enableSharedModalDimensions = false
50
+ enableSharedModalDimensions = false,
51
+ inline = false
50
52
  }) {
51
53
  const isPro = (0, _license.useIsPro)();
52
54
  const [showProModal, setShowProModal] = (0, _react.useState)(false);
@@ -56,7 +58,8 @@ function ReduxModal({
56
58
  isInstrumented,
57
59
  isLoading: isInstrumentLoading,
58
60
  error: instrumentError,
59
- isReactReduxAvailable
61
+ isReactReduxAvailable,
62
+ captureMechanism
60
63
  } = (0, _useAutoInstrumentRedux.useAutoInstrumentRedux)();
61
64
  const {
62
65
  filteredActions,
@@ -285,6 +288,95 @@ function ReduxModal({
285
288
  // The action list owns its scrolling so FlatList virtualization works
286
289
  // (see ZustandModal — same pattern, same tool-bench evidence).
287
290
  const listOwnsScroll = selectedAction == null;
291
+ const body = /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
292
+ style: styles.container,
293
+ children: selectedAction && selectedActionIndex !== null ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_ReduxActionDetailContent.ReduxActionDetailContent, {
294
+ action: selectedAction,
295
+ actions: visibleActions,
296
+ selectedIndex: selectedActionIndex,
297
+ onIndexChange: handleIndexChange,
298
+ disableInternalFooter: true
299
+ }) : /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
300
+ children: [instrumentError && !isInstrumented && !_reduxActionStore.reduxActionStore.isCaptureSuppressed() && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
301
+ style: styles.errorBanner,
302
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.AlertTriangle, {
303
+ size: 14,
304
+ color: _sharedUi.macOSColors.semantic.error
305
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
306
+ style: styles.errorText,
307
+ children: isReactReduxAvailable ? "Not inside a Redux Provider. Wrap your app with <Provider store={store}>." : "react-redux not installed. Run: npm install react-redux"
308
+ })]
309
+ }), !isEnabled && isInstrumented && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
310
+ style: styles.disabledBanner,
311
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Power, {
312
+ size: 14,
313
+ color: _sharedUi.macOSColors.semantic.warning
314
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
315
+ style: styles.disabledText,
316
+ children: "Action capture is disabled"
317
+ })]
318
+ }), captureMechanism === "patch" && !_reduxActionStore.reduxActionStore.isCaptureSuppressed() && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
319
+ style: styles.disabledBanner,
320
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.AlertTriangle, {
321
+ size: 14,
322
+ color: _sharedUi.macOSColors.semantic.warning
323
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
324
+ style: styles.disabledText,
325
+ children: "Top-level capture only \u2014 thunk/RTK Query internals aren't visible. Put import '@buoy-gg/redux' first in your app entry (or add buoyReduxMiddleware) for full capture."
326
+ })]
327
+ }), hasLockedActions && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.TouchableOpacity, {
328
+ style: styles.lockedBanner,
329
+ onPress: handleUpgradePress,
330
+ activeOpacity: 0.8,
331
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Lock, {
332
+ size: 14,
333
+ color: _sharedUi.buoyColors.primary
334
+ }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Text, {
335
+ style: styles.lockedText,
336
+ children: [lockedActionCount, " older ", lockedActionCount === 1 ? "action" : "actions", " locked"]
337
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
338
+ style: styles.upgradeBadge,
339
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
340
+ style: styles.upgradeBadgeText,
341
+ children: "UPGRADE"
342
+ })
343
+ })]
344
+ }), visibleActions.length > 0 ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.FlatList, {
345
+ ref: flatListRef,
346
+ data: visibleActions,
347
+ renderItem: renderItem,
348
+ keyExtractor: keyExtractor,
349
+ contentContainerStyle: styles.listContent,
350
+ showsVerticalScrollIndicator: true,
351
+ removeClippedSubviews: true,
352
+ initialNumToRender: 12,
353
+ maxToRenderPerBatch: 8,
354
+ windowSize: 5,
355
+ updateCellsBatchingPeriod: 50
356
+ }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(EmptyState, {
357
+ isEnabled: isEnabled
358
+ })]
359
+ })
360
+ });
361
+ const upgradeModal = /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ProUpgradeModal, {
362
+ visible: showProModal,
363
+ onClose: () => setShowProModal(false),
364
+ featureName: "Full Action History"
365
+ });
366
+
367
+ // The ONLY difference between the phone and the desktop dashboard is which
368
+ // shell the header, body and footer go in. Everything above this is shared.
369
+ if (inline) {
370
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_sharedUi.TickProvider, {
371
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
372
+ style: styles.inlineRoot,
373
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
374
+ style: styles.inlineHeader,
375
+ children: renderHeaderContent()
376
+ }), body, footerNode]
377
+ }), upgradeModal]
378
+ });
379
+ }
288
380
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_sharedUi.TickProvider, {
289
381
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.JsModal, {
290
382
  visible: visible,
@@ -303,75 +395,38 @@ function ReduxModal({
303
395
  styles: {},
304
396
  footer: footerNode,
305
397
  footerHeight: footerHeight,
306
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
307
- style: styles.container,
308
- children: selectedAction && selectedActionIndex !== null ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_ReduxActionDetailContent.ReduxActionDetailContent, {
309
- action: selectedAction,
310
- actions: visibleActions,
311
- selectedIndex: selectedActionIndex,
312
- onIndexChange: handleIndexChange,
313
- disableInternalFooter: true
314
- }) : /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
315
- children: [instrumentError && !isInstrumented && !_reduxActionStore.reduxActionStore.isCaptureSuppressed() && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
316
- style: styles.errorBanner,
317
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.AlertTriangle, {
318
- size: 14,
319
- color: _sharedUi.macOSColors.semantic.error
320
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
321
- style: styles.errorText,
322
- children: isReactReduxAvailable ? "Not inside a Redux Provider. Wrap your app with <Provider store={store}>." : "react-redux not installed. Run: npm install react-redux"
323
- })]
324
- }), !isEnabled && isInstrumented && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
325
- style: styles.disabledBanner,
326
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Power, {
327
- size: 14,
328
- color: _sharedUi.macOSColors.semantic.warning
329
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
330
- style: styles.disabledText,
331
- children: "Action capture is disabled"
332
- })]
333
- }), hasLockedActions && /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.TouchableOpacity, {
334
- style: styles.lockedBanner,
335
- onPress: handleUpgradePress,
336
- activeOpacity: 0.8,
337
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.Lock, {
338
- size: 14,
339
- color: _sharedUi.buoyColors.primary
340
- }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Text, {
341
- style: styles.lockedText,
342
- children: [lockedActionCount, " older ", lockedActionCount === 1 ? "action" : "actions", " locked"]
343
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
344
- style: styles.upgradeBadge,
345
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
346
- style: styles.upgradeBadgeText,
347
- children: "UPGRADE"
348
- })
349
- })]
350
- }), visibleActions.length > 0 ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.FlatList, {
351
- ref: flatListRef,
352
- data: visibleActions,
353
- renderItem: renderItem,
354
- keyExtractor: keyExtractor,
355
- contentContainerStyle: styles.listContent,
356
- showsVerticalScrollIndicator: true,
357
- removeClippedSubviews: true,
358
- initialNumToRender: 12,
359
- maxToRenderPerBatch: 8,
360
- windowSize: 5,
361
- updateCellsBatchingPeriod: 50
362
- }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(EmptyState, {
363
- isEnabled: isEnabled
364
- })]
365
- })
366
- })
367
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_sharedUi.ProUpgradeModal, {
368
- visible: showProModal,
369
- onClose: () => setShowProModal(false),
370
- featureName: "Full Action History"
371
- })]
398
+ children: body
399
+ }), upgradeModal]
400
+ });
401
+ }
402
+
403
+ /**
404
+ * The same Redux tool as `ReduxModal`, rendered INLINE for hosts that place
405
+ * tools in a panel rather than a modal. Exists so there is exactly one
406
+ * implementation — the desktop dashboard used to ship its own copy of this
407
+ * component's header, action list and detail router.
408
+ */
409
+ function ReduxToolView(props = {}) {
410
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(ReduxModal, {
411
+ ...props,
412
+ inline: true,
413
+ visible: true,
414
+ onClose: () => {}
372
415
  });
373
416
  }
374
417
  const styles = _reactNative.StyleSheet.create({
418
+ inlineRoot: {
419
+ flex: 1,
420
+ minHeight: 0,
421
+ backgroundColor: _sharedUi.macOSColors.background.base
422
+ },
423
+ inlineHeader: {
424
+ paddingHorizontal: 12,
425
+ paddingVertical: 8,
426
+ borderBottomWidth: 1,
427
+ borderBottomColor: _sharedUi.macOSColors.border.default,
428
+ backgroundColor: _sharedUi.macOSColors.background.card
429
+ },
375
430
  container: {
376
431
  flex: 1,
377
432
  backgroundColor: _sharedUi.macOSColors.background.base
@@ -33,6 +33,12 @@ Object.defineProperty(exports, "ReduxActionItem", {
33
33
  return _ReduxActionItem.ReduxActionItem;
34
34
  }
35
35
  });
36
+ Object.defineProperty(exports, "ReduxAutoInstrument", {
37
+ enumerable: true,
38
+ get: function () {
39
+ return _ReduxAutoInstrument.ReduxAutoInstrument;
40
+ }
41
+ });
36
42
  Object.defineProperty(exports, "ReduxIcon", {
37
43
  enumerable: true,
38
44
  get: function () {
@@ -49,4 +55,5 @@ var _ReduxModal = require("./ReduxModal");
49
55
  var _ReduxActionItem = require("./ReduxActionItem");
50
56
  var _ReduxActionDetailView = require("./ReduxActionDetailView");
51
57
  var _ReduxActionDetailContent = require("./ReduxActionDetailContent");
52
- var _ReduxIcon = require("./ReduxIcon");
58
+ var _ReduxIcon = require("./ReduxIcon");
59
+ var _ReduxAutoInstrument = require("./ReduxAutoInstrument");
@@ -7,29 +7,21 @@ exports.useAutoInstrumentRedux = useAutoInstrumentRedux;
7
7
  exports.useReduxAvailability = useReduxAvailability;
8
8
  var _react = require("react");
9
9
  var _autoInstrument = require("../utils/autoInstrument");
10
+ var _activeStoreRegistry = require("../utils/activeStoreRegistry");
10
11
  /**
11
12
  * React hook for auto-instrumenting Redux stores
12
13
  *
13
- * This hook automatically patches the Redux store when the component mounts,
14
- * providing zero-config action capture for the Redux DevTools.
14
+ * Mounted for the app's lifetime by FloatingDevTools (via ReduxAutoInstrument)
15
+ * and also called by the Redux modal. Binds Buoy to the store from react-redux
16
+ * context:
17
+ * - If the store was created through the devtools-hook enhancer, capture is
18
+ * already live — this just keeps the registry pointed at it.
19
+ * - Otherwise it applies the dispatch-patch fallback (top-level-only capture).
20
+ * - If react-redux is missing or there's no Provider above, it fails
21
+ * gracefully and records WHY (surfaced by the sync adapter / MCP).
15
22
  *
16
- * Usage:
17
- * - The hook attempts to use react-redux's useStore() to get the store
18
- * - If successful, it instruments the store automatically
19
- * - If react-redux is not available or not in a Provider, it fails gracefully
20
- *
21
- * @example
22
- * ```tsx
23
- * function ReduxDevTools() {
24
- * const { isInstrumented, error } = useAutoInstrumentRedux();
25
- *
26
- * if (error) {
27
- * return <Text>Redux not available: {error}</Text>;
28
- * }
29
- *
30
- * return <ReduxActionList />;
31
- * }
32
- * ```
23
+ * Re-instruments when the store INSTANCE changes (hosts that rebuild their
24
+ * store at runtime), so the registry never pins a dead store.
33
25
  */
34
26
 
35
27
  // ============================================
@@ -59,19 +51,16 @@ function getReactReduxUseStore() {
59
51
  // Main Hook
60
52
  // ============================================
61
53
 
62
- /**
63
- * Hook to automatically instrument Redux store for DevTools
64
- *
65
- * This provides zero-config Redux action capture by:
66
- * 1. Using react-redux's useStore() to get the Redux store
67
- * 2. Automatically patching store.dispatch to capture actions
68
- * 3. Cleaning up when the component unmounts
69
- */
70
54
  function useAutoInstrumentRedux(options = {}) {
71
55
  const {
72
- autoStart = true,
73
- ...instrumentOptions
56
+ autoStart = true
74
57
  } = options;
58
+
59
+ // Options live in a ref: this hook is app-lifetime-mounted, and a fresh
60
+ // `{...rest}` object every render would churn startInstrumentation's
61
+ // identity and re-run the effect on every render.
62
+ const optionsRef = (0, _react.useRef)(options);
63
+ optionsRef.current = options;
75
64
  const [isInstrumented, setIsInstrumented] = (0, _react.useState)(false);
76
65
  const [isLoading, setIsLoading] = (0, _react.useState)(true);
77
66
  const [error, setError] = (0, _react.useState)(null);
@@ -86,37 +75,35 @@ function useAutoInstrumentRedux(options = {}) {
86
75
  // This must be called unconditionally due to hooks rules
87
76
  let reduxStore = null;
88
77
  let storeError = null;
78
+ let storeDiagnostic = null;
89
79
  if (useStoreHook) {
90
80
  try {
91
81
  // eslint-disable-next-line react-hooks/rules-of-hooks
92
82
  reduxStore = useStoreHook();
93
83
  } catch (e) {
94
84
  // Not inside a Provider or other error
85
+ storeDiagnostic = "no-provider";
95
86
  storeError = e instanceof Error ? e.message : "Failed to access Redux store. Make sure your app is wrapped in a Redux Provider.";
96
87
  }
97
88
  } else {
89
+ storeDiagnostic = "no-react-redux";
98
90
  storeError = "react-redux is not installed or not available";
99
91
  }
100
92
 
101
93
  // Function to start instrumentation
102
94
  const startInstrumentation = (0, _react.useCallback)(() => {
103
95
  if (!reduxStore) {
96
+ // Record the failure reason for non-React surfaces (sync adapter/MCP).
97
+ (0, _activeStoreRegistry.setReduxDiagnostic)(storeDiagnostic);
104
98
  setError(storeError || "No Redux store available");
105
99
  setIsLoading(false);
106
100
  return;
107
101
  }
108
-
109
- // Check if already instrumented
110
- if ((0, _autoInstrument.isStoreInstrumented)(reduxStore)) {
111
- setIsInstrumented(true);
112
- setStore(reduxStore);
113
- setIsLoading(false);
114
- setError(null);
115
- return;
116
- }
117
102
  try {
118
- // Instrument the store
119
- const cleanup = (0, _autoInstrument.instrumentStore)(reduxStore, instrumentOptions);
103
+ // Instrument the store. Idempotent per store: enhancer-created and
104
+ // already-patched stores just get (re)registered, so a store SWAP at
105
+ // runtime re-binds the registry to the new instance.
106
+ const cleanup = (0, _autoInstrument.instrumentStore)(reduxStore, optionsRef.current);
120
107
  cleanupRef.current = cleanup;
121
108
  setIsInstrumented(true);
122
109
  setStore(reduxStore);
@@ -127,7 +114,8 @@ function useAutoInstrumentRedux(options = {}) {
127
114
  } finally {
128
115
  setIsLoading(false);
129
116
  }
130
- }, [reduxStore, storeError, instrumentOptions]);
117
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- storeError/storeDiagnostic derive from reduxStore
118
+ }, [reduxStore]);
131
119
 
132
120
  // Function to stop instrumentation
133
121
  const stopInstrumentation = (0, _react.useCallback)(() => {
@@ -142,7 +130,8 @@ function useAutoInstrumentRedux(options = {}) {
142
130
  setStore(null);
143
131
  }, [store]);
144
132
 
145
- // Auto-start instrumentation on mount
133
+ // Auto-start instrumentation on mount, and again whenever the store
134
+ // INSTANCE changes (hosts that rebuild their store re-point the registry).
146
135
  (0, _react.useEffect)(() => {
147
136
  if (autoStart) {
148
137
  startInstrumentation();
@@ -163,6 +152,7 @@ function useAutoInstrumentRedux(options = {}) {
163
152
  isLoading,
164
153
  error,
165
154
  isReactReduxAvailable,
155
+ captureMechanism: (0, _autoInstrument.getInstrumentationStatus)().mechanism,
166
156
  startInstrumentation,
167
157
  stopInstrumentation,
168
158
  store
@@ -11,14 +11,31 @@ function topLevelKeys(state) {
11
11
  return state && typeof state === "object" && !Array.isArray(state) ? Object.keys(state) : [];
12
12
  }
13
13
 
14
+ /**
15
+ * Why there's no bound store, in a shape remote drivers (MCP/desktop) can turn
16
+ * into a SPECIFIC remedy instead of a generic "check your setup".
17
+ */
18
+ function unavailable() {
19
+ const status = (0, _autoInstrument.getInstrumentationStatus)();
20
+ const reason = status.diagnostic ?? "not-instrumented";
21
+ return {
22
+ available: false,
23
+ slices: [],
24
+ /** 'no-react-redux' | 'no-provider' | 'not-instrumented' */
25
+ reason
26
+ };
27
+ }
28
+
14
29
  /**
15
30
  * Sync adapter for the redux tool, consumed by @buoy-gg/external-sync's
16
31
  * `useExternalSync` (structurally matches its ToolSyncAdapter interface so
17
32
  * this package doesn't need a dependency on it).
18
33
  *
19
- * Actions are captured by the auto-instrumentation middleware (installed when
20
- * FloatingDevTools mounts), independent of whether a dashboard is watching —
21
- * subscribing here only streams what the store already records.
34
+ * Actions are captured from app startup — by the store-creation enhancer or
35
+ * the explicit middleware (full capture), or by the dispatch-patch fallback
36
+ * mounted with FloatingDevTools (top-level-only) — independent of whether a
37
+ * dashboard is watching. Subscribing here only streams what's already
38
+ * recorded.
22
39
  *
23
40
  * `getState`/`dispatch` read & drive the live instrumented store (via
24
41
  * getActiveStore()) so a remote driver (MCP/LLM) can inspect current state and
@@ -43,15 +60,16 @@ const reduxSyncAdapter = exports.reduxSyncAdapter = {
43
60
  } = params ?? {};
44
61
  const store = (0, _autoInstrument.getActiveStore)();
45
62
  if (!store) {
46
- return {
47
- available: false,
48
- slices: []
49
- };
63
+ return unavailable();
50
64
  }
51
65
  const state = store.getState();
66
+ const status = (0, _autoInstrument.getInstrumentationStatus)();
52
67
  return {
53
68
  available: true,
54
69
  slices: topLevelKeys(state),
70
+ /** 'full' | 'top-level-only' — what the live capture mechanism sees. */
71
+ capture: status.capture,
72
+ mechanism: status.mechanism,
55
73
  ...(includeValues ? {
56
74
  state
57
75
  } : {})
@@ -65,7 +83,8 @@ const reduxSyncAdapter = exports.reduxSyncAdapter = {
65
83
  const store = (0, _autoInstrument.getActiveStore)();
66
84
  if (!store) {
67
85
  return {
68
- dispatched: false
86
+ dispatched: false,
87
+ ...unavailable()
69
88
  };
70
89
  }
71
90
  store.dispatch(action);