@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
@@ -29,9 +29,20 @@
29
29
  *
30
30
  * That's it! The Redux tool will auto-detect your store and capture actions.
31
31
  *
32
- * @example Advanced: Manual middleware (only if you need more control)
32
+ * HOW CAPTURE WORKS (in priority order):
33
+ * 1. ENHANCER — importing this package claims the official Redux DevTools
34
+ * global (`__REDUX_DEVTOOLS_EXTENSION_COMPOSE__`), which RTK checks by
35
+ * default. If this package loads before your store module, Buoy is inside
36
+ * the store from creation: FULL capture (thunk/RTK Query actions included)
37
+ * and real time travel. To guarantee this ordering, make
38
+ * `import '@buoy-gg/redux';` the FIRST import of your app entry.
39
+ * 2. MIDDLEWARE — add `buoyReduxMiddleware` for explicit full capture.
40
+ * 3. PATCH — otherwise, `FloatingDevTools` binds the store at first mount by
41
+ * patching store.dispatch. Top-level dispatches only (thunk-internal
42
+ * actions bypass a dispatch patch — that's a Redux architecture fact).
43
+ *
44
+ * @example Advanced: Manual middleware (explicit full capture)
33
45
  * ```tsx
34
- * // For time-travel or custom options, you can optionally use middleware:
35
46
  * import { buoyReduxMiddleware, withBuoyDevTools } from '@buoy-gg/redux';
36
47
  *
37
48
  * const store = configureStore({
@@ -42,6 +53,11 @@
42
53
  * ```
43
54
  */
44
55
 
56
+ // SIDE EFFECT (must stay first): claim the Redux DevTools global so stores
57
+ // created after this module loads get the full-capture enhancer. Dev-only,
58
+ // never overrides an existing claimant. See ./redux/utils/devtoolsHook.ts.
59
+ import "./redux/utils/devtoolsHook";
60
+
45
61
  // =============================================================================
46
62
  // PRESET (Primary entry point for users)
47
63
  // =============================================================================
@@ -55,7 +71,11 @@ export { buoyReduxMiddleware, createBuoyReduxMiddleware, withBuoyDevTools, jumpT
55
71
  // =============================================================================
56
72
  // AUTO-INSTRUMENTATION (For programmatic store instrumentation)
57
73
  // =============================================================================
58
- export { instrumentStore, uninstrumentStore, isStoreInstrumented, isAutoInstrumentActive, getActiveStore } from "./redux/utils/autoInstrument";
74
+ export { instrumentStore, uninstrumentStore, isStoreInstrumented, isAutoInstrumentActive, getActiveStore, getInstrumentationStatus, registerReduxStore } from "./redux/utils/autoInstrument";
75
+ // Store-creation-time capture (the primary, full-capture path). Claimed as a
76
+ // module side effect above; exported for tests/advanced hosts.
77
+ export { buoyDevtoolsEnhancer, claimDevtoolsHook, isDevtoolsHookClaimed } from "./redux/utils/devtoolsHook";
78
+
59
79
  // =============================================================================
60
80
  // HISTORY ADAPTER (For universal history devtools)
61
81
  // =============================================================================
@@ -75,11 +95,16 @@ export { useAutoInstrumentRedux, useReduxAvailability } from "./redux/hooks/useA
75
95
  // =============================================================================
76
96
  // COMPONENTS (For custom UI implementations)
77
97
  // =============================================================================
78
- export { ReduxModal, FREE_TIER_ACTION_LIMIT } from "./redux/components/ReduxModal";
98
+ // `ReduxToolView` is the same tool rendered inline (header bar + body + footer)
99
+ // instead of inside a JsModal — for hosts that show tools in a panel.
100
+ export { ReduxModal, ReduxToolView, FREE_TIER_ACTION_LIMIT } from "./redux/components/ReduxModal";
79
101
  export { ReduxActionItem } from "./redux/components/ReduxActionItem";
80
102
  export { ReduxActionDetailView } from "./redux/components/ReduxActionDetailView";
81
103
  export { ReduxActionDetailContent, ReduxActionDetailFooter } from "./redux/components/ReduxActionDetailContent";
82
104
  export { ReduxIcon } from "./redux/components/ReduxIcon";
105
+ // Headless startup instrumentation — auto-rendered by @buoy-gg/core's
106
+ // FloatingDevTools; exported for hosts that mount devtools their own way.
107
+ export { ReduxAutoInstrument } from "./redux/components/ReduxAutoInstrument";
83
108
 
84
109
  // =============================================================================
85
110
  // TYPES
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+
3
+ import { useAutoInstrumentRedux } from "../hooks/useAutoInstrumentRedux";
4
+
5
+ /**
6
+ * Headless startup instrumentation. Auto-rendered by FloatingDevTools (same
7
+ * pattern as RouteTracker & the overlay tools) so the Redux store is bound
8
+ * from app mount — action log, MCP getState/dispatch, and desktop sync all
9
+ * work WITHOUT the user ever opening the Redux tool. Renders nothing.
10
+ *
11
+ * Safe anywhere: if react-redux is missing or there's no Provider above, the
12
+ * hook records the diagnostic and fails gracefully.
13
+ */
14
+ export function ReduxAutoInstrument() {
15
+ useAutoInstrumentRedux();
16
+ return null;
17
+ }
@@ -41,7 +41,8 @@ export function ReduxModal({
41
41
  onClose,
42
42
  onBack,
43
43
  onMinimize,
44
- enableSharedModalDimensions = false
44
+ enableSharedModalDimensions = false,
45
+ inline = false
45
46
  }) {
46
47
  const isPro = useIsPro();
47
48
  const [showProModal, setShowProModal] = useState(false);
@@ -51,7 +52,8 @@ export function ReduxModal({
51
52
  isInstrumented,
52
53
  isLoading: isInstrumentLoading,
53
54
  error: instrumentError,
54
- isReactReduxAvailable
55
+ isReactReduxAvailable,
56
+ captureMechanism
55
57
  } = useAutoInstrumentRedux();
56
58
  const {
57
59
  filteredActions,
@@ -280,6 +282,95 @@ export function ReduxModal({
280
282
  // The action list owns its scrolling so FlatList virtualization works
281
283
  // (see ZustandModal — same pattern, same tool-bench evidence).
282
284
  const listOwnsScroll = selectedAction == null;
285
+ const body = /*#__PURE__*/_jsx(View, {
286
+ style: styles.container,
287
+ children: selectedAction && selectedActionIndex !== null ? /*#__PURE__*/_jsx(ReduxActionDetailContent, {
288
+ action: selectedAction,
289
+ actions: visibleActions,
290
+ selectedIndex: selectedActionIndex,
291
+ onIndexChange: handleIndexChange,
292
+ disableInternalFooter: true
293
+ }) : /*#__PURE__*/_jsxs(_Fragment, {
294
+ children: [instrumentError && !isInstrumented && !reduxActionStore.isCaptureSuppressed() && /*#__PURE__*/_jsxs(View, {
295
+ style: styles.errorBanner,
296
+ children: [/*#__PURE__*/_jsx(AlertTriangle, {
297
+ size: 14,
298
+ color: macOSColors.semantic.error
299
+ }), /*#__PURE__*/_jsx(Text, {
300
+ style: styles.errorText,
301
+ children: isReactReduxAvailable ? "Not inside a Redux Provider. Wrap your app with <Provider store={store}>." : "react-redux not installed. Run: npm install react-redux"
302
+ })]
303
+ }), !isEnabled && isInstrumented && /*#__PURE__*/_jsxs(View, {
304
+ style: styles.disabledBanner,
305
+ children: [/*#__PURE__*/_jsx(Power, {
306
+ size: 14,
307
+ color: macOSColors.semantic.warning
308
+ }), /*#__PURE__*/_jsx(Text, {
309
+ style: styles.disabledText,
310
+ children: "Action capture is disabled"
311
+ })]
312
+ }), captureMechanism === "patch" && !reduxActionStore.isCaptureSuppressed() && /*#__PURE__*/_jsxs(View, {
313
+ style: styles.disabledBanner,
314
+ children: [/*#__PURE__*/_jsx(AlertTriangle, {
315
+ size: 14,
316
+ color: macOSColors.semantic.warning
317
+ }), /*#__PURE__*/_jsx(Text, {
318
+ style: styles.disabledText,
319
+ 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."
320
+ })]
321
+ }), hasLockedActions && /*#__PURE__*/_jsxs(TouchableOpacity, {
322
+ style: styles.lockedBanner,
323
+ onPress: handleUpgradePress,
324
+ activeOpacity: 0.8,
325
+ children: [/*#__PURE__*/_jsx(Lock, {
326
+ size: 14,
327
+ color: buoyColors.primary
328
+ }), /*#__PURE__*/_jsxs(Text, {
329
+ style: styles.lockedText,
330
+ children: [lockedActionCount, " older ", lockedActionCount === 1 ? "action" : "actions", " locked"]
331
+ }), /*#__PURE__*/_jsx(View, {
332
+ style: styles.upgradeBadge,
333
+ children: /*#__PURE__*/_jsx(Text, {
334
+ style: styles.upgradeBadgeText,
335
+ children: "UPGRADE"
336
+ })
337
+ })]
338
+ }), visibleActions.length > 0 ? /*#__PURE__*/_jsx(FlatList, {
339
+ ref: flatListRef,
340
+ data: visibleActions,
341
+ renderItem: renderItem,
342
+ keyExtractor: keyExtractor,
343
+ contentContainerStyle: styles.listContent,
344
+ showsVerticalScrollIndicator: true,
345
+ removeClippedSubviews: true,
346
+ initialNumToRender: 12,
347
+ maxToRenderPerBatch: 8,
348
+ windowSize: 5,
349
+ updateCellsBatchingPeriod: 50
350
+ }) : /*#__PURE__*/_jsx(EmptyState, {
351
+ isEnabled: isEnabled
352
+ })]
353
+ })
354
+ });
355
+ const upgradeModal = /*#__PURE__*/_jsx(ProUpgradeModal, {
356
+ visible: showProModal,
357
+ onClose: () => setShowProModal(false),
358
+ featureName: "Full Action History"
359
+ });
360
+
361
+ // The ONLY difference between the phone and the desktop dashboard is which
362
+ // shell the header, body and footer go in. Everything above this is shared.
363
+ if (inline) {
364
+ return /*#__PURE__*/_jsxs(TickProvider, {
365
+ children: [/*#__PURE__*/_jsxs(View, {
366
+ style: styles.inlineRoot,
367
+ children: [/*#__PURE__*/_jsx(View, {
368
+ style: styles.inlineHeader,
369
+ children: renderHeaderContent()
370
+ }), body, footerNode]
371
+ }), upgradeModal]
372
+ });
373
+ }
283
374
  return /*#__PURE__*/_jsxs(TickProvider, {
284
375
  children: [/*#__PURE__*/_jsx(JsModal, {
285
376
  visible: visible,
@@ -298,75 +389,38 @@ export function ReduxModal({
298
389
  styles: {},
299
390
  footer: footerNode,
300
391
  footerHeight: footerHeight,
301
- children: /*#__PURE__*/_jsx(View, {
302
- style: styles.container,
303
- children: selectedAction && selectedActionIndex !== null ? /*#__PURE__*/_jsx(ReduxActionDetailContent, {
304
- action: selectedAction,
305
- actions: visibleActions,
306
- selectedIndex: selectedActionIndex,
307
- onIndexChange: handleIndexChange,
308
- disableInternalFooter: true
309
- }) : /*#__PURE__*/_jsxs(_Fragment, {
310
- children: [instrumentError && !isInstrumented && !reduxActionStore.isCaptureSuppressed() && /*#__PURE__*/_jsxs(View, {
311
- style: styles.errorBanner,
312
- children: [/*#__PURE__*/_jsx(AlertTriangle, {
313
- size: 14,
314
- color: macOSColors.semantic.error
315
- }), /*#__PURE__*/_jsx(Text, {
316
- style: styles.errorText,
317
- children: isReactReduxAvailable ? "Not inside a Redux Provider. Wrap your app with <Provider store={store}>." : "react-redux not installed. Run: npm install react-redux"
318
- })]
319
- }), !isEnabled && isInstrumented && /*#__PURE__*/_jsxs(View, {
320
- style: styles.disabledBanner,
321
- children: [/*#__PURE__*/_jsx(Power, {
322
- size: 14,
323
- color: macOSColors.semantic.warning
324
- }), /*#__PURE__*/_jsx(Text, {
325
- style: styles.disabledText,
326
- children: "Action capture is disabled"
327
- })]
328
- }), hasLockedActions && /*#__PURE__*/_jsxs(TouchableOpacity, {
329
- style: styles.lockedBanner,
330
- onPress: handleUpgradePress,
331
- activeOpacity: 0.8,
332
- children: [/*#__PURE__*/_jsx(Lock, {
333
- size: 14,
334
- color: buoyColors.primary
335
- }), /*#__PURE__*/_jsxs(Text, {
336
- style: styles.lockedText,
337
- children: [lockedActionCount, " older ", lockedActionCount === 1 ? "action" : "actions", " locked"]
338
- }), /*#__PURE__*/_jsx(View, {
339
- style: styles.upgradeBadge,
340
- children: /*#__PURE__*/_jsx(Text, {
341
- style: styles.upgradeBadgeText,
342
- children: "UPGRADE"
343
- })
344
- })]
345
- }), visibleActions.length > 0 ? /*#__PURE__*/_jsx(FlatList, {
346
- ref: flatListRef,
347
- data: visibleActions,
348
- renderItem: renderItem,
349
- keyExtractor: keyExtractor,
350
- contentContainerStyle: styles.listContent,
351
- showsVerticalScrollIndicator: true,
352
- removeClippedSubviews: true,
353
- initialNumToRender: 12,
354
- maxToRenderPerBatch: 8,
355
- windowSize: 5,
356
- updateCellsBatchingPeriod: 50
357
- }) : /*#__PURE__*/_jsx(EmptyState, {
358
- isEnabled: isEnabled
359
- })]
360
- })
361
- })
362
- }), /*#__PURE__*/_jsx(ProUpgradeModal, {
363
- visible: showProModal,
364
- onClose: () => setShowProModal(false),
365
- featureName: "Full Action History"
366
- })]
392
+ children: body
393
+ }), upgradeModal]
394
+ });
395
+ }
396
+
397
+ /**
398
+ * The same Redux tool as `ReduxModal`, rendered INLINE for hosts that place
399
+ * tools in a panel rather than a modal. Exists so there is exactly one
400
+ * implementation — the desktop dashboard used to ship its own copy of this
401
+ * component's header, action list and detail router.
402
+ */
403
+ export function ReduxToolView(props = {}) {
404
+ return /*#__PURE__*/_jsx(ReduxModal, {
405
+ ...props,
406
+ inline: true,
407
+ visible: true,
408
+ onClose: () => {}
367
409
  });
368
410
  }
369
411
  const styles = StyleSheet.create({
412
+ inlineRoot: {
413
+ flex: 1,
414
+ minHeight: 0,
415
+ backgroundColor: macOSColors.background.base
416
+ },
417
+ inlineHeader: {
418
+ paddingHorizontal: 12,
419
+ paddingVertical: 8,
420
+ borderBottomWidth: 1,
421
+ borderBottomColor: macOSColors.border.default,
422
+ backgroundColor: macOSColors.background.card
423
+ },
370
424
  container: {
371
425
  flex: 1,
372
426
  backgroundColor: macOSColors.background.base
@@ -4,4 +4,5 @@ export { ReduxModal, FREE_TIER_ACTION_LIMIT } from "./ReduxModal";
4
4
  export { ReduxActionItem } from "./ReduxActionItem";
5
5
  export { ReduxActionDetailView } from "./ReduxActionDetailView";
6
6
  export { ReduxActionDetailContent, ReduxActionDetailFooter } from "./ReduxActionDetailContent";
7
- export { ReduxIcon } from "./ReduxIcon";
7
+ export { ReduxIcon } from "./ReduxIcon";
8
+ export { ReduxAutoInstrument } from "./ReduxAutoInstrument";
@@ -3,30 +3,22 @@
3
3
  /**
4
4
  * React hook for auto-instrumenting Redux stores
5
5
  *
6
- * This hook automatically patches the Redux store when the component mounts,
7
- * providing zero-config action capture for the Redux DevTools.
6
+ * Mounted for the app's lifetime by FloatingDevTools (via ReduxAutoInstrument)
7
+ * and also called by the Redux modal. Binds Buoy to the store from react-redux
8
+ * context:
9
+ * - If the store was created through the devtools-hook enhancer, capture is
10
+ * already live — this just keeps the registry pointed at it.
11
+ * - Otherwise it applies the dispatch-patch fallback (top-level-only capture).
12
+ * - If react-redux is missing or there's no Provider above, it fails
13
+ * gracefully and records WHY (surfaced by the sync adapter / MCP).
8
14
  *
9
- * Usage:
10
- * - The hook attempts to use react-redux's useStore() to get the store
11
- * - If successful, it instruments the store automatically
12
- * - If react-redux is not available or not in a Provider, it fails gracefully
13
- *
14
- * @example
15
- * ```tsx
16
- * function ReduxDevTools() {
17
- * const { isInstrumented, error } = useAutoInstrumentRedux();
18
- *
19
- * if (error) {
20
- * return <Text>Redux not available: {error}</Text>;
21
- * }
22
- *
23
- * return <ReduxActionList />;
24
- * }
25
- * ```
15
+ * Re-instruments when the store INSTANCE changes (hosts that rebuild their
16
+ * store at runtime), so the registry never pins a dead store.
26
17
  */
27
18
 
28
19
  import { useEffect, useState, useRef, useCallback } from "react";
29
- import { instrumentStore, uninstrumentStore, isStoreInstrumented } from "../utils/autoInstrument";
20
+ import { instrumentStore, uninstrumentStore, getInstrumentationStatus } from "../utils/autoInstrument";
21
+ import { setReduxDiagnostic } from "../utils/activeStoreRegistry";
30
22
 
31
23
  // ============================================
32
24
  // Safe Module Loading
@@ -55,19 +47,16 @@ function getReactReduxUseStore() {
55
47
  // Main Hook
56
48
  // ============================================
57
49
 
58
- /**
59
- * Hook to automatically instrument Redux store for DevTools
60
- *
61
- * This provides zero-config Redux action capture by:
62
- * 1. Using react-redux's useStore() to get the Redux store
63
- * 2. Automatically patching store.dispatch to capture actions
64
- * 3. Cleaning up when the component unmounts
65
- */
66
50
  export function useAutoInstrumentRedux(options = {}) {
67
51
  const {
68
- autoStart = true,
69
- ...instrumentOptions
52
+ autoStart = true
70
53
  } = options;
54
+
55
+ // Options live in a ref: this hook is app-lifetime-mounted, and a fresh
56
+ // `{...rest}` object every render would churn startInstrumentation's
57
+ // identity and re-run the effect on every render.
58
+ const optionsRef = useRef(options);
59
+ optionsRef.current = options;
71
60
  const [isInstrumented, setIsInstrumented] = useState(false);
72
61
  const [isLoading, setIsLoading] = useState(true);
73
62
  const [error, setError] = useState(null);
@@ -82,37 +71,35 @@ export function useAutoInstrumentRedux(options = {}) {
82
71
  // This must be called unconditionally due to hooks rules
83
72
  let reduxStore = null;
84
73
  let storeError = null;
74
+ let storeDiagnostic = null;
85
75
  if (useStoreHook) {
86
76
  try {
87
77
  // eslint-disable-next-line react-hooks/rules-of-hooks
88
78
  reduxStore = useStoreHook();
89
79
  } catch (e) {
90
80
  // Not inside a Provider or other error
81
+ storeDiagnostic = "no-provider";
91
82
  storeError = e instanceof Error ? e.message : "Failed to access Redux store. Make sure your app is wrapped in a Redux Provider.";
92
83
  }
93
84
  } else {
85
+ storeDiagnostic = "no-react-redux";
94
86
  storeError = "react-redux is not installed or not available";
95
87
  }
96
88
 
97
89
  // Function to start instrumentation
98
90
  const startInstrumentation = useCallback(() => {
99
91
  if (!reduxStore) {
92
+ // Record the failure reason for non-React surfaces (sync adapter/MCP).
93
+ setReduxDiagnostic(storeDiagnostic);
100
94
  setError(storeError || "No Redux store available");
101
95
  setIsLoading(false);
102
96
  return;
103
97
  }
104
-
105
- // Check if already instrumented
106
- if (isStoreInstrumented(reduxStore)) {
107
- setIsInstrumented(true);
108
- setStore(reduxStore);
109
- setIsLoading(false);
110
- setError(null);
111
- return;
112
- }
113
98
  try {
114
- // Instrument the store
115
- const cleanup = instrumentStore(reduxStore, instrumentOptions);
99
+ // Instrument the store. Idempotent per store: enhancer-created and
100
+ // already-patched stores just get (re)registered, so a store SWAP at
101
+ // runtime re-binds the registry to the new instance.
102
+ const cleanup = instrumentStore(reduxStore, optionsRef.current);
116
103
  cleanupRef.current = cleanup;
117
104
  setIsInstrumented(true);
118
105
  setStore(reduxStore);
@@ -123,7 +110,8 @@ export function useAutoInstrumentRedux(options = {}) {
123
110
  } finally {
124
111
  setIsLoading(false);
125
112
  }
126
- }, [reduxStore, storeError, instrumentOptions]);
113
+ // eslint-disable-next-line react-hooks/exhaustive-deps -- storeError/storeDiagnostic derive from reduxStore
114
+ }, [reduxStore]);
127
115
 
128
116
  // Function to stop instrumentation
129
117
  const stopInstrumentation = useCallback(() => {
@@ -138,7 +126,8 @@ export function useAutoInstrumentRedux(options = {}) {
138
126
  setStore(null);
139
127
  }, [store]);
140
128
 
141
- // Auto-start instrumentation on mount
129
+ // Auto-start instrumentation on mount, and again whenever the store
130
+ // INSTANCE changes (hosts that rebuild their store re-point the registry).
142
131
  useEffect(() => {
143
132
  if (autoStart) {
144
133
  startInstrumentation();
@@ -159,6 +148,7 @@ export function useAutoInstrumentRedux(options = {}) {
159
148
  isLoading,
160
149
  error,
161
150
  isReactReduxAvailable,
151
+ captureMechanism: getInstrumentationStatus().mechanism,
162
152
  startInstrumentation,
163
153
  stopInstrumentation,
164
154
  store
@@ -1,20 +1,37 @@
1
1
  "use strict";
2
2
 
3
3
  import { reduxActionStore } from "../utils/reduxActionStore";
4
- import { getActiveStore } from "../utils/autoInstrument";
4
+ import { getActiveStore, getInstrumentationStatus } from "../utils/autoInstrument";
5
5
  /** Top-level slice names of the state tree, for the compact (no-values) view. */
6
6
  function topLevelKeys(state) {
7
7
  return state && typeof state === "object" && !Array.isArray(state) ? Object.keys(state) : [];
8
8
  }
9
9
 
10
+ /**
11
+ * Why there's no bound store, in a shape remote drivers (MCP/desktop) can turn
12
+ * into a SPECIFIC remedy instead of a generic "check your setup".
13
+ */
14
+ function unavailable() {
15
+ const status = getInstrumentationStatus();
16
+ const reason = status.diagnostic ?? "not-instrumented";
17
+ return {
18
+ available: false,
19
+ slices: [],
20
+ /** 'no-react-redux' | 'no-provider' | 'not-instrumented' */
21
+ reason
22
+ };
23
+ }
24
+
10
25
  /**
11
26
  * Sync adapter for the redux tool, consumed by @buoy-gg/external-sync's
12
27
  * `useExternalSync` (structurally matches its ToolSyncAdapter interface so
13
28
  * this package doesn't need a dependency on it).
14
29
  *
15
- * Actions are captured by the auto-instrumentation middleware (installed when
16
- * FloatingDevTools mounts), independent of whether a dashboard is watching —
17
- * subscribing here only streams what the store already records.
30
+ * Actions are captured from app startup — by the store-creation enhancer or
31
+ * the explicit middleware (full capture), or by the dispatch-patch fallback
32
+ * mounted with FloatingDevTools (top-level-only) — independent of whether a
33
+ * dashboard is watching. Subscribing here only streams what's already
34
+ * recorded.
18
35
  *
19
36
  * `getState`/`dispatch` read & drive the live instrumented store (via
20
37
  * getActiveStore()) so a remote driver (MCP/LLM) can inspect current state and
@@ -39,15 +56,16 @@ export const reduxSyncAdapter = {
39
56
  } = params ?? {};
40
57
  const store = getActiveStore();
41
58
  if (!store) {
42
- return {
43
- available: false,
44
- slices: []
45
- };
59
+ return unavailable();
46
60
  }
47
61
  const state = store.getState();
62
+ const status = getInstrumentationStatus();
48
63
  return {
49
64
  available: true,
50
65
  slices: topLevelKeys(state),
66
+ /** 'full' | 'top-level-only' — what the live capture mechanism sees. */
67
+ capture: status.capture,
68
+ mechanism: status.mechanism,
51
69
  ...(includeValues ? {
52
70
  state
53
71
  } : {})
@@ -61,7 +79,8 @@ export const reduxSyncAdapter = {
61
79
  const store = getActiveStore();
62
80
  if (!store) {
63
81
  return {
64
- dispatched: false
82
+ dispatched: false,
83
+ ...unavailable()
65
84
  };
66
85
  }
67
86
  store.dispatch(action);
@@ -0,0 +1,63 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Shared registry for the one live Redux store Buoy is bound to, plus how it
5
+ * got bound (the capture mechanism) and why binding failed when it did.
6
+ *
7
+ * Lives in its own module so the three capture paths (devtools-hook enhancer,
8
+ * explicit middleware, dispatch-patch fallback) and the sync adapter can all
9
+ * import it without cycles.
10
+ */
11
+
12
+ /** The minimum store surface Buoy needs (middleware storeApi satisfies this). */
13
+
14
+ /**
15
+ * How the store got instrumented:
16
+ * - `enhancer` — store-creation-time enhancer via the Redux DevTools global.
17
+ * FULL capture (thunk-internal & RTK Query actions included).
18
+ * - `middleware` — user-added buoyReduxMiddleware. FULL capture.
19
+ * - `patch` — store.dispatch patched after creation (auto-instrumentation
20
+ * fallback). TOP-LEVEL-ONLY capture: actions dispatched from
21
+ * inside middleware/thunks bypass the patch.
22
+ */
23
+
24
+ /** Why there is no bound store, when there isn't one. */
25
+
26
+ /** Marks stores created through the Buoy devtools-hook enhancer. */
27
+ export const ENHANCER_SYMBOL = Symbol.for("@@buoy/enhancerInstrumented");
28
+ let activeStore = null;
29
+ let mechanism = null;
30
+ let diagnostic = null;
31
+ export function registerActiveStore(store, mech) {
32
+ activeStore = store;
33
+ mechanism = mech;
34
+ diagnostic = null;
35
+ }
36
+ export function unregisterActiveStore(store) {
37
+ if (activeStore === store) {
38
+ activeStore = null;
39
+ mechanism = null;
40
+ }
41
+ }
42
+ export function getActiveStore() {
43
+ return activeStore;
44
+ }
45
+ export function getCaptureMechanism() {
46
+ return mechanism;
47
+ }
48
+
49
+ /**
50
+ * Recorded by the auto-instrument hook so non-React surfaces (sync adapter,
51
+ * MCP) can report WHY there's no store instead of a generic failure.
52
+ */
53
+ export function setReduxDiagnostic(d) {
54
+ diagnostic = d;
55
+ }
56
+ export function getInstrumentationStatus() {
57
+ return {
58
+ mechanism,
59
+ capture: mechanism === "enhancer" || mechanism === "middleware" ? "full" : mechanism === "patch" ? "top-level-only" : "none",
60
+ hasStore: activeStore !== null,
61
+ diagnostic
62
+ };
63
+ }