@buoy-gg/core 7.0.30 → 7.0.31

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 (55) hide show
  1. package/lib/commonjs/floatingMenu/AppHost.js +25 -1
  2. package/lib/commonjs/floatingMenu/DevToolsSettingsModal.js +3 -3
  3. package/lib/commonjs/floatingMenu/DevToolsSettingsModal.web.js +2 -2
  4. package/lib/commonjs/floatingMenu/FloatingDevTools.js +53 -4
  5. package/lib/commonjs/floatingMenu/MinimizedToolsContext.js +35 -7
  6. package/lib/commonjs/floatingMenu/MinimizedToolsStack.js +3 -3
  7. package/lib/commonjs/floatingMenu/ToolOpenerBridge.js +41 -0
  8. package/lib/commonjs/floatingMenu/allToolsRegistry.js +14 -0
  9. package/lib/commonjs/floatingMenu/autoDiscoverPresets.js +32 -0
  10. package/lib/commonjs/floatingMenu/autoExternalSync.js +107 -13
  11. package/lib/commonjs/floatingMenu/sync/appSyncAdapter.js +26 -5
  12. package/lib/commonjs/version.js +1 -1
  13. package/lib/module/floatingMenu/AppHost.js +25 -1
  14. package/lib/module/floatingMenu/DevToolsSettingsModal.js +3 -3
  15. package/lib/module/floatingMenu/DevToolsSettingsModal.web.js +2 -2
  16. package/lib/module/floatingMenu/FloatingDevTools.js +54 -5
  17. package/lib/module/floatingMenu/MinimizedToolsContext.js +35 -7
  18. package/lib/module/floatingMenu/MinimizedToolsStack.js +3 -3
  19. package/lib/module/floatingMenu/ToolOpenerBridge.js +37 -0
  20. package/lib/module/floatingMenu/allToolsRegistry.js +15 -1
  21. package/lib/module/floatingMenu/autoDiscoverPresets.js +32 -0
  22. package/lib/module/floatingMenu/autoExternalSync.js +105 -13
  23. package/lib/module/floatingMenu/sync/appSyncAdapter.js +26 -5
  24. package/lib/module/version.js +1 -1
  25. package/lib/typescript/commonjs/floatingMenu/AppHost.d.ts.map +1 -1
  26. package/lib/typescript/commonjs/floatingMenu/FloatingDevTools.d.ts.map +1 -1
  27. package/lib/typescript/commonjs/floatingMenu/MinimizedToolsContext.d.ts.map +1 -1
  28. package/lib/typescript/commonjs/floatingMenu/MinimizedToolsStack.d.ts.map +1 -1
  29. package/lib/typescript/commonjs/floatingMenu/ToolOpenerBridge.d.ts +18 -0
  30. package/lib/typescript/commonjs/floatingMenu/ToolOpenerBridge.d.ts.map +1 -0
  31. package/lib/typescript/commonjs/floatingMenu/allToolsRegistry.d.ts.map +1 -1
  32. package/lib/typescript/commonjs/floatingMenu/autoDiscoverPresets.d.ts.map +1 -1
  33. package/lib/typescript/commonjs/floatingMenu/autoExternalSync.d.ts +28 -0
  34. package/lib/typescript/commonjs/floatingMenu/autoExternalSync.d.ts.map +1 -1
  35. package/lib/typescript/commonjs/floatingMenu/defaultConfig.d.ts +1 -1
  36. package/lib/typescript/commonjs/floatingMenu/defaultConfig.d.ts.map +1 -1
  37. package/lib/typescript/commonjs/floatingMenu/sync/appSyncAdapter.d.ts +5 -0
  38. package/lib/typescript/commonjs/floatingMenu/sync/appSyncAdapter.d.ts.map +1 -1
  39. package/lib/typescript/commonjs/version.d.ts +1 -1
  40. package/lib/typescript/module/floatingMenu/AppHost.d.ts.map +1 -1
  41. package/lib/typescript/module/floatingMenu/FloatingDevTools.d.ts.map +1 -1
  42. package/lib/typescript/module/floatingMenu/MinimizedToolsContext.d.ts.map +1 -1
  43. package/lib/typescript/module/floatingMenu/MinimizedToolsStack.d.ts.map +1 -1
  44. package/lib/typescript/module/floatingMenu/ToolOpenerBridge.d.ts +18 -0
  45. package/lib/typescript/module/floatingMenu/ToolOpenerBridge.d.ts.map +1 -0
  46. package/lib/typescript/module/floatingMenu/allToolsRegistry.d.ts.map +1 -1
  47. package/lib/typescript/module/floatingMenu/autoDiscoverPresets.d.ts.map +1 -1
  48. package/lib/typescript/module/floatingMenu/autoExternalSync.d.ts +28 -0
  49. package/lib/typescript/module/floatingMenu/autoExternalSync.d.ts.map +1 -1
  50. package/lib/typescript/module/floatingMenu/defaultConfig.d.ts +1 -1
  51. package/lib/typescript/module/floatingMenu/defaultConfig.d.ts.map +1 -1
  52. package/lib/typescript/module/floatingMenu/sync/appSyncAdapter.d.ts +5 -0
  53. package/lib/typescript/module/floatingMenu/sync/appSyncAdapter.d.ts.map +1 -1
  54. package/lib/typescript/module/version.d.ts +1 -1
  55. package/package.json +5 -5
