@buoy-gg/core 7.0.31 → 7.0.33

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 (47) hide show
  1. package/lib/commonjs/floatingMenu/FloatingDevTools.js +24 -1
  2. package/lib/commonjs/floatingMenu/autoDiscoverPresets.js +21 -5
  3. package/lib/commonjs/floatingMenu/autoExternalSync.js +32 -17
  4. package/lib/commonjs/floatingMenu/deviceIdentity.js +173 -0
  5. package/lib/commonjs/floatingMenu/dial/DialDevTools.js +5 -46
  6. package/lib/commonjs/floatingMenu/dial/DialIcon.js +2 -46
  7. package/lib/commonjs/version.js +1 -1
  8. package/lib/module/floatingMenu/FloatingDevTools.js +24 -1
  9. package/lib/module/floatingMenu/autoDiscoverPresets.js +21 -5
  10. package/lib/module/floatingMenu/autoExternalSync.js +32 -17
  11. package/lib/module/floatingMenu/deviceIdentity.js +163 -0
  12. package/lib/module/floatingMenu/dial/DialDevTools.js +6 -47
  13. package/lib/module/floatingMenu/dial/DialIcon.js +2 -46
  14. package/lib/module/version.js +1 -1
  15. package/lib/typescript/commonjs/floatingMenu/FloatingDevTools.d.ts +38 -1
  16. package/lib/typescript/commonjs/floatingMenu/FloatingDevTools.d.ts.map +1 -1
  17. package/lib/typescript/commonjs/floatingMenu/autoDiscoverPresets.d.ts.map +1 -1
  18. package/lib/typescript/commonjs/floatingMenu/autoExternalSync.d.ts +12 -2
  19. package/lib/typescript/commonjs/floatingMenu/autoExternalSync.d.ts.map +1 -1
  20. package/lib/typescript/commonjs/floatingMenu/defaultConfig.d.ts +1 -1
  21. package/lib/typescript/commonjs/floatingMenu/defaultConfig.d.ts.map +1 -1
  22. package/lib/typescript/commonjs/floatingMenu/deviceIdentity.d.ts +66 -0
  23. package/lib/typescript/commonjs/floatingMenu/deviceIdentity.d.ts.map +1 -0
  24. package/lib/typescript/commonjs/floatingMenu/dial/DialDevTools.d.ts +0 -7
  25. package/lib/typescript/commonjs/floatingMenu/dial/DialDevTools.d.ts.map +1 -1
  26. package/lib/typescript/commonjs/floatingMenu/dial/DialIcon.d.ts.map +1 -1
  27. package/lib/typescript/commonjs/version.d.ts +1 -1
  28. package/lib/typescript/module/floatingMenu/FloatingDevTools.d.ts +38 -1
  29. package/lib/typescript/module/floatingMenu/FloatingDevTools.d.ts.map +1 -1
  30. package/lib/typescript/module/floatingMenu/autoDiscoverPresets.d.ts.map +1 -1
  31. package/lib/typescript/module/floatingMenu/autoExternalSync.d.ts +12 -2
  32. package/lib/typescript/module/floatingMenu/autoExternalSync.d.ts.map +1 -1
  33. package/lib/typescript/module/floatingMenu/defaultConfig.d.ts +1 -1
  34. package/lib/typescript/module/floatingMenu/defaultConfig.d.ts.map +1 -1
  35. package/lib/typescript/module/floatingMenu/deviceIdentity.d.ts +66 -0
  36. package/lib/typescript/module/floatingMenu/deviceIdentity.d.ts.map +1 -0
  37. package/lib/typescript/module/floatingMenu/dial/DialDevTools.d.ts +0 -7
  38. package/lib/typescript/module/floatingMenu/dial/DialDevTools.d.ts.map +1 -1
  39. package/lib/typescript/module/floatingMenu/dial/DialIcon.d.ts.map +1 -1
  40. package/lib/typescript/module/version.d.ts +1 -1
  41. package/package.json +5 -5
  42. package/lib/commonjs/floatingMenu/allToolsRegistry.js +0 -159
  43. package/lib/module/floatingMenu/allToolsRegistry.js +0 -155
  44. package/lib/typescript/commonjs/floatingMenu/allToolsRegistry.d.ts +0 -42
  45. package/lib/typescript/commonjs/floatingMenu/allToolsRegistry.d.ts.map +0 -1
  46. package/lib/typescript/module/floatingMenu/allToolsRegistry.d.ts +0 -42
  47. package/lib/typescript/module/floatingMenu/allToolsRegistry.d.ts.map +0 -1
