@buoy-gg/core 7.0.35 → 7.0.36

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/README.md CHANGED
@@ -30,47 +30,50 @@ export default function App() {
30
30
  }
31
31
  ```
32
32
 
33
- That's the whole setup — a floating dev menu appears inside your app. Install any tool package and it auto-appears in the menu; most need zero config, a few take one line (like passing `zustandStores` or calling `watchAtoms()`). Add a `licenseKey` prop to unlock [Pro](https://buoy.gg/pricing): production builds, the MCP server, and unlimited capture.
33
+ That's the whole setup — a floating dev menu appears inside your app. Install any tool package and it auto-appears in the menu; most need zero config, a few take one line (like passing `zustandStores` or calling `watchAtoms()`). Add a `licenseKey` prop to unlock [Pro](https://buoy.gg/pricing): production builds, the MCP server, Ask Buoy, and unlimited capture.
34
34
 
35
- ## The 15 tools
35
+ ## The 21 tools
36
36
 
37
37
  | Tool | Package | What it does |
38
38
  | --- | --- | --- |
39
39
  | [Network](https://buoy.gg/buoy/latest/docs/tools/network) | `@buoy-gg/network` | Every request, response, timing & error — GraphQL operations extracted and named |
40
40
  | [Storage](https://buoy.gg/buoy/latest/docs/tools/storage) | `@buoy-gg/storage` | Browse & edit AsyncStorage, MMKV & SecureStore with change history |
41
+ | [Time Machine](https://buoy.gg/buoy/latest/docs/tools/time-machine) | `@buoy-gg/time-machine` | Snapshot storage, Redux, Zustand, Jotai & the query cache as restore points — jump back in one tap |
42
+ | [Ask Buoy](https://buoy.gg/buoy/latest/docs/tools/ask-buoy) | `@buoy-gg/ask-buoy` | An in-app AI chat that drives every tool in plain English, on your own model endpoint — with a changes bar and real undo *(beta)* |
41
43
  | [Env](https://buoy.gg/buoy/latest/docs/tools/env) | `@buoy-gg/env` | Auto-discovers `EXPO_PUBLIC_` vars, validates types, scores config health |
42
44
  | [Query](https://buoy.gg/buoy/latest/docs/tools/react-query) | `@buoy-gg/react-query` | TanStack Query cache — refetch, invalidate, simulate loading & error states |
43
45
  | [Routes](https://buoy.gg/buoy/latest/docs/tools/routes) | `@buoy-gg/route-events` | Navigation events, the live stack, and the full sitemap — jump to any screen |
44
46
  | [Debug Borders](https://buoy.gg/buoy/latest/docs/tools/debug-borders) | `@buoy-gg/debug-borders` | Depth-colored layout borders — tap any label for testID, a11y props & styles |
45
47
  | [Highlight Updates](https://buoy.gg/buoy/latest/docs/tools/highlight-updates) | `@buoy-gg/highlight-updates` | Every render, with the cause: mount, state, props, or parent |
46
48
  | [Bench](https://buoy.gg/buoy/latest/docs/tools/perf-monitor) | `@buoy-gg/perf-monitor` | UI/JS FPS, CPU, memory & jank on a real device — batch benchmarks, ranked reports |
49
+ | [JS Top](https://buoy.gg/buoy/latest/docs/tools/js-top) | `@buoy-gg/js-top` | Task Manager for the JS thread — which timers, Promise chains & callbacks eat your JS FPS |
50
+ | [Images](https://buoy.gg/buoy/latest/docs/tools/images) | `@buoy-gg/images` | Every image load with cache verdict, timing, and oversize & wasted-memory audit |
51
+ | [Assets](https://buoy.gg/buoy/latest/docs/tools/assets) | `@buoy-gg/assets` | Everything you ship — bundled images, fonts, video & audio with sizes, duplicates & never-loaded detection |
47
52
  | [Events](https://buoy.gg/buoy/latest/docs/tools/events) | `@buoy-gg/events` | One timeline across every tool, with LLM-friendly export presets |
48
53
  | [Console](https://buoy.gg/buoy/latest/docs/tools/console) | `@buoy-gg/console` | Chrome-DevTools console on device — read logs with no cable and no Metro |
54
+ | [Sentry](https://buoy.gg/buoy/latest/docs/tools/sentry) | `@buoy-gg/sentry` | Every envelope your app sends to Sentry, what it costs, and why an event never arrived |
49
55
  | [Redux](https://buoy.gg/buoy/latest/docs/tools/redux) | `@buoy-gg/redux` | Inspect, dispatch & time travel — flags actions that blow the frame budget |
50
56
  | [Zustand](https://buoy.gg/buoy/latest/docs/tools/zustand) | `@buoy-gg/zustand` | State, diffs, jump-to-state, one-tap reset — no middleware needed |
51
57
  | [Jotai](https://buoy.gg/buoy/latest/docs/tools/jotai) | `@buoy-gg/jotai` | One `watchAtoms()` call — every event shows `prev → next` |
52
58
  | [Impersonate](https://buoy.gg/buoy/latest/docs/tools/impersonate) | `@buoy-gg/impersonate` | Switch users, roles & flags without rebuilding |
53
59
  | [Overlay](https://buoy.gg/buoy/latest/docs/tools/image-overlay) | `@buoy-gg/image-overlay` | Pin Figma mockups pixel-perfect over the app |
54
60
 
55
- Grab everything in one line:
61
+ Two more ship for React Native TV — [TV Remote](https://buoy.gg/buoy/latest/docs/tools/tv-remote) and [Focus Inspector](https://buoy.gg/buoy/latest/docs/tools/focus-inspector) — and [Camera](https://buoy.gg/buoy/latest/docs/tools/camera) gives the iOS Simulator a working camera with no app integration at all.
56
62
 
57
- ```bash
58
- npm i @buoy-gg/{core,network,storage,env,react-query,route-events,debug-borders,highlight-updates,perf-monitor,events,console,redux,zustand,jotai,impersonate,image-overlay}
59
- ```
60
-
61
- ## Three surfaces, one live session
63
+ ## Four surfaces, one live session
62
64
 
63
- Every tool runs inside your app's process. The phone, the desktop, and your agent all see the same session, live.
65
+ Every tool runs inside your app's process. The phone, the desktop, your agent and the in-app chat all see the same session, live.
64
66
 
65
67
  - **On the phone** — tap the floating menu. Works on any device, no cable, no desktop app, no Metro.
66
68
  - **On your desktop** — [Buoy Desktop](https://github.com/Buoy-gg/Buoy-Desktop) (free) mirrors the tools to full-screen panels on macOS, Windows & Linux, with a live FPS / CPU / memory HUD and remote control of the device. Devices connect automatically — physical phones included, no URLs to configure.
67
69
  - **Through your agent** — the [Buoy MCP server](https://buoy.gg/buoy/latest/docs/mcp) gives Claude Code, Cursor, or any MCP editor structured tool calls into the running app: read live state, tap real buttons, run benchmarks on a physical device.
70
+ - **In plain English** — [Ask Buoy](https://buoy.gg/buoy/latest/docs/tools/ask-buoy) *(beta)* puts an AI chat inside the app that drives the same tools for QA, support and product. Your model endpoint, your key; nothing goes through Buoy.
68
71
 
69
72
  ## Free vs Pro
70
73
 
71
74
  Every tool is **free** on every tier — only how much history each keeps changes. A free key (`npx buoy login`, no card) restores the full free tier.
72
75
 
73
- **Pro** unlocks production builds, the MCP server, and unlimited capture. Activate with one prop:
76
+ **Pro** unlocks production builds, the MCP server, Ask Buoy, and unlimited capture. Activate with one prop:
74
77
 
75
78
  ```tsx
