@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.
- package/lib/commonjs/floatingMenu/AppHost.js +25 -1
- package/lib/commonjs/floatingMenu/DevToolsSettingsModal.js +3 -3
- package/lib/commonjs/floatingMenu/DevToolsSettingsModal.web.js +2 -2
- package/lib/commonjs/floatingMenu/FloatingDevTools.js +53 -4
- package/lib/commonjs/floatingMenu/MinimizedToolsContext.js +35 -7
- package/lib/commonjs/floatingMenu/MinimizedToolsStack.js +3 -3
- package/lib/commonjs/floatingMenu/ToolOpenerBridge.js +41 -0
- package/lib/commonjs/floatingMenu/allToolsRegistry.js +14 -0
- package/lib/commonjs/floatingMenu/autoDiscoverPresets.js +32 -0
- package/lib/commonjs/floatingMenu/autoExternalSync.js +107 -13
- package/lib/commonjs/floatingMenu/sync/appSyncAdapter.js +26 -5
- package/lib/commonjs/version.js +1 -1
- package/lib/module/floatingMenu/AppHost.js +25 -1
- package/lib/module/floatingMenu/DevToolsSettingsModal.js +3 -3
- package/lib/module/floatingMenu/DevToolsSettingsModal.web.js +2 -2
- package/lib/module/floatingMenu/FloatingDevTools.js +54 -5
- package/lib/module/floatingMenu/MinimizedToolsContext.js +35 -7
- package/lib/module/floatingMenu/MinimizedToolsStack.js +3 -3
- package/lib/module/floatingMenu/ToolOpenerBridge.js +37 -0
- package/lib/module/floatingMenu/allToolsRegistry.js +15 -1
- package/lib/module/floatingMenu/autoDiscoverPresets.js +32 -0
- package/lib/module/floatingMenu/autoExternalSync.js +105 -13
- package/lib/module/floatingMenu/sync/appSyncAdapter.js +26 -5
- package/lib/module/version.js +1 -1
- package/lib/typescript/commonjs/floatingMenu/AppHost.d.ts.map +1 -1
- package/lib/typescript/commonjs/floatingMenu/FloatingDevTools.d.ts.map +1 -1
- package/lib/typescript/commonjs/floatingMenu/MinimizedToolsContext.d.ts.map +1 -1
- package/lib/typescript/commonjs/floatingMenu/MinimizedToolsStack.d.ts.map +1 -1
- package/lib/typescript/commonjs/floatingMenu/ToolOpenerBridge.d.ts +18 -0
- package/lib/typescript/commonjs/floatingMenu/ToolOpenerBridge.d.ts.map +1 -0
- package/lib/typescript/commonjs/floatingMenu/allToolsRegistry.d.ts.map +1 -1
- package/lib/typescript/commonjs/floatingMenu/autoDiscoverPresets.d.ts.map +1 -1
- package/lib/typescript/commonjs/floatingMenu/autoExternalSync.d.ts +28 -0
- package/lib/typescript/commonjs/floatingMenu/autoExternalSync.d.ts.map +1 -1
- package/lib/typescript/commonjs/floatingMenu/defaultConfig.d.ts +1 -1
- package/lib/typescript/commonjs/floatingMenu/defaultConfig.d.ts.map +1 -1
- package/lib/typescript/commonjs/floatingMenu/sync/appSyncAdapter.d.ts +5 -0
- package/lib/typescript/commonjs/floatingMenu/sync/appSyncAdapter.d.ts.map +1 -1
- package/lib/typescript/commonjs/version.d.ts +1 -1
- package/lib/typescript/module/floatingMenu/AppHost.d.ts.map +1 -1
- package/lib/typescript/module/floatingMenu/FloatingDevTools.d.ts.map +1 -1
- package/lib/typescript/module/floatingMenu/MinimizedToolsContext.d.ts.map +1 -1
- package/lib/typescript/module/floatingMenu/MinimizedToolsStack.d.ts.map +1 -1
- package/lib/typescript/module/floatingMenu/ToolOpenerBridge.d.ts +18 -0
- package/lib/typescript/module/floatingMenu/ToolOpenerBridge.d.ts.map +1 -0
- package/lib/typescript/module/floatingMenu/allToolsRegistry.d.ts.map +1 -1
- package/lib/typescript/module/floatingMenu/autoDiscoverPresets.d.ts.map +1 -1
- package/lib/typescript/module/floatingMenu/autoExternalSync.d.ts +28 -0
- package/lib/typescript/module/floatingMenu/autoExternalSync.d.ts.map +1 -1
- package/lib/typescript/module/floatingMenu/defaultConfig.d.ts +1 -1
- package/lib/typescript/module/floatingMenu/defaultConfig.d.ts.map +1 -1
- package/lib/typescript/module/floatingMenu/sync/appSyncAdapter.d.ts +5 -0
- package/lib/typescript/module/floatingMenu/sync/appSyncAdapter.d.ts.map +1 -1
- package/lib/typescript/module/version.d.ts +1 -1
- 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
|
-
|
|
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
|
|
972
|
-
//
|
|
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:
|
|
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:
|
|
798
|
-
//
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
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: () =>
|
|
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
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
19
|
-
* (there is nothing to stream), it just advertises
|
|
20
|
-
* work and which mechanism would run, so a driver can
|
|
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
|
-
|
|
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
|
package/lib/commonjs/version.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
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");
|