@@ -46,6 +46,13 @@ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r
46
46
  * Storage key configuration for monitoring
47
47
  */
48
48
 
49
+ /**
50
+ * Ask Buoy's config, mirrored structurally so core never depends on the
51
+ * optional `@buoy-gg/ask-buoy` package (it is required lazily, like every other
52
+ * tool). The object is handed to `createAskBuoyTool` untouched, so any field
53
+ * that package accepts passes through — the index signature is the seam.
54
+ */
55
+
49
56
  /**
50
57
  * Props for FloatingDevTools component.
51
58
  *
@@ -112,6 +119,7 @@ const FloatingDevTools = ({
112
119
  apps,
113
120
  requiredEnvVars,
114
121
  requiredStorageKeys,
122
+ askBuoy,
115
123
  children,
116
124
  disableHints = false,
117
125
  defaultFloatingTools,
@@ -232,8 +240,23 @@ const FloatingDevTools = ({
232
240
  (0, _optionalRequire.reportOptionalRequireError)("@buoy-gg/storage", error);
233
241
  }
234
242
  }
243
+
244
+ // The documented one-prop Ask Buoy setup. Same lazy-require shape as env
245
+ // and storage above: the configured tool replaces the bare auto-discovered
246
+ // preset by id, and an app without @buoy-gg/ask-buoy installed gets the
247
+ // usual optional-require report instead of a crash.
248
+ if (askBuoy) {
249
+ try {
250
+ const {
251
+ createAskBuoyTool
252
+ } = require("@buoy-gg/ask-buoy");
253
+ overrides.push(createAskBuoyTool(askBuoy));
254
+ } catch (error) {
255
+ (0, _optionalRequire.reportOptionalRequireError)("@buoy-gg/ask-buoy", error);
256
+ }
257
+ }
235
258
  return overrides;
236
- }, [requiredEnvVars, requiredStorageKeys]);
259
+ }, [requiredEnvVars, requiredStorageKeys, askBuoy]);
237
260
 
238
261
  // Combine user apps with config overrides
239
262
  const userApps = (0, _react.useMemo)(() => {
@@ -189,6 +189,22 @@ function autoDiscoverPresets() {
189
189
  }
190
190
  }
191
191
  },
192
+ // Sentry
193
+ {
194
+ name: "@buoy-gg/sentry",
195
+ loader: () => {
196
+ try {
197
+ // @ts-ignore - Dynamic import that may not exist
198
+ const {
199
+ sentryToolPreset
200
+ } = require("@buoy-gg/sentry");
201
+ return sentryToolPreset;
202
+ } catch (e) {
203
+ (0, _optionalRequire.reportOptionalRequireError)("@buoy-gg/sentry", e);
204
+ return null;
205
+ }
206
+ }
207
+ },
192
208
  // Time Machine
193
209
  {
194
210
  name: "@buoy-gg/time-machine",
@@ -223,16 +239,16 @@ function autoDiscoverPresets() {
223
239
  },
224
240
  // Ask Buoy (AI chat)
225
241
  {
226
- name: "@buoy-gg/copilot",
242
+ name: "@buoy-gg/ask-buoy",
227
243
  loader: () => {
228
244
  try {
229
245
  // @ts-ignore - Dynamic import that may not exist
230
246
  const {
231
- copilotToolPreset
232
- } = require("@buoy-gg/copilot");
233
- return copilotToolPreset;
247
+ askBuoyToolPreset
248
+ } = require("@buoy-gg/ask-buoy");
249
+ return askBuoyToolPreset;
234
250
  } catch (e) {
235
- (0, _optionalRequire.reportOptionalRequireError)("@buoy-gg/copilot", e);
251
+ (0, _optionalRequire.reportOptionalRequireError)("@buoy-gg/ask-buoy", e);
236
252
  return null;
237
253
  }
238
254
  }
@@ -11,6 +11,7 @@ var _react = require("react");
11
11
  var _reactNative = require("react-native");
12
12
  var _optionalRequire = require("./optionalRequire.js");
13
13
  var _appSyncAdapter = require("./sync/appSyncAdapter.js");
14
+ var _deviceIdentity = require("./deviceIdentity.js");
14
15
  var _sharedUi = require("@buoy-gg/shared-ui");
15
16
  /**
16
17
  * Zero-config device→desktop sync for FloatingDevTools.
@@ -24,8 +25,10 @@ var _sharedUi = require("@buoy-gg/shared-ui");
24
25
  * package, discovered with the same guarded-require pattern as
25
26
  * autoDiscoverPresets. No <DevToolsSync> wiring needed in the app.
26
27
  *
27
- * Identity defaults come from expo-constants when available (app name →
28
- * "MyApp (ios)" / "myapp-ios"), overridable via the `externalSync` prop.
28
+ * Identity defaults come from deviceIdentity.ts: the app name (expo-constants)
29
+ * plus a persisted per-install tail, so two devices on one build register as
30
+ * two devices — "MyApp (iPhone 17 Pro · 2c1d)" / "myapp-ios-3f9a2c1d".
31
+ * Overridable via the `externalSync` prop.
29
32
  */