76
79
  <FloatingDevTools licenseKey="YOUR_LICENSE_KEY" />
@@ -10,9 +10,50 @@ var _sharedUi = require("@buoy-gg/shared-ui");
10
10
  var _MinimizedToolsContext = require("./MinimizedToolsContext.js");
11
11
  var _jsxRuntime = require("react/jsx-runtime");
12
12
  function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
13
+ /**
14
+ * A hidden tool's "I need you". See `setToolAttention` in shared-ui — the tool
15
+ * publishes, the rail draws. Read through useSyncExternalStore so a badge set
16
+ * from inside a minimized sheet lands on the chip without the rail polling.
17
+ */function useToolAttention(toolId) {
18
+ return (0, _react.useSyncExternalStore)(_sharedUi.subscribeToolAttention, () => (0, _sharedUi.getToolAttention)(toolId), () => (0, _sharedUi.getToolAttention)(toolId));
19
+ }
20
+ const ATTENTION_COLORS = {
21
+ needsInput: "#FF8329",
22
+ done: "#20C997",
23
+ failed: "#FF3B30"
24
+ };
25
+
26
+ /** needsInput outranks failed outranks done — the peek shows the one that matters most. */
27
+ const ATTENTION_RANK = {
28
+ needsInput: 3,
29
+ failed: 2,
30
+ done: 1
31
+ };
32
+
33
+ /** The strongest attention across every minimized tool, for the folded peek. */
34
+ function useStrongestAttention(toolIds) {
35
+ const key = toolIds.join("|");
36
+ return (0, _react.useSyncExternalStore)(_sharedUi.subscribeToolAttention, () => strongest(key), () => strongest(key));
37
+ }
38
+ const strongestCache = new Map();
39
+ function strongest(key) {
40
+ let best = null;
41
+ for (const id of key ? key.split("|") : []) {
42
+ const a = (0, _sharedUi.getToolAttention)(id);
43
+ if (a && (!best || ATTENTION_RANK[a.kind] > ATTENTION_RANK[best.kind])) best = a;
44
+ }
45
+ // Same value → same object, or useSyncExternalStore re-renders forever.
46
+ const prev = strongestCache.get(key);
47
+ if (prev && best && prev.kind === best.kind && prev.label === best.label) return prev;
48
+ if (!prev && !best) return null;
49
+ strongestCache.set(key, best);
50
+ return best;
51
+ }
52
+
13
53
  // ============================================================================
14
54
  // Constants
15
55
  // ============================================================================
56
+
16
57
  const PEEK_HEIGHT = 28;
17
58
  const TOOLBAR_WIDTH = 44;
18
59
  const TOOL_ITEM_SIZE = 32;
@@ -35,6 +76,7 @@ function GlitchToolButton({
35
76
  onSelect,
36
77
  index
37
78
  }) {
79
+ const attention = useToolAttention(tool.id);
38
80
  const glitchX = (0, _react.useRef)(new _reactNative.Animated.Value(0)).current;
39
81
  const glitchOpacity = (0, _react.useRef)(new _reactNative.Animated.Value(1)).current;
40
82
  const glitchScale = (0, _react.useRef)(new _reactNative.Animated.Value(1)).current;
@@ -126,14 +168,14 @@ function GlitchToolButton({
126
168
  clearTimeout(timeoutId);
127
169
  };
128
170
  }, [glitchX, glitchOpacity, glitchScale, index]);