@@ -346,6 +346,7 @@ const AppOverlay = () => {
346
346
  } = useAppHost();
347
347
  const {
348
348
  minimize: addToMinimizedStack,
349
+ restore: removeFromMinimizedStack,
349
350
  getNextIconPosition,
350
351
  minimizedTools
351
352
  } = (0, _MinimizedToolsContext.useMinimizedTools)();
@@ -378,7 +379,30 @@ const AppOverlay = () => {
378
379
  }
379
380
  }
380
381
  });
381
- }, [openApps, minimizedTools, addToMinimizedStack]);
382
+
383
+ // ...and take out anything the rail is still showing that is no longer
384
+ // minimized, or no longer open at all.
385
+ //
386
+ // WHY THIS HALF EXISTS. `openApps` and the rail are two separate stores
387
+ // for one fact, and only the ADD side was reliable. Minimizing goes
388
+ // through an event handler; un-minimizing happens inside `open`, which
389
+ // schedules the rail removal with a `setTimeout` from INSIDE a
390
+ // `setOpenApps` updater. Updaters are not a safe place for side effects —
391
+ // React may double-invoke or discard them — so the removal could be
392
+ // skipped while `minimized: false` still landed. The rail then kept an
393
+ // icon for an already-open tool, and tapping it did nothing except delete
394
+ // the icon: the tool looked like it vanished, with no way back to it, and
395
+ // both stores are persisted so it survived every reload.
396
+ //
397
+ // Reconciling here makes the rail eventually consistent with `openApps`
398
+ // whichever way the state moved, without moving the side effect.
399
+ minimizedTools.forEach(tool => {
400
+ const app = openApps.find(a => a.instanceId === tool.instanceId);
401
+ if (!app || !app.minimized) {
402
+ removeFromMinimizedStack(tool.instanceId);
403
+ }
404
+ });
405
+ }, [openApps, minimizedTools, addToMinimizedStack, removeFromMinimizedStack]);
382
406
 
383
407
  // Filter to renderable apps (exclude toggle-only)
384
408
  const renderableApps = openApps.filter(app => app.launchMode !== "toggle-only");