30
33
 
31
34
  /* eslint-disable @typescript-eslint/no-var-requires */
@@ -48,6 +51,7 @@ let zustandModule = null;
48
51
  let jotaiModule = null;
49
52
  let eventsModule = null;
50
53
  let consoleModule = null;
54
+ let sentryModule = null;
51
55
  let envModule = null;
52
56
  let highlightUpdatesModule = null;
53
57
  let debugBordersModule = null;
@@ -113,6 +117,11 @@ try {
113
117
  } catch (e) {
114
118
  (0, _optionalRequire.reportOptionalRequireError)("@buoy-gg/console", e);
115
119
  }
120
+ try {
121
+ sentryModule = require("@buoy-gg/sentry");
122
+ } catch (e) {
123
+ (0, _optionalRequire.reportOptionalRequireError)("@buoy-gg/sentry", e);
124
+ }
116
125
  try {
117
126
  envModule = require("@buoy-gg/env");
118
127
  } catch (e) {
@@ -178,13 +187,12 @@ function getAppName() {
178
187
  const constants = expoConstantsModule?.default ?? expoConstantsModule;
179
188
  return constants?.expoConfig?.name ?? constants?.manifest2?.extra?.expoClient?.name ?? null;
180
189
  }
181
- function defaultIdentity() {
182
- const appName = getAppName();
183
- const slug = (appName ?? "buoy").toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
184
- return {
185
- deviceName: `${appName ?? "Buoy Device"} (${_reactNative.Platform.OS})`,
186
- deviceId: `${slug || "buoy"}-${_reactNative.Platform.OS}`
187
- };
190
+
191
+ /** expo-constants' `deviceName`: the simulator's name on a sim, the model on Android. */
192
+ function getHardwareName() {
193
+ const constants = expoConstantsModule?.default ?? expoConstantsModule;
194
+ const name = constants?.deviceName;
195
+ return typeof name === "string" && name.length > 0 ? name : null;
188
196
  }
189
197
  /**
190
198
  * Build the `toolId -> sync adapter` map from whatever tool packages the app
@@ -248,12 +256,16 @@ function AutoExternalSync({
248
256
  requiredEnvVars,
249
257
  customApps
250
258
  });
251
- const identity = (0, _react.useMemo)(defaultIdentity, []);
252
- const deviceId = options?.deviceId ?? identity.deviceId;
259
+ const identity = (0, _deviceIdentity.useDeviceIdentity)(options, {
260
+ appName: getAppName(),
261
+ hardwareName: getHardwareName(),
262
+ platform: _reactNative.Platform.OS
263
+ });
264
+ const deviceId = identity?.deviceId ?? null;
253
265
  const {
254
266
  socket
255
267
  } = useExternalSyncSocket({
256
- deviceName: options?.deviceName ?? identity.deviceName,
268
+ deviceName: identity?.deviceName ?? "Buoy Device",
257
269
  // Older external-sync versions predate getDefaultSocketURL and require
258
270
  // an explicit URL — keep the literal fallback for them.
259
271
  socketURL: options?.socketURL ?? externalSyncModule.getDefaultSocketURL?.() ?? "http://localhost:42831",
@@ -342,6 +354,9 @@ function useToolAdapterMapInner(queryAdapter, requiredEnvVars, customApps) {
342
354
  if (consoleModule?.consoleSyncAdapter) {
343
355
  map.console = consoleModule.consoleSyncAdapter;
344
356
  }
357
+ if (sentryModule?.sentrySyncAdapter) {
358
+ map.sentry = sentryModule.sentrySyncAdapter;
359
+ }
345
360
  // Lazy: time-machine imports `@buoy-gg/core` (useToolHost), and this file
346
361
  // is loaded from core's index. Requiring it at module top-level is a
347
362
  // require cycle (LogBox on every reload). Waiting until first render
@@ -365,14 +380,14 @@ function useToolAdapterMapInner(queryAdapter, requiredEnvVars, customApps) {
365
380
  (0, _optionalRequire.reportOptionalRequireError)("@buoy-gg/scenarios", e);
366
381
  }
367
382
  // Lazy, same require-cycle reason as time-machine and scenarios: the
368
- // copilot package imports `@buoy-gg/core` for the tool-host hooks.
383
+ // ask-buoy package imports `@buoy-gg/core` for the tool-host hooks.
369
384
  try {
370
- const copilotModule = require("@buoy-gg/copilot");
371
- if (copilotModule?.copilotSyncAdapter) {
372
- map.copilot = copilotModule.copilotSyncAdapter;
385
+ const askBuoyModule = require("@buoy-gg/ask-buoy");
386
+ if (askBuoyModule?.askBuoySyncAdapter) {
387
+ map["ask-buoy"] = askBuoyModule.askBuoySyncAdapter;
373
388
  }
374
389
  } catch (e) {
375
- (0, _optionalRequire.reportOptionalRequireError)("@buoy-gg/copilot", e);
390
+ (0, _optionalRequire.reportOptionalRequireError)("@buoy-gg/ask-buoy", e);
376
391
  }
377
392
  if (envModule?.createEnvSyncAdapter) {
378
393
  map.env = envModule.createEnvSyncAdapter(requiredEnvVars ?? []);
@@ -0,0 +1,173 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.INSTALL_ID_LENGTH = void 0;
7
+ exports.buildDeviceIdentity = buildDeviceIdentity;
8
+ exports.generateInstallId = generateInstallId;
9
+ exports.getInstallId = getInstallId;
10
+ exports.loadInstallId = loadInstallId;
11
+ exports.resetInstallIdForTests = resetInstallIdForTests;
12
+ exports.useDeviceIdentity = useDeviceIdentity;
13
+ var _react = require("react");
14
+ var _reactNative = require("react-native");
15
+ var _sharedUi = require("@buoy-gg/shared-ui");
16
+ /**
17
+ * How a device introduces itself to the Buoy Desktop broker.
18
+ *
19
+ * The id used to be `<app-slug>-<platform>` — a BUILD identity, not a device
20
+ * identity. Every phone and simulator running the same app registered under
21
+ * the same id, and the broker (which deliberately lets one device hold
22
+ * several sockets for Fast Refresh) treated each newcomer as a reconnect and
23
+ * handed it the identity. Actions then landed on whichever device connected
24
+ * most recently, silently. See DEVICE_IDENTITY_COLLISION.md.
25
+ *
26
+ * Now the id carries a per-install tail minted once and persisted:
27
+ *
28
+ * deviceId = "<app-slug>-<platform>-<8 hex>" e.g. acme-app-ios-3f9a2c1d
29
+ * deviceName = "<App Name> (<hardware> · <last 4>)" e.g. Acme App (iPhone 17 Pro · 2c1d)
30
+ *
31
+ * The tail is what makes two devices on one build two devices. The name's
32
+ * hardware label comes from expo-constants' `deviceName` when available (the
33
+ * simulator's own name on a sim, "iPhone" on iOS 16+ hardware, the model on
34
+ * Android) and falls back to the platform; the 4-char tail is always on so
35
+ * two identical simulators never share a label.
36
+ *
37
+ * Persistence: native goes through shared's `persistentStorage` (file system
38
+ * → AsyncStorage → memory); web uses `sessionStorage` so every browser TAB
39
+ * is its own device — `localStorage` is shared across tabs and would collide
40
+ * exactly the way the old default did. The id does not survive a reinstall
41
+ * on purpose: it crosses an unauthenticated LAN socket, and deleting the app
42
+ * should reset it.
43
+ *
44
+ * An explicit `externalSync.deviceId` bypasses all of this; whoever pins an
45
+ * id owns its uniqueness.
46
+ */
47
+
48
+ /** Minimal getItem/setItem surface, so storage can be injected in tests. */
49
+
50
+ const INSTALL_ID_LENGTH = exports.INSTALL_ID_LENGTH = 8;
51
+
52
+ /** 8 lowercase hex chars — short enough to read, random enough for a LAN. */
53
+ function generateInstallId() {
54
+ let out = "";
55
+ while (out.length < INSTALL_ID_LENGTH) {
56
+ out += (Math.random() * 16 | 0).toString(16);
57
+ }
58
+ return out;
59
+ }
60
+ function slugify(name) {
61
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/(^-|-$)/g, "");
62
+ }
63
+ /** Pure: turns the inputs into the id/name pair the broker sees. */
64
+ function buildDeviceIdentity({
65
+ appName,
66
+ hardwareName,
67
+ platform,
68
+ installId,
69
+ deviceId
70
+ }) {
71
+ const slug = slugify(appName ?? "buoy") || "buoy";
72
+ const label = appName ?? "Buoy Device";
73
+ const hardware = hardwareName?.trim() || platform;
74
+ if (deviceId) {
75
+ // The app pinned its own id: it can also pin its own name, so keep the
76
+ // default name free of a tail that would refer to an id nobody sees.
77
+ return {
78
+ deviceId,
79
+ deviceName: `${label} (${hardware})`
80
+ };
81
+ }
82
+ const id = `${slug}-${platform}-${installId}`;
83
+ return {
84
+ deviceId: id,
85
+ deviceName: `${label} (${hardware} · ${installId.slice(-4)})`
86
+ };
87
+ }
88
+
89
+ // Web: every tab is a device, so the id lives in the tab's sessionStorage.
90
+ function sessionStore() {
91
+ const g = globalThis;
92
+ try {
93
+ return g.sessionStorage ?? null;
94
+ } catch {
95
+ return null;
96
+ }
97
+ }
98
+ /**
99
+ * The persisted per-install tail: read it, or mint and store it on the first
100
+ * dial. Storage failures degrade to a fresh in-memory id rather than blocking
101
+ * the socket — a per-launch id still beats the old colliding one.
102
+ */
103
+ async function loadInstallId({
104
+ platform = _reactNative.Platform.OS,
105
+ store,
106
+ mint = generateInstallId
107
+ } = {}) {
108
+ const key = _sharedUi.devToolsStorageKeys.sync.deviceId();
109
+ const target = store === undefined ? platform === "web" ? sessionStore() : _sharedUi.persistentStorage : store;
110
+ if (!target) return mint();
111
+ try {
112
+ const existing = await target.getItem(key);
113
+ if (existing && existing.length > 0) return existing;
114
+ const fresh = mint();
115
+ await target.setItem(key, fresh);
116
+ return fresh;
117
+ } catch {
118
+ return mint();
119
+ }
120
+ }
121
+ let installIdPromise = null;
122
+
123
+ /**
124
+ * Process-wide memo so every mount (and every Fast Refresh of the component
125
+ * that calls it) shares one storage round-trip and one id.
126
+ */
127
+ function getInstallId() {
128
+ if (!installIdPromise) installIdPromise = loadInstallId();
129
+ return installIdPromise;
130
+ }
131
+
132
+ /** Tests only. */
133
+ function resetInstallIdForTests() {
134
+ installIdPromise = null;
135
+ }
136
+ /**
137
+ * The identity to dial with. Null until the per-install id is read from
138
+ * storage (one round-trip on first mount; memoized process-wide after) —
139
+ * both sync hooks no-op on a null id, so the socket simply dials a beat
140
+ * later. An explicit `deviceId` skips the read entirely; an explicit
141
+ * `deviceName` is used verbatim.
142
+ */
143
+ function useDeviceIdentity(options, env) {
144
+ const pinnedId = options?.deviceId;
145
+ const pinnedName = options?.deviceName;
146
+ const loadId = env.loadId ?? getInstallId;
147
+ const platform = env.platform ?? _reactNative.Platform.OS;
148
+ const [installId, setInstallId] = (0, _react.useState)(null);
149
+ (0, _react.useEffect)(() => {
150
+ if (pinnedId) return;
151
+ let cancelled = false;
152
+ loadId().then(id => {
153
+ if (!cancelled) setInstallId(id);
154
+ });
155
+ return () => {
156
+ cancelled = true;
157
+ };
158
+ }, [pinnedId, loadId]);
159
+ return (0, _react.useMemo)(() => {
160
+ if (!pinnedId && !installId) return null;
161
+ const identity = buildDeviceIdentity({
162
+ appName: env.appName,
163
+ hardwareName: env.hardwareName,
164
+ platform,
165
+ installId: installId ?? "",
166
+ deviceId: pinnedId
167
+ });
168
+ return pinnedName ? {
169
+ ...identity,
170
+ deviceName: pinnedName
171
+ } : identity;
172
+ }, [pinnedId, pinnedName, installId, env.appName, env.hardwareName, platform]);
173
+ }
@@ -9,7 +9,6 @@ var _reactNative = require("react-native");
9
9
  var _sharedUi = require("@buoy-gg/shared-ui");
10
10
  var _DialIcon = require("./DialIcon.js");
11
11
  var _DialHyperspace = require("./DialHyperspace.js");
12
- var _allToolsRegistry = require("../allToolsRegistry.js");
13
12
  var _DialPagination = require("./DialPagination.js");
14
13
  var _dialUsageStore = require("./dialUsageStore.js");
15
14
  var _DevToolsSettingsModal = require("../DevToolsSettingsModal");
@@ -261,38 +260,6 @@ const DialDevTools = ({
261
260
  return map;
262
261
  }, [dialApps, actions, open, onClose]);
263
262
 
264
- // Tools from the global registry that are NOT installed in this app.
265
- // These are appended after all available tools so paging "next" eventually
266
- // reveals them. They are display-only (real icon + real name + "Unavailable").
267
- const unavailableTools = (0, _react.useMemo)(() => {
268
- const installedIds = new Set(dialApps.map(a => a.id));
269
- return _allToolsRegistry.ALL_TOOLS_REGISTRY.filter(t => !installedIds.has(t.id));
270
- }, [dialApps]);
271
-
272
- // Build stable IconType entries for each unavailable tool.
273
- const unavailableIconsById = (0, _react.useMemo)(() => {
274
- const map = new Map();
275
- for (const tool of unavailableTools) {
276
- map.set(tool.id, {
277
- id: tool.id,
278
- name: tool.name,
279
- // Pre-render the icon at the standard GLYPH size (not the slot size —
280
- // the slot also holds the label + "Unavailable" caption, so a glyph
281
- // rendered at DIAL_ICON_SIZE overflows the slot and looks ~2x bigger
282
- // than installed tools' icons).
283
- icon: tool.renderIcon(_floatingToolsCore.DIAL_ICON_GLYPH_SIZE),
284
- iconComponent: undefined,
285
- color: tool.color,
286
- unavailable: true,
287
- // Tapping an unavailable tool is intentionally a no-op: the dial stays
288
- // open and no modal launches, signalling the tool is not installed.
289
- // Future: add a brief Toast with an install link here.
290
- onPress: () => {}
291
- });
292
- }
293
- return map;
294
- }, [unavailableTools]);
295
-
296
263
  // Snapshot the usage-ranked order when the dial opens. It stays stable while
297
264
  // open so icons don't jump positions mid-interaction.
298
265
  const [rankedIds, setRankedIds] = (0, _react.useState)(() => (0, _dialUsageStore.getRankedToolIds)(dialApps.map(a => a.id)));
@@ -312,8 +279,9 @@ const DialDevTools = ({
312
279
  };
313
280
  }, [dialApps]);