129
- return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.TouchableOpacity, {
171
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.TouchableOpacity, {
130
172
  onPress: () => onSelect(tool),
131
173
  activeOpacity: 0.7,
132
174
  style: styles.toolButton,
133
- accessibilityLabel: `Restore ${tool.title}`,
175
+ accessibilityLabel: attention ? `${tool.title} — ${attention.label}. Restore` : `Restore ${tool.title}`,
134
176
  accessibilityRole: "button",
135
177
  testID: `buoy-dock-restore-${tool.id}`,
136
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Animated.View, {
178
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Animated.View, {
137
179
  style: {
138
180
  opacity: glitchOpacity,
139
181
  transform: [{
@@ -143,7 +185,17 @@ function GlitchToolButton({
143
185
  }]
144
186
  },
145
187
  children: tool.icon
146
- })
188
+ }), attention ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
189
+ style: [styles.badge, {
190
+ backgroundColor: ATTENTION_COLORS[attention.kind]
191
+ }],
192
+ testID: `buoy-dock-badge-${tool.id}-${attention.kind}`,
193
+ pointerEvents: "none",
194
+ children: attention.kind === "needsInput" ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.Text, {
195
+ style: styles.badgeText,
196
+ children: "!"
197
+ }) : null
198
+ }) : null]
147
199
  });
148
200
  }
149
201
 