@@ -968,13 +968,13 @@ const styles = _reactNative.StyleSheet.create({
968
968
  // Transparent so JsModal's night star field shows through everything.
969
969
  backgroundColor: "transparent"
970
970
  },
971
- // Header — the segmented control, inset so it clears the absolutely
972
- // positioned window controls on the right.
971
+ // Header — the segmented control. The window controls are cleared
972
+ // vertically by JsModal's drag-indicator strip, so this runs full width.
973
973
  headerRow: {
974
974
  flexDirection: "row",
975
975
  alignItems: "center",
976
976
  paddingLeft: _sharedUi.night.gutter,
977
- paddingRight: 64,
977
+ paddingRight: _sharedUi.night.gutter,
978
978
  paddingBottom: 8
979
979
  },
980
980
  // Scroll content — labeled groups on a 24pt rhythm (Everlights create screen)
@@ -794,8 +794,8 @@ function SettingsModal({
794
794
  display: 'flex',
795
795
  alignItems: 'center',
796
796
  paddingLeft: 16,
797
- paddingRight: 64,
798
- // clears JsModal's absolutely-positioned window controls
797
+ paddingRight: 16,
798
+ // window controls are cleared vertically, not sideways
799
799
  paddingBottom: 8
800
800
  },
801
801
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
@@ -8,6 +8,7 @@ var _react = _interopRequireWildcard(require("react"));
8
8
  var _reactNative = require("react-native");
9
9
  var _AppHost = require("./AppHost");
10
10
  var _FloatingMenu = require("./FloatingMenu.js");
11
+ var _ToolOpenerBridge = require("./ToolOpenerBridge.js");
11
12
  var _autoDiscoverPresets = require("./autoDiscoverPresets.js");
12
13
  var _installPing = require("../telemetry/installPing.js");
13
14
  var _Buoy = require("../Buoy.js");
@@ -336,6 +337,23 @@ const FloatingDevTools = ({
336
337
  }
337
338
  }, []);
338
339
 
340
+ // Check if react-query is installed and auto-render the ReactQueryTracker.
341
+ // Without it the event store has no QueryClient unless someone opens the
342
+ // React Query TOOL, so every other consumer of those events — the Events
343
+ // timeline most visibly — sits at a confident zero forever.
344
+ const ReactQueryTracker = (0, _react.useMemo)(() => {
345
+ try {
346
+ // @ts-ignore - Dynamic import that may not exist
347
+ const {
348
+ ReactQueryTracker: Tracker
349
+ } = require("@buoy-gg/react-query");
350
+ return Tracker;
351
+ } catch (e) {
352
+ (0, _optionalRequire.reportOptionalRequireError)("@buoy-gg/react-query", e);
353
+ return null;
354
+ }
355
+ }, []);
356
+
339
357
  // Check if impersonate is installed and auto-render the floating banner
340
358
  const ImpersonateOverlay = (0, _react.useMemo)(() => {
341
359
  try {
@@ -350,6 +368,23 @@ const FloatingDevTools = ({
350
368
  }
351
369
  }, []);
352
370
 
371
+ // Check if scenarios is installed and auto-render the SIMULATED banner.
372
+ // Not optional and not suppressible: while a scenario is active the app is
373
+ // showing state that isn't real, and the banner is what stops that reaching
374
+ // a bug report or a screenshot as if it were.
375
+ const ScenariosOverlay = (0, _react.useMemo)(() => {
376
+ try {
377
+ // @ts-ignore - Dynamic import that may not exist
378
+ const {
379
+ ScenariosOverlay: Overlay
380
+ } = require("@buoy-gg/scenarios");
381
+ return Overlay;
382
+ } catch (e) {
383
+ (0, _optionalRequire.reportOptionalRequireError)("@buoy-gg/scenarios", e);
384
+ return null;
385
+ }
386
+ }, []);
387
+
353
388
  // Check if redux is installed and auto-mount startup instrumentation.
354
389
  // Binds the store from app mount (renders nothing) so the action log, MCP
355
390
  // getState/dispatch, and desktop sync work WITHOUT the Redux tool ever
@@ -423,6 +458,17 @@ const FloatingDevTools = ({
423
458
  isAvailable: (0, _autoExternalSync.isExternalSyncAvailable)()
424
459
  });
425
460
 
461
+ // The on-device action registry is published unconditionally, separately
462
+ // from the broker socket. They used to be the same mount: `registerToolAdapters`
463
+ // lived inside AutoExternalSync, so anything driving tools from INSIDE the
464
+ // app (the Scenarios runner, Ask Buoy) got an empty registry in exactly the
465
+ // builds where sync was off, and every dispatch threw `Unknown tool`.
466
+ // Publishing the map opens no socket and contacts nothing.
467
+ const toolRegistry = /*#__PURE__*/(0, _jsxRuntime.jsx)(_autoExternalSync.ToolRegistryPublisher, {
468
+ requiredEnvVars: requiredEnvVars,
469
+ customApps: syncApps
470
+ });
471
+
426
472
  // Headless (sync-only): tool adapters + route tracking + desktop sync, no
427
473
  // on-device UI. requireLicense is intentionally skipped — there's nothing
428
474
  // to gate. All hooks above already ran, so hook order is preserved, and
@@ -430,7 +476,7 @@ const FloatingDevTools = ({
430
476
  // contacted from a shipped build that didn't ask for it).
431
477
  if (headless) {
432
478
  return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
433
- children: [ReduxAutoInstrument && /*#__PURE__*/(0, _jsxRuntime.jsx)(ReduxAutoInstrument, {}), ConsoleRoot && /*#__PURE__*/(0, _jsxRuntime.jsx)(ConsoleRoot, {}), RouteTracker && /*#__PURE__*/(0, _jsxRuntime.jsx)(RouteTracker, {}), syncEnabled && /*#__PURE__*/(0, _jsxRuntime.jsx)(_autoExternalSync.AutoExternalSync, {
479
+ children: [ReduxAutoInstrument && /*#__PURE__*/(0, _jsxRuntime.jsx)(ReduxAutoInstrument, {}), ConsoleRoot && /*#__PURE__*/(0, _jsxRuntime.jsx)(ConsoleRoot, {}), RouteTracker && /*#__PURE__*/(0, _jsxRuntime.jsx)(RouteTracker, {}), ReactQueryTracker && /*#__PURE__*/(0, _jsxRuntime.jsx)(ReactQueryTracker, {}), toolRegistry, syncEnabled && /*#__PURE__*/(0, _jsxRuntime.jsx)(_autoExternalSync.AutoExternalSync, {
434
480
  options: (0, _externalSyncGate.externalSyncOptions)(externalSync),
435
481
  requiredEnvVars: requiredEnvVars,
436
482
  isPro: isLicensed,
@@ -461,7 +507,10 @@ const FloatingDevTools = ({
461
507
  disableHints: disableHints,
462
508
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
463
509
  style: styles.container,
464
- pointerEvents: "box-none",
510
+ pointerEvents: "box-none"
511
+ // @ts-expect-error — deliberate marker prop, not part of ViewProps.
512
+ ,
513
+ __buoyDevToolsRoot: true,
465
514
  children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_DevToolsVisibilityContext.DevToolsVisibilityProvider, {
466
515
  children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_AppHost.AppHostProvider, {
467
516
  children: /*#__PURE__*/(0, _jsxRuntime.jsxs)(_MinimizedToolsContext.MinimizedToolsProvider, {
@@ -470,9 +519,9 @@ const FloatingDevTools = ({
470
519
  ...props,
471
520
  apps: finalApps,
472
521
  environment: resolvedEnvironment
473
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_AppHost.AppOverlay, {})]
522
+ }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_AppHost.AppOverlay, {}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_ToolOpenerBridge.ToolOpenerBridge, {})]
474
523
  })
475
- }), children, DebugBordersOverlay && /*#__PURE__*/(0, _jsxRuntime.jsx)(DebugBordersOverlay, {}), HighlightUpdatesOverlay && /*#__PURE__*/(0, _jsxRuntime.jsx)(HighlightUpdatesOverlay, {}), ImageOverlayOverlay && /*#__PURE__*/(0, _jsxRuntime.jsx)(ImageOverlayOverlay, {}), PerfMonitorOverlay && /*#__PURE__*/(0, _jsxRuntime.jsx)(PerfMonitorOverlay, {}), JsTopOverlay && /*#__PURE__*/(0, _jsxRuntime.jsx)(JsTopOverlay, {}), ImpersonateOverlay && /*#__PURE__*/(0, _jsxRuntime.jsx)(ImpersonateOverlay, {}), ReduxAutoInstrument && /*#__PURE__*/(0, _jsxRuntime.jsx)(ReduxAutoInstrument, {}), ConsoleRoot && /*#__PURE__*/(0, _jsxRuntime.jsx)(ConsoleRoot, {}), RouteTracker && /*#__PURE__*/(0, _jsxRuntime.jsx)(RouteTracker, {}), syncEnabled && /*#__PURE__*/(0, _jsxRuntime.jsx)(_autoExternalSync.AutoExternalSync, {
524
+ }), children, DebugBordersOverlay && /*#__PURE__*/(0, _jsxRuntime.jsx)(DebugBordersOverlay, {}), HighlightUpdatesOverlay && /*#__PURE__*/(0, _jsxRuntime.jsx)(HighlightUpdatesOverlay, {}), ImageOverlayOverlay && /*#__PURE__*/(0, _jsxRuntime.jsx)(ImageOverlayOverlay, {}), PerfMonitorOverlay && /*#__PURE__*/(0, _jsxRuntime.jsx)(PerfMonitorOverlay, {}), JsTopOverlay && /*#__PURE__*/(0, _jsxRuntime.jsx)(JsTopOverlay, {}), ImpersonateOverlay && /*#__PURE__*/(0, _jsxRuntime.jsx)(ImpersonateOverlay, {}), ScenariosOverlay && /*#__PURE__*/(0, _jsxRuntime.jsx)(ScenariosOverlay, {}), ReduxAutoInstrument && /*#__PURE__*/(0, _jsxRuntime.jsx)(ReduxAutoInstrument, {}), ConsoleRoot && /*#__PURE__*/(0, _jsxRuntime.jsx)(ConsoleRoot, {}), RouteTracker && /*#__PURE__*/(0, _jsxRuntime.jsx)(RouteTracker, {}), ReactQueryTracker && /*#__PURE__*/(0, _jsxRuntime.jsx)(ReactQueryTracker, {}), toolRegistry, syncEnabled && /*#__PURE__*/(0, _jsxRuntime.jsx)(_autoExternalSync.AutoExternalSync, {
476
525
  options: (0, _externalSyncGate.externalSyncOptions)(externalSync),
477
526
  requiredEnvVars: requiredEnvVars,
478
527
  isPro: isLicensed,
@@ -101,6 +101,16 @@ function MinimizedToolsProvider({
101
101
  getToolIcon
102
102
  }) {
103
103
  const [minimizedTools, setMinimizedTools] = (0, _react.useState)([]);
104
+ /**
105
+ * A mirror of the list that can be read SYNCHRONOUSLY.
106
+ *
107
+ * `restore()` has to return the tool it removed, in the same tick, because
108
+ * its caller uses that return value to un-minimize the tool in AppHost. It
109
+ * cannot get that from `minimizedTools` (the callback is memoized with no
110
+ * deps, so it would close over a stale list) and it cannot get it from
111
+ * inside a state updater either — see the note on `restore`.
112
+ */
113
+ const minimizedToolsRef = (0, _react.useRef)([]);
104
114
  const [isRestored, setIsRestored] = (0, _react.useState)(false);
105
115
  const persistenceTimeoutRef = (0, _react.useRef)(null);
106
116
  const getToolIconRef = (0, _react.useRef)(getToolIcon);
@@ -132,6 +142,9 @@ function MinimizedToolsProvider({
132
142
  restoreMinimizedTools();
133
143
  }, []);
134
144
 
145
+ // One assignment, every render, so the ref never lags the state it mirrors.
146
+ minimizedToolsRef.current = minimizedTools;
147
+
135
148
  // Persist minimized tools to storage with debounce
136
149
  (0, _react.useEffect)(() => {
137
150
  if (!isRestored) return;
@@ -170,14 +183,29 @@ function MinimizedToolsProvider({
170
183
  }];
171
184
  });
172
185
  }, []);
186
+
187
+ /**
188
+ * Take a tool out of the minimized list and hand it back to the caller.
189
+ *
190
+ * THE RETURN VALUE IS LOAD-BEARING. `MinimizedToolsStack` only calls
191
+ * `onRestore` — which is what actually un-minimizes the tool in AppHost —
192
+ * when this returns non-null. This used to assign the tool inside the
193
+ * `setMinimizedTools` updater and read it back on the next line, which
194
+ * assumes React runs updaters synchronously at the call site. Under React
195
+ * 18's automatic batching it does not: the updater runs in the render
196
+ * phase, so the caller always saw `null`, `onRestore` never fired, and
197
+ * AppHost kept the tool flagged `minimized: true`. The tool then sat in the
198
+ * rail with no way back — tapping it did nothing, and neither did opening
199
+ * it again from the dial, because AppHost already had it open. It survived
200
+ * reloads, because both stores are persisted.
201
+ *
202
+ * Reading the ref instead keeps the lookup synchronous and leaves the
203
+ * updater doing only what an updater should: deriving the next state.
204
+ */
173
205
  const restore = (0, _react.useCallback)(instanceId => {
174
- let restoredTool = null;
175
- setMinimizedTools(current => {
176
- const index = current.findIndex(t => t.instanceId === instanceId);
177
- if (index === -1) return current;
178
- restoredTool = current[index];
179
- return current.filter(t => t.instanceId !== instanceId);
180
- });
206
+ const restoredTool = minimizedToolsRef.current.find(t => t.instanceId === instanceId) ?? null;
207
+ if (!restoredTool) return null;
208
+ setMinimizedTools(current => current.filter(t => t.instanceId !== instanceId));
181
209
  return restoredTool;
182
210
  }, []);
183
211
  const isMinimized = (0, _react.useCallback)(id => {
@@ -32,7 +32,7 @@ const MAX_GLITCH_DELAY = 6000;
32
32
 
33
33
  function GlitchToolButton({
34
34
  tool,
35
- onPress,
35
+ onSelect,
36
36
  index
37
37
  }) {
38
38
  const glitchX = (0, _react.useRef)(new _reactNative.Animated.Value(0)).current;
@@ -127,7 +127,7 @@ function GlitchToolButton({
127
127
  };
128
128
  }, [glitchX, glitchOpacity, glitchScale, index]);
129
129
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
130
- onPress: () => onPress(tool),
130
+ onPress: () => onSelect(tool),
131
131
  activeOpacity: 0.7,
132
132
  style: styles.toolButton,
133
133
  accessibilityLabel: `Restore ${tool.title}`,
@@ -185,7 +185,7 @@ function MinimizedToolsStack({
185
185
  collapsedLabel: `Show ${minimizedTools.length} minimized tools`,
186
186
  children: [...minimizedTools].reverse().map((tool, index) => /*#__PURE__*/(0, _jsxRuntime.jsx)(GlitchToolButton, {
187
187
  tool: tool,
188
- onPress: handleToolPress,
188
+ onSelect: handleToolPress,
189
189
  index: index
190
190
  }, tool.instanceId))
191
191
  });
@@ -0,0 +1,41 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.ToolOpenerBridge = ToolOpenerBridge;
7
+ var _react = require("react");
8
+ var _sharedUi = require("@buoy-gg/shared-ui");
9
+ var _AppHost = require("./AppHost");
10
+ /**
11
+ * ToolOpenerBridge — publishes "bring a tool back on screen" to the overlays.
12
+ *
13
+ * Renders nothing. It exists only to sit INSIDE `AppHostProvider` (where the
14
+ * open-tool state lives) and hand that capability out through the global seam
15
+ * in `@buoy-gg/shared-ui`, so an overlay rendered as a sibling of the provider
16
+ * can still offer a button that opens a tool. See `toolUiBridge.ts` for why the
17
+ * overlays can't just be moved inside the provider.
18
+ *
19
+ * It only RESTORES a tool that is already open — including one that has been
20
+ * minimized, which is the case that matters (the macro recorder minimizes
21
+ * Scenarios so the tester can drive the app, then needs it back). It
22
+ * deliberately does not construct and open a tool from scratch: an opener that
23
+ * guesses at an instance is a worse failure than reporting that it couldn't,
24
+ * and every caller already has to handle `false`.
25
+ */
26
+
27
+ function ToolOpenerBridge() {
28
+ const {
29
+ openApps,
30
+ restore
31
+ } = (0, _AppHost.useAppHost)();
32
+ (0, _react.useEffect)(() => {
33
+ return (0, _sharedUi.registerToolOpener)(toolId => {
34
+ const instance = openApps.find(app => app.id === toolId);
35
+ if (!instance) return false;
36
+ restore(instance.instanceId);
37
+ return true;
38
+ });
39
+ }, [openApps, restore]);
40
+ return null;
41
+ }
@@ -120,6 +120,20 @@ const ALL_TOOLS_REGISTRY = exports.ALL_TOOLS_REGISTRY = [{
120
120
  renderIcon: size => /*#__PURE__*/(0, _jsxRuntime.jsx)(_floatingToolsCore.TimeMachineIcon, {
121
121
  size: size
122
122
  })
123
+ }, {
124
+ id: "scenarios",
125
+ name: "SCENARIOS",
126
+ color: _floatingToolsCore.SCENARIOS_ICON_COLOR,
127
+ renderIcon: size => /*#__PURE__*/(0, _jsxRuntime.jsx)(_floatingToolsCore.ScenariosIcon, {
128
+ size: size
129
+ })
130
+ }, {
131
+ id: "copilot",
132
+ name: "ASK BUOY",
133
+ color: _floatingToolsCore.COPILOT_ICON_COLOR,
134
+ renderIcon: size => /*#__PURE__*/(0, _jsxRuntime.jsx)(_floatingToolsCore.CopilotIcon, {
135
+ size: size
136
+ })
123
137
  }, {
124
138
  id: "jotai",
125
139
  name: "JOTAI",
@@ -205,6 +205,38 @@ function autoDiscoverPresets() {
205
205
  }
206
206
  }
207
207
  },
208
+ // Scenarios
209
+ {
210
+ name: "@buoy-gg/scenarios",
211
+ loader: () => {
212
+ try {
213
+ // @ts-ignore - Dynamic import that may not exist
214
+ const {
215
+ scenariosToolPreset
216
+ } = require("@buoy-gg/scenarios");
217
+ return scenariosToolPreset;
218
+ } catch (e) {
219
+ (0, _optionalRequire.reportOptionalRequireError)("@buoy-gg/scenarios", e);
220
+ return null;
221
+ }
222
+ }
223
+ },
224
+ // Ask Buoy (AI chat)
225
+ {
226
+ name: "@buoy-gg/copilot",
227
+ loader: () => {
228
+ try {
229
+ // @ts-ignore - Dynamic import that may not exist
230
+ const {
231
+ copilotToolPreset
232
+ } = require("@buoy-gg/copilot");
233
+ return copilotToolPreset;
234
+ } catch (e) {
235
+ (0, _optionalRequire.reportOptionalRequireError)("@buoy-gg/copilot", e);
236
+ return null;
237
+ }
238
+ }
239
+ },
208
240
  // Redux DevTools
209
241
  {
210
242
  name: "@buoy-gg/redux",
@@ -4,11 +4,14 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.AutoExternalSync = AutoExternalSync;
7
+ exports.ToolRegistryPublisher = ToolRegistryPublisher;
7
8
  exports.isExternalSyncAvailable = isExternalSyncAvailable;
9
+ exports.useToolAdapterMap = useToolAdapterMap;
8
10
  var _react = require("react");
9
11
  var _reactNative = require("react-native");
10
12
  var _optionalRequire = require("./optionalRequire.js");
11
13
  var _appSyncAdapter = require("./sync/appSyncAdapter.js");
14
+ var _sharedUi = require("@buoy-gg/shared-ui");
12
15
  /**
13
16
  * Zero-config device→desktop sync for FloatingDevTools.
14
17
  *
@@ -53,6 +56,8 @@ let perfMonitorModule = null;
53
56
  let jsTopModule = null;
54
57
  let imagesModule = null;
55
58
  let assetsModule = null;
59
+ let tvRemoteModule = null;
60
+ let focusInspectorModule = null;
56
61
  let expoConstantsModule = null;
57
62
 
58
63
  // Catch bodies report through reportOptionalRequireError: silent for a
@@ -148,6 +153,16 @@ try {
148
153
  } catch (e) {
149
154
  (0, _optionalRequire.reportOptionalRequireError)("@buoy-gg/assets", e);
150
155
  }
156
+ try {
157
+ tvRemoteModule = require("@buoy-gg/tv-remote");
158
+ } catch (e) {
159
+ (0, _optionalRequire.reportOptionalRequireError)("@buoy-gg/tv-remote", e);
160
+ }
161
+ try {
162
+ focusInspectorModule = require("@buoy-gg/focus-inspector");
163
+ } catch (e) {
164
+ (0, _optionalRequire.reportOptionalRequireError)("@buoy-gg/focus-inspector", e);
165
+ }
151
166
  try {
152
167
  expoConstantsModule = require("expo-constants");
153
168
  } catch (e) {
@@ -171,6 +186,52 @@ function defaultIdentity() {
171
186
  deviceId: `${slug || "buoy"}-${_reactNative.Platform.OS}`
172
187
  };
173
188
  }
189
+ /**
190
+ * Build the `toolId -> sync adapter` map from whatever tool packages the app
191
+ * installed.
192
+ *
193
+ * Extracted out of AutoExternalSync so the map can be published on-device
194
+ * WITHOUT opening a broker socket. It used to be inline, which quietly welded
195
+ * the two together: `registerToolAdapters` had exactly one caller, inside a
196
+ * component that only mounts when the sync gate passes, so in any build where
197
+ * the broker path was closed the on-device registry was empty too and every
198
+ * `dispatchToolAction` threw `Unknown tool`. Anything that drives tools from
199
+ * inside the app — the Scenarios runner, Ask Buoy — needs the map without the
200
+ * socket.
201
+ */
202
+ function useToolAdapterMap({
203
+ requiredEnvVars,
204
+ customApps
205
+ }) {
206
+ // Context-based query adapter; null when there is no QueryClientProvider
207
+ // above FloatingDevTools (or the installed @buoy-gg/react-query predates
208
+ // the hook).
209
+ const queryAdapter = reactQueryModule?.useReactQuerySyncAdapter ? reactQueryModule.useReactQuerySyncAdapter() : null;
210
+ return useToolAdapterMapInner(queryAdapter, requiredEnvVars, customApps);
211
+ }
212
+
213
+ /**
214
+ * Publish the adapter map on-device, with no socket and no external-sync
215
+ * dependency.
216
+ *
217
+ * Mounted unconditionally by FloatingDevTools. A getter is registered rather
218
+ * than a snapshot for the same reason `attachDeviceSync` does it: the memo is
219
+ * rebuilt whenever its inputs change, and a captured copy would keep
220
+ * dispatching into adapters the tree has already replaced.
221
+ */
222
+ function ToolRegistryPublisher({
223
+ requiredEnvVars,
224
+ customApps
225
+ }) {
226
+ const tools = useToolAdapterMap({
227
+ requiredEnvVars,
228
+ customApps
229
+ });
230
+ const toolsRef = (0, _react.useRef)(tools);
231
+ toolsRef.current = tools;
232
+ (0, _react.useEffect)(() => (0, _sharedUi.registerToolAdapters)(() => toolsRef.current), []);
233
+ return null;
234
+ }
174
235
  function AutoExternalSync({
175
236
  options,
176
237
  requiredEnvVars,
@@ -183,11 +244,10 @@ function AutoExternalSync({
183
244
  useExternalSyncSocket,
184
245
  useExternalSync
185
246
  } = externalSyncModule;
186
-
187
- // Context-based query adapter; null when there is no QueryClientProvider
188
- // above FloatingDevTools (or the installed @buoy-gg/react-query predates
189
- // the hook).
190
- const queryAdapter = reactQueryModule?.useReactQuerySyncAdapter ? reactQueryModule.useReactQuerySyncAdapter() : null;
247
+ const tools = useToolAdapterMap({
248
+ requiredEnvVars,
249
+ customApps
250
+ });
191
251
  const identity = (0, _react.useMemo)(defaultIdentity, []);
192
252
  const deviceId = options?.deviceId ?? identity.deviceId;
193
253
  const {
@@ -233,7 +293,22 @@ function AutoExternalSync({
233
293
  socket.off("connect", emitLicense);
234
294
  };
235
295
  }, [socket, isPro, tier, licenseKey]);
236
- const tools = (0, _react.useMemo)(() => {
296
+ useExternalSync({
297
+ tools,
298
+ socket,
299
+ deviceId,
300
+ enableLogs: options?.enableLogs
301
+ });
302
+ return null;
303
+ }
304
+
305
+ /**
306
+ * The map itself. Split from `useToolAdapterMap` only so the react-query hook
307
+ * is called at a stable position while the memo takes its result as a plain
308
+ * dependency.
309
+ */
310
+ function useToolAdapterMapInner(queryAdapter, requiredEnvVars, customApps) {
311
+ return (0, _react.useMemo)(() => {
237
312
  // Core's own adapter — device-level capabilities (JS-bundle reload) that
238
313
  // don't belong to any tool package. Registered unconditionally so remote
239
314
  // drivers can rely on it for every app, whatever tools are installed.
@@ -279,6 +354,26 @@ function AutoExternalSync({
279
354
  } catch (e) {
280
355
  (0, _optionalRequire.reportOptionalRequireError)("@buoy-gg/time-machine", e);
281
356
  }
357
+ // Lazy for the same reason as time-machine: scenarios imports
358
+ // `@buoy-gg/core` for useToolHost/useToolHeader.
359
+ try {
360
+ const scenariosModule = require("@buoy-gg/scenarios");
361
+ if (scenariosModule?.scenariosSyncAdapter) {
362
+ map.scenarios = scenariosModule.scenariosSyncAdapter;
363
+ }
364
+ } catch (e) {
365
+ (0, _optionalRequire.reportOptionalRequireError)("@buoy-gg/scenarios", e);
366
+ }
367
+ // Lazy, same require-cycle reason as time-machine and scenarios: the
368
+ // copilot package imports `@buoy-gg/core` for the tool-host hooks.
369
+ try {
370
+ const copilotModule = require("@buoy-gg/copilot");
371
+ if (copilotModule?.copilotSyncAdapter) {
372
+ map.copilot = copilotModule.copilotSyncAdapter;
373
+ }
374
+ } catch (e) {
375
+ (0, _optionalRequire.reportOptionalRequireError)("@buoy-gg/copilot", e);
376
+ }
282
377
  if (envModule?.createEnvSyncAdapter) {
283
378
  map.env = envModule.createEnvSyncAdapter(requiredEnvVars ?? []);
284
379
  }
@@ -303,6 +398,12 @@ function AutoExternalSync({
303
398
  if (assetsModule?.assetsSyncAdapter) {
304
399
  map.assets = assetsModule.assetsSyncAdapter;
305
400
  }
401
+ if (tvRemoteModule?.tvRemoteSyncAdapter) {
402
+ map["tv-remote"] = tvRemoteModule.tvRemoteSyncAdapter;
403
+ }
404
+ if (focusInspectorModule?.focusInspectorSyncAdapter) {
405
+ map["focus-inspector"] = focusInspectorModule.focusInspectorSyncAdapter;
406
+ }
306
407
 
307
408
  // Third-party custom tools that declared InstalledApp.sync. Namespaced so
308
409
  // a custom tool named "network" can't shadow the first-party adapter on
@@ -314,11 +415,4 @@ function AutoExternalSync({
314
415
  }
315
416
  return map;
316
417
  }, [queryAdapter, requiredEnvVars, customApps]);
317
- useExternalSync({
318
- tools,
319
- socket,
320
- deviceId,
321
- enableLogs: options?.enableLogs
322
- });
323
- return null;
324
418
  }
@@ -4,6 +4,7 @@ Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
6
  exports.appSyncAdapter = void 0;
7
+ var _reactNative = require("react-native");
7
8
  var _sharedUi = require("@buoy-gg/shared-ui");
8
9
  /**
9
10
  * Sync adapter for the "app" pseudo-tool — device-level capabilities that
@@ -15,10 +16,10 @@ var _sharedUi = require("@buoy-gg/shared-ui");
15
16
  * Desktop, the MCP server) can count on it for every connected app — no tool
16
17
  * package, and no native peer deps, required.
17
18
  *
18
- * Today that's one thing: reloading the JS bundle. The snapshot is static
19
- * (there is nothing to stream), it just advertises whether a reload would
20
- * work and which mechanism would run, so a driver can say so up front instead
21
- * of discovering it by failing.
19
+ * Two things today: reloading the JS bundle, and what KIND of device this is.
20
+ * The snapshot is static (there is nothing to stream), it just advertises
21
+ * whether a reload would work and which mechanism would run, so a driver can
22
+ * say so up front instead of discovering it by failing.
22
23
  */
23
24
  /**
24
25
  * When THIS JS realm booted. A reload replaces the realm, so a driver that
@@ -38,13 +39,33 @@ const REALM_STARTED_AT = Date.now();
38
39
  * that learned this origin while the app was healthy.
39
40
  */
40
41
  const DEV_SERVER_ORIGIN = (0, _sharedUi.getDevServerOrigin)();
42
+
43
+ /**
44
+ * What kind of device this is, resolved once at module load.
45
+ *
46
+ * A dashboard cannot infer this from `platform` alone: an Apple TV and an
47
+ * iPhone both report "ios", and an Android TV and a phone both report
48
+ * "android". Without it Buoy Desktop has to show every tool for every device —
49
+ * including TV-only tools on a phone, where they can never do anything.
50
+ *
51
+ * `Platform.isTVOS` is `undefined` (not `false`) on Android TV, so it is only
52
+ * ever compared against `true`.
53
+ */
54
+ const DEVICE = {
55
+ platform: _reactNative.Platform.OS,
56
+ isTV: _reactNative.Platform.isTV === true,
57
+ isTVOS: _reactNative.Platform.isTVOS === true
58
+ };
41
59
  const appSyncAdapter = exports.appSyncAdapter = {
42
- version: 2,
60
+ // v3 adds `device`. Consumers must tolerate its absence — an older app on a
61
+ // newer dashboard simply reports an unknown device kind.
62
+ version: 3,
43
63
  getSnapshot: () => {
44
64
  const strategy = (0, _sharedUi.resolveReloadStrategy)("auto");
45
65
  return {
46
66
  startedAt: REALM_STARTED_AT,
47
67
  devServerOrigin: DEV_SERVER_ORIGIN,
68
+ device: DEVICE,
48
69
  reload: {
49
70
  available: strategy !== null,
50
71
  strategy
@@ -15,4 +15,4 @@ exports.SDK_VERSION = void 0;
15
15
  * Drift is prevented by a test that fails when this disagrees with
16
16
  * package.json, so a version bump that forgets this file cannot ship.
17
17
  */
18
- const SDK_VERSION = exports.SDK_VERSION = "7.0.30";
18
+ const SDK_VERSION = exports.SDK_VERSION = "7.0.31";
@@ -339,6 +339,7 @@ export const AppOverlay = () => {
339
339
  } = useAppHost();
340
340
  const {
341
341
  minimize: addToMinimizedStack,
342
+ restore: removeFromMinimizedStack,
342
343
  getNextIconPosition,
343
344
  minimizedTools
344
345
  } = useMinimizedTools();
@@ -371,7 +372,30 @@ export const AppOverlay = () => {
371
372
  }
372
373
  }
373
374
  });
374
- }, [openApps, minimizedTools, addToMinimizedStack]);
375
+
376
+ // ...and take out anything the rail is still showing that is no longer
377
+ // minimized, or no longer open at all.
378
+ //
379
+ // WHY THIS HALF EXISTS. `openApps` and the rail are two separate stores
380
+ // for one fact, and only the ADD side was reliable. Minimizing goes
381
+ // through an event handler; un-minimizing happens inside `open`, which
382
+ // schedules the rail removal with a `setTimeout` from INSIDE a
383
+ // `setOpenApps` updater. Updaters are not a safe place for side effects —
384
+ // React may double-invoke or discard them — so the removal could be
385
+ // skipped while `minimized: false` still landed. The rail then kept an
386
+ // icon for an already-open tool, and tapping it did nothing except delete
387
+ // the icon: the tool looked like it vanished, with no way back to it, and
388
+ // both stores are persisted so it survived every reload.
389
+ //
390
+ // Reconciling here makes the rail eventually consistent with `openApps`
391
+ // whichever way the state moved, without moving the side effect.
392
+ minimizedTools.forEach(tool => {
393
+ const app = openApps.find(a => a.instanceId === tool.instanceId);
394
+ if (!app || !app.minimized) {
395
+ removeFromMinimizedStack(tool.instanceId);
396
+ }
397
+ });
398
+ }, [openApps, minimizedTools, addToMinimizedStack, removeFromMinimizedStack]);
375
399
 
376
400
  // Filter to renderable apps (exclude toggle-only)
377
401
  const renderableApps = openApps.filter(app => app.launchMode !== "toggle-only");