314
281
 
315
- // Page count spans available tools (usage-ranked) + unavailable tools (appended).
316
- const pageCount = Math.max(1, Math.ceil((rankedIds.length + unavailableTools.length) / _floatingToolsCore.MAX_DIAL_SLOTS));
282
+ // Page count spans the installed tools (usage-ranked). Only tools the app
283
+ // actually has appear in the dial — nothing is shown for uninstalled ones.
284
+ const pageCount = Math.max(1, Math.ceil(rankedIds.length / _floatingToolsCore.MAX_DIAL_SLOTS));
317
285
  const [currentPage, setCurrentPage] = (0, _react.useState)(0);
318
286
  const safePage = Math.min(currentPage, pageCount - 1);
319
287
 
@@ -321,18 +289,13 @@ const DialDevTools = ({
321
289
  // snapshotted on open, so each icon's page and slot are fixed for the
322
290
  // session — which lets us mount all icons once and paginate purely by
323
291
  // toggling visibility (no remounts on page change).
324
- //
325
- // Order: usage-ranked available tools first, unavailable tools appended at
326
- // the very end. This guarantees unavailable items always appear on the last
327
- // pages and never displace available tools.
328
292
  const allDialIcons = (0, _react.useMemo)(() => {
329
- const allIds = [...rankedIds, ...unavailableTools.map(t => t.id)];
330
- return allIds.map(id => iconsById.get(id) ?? unavailableIconsById.get(id)).filter(icon => Boolean(icon)).map((icon, i) => ({
293
+ return rankedIds.map(id => iconsById.get(id)).filter(icon => Boolean(icon)).map((icon, i) => ({
331
294
  icon,
332
295
  page: Math.floor(i / _floatingToolsCore.MAX_DIAL_SLOTS),
333
296
  slot: i % _floatingToolsCore.MAX_DIAL_SLOTS
334
297
  }));
335
- }, [rankedIds, iconsById, unavailableTools, unavailableIconsById]);
298
+ }, [rankedIds, iconsById]);
336
299
 
337
300
  // Empty slot indices for the current page (only the last page can be
338
301
  // partial). These are cheap placeholder dots.
@@ -535,10 +498,6 @@ const DialDevTools = ({
535
498
  });
536
499
  };
537
500
  const handleIconPress = icon => {
538
- // Unavailable tools are display-only. Return early so the dial stays open
539
- // and no modal or animation is triggered. The dimmed appearance already
540
- // communicates that the tool is not installed.
541
- if (icon.unavailable) return;
542
501
  setSelectedIcon(1);
543
502
  const interaction = _floatingToolsCore.dialAnimationConfig?.interaction ?? {
544
503
  iconSelect: {
@@ -185,35 +185,6 @@ const DialIcon = ({
185
185
  children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
186
186
  style: styles.emptyDot
187
187
  })
188
- }) : icon.unavailable ?
189
- /*#__PURE__*/
190
- // ── Unavailable tool ──────────────────────────────────────────────────
191
- // Renders the real icon + real name at NORMAL styling (visually identical
192
- // to an installed tool). The ONLY signal that it's unavailable is the small
193
- // muted "Unavailable" caption under the name. It's non-interactive: no
194
- // Pressable, and the dial stays open when tapped because
195
- // DialDevTools.handleIconPress returns early for unavailable icons.
196
- (0, _jsxRuntime.jsxs)(_reactNative.View, {
197
- style: styles.pressable,
198
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
199
- style: styles.iconWrapper,
200
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
201
- style: styles.iconClamp,
202
- children: icon.icon
203
- })
204
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
205
- style: styles.label,
206
- numberOfLines: 1,
207
- adjustsFontSizeToFit: true,
208
- minimumFontScale: 0.7,
209
- children: icon.name.toUpperCase()
210
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
211
- style: styles.unavailableCaption,
212
- numberOfLines: 1,
213
- adjustsFontSizeToFit: true,
214
- minimumFontScale: 0.6,
215
- children: "Unavailable"
216
- })]
217
188
  }) : /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.Pressable, {
218
189
  onPress: () => onPress(icon),
219
190
  onPressIn: handlePressIn,
@@ -292,8 +263,8 @@ const styles = _reactNative.StyleSheet.create({
292
263
  },
293
264
  /**
294
265
  * Hard clamp for PRE-RENDERED icon nodes (icon.icon) — the path where the
295
- * glyph's size is baked in by whoever created the node (unavailable-tool
296
- * registry entries, consumer custom tools) rather than controlled here. The
266
+ * glyph's size is baked in by whoever created the node (consumer custom
267
+ * tools) rather than controlled here. The
297
268
  * fixed overflow-hidden box guarantees an oversized node can never blow past
298
269
  * its slot and dwarf neighboring icons — worst case it center-crops. The
299
270
  * iconComponent path stays unclipped: we render it at DIAL_ICON_GLYPH_SIZE
@@ -327,20 +298,5 @@ const styles = _reactNative.StyleSheet.create({
327
298
  backgroundColor: _floatingToolsCore.dialColors.emptyDotBackground,
328
299
  borderWidth: _floatingToolsCore.dialStyles.emptySlot.borderWidth,
329
300
  borderColor: _floatingToolsCore.dialColors.emptyDotBorder
330
- },
331
- // ── Unavailable tool styles ────────────────────────────────────────────────
332
- /**
333
- * Small muted caption shown under the (normal-styled) tool name — the only
334
- * signal that the tool is not installed. Kept compact (smaller than the label)
335
- * so the extra line doesn't disturb the arc layout. Uses the shared muted
336
- * theme token rather than a hardcoded color.
337
- */
338
- unavailableCaption: {
339
- fontSize: _floatingToolsCore.dialStyles.icon.label.fontSize - 1,
340
- fontWeight: _floatingToolsCore.dialStyles.icon.label.fontWeight,
341
- letterSpacing: _floatingToolsCore.dialStyles.icon.label.letterSpacing,
342
- fontFamily: _floatingToolsCore.dialStyles.icon.label.fontFamily,
343
- color: _floatingToolsCore.dialColors.iconLabelMuted,
344
- textTransform: "uppercase"
345
301
  }
346
302
  });