@@ -163,6 +215,7 @@ function MinimizedToolsStack({
163
215
  minimizedTools,
164
216
  restore
165
217
  } = (0, _MinimizedToolsContext.useMinimizedTools)();
218
+ const peekAttention = useStrongestAttention(minimizedTools.map(t => t.id));
166
219
 
167
220
  // Calculate expanded height based on number of tools
168
221
  const expandedHeight = TOOLBAR_PADDING + minimizedTools.length * TOOL_ITEM_SIZE + (minimizedTools.length - 1) * ICON_GAP + ICON_GAP + COLLAPSE_BUTTON_SIZE + 4;
@@ -186,6 +239,11 @@ function MinimizedToolsStack({
186
239
  collapsedLabel: `Show ${minimizedTools.length} minimized tools`,
187
240
  testID: "buoy-dock-peek",
188
241
  collapseTestID: "buoy-dock-collapse",
242
+ attention: peekAttention ? {
243
+ color: ATTENTION_COLORS[peekAttention.kind],
244
+ glyph: peekAttention.kind === "needsInput" ? "!" : undefined,
245
+ label: peekAttention.label
246
+ } : null,
189
247
  children: [...minimizedTools].reverse().map((tool, index) => /*#__PURE__*/(0, _jsxRuntime.jsx)(GlitchToolButton, {
190
248
  tool: tool,
191
249
  onSelect: handleToolPress,
@@ -205,5 +263,26 @@ const styles = _reactNative.StyleSheet.create({
205
263
  alignItems: "center",
206
264
  justifyContent: "center",
207
265
  backgroundColor: "transparent"
266
+ },
267
+ // Top-right of the chip. Needs-input carries a glyph; done/failed are a dot —
268
+ // the colour says which, the glyph says "act".
269
+ badge: {
270
+ position: "absolute",
271
+ top: 0,
272
+ right: 0,
273
+ minWidth: 12,
274
+ height: 12,
275
+ borderRadius: 6,
276
+ paddingHorizontal: 2,
277
+ alignItems: "center",
278
+ justifyContent: "center",
279
+ borderWidth: 1.5,
280
+ borderColor: "#0C0C0C"
281
+ },
282
+ badgeText: {
283
+ color: "#000",
284
+ fontSize: 8,
285
+ fontWeight: "800",
286
+ lineHeight: 9
208
287
  }
209
288
  });
@@ -24,6 +24,39 @@ var _jsxRuntime = require("react/jsx-runtime");
24
24
  * used before this replaced it.
25
25
  */
26
26
 
27
+ /** Twin of the native hook — see MinimizedToolsStack.tsx. */
28
+ function useToolAttention(toolId) {
29
+ return (0, _react.useSyncExternalStore)(_sharedUi.subscribeToolAttention, () => (0, _sharedUi.getToolAttention)(toolId), () => (0, _sharedUi.getToolAttention)(toolId));
30
+ }
31
+ const ATTENTION_COLORS = {
32
+ needsInput: '#FF8329',
33
+ done: '#20C997',
34
+ failed: '#FF3B30'
35
+ };
36
+ const ATTENTION_RANK = {
37
+ needsInput: 3,
38
+ failed: 2,
39
+ done: 1
40
+ };
41
+ const strongestCache = new Map();
42
+ function strongest(key) {
43
+ let best = null;
44
+ for (const id of key ? key.split('|') : []) {
45
+ const a = (0, _sharedUi.getToolAttention)(id);
46
+ if (a && (!best || ATTENTION_RANK[a.kind] > ATTENTION_RANK[best.kind])) best = a;
47
+ }
48
+ const prev = strongestCache.get(key);
49
+ if (prev && best && prev.kind === best.kind && prev.label === best.label) return prev;
50
+ if (!prev && !best) return null;
51
+ strongestCache.set(key, best);
52
+ return best;
53
+ }
54
+ /** Twin of the native hook: the strongest attention across the folded dock. */
55
+ function useStrongestAttention(toolIds) {
56
+ const key = toolIds.join('|');
57
+ return (0, _react.useSyncExternalStore)(_sharedUi.subscribeToolAttention, () => strongest(key), () => strongest(key));
58
+ }
59
+
27
60
  // native MinimizedToolsStack.tsx constants
28
61
  const PEEK_HEIGHT = 28;
29
62
  const TOOL_ITEM_SIZE = 32;
@@ -61,11 +94,13 @@ function ChevronGlyph({
61
94
  });
62
95
  }
63
96
  function GlitchToolButton({
97
+ id,
64
98
  title,
65
99
  icon,
66
100
  index,
67
101
  onPress
68
102
  }) {
103
+ const attention = useToolAttention(id);
69
104
  const innerRef = (0, _react.useRef)(null);
70
105
  (0, _react.useEffect)(() => {
71
106
  let mounted = true;
@@ -116,9 +151,9 @@ function GlitchToolButton({
116
151
  clearTimeout(timer);
117
152
  };
118
153
  }, [index]);
119
- return /*#__PURE__*/(0, _jsxRuntime.jsx)("button", {
154
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)("button", {
120
155
  type: "button",
121
- "aria-label": `Restore ${title}`,
156
+ "aria-label": attention ? `${title} — ${attention.label}. Restore` : `Restore ${title}`,
122
157
  onClick: onPress,
123
158
  style: {
124
159
  width: TOOL_ITEM_SIZE,
@@ -129,9 +164,10 @@ function GlitchToolButton({
129
164
  background: 'none',
130
165
  border: 'none',
131
166
  padding: 0,
132
- cursor: 'pointer'
167
+ cursor: 'pointer',
168
+ position: 'relative'
133
169
  },
134
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
170
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)("div", {
135
171
  ref: innerRef,
136
172
  style: {
137
173
  display: 'flex',
@@ -139,7 +175,30 @@ function GlitchToolButton({
139
175
  justifyContent: 'center'
140
176
  },
141
177
  children: icon
142
- })
178
+ }), attention ? /*#__PURE__*/(0, _jsxRuntime.jsx)("span", {
179
+ "aria-hidden": true,
180
+ "data-testid": `buoy-dock-badge-${id}-${attention.kind}`,
181
+ style: {
182
+ position: 'absolute',
183
+ top: 0,
184
+ right: 0,
185
+ minWidth: 12,
186
+ height: 12,
187
+ borderRadius: 6,
188
+ padding: '0 2px',
189
+ boxSizing: 'border-box',
190
+ background: ATTENTION_COLORS[attention.kind],
191
+ border: '1.5px solid #0C0C0C',
192
+ color: '#000',
193
+ fontSize: 8,
194
+ fontWeight: 800,
195
+ lineHeight: '9px',
196
+ display: 'flex',
197
+ alignItems: 'center',
198
+ justifyContent: 'center'
199
+ },
200
+ children: attention.kind === 'needsInput' ? '!' : ''
201
+ }) : null]
143
202
  });
144
203
  }
145
204
  function MinimizedToolsStack({
@@ -152,6 +211,7 @@ function MinimizedToolsStack({
152
211
  const minimized = openApps.filter(app => app.minimized && app.launchMode !== 'toggle-only');
153
212
  const [isExpanded, setIsExpanded] = (0, _react.useState)(false);
154
213
  const [isStateRestored, setIsStateRestored] = (0, _react.useState)(false);
214
+ const peekAttention = useStrongestAttention(minimized.map(app => app.id));
155
215
 
156
216
  // Restore + persist the expanded flag (native ExpandablePopover persistState).
157
217
  (0, _react.useEffect)(() => {
@@ -219,6 +279,7 @@ function MinimizedToolsStack({
219
279
  boxSizing: 'border-box'
220
280
  },
221
281
  children: [[...minimized].reverse().map((app, index) => /*#__PURE__*/(0, _jsxRuntime.jsx)(GlitchToolButton, {
282
+ id: app.id,
222
283
  title: app.title || app.id,
223
284
  icon: app.icon ?? /*#__PURE__*/(0, _jsxRuntime.jsx)("span", {
224
285
  style: {
@@ -273,7 +334,30 @@ function MinimizedToolsStack({
273
334
  pointerEvents: isExpanded ? 'none' : 'auto',
274
335
  ...frame
275
336
  },
276
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(ChevronGlyph, {}), minimized.length > 1 && /*#__PURE__*/(0, _jsxRuntime.jsx)("span", {
337
+ children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(ChevronGlyph, {}), peekAttention ? /*#__PURE__*/(0, _jsxRuntime.jsx)("span", {
338
+ "aria-hidden": true,
339
+ "data-testid": `buoy-dock-peek-attention`,
340
+ style: {
341
+ position: 'absolute',
342
+ top: 2,
343
+ right: 2,
344
+ minWidth: 11,
345
+ height: 11,
346
+ borderRadius: 6,
347
+ padding: '0 2px',
348
+ boxSizing: 'border-box',
349
+ background: ATTENTION_COLORS[peekAttention.kind],
350
+ border: '1.5px solid #0C0C0C',
351
+ color: '#000',
352
+ fontSize: 8,
353
+ fontWeight: 800,
354
+ lineHeight: '9px',
355
+ display: 'flex',
356
+ alignItems: 'center',
357
+ justifyContent: 'center'
358
+ },
359
+ children: peekAttention.kind === 'needsInput' ? '!' : ''
360
+ }) : null, minimized.length > 1 && /*#__PURE__*/(0, _jsxRuntime.jsx)("span", {
277
361
  style: {
278
362
  fontSize: 9,
279
363
  fontWeight: 600,
@@ -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.35";
18
+ const SDK_VERSION = exports.SDK_VERSION = "7.0.36";
@@ -1,14 +1,56 @@
1
1
  "use strict";
2
2
 
3
- import React, { useCallback, useEffect, useRef } from "react";
4
- import { Animated, TouchableOpacity, StyleSheet, Easing } from "react-native";
5
- import { ExpandablePopover } from "@buoy-gg/shared-ui";
3
+ import React, { useCallback, useEffect, useRef, useSyncExternalStore } from "react";
4
+ import { Animated, TouchableOpacity, View, Text, StyleSheet, Easing } from "react-native";
5
+ import { ExpandablePopover, getToolAttention, subscribeToolAttention } from "@buoy-gg/shared-ui";
6
6
  import { useMinimizedTools } from "./MinimizedToolsContext.js";
7
7
 
8
+ /**
9
+ * A hidden tool's "I need you". See `setToolAttention` in shared-ui — the tool
10
+ * publishes, the rail draws. Read through useSyncExternalStore so a badge set
11
+ * from inside a minimized sheet lands on the chip without the rail polling.
12
+ */
13
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
14
+ function useToolAttention(toolId) {
15
+ return useSyncExternalStore(subscribeToolAttention, () => getToolAttention(toolId), () => getToolAttention(toolId));
16
+ }
17
+ const ATTENTION_COLORS = {
18
+ needsInput: "#FF8329",
19
+ done: "#20C997",
20
+ failed: "#FF3B30"
21
+ };
22
+
23
+ /** needsInput outranks failed outranks done — the peek shows the one that matters most. */
24
+ const ATTENTION_RANK = {
25
+ needsInput: 3,
26
+ failed: 2,
27
+ done: 1
28
+ };
29
+
30
+ /** The strongest attention across every minimized tool, for the folded peek. */
31
+ function useStrongestAttention(toolIds) {
32
+ const key = toolIds.join("|");
33
+ return useSyncExternalStore(subscribeToolAttention, () => strongest(key), () => strongest(key));
34
+ }
35
+ const strongestCache = new Map();
36
+ function strongest(key) {
37
+ let best = null;
38
+ for (const id of key ? key.split("|") : []) {
39
+ const a = getToolAttention(id);
40
+ if (a && (!best || ATTENTION_RANK[a.kind] > ATTENTION_RANK[best.kind])) best = a;
41
+ }
42
+ // Same value → same object, or useSyncExternalStore re-renders forever.
43
+ const prev = strongestCache.get(key);
44
+ if (prev && best && prev.kind === best.kind && prev.label === best.label) return prev;
45
+ if (!prev && !best) return null;
46
+ strongestCache.set(key, best);
47
+ return best;
48
+ }
49
+
8
50
  // ============================================================================
9
51
  // Constants
10
52
  // ============================================================================
11
- import { jsx as _jsx } from "react/jsx-runtime";
53
+
12
54
  const PEEK_HEIGHT = 28;
13
55
  const TOOLBAR_WIDTH = 44;
14
56
  const TOOL_ITEM_SIZE = 32;
@@ -31,6 +73,7 @@ function GlitchToolButton({
31
73
  onSelect,
32
74
  index
33
75
  }) {
76
+ const attention = useToolAttention(tool.id);
34
77
  const glitchX = useRef(new Animated.Value(0)).current;
35
78
  const glitchOpacity = useRef(new Animated.Value(1)).current;
36
79
  const glitchScale = useRef(new Animated.Value(1)).current;
@@ -122,14 +165,14 @@ function GlitchToolButton({
122
165
  clearTimeout(timeoutId);
123
166
  };
124
167
  }, [glitchX, glitchOpacity, glitchScale, index]);
125
- return /*#__PURE__*/_jsx(TouchableOpacity, {
168
+ return /*#__PURE__*/_jsxs(TouchableOpacity, {
126
169
  onPress: () => onSelect(tool),
127
170
  activeOpacity: 0.7,
128
171
  style: styles.toolButton,
129
- accessibilityLabel: `Restore ${tool.title}`,
172
+ accessibilityLabel: attention ? `${tool.title} — ${attention.label}. Restore` : `Restore ${tool.title}`,
130
173
  accessibilityRole: "button",
131
174
  testID: `buoy-dock-restore-${tool.id}`,
132
- children: /*#__PURE__*/_jsx(Animated.View, {
175
+ children: [/*#__PURE__*/_jsx(Animated.View, {
133
176
  style: {
134
177
  opacity: glitchOpacity,
135
178
  transform: [{
@@ -139,7 +182,17 @@ function GlitchToolButton({
139
182
  }]
140
183
  },
141
184
  children: tool.icon
142
- })
185
+ }), attention ? /*#__PURE__*/_jsx(View, {
186
+ style: [styles.badge, {
187
+ backgroundColor: ATTENTION_COLORS[attention.kind]
188
+ }],
189
+ testID: `buoy-dock-badge-${tool.id}-${attention.kind}`,
190
+ pointerEvents: "none",
191
+ children: attention.kind === "needsInput" ? /*#__PURE__*/_jsx(Text, {
192
+ style: styles.badgeText,
193
+ children: "!"
194
+ }) : null
195
+ }) : null]
143
196
  });
144
197
  }
145
198
 
@@ -159,6 +212,7 @@ export function MinimizedToolsStack({
159
212
  minimizedTools,
160
213
  restore
161
214
  } = useMinimizedTools();
215
+ const peekAttention = useStrongestAttention(minimizedTools.map(t => t.id));
162
216
 
163
217
  // Calculate expanded height based on number of tools
164
218
  const expandedHeight = TOOLBAR_PADDING + minimizedTools.length * TOOL_ITEM_SIZE + (minimizedTools.length - 1) * ICON_GAP + ICON_GAP + COLLAPSE_BUTTON_SIZE + 4;
@@ -182,6 +236,11 @@ export function MinimizedToolsStack({
182
236
  collapsedLabel: `Show ${minimizedTools.length} minimized tools`,
183
237
  testID: "buoy-dock-peek",
184
238
  collapseTestID: "buoy-dock-collapse",
239
+ attention: peekAttention ? {
240
+ color: ATTENTION_COLORS[peekAttention.kind],
241
+ glyph: peekAttention.kind === "needsInput" ? "!" : undefined,
242
+ label: peekAttention.label
243
+ } : null,
185
244
  children: [...minimizedTools].reverse().map((tool, index) => /*#__PURE__*/_jsx(GlitchToolButton, {
186
245
  tool: tool,
187
246
  onSelect: handleToolPress,
@@ -201,5 +260,26 @@ const styles = StyleSheet.create({
201
260
  alignItems: "center",
202
261
  justifyContent: "center",
203
262
  backgroundColor: "transparent"
263
+ },
264
+ // Top-right of the chip. Needs-input carries a glyph; done/failed are a dot —
265
+ // the colour says which, the glyph says "act".
266
+ badge: {
267
+ position: "absolute",
268
+ top: 0,
269
+ right: 0,
270
+ minWidth: 12,
271
+ height: 12,
272
+ borderRadius: 6,
273
+ paddingHorizontal: 2,
274
+ alignItems: "center",
275
+ justifyContent: "center",
276
+ borderWidth: 1.5,
277
+ borderColor: "#0C0C0C"
278
+ },
279
+ badgeText: {
280
+ color: "#000",
281
+ fontSize: 8,
282
+ fontWeight: "800",
283
+ lineHeight: 9
204
284
  }
205
285
  });
@@ -16,8 +16,41 @@
16
16
  * used before this replaced it.
17
17
  */
18
18
 
19
- import { useCallback, useEffect, useRef, useState } from 'react';
20
- import { persistentStorage } from '@buoy-gg/shared-ui';
19
+ import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from 'react';
20
+ import { persistentStorage, getToolAttention, subscribeToolAttention } from '@buoy-gg/shared-ui';
21
+
22
+ /** Twin of the native hook — see MinimizedToolsStack.tsx. */
23
+ function useToolAttention(toolId) {
24
+ return useSyncExternalStore(subscribeToolAttention, () => getToolAttention(toolId), () => getToolAttention(toolId));
25
+ }
26
+ const ATTENTION_COLORS = {
27
+ needsInput: '#FF8329',
28
+ done: '#20C997',
29
+ failed: '#FF3B30'
30
+ };
31
+ const ATTENTION_RANK = {
32
+ needsInput: 3,
33
+ failed: 2,
34
+ done: 1
35
+ };
36
+ const strongestCache = new Map();
37
+ function strongest(key) {
38
+ let best = null;
39
+ for (const id of key ? key.split('|') : []) {
40
+ const a = getToolAttention(id);
41
+ if (a && (!best || ATTENTION_RANK[a.kind] > ATTENTION_RANK[best.kind])) best = a;
42
+ }
43
+ const prev = strongestCache.get(key);
44
+ if (prev && best && prev.kind === best.kind && prev.label === best.label) return prev;
45
+ if (!prev && !best) return null;
46
+ strongestCache.set(key, best);
47
+ return best;
48
+ }
49
+ /** Twin of the native hook: the strongest attention across the folded dock. */
50
+ function useStrongestAttention(toolIds) {
51
+ const key = toolIds.join('|');
52
+ return useSyncExternalStore(subscribeToolAttention, () => strongest(key), () => strongest(key));
53
+ }
21
54
  import { useAppHost } from "./AppHost.web.js";
22
55
 
23
56
  // native MinimizedToolsStack.tsx constants
@@ -58,11 +91,13 @@ function ChevronGlyph({
58
91
  });
59
92
  }
60
93
  function GlitchToolButton({
94
+ id,
61
95
  title,
62
96
  icon,
63
97
  index,
64
98
  onPress
65
99
  }) {
100
+ const attention = useToolAttention(id);
66
101
  const innerRef = useRef(null);
67
102
  useEffect(() => {
68
103
  let mounted = true;
@@ -113,9 +148,9 @@ function GlitchToolButton({
113
148
  clearTimeout(timer);
114
149
  };
115
150
  }, [index]);
116
- return /*#__PURE__*/_jsx("button", {
151
+ return /*#__PURE__*/_jsxs("button", {
117
152
  type: "button",
118
- "aria-label": `Restore ${title}`,
153
+ "aria-label": attention ? `${title} — ${attention.label}. Restore` : `Restore ${title}`,
119
154
  onClick: onPress,
120
155
  style: {
121
156
  width: TOOL_ITEM_SIZE,
@@ -126,9 +161,10 @@ function GlitchToolButton({
126
161
  background: 'none',
127
162
  border: 'none',
128
163
  padding: 0,
129
- cursor: 'pointer'
164
+ cursor: 'pointer',
165
+ position: 'relative'
130
166
  },
131
- children: /*#__PURE__*/_jsx("div", {
167
+ children: [/*#__PURE__*/_jsx("div", {
132
168
  ref: innerRef,
133
169
  style: {
134
170
  display: 'flex',
@@ -136,7 +172,30 @@ function GlitchToolButton({
136
172
  justifyContent: 'center'
137
173
  },
138
174
  children: icon
139
- })
175
+ }), attention ? /*#__PURE__*/_jsx("span", {
176
+ "aria-hidden": true,
177
+ "data-testid": `buoy-dock-badge-${id}-${attention.kind}`,
178
+ style: {
179
+ position: 'absolute',
180
+ top: 0,
181
+ right: 0,
182
+ minWidth: 12,
183
+ height: 12,
184
+ borderRadius: 6,
185
+ padding: '0 2px',
186
+ boxSizing: 'border-box',
187
+ background: ATTENTION_COLORS[attention.kind],
188
+ border: '1.5px solid #0C0C0C',
189
+ color: '#000',
190
+ fontSize: 8,
191
+ fontWeight: 800,
192
+ lineHeight: '9px',
193
+ display: 'flex',
194
+ alignItems: 'center',
195
+ justifyContent: 'center'
196
+ },
197
+ children: attention.kind === 'needsInput' ? '!' : ''
198
+ }) : null]
140
199
  });
141
200
  }
142
201
  export function MinimizedToolsStack({
@@ -149,6 +208,7 @@ export function MinimizedToolsStack({
149
208
  const minimized = openApps.filter(app => app.minimized && app.launchMode !== 'toggle-only');
150
209
  const [isExpanded, setIsExpanded] = useState(false);
151
210
  const [isStateRestored, setIsStateRestored] = useState(false);
211
+ const peekAttention = useStrongestAttention(minimized.map(app => app.id));
152
212
 
153
213
  // Restore + persist the expanded flag (native ExpandablePopover persistState).
154
214
  useEffect(() => {
@@ -216,6 +276,7 @@ export function MinimizedToolsStack({
216
276
  boxSizing: 'border-box'
217
277
  },
218
278
  children: [[...minimized].reverse().map((app, index) => /*#__PURE__*/_jsx(GlitchToolButton, {
279
+ id: app.id,
219
280
  title: app.title || app.id,
220
281
  icon: app.icon ?? /*#__PURE__*/_jsx("span", {
221
282
  style: {
@@ -270,7 +331,30 @@ export function MinimizedToolsStack({
270
331
  pointerEvents: isExpanded ? 'none' : 'auto',
271
332
  ...frame
272
333
  },
273
- children: [/*#__PURE__*/_jsx(ChevronGlyph, {}), minimized.length > 1 && /*#__PURE__*/_jsx("span", {
334
+ children: [/*#__PURE__*/_jsx(ChevronGlyph, {}), peekAttention ? /*#__PURE__*/_jsx("span", {
335
+ "aria-hidden": true,
336
+ "data-testid": `buoy-dock-peek-attention`,
337
+ style: {
338
+ position: 'absolute',
339
+ top: 2,
340
+ right: 2,
341
+ minWidth: 11,
342
+ height: 11,
343
+ borderRadius: 6,
344
+ padding: '0 2px',
345
+ boxSizing: 'border-box',
346
+ background: ATTENTION_COLORS[peekAttention.kind],
347
+ border: '1.5px solid #0C0C0C',
348
+ color: '#000',
349
+ fontSize: 8,
350
+ fontWeight: 800,
351
+ lineHeight: '9px',
352
+ display: 'flex',
353
+ alignItems: 'center',
354
+ justifyContent: 'center'
355
+ },
356
+ children: peekAttention.kind === 'needsInput' ? '!' : ''
357
+ }) : null, minimized.length > 1 && /*#__PURE__*/_jsx("span", {
274
358
  style: {
275
359
  fontSize: 9,
276
360
  fontWeight: 600,
@@ -11,4 +11,4 @@
11
11
  * Drift is prevented by a test that fails when this disagrees with
12
12
  * package.json, so a version bump that forgets this file cannot ship.
13
13
  */
14
- export const SDK_VERSION = "7.0.35";
14
+ export const SDK_VERSION = "7.0.36";
@@ -1 +1 @@
1
- {"version":3,"file":"MinimizedToolsStack.d.ts","sourceRoot":"","sources":["../../../../src/floatingMenu/MinimizedToolsStack.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAyC,MAAM,OAAO,CAAC;AAS9D,OAAO,EAAqB,aAAa,EAAE,MAAM,yBAAyB,CAAC;AA4L3E,MAAM,WAAW,wBAAwB;IACvC,8CAA8C;IAC9C,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,aAAa,KAAK,IAAI,CAAC;IAC1C,0DAA0D;IAC1D,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAMD,wBAAgB,mBAAmB,CAAC,EAClC,SAAS,EACT,cAA8B,GAC/B,EAAE,wBAAwB,qBAgD1B"}
1
+ {"version":3,"file":"MinimizedToolsStack.d.ts","sourceRoot":"","sources":["../../../../src/floatingMenu/MinimizedToolsStack.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA+D,MAAM,OAAO,CAAC;AAepF,OAAO,EAAqB,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAsP3E,MAAM,WAAW,wBAAwB;IACvC,8CAA8C;IAC9C,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,aAAa,KAAK,IAAI,CAAC;IAC1C,0DAA0D;IAC1D,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAMD,wBAAgB,mBAAmB,CAAC,EAClC,SAAS,EACT,cAA8B,GAC/B,EAAE,wBAAwB,qBA0D1B"}
@@ -1 +1 @@
1
- {"version":3,"file":"MinimizedToolsStack.web.d.ts","sourceRoot":"","sources":["../../../../src/floatingMenu/MinimizedToolsStack.web.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAyHH,MAAM,WAAW,wBAAwB;IACvC,gEAAgE;IAChE,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,wBAAgB,mBAAmB,CAAC,EAAE,cAAmB,EAAE,EAAE,wBAAwB,sCAsKpF"}
1
+ {"version":3,"file":"MinimizedToolsStack.web.d.ts","sourceRoot":"","sources":["../../../../src/floatingMenu/MinimizedToolsStack.web.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAiMH,MAAM,WAAW,wBAAwB;IACvC,gEAAgE;IAChE,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,wBAAgB,mBAAmB,CAAC,EAAE,cAAmB,EAAE,EAAE,wBAAwB,sCAmMpF"}
@@ -9,5 +9,5 @@
9
9
  * Drift is prevented by a test that fails when this disagrees with
10
10
  * package.json, so a version bump that forgets this file cannot ship.
11
11
  */
12
- export declare const SDK_VERSION = "7.0.35";
12
+ export declare const SDK_VERSION = "7.0.36";
13
13
  //# sourceMappingURL=version.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"MinimizedToolsStack.d.ts","sourceRoot":"","sources":["../../../../src/floatingMenu/MinimizedToolsStack.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAyC,MAAM,OAAO,CAAC;AAS9D,OAAO,EAAqB,aAAa,EAAE,MAAM,yBAAyB,CAAC;AA4L3E,MAAM,WAAW,wBAAwB;IACvC,8CAA8C;IAC9C,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,aAAa,KAAK,IAAI,CAAC;IAC1C,0DAA0D;IAC1D,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAMD,wBAAgB,mBAAmB,CAAC,EAClC,SAAS,EACT,cAA8B,GAC/B,EAAE,wBAAwB,qBAgD1B"}
1
+ {"version":3,"file":"MinimizedToolsStack.d.ts","sourceRoot":"","sources":["../../../../src/floatingMenu/MinimizedToolsStack.tsx"],"names":[],"mappings":"AAAA,OAAO,KAA+D,MAAM,OAAO,CAAC;AAepF,OAAO,EAAqB,aAAa,EAAE,MAAM,yBAAyB,CAAC;AAsP3E,MAAM,WAAW,wBAAwB;IACvC,8CAA8C;IAC9C,SAAS,CAAC,EAAE,CAAC,IAAI,EAAE,aAAa,KAAK,IAAI,CAAC;IAC1C,0DAA0D;IAC1D,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAMD,wBAAgB,mBAAmB,CAAC,EAClC,SAAS,EACT,cAA8B,GAC/B,EAAE,wBAAwB,qBA0D1B"}
@@ -1 +1 @@
1
- {"version":3,"file":"MinimizedToolsStack.web.d.ts","sourceRoot":"","sources":["../../../../src/floatingMenu/MinimizedToolsStack.web.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAyHH,MAAM,WAAW,wBAAwB;IACvC,gEAAgE;IAChE,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,wBAAgB,mBAAmB,CAAC,EAAE,cAAmB,EAAE,EAAE,wBAAwB,sCAsKpF"}
1
+ {"version":3,"file":"MinimizedToolsStack.web.d.ts","sourceRoot":"","sources":["../../../../src/floatingMenu/MinimizedToolsStack.web.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAiMH,MAAM,WAAW,wBAAwB;IACvC,gEAAgE;IAChE,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,wBAAgB,mBAAmB,CAAC,EAAE,cAAmB,EAAE,EAAE,wBAAwB,sCAmMpF"}
@@ -9,5 +9,5 @@
9
9
  * Drift is prevented by a test that fails when this disagrees with
10
10
  * package.json, so a version bump that forgets this file cannot ship.
11
11
  */
12
- export declare const SDK_VERSION = "7.0.35";
12
+ export declare const SDK_VERSION = "7.0.36";
13
13
  //# sourceMappingURL=version.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@buoy-gg/core",
3
- "version": "7.0.35",
3
+ "version": "7.0.36",
4
4
  "description": "Buoy devtools for React Native — a floating in-app dev menu; every tool streams live to your phone, desktop, and AI agent.",
5
5
  "main": "lib/commonjs/index.js",
6
6
  "module": "lib/module/index.js",
@@ -37,10 +37,10 @@
37
37
  ],
38
38
  "sideEffects": false,
39
39
  "dependencies": {
40
- "@buoy-gg/shared-ui": "^7.0.35",
41
- "@buoy-gg/floating-tools-core": "^7.0.35",
42
- "@buoy-gg/license": "^7.0.35",
43
- "@buoy-gg/floating-tools-react": "^7.0.35"
40
+ "@buoy-gg/shared-ui": "^7.0.36",
41
+ "@buoy-gg/license": "^7.0.36",
42
+ "@buoy-gg/floating-tools-react": "^7.0.36",
43
+ "@buoy-gg/floating-tools-core": "^7.0.36"
44
44
  },
45
45
  "peerDependencies": {
46
46
  "react": "*",