@buoy-gg/shared-ui 7.0.29 → 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/JsModal.js +18 -2
- package/lib/commonjs/JsModal.web.js +7 -4
- package/lib/commonjs/dataViewer/DataTreeActionDock.js +4 -2
- package/lib/commonjs/dataViewer/LiveExplorer.js +930 -0
- package/lib/commonjs/dataViewer/ValueEditModal.js +385 -0
- package/lib/commonjs/dataViewer/index.js +47 -0
- package/lib/commonjs/dataViewer/nestedPath.js +115 -0
- package/lib/commonjs/dataViewer/treeActions.js +3 -2
- package/lib/commonjs/dataViewer/useTreeDraft.js +125 -0
- package/lib/commonjs/dataViewer/useTreeEditor.js +105 -0
- package/lib/commonjs/env/EnvironmentIndicator.js +7 -1
- package/lib/commonjs/hooks/safe-area-impl.js +1 -1
- package/lib/commonjs/index.js +24 -0
- package/lib/commonjs/license/tierLimits.js +14 -0
- package/lib/commonjs/storage/devToolsStorageKeys.js +36 -0
- package/lib/commonjs/sync/toolActionRegistry.js +166 -0
- package/lib/commonjs/sync/toolUiBridge.js +74 -0
- package/lib/commonjs/ui/components/CompactRow.js +4 -0
- package/lib/commonjs/ui/components/EventHistoryViewer/DiffModeTabs.js +9 -1
- package/lib/commonjs/ui/components/ExpandedInfoRow.js +3 -1
- package/lib/commonjs/ui/components/ModalHeader.js +7 -4
- package/lib/commonjs/ui/components/SearchBar.js +5 -109
- package/lib/commonjs/ui/components/ToolButton.js +5 -2
- package/lib/module/JsModal.js +18 -2
- package/lib/module/JsModal.web.js +7 -4
- package/lib/module/dataViewer/DataTreeActionDock.js +4 -2
- package/lib/module/dataViewer/LiveExplorer.js +925 -0
- package/lib/module/dataViewer/ValueEditModal.js +381 -0
- package/lib/module/dataViewer/index.js +10 -1
- package/lib/module/dataViewer/nestedPath.js +109 -0
- package/lib/module/dataViewer/treeActions.js +3 -2
- package/lib/module/dataViewer/useTreeDraft.js +121 -0
- package/lib/module/dataViewer/useTreeEditor.js +101 -0
- package/lib/module/env/EnvironmentIndicator.js +7 -1
- package/lib/module/hooks/safe-area-impl.js +1 -1
- package/lib/module/index.js +2 -0
- package/lib/module/license/tierLimits.js +14 -0
- package/lib/module/storage/devToolsStorageKeys.js +36 -0
- package/lib/module/sync/toolActionRegistry.js +154 -0
- package/lib/module/sync/toolUiBridge.js +68 -0
- package/lib/module/ui/components/CompactRow.js +4 -0
- package/lib/module/ui/components/EventHistoryViewer/DiffModeTabs.js +9 -1
- package/lib/module/ui/components/ExpandedInfoRow.js +3 -1
- package/lib/module/ui/components/ModalHeader.js +7 -4
- package/lib/module/ui/components/SearchBar.js +8 -112
- package/lib/module/ui/components/ToolButton.js +5 -2
- package/lib/typescript/commonjs/JsModal.d.ts.map +1 -1
- package/lib/typescript/commonjs/JsModal.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/dataViewer/DataTreeActionDock.d.ts +6 -1
- package/lib/typescript/commonjs/dataViewer/DataTreeActionDock.d.ts.map +1 -1
- package/lib/typescript/commonjs/dataViewer/LiveExplorer.d.ts +73 -0
- package/lib/typescript/commonjs/dataViewer/LiveExplorer.d.ts.map +1 -0
- package/lib/typescript/commonjs/dataViewer/ValueEditModal.d.ts +40 -0
- package/lib/typescript/commonjs/dataViewer/ValueEditModal.d.ts.map +1 -0
- package/lib/typescript/commonjs/dataViewer/index.d.ts +5 -0
- package/lib/typescript/commonjs/dataViewer/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/dataViewer/nestedPath.d.ts +20 -0
- package/lib/typescript/commonjs/dataViewer/nestedPath.d.ts.map +1 -0
- package/lib/typescript/commonjs/dataViewer/treeActions.d.ts +10 -0
- package/lib/typescript/commonjs/dataViewer/treeActions.d.ts.map +1 -1
- package/lib/typescript/commonjs/dataViewer/useTreeDraft.d.ts +81 -0
- package/lib/typescript/commonjs/dataViewer/useTreeDraft.d.ts.map +1 -0
- package/lib/typescript/commonjs/dataViewer/useTreeEditor.d.ts +52 -0
- package/lib/typescript/commonjs/dataViewer/useTreeEditor.d.ts.map +1 -0
- package/lib/typescript/commonjs/env/EnvironmentIndicator.d.ts.map +1 -1
- package/lib/typescript/commonjs/hooks/safe-area-impl.d.ts +1 -1
- package/lib/typescript/commonjs/index.d.ts +2 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/license/tierLimits.d.ts +10 -0
- package/lib/typescript/commonjs/license/tierLimits.d.ts.map +1 -1
- package/lib/typescript/commonjs/storage/devToolsStorageKeys.d.ts +36 -0
- package/lib/typescript/commonjs/storage/devToolsStorageKeys.d.ts.map +1 -1
- package/lib/typescript/commonjs/sync/toolActionRegistry.d.ts +85 -0
- package/lib/typescript/commonjs/sync/toolActionRegistry.d.ts.map +1 -0
- package/lib/typescript/commonjs/sync/toolUiBridge.d.ts +39 -0
- package/lib/typescript/commonjs/sync/toolUiBridge.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/components/CompactRow.d.ts +5 -1
- package/lib/typescript/commonjs/ui/components/CompactRow.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/EventHistoryViewer/DiffModeTabs.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/EventHistoryViewer/types.d.ts +6 -0
- package/lib/typescript/commonjs/ui/components/EventHistoryViewer/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/ExpandedInfoRow.d.ts +7 -1
- package/lib/typescript/commonjs/ui/components/ExpandedInfoRow.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/SearchBar.d.ts +1 -3
- package/lib/typescript/commonjs/ui/components/SearchBar.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/ToolButton.d.ts +5 -1
- package/lib/typescript/commonjs/ui/components/ToolButton.d.ts.map +1 -1
- package/lib/typescript/module/JsModal.d.ts.map +1 -1
- package/lib/typescript/module/JsModal.web.d.ts.map +1 -1
- package/lib/typescript/module/dataViewer/DataTreeActionDock.d.ts +6 -1
- package/lib/typescript/module/dataViewer/DataTreeActionDock.d.ts.map +1 -1
- package/lib/typescript/module/dataViewer/LiveExplorer.d.ts +73 -0
- package/lib/typescript/module/dataViewer/LiveExplorer.d.ts.map +1 -0
- package/lib/typescript/module/dataViewer/ValueEditModal.d.ts +40 -0
- package/lib/typescript/module/dataViewer/ValueEditModal.d.ts.map +1 -0
- package/lib/typescript/module/dataViewer/index.d.ts +5 -0
- package/lib/typescript/module/dataViewer/index.d.ts.map +1 -1
- package/lib/typescript/module/dataViewer/nestedPath.d.ts +20 -0
- package/lib/typescript/module/dataViewer/nestedPath.d.ts.map +1 -0
- package/lib/typescript/module/dataViewer/treeActions.d.ts +10 -0
- package/lib/typescript/module/dataViewer/treeActions.d.ts.map +1 -1
- package/lib/typescript/module/dataViewer/useTreeDraft.d.ts +81 -0
- package/lib/typescript/module/dataViewer/useTreeDraft.d.ts.map +1 -0
- package/lib/typescript/module/dataViewer/useTreeEditor.d.ts +52 -0
- package/lib/typescript/module/dataViewer/useTreeEditor.d.ts.map +1 -0
- package/lib/typescript/module/env/EnvironmentIndicator.d.ts.map +1 -1
- package/lib/typescript/module/hooks/safe-area-impl.d.ts +1 -1
- package/lib/typescript/module/index.d.ts +2 -0
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/license/tierLimits.d.ts +10 -0
- package/lib/typescript/module/license/tierLimits.d.ts.map +1 -1
- package/lib/typescript/module/storage/devToolsStorageKeys.d.ts +36 -0
- package/lib/typescript/module/storage/devToolsStorageKeys.d.ts.map +1 -1
- package/lib/typescript/module/sync/toolActionRegistry.d.ts +85 -0
- package/lib/typescript/module/sync/toolActionRegistry.d.ts.map +1 -0
- package/lib/typescript/module/sync/toolUiBridge.d.ts +39 -0
- package/lib/typescript/module/sync/toolUiBridge.d.ts.map +1 -0
- package/lib/typescript/module/ui/components/CompactRow.d.ts +5 -1
- package/lib/typescript/module/ui/components/CompactRow.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/EventHistoryViewer/DiffModeTabs.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/EventHistoryViewer/types.d.ts +6 -0
- package/lib/typescript/module/ui/components/EventHistoryViewer/types.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/ExpandedInfoRow.d.ts +7 -1
- package/lib/typescript/module/ui/components/ExpandedInfoRow.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/SearchBar.d.ts +1 -3
- package/lib/typescript/module/ui/components/SearchBar.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/ToolButton.d.ts +5 -1
- package/lib/typescript/module/ui/components/ToolButton.d.ts.map +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Everything editing a tree document involves, minus how it looks.
|
|
5
|
+
*
|
|
6
|
+
* The phone and the dashboard run the same editor: one buffered draft, one
|
|
7
|
+
* write on save, a selection the structural actions operate on, a modal for
|
|
8
|
+
* typing a value, and a guard so closing can't silently drop unsaved work.
|
|
9
|
+
* What differs is presentation — thumb-sized tiles under a full screen versus
|
|
10
|
+
* chips docked in a panel — which is a rendering concern and nothing more.
|
|
11
|
+
*
|
|
12
|
+
* Keeping the rules here means the surfaces (and the tools — storage keys and
|
|
13
|
+
* zustand stores run this same hook) can't drift on the things that would
|
|
14
|
+
* actually hurt: whether a boolean opens an editor, whether a failed write
|
|
15
|
+
* closes the editor anyway, what "dirty" means.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { useCallback, useEffect, useState } from "react";
|
|
19
|
+
import { getAtPath } from "./jsonOps.js";
|
|
20
|
+
import { useTreeDraft } from "./useTreeDraft.js";
|
|
21
|
+
export function useTreeEditor({
|
|
22
|
+
value,
|
|
23
|
+
save,
|
|
24
|
+
onClose,
|
|
25
|
+
confirmDiscard
|
|
26
|
+
}) {
|
|
27
|
+
const draft = useTreeDraft(value, save);
|
|
28
|
+
const [selection, setSelection] = useState(null);
|
|
29
|
+
const [valuePath, setValuePath] = useState(null);
|
|
30
|
+
const {
|
|
31
|
+
begin
|
|
32
|
+
} = draft;
|
|
33
|
+
useEffect(() => {
|
|
34
|
+
begin();
|
|
35
|
+
// Seed the draft ONCE, when the editor opens. `begin` closes over the live
|
|
36
|
+
// value, whose identity changes every time the list refreshes underneath
|
|
37
|
+
// us (a parsed JSON string is a new object each pass), so re-running would
|
|
38
|
+
// reset the draft mid-edit and discard the user's work.
|
|
39
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
40
|
+
}, []);
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* "Change this node". For a boolean that's a flip rather than a modal — the
|
|
44
|
+
* same rule `treeActionsFor` applies to the action controls, so the gesture
|
|
45
|
+
* and the button can't mean different things.
|
|
46
|
+
*/
|
|
47
|
+
const openValueEditor = useCallback(path => {
|
|
48
|
+
const current = getAtPath(draft.value, path);
|
|
49
|
+
if (typeof current === "boolean") {
|
|
50
|
+
draft.apply({
|
|
51
|
+
kind: "replace",
|
|
52
|
+
path,
|
|
53
|
+
value: !current
|
|
54
|
+
});
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
setValuePath(path);
|
|
58
|
+
}, [draft]);
|
|
59
|
+
const applyValue = useCallback(next => {
|
|
60
|
+
draft.apply({
|
|
61
|
+
kind: "replace",
|
|
62
|
+
path: valuePath ?? [],
|
|
63
|
+
value: next
|
|
64
|
+
});
|
|
65
|
+
setValuePath(null);
|
|
66
|
+
}, [draft, valuePath]);
|
|
67
|
+
|
|
68
|
+
// Only leave once the write actually landed — `commit` resolves false and
|
|
69
|
+
// keeps the draft on screen when the owner refuses it, and closing anyway
|
|
70
|
+
// would throw the work away while showing an error nobody ever sees.
|
|
71
|
+
const saveAndClose = useCallback(() => {
|
|
72
|
+
void draft.commit().then(saved => saved && onClose());
|
|
73
|
+
}, [draft, onClose]);
|
|
74
|
+
const requestClose = useCallback(() => {
|
|
75
|
+
if (!draft.isDirty) {
|
|
76
|
+
onClose();
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
// The draft is the only copy of this work, so closing over it silently is
|
|
80
|
+
// the one way this editor can lose something the user can't get back.
|
|
81
|
+
confirmDiscard({
|
|
82
|
+
save: saveAndClose,
|
|
83
|
+
discard: () => {
|
|
84
|
+
draft.discard();
|
|
85
|
+
onClose();
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}, [confirmDiscard, draft, onClose, saveAndClose]);
|
|
89
|
+
return {
|
|
90
|
+
draft,
|
|
91
|
+
selection,
|
|
92
|
+
setSelection,
|
|
93
|
+
valuePath,
|
|
94
|
+
openValueEditor,
|
|
95
|
+
closeValueEditor: () => setValuePath(null),
|
|
96
|
+
applyValue,
|
|
97
|
+
openRawEditor: () => setValuePath([]),
|
|
98
|
+
requestClose,
|
|
99
|
+
saveAndClose
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -89,7 +89,13 @@ export function EnvironmentIndicator({
|
|
|
89
89
|
style: {
|
|
90
90
|
fontSize: 11,
|
|
91
91
|
fontWeight: "600",
|
|
92
|
-
fontFamily:
|
|
92
|
+
// No fontFamily on purpose: the platform UI font (SF Pro / Roboto)
|
|
93
|
+
// is what this badge is meant to look like, and it is what the DOM
|
|
94
|
+
// twin in EnvironmentIndicator.web.tsx has always rendered. This
|
|
95
|
+
// used to name "Poppins-SemiBold" — a family no app, package or
|
|
96
|
+
// example in the repo has ever loaded, so it resolved nowhere:
|
|
97
|
+
// silently the system font on device, and the browser's default
|
|
98
|
+
// SERIF under react-native-web (the docs demo).
|
|
93
99
|
color: gameUIColors.primaryLight,
|
|
94
100
|
letterSpacing: 0.5
|
|
95
101
|
},
|
package/lib/module/index.js
CHANGED
|
@@ -6,6 +6,8 @@ export * from "./ui/index.js";
|
|
|
6
6
|
// Store base classes
|
|
7
7
|
export * from "./stores/index.js";
|
|
8
8
|
export * from "./sync/wireBudget.js";
|
|
9
|
+
export * from "./sync/toolActionRegistry.js";
|
|
10
|
+
export * from "./sync/toolUiBridge.js";
|
|
9
11
|
|
|
10
12
|
// Utils exports - selectively export to avoid conflicts
|
|
11
13
|
export {
|
|
@@ -100,6 +100,20 @@ const LIMITS = {
|
|
|
100
100
|
anonymous: 1,
|
|
101
101
|
free: 3,
|
|
102
102
|
pro: Infinity
|
|
103
|
+
},
|
|
104
|
+
// ── scenarios ─────────────────────────────────────────────────────────────
|
|
105
|
+
// The SCENARIO is the unit, not the rules it creates: a free user's
|
|
106
|
+
// three-endpoint scenario either runs whole or refuses up front, and is never
|
|
107
|
+
// silently trimmed to one rule mid-run. Enforcement is atomic in pre-flight.
|
|
108
|
+
"scenarios.saved": {
|
|
109
|
+
anonymous: 1,
|
|
110
|
+
free: 3,
|
|
111
|
+
pro: Infinity
|
|
112
|
+
},
|
|
113
|
+
"scenarios.active": {
|
|
114
|
+
anonymous: 1,
|
|
115
|
+
free: 1,
|
|
116
|
+
pro: 3
|
|
103
117
|
}
|
|
104
118
|
};
|
|
105
119
|
export const TIER_LIMITS = LIMITS;
|
|
@@ -128,6 +128,18 @@ export const devToolsStorageKeys = {
|
|
|
128
128
|
root: () => `${devToolsStorageKeys.base}_time_machine`,
|
|
129
129
|
diffViewerMode: () => `${devToolsStorageKeys.timeMachine.root()}_diff_viewer_mode` // 'split' | 'tree'
|
|
130
130
|
},
|
|
131
|
+
/**
|
|
132
|
+
* Scenarios storage keys. The library (saved + draft scenarios) and the
|
|
133
|
+
* active-scenario record live in separate keys deliberately: the active
|
|
134
|
+
* record is written on every run and deactivation, while the library changes
|
|
135
|
+
* rarely, and a single blob would rewrite the whole library each run.
|
|
136
|
+
*/
|
|
137
|
+
scenarios: {
|
|
138
|
+
root: () => `${devToolsStorageKeys.base}_scenarios`,
|
|
139
|
+
modal: () => `${devToolsStorageKeys.scenarios.root()}_modal`,
|
|
140
|
+
library: () => `${devToolsStorageKeys.scenarios.root()}_library`,
|
|
141
|
+
active: () => `${devToolsStorageKeys.scenarios.root()}_active`
|
|
142
|
+
},
|
|
131
143
|
/**
|
|
132
144
|
* React Query-related storage keys
|
|
133
145
|
*/
|
|
@@ -202,16 +214,40 @@ export const devToolsStorageKeys = {
|
|
|
202
214
|
settings: () => `${devToolsStorageKeys.benchmark.root()}_settings`,
|
|
203
215
|
selectedReports: () => `${devToolsStorageKeys.benchmark.root()}_selected_reports`
|
|
204
216
|
},
|
|
217
|
+
/**
|
|
218
|
+
* Debug Borders overlay keys
|
|
219
|
+
*/
|
|
220
|
+
debugBorders: {
|
|
221
|
+
root: () => `${devToolsStorageKeys.base}_debug_borders`,
|
|
222
|
+
/** JsModal `persistenceKey` for the overlay's info window. */
|
|
223
|
+
infoModal: () => `${devToolsStorageKeys.debugBorders.root()}_info_modal`
|
|
224
|
+
},
|
|
205
225
|
/**
|
|
206
226
|
* Unified events-related storage keys
|
|
207
227
|
*/
|
|
208
228
|
events: {
|
|
209
229
|
root: () => `${devToolsStorageKeys.base}_events`,
|
|
230
|
+
/**
|
|
231
|
+
* NOT what the Events tool uses. Its preset passes the literal
|
|
232
|
+
* `"buoy-events-modal"` to JsModal, and three things outside this repo's
|
|
233
|
+
* runtime now pin that string: the website's live tour, its QA script, and
|
|
234
|
+
* the e2e runner's reset. `isDevToolsStorageKey` treats the `buoy-` prefix
|
|
235
|
+
* as internal, so nothing leaks — this is a naming inconsistency, not a
|
|
236
|
+
* bug, and moving the key would cost a coordinated change across four
|
|
237
|
+
* surfaces to buy tidiness. Kept so the shape of this file stays uniform.
|
|
238
|
+
*/
|
|
210
239
|
modal: () => `${devToolsStorageKeys.events.root()}_modal`,
|
|
211
240
|
/** Selected badge/source filters */
|
|
212
241
|
enabledSources: () => `${devToolsStorageKeys.events.root()}_enabled_sources`,
|
|
213
242
|
/** Whether event capturing is active */
|
|
214
243
|
isCapturing: () => `${devToolsStorageKeys.events.root()}_is_capturing`,
|
|
244
|
+
/**
|
|
245
|
+
* When capture was last switched off (ms epoch). Persisted with the
|
|
246
|
+
* capturing flag so a remount/relaunch while paused keeps hiding events
|
|
247
|
+
* captured into the shared store after the ORIGINAL pause moment (a
|
|
248
|
+
* watching dashboard keeps sources alive through the device's pause).
|
|
249
|
+
*/
|
|
250
|
+
pausedAt: () => `${devToolsStorageKeys.events.root()}_paused_at`,
|
|
215
251
|
/** Copy/export settings */
|
|
216
252
|
copySettings: () => `${devToolsStorageKeys.events.root()}_copy_settings`
|
|
217
253
|
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* toolActionRegistry — the on-device twin of the broker's action dispatch.
|
|
5
|
+
*
|
|
6
|
+
* Buoy already has exactly one place where `{toolId, action, params}` becomes a
|
|
7
|
+
* real call into a tool: `handleAction` in external-sync, fed by the adapter
|
|
8
|
+
* map that `autoExternalSync` assembles. That map was private to the React
|
|
9
|
+
* tree, so anything running ON the device (the Scenarios runner) had no way to
|
|
10
|
+
* reach it and would have had to duplicate the map — two dispatch paths that
|
|
11
|
+
* drift the first time an adapter changes.
|
|
12
|
+
*
|
|
13
|
+
* This module publishes the SAME map through a global seam, so a scenario step,
|
|
14
|
+
* a desktop click and an MCP call are one code path with two entry points.
|
|
15
|
+
*
|
|
16
|
+
* WHY A GLOBAL, NOT AN IMPORT: the map is built inside a component (the
|
|
17
|
+
* react-query adapter is a hook, the env adapter is a factory closed over a
|
|
18
|
+
* prop, and custom tools arrive as props), so it cannot be a module constant.
|
|
19
|
+
* Meanwhile the consumer must not import `@buoy-gg/core` — that package imports
|
|
20
|
+
* the tools, and a tool importing it back is the require cycle that puts a
|
|
21
|
+
* LogBox on every reload. Same reasoning, and the same shape, as
|
|
22
|
+
* `crashFlush.ts` in external-sync.
|
|
23
|
+
*
|
|
24
|
+
* WE STORE A GETTER, NOT A SNAPSHOT, for the reason `attachDeviceSync` does:
|
|
25
|
+
* the adapter map is re-created on renders that change its inputs, and a stale
|
|
26
|
+
* captured copy would keep dispatching into adapters the tree has replaced.
|
|
27
|
+
*
|
|
28
|
+
* SAFETY: this registry is populated only while `AutoExternalSync` is mounted,
|
|
29
|
+
* which is itself gated (dev, or release + an explicit opt-in + a real Pro
|
|
30
|
+
* license). It is deliberately NOT a substitute for a caller's own production
|
|
31
|
+
* gate — anything that bends app state through it must check `__DEV__` itself,
|
|
32
|
+
* because the actions reachable here (network overrides, storage writes,
|
|
33
|
+
* impersonation) have no business running in a shipped app.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
const REGISTRY_KEY = "__buoyToolActionRegistry";
|
|
37
|
+
|
|
38
|
+
/** The duck-typed adapter shape, matching external-sync's `ToolSyncAdapter`. */
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* A SET of getters, not one slot — React can briefly mount two providers, or
|
|
42
|
+
* re-run an effect's setup before the previous cleanup. A last-writer-wins slot
|
|
43
|
+
* would let the second registration silently blank the first.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
function sources() {
|
|
47
|
+
const g = globalThis;
|
|
48
|
+
if (!g[REGISTRY_KEY]) g[REGISTRY_KEY] = new Set();
|
|
49
|
+
return g[REGISTRY_KEY];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Publish the live adapter map. Called by `autoExternalSync` with a getter that
|
|
54
|
+
* reads its current `useMemo` value. Returns the unregister function.
|
|
55
|
+
*/
|
|
56
|
+
export function registerToolAdapters(getTools) {
|
|
57
|
+
const set = sources();
|
|
58
|
+
set.add(getTools);
|
|
59
|
+
return () => {
|
|
60
|
+
set.delete(getTools);
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** The merged adapter map across every live source. */
|
|
65
|
+
export function getToolAdapters() {
|
|
66
|
+
const merged = {};
|
|
67
|
+
for (const get of sources()) {
|
|
68
|
+
try {
|
|
69
|
+
Object.assign(merged, get());
|
|
70
|
+
} catch {
|
|
71
|
+
// A source that throws must not take the whole registry down.
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return merged;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** Tool ids currently dispatchable. */
|
|
78
|
+
export function listToolIds() {
|
|
79
|
+
return Object.keys(getToolAdapters());
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** `toolId -> action names`, for pre-flight validation and capability display. */
|
|
83
|
+
export function listToolActions() {
|
|
84
|
+
const out = {};
|
|
85
|
+
for (const [id, adapter] of Object.entries(getToolAdapters())) {
|
|
86
|
+
out[id] = Object.keys(adapter.actions ?? {});
|
|
87
|
+
}
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Tool ids whose adapter publishes a `getSnapshot`.
|
|
93
|
+
*
|
|
94
|
+
* Buoy's read surface is split in two: some tools answer through `actions`
|
|
95
|
+
* (`listQueries`, `async.getAllKeys`), and some answer only through the
|
|
96
|
+
* snapshot the sync layer polls — console entries, network events, the route
|
|
97
|
+
* sitemap, the env table. Anything driving tools ON the device sees only the
|
|
98
|
+
* first half unless it asks for the second, which is why the console and
|
|
99
|
+
* network panels look empty to an in-app agent that has an action for
|
|
100
|
+
* everything except reading them.
|
|
101
|
+
*/
|
|
102
|
+
export function listSnapshotToolIds() {
|
|
103
|
+
return Object.entries(getToolAdapters()).filter(([, adapter]) => typeof adapter.getSnapshot === "function").map(([id]) => id);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Read one tool's snapshot — the same value `attachDeviceSync` puts on the wire
|
|
108
|
+
* for MCP's `get_snapshot`, without a socket.
|
|
109
|
+
*
|
|
110
|
+
* Async to match {@link dispatchToolAction}: every caller already awaits, and
|
|
111
|
+
* an adapter is free to make this a promise later without breaking them.
|
|
112
|
+
*/
|
|
113
|
+
export async function readToolSnapshot(toolId) {
|
|
114
|
+
const tools = getToolAdapters();
|
|
115
|
+
const adapter = tools[toolId];
|
|
116
|
+
if (!adapter) throw new Error(`Unknown tool "${toolId}"`);
|
|
117
|
+
if (typeof adapter.getSnapshot !== "function") {
|
|
118
|
+
throw new Error(`Tool "${toolId}" has no snapshot to read`);
|
|
119
|
+
}
|
|
120
|
+
return await adapter.getSnapshot();
|
|
121
|
+
}
|
|
122
|
+
export function isToolRegistryReady() {
|
|
123
|
+
return sources().size > 0;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Dispatch one action, shaping failures exactly as `handleAction` does: an
|
|
128
|
+
* unknown tool and an unknown action are distinguishable, and adapter errors
|
|
129
|
+
* surface as plain messages rather than objects nobody can read on a phone.
|
|
130
|
+
*/
|
|
131
|
+
export async function dispatchToolAction(toolId, action, params) {
|
|
132
|
+
const tools = getToolAdapters();
|
|
133
|
+
const tool = tools[toolId];
|
|
134
|
+
const fn = tool?.actions?.[action];
|
|
135
|
+
if (!fn) {
|
|
136
|
+
throw new Error(tool ? `Unknown action "${action}" on tool "${toolId}"` : `Unknown tool "${toolId}"`);
|
|
137
|
+
}
|
|
138
|
+
return await fn(params);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Wait for the registry to be populated, for the narrow window between first
|
|
143
|
+
* frame and `AutoExternalSync` mounting. Resolves false on timeout so the
|
|
144
|
+
* caller can say "Buoy tools are still starting up" instead of "unknown tool".
|
|
145
|
+
*/
|
|
146
|
+
export async function waitForToolRegistry(timeoutMs = 2000) {
|
|
147
|
+
if (isToolRegistryReady()) return true;
|
|
148
|
+
const deadline = Date.now() + timeoutMs;
|
|
149
|
+
while (Date.now() < deadline) {
|
|
150
|
+
await new Promise(r => setTimeout(r, 50));
|
|
151
|
+
if (isToolRegistryReady()) return true;
|
|
152
|
+
}
|
|
153
|
+
return false;
|
|
154
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* toolUiBridge — let a tool ask the host to bring another tool back on screen.
|
|
5
|
+
*
|
|
6
|
+
* WHY THIS EXISTS. Buoy's tool overlays (the SIMULATED banner, the recording
|
|
7
|
+
* banner, the perf HUD) render as siblings of `AppHostProvider`, not inside it,
|
|
8
|
+
* so none of them can reach the host that knows how to open and restore tools.
|
|
9
|
+
* That is usually fine — an overlay's job is to be seen, not to navigate. It
|
|
10
|
+
* stops being fine the moment an overlay is the ONLY thing on screen and the
|
|
11
|
+
* next step lives inside a tool: the macro recorder minimizes Scenarios so a
|
|
12
|
+
* tester can drive the app, and when they stop, the review screen they need is
|
|
13
|
+
* behind a tool icon they have to know to hunt for.
|
|
14
|
+
*
|
|
15
|
+
* The alternative was moving every overlay inside the provider, which would
|
|
16
|
+
* re-order the stacking of six unrelated tools to fix one banner's button. This
|
|
17
|
+
* is the same global-seam shape `toolActionRegistry` already uses for the same
|
|
18
|
+
* reason: the capability lives inside a component tree, the caller must not
|
|
19
|
+
* import `@buoy-gg/core` (that package imports the tools, and a tool importing
|
|
20
|
+
* it back is a require cycle), so it is published through a global.
|
|
21
|
+
*
|
|
22
|
+
* A SET of openers, not one slot, for the reason the action registry keeps one:
|
|
23
|
+
* React can briefly mount two providers, or re-run an effect's setup before the
|
|
24
|
+
* previous cleanup, and a last-writer-wins slot lets the second registration
|
|
25
|
+
* silently blank the first.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const BRIDGE_KEY = "__buoyToolUiBridge";
|
|
29
|
+
|
|
30
|
+
/** Returns true when the tool was actually brought back on screen. */
|
|
31
|
+
|
|
32
|
+
function openers() {
|
|
33
|
+
const g = globalThis;
|
|
34
|
+
if (!g[BRIDGE_KEY]) g[BRIDGE_KEY] = new Set();
|
|
35
|
+
return g[BRIDGE_KEY];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Published by the host, inside the provider that owns open tools. */
|
|
39
|
+
export function registerToolOpener(opener) {
|
|
40
|
+
const set = openers();
|
|
41
|
+
set.add(opener);
|
|
42
|
+
return () => {
|
|
43
|
+
set.delete(opener);
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Ask the host to show a tool.
|
|
49
|
+
*
|
|
50
|
+
* Returns false when nothing could do it — no host mounted, or the tool isn't
|
|
51
|
+
* open for it to restore. Callers must treat that as a real answer and say so,
|
|
52
|
+
* rather than rendering a button that silently does nothing.
|
|
53
|
+
*/
|
|
54
|
+
export function openBuoyTool(toolId) {
|
|
55
|
+
for (const opener of openers()) {
|
|
56
|
+
try {
|
|
57
|
+
if (opener(toolId)) return true;
|
|
58
|
+
} catch {
|
|
59
|
+
// A host that throws must not stop another from answering.
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** True when some host is listening — for deciding whether to offer the button. */
|
|
66
|
+
export function canOpenBuoyTools() {
|
|
67
|
+
return openers().size > 0;
|
|
68
|
+
}
|
|
@@ -25,6 +25,8 @@ export function CompactRow({
|
|
|
25
25
|
testID,
|
|
26
26
|
statusTestID,
|
|
27
27
|
statusSublabelTestID,
|
|
28
|
+
primaryTestID,
|
|
29
|
+
secondaryTestID,
|
|
28
30
|
accessibilityLabel
|
|
29
31
|
}) {
|
|
30
32
|
return /*#__PURE__*/_jsx(View, {
|
|
@@ -78,12 +80,14 @@ export function CompactRow({
|
|
|
78
80
|
children: [/*#__PURE__*/_jsx(Text, {
|
|
79
81
|
style: styles.queryHash,
|
|
80
82
|
numberOfLines: isExpanded ? undefined : 2,
|
|
83
|
+
testID: primaryTestID,
|
|
81
84
|
children: primaryText
|
|
82
85
|
}), !isExpanded && (secondaryText || secondaryAccessory) ? /*#__PURE__*/_jsxs(View, {
|
|
83
86
|
style: styles.secondaryRow,
|
|
84
87
|
children: [secondaryText ? /*#__PURE__*/_jsx(Text, {
|
|
85
88
|
style: styles.secondaryText,
|
|
86
89
|
numberOfLines: 1,
|
|
90
|
+
testID: secondaryTestID,
|
|
87
91
|
children: secondaryText
|
|
88
92
|
}) : null, secondaryAccessory]
|
|
89
93
|
}) : null]
|
|
@@ -19,7 +19,8 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
19
19
|
export const DiffModeTabs = /*#__PURE__*/memo(function DiffModeTabs({
|
|
20
20
|
tabs,
|
|
21
21
|
activeTab,
|
|
22
|
-
onTabChange
|
|
22
|
+
onTabChange,
|
|
23
|
+
testIDPrefix
|
|
23
24
|
}) {
|
|
24
25
|
return /*#__PURE__*/_jsx(View, {
|
|
25
26
|
style: styles.container,
|
|
@@ -31,6 +32,13 @@ export const DiffModeTabs = /*#__PURE__*/memo(function DiffModeTabs({
|
|
|
31
32
|
onPress: () => !isDisabled && onTabChange(tab.key),
|
|
32
33
|
activeOpacity: isDisabled ? 1 : 0.7,
|
|
33
34
|
disabled: isDisabled,
|
|
35
|
+
testID: testIDPrefix ? `${testIDPrefix}-${tab.key}` : undefined,
|
|
36
|
+
accessibilityRole: "button",
|
|
37
|
+
accessibilityLabel: tab.label,
|
|
38
|
+
accessibilityState: {
|
|
39
|
+
selected: isActive,
|
|
40
|
+
disabled: isDisabled
|
|
41
|
+
},
|
|
34
42
|
children: /*#__PURE__*/_jsx(Text, {
|
|
35
43
|
style: [styles.tabText, isActive && styles.tabTextActive, isDisabled && styles.tabTextDisabled],
|
|
36
44
|
children: tab.label
|
|
@@ -40,7 +40,8 @@ export function PillBadge({
|
|
|
40
40
|
color,
|
|
41
41
|
children,
|
|
42
42
|
icon,
|
|
43
|
-
size = "md"
|
|
43
|
+
size = "md",
|
|
44
|
+
testID
|
|
44
45
|
}) {
|
|
45
46
|
const isSm = size === "sm";
|
|
46
47
|
return /*#__PURE__*/_jsxs(View, {
|
|
@@ -55,6 +56,7 @@ export function PillBadge({
|
|
|
55
56
|
style: [styles.pillText, isSm && styles.pillTextSm, {
|
|
56
57
|
color
|
|
57
58
|
}],
|
|
59
|
+
testID: testID,
|
|
58
60
|
children: children
|
|
59
61
|
})]
|
|
60
62
|
});
|
|
@@ -182,10 +182,13 @@ const styles = StyleSheet.create({
|
|
|
182
182
|
flex: 1,
|
|
183
183
|
gap: 8,
|
|
184
184
|
minHeight: 32,
|
|
185
|
-
paddingLeft: 4
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
|
|
185
|
+
paddingLeft: 4
|
|
186
|
+
// No right inset: the traffic-light window controls are cleared
|
|
187
|
+
// VERTICALLY — JsModal's drag-indicator strip owns their band (see
|
|
188
|
+
// WINDOW_CONTROLS_BAND) — so nothing here has to be pushed sideways to
|
|
189
|
+
// stay out from under them. `headerActions` already ends 4pt off the
|
|
190
|
+
// edge, matching the left, and hosts that render this header with no
|
|
191
|
+
// window controls at all (the desktop inline panels) get no phantom gap.
|
|
189
192
|
},
|
|
190
193
|
navigationButton: {
|
|
191
194
|
padding: 4
|