@@ -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.31";
18
+ const SDK_VERSION = exports.SDK_VERSION = "7.0.33";
@@ -42,6 +42,13 @@ import { externalSyncOptions, shouldMountExternalSync } from "./externalSyncGate
42
42
  * Storage key configuration for monitoring
43
43
  */
44
44
 
45
+ /**
46
+ * Ask Buoy's config, mirrored structurally so core never depends on the
47
+ * optional `@buoy-gg/ask-buoy` package (it is required lazily, like every other
48
+ * tool). The object is handed to `createAskBuoyTool` untouched, so any field
49
+ * that package accepts passes through — the index signature is the seam.
50
+ */
51
+
45
52
  /**
46
53
  * Props for FloatingDevTools component.
47
54
  *
@@ -108,6 +115,7 @@ export const FloatingDevTools = ({
108
115
  apps,
109
116
  requiredEnvVars,
110
117
  requiredStorageKeys,
118
+ askBuoy,
111
119
  children,
112
120
  disableHints = false,
113
121
  defaultFloatingTools,
@@ -228,8 +236,23 @@ export const FloatingDevTools = ({
228
236
  reportOptionalRequireError("@buoy-gg/storage", error);
229
237
  }
230
238
  }
239
+
240
+ // The documented one-prop Ask Buoy setup. Same lazy-require shape as env
241
+ // and storage above: the configured tool replaces the bare auto-discovered
242
+ // preset by id, and an app without @buoy-gg/ask-buoy installed gets the
243
+ // usual optional-require report instead of a crash.
244
+ if (askBuoy) {
245
+ try {
246
+ const {
247
+ createAskBuoyTool
248
+ } = require("@buoy-gg/ask-buoy");
249
+ overrides.push(createAskBuoyTool(askBuoy));
250
+ } catch (error) {
251
+ reportOptionalRequireError("@buoy-gg/ask-buoy", error);
252
+ }
253
+ }
231
254
  return overrides;
232
- }, [requiredEnvVars, requiredStorageKeys]);
255
+ }, [requiredEnvVars, requiredStorageKeys, askBuoy]);
233
256
 
234
257
  // Combine user apps with config overrides
235
258
  const userApps = useMemo(() => {
@@ -185,6 +185,22 @@ export function autoDiscoverPresets() {
185
185
  }
186
186
  }
187
187
  },
188
+ // Sentry
189
+ {
190
+ name: "@buoy-gg/sentry",
191
+ loader: () => {
192
+ try {
193
+ // @ts-ignore - Dynamic import that may not exist
194
+ const {
195
+ sentryToolPreset
196
+ } = require("@buoy-gg/sentry");
197
+ return sentryToolPreset;
198
+ } catch (e) {
199
+ reportOptionalRequireError("@buoy-gg/sentry", e);
200
+ return null;
201
+ }
202
+ }
203
+ },
188
204
  // Time Machine
189
205
  {
190
206
  name: "@buoy-gg/time-machine",
@@ -219,16 +235,16 @@ export function autoDiscoverPresets() {
219
235
  },
220
236
  // Ask Buoy (AI chat)
221
237
  {
222
- name: "@buoy-gg/copilot",
238
+ name: "@buoy-gg/ask-buoy",
223
239
  loader: () => {
224
240
  try {
225
241
  // @ts-ignore - Dynamic import that may not exist
226
242
  const {
227
- copilotToolPreset
228
- } = require("@buoy-gg/copilot");
229
- return copilotToolPreset;
243
+ askBuoyToolPreset
244
+ } = require("@buoy-gg/ask-buoy");
245
+ return askBuoyToolPreset;
230
246
  } catch (e) {
231
- reportOptionalRequireError("@buoy-gg/copilot", e);
247
+ reportOptionalRequireError("@buoy-gg/ask-buoy", e);
232
248
  return null;
233
249
  }
234
250
  }