@buoy-gg/agent-core 7.0.43 → 7.0.45
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/catalog/catalog.g.js +2 -2
- package/lib/commonjs/catalog/catalog.source.json +1285 -401
- package/lib/commonjs/catalog/catalog.types.g.js +1 -1
- package/lib/commonjs/catalog/normalizeParams.js +2 -2
- package/lib/commonjs/catalog/snapshotReads.js +1 -1
- package/lib/commonjs/context/buildContextPack.js +1 -1
- package/lib/commonjs/effects/ledger.js +1 -1
- package/lib/commonjs/effects/storeDiff.js +1 -0
- package/lib/commonjs/engine/compactScreen.js +1 -0
- package/lib/commonjs/engine/effectFor.js +1 -1
- package/lib/commonjs/engine/planOnlyStop.js +1 -0
- package/lib/commonjs/engine/runAgentTurn.js +7 -7
- package/lib/commonjs/engine/systemPrompt.js +13 -6
- package/lib/commonjs/engine/verify.js +1 -1
- package/lib/commonjs/providers/openai.js +3 -5
- package/lib/commonjs/providers/types.js +1 -1
- package/lib/module/catalog/catalog.g.js +2 -2
- package/lib/module/catalog/catalog.source.json +1285 -401
- package/lib/module/catalog/catalog.types.g.js +1 -1
- package/lib/module/catalog/normalizeParams.js +1 -1
- package/lib/module/catalog/snapshotReads.js +1 -1
- package/lib/module/context/buildContextPack.js +1 -1
- package/lib/module/effects/ledger.js +1 -1
- package/lib/module/effects/storeDiff.js +1 -0
- package/lib/module/engine/compactScreen.js +1 -0
- package/lib/module/engine/effectFor.js +1 -1
- package/lib/module/engine/planOnlyStop.js +1 -0
- package/lib/module/engine/runAgentTurn.js +7 -7
- package/lib/module/engine/systemPrompt.js +13 -6
- package/lib/module/engine/verify.js +1 -1
- package/lib/module/providers/openai.js +3 -5
- package/lib/module/providers/types.js +1 -1
- package/lib/typescript/catalog/catalog.g.d.ts +3 -3
- package/lib/typescript/catalog/catalog.types.g.d.ts +7 -4
- package/lib/typescript/effects/ledger.d.ts +13 -0
- package/lib/typescript/effects/storeDiff.d.ts +23 -0
- package/lib/typescript/engine/compactScreen.d.ts +18 -0
- package/lib/typescript/engine/planOnlyStop.d.ts +22 -0
- package/lib/typescript/engine/runAgentTurn.d.ts +1 -1
- package/lib/typescript/providers/types.d.ts +8 -0
- package/lib/web/index.mjs +46 -41
- package/package.json +1 -1
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
{
|
|
3
3
|
"toolId": "env",
|
|
4
4
|
"title": "Env",
|
|
5
|
-
"summary": "Env is a read-only snapshot tool with exactly one action, getSnapshot. The payload is `{ env: Record<string,string>, requiredEnvVars: (string | {key, expectedValue, description?} | {key, expectedType, description?})[] }` where `expectedType` is one of string|number|boolean|array|object|url. Reach for it to answer \"what API URL / feature flag / environment is this build pointed at\" and \"which required env vars are missing, empty, or the wrong value/type\". Values are baked into the JS bundle at build time and the adapter's `subscribe` is a no-op, so the snapshot is static for the life of the app
|
|
5
|
+
"summary": "Env is a read-only snapshot tool with exactly one action, getSnapshot. The payload is `{ env: Record<string,string>, requiredEnvVars: (string | {key, expectedValue, description?} | {key, expectedType, description?})[] }` where `expectedType` is one of string|number|boolean|array|object|url. Reach for it to answer \"what API URL / feature flag / environment is this build pointed at\" and \"which required env vars are missing, empty, or the wrong value/type\". Values are baked into the JS bundle at build time and the adapter's `subscribe` is a no-op, so the snapshot is static for the life of the app \u2014 there is no way to set, change, or reload an env var from here, and only EXPO_PUBLIC_-style vars exist in the RN runtime at all (secrets are not present).",
|
|
6
6
|
"actions": [
|
|
7
7
|
{
|
|
8
8
|
"action": "getSnapshot",
|
|
9
9
|
"summary": "Read this build's environment: every EXPO_PUBLIC_-style variable it was compiled with, plus the app's required-env-var checks.",
|
|
10
|
-
"description": "Returns `{ env: Record<string,string>, requiredEnvVars: [...] }`. This is the ONLY read env has
|
|
10
|
+
"description": "Returns `{ env: Record<string,string>, requiredEnvVars: [...] }`. This is the ONLY read env has \u2014 it exposes no other action. Values are baked into the JS bundle at build time and never change at runtime, so one read is good for the life of the app, and real secrets are not present (only EXPO_PUBLIC_-style vars exist in the RN runtime at all).",
|
|
11
11
|
"params": {
|
|
12
12
|
"type": "object",
|
|
13
13
|
"properties": {},
|
|
@@ -17,17 +17,17 @@
|
|
|
17
17
|
"release": "works"
|
|
18
18
|
}
|
|
19
19
|
],
|
|
20
|
-
"unavailableWhen": "The whole tool is absent unless @buoy-gg/env resolves at runtime (autoExternalSync only registers `map.env` when the optional require succeeds)
|
|
20
|
+
"unavailableWhen": "The whole tool is absent unless @buoy-gg/env resolves at runtime (autoExternalSync only registers `map.env` when the optional require succeeds) \u2014 and, separately, no env snapshot reaches the agent at all in a release bundle: the device only dials the broker when `__DEV__` is true, or when the app opts in with `enableInRelease: true` plus a valid Pro license."
|
|
21
21
|
},
|
|
22
22
|
{
|
|
23
23
|
"toolId": "console",
|
|
24
24
|
"title": "Console",
|
|
25
|
-
"summary": "Reach for this when the app crashed, redboxed, or logged something you need to see: the device's captured console.log/info/warn/error/debug/trace/dir/table/assert/group output plus uncaught JS errors tagged [FATAL]/[UNCAUGHT]/[RENDER ERROR] with component stacks. Read it with getSnapshot (a compact time
|
|
25
|
+
"summary": "Reach for this when the app crashed, redboxed, or logged something you need to see: the device's captured console.log/info/warn/error/debug/trace/dir/table/assert/group output plus uncaught JS errors tagged [FATAL]/[UNCAUGHT]/[RENDER ERROR] with component stacks. Read it with getSnapshot (a compact time\u00b7level\u00b7message tail, filterable by level/pattern and capped by limit). The only other action is clearEntries, and it is destructive: it wipes the crash evidence. Capture is a 1000-entry in-memory ring buffer that starts when Buoy mounts, so anything logged before that is absent, and it resets on every JS reload unless the user turned on \"Preserve log\" (@react_buoy_console_preserve).",
|
|
26
26
|
"actions": [
|
|
27
27
|
{
|
|
28
28
|
"action": "getSnapshot",
|
|
29
|
-
"summary": "Read the app's captured console output
|
|
30
|
-
"description": "Returns `{ totalCaptured, shown, entries: [{at, level, message}] }`, newest last. Narrow with `level` (severity threshold), `pattern` (substring) and `limit`
|
|
29
|
+
"summary": "Read the app's captured console output \u2014 log/info/warn/error plus uncaught JS errors tagged [FATAL]. Start here when the app crashed, redboxed, or logged something.",
|
|
30
|
+
"description": "Returns `{ totalCaptured, shown, entries: [{at, level, message}] }`, newest last. Narrow with `level` (severity threshold), `pattern` (substring) and `limit` \u2014 the raw buffer holds up to 1000 entries at roughly 1KB each, so an unfiltered read is the most expensive thing you can ask for. Only the pre-rendered message is returned; full args and stacks stay on the device. Capture is an in-memory ring buffer that starts when Buoy mounts and resets on every JS reload unless the user turned on \"Preserve log\", so anything logged before that is genuinely absent \u2014 say \"nothing was captured\", never \"nothing happened\".",
|
|
31
31
|
"params": {
|
|
32
32
|
"type": "object",
|
|
33
33
|
"properties": {
|
|
@@ -68,25 +68,25 @@
|
|
|
68
68
|
},
|
|
69
69
|
"effect": "destructive",
|
|
70
70
|
"release": "works",
|
|
71
|
-
"description": "Takes no params
|
|
71
|
+
"description": "Takes no params \u2014 the handler ignores anything passed. Calls consoleLogStore.clearEntries(), which (1) fires the onClear listeners so a desktop dashboard in mirror mode forwards the clear down to the device, (2) removes the persisted key `@react_buoy_console_buffer` from storage, and (3) empties the in-memory 1000-entry ring buffer and notifies subscribers. Always returns {cleared:true}, even if the buffer was already empty \u2014 a true result is NOT evidence anything existed. There is no undo and no re-capture: a crash entry ([FATAL]/[UNCAUGHT]/[RENDER ERROR], the app's last words before it died) is gone for good, and a dead app will never re-log it. Read with get_console / get_snapshot('console') BEFORE clearing. Legitimate use is narrow: zeroing the log right before reproducing a bug so the next read contains only that repro.",
|
|
72
72
|
"requires": [
|
|
73
73
|
"@buoy-gg/console installed in the app",
|
|
74
|
-
"<FloatingDevTools /> rendered
|
|
74
|
+
"<FloatingDevTools /> rendered \u2014 it auto-mounts ConsoleRoot (capture) and registers consoleSyncAdapter as the \"console\" capability",
|
|
75
75
|
"external-sync connection to the broker (:42831)",
|
|
76
|
-
"in a release build capture starts only when ConsoleRoot mounts
|
|
76
|
+
"in a release build capture starts only when ConsoleRoot mounts \u2014 the import-time install is __DEV__-gated, so pre-mount/boot console output is never captured"
|
|
77
77
|
]
|
|
78
78
|
}
|
|
79
79
|
],
|
|
80
|
-
"unavailableWhen": "The app doesn't depend on @buoy-gg/console, or FloatingDevTools never renders
|
|
80
|
+
"unavailableWhen": "The app doesn't depend on @buoy-gg/console, or FloatingDevTools never renders \u2014 in a release build it bails out for free users (only a Pro license renders it), so neither the console capability nor its snapshot is announced at all."
|
|
81
81
|
},
|
|
82
82
|
{
|
|
83
83
|
"toolId": "sentry",
|
|
84
84
|
"title": "Sentry",
|
|
85
|
-
"summary": "What the app is SENDING to Sentry
|
|
85
|
+
"summary": "What the app is SENDING to Sentry \u2014 errors, transactions, logs, sessions \u2014 captured at the SDK's beforeEnvelope tee on their way out of the device. Reach for it when the user asks whether an error/crash was reported to Sentry, what Sentry traffic the app produces, or why the Sentry bill is big (transaction items carry spanCount \u2014 spans are Sentry's tracing billing unit). Read with getSnapshot; clearEnvelopes wipes the captured list on the device only (copies already sent to Sentry are unaffected). The snapshot's `status` matters: \"sdk-not-found\" or \"no-client\" means the app has no live Sentry client, so say that plainly \u2014 an empty list is NOT evidence that nothing errored. For crash stack traces themselves, the console tool's [FATAL] entries are usually the better read.",
|
|
86
86
|
"actions": [
|
|
87
87
|
{
|
|
88
88
|
"action": "getSnapshot",
|
|
89
|
-
"summary": "Read the envelopes the app has sent to Sentry
|
|
89
|
+
"summary": "Read the envelopes the app has sent to Sentry \u2014 newest first, summaries only. Start here for \"was that error reported?\" and \"what is the app sending to Sentry?\".",
|
|
90
90
|
"params": {
|
|
91
91
|
"type": "object",
|
|
92
92
|
"properties": {
|
|
@@ -107,7 +107,7 @@
|
|
|
107
107
|
},
|
|
108
108
|
"effect": "read",
|
|
109
109
|
"release": "works",
|
|
110
|
-
"description": "Returns `{status, totalCaptured, shown, envelopes:[{id, at, origin, eventId, totalBytes, items:[{type, summary, spanCount, bytes}]}]}`, newest first. Item payloads stay on the device
|
|
110
|
+
"description": "Returns `{status, totalCaptured, shown, envelopes:[{id, at, origin, eventId, totalBytes, items:[{type, summary, spanCount, bytes}]}]}`, newest first. Item payloads stay on the device \u2014 the summary line is the one-line human read (error headline, transaction name, log count). `status` is \"attached\" when capture is live; \"searching\" right after launch; \"sdk-not-found\"/\"no-client\" when the app has no Sentry client, in which case nothing can ever appear here. Capture starts when Buoy mounts, so envelopes sent before that are absent \u2014 \"nothing was captured\", never \"nothing was sent\"."
|
|
111
111
|
},
|
|
112
112
|
{
|
|
113
113
|
"action": "clearEnvelopes",
|
|
@@ -128,11 +128,11 @@
|
|
|
128
128
|
{
|
|
129
129
|
"toolId": "jotai",
|
|
130
130
|
"title": "Jotai",
|
|
131
|
-
"summary": "Reads and writes the app's Jotai atoms: list registered atoms with change counts and writability, fetch one atom's current value, fetch the real prev/next values behind one recorded change, wipe the change timeline, and set a writable atom. Reach for it when on-screen data disagrees with the API or a value looks stale/wrong and the app uses Jotai. Only atoms the app explicitly passed to watchAtoms()/watchDefaultStoreAtoms() exist here
|
|
131
|
+
"summary": "Reads and writes the app's Jotai atoms: list registered atoms with change counts and writability, fetch one atom's current value, fetch the real prev/next values behind one recorded change, wipe the change timeline, and set a writable atom. Reach for it when on-screen data disagrees with the API or a value looks stale/wrong and the app uses Jotai. Only atoms the app explicitly passed to watchAtoms()/watchDefaultStoreAtoms() exist here \u2014 coverage is opt-in, so an empty list means nothing was registered, not that the app has no state. For the HISTORY of atom changes use get_events with sources:['jotai']; this tool's snapshot deliberately ships value-free markers and you fetch values on demand.",
|
|
132
132
|
"actions": [
|
|
133
133
|
{
|
|
134
134
|
"action": "listAtoms",
|
|
135
|
-
"summary": "Compact reader for a remote driver: the registered atoms with light metadata. Each atom's value stays on the device unless `includeValues` is set, and `limit` caps how many are returned. `writable` says which can be set via setAtom. Each atom also carries `shape`
|
|
135
|
+
"summary": "Compact reader for a remote driver: the registered atoms with light metadata. Each atom's value stays on the device unless `includeValues` is set, and `limit` caps how many are returned. `writable` says which can be set via setAtom. Each atom also carries `shape` \u2014 the item type of any list it holds \u2014 which is the cheap way to learn what an addition must look like.",
|
|
136
136
|
"params": {
|
|
137
137
|
"type": "object",
|
|
138
138
|
"properties": {
|
|
@@ -149,7 +149,7 @@
|
|
|
149
149
|
},
|
|
150
150
|
"effect": "read",
|
|
151
151
|
"release": "works",
|
|
152
|
-
"description": "Compact reader for a remote driver: the registered atoms with light metadata. Each atom's value stays on the device unless `includeValues` is set, and `limit` caps how many are returned. `writable` says which can be set via setAtom. Each atom also carries `shape`
|
|
152
|
+
"description": "Compact reader for a remote driver: the registered atoms with light metadata. Each atom's value stays on the device unless `includeValues` is set, and `limit` caps how many are returned. `writable` says which can be set via setAtom. Each atom also carries `shape` \u2014 the item type of any list it holds \u2014 which is the cheap way to learn what an addition must look like.",
|
|
153
153
|
"requires": [
|
|
154
154
|
"@buoy-gg/jotai installed in the app",
|
|
155
155
|
"watchAtoms(store, atoms) or watchDefaultStoreAtoms(atoms) called at app startup"
|
|
@@ -157,13 +157,13 @@
|
|
|
157
157
|
},
|
|
158
158
|
{
|
|
159
159
|
"action": "getAtomValue",
|
|
160
|
-
"summary": "Fetch one atom's current value on demand, by label. Send `path` to get back ONE value instead of the whole atom (path:\"lines[id=seed-1].qty\")
|
|
160
|
+
"summary": "Fetch one atom's current value on demand, by label. Send `path` to get back ONE value instead of the whole atom (path:\"lines[id=seed-1].qty\") \u2014 do that whenever the value is big, because results are cut off at 24,000 characters. Also returns `shape`: a one-line sketch of the value's type plus, for each list in it, what its items look like. Read it before writing and match it exactly.",
|
|
161
161
|
"params": {
|
|
162
162
|
"type": "object",
|
|
163
163
|
"properties": {
|
|
164
164
|
"label": {
|
|
165
165
|
"type": "string",
|
|
166
|
-
"description": "Atom label exactly as registered
|
|
166
|
+
"description": "Atom label exactly as registered \u2014 the object key in watchAtoms(store, { countAtom }), e.g. \"countAtom\". Get exact labels from listAtoms."
|
|
167
167
|
},
|
|
168
168
|
"path": {
|
|
169
169
|
"type": "string",
|
|
@@ -177,9 +177,9 @@
|
|
|
177
177
|
},
|
|
178
178
|
"effect": "read",
|
|
179
179
|
"release": "works",
|
|
180
|
-
"description": "Fetch one atom's current value on demand, by label. Send `path` to get back ONE value instead of the whole atom (path:\"lines[id=seed-1].qty\")
|
|
180
|
+
"description": "Fetch one atom's current value on demand, by label. Send `path` to get back ONE value instead of the whole atom (path:\"lines[id=seed-1].qty\") \u2014 do that whenever the value is big, because results are cut off at 24,000 characters. Also returns `shape`: a one-line sketch of the value's type plus, for each list in it, what its items look like. Read it before writing and match it exactly.",
|
|
181
181
|
"requires": [
|
|
182
|
-
"The label must already be registered via watchAtoms
|
|
182
|
+
"The label must already be registered via watchAtoms \u2014 call listAtoms first for exact labels"
|
|
183
183
|
]
|
|
184
184
|
},
|
|
185
185
|
{
|
|
@@ -190,7 +190,7 @@
|
|
|
190
190
|
"properties": {
|
|
191
191
|
"id": {
|
|
192
192
|
"type": "string",
|
|
193
|
-
"description": "Change id from a jotai snapshot row or get_events sources:['jotai'], shaped \"<epochMs>-<counter>\" e.g. \"1755102003123-42\". Omitting it does not throw
|
|
193
|
+
"description": "Change id from a jotai snapshot row or get_events sources:['jotai'], shaped \"<epochMs>-<counter>\" e.g. \"1755102003123-42\". Omitting it does not throw \u2014 it returns {found:false, reason:'missing id'}."
|
|
194
194
|
}
|
|
195
195
|
},
|
|
196
196
|
"required": [
|
|
@@ -200,14 +200,14 @@
|
|
|
200
200
|
},
|
|
201
201
|
"effect": "read",
|
|
202
202
|
"release": "works",
|
|
203
|
-
"description": "Returns { found:true, id, prevValue, nextValue } or { found:false, reason:'missing id' | 'unknown id' }. The streamed change timeline carries {__buoyValueOnDevice:true} in place of both values (up to 200 changes x 2 values per snapshot would blow the wire budget), so this is the only way to see what a change actually contained. `id` comes from a jotai snapshot row or a get_events sources:['jotai'] row and has the shape \"<epochMs>-<counter>\" (e.g. \"1755102003123-42\"). Only the newest 200 changes are retained
|
|
203
|
+
"description": "Returns { found:true, id, prevValue, nextValue } or { found:false, reason:'missing id' | 'unknown id' }. The streamed change timeline carries {__buoyValueOnDevice:true} in place of both values (up to 200 changes x 2 values per snapshot would blow the wire budget), so this is the only way to see what a change actually contained. `id` comes from a jotai snapshot row or a get_events sources:['jotai'] row and has the shape \"<epochMs>-<counter>\" (e.g. \"1755102003123-42\"). Only the newest 200 changes are retained \u2014 older ids return found:false, and clearEvents drops them all. Values over 8MB are replaced with {__buoyTruncated:true}.",
|
|
204
204
|
"requires": [
|
|
205
205
|
"A change id from the jotai snapshot or get_events sources:['jotai']"
|
|
206
206
|
]
|
|
207
207
|
},
|
|
208
208
|
{
|
|
209
209
|
"action": "setAtom",
|
|
210
|
-
"summary": "Write to a writable Jotai atom, named by its `label`
|
|
210
|
+
"summary": "Write to a writable Jotai atom, named by its `label` \u2014 this mutates the running app's state. Three forms: `value` alone REPLACES; `value` with `merge:true` merges a patch under the typed-edit rule (change existing fields to the same type; a list may grow or shrink but every item must have the fields the others have); `path`+`value` sets ONE value with no nesting to get wrong (path:\"lines[id=seed-1].qty\"). To change one item of a list, address it by its own id \u2014 {\"lines\":{\"seed-1\":{\"qty\":5}}} changes one, {\"lines\":{\"seed-2\":null}} removes one, a new id adds one; items you don't name are untouched. A plain array replaces the whole list. A refused merge names the field and, when the problem was depth, hands back the corrected patch \u2014 fix it that way rather than reaching for `force`, which writes raw and can crash the screen.",
|
|
211
211
|
"params": {
|
|
212
212
|
"type": "object",
|
|
213
213
|
"properties": {
|
|
@@ -216,7 +216,7 @@
|
|
|
216
216
|
"description": "Atom label from listAtoms; must be writable:true. The wire param is `label` (the MCP tool calls it `atom`)."
|
|
217
217
|
},
|
|
218
218
|
"value": {
|
|
219
|
-
"description": "The new value
|
|
219
|
+
"description": "The new value \u2014 any JSON (number, string, boolean, object, array, null). Passed straight to store.set(atom, value); it REPLACES the value, no merging."
|
|
220
220
|
},
|
|
221
221
|
"merge": {
|
|
222
222
|
"type": "boolean",
|
|
@@ -224,7 +224,7 @@
|
|
|
224
224
|
},
|
|
225
225
|
"path": {
|
|
226
226
|
"type": "string",
|
|
227
|
-
"description": "Set ONE value inside the atom, named by its path in the CURRENT value
|
|
227
|
+
"description": "Set ONE value inside the atom, named by its path in the CURRENT value \u2014 read it first and copy the path from `shape`. Always a merge. A list step is written [field=value] or [0] and resolves to that item's own id."
|
|
228
228
|
},
|
|
229
229
|
"force": {
|
|
230
230
|
"type": "boolean",
|
|
@@ -239,7 +239,7 @@
|
|
|
239
239
|
},
|
|
240
240
|
"effect": "write",
|
|
241
241
|
"release": "works",
|
|
242
|
-
"description": "Write to a writable Jotai atom, named by its `label`
|
|
242
|
+
"description": "Write to a writable Jotai atom, named by its `label` \u2014 this mutates the running app's state. Three forms: `value` alone REPLACES; `value` with `merge:true` merges a patch under the typed-edit rule (change existing fields to the same type; a list may grow or shrink but every item must have the fields the others have); `path`+`value` sets ONE value with no nesting to get wrong (path:\"lines[id=seed-1].qty\"). To change one item of a list, address it by its own id \u2014 {\"lines\":{\"seed-1\":{\"qty\":5}}} changes one, {\"lines\":{\"seed-2\":null}} removes one, a new id adds one; items you don't name are untouched. A plain array replaces the whole list. A refused merge names the field and, when the problem was depth, hands back the corrected patch \u2014 fix it that way rather than reaching for `force`, which writes raw and can crash the screen.",
|
|
243
243
|
"requires": [
|
|
244
244
|
"listAtoms reports writable:true for this label (atom has a write fn AND the store exposes set())"
|
|
245
245
|
]
|
|
@@ -254,20 +254,20 @@
|
|
|
254
254
|
},
|
|
255
255
|
"effect": "destructive",
|
|
256
256
|
"release": "works",
|
|
257
|
-
"description": "Empties the 200-entry change ring buffer and sets changeCount to 0 on every registered atom, then notifies listeners. Irreversible
|
|
257
|
+
"description": "Empties the 200-entry change ring buffer and sets changeCount to 0 on every registered atom, then notifies listeners. Irreversible \u2014 the discarded prev/next values are gone, so getChangeDetail on any prior id will return found:false and get_events sources:['jotai'] will show nothing until new writes land. Atom REGISTRATION and current values are untouched; only history is destroyed. Use it deliberately to get a clean baseline before reproducing a bug, not as housekeeping."
|
|
258
258
|
}
|
|
259
259
|
],
|
|
260
|
-
"unavailableWhen": "The app never calls watchAtoms(store, atoms) or watchDefaultStoreAtoms(atoms)
|
|
260
|
+
"unavailableWhen": "The app never calls watchAtoms(store, atoms) or watchDefaultStoreAtoms(atoms) \u2014 the registry is then empty and every action succeeds but returns nothing (listAtoms \u2192 total 0, getAtomValue \u2192 found:false \"unknown label\"). Also inert against a store in remote-mirror mode (jotaiStateStore.disableCapture(), used by the desktop dashboard's own copy)."
|
|
261
261
|
},
|
|
262
262
|
{
|
|
263
263
|
"toolId": "route-events",
|
|
264
264
|
"title": "Routes",
|
|
265
|
-
"summary": "Reads and drives app navigation: the snapshot carries recorded route-change events, the expo-router sitemap (paths are TEMPLATES like /pokemon/[id]), and the live navigation stack (top-most last); the actions navigate the device to a path and manipulate that stack. Reach for it to answer \"what screen am I on / what routes exist\" and to move a QA user to a screen before exercising another tool. Unlike several Buoy tools, nothing here is __DEV__-gated
|
|
265
|
+
"summary": "Reads and drives app navigation: the snapshot carries recorded route-change events, the expo-router sitemap (paths are TEMPLATES like /pokemon/[id]), and the live navigation stack (top-most last); the actions navigate the device to a path and manipulate that stack. Reach for it to answer \"what screen am I on / what routes exist\" and to move a QA user to a screen before exercising another tool. Unlike several Buoy tools, nothing here is __DEV__-gated \u2014 all six actions really run in a release build.",
|
|
266
266
|
"actions": [
|
|
267
267
|
{
|
|
268
268
|
"action": "getSnapshot",
|
|
269
269
|
"summary": "Read where the app is: the current screen, the live navigation stack, every route the app declares, and recent navigations. Answers \"what screen am I on\" and \"what routes exist\".",
|
|
270
|
-
"description": "Returns `{ currentRoute, stack, routes, sitemapSource, recentNavigations }`. `routes` are expo-router TEMPLATES like /pokemon/[id]
|
|
270
|
+
"description": "Returns `{ currentRoute, stack, routes, sitemapSource, recentNavigations }`. `routes` are expo-router TEMPLATES like /pokemon/[id] \u2014 resolve dynamic segments yourself before passing a path to `navigate`, which takes a concrete path only. `stack` and `recentNavigations` stay empty unless the app mounts <RouteTracker />; `routes` does not depend on it.",
|
|
271
271
|
"params": {
|
|
272
272
|
"type": "object",
|
|
273
273
|
"properties": {
|
|
@@ -283,7 +283,7 @@
|
|
|
283
283
|
},
|
|
284
284
|
{
|
|
285
285
|
"action": "getCurrentRoute",
|
|
286
|
-
"summary": "Just the current route
|
|
286
|
+
"summary": "Just the current route \u2014 {path, params, at, source} \u2014 without the sitemap or history. The cheap way to confirm a navigation landed.",
|
|
287
287
|
"params": {
|
|
288
288
|
"type": "object",
|
|
289
289
|
"properties": {},
|
|
@@ -292,17 +292,17 @@
|
|
|
292
292
|
},
|
|
293
293
|
"effect": "read",
|
|
294
294
|
"release": "works",
|
|
295
|
-
"description": "Returns `{path, params, at, source}` where `source` is \"event\" (the newest navigation event won) or \"stack\" (a freshly launched app that has not navigated yet
|
|
295
|
+
"description": "Returns `{path, params, at, source}` where `source` is \"event\" (the newest navigation event won) or \"stack\" (a freshly launched app that has not navigated yet \u2014 the focused stack item answered). Returns `{path:null}` when the app has no <RouteTracker/> and no live navigation stack: a null path means \"unknown\", never \"/\". Prefer this over the full getSnapshot when all you need is where the app is right now."
|
|
296
296
|
},
|
|
297
297
|
{
|
|
298
298
|
"action": "navigate",
|
|
299
|
-
"summary": "Navigate the device to a concrete path (pushes by default; replace:true swaps the current screen). ALSO HOW YOU LOAD DATA THE APP HAS NOT FETCHED YET: go to the screen that fetches it, waitFor something on it, then read. Moving the user's screen to go and look is not a change to the app
|
|
299
|
+
"summary": "Navigate the device to a concrete path (pushes by default; replace:true swaps the current screen). ALSO HOW YOU LOAD DATA THE APP HAS NOT FETCHED YET: go to the screen that fetches it, waitFor something on it, then read. Moving the user's screen to go and look is not a change to the app \u2014 you do not need to ask, and you do not have to navigate back.",
|
|
300
300
|
"params": {
|
|
301
301
|
"type": "object",
|
|
302
302
|
"properties": {
|
|
303
303
|
"path": {
|
|
304
304
|
"type": "string",
|
|
305
|
-
"description": "Concrete route path, e.g. '/settings' or '/pokemon/25'. Dynamic segments must already be resolved
|
|
305
|
+
"description": "Concrete route path, e.g. '/settings' or '/pokemon/25'. Dynamic segments must already be resolved \u2014 '/pokemon/[id]' navigates to a literal '[id]' screen or nowhere."
|
|
306
306
|
},
|
|
307
307
|
"replace": {
|
|
308
308
|
"type": "boolean",
|
|
@@ -316,7 +316,7 @@
|
|
|
316
316
|
},
|
|
317
317
|
"effect": "write",
|
|
318
318
|
"release": "works",
|
|
319
|
-
"description": "Calls expo-router's router.navigate(path), or router.replace(path) when replace:true. On bare React Navigation (no expo-router) it falls back to navigating by SCREEN NAME through the captured container ref
|
|
319
|
+
"description": "Calls expo-router's router.navigate(path), or router.replace(path) when replace:true. On bare React Navigation (no expo-router) it falls back to navigating by SCREEN NAME through the captured container ref \u2014 pass the screen's name ('Settings' or '/Settings'; replace is ignored there), and nested navigators are resolved automatically. The fallback needs <RouteTracker/> mounted inside the NavigationContainer. Pass a CONCRETE path \u2014 sitemap entries are templates ('/pokemon/[id]'), so resolve dynamic segments to real values ('/pokemon/25') first; a query string is allowed ('/pokemon/25?tab=stats'). Push is the default so navigate-then-back flows keep working; pass replace:true when you mean 'leave this screen' (e.g. resetting to '/'), otherwise repeated 'go to home' stacks another '/' on top. Throws 'navigate requires a path param' when path is missing/empty, and 'expo-router is not available on this device' on React-Navigation-only apps. Returns { navigated: path, replaced: boolean }. The return only proves the router call was made, not that the screen rendered \u2014 re-read the snapshot's stack to confirm, and use highlight-updates.waitFor before reading anything the new screen has to FETCH. This is the main tool for filling a gap in what you can read: the query cache, the request list and the events timeline only ever hold what the app has ALREADY done, so when the data you were asked about was never loaded, the answer is to go to the screen that loads it rather than to report the gap.",
|
|
320
320
|
"requires": [
|
|
321
321
|
"expo-router installed and initialized in the app",
|
|
322
322
|
"<FloatingDevTools> mounted (adapter registered)"
|
|
@@ -332,20 +332,20 @@
|
|
|
332
332
|
},
|
|
333
333
|
"effect": "write",
|
|
334
334
|
"release": "works",
|
|
335
|
-
"description": "Delegates to the live navigation actions captured by <RouteTracker />: expo-router's router.back(), or containerRef.goBack() on React Navigation. IMPORTANT: it silently does nothing when the stack is already at its root (depth <= 1) yet still returns { wentBack: true }
|
|
335
|
+
"description": "Delegates to the live navigation actions captured by <RouteTracker />: expo-router's router.back(), or containerRef.goBack() on React Navigation. IMPORTANT: it silently does nothing when the stack is already at its root (depth <= 1) yet still returns { wentBack: true } \u2014 never report 'went back' from the return value alone; re-read the snapshot's stack (or get_routes) and compare the focused pathname. Throws 'navigation stack is not available' when <RouteTracker /> is not mounted.",
|
|
336
336
|
"requires": [
|
|
337
337
|
"<RouteTracker /> mounted inside the navigation tree"
|
|
338
338
|
]
|
|
339
339
|
},
|
|
340
340
|
{
|
|
341
341
|
"action": "stackNavigateToIndex",
|
|
342
|
-
"summary": "Jump to the screen at a 0-based index of the current navigation stack
|
|
342
|
+
"summary": "Jump to the screen at a 0-based index of the current navigation stack \u2014 note this PUSHES on expo-router, it does not pop.",
|
|
343
343
|
"params": {
|
|
344
344
|
"type": "object",
|
|
345
345
|
"properties": {
|
|
346
346
|
"index": {
|
|
347
347
|
"type": "number",
|
|
348
|
-
"description": "0-based index into the synced `stack` array (top-most last; 0 = root). Must be a number
|
|
348
|
+
"description": "0-based index into the synced `stack` array (top-most last; 0 = root). Must be a number \u2014 strings throw."
|
|
349
349
|
}
|
|
350
350
|
},
|
|
351
351
|
"required": [
|
|
@@ -355,7 +355,7 @@
|
|
|
355
355
|
},
|
|
356
356
|
"effect": "write",
|
|
357
357
|
"release": "works",
|
|
358
|
-
"description": "Reads stack[index] from the live stack and navigates to its pathname. On expo-router this is router.navigate(pathname), which PUSHES that path
|
|
358
|
+
"description": "Reads stack[index] from the live stack and navigates to its pathname. On expo-router this is router.navigate(pathname), which PUSHES that path \u2014 the stack gets deeper, it does not rewind (use stackPopToIndex to actually rewind). On React Navigation it dispatches a nested CommonActions.navigate built by walking the root state. Index is 0-based into the same `stack` array the snapshot sends (top-most last), so index 0 is the root. Out-of-range indexes (index < 0 or >= stack.length) are silently ignored while the action still returns { navigatedToIndex: index } \u2014 verify by re-reading the stack. Throws 'stackNavigateToIndex requires a numeric index' for a missing/non-number index, and 'navigation stack is not available' without <RouteTracker />.",
|
|
359
359
|
"requires": [
|
|
360
360
|
"<RouteTracker /> mounted inside the navigation tree"
|
|
361
361
|
]
|
|
@@ -378,7 +378,7 @@
|
|
|
378
378
|
},
|
|
379
379
|
"effect": "destructive",
|
|
380
380
|
"release": "works",
|
|
381
|
-
"description": "Pops (stack.length - 1 - index) screens: expo-router calls router.back() that many times in a loop; React Navigation dispatches StackActions.pop(count). Index is 0-based into the synced `stack` array (top-most last). Everything above `index` is destroyed along with its in-memory screen state
|
|
381
|
+
"description": "Pops (stack.length - 1 - index) screens: expo-router calls router.back() that many times in a loop; React Navigation dispatches StackActions.pop(count). Index is 0-based into the synced `stack` array (top-most last). Everything above `index` is destroyed along with its in-memory screen state \u2014 unsaved form input on those screens is gone and cannot be restored. No-ops silently when index is already at or above the top (popCount <= 0) or out of range, while still returning { poppedToIndex: index }; confirm by re-reading the stack. Throws 'stackPopToIndex requires a numeric index' or 'navigation stack is not available'.",
|
|
382
382
|
"requires": [
|
|
383
383
|
"<RouteTracker /> mounted inside the navigation tree"
|
|
384
384
|
]
|
|
@@ -393,7 +393,7 @@
|
|
|
393
393
|
},
|
|
394
394
|
"effect": "destructive",
|
|
395
395
|
"release": "works",
|
|
396
|
-
"description": "Delegates to the captured actions: on expo-router it is popToIndex(0) (a loop of router.back() calls); on React Navigation it dispatches StackActions.popToTop(). Discards every screen above the root along with its in-memory state
|
|
396
|
+
"description": "Delegates to the captured actions: on expo-router it is popToIndex(0) (a loop of router.back() calls); on React Navigation it dispatches StackActions.popToTop(). Discards every screen above the root along with its in-memory state \u2014 call this only when the user asked to reset, not to 'tidy up' mid-flow, because an in-progress form or checkout is lost. Silently does nothing when already at root (depth <= 1) yet still returns { poppedToTop: true }; verify with a fresh stack read. Throws 'navigation stack is not available' without <RouteTracker />.",
|
|
397
397
|
"requires": [
|
|
398
398
|
"<RouteTracker /> mounted inside the navigation tree"
|
|
399
399
|
]
|
|
@@ -408,19 +408,19 @@
|
|
|
408
408
|
},
|
|
409
409
|
"effect": "destructive",
|
|
410
410
|
"release": "works",
|
|
411
|
-
"description": "Calls routeEventStore.clearEvents(), emptying the in-memory RouteChangeEvent ring buffer (pathname, params, segments, timestamp, previousPathname, timeSincePrevious) that feeds the snapshot's `events` array and get_events(sources:['route']). Irreversible
|
|
411
|
+
"description": "Calls routeEventStore.clearEvents(), emptying the in-memory RouteChangeEvent ring buffer (pathname, params, segments, timestamp, previousPathname, timeSincePrevious) that feeds the snapshot's `events` array and get_events(sources:['route']). Irreversible \u2014 the history is memory-only and not persisted anywhere. Useful as a 'start clean' marker before driving a reproduction. Does NOT touch the navigation stack, the sitemap, or the current screen. Takes no params and returns undefined.",
|
|
412
412
|
"requires": [
|
|
413
413
|
"<RouteTracker /> mounted inside the navigation tree (for events to exist at all)"
|
|
414
414
|
],
|
|
415
415
|
"armsCapture": true
|
|
416
416
|
}
|
|
417
417
|
],
|
|
418
|
-
"unavailableWhen": "The app has no `<RouteTracker />` mounted inside its navigation tree: the four `stack*` actions then throw \"navigation stack is not available\", and the synced `events`/`stack` arrays stay empty. `navigate` additionally needs expo-router
|
|
418
|
+
"unavailableWhen": "The app has no `<RouteTracker />` mounted inside its navigation tree: the four `stack*` actions then throw \"navigation stack is not available\", and the synced `events`/`stack` arrays stay empty. `navigate` additionally needs expo-router \u2014 in a bare React Navigation (RN CLI) app `getSafeRouter()` returns null and it throws \"expo-router is not available on this device\", though the `stack*` actions still work there via the React Navigation container ref."
|
|
419
419
|
},
|
|
420
420
|
{
|
|
421
421
|
"toolId": "debug-borders",
|
|
422
422
|
"title": "Debug Borders",
|
|
423
|
-
"summary": "Remote control for the on-device layout debugger: draws colored outlines (and, on Pro, tappable labels) around every native view in the running app. It is a pure remote control
|
|
423
|
+
"summary": "Remote control for the on-device layout debugger: draws colored outlines (and, on Pro, tappable labels) around every native view in the running app. It is a pure remote control \u2014 there is no dashboard mirror, so the ONLY readable state is the snapshot field `mode` (\"off\" | \"borders\" | \"labels\"). Reach for it when a QA/support user asks \"why is this misaligned / what component is this / what's the testID of that button\", not for reading data. Both actions are visual-only and in-memory: the mode resets to \"off\" on reload, and in a release bundle they flip the flag while nothing is ever drawn on screen.",
|
|
424
424
|
"actions": [
|
|
425
425
|
{
|
|
426
426
|
"action": "cycleMode",
|
|
@@ -432,8 +432,8 @@
|
|
|
432
432
|
},
|
|
433
433
|
"effect": "write",
|
|
434
434
|
"release": "noop",
|
|
435
|
-
"description": "Calls DebugBordersManager.cycle(). The cycle list is license-dependent: Pro gets [off, borders, labels], free gets [off, borders] only
|
|
436
|
-
"releaseNote": "packages/debug-borders/src/debug-borders/utils/fiberTreeTraversal.js:30
|
|
435
|
+
"description": "Calls DebugBordersManager.cycle(). The cycle list is license-dependent: Pro gets [off, borders, labels], free gets [off, borders] only \u2014 so on a free device this is a plain on/off toggle and will NEVER reach labels no matter how many times it is called. If the current mode isn't in the available list (e.g. the device was in labels and the license lapsed) it resets to \"off\" instead of advancing. Returns undefined, so the wire result is ok:true with no data \u2014 read the new mode from the tool snapshot, never assume it. Prefer setMode when you know the mode you want; use cycleMode only for a literal \"toggle it\" request. Once enabled, the overlay draws its first pass ~500ms later and re-measures every 2s, and it hides itself entirely while any Buoy modal or the dial is open, so a screenshot taken with the Buoy UI open shows no borders.",
|
|
436
|
+
"releaseNote": "packages/debug-borders/src/debug-borders/utils/fiberTreeTraversal.js:30 \u2014 the overlay enumerates views through global.__REACT_DEVTOOLS_GLOBAL_HOOK__, which React Native installs only when __DEV__ is true. In a release bundle getFiberRoots() returns [], the overlay bails on `instances.length === 0`, and zero borders are drawn even though the mode changed and the snapshot reports \"borders\". Do not tell the user borders are on screen in a release build.",
|
|
437
437
|
"requires": [
|
|
438
438
|
"@buoy-gg/debug-borders installed in the app",
|
|
439
439
|
"<FloatingDevTools> mounted non-headless (it auto-renders DebugBordersStandaloneOverlay), or DebugBordersStandaloneOverlay rendered manually at the app root",
|
|
@@ -463,8 +463,8 @@
|
|
|
463
463
|
},
|
|
464
464
|
"effect": "write",
|
|
465
465
|
"release": "noop",
|
|
466
|
-
"description": "Calls DebugBordersManager.setMode(params.mode). `mode` is REQUIRED
|
|
467
|
-
"releaseNote": "Same path as cycleMode: packages/debug-borders/src/debug-borders/utils/fiberTreeTraversal.js:30 depends on global.__REACT_DEVTOOLS_GLOBAL_HOOK__, which exists only under __DEV__, so no rectangles are ever measured in a release bundle. Additionally, in a release build FloatingDevTools returns null unless a real Pro license is present (FloatingDevTools.tsx:708) and the headless branch (FloatingDevTools.tsx:752+) never mounts the overlay at all
|
|
466
|
+
"description": "Calls DebugBordersManager.setMode(params.mode). `mode` is REQUIRED \u2014 the handler hand-casts `(params as {mode}).mode` with no optional chaining and no validation, so omitting params entirely throws (ok:false, \"Cannot read property 'mode' of undefined\"), while an object with a missing or misspelled mode is written verbatim into global state: the snapshot then reports that junk value and, because the overlay only checks `mode !== \"off\"`, borders keep drawing in an unnamed mode. Always pass one of the three literals. \"borders\" outlines every native view, colored by tree depth. \"labels\" (Pro) outlines only views that have a testID or accessibilityLabel and puts a tappable colored chip above each one; tapping a chip opens a device-side sheet with testID / nativeID / component name / x,y,w,h / accessibility props / styles. Those chips sit at zIndex 9000 and DO swallow taps aimed at the app underneath, so set the mode back to \"off\" before driving the UI with taps. Pro gate: setMode(\"labels\") without a license logs \"[DebugBorders] Labels mode requires React Buoy Pro\" and returns false, but the adapter discards that boolean \u2014 the action still resolves ok:true with the mode unchanged. Confirm the result in the snapshot before reporting success. Mode is a module-level variable, not persisted: a reload or app restart returns it to \"off\".",
|
|
467
|
+
"releaseNote": "Same path as cycleMode: packages/debug-borders/src/debug-borders/utils/fiberTreeTraversal.js:30 depends on global.__REACT_DEVTOOLS_GLOBAL_HOOK__, which exists only under __DEV__, so no rectangles are ever measured in a release bundle. Additionally, in a release build FloatingDevTools returns null unless a real Pro license is present (FloatingDevTools.tsx:708) and the headless branch (FloatingDevTools.tsx:752+) never mounts the overlay at all \u2014 three independent reasons nothing appears, while the action still reports ok:true.",
|
|
468
468
|
"requires": [
|
|
469
469
|
"@buoy-gg/debug-borders installed in the app",
|
|
470
470
|
"<FloatingDevTools> mounted non-headless (it auto-renders DebugBordersStandaloneOverlay), or DebugBordersStandaloneOverlay rendered manually at the app root",
|
|
@@ -477,7 +477,7 @@
|
|
|
477
477
|
{
|
|
478
478
|
"toolId": "zustand",
|
|
479
479
|
"title": "Zustand",
|
|
480
|
-
"summary": "Reads and writes the app's live Zustand stores: list registered stores with their top-level keys or full state, fetch one store's current state, fetch the real before/after trees for one recorded state change, and setState a store for time-travel/reset. Reach for it when on-screen data disagrees with the API, or when a QA user needs the app put into a specific state. The change TIMELINE (history over time) is better read via get_events sources:['zustand']; this tool is for CURRENT state plus on-demand detail. Everything here works in release builds
|
|
480
|
+
"summary": "Reads and writes the app's live Zustand stores: list registered stores with their top-level keys or full state, fetch one store's current state, fetch the real before/after trees for one recorded state change, and setState a store for time-travel/reset. Reach for it when on-screen data disagrees with the API, or when a QA user needs the app put into a specific state. The change TIMELINE (history over time) is better read via get_events sources:['zustand']; this tool is for CURRENT state plus on-demand detail. Everything here works in release builds \u2014 but setState defaults to replace:true, which wipes the store's action functions.",
|
|
481
481
|
"actions": [
|
|
482
482
|
{
|
|
483
483
|
"action": "listStores",
|
|
@@ -487,7 +487,7 @@
|
|
|
487
487
|
"properties": {
|
|
488
488
|
"includeValues": {
|
|
489
489
|
"type": "boolean",
|
|
490
|
-
"description": "Include each store's full currentState object (HEAVY
|
|
490
|
+
"description": "Include each store's full currentState object (HEAVY \u2014 whole state trees). Default false, which returns only top-level `keys` so the shape is visible cheaply."
|
|
491
491
|
},
|
|
492
492
|
"limit": {
|
|
493
493
|
"type": "number",
|
|
@@ -498,15 +498,15 @@
|
|
|
498
498
|
},
|
|
499
499
|
"effect": "read",
|
|
500
500
|
"release": "works",
|
|
501
|
-
"description": "The entry point
|
|
501
|
+
"description": "The entry point \u2014 call this first to learn valid storeName values for getStoreState/setState. Returns {stores:[{name, changes, isPersisted, persistName?, keys?|currentState?}], total, returned, includedValues}. Compact by default: `keys` is Object.keys() of the state (undefined when the state isn't a plain object). includeValues:true swaps `keys` for the full `currentState` object and can be very large \u2014 the state objects here are NOT wire-budget-capped the way the streaming snapshot is. `changes` is that store's recorded state-change count, reset to 0 by clearEvents. Names come from the app: the object keys passed to watchStores({counterStore: useCounterStore}) or the `name` option of buoyDevTools(). An empty stores array means the app never instrumented its stores, not that it has none. Every read here also returns `shape`: a one-line sketch of the value's type plus, for each list in it, what its items look like. It is tiny and does not grow with the data, so it survives the 24,000-character cut when the data itself does not \u2014 read it before writing, and match it exactly. A list whose `item` is missing is EMPTY, which means nothing in the running app knows what belongs in it; anything you add there cannot be checked and will be accepted as-is, so say so rather than inventing fields.",
|
|
502
502
|
"requires": [
|
|
503
503
|
"@buoy-gg/zustand installed and the zustand tool registered with FloatingDevTools",
|
|
504
|
-
"the app calls watchStores({...}) or wraps stores with buoyDevTools()
|
|
504
|
+
"the app calls watchStores({...}) or wraps stores with buoyDevTools() \u2014 otherwise the registry is empty"
|
|
505
505
|
]
|
|
506
506
|
},
|
|
507
507
|
{
|
|
508
508
|
"action": "getStoreState",
|
|
509
|
-
"summary": "Fetch one store's full current state object on demand, by store name. Send `path` to get back ONE value instead of the whole store (path:\"lines[lineId=seed-1].qty\")
|
|
509
|
+
"summary": "Fetch one store's full current state object on demand, by store name. Send `path` to get back ONE value instead of the whole store (path:\"lines[lineId=seed-1].qty\") \u2014 do that whenever the store is big, because results are cut off at 24,000 characters.",
|
|
510
510
|
"params": {
|
|
511
511
|
"type": "object",
|
|
512
512
|
"properties": {
|
|
@@ -516,7 +516,7 @@
|
|
|
516
516
|
},
|
|
517
517
|
"path": {
|
|
518
518
|
"type": "string",
|
|
519
|
-
"description": "Return only the value at this path instead of the whole payload. Use it when you only need one field, and ALWAYS when the payload is big: results are cut off at 24,000 characters, and a field you never saw is a field you will guess the shape of. The path must match the CURRENT data
|
|
519
|
+
"description": "Return only the value at this path instead of the whole payload. Use it when you only need one field, and ALWAYS when the payload is big: results are cut off at 24,000 characters, and a field you never saw is a field you will guess the shape of. The path must match the CURRENT data \u2014 read `shape` first rather than assuming a wrapper. On a bad path it answers with the field names that do exist, and with the path that would have worked if that field lives somewhere else."
|
|
520
520
|
}
|
|
521
521
|
},
|
|
522
522
|
"required": [
|
|
@@ -526,9 +526,9 @@
|
|
|
526
526
|
},
|
|
527
527
|
"effect": "read",
|
|
528
528
|
"release": "works",
|
|
529
|
-
"description": "Use when listStores' compact `keys` view isn't enough, or when the streaming snapshot showed the {__buoyStateOnDevice:true} marker (states over 16KB are withheld from the per-snapshot wire). Returns {found:true, storeName, currentState}
|
|
529
|
+
"description": "Use when listStores' compact `keys` view isn't enough, or when the streaming snapshot showed the {__buoyStateOnDevice:true} marker (states over 16KB are withheld from the per-snapshot wire). Returns {found:true, storeName, currentState} \u2014 or {found:false, reason:\"missing storeName\"} / {found:false, reason:\"unknown storeName\"}, which is a plain answer, not an error. currentState is read live via store.api.getState(); if that throws it comes back undefined. Over the 8MB detail cap it returns {__buoyTruncated:true, note} instead, or the STATE_ON_DEVICE marker when the state isn't JSON-serializable. Note that action functions living in state (increment, reset, \u2026) do not survive the JSON wire \u2014 what you read back is the data half of the store only. Every read here also returns `shape`: a one-line sketch of the value's type plus, for each list in it, what its items look like. It is tiny and does not grow with the data, so it survives the 24,000-character cut when the data itself does not \u2014 read it before writing, and match it exactly. A list whose `item` is missing is EMPTY, which means nothing in the running app knows what belongs in it; anything you add there cannot be checked and will be accepted as-is, so say so rather than inventing fields. Send `path` to get back ONE value instead of the whole store (path:\"lines[lineId=seed-1].qty\") \u2014 do that whenever the store is big, because results are cut off at 24,000 characters.",
|
|
530
530
|
"requires": [
|
|
531
|
-
"the store must already be registered
|
|
531
|
+
"the store must already be registered \u2014 get the exact name from listStores"
|
|
532
532
|
]
|
|
533
533
|
},
|
|
534
534
|
{
|
|
@@ -549,14 +549,14 @@
|
|
|
549
549
|
},
|
|
550
550
|
"effect": "read",
|
|
551
551
|
"release": "works",
|
|
552
|
-
"description": "The streaming change log deliberately carries no state trees
|
|
552
|
+
"description": "The streaming change log deliberately carries no state trees \u2014 every change's prevState/nextState arrive as the {__buoyStateOnDevice:true} marker, and an oversized `partial` as {__buoyPayloadOnDevice:true}. This is the explicit channel that fetches the real trees for one change so you can diff before/after. `id` comes from a change row (format `<epochMs>-<counter>`, e.g. \"1761580000123-42\"). Returns {found:true, id, prevState, nextState, partial} or {found:false, reason:\"missing id\"|\"unknown id\"}. Each value over 8MB is replaced by {__buoyTruncated:true, note}. Only the newest 200 changes are retained (ring buffer), and clearEvents empties it \u2014 an id that scrolled off returns \"unknown id\". `partial` is undefined for stores instrumented with watchStores (subscribe-only mode can't see the setState argument); only the buoyDevTools middleware records partial and duration.",
|
|
553
553
|
"requires": [
|
|
554
554
|
"a change id from the zustand change log (the tool snapshot, or get_events sources:['zustand'])"
|
|
555
555
|
]
|
|
556
556
|
},
|
|
557
557
|
{
|
|
558
558
|
"action": "setState",
|
|
559
|
-
"summary": "Change a live zustand store. Ask Buoy MERGES by default (replace:false): it merges the fields you send, including inside nested objects (so {\"member\":{\"crowns\":5}} changes crowns and keeps member's other fields), and KEEPS the store's action functions (setQty, removeLine,
|
|
559
|
+
"summary": "Change a live zustand store. Ask Buoy MERGES by default (replace:false): it merges the fields you send, including inside nested objects (so {\"member\":{\"crowns\":5}} changes crowns and keeps member's other fields), and KEEPS the store's action functions (setQty, removeLine, \u2026) and untouched keys. Never send replace:true unless you mean to reset the whole store \u2014 replacing drops the functions (they can't cross the wire), and the app's buttons that call them then crash. To change a list in the store, ADDRESS ITEMS BY THEIR OWN id instead of resending the list: {\"lines\":{\"seed-1\":{\"qty\":5}}} changes one field of one item, {\"lines\":{\"seed-2\":null}} removes that item, and a key that isn't in the list yet appends a new item (send all its fields). Items you don't name are untouched, so this is the only safe form when you haven't seen the whole list \u2014 a capped read means you CANNOT resend it without deleting what you weren't shown. Sending a plain ARRAY still works and still replaces the whole list, which is how you deliberately empty it. On a merge it's a TYPED EDIT: you can only change a field that already exists, to the same type, and any list item you add must match the shape of the ones already there \u2014 a wrong type, an unknown field, or a malformed new item is refused with the exact path. force:true bypasses. To change a single value, `path` + `value` is the safest form (path:\"lines[lineId=seed-1].qty\", value:5): it is always a merge and there is no nesting to get wrong.",
|
|
560
560
|
"params": {
|
|
561
561
|
"type": "object",
|
|
562
562
|
"properties": {
|
|
@@ -566,21 +566,21 @@
|
|
|
566
566
|
},
|
|
567
567
|
"state": {
|
|
568
568
|
"type": "object",
|
|
569
|
-
"description": "The state to write. With replace true (the default) this becomes the store's ENTIRE state and every absent key
|
|
569
|
+
"description": "The state to write. With replace true (the default) this becomes the store's ENTIRE state and every absent key \u2014 including action functions \u2014 is removed. With replace false it is merged as a partial, so pass only the fields you intend to change (e.g. {\"count\": 5}).",
|
|
570
570
|
"additionalProperties": true
|
|
571
571
|
},
|
|
572
572
|
"replace": {
|
|
573
573
|
"type": "boolean",
|
|
574
|
-
"description": "true replaces the whole state
|
|
574
|
+
"description": "true replaces the whole state \u2014 including the store's action functions, which breaks every button wired to them until the app reloads. Buoy sends false for you unless you explicitly pass true, so a partial merge is the safe path: pass only the fields you intend to change (e.g. {\"count\": 5}). The ADAPTER's own default is true; this is a deliberately safer default for agent calls.",
|
|
575
575
|
"default": false
|
|
576
576
|
},
|
|
577
577
|
"force": {
|
|
578
578
|
"type": "boolean",
|
|
579
|
-
"description": "Bypass the typed-edit safety on a merge and write raw. Default false
|
|
579
|
+
"description": "Bypass the typed-edit safety on a merge and write raw. Default false \u2014 a violating merge is refused."
|
|
580
580
|
},
|
|
581
581
|
"path": {
|
|
582
582
|
"type": "string",
|
|
583
|
-
"description": "Set ONE value, named by its full path in the CURRENT data: path:\"<the real path>\", value:<new value>. READ THE DATA FIRST AND COPY THE PATH FROM IT
|
|
583
|
+
"description": "Set ONE value, named by its full path in the CURRENT data: path:\"<the real path>\", value:<new value>. READ THE DATA FIRST AND COPY THE PATH FROM IT \u2014 `shape` on getStoreState/listStores prints it. A path is only safer than a hand-nested `data` if it matches the payload you are actually looking at; if the field is at the top level the path is just \"name\", and inventing a wrapper that is not there is refused. A list step is written [field=value] or [0] and resolves to that item's own id, so it still means the same row if the list changed. Always a merge, and always through the same typed-edit guard as `data`. Requires `value`; send `data` OR `path`+`value`, not both."
|
|
584
584
|
},
|
|
585
585
|
"value": {
|
|
586
586
|
"description": "The value `path` is set to. Required whenever `path` is sent. null is a real value (only allowed if the field is already nullable), not a delete."
|
|
@@ -593,16 +593,16 @@
|
|
|
593
593
|
},
|
|
594
594
|
"effect": "destructive",
|
|
595
595
|
"release": "works",
|
|
596
|
-
"description": "Powers time-travel / reset / 'put the app in this state' from a dashboard. Calls the store's real setState(state, replace ?? true). THE DEFAULT IS DESTRUCTIVE: `replace` defaults to TRUE, so any key missing from `state` is deleted
|
|
596
|
+
"description": "Powers time-travel / reset / 'put the app in this state' from a dashboard. Calls the store's real setState(state, replace ?? true). THE DEFAULT IS DESTRUCTIVE: `replace` defaults to TRUE, so any key missing from `state` is deleted \u2014 and Zustand stores conventionally keep their action functions in state (increment, login, addToCart). Functions cannot cross the sync wire, so a JSON `state` object can never carry them back; a default-replace leaves every `useStore(s => s.increment)` call site reading undefined and the app broken until reload. Buoy's own Time Machine restore avoids this by re-grafting the live store's functions onto the snapshot before replacing (packages/zustand/src/zustand/utils/snapshotProvider.ts:8-12) \u2014 this raw action does NOT do that. For a QA-facing tweak always pass replace:false to merge just the fields you're changing. Returns {ok:true}, or {ok:false, error:\"Missing storeName.\"} / {ok:false, error:'No store named \"X\".'}. The write also lands in the change log as a normal recorded change. A write that adds the first item to an EMPTY list comes back ok with `unchecked`: the list had no items, so nothing knew its item shape and yours was not verified. Treat that as a warning to check the screen, not as a pass. To change a single value, `path` + `value` is the safest form (path:\"lines[lineId=seed-1].qty\", value:5): it is always a merge and there is no nesting to get wrong.",
|
|
597
597
|
"requires": [
|
|
598
|
-
"the store must be registered
|
|
598
|
+
"the store must be registered \u2014 get the exact name from listStores",
|
|
599
599
|
"the store's registered setState handle: watchStores registers the raw setState, buoyDevTools registers its instrumented set (both apply the write)"
|
|
600
600
|
]
|
|
601
601
|
},
|
|
602
602
|
{
|
|
603
603
|
"action": "rehydrate",
|
|
604
604
|
"summary": "Re-read a persisted store's saved value from storage and merge it into the live store. Use it after anything wrote that storage key directly.",
|
|
605
|
-
"description": "Returns {ok, storeName, persistName}. THIS IS HOW A STORAGE EDIT TAKES EFFECT. A persisted store reads its key once at startup and then holds the state in memory, so writing the key with storage.async.setItem / mmkv.set changes the disk and nothing else
|
|
605
|
+
"description": "Returns {ok, storeName, persistName}. THIS IS HOW A STORAGE EDIT TAKES EFFECT. A persisted store reads its key once at startup and then holds the state in memory, so writing the key with storage.async.setItem / mmkv.set changes the disk and nothing else \u2014 the screen does not move, and the next time the store saves it writes its own copy back over the edit. Rehydrating closes that loop. It merges the saved value OVER current state, so the store's action functions survive, and it does not rewrite storage unless a version migration ran. Two limits, both inherent to how persist works: a field the store's `partialize` excludes is not in the saved value and cannot be applied, and because the merge is shallow, DELETING a key from the saved JSON does not remove it from the live store. Prefer setState for an ordinary change; this is for when the storage key is what changed.",
|
|
606
606
|
"params": {
|
|
607
607
|
"type": "object",
|
|
608
608
|
"properties": {
|
|
@@ -632,35 +632,35 @@
|
|
|
632
632
|
},
|
|
633
633
|
"effect": "destructive",
|
|
634
634
|
"release": "works",
|
|
635
|
-
"description": "Empties the in-memory change log for ALL stores (not one store) and sets each registered store's stateChangeCount to 0, then notifies listeners. Irreversible
|
|
635
|
+
"description": "Empties the in-memory change log for ALL stores (not one store) and sets each registered store's stateChangeCount to 0, then notifies listeners. Irreversible \u2014 the discarded changes are not persisted anywhere, and any change id you were holding becomes \"unknown id\" for getChangeDetail. Does NOT touch app state: the stores keep their current values, only the history is destroyed. Useful to get a clean baseline before reproducing a bug. Returns nothing (undefined) on success. Capture continues afterwards without needing a re-subscribe."
|
|
636
636
|
}
|
|
637
637
|
],
|
|
638
|
-
"unavailableWhen": "The app doesn't depend on @buoy-gg/zustand or the zustand tool isn't registered with FloatingDevTools
|
|
638
|
+
"unavailableWhen": "The app doesn't depend on @buoy-gg/zustand or the zustand tool isn't registered with FloatingDevTools \u2014 the adapter is never mapped in (packages/devtools-floating-menu/src/floatingMenu/autoExternalSync.tsx:335) and the tool id \"zustand\" is absent from the device's action inventory. Installed but never instrumented (no watchStores() / buoyDevTools() call) is different: every action still answers, but listStores returns zero stores and the change log is empty. On a desktop mirror the store runs with capture suppressed (zustandStateStore.disableCapture()), so it only reflects what the device sent."
|
|
639
639
|
},
|
|
640
640
|
{
|
|
641
641
|
"toolId": "redux",
|
|
642
642
|
"title": "Redux",
|
|
643
|
-
"summary": "Reads and drives the app's LIVE Redux store, plus the captured action log. Use getState for what is in the store right now (Redux is action-based, so current state is NOT in the action-log snapshot
|
|
643
|
+
"summary": "Reads and drives the app's LIVE Redux store, plus the captured action log. Use getState for what is in the store right now (Redux is action-based, so current state is NOT in the action-log snapshot \u2014 that history comes from get_events sources:['redux']), dispatch to push a plain action into the running app, getActionDetail to pull one action's real prevState/nextState trees (snapshots ship markers, not trees), and clearEvents to wipe the recorded log. All four need a Redux store bound to Buoy; when none is, getState/dispatch answer available:false with a specific reason instead of failing generically.",
|
|
644
644
|
"actions": [
|
|
645
645
|
{
|
|
646
646
|
"action": "getState",
|
|
647
|
-
"summary": "Read the app's CURRENT Redux state
|
|
647
|
+
"summary": "Read the app's CURRENT Redux state \u2014 top-level slice names by default, full state tree with includeValues:true.",
|
|
648
648
|
"params": {
|
|
649
649
|
"type": "object",
|
|
650
650
|
"properties": {
|
|
651
651
|
"includeValues": {
|
|
652
652
|
"type": "boolean",
|
|
653
|
-
"description": "Include the full current state tree (HEAVY
|
|
653
|
+
"description": "Include the full current state tree (HEAVY \u2014 the whole store is serialized over the wire). Default false: slice names only."
|
|
654
654
|
}
|
|
655
655
|
},
|
|
656
656
|
"additionalProperties": false
|
|
657
657
|
},
|
|
658
658
|
"effect": "read",
|
|
659
659
|
"release": "works",
|
|
660
|
-
"description": "Compact by DEFAULT and token-cheap: returns {available:true, slices:string[], capture:'full'|'top-level-only', mechanism:'enhancer'|'middleware'|'patch'}. Pass includeValues:true to add `state`
|
|
660
|
+
"description": "Compact by DEFAULT and token-cheap: returns {available:true, slices:string[], capture:'full'|'top-level-only', mechanism:'enhancer'|'middleware'|'patch'}. Pass includeValues:true to add `state` \u2014 the entire store tree, which can be megabytes on a real app. Use for 'what's in my redux store / current auth state'. When no store is bound it returns {available:false, slices:[], reason:'no-react-redux'|'no-provider'|'not-instrumented'} \u2014 report that specific reason: no-react-redux means the app lacks react-redux, no-provider means <FloatingDevTools /> is not inside <Provider store={store}>, not-instrumented usually means an older @buoy-gg/core or the app should call registerReduxStore(store). If `capture` is 'top-level-only', warn the user that thunk-internal and RTK Query actions are NOT in the action log (the fix is `import '@buoy-gg/redux';` first in the app entry, or adding buoyReduxMiddleware) \u2014 this does not affect the state values you just read, which are always live and correct.",
|
|
661
661
|
"requires": [
|
|
662
662
|
"react-redux installed in the app",
|
|
663
|
-
"a Redux <Provider> above <FloatingDevTools />
|
|
663
|
+
"a Redux <Provider> above <FloatingDevTools /> \u2014 or an explicit registerReduxStore(store) from @buoy-gg/redux"
|
|
664
664
|
]
|
|
665
665
|
},
|
|
666
666
|
{
|
|
@@ -681,14 +681,14 @@
|
|
|
681
681
|
},
|
|
682
682
|
"effect": "read",
|
|
683
683
|
"release": "works",
|
|
684
|
-
"description": "The per-snapshot action stream deliberately carries markers ({__buoyStateOnDevice:true}, {__buoyPayloadOnDevice:true}) instead of state trees
|
|
684
|
+
"description": "The per-snapshot action stream deliberately carries markers ({__buoyStateOnDevice:true}, {__buoyPayloadOnDevice:true}) instead of state trees \u2014 shipping them froze and OOM-killed large apps \u2014 so this is the ONLY way to see an action's before/after state. `id` comes from a redux action row and is formatted \"<epochMillis>-<counter>\" (e.g. \"1724612345678-42\"). Returns {found:true, id, prevState, nextState, payload, action, meta, error}; any single field over 8MB is replaced by {__buoyTruncated:true, note} rather than failing the whole call. Two negative shapes to relay verbatim: {found:false, reason:'unknown id'|'missing id'} (id not in the current log, or omitted), and {found:true, evicted:true, reason:...} \u2014 raw trees are retained for the 25 MOST RECENT actions only, so an older action keeps its diff metadata but its trees are gone forever. Do not retry an evicted action; reproduce the behavior again and read the fresh entry.",
|
|
685
685
|
"requires": [
|
|
686
686
|
"an instrumented store that has already recorded the action (log holds the 200 most recent actions; raw state trees only the 25 most recent)"
|
|
687
687
|
]
|
|
688
688
|
},
|
|
689
689
|
{
|
|
690
690
|
"action": "dispatch",
|
|
691
|
-
"summary": "Dispatch a plain action object into the app's live Redux store
|
|
691
|
+
"summary": "Dispatch a plain action object into the app's live Redux store \u2014 really changes app state.",
|
|
692
692
|
"params": {
|
|
693
693
|
"type": "object",
|
|
694
694
|
"properties": {
|
|
@@ -716,10 +716,10 @@
|
|
|
716
716
|
},
|
|
717
717
|
"effect": "destructive",
|
|
718
718
|
"release": "works",
|
|
719
|
-
"description": "Sends the given plain action straight to store.dispatch, e.g. {\"action\":{\"type\":\"counter/increment\",\"payload\":1}} or {\"action\":{\"type\":\"auth/logout\"}}. (The MCP redux_dispatch tool takes flat {type, payload} and wraps it into this shape for you.) `action` is REQUIRED and must be a plain object with a string `type`
|
|
719
|
+
"description": "Sends the given plain action straight to store.dispatch, e.g. {\"action\":{\"type\":\"counter/increment\",\"payload\":1}} or {\"action\":{\"type\":\"auth/logout\"}}. (The MCP redux_dispatch tool takes flat {type, payload} and wraps it into this shape for you.) `action` is REQUIRED and must be a plain object with a string `type` \u2014 Redux itself throws on a missing/undefined type, and thunk functions cannot be sent over the wire. Returns {dispatched:true, type} on success, or {dispatched:false, available:false, reason:'no-react-redux'|'no-provider'|'not-instrumented'} when no store is bound \u2014 never claim a dispatch landed unless dispatched is true. Treat as destructive: this mutates the real app the user is looking at, and a type like auth/logout, cart/clear or a rehydrate action is irreversible from here \u2014 the adapter exposes no time travel (jumpToState is NOT a sync action). Confirm the exact action type with the user before dispatching anything that resets, clears, or logs out.",
|
|
720
720
|
"requires": [
|
|
721
721
|
"react-redux installed in the app",
|
|
722
|
-
"a Redux <Provider> above <FloatingDevTools />
|
|
722
|
+
"a Redux <Provider> above <FloatingDevTools /> \u2014 or an explicit registerReduxStore(store)",
|
|
723
723
|
"the app must actually handle the action type; an unknown type dispatches successfully and changes nothing"
|
|
724
724
|
]
|
|
725
725
|
},
|
|
@@ -733,15 +733,15 @@
|
|
|
733
733
|
},
|
|
734
734
|
"effect": "destructive",
|
|
735
735
|
"release": "works",
|
|
736
|
-
"description": "Empties reduxActionStore
|
|
736
|
+
"description": "Empties reduxActionStore \u2014 every captured action, its payload and its retained prevState/nextState trees are gone, and any pending getActionDetail id becomes 'unknown id'. Does NOT touch the app's actual Redux state: the store keeps whatever it currently holds, only the recording is cleared. Returns nothing (undefined) \u2014 success is the absence of an error. Useful to get a clean baseline right before reproducing a bug; never call it before you have read anything the user might still need, since there is no export or restore."
|
|
737
737
|
}
|
|
738
738
|
],
|
|
739
|
-
"unavailableWhen": "No Redux store is bound to Buoy
|
|
739
|
+
"unavailableWhen": "No Redux store is bound to Buoy \u2014 react-redux not installed (\"no-react-redux\"), no <Provider> above <FloatingDevTools /> (\"no-provider\"), or nothing instrumented the store yet (\"not-instrumented\"). getState/dispatch then return available:false with that reason and dispatched:false; the log actions still respond but the log stays empty. Separately, in a release build (__DEV__ === false) the whole sync channel only exists if the app opted in with externalSync.enableInRelease AND holds a real Pro license (packages/devtools-floating-menu/src/floatingMenu/externalSyncGate.ts:49-60) \u2014 otherwise no action on this tool is reachable at all."
|
|
740
740
|
},
|
|
741
741
|
{
|
|
742
742
|
"toolId": "impersonate",
|
|
743
743
|
"title": "Impersonate",
|
|
744
|
-
"summary": "Become another user inside the running app without logging out: it injects an impersonation header (default `x-impersonate-user-id: <user.id>`) into every outgoing globalThis.fetch and XMLHttpRequest, so the backend returns that user's data. Reach for it to reproduce a specific customer's bug (\"show me what account 8812 sees\"), then stop/pause to return to the real login. Actions only mutate state
|
|
744
|
+
"summary": "Become another user inside the running app without logging out: it injects an impersonation header (default `x-impersonate-user-id: <user.id>`) into every outgoing globalThis.fetch and XMLHttpRequest, so the backend returns that user's data. Reach for it to reproduce a specific customer's bug (\"show me what account 8812 sees\"), then stop/pause to return to the real login. Actions only mutate state \u2014 they all resolve to void, so read the tool snapshot (isActive / isPaused / currentUser / history) to confirm anything took effect. Note that switching or stopping also CLEARS app caches per dataNukeSettings (react-query + redux on by default), so it is not a passive read-only view.",
|
|
745
745
|
"actions": [
|
|
746
746
|
{
|
|
747
747
|
"action": "searchUsers",
|
|
@@ -751,7 +751,7 @@
|
|
|
751
751
|
"properties": {
|
|
752
752
|
"query": {
|
|
753
753
|
"type": "string",
|
|
754
|
-
"description": "Search text handed verbatim to the app's onSearchUsers
|
|
754
|
+
"description": "Search text handed verbatim to the app's onSearchUsers \u2014 usually an email, name, or user id. Coerced via String(); omitting it sends an empty string, which most apps treat as 'list everything'."
|
|
755
755
|
}
|
|
756
756
|
},
|
|
757
757
|
"required": [],
|
|
@@ -759,20 +759,20 @@
|
|
|
759
759
|
},
|
|
760
760
|
"effect": "read",
|
|
761
761
|
"release": "works",
|
|
762
|
-
"description": "Proxies straight to the host app's onSearchUsers(query) callback
|
|
762
|
+
"description": "Proxies straight to the host app's onSearchUsers(query) callback \u2014 a real request to the company's own admin/user API, so results and latency are entirely the app's. Returns an array of User objects: { id, displayName?, email?, avatarUrl?, metadata? }. Wire-shrinking is applied before it reaches you: any avatarUrl that is a data: URI or longer than 2048 chars is replaced by a stub string like \"data:image/png;base64,[3145728 chars]\", and a metadata object over 16KB is replaced by { role, __buoyOmitted: \"user-metadata\" } \u2014 the device keeps the real values. ALWAYS call this before startImpersonation instead of hand-constructing a user; the app's real user id is what the backend checks. Throws \"No onSearchUsers configured\" if the app never passed onSearchUsers to createImpersonateTool().",
|
|
763
763
|
"requires": [
|
|
764
764
|
"createImpersonateTool({ onSearchUsers }) called by the host app"
|
|
765
765
|
]
|
|
766
766
|
},
|
|
767
767
|
{
|
|
768
768
|
"action": "startImpersonation",
|
|
769
|
-
"summary": "Begin impersonating a user
|
|
769
|
+
"summary": "Begin impersonating a user \u2014 every subsequent fetch/XHR carries the impersonation header, and app caches are wiped per dataNukeSettings.",
|
|
770
770
|
"params": {
|
|
771
771
|
"type": "object",
|
|
772
772
|
"properties": {
|
|
773
773
|
"user": {
|
|
774
774
|
"type": "object",
|
|
775
|
-
"description": "The full User object to impersonate. Pass one returned by searchUsers; a bare object with only an id also works. Required
|
|
775
|
+
"description": "The full User object to impersonate. Pass one returned by searchUsers; a bare object with only an id also works. Required \u2014 a missing user throws a TypeError on the device.",
|
|
776
776
|
"properties": {
|
|
777
777
|
"id": {
|
|
778
778
|
"type": "string",
|
|
@@ -805,7 +805,7 @@
|
|
|
805
805
|
},
|
|
806
806
|
"effect": "destructive",
|
|
807
807
|
"release": "works",
|
|
808
|
-
"description": "Sets isActive=true and currentUser=user, points the fetch/XHR interceptor at user.id, prepends the user to history (deduped by id, capped at 10, persisted to @buoy/impersonate/state), THEN runs the data nuke and persists. The nuke clears react-query and resets redux by default (dataNukeSettings.reactQuery/redux default true) and can also wipe AsyncStorage and MMKV when those settings were turned on
|
|
808
|
+
"description": "Sets isActive=true and currentUser=user, points the fetch/XHR interceptor at user.id, prepends the user to history (deduped by id, capped at 10, persisted to @buoy/impersonate/state), THEN runs the data nuke and persists. The nuke clears react-query and resets redux by default (dataNukeSettings.reactQuery/redux default true) and can also wipe AsyncStorage and MMKV when those settings were turned on \u2014 that part is irreversible. Calling this while already impersonating switches users (that is the 'quick switch' path). Two ways it can look successful but change nothing on screen: (1) the nuke callbacks are only registered once the Impersonate panel has been opened at least once in this app session, so caches may keep the previous user's data and the UI won't refresh \u2014 tell the user to open the Impersonate tool once, or reload the app; (2) the header is injected only into globalThis.fetch and XMLHttpRequest.prototype, so a client that bypasses both (e.g. Expo's native expo/fetch) sends no header. Metro/dev URLs (localhost:8081, /symbolicate, /logs, .hot-update., __metro) are always excluded. Resolves to void \u2014 read the snapshot's isActive/currentUser to confirm."
|
|
809
809
|
},
|
|
810
810
|
{
|
|
811
811
|
"action": "stopImpersonation",
|
|
@@ -817,7 +817,7 @@
|
|
|
817
817
|
},
|
|
818
818
|
"effect": "destructive",
|
|
819
819
|
"release": "works",
|
|
820
|
-
"description": "Clears isActive, isPaused and currentUser, stops header injection, then runs the same data nuke as startImpersonation (react-query + redux by default; AsyncStorage/MMKV if enabled) and persists. Use this to return the device to its real identity
|
|
820
|
+
"description": "Clears isActive, isPaused and currentUser, stops header injection, then runs the same data nuke as startImpersonation (react-query + redux by default; AsyncStorage/MMKV if enabled) and persists. Use this to return the device to its real identity \u2014 it is the correct 'undo' after any impersonation session. History is untouched. Safe to call when not impersonating (state is already clear), but note the nuke still fires. Resolves to void."
|
|
821
821
|
},
|
|
822
822
|
{
|
|
823
823
|
"action": "pauseImpersonation",
|
|
@@ -829,7 +829,7 @@
|
|
|
829
829
|
},
|
|
830
830
|
"effect": "write",
|
|
831
831
|
"release": "works",
|
|
832
|
-
"description": "Sets isPaused=true and passes a null userId to the interceptor, so requests go out as the real account again while currentUser is remembered. No cache nuke runs, which is exactly why it is the safer A/B toggle: pause, check the screen as yourself, resume. IMPORTANT
|
|
832
|
+
"description": "Sets isPaused=true and passes a null userId to the interceptor, so requests go out as the real account again while currentUser is remembered. No cache nuke runs, which is exactly why it is the safer A/B toggle: pause, check the screen as yourself, resume. IMPORTANT \u2014 it silently returns and does nothing if isActive is false or isPaused is already true, and it still resolves successfully, so verify isPaused in the snapshot rather than assuming. Because no cache is cleared, already-fetched data on screen will not change until something refetches."
|
|
833
833
|
},
|
|
834
834
|
{
|
|
835
835
|
"action": "resumeImpersonation",
|
|
@@ -841,7 +841,7 @@
|
|
|
841
841
|
},
|
|
842
842
|
"effect": "destructive",
|
|
843
843
|
"release": "works",
|
|
844
|
-
"description": "Sets isPaused=false and re-points the interceptor at currentUser.id. No cache nuke runs. Silently does nothing (while still reporting success) when isActive is false or isPaused is already false
|
|
844
|
+
"description": "Sets isPaused=false and re-points the interceptor at currentUser.id. No cache nuke runs. Silently does nothing (while still reporting success) when isActive is false or isPaused is already false \u2014 check isPaused in the snapshot to confirm. Stale on-screen data from the paused window persists until a refetch."
|
|
845
845
|
},
|
|
846
846
|
{
|
|
847
847
|
"action": "updateSettings",
|
|
@@ -851,7 +851,7 @@
|
|
|
851
851
|
"properties": {
|
|
852
852
|
"settings": {
|
|
853
853
|
"type": "object",
|
|
854
|
-
"description": "Required wrapper. Partial patch
|
|
854
|
+
"description": "Required wrapper. Partial patch \u2014 omitted keys keep their current value.",
|
|
855
855
|
"properties": {
|
|
856
856
|
"headerKey": {
|
|
857
857
|
"type": "string",
|
|
@@ -866,7 +866,7 @@
|
|
|
866
866
|
},
|
|
867
867
|
"showBanner": {
|
|
868
868
|
"type": "boolean",
|
|
869
|
-
"description": "Show the floating on-device banner while impersonating. Default true
|
|
869
|
+
"description": "Show the floating on-device banner while impersonating. Default true \u2014 leave it on so a QA user can see they are not themselves."
|
|
870
870
|
},
|
|
871
871
|
"dataNukeSettings": {
|
|
872
872
|
"type": "object",
|
|
@@ -902,7 +902,7 @@
|
|
|
902
902
|
},
|
|
903
903
|
"effect": "write",
|
|
904
904
|
"release": "works",
|
|
905
|
-
"description": "Shallow-merges the given settings into state and persists them to @buoy/impersonate/state. Only the keys you send change. headerKey is the HTTP header name used for injection (default x-impersonate-user-id)
|
|
905
|
+
"description": "Shallow-merges the given settings into state and persists them to @buoy/impersonate/state. Only the keys you send change. headerKey is the HTTP header name used for injection (default x-impersonate-user-id) \u2014 change it only if the backend expects a different one, since a wrong key means the backend silently ignores impersonation. ignorePatterns are REGEX SOURCE STRINGS (compiled with new RegExp) for URLs that must never receive the header; an invalid pattern throws on the device. dataNukeSettings is itself merged key-by-key. DANGER: setting dataNukeSettings.asyncStorage or .mmkv to true arms a full app-storage wipe that fires on the NEXT startImpersonation/stopImpersonation \u2014 both default to false for that reason, so do not enable them without the user explicitly asking. Changing headerKey or ignorePatterns takes effect on the very next request."
|
|
906
906
|
},
|
|
907
907
|
{
|
|
908
908
|
"action": "removeFromHistory",
|
|
@@ -922,7 +922,7 @@
|
|
|
922
922
|
},
|
|
923
923
|
"effect": "destructive",
|
|
924
924
|
"release": "works",
|
|
925
|
-
"description": "Filters the persisted history down to entries whose user.id !== userId, then writes @buoy/impersonate/state. Permanent
|
|
925
|
+
"description": "Filters the persisted history down to entries whose user.id !== userId, then writes @buoy/impersonate/state. Permanent \u2014 there is no undo and the entry can only come back by impersonating that user again. Does not stop an active impersonation of that same user; call stopImpersonation for that. A userId that matches nothing is a silent no-op that still reports success, so compare history length in the snapshot before and after."
|
|
926
926
|
},
|
|
927
927
|
{
|
|
928
928
|
"action": "clearHistory",
|
|
@@ -934,25 +934,25 @@
|
|
|
934
934
|
},
|
|
935
935
|
"effect": "destructive",
|
|
936
936
|
"release": "works",
|
|
937
|
-
"description": "Empties history (max 10 entries) and persists the empty list to @buoy/impersonate/state. Permanent and unrecoverable
|
|
937
|
+
"description": "Empties history (max 10 entries) and persists the empty list to @buoy/impersonate/state. Permanent and unrecoverable \u2014 every quick-switch shortcut the user built up is gone. Does not stop an active impersonation and does not touch settings or app caches. Only call when the user explicitly asks to clear the list."
|
|
938
938
|
}
|
|
939
939
|
],
|
|
940
|
-
"unavailableWhen": "The app doesn't depend on @buoy-gg/impersonate (the adapter is absent from the device's tool list). Note the adapter self-registers whenever the package merely resolves, even if the app never called createImpersonateTool()
|
|
940
|
+
"unavailableWhen": "The app doesn't depend on @buoy-gg/impersonate (the adapter is absent from the device's tool list). Note the adapter self-registers whenever the package merely resolves, even if the app never called createImpersonateTool() \u2014 in that state every action still works except searchUsers, which throws \"No onSearchUsers configured \u2014 pass it to createImpersonateTool()\"."
|
|
941
941
|
},
|
|
942
942
|
{
|
|
943
943
|
"toolId": "query",
|
|
944
944
|
"title": "React Query",
|
|
945
|
-
"summary": "THE way to change what a server-backed screen shows. Most screens that render API data render this cache, so \"edit what I'm looking at\" on such a screen means setQueryData on the query that is mounted (observers > 0), not a store and not the API. Also reads and mutates the rest of the app's live TanStack Query (React Query) cache on the device: list every query with status/staleness/observers/error, pull one query's real cached data, and then refetch / invalidate / reset / remove / overwrite it, simulate a query error or a perpetual loading state, clear the whole query or mutation cache, and flip TanStack's onlineManager to fake offline. Reach for it when data on screen is stale, wrong, or missing and you need to know whether the CACHE or the API is at fault (network.getSnapshot answers the API half), and for the QA moves
|
|
945
|
+
"summary": "THE way to change what a server-backed screen shows. Most screens that render API data render this cache, so \"edit what I'm looking at\" on such a screen means setQueryData on the query that is mounted (observers > 0), not a store and not the API. Also reads and mutates the rest of the app's live TanStack Query (React Query) cache on the device: list every query with status/staleness/observers/error, pull one query's real cached data, and then refetch / invalidate / reset / remove / overwrite it, simulate a query error or a perpetual loading state, clear the whole query or mutation cache, and flip TanStack's onlineManager to fake offline. Reach for it when data on screen is stale, wrong, or missing and you need to know whether the CACHE or the API is at fault (network.getSnapshot answers the API half), and for the QA moves \u2014 force the error view (triggerError), the loading view (triggerLoading), a specific payload (setQueryData). A cache edit lasts until the next successful refetch; when the change must survive a refetch or a reload, put a network override on the request instead and invalidate. Everything here is the current cache \u2014 for the history of query updates over time use the events tool with sources:['react-query'].",
|
|
946
946
|
"actions": [
|
|
947
947
|
{
|
|
948
948
|
"action": "listQueries",
|
|
949
|
-
"summary": "List every query in the cache
|
|
949
|
+
"summary": "List every query in the cache \u2014 hash, key, status, staleness, observer count, last-updated, error message. The token-cheap cache reader; start here. Send `staleOnly:true` to see only stale queries. THIS IS A CACHE, NOT AN INVENTORY: it holds only what this app has already fetched in this session, so a short list means the user has not visited those screens yet, never that the data does not exist.",
|
|
950
950
|
"params": {
|
|
951
951
|
"type": "object",
|
|
952
952
|
"properties": {
|
|
953
953
|
"includeData": {
|
|
954
954
|
"type": "boolean",
|
|
955
|
-
"description": "Include each query's full cached data payload verbatim and UNCAPPED. Default false. Heavy
|
|
955
|
+
"description": "Include each query's full cached data payload verbatim and UNCAPPED. Default false. Heavy \u2014 one cached list can be megabytes."
|
|
956
956
|
},
|
|
957
957
|
"staleOnly": {
|
|
958
958
|
"type": "boolean",
|
|
@@ -960,14 +960,14 @@
|
|
|
960
960
|
},
|
|
961
961
|
"limit": {
|
|
962
962
|
"type": "number",
|
|
963
|
-
"description": "Cap to the N most-recently-updated queries. NO default in the adapter
|
|
963
|
+
"description": "Cap to the N most-recently-updated queries. NO default in the adapter \u2014 omit and every query is returned. Values <= 0 are ignored. 25 is a sane value."
|
|
964
964
|
}
|
|
965
965
|
},
|
|
966
966
|
"additionalProperties": false
|
|
967
967
|
},
|
|
968
968
|
"effect": "read",
|
|
969
969
|
"release": "works",
|
|
970
|
-
"description": "Projects each live query to light fields: queryHash, queryKey, status (one of fresh/stale/fetching/error/inactive/paused/disabled, from getQueryStatusLabel), fetchStatus, isStale, observers, updatedAt (state.dataUpdatedAt), and error.message when present. Returns {queries, total, returned, includedData}. Sorted most-recently-updated first. Safe to call repeatedly
|
|
970
|
+
"description": "Projects each live query to light fields: queryHash, queryKey, status (one of fresh/stale/fetching/error/inactive/paused/disabled, from getQueryStatusLabel), fetchStatus, isStale, observers, updatedAt (state.dataUpdatedAt), and error.message when present. Returns {queries, total, returned, includedData}. Sorted most-recently-updated first. Safe to call repeatedly \u2014 unlike the full dehydrated snapshot, the heavy cached data stays on the device by default. TWO TRAPS: (1) the adapter has NO default limit \u2014 omit it and you get every query in the cache; (2) includeData:true returns q.state.data RAW and UNCAPPED (no 16KB wire marker, no 8MB cap like getQueryData), so it can blow the wire/token budget on a big cache \u2014 prefer getQueryData for one query's payload. The queryHash of each row is the handle every other action takes. What is NOT here has not been fetched yet \u2014 the cache fills as the user visits screens \u2014 so treat a missing key as a screen to go to (route-events.navigate, then highlight-updates.waitFor, then read again), not as an absence to report. Every read here also returns `shape`: a one-line sketch of the value's type plus, for each list in it, what its items look like. It is tiny and does not grow with the data, so it survives the 24,000-character cut when the data itself does not \u2014 read it before writing, and match it exactly. A list whose `item` is missing is EMPTY, which means nothing in the running app knows what belongs in it; anything you add there cannot be checked and will be accepted as-is, so say so rather than inventing fields. Send `staleOnly:true` to see only stale queries. `refetchEveryMs` appears on a query a mounted screen refetches on a timer: any cache edit, error or loading pin on it is replaced within that many ms, so use a network override for anything that must last \u2014 and it answers \"is the app calling this API over and over?\".",
|
|
971
971
|
"requires": [
|
|
972
972
|
"QueryClientProvider above <FloatingDevTools/>",
|
|
973
973
|
"@tanstack/react-query v5"
|
|
@@ -975,17 +975,17 @@
|
|
|
975
975
|
},
|
|
976
976
|
{
|
|
977
977
|
"action": "getQueryData",
|
|
978
|
-
"summary": "Get ONE query's real cached data by queryHash
|
|
978
|
+
"summary": "Get ONE query's real cached data by queryHash \u2014 the size-guarded channel for a payload the snapshot replaced with a marker. Send `path` to get back ONE value instead of the whole payload (path:\"item.name\", path:\"results[name=pikachu].url\") \u2014 do that whenever the payload is big, because results are cut off at 24,000 characters.",
|
|
979
979
|
"params": {
|
|
980
980
|
"type": "object",
|
|
981
981
|
"properties": {
|
|
982
982
|
"queryHash": {
|
|
983
983
|
"type": "string",
|
|
984
|
-
"description": "The target query's hash from listQueries
|
|
984
|
+
"description": "The target query's hash from listQueries \u2014 TanStack's default hash is the JSON-stringified key, e.g. '[\"todos\",{\"page\":1}]'. Tolerated if omitted (returns found:false) but then the call does nothing useful."
|
|
985
985
|
},
|
|
986
986
|
"path": {
|
|
987
987
|
"type": "string",
|
|
988
|
-
"description": "Return only the value at this path instead of the whole payload. Use it when you only need one field, and ALWAYS when the payload is big: results are cut off at 24,000 characters, and a field you never saw is a field you will guess the shape of. The path must match the CURRENT data
|
|
988
|
+
"description": "Return only the value at this path instead of the whole payload. Use it when you only need one field, and ALWAYS when the payload is big: results are cut off at 24,000 characters, and a field you never saw is a field you will guess the shape of. The path must match the CURRENT data \u2014 read `shape` first rather than assuming a wrapper. On a bad path it answers with the field names that do exist, and with the path that would have worked if that field lives somewhere else."
|
|
989
989
|
}
|
|
990
990
|
},
|
|
991
991
|
"required": [
|
|
@@ -995,7 +995,7 @@
|
|
|
995
995
|
},
|
|
996
996
|
"effect": "read",
|
|
997
997
|
"release": "works",
|
|
998
|
-
"description": "Returns {found:true, queryHash, data} where data is query.state.data capped at 8MB (over that you get {__buoyTruncated:true}; non-JSON-serializable values such as circular refs or bigint come back as {__buoyUnserializable:true}). Use this when a snapshot or detail pane shows the {__buoyDataOnDevice:true} marker
|
|
998
|
+
"description": "Returns {found:true, queryHash, data} where data is query.state.data capped at 8MB (over that you get {__buoyTruncated:true}; non-JSON-serializable values such as circular refs or bigint come back as {__buoyUnserializable:true}). Use this when a snapshot or detail pane shows the {__buoyDataOnDevice:true} marker \u2014 streamed snapshots strip anything over 16KB. Never throws: an unknown or missing hash returns {found:false, reason:'unknown queryHash'|'missing queryHash'}. Every read here also returns `shape`: a one-line sketch of the value's type plus, for each list in it, what its items look like. It is tiny and does not grow with the data, so it survives the 24,000-character cut when the data itself does not \u2014 read it before writing, and match it exactly. A list whose `item` is missing is EMPTY, which means nothing in the running app knows what belongs in it; anything you add there cannot be checked and will be accepted as-is, so say so rather than inventing fields. Send `path` to get back ONE value instead of the whole payload (path:\"item.name\", path:\"results[name=pikachu].url\") \u2014 do that whenever the payload is big, because results are cut off at 24,000 characters.",
|
|
999
999
|
"requires": [
|
|
1000
1000
|
"QueryClientProvider above <FloatingDevTools/>"
|
|
1001
1001
|
]
|
|
@@ -1008,7 +1008,7 @@
|
|
|
1008
1008
|
"properties": {
|
|
1009
1009
|
"queryHash": {
|
|
1010
1010
|
"type": "string",
|
|
1011
|
-
"description": "Target query's hash from listQueries, e.g. '[\"todos\",{\"page\":1}]'. Required
|
|
1011
|
+
"description": "Target query's hash from listQueries, e.g. '[\"todos\",{\"page\":1}]'. Required \u2014 an unknown hash throws."
|
|
1012
1012
|
}
|
|
1013
1013
|
},
|
|
1014
1014
|
"required": [
|
|
@@ -1018,7 +1018,7 @@
|
|
|
1018
1018
|
},
|
|
1019
1019
|
"effect": "write",
|
|
1020
1020
|
"release": "works",
|
|
1021
|
-
"description": "Calls query.fetch() on the single query with that hash and SWALLOWS the rejection
|
|
1021
|
+
"description": "Calls query.fetch() on the single query with that hash and SWALLOWS the rejection \u2014 it resolves ok even when the fetch fails, because the resulting error state syncs anyway. So never report 'refetch succeeded' from the return value: call listQueries afterwards and read that row's status/error. If the query has no queryFn (e.g. it was created by setQueryData) the fetch fails and the query lands in error status. THROWS 'Query with hash \"X\" not found' for an unknown hash.",
|
|
1022
1022
|
"requires": [
|
|
1023
1023
|
"QueryClientProvider above <FloatingDevTools/>",
|
|
1024
1024
|
"the query must have a queryFn to actually fetch"
|
|
@@ -1026,7 +1026,7 @@
|
|
|
1026
1026
|
},
|
|
1027
1027
|
{
|
|
1028
1028
|
"action": "invalidate",
|
|
1029
|
-
"summary": "Mark a query stale and refetch it if it has active observers
|
|
1029
|
+
"summary": "Mark a query stale and refetch it if it has active observers \u2014 the normal 'this data is out of date' fix. Takes the query's `queryHash`.",
|
|
1030
1030
|
"params": {
|
|
1031
1031
|
"type": "object",
|
|
1032
1032
|
"properties": {
|
|
@@ -1049,7 +1049,7 @@
|
|
|
1049
1049
|
},
|
|
1050
1050
|
{
|
|
1051
1051
|
"action": "reset",
|
|
1052
|
-
"summary": "Reset a query to its initial state
|
|
1052
|
+
"summary": "Reset a query to its initial state \u2014 DISCARDS its cached data, then refetches if it is active. Takes the query's `queryHash`.",
|
|
1053
1053
|
"params": {
|
|
1054
1054
|
"type": "object",
|
|
1055
1055
|
"properties": {
|
|
@@ -1065,20 +1065,20 @@
|
|
|
1065
1065
|
},
|
|
1066
1066
|
"effect": "destructive",
|
|
1067
1067
|
"release": "works",
|
|
1068
|
-
"description": "queryClient.resetQueries() with the Query as filter, so the same NON-EXACT prefix match as invalidate applies (resetting '[\\\"todos\\\"]' also resets deeper todos keys). Unlike invalidate this throws the cached value away and reverts to initialData/pending
|
|
1068
|
+
"description": "queryClient.resetQueries() with the Query as filter, so the same NON-EXACT prefix match as invalidate applies (resetting '[\\\"todos\\\"]' also resets deeper todos keys). Unlike invalidate this throws the cached value away and reverts to initialData/pending \u2014 screens bound to it will flash their loading state. There is no undo: the data only comes back if the query has a queryFn and an active observer to refetch it. THROWS for an unknown hash. Takes the query's `queryHash`.",
|
|
1069
1069
|
"requires": [
|
|
1070
1070
|
"QueryClientProvider above <FloatingDevTools/>"
|
|
1071
1071
|
]
|
|
1072
1072
|
},
|
|
1073
1073
|
{
|
|
1074
1074
|
"action": "remove",
|
|
1075
|
-
"summary": "Delete a query from the cache entirely
|
|
1075
|
+
"summary": "Delete a query from the cache entirely \u2014 the entry, its data, and its state are gone. Takes the query's `queryHash`.",
|
|
1076
1076
|
"params": {
|
|
1077
1077
|
"type": "object",
|
|
1078
1078
|
"properties": {
|
|
1079
1079
|
"queryHash": {
|
|
1080
1080
|
"type": "string",
|
|
1081
|
-
"description": "Target query's hash from listQueries. Prefix-matches
|
|
1081
|
+
"description": "Target query's hash from listQueries. Prefix-matches \u2014 it can delete more than the one row you picked."
|
|
1082
1082
|
}
|
|
1083
1083
|
},
|
|
1084
1084
|
"required": [
|
|
@@ -1088,39 +1088,39 @@
|
|
|
1088
1088
|
},
|
|
1089
1089
|
"effect": "destructive",
|
|
1090
1090
|
"release": "works",
|
|
1091
|
-
"description": "queryClient.removeQueries() with the Query as filter
|
|
1091
|
+
"description": "queryClient.removeQueries() with the Query as filter \u2014 again a NON-EXACT prefix match, so removing '[\\\"todos\\\"]' removes every key that extends it. Harsher than reset: the cache entry itself disappears rather than reverting to pending. Irreversible; a mounted component will create a brand-new entry and fetch from scratch on its next render. THROWS for an unknown hash. Returns nothing. Takes the query's `queryHash`.",
|
|
1092
1092
|
"requires": [
|
|
1093
1093
|
"QueryClientProvider above <FloatingDevTools/>"
|
|
1094
1094
|
]
|
|
1095
1095
|
},
|
|
1096
1096
|
{
|
|
1097
1097
|
"action": "setQueryData",
|
|
1098
|
-
"summary": "Change a query's cached data
|
|
1098
|
+
"summary": "Change a query's cached data \u2014 takes the queryKey ARRAY, not the hash. The instant, on-screen edit for anything a mounted query renders. To change some fields send merge:true and ONLY those fields (deep-merged into the cached value); never paste a whole payload back \u2014 large getQueryData results are truncated, and a replace that drops fields the screen renders is refused. Reverts on the next successful refetch \u2014 say so; use a network override when it must not. To change a single field, `path` + `value` is the safest form (path:\"item.name\", value:\"test123\") \u2014 always a merge, and no nesting to get wrong. Read the data first either way: the path has to match the shape that is actually there.",
|
|
1099
1099
|
"params": {
|
|
1100
1100
|
"type": "object",
|
|
1101
1101
|
"properties": {
|
|
1102
1102
|
"queryKey": {
|
|
1103
1103
|
"type": "array",
|
|
1104
|
-
"description": "The query's key ARRAY exactly as listQueries returns it, e.g. [\"todos\",{\"page\":1}]
|
|
1104
|
+
"description": "The query's key ARRAY exactly as listQueries returns it, e.g. [\"todos\",{\"page\":1}] \u2014 NOT the queryHash string. An unknown key creates a new cache entry."
|
|
1105
1105
|
},
|
|
1106
1106
|
"data": {
|
|
1107
|
-
"description": "With merge:true: only the fields to change, nested to match the current shape. Without: the complete new value. If you are changing a single field, use `path`+`value` instead
|
|
1107
|
+
"description": "With merge:true: only the fields to change, nested to match the current shape. Without: the complete new value. If you are changing a single field, use `path`+`value` instead \u2014 it cannot be nested wrongly."
|
|
1108
1108
|
},
|
|
1109
1109
|
"queryHash": {
|
|
1110
1110
|
"type": "string",
|
|
1111
|
-
"description": "Declared by the adapter's param type but never read by the handler
|
|
1111
|
+
"description": "Declared by the adapter's param type but never read by the handler \u2014 passing it has no effect."
|
|
1112
1112
|
},
|
|
1113
1113
|
"merge": {
|
|
1114
1114
|
"type": "boolean",
|
|
1115
|
-
"description": "Deep-merge `data` into the cached value as a TYPED LEAF EDIT
|
|
1115
|
+
"description": "Deep-merge `data` into the cached value as a TYPED LEAF EDIT \u2014 like the React Query devtools editor. Change the value of a field that already exists, to the SAME type: no new object fields, no type changes, no null-ing a rendered list/object. A LIST may gain or lose items, but every item you send must have the same fields as the items already in it \u2014 to change one item, resend the WHOLE list with just that item changed. Refused with the exact field on a violation. Default false."
|
|
1116
1116
|
},
|
|
1117
1117
|
"force": {
|
|
1118
1118
|
"type": "boolean",
|
|
1119
|
-
"description": "Bypass the typed-edit safety and write raw
|
|
1119
|
+
"description": "Bypass the typed-edit safety and write raw \u2014 can add/remove fields, change types, resize lists. This is what crashes screens; use ONLY when you deliberately mean to replace the whole shape. Default false."
|
|
1120
1120
|
},
|
|
1121
1121
|
"path": {
|
|
1122
1122
|
"type": "string",
|
|
1123
|
-
"description": "Set ONE value, named by its full path in the CURRENT data: path:\"<the real path>\", value:<new value>. READ THE DATA FIRST AND COPY THE PATH FROM IT
|
|
1123
|
+
"description": "Set ONE value, named by its full path in the CURRENT data: path:\"<the real path>\", value:<new value>. READ THE DATA FIRST AND COPY THE PATH FROM IT \u2014 `shape` on getQueryData/listQueries prints it. A path is only safer than a hand-nested `data` if it matches the payload you are actually looking at; if the field is at the top level the path is just \"name\", and inventing a wrapper that is not there is refused. A list step is written [field=value] or [0] and resolves to that item's own id, so it still means the same row if the list changed. Always a merge, and always through the same typed-edit guard as `data`. Requires `value`; send `data` OR `path`+`value`, not both."
|
|
1124
1124
|
},
|
|
1125
1125
|
"value": {
|
|
1126
1126
|
"description": "The value `path` is set to. Required whenever `path` is sent. null is a real value (only allowed if the field is already nullable), not a delete."
|
|
@@ -1133,7 +1133,7 @@
|
|
|
1133
1133
|
},
|
|
1134
1134
|
"effect": "write",
|
|
1135
1135
|
"release": "works",
|
|
1136
|
-
"description": "queryClient.setQueryData(queryKey, value, {updatedAt: Date.now()}). With merge:true the value written is the CURRENT cached data deep-merged with `data` (plain objects merge key by key, arrays and primitives are replaced), so `{name:'test123'}` changes one field of a 30KB payload. TO CHANGE ONE ITEM IN A LIST, address it by its own id rather than resending the array: if rows carry an id, `{results:{pikachu:{name:'test123'}}}` edits that row and leaves every other row untouched. This is the ONLY correct form when the read was capped and you did not see every row
|
|
1136
|
+
"description": "queryClient.setQueryData(queryKey, value, {updatedAt: Date.now()}). With merge:true the value written is the CURRENT cached data deep-merged with `data` (plain objects merge key by key, arrays and primitives are replaced), so `{name:'test123'}` changes one field of a 30KB payload. TO CHANGE ONE ITEM IN A LIST, address it by its own id rather than resending the array: if rows carry an id, `{results:{pikachu:{name:'test123'}}}` edits that row and leaves every other row untouched. This is the ONLY correct form when the read was capped and you did not see every row \u2014 a plain array REPLACES the list, so a one-item array deletes the rest. Without merge it REPLACES \u2014 and if the cached value is an object whose top-level keys `data` lacks, the call is refused with {ok:false, error, missingKeys} unless force:true, because a screen that renders the dropped fields crashes. Returns {ok:true, mode:'merge'|'replace'}. Gotchas: (1) it keys off queryKey, the actual array from a listQueries row (e.g. ['todos',{page:1}]) \u2014 the declared param type also mentions queryHash but the handler IGNORES it; (2) if that key is not in the cache, setQueryData CREATES a new entry with no queryFn, which then errors if anything refetches it. The injected value is overwritten by the next successful refetch. When a merge is refused for writing at the wrong depth, the refusal carries `suggestedData`: the SAME edit rebuilt at the right depth. Send that back as `data` rather than re-deriving it. To change a single field, `path` + `value` is the safest form (path:\"item.name\", value:\"test123\") \u2014 always a merge, and no nesting to get wrong. Read the data first either way: the path has to match the shape that is actually there.",
|
|
1137
1137
|
"requires": [
|
|
1138
1138
|
"QueryClientProvider above <FloatingDevTools/>"
|
|
1139
1139
|
]
|
|
@@ -1156,14 +1156,14 @@
|
|
|
1156
1156
|
},
|
|
1157
1157
|
"effect": "write",
|
|
1158
1158
|
"release": "works",
|
|
1159
|
-
"description": "Sets the query's state to {status:'error', error: new Error('Unknown error from devtools')} and stashes the real options in fetchMeta.__previousQueryOptions. The app's error boundary / error view for that screen should appear. Nothing is fetched and the network is untouched
|
|
1159
|
+
"description": "Sets the query's state to {status:'error', error: new Error('Unknown error from devtools')} and stashes the real options in fetchMeta.__previousQueryOptions. The app's error boundary / error view for that screen should appear. Nothing is fetched and the network is untouched \u2014 this is a pure cache-state simulation. Always undo it with restoreError when you're done, or that screen stays broken for the person holding the device. THROWS for an unknown hash.",
|
|
1160
1160
|
"requires": [
|
|
1161
1161
|
"QueryClientProvider above <FloatingDevTools/>"
|
|
1162
1162
|
]
|
|
1163
1163
|
},
|
|
1164
1164
|
{
|
|
1165
1165
|
"action": "restoreError",
|
|
1166
|
-
"summary": "Undo triggerError
|
|
1166
|
+
"summary": "Undo triggerError \u2014 clears the fake error. Implemented as resetQueries, so it also discards cached data.",
|
|
1167
1167
|
"params": {
|
|
1168
1168
|
"type": "object",
|
|
1169
1169
|
"properties": {
|
|
@@ -1179,7 +1179,7 @@
|
|
|
1179
1179
|
},
|
|
1180
1180
|
"effect": "destructive",
|
|
1181
1181
|
"release": "works",
|
|
1182
|
-
"description": "The undo for triggerError, but the handler is literally queryClient.resetQueries(query)
|
|
1182
|
+
"description": "The undo for triggerError, but the handler is literally queryClient.resetQueries(query) \u2014 identical to the reset action. That means it clears the query's cached data and reverts it to pending as well as clearing the fake error, and it prefix-matches on queryKey. Active queries refetch and recover; an inactive query is left empty until something observes it. THROWS for an unknown hash.",
|
|
1183
1183
|
"requires": [
|
|
1184
1184
|
"QueryClientProvider above <FloatingDevTools/>"
|
|
1185
1185
|
]
|
|
@@ -1202,14 +1202,14 @@
|
|
|
1202
1202
|
},
|
|
1203
1203
|
"effect": "destructive",
|
|
1204
1204
|
"release": "works",
|
|
1205
|
-
"description": "Clears state.data, sets status 'pending', and starts a fetch whose queryFn is a promise that NEVER resolves (with gcTime:-1), stashing the real options in fetchMeta.__previousQueryOptions. The app's skeleton/spinner/suspense fallback for that screen stays up FOREVER until you call restoreLoading
|
|
1205
|
+
"description": "Clears state.data, sets status 'pending', and starts a fetch whose queryFn is a promise that NEVER resolves (with gcTime:-1), stashing the real options in fetchMeta.__previousQueryOptions. The app's skeleton/spinner/suspense fallback for that screen stays up FOREVER until you call restoreLoading \u2014 nothing times out and a reload of the app is the only other escape. Tell the user this is a simulation, and always pair it with restoreLoading. THROWS for an unknown hash.",
|
|
1206
1206
|
"requires": [
|
|
1207
1207
|
"QueryClientProvider above <FloatingDevTools/>"
|
|
1208
1208
|
]
|
|
1209
1209
|
},
|
|
1210
1210
|
{
|
|
1211
1211
|
"action": "restoreLoading",
|
|
1212
|
-
"summary": "Undo triggerLoading
|
|
1212
|
+
"summary": "Undo triggerLoading \u2014 cancel the never-resolving fetch and refetch with the query's real options.",
|
|
1213
1213
|
"params": {
|
|
1214
1214
|
"type": "object",
|
|
1215
1215
|
"properties": {
|
|
@@ -1232,7 +1232,7 @@
|
|
|
1232
1232
|
},
|
|
1233
1233
|
{
|
|
1234
1234
|
"action": "clearQueryCache",
|
|
1235
|
-
"summary": "Wipe the ENTIRE query cache
|
|
1235
|
+
"summary": "Wipe the ENTIRE query cache \u2014 every query on the device, not just one.",
|
|
1236
1236
|
"params": {
|
|
1237
1237
|
"type": "object",
|
|
1238
1238
|
"properties": {},
|
|
@@ -1240,14 +1240,14 @@
|
|
|
1240
1240
|
},
|
|
1241
1241
|
"effect": "destructive",
|
|
1242
1242
|
"release": "works",
|
|
1243
|
-
"description": "queryClient.getQueryCache().clear(). Nukes all cached data app-wide and is irreversible; mounted screens will refetch from scratch and briefly show loading or empty states. Only reach for this when the user explicitly asks to clear the cache or to reproduce a cold-start
|
|
1243
|
+
"description": "queryClient.getQueryCache().clear(). Nukes all cached data app-wide and is irreversible; mounted screens will refetch from scratch and briefly show loading or empty states. Only reach for this when the user explicitly asks to clear the cache or to reproduce a cold-start \u2014 for one bad query use remove or invalidate instead. Takes no parameters.",
|
|
1244
1244
|
"requires": [
|
|
1245
1245
|
"QueryClientProvider above <FloatingDevTools/>"
|
|
1246
1246
|
]
|
|
1247
1247
|
},
|
|
1248
1248
|
{
|
|
1249
1249
|
"action": "clearMutationCache",
|
|
1250
|
-
"summary": "Wipe the entire mutation cache
|
|
1250
|
+
"summary": "Wipe the entire mutation cache \u2014 all recorded mutations and their states.",
|
|
1251
1251
|
"params": {
|
|
1252
1252
|
"type": "object",
|
|
1253
1253
|
"properties": {},
|
|
@@ -1255,7 +1255,7 @@
|
|
|
1255
1255
|
},
|
|
1256
1256
|
"effect": "destructive",
|
|
1257
1257
|
"release": "works",
|
|
1258
|
-
"description": "queryClient.getMutationCache().clear(). Drops the record of every mutation (pending, success, error) so the mutations list goes empty; it does NOT cancel work already in flight on the server. Irreversible
|
|
1258
|
+
"description": "queryClient.getMutationCache().clear(). Drops the record of every mutation (pending, success, error) so the mutations list goes empty; it does NOT cancel work already in flight on the server. Irreversible \u2014 the mutation history you were reading disappears. Takes no parameters.",
|
|
1259
1259
|
"requires": [
|
|
1260
1260
|
"QueryClientProvider above <FloatingDevTools/>"
|
|
1261
1261
|
]
|
|
@@ -1278,18 +1278,18 @@
|
|
|
1278
1278
|
},
|
|
1279
1279
|
"effect": "write",
|
|
1280
1280
|
"release": "works",
|
|
1281
|
-
"description": "onlineManager.setOnline(online). With false, React Query treats the device as offline: fetches go to fetchStatus 'paused' instead of running, and mutations queue
|
|
1281
|
+
"description": "onlineManager.setOnline(online). With false, React Query treats the device as offline: fetches go to fetchStatus 'paused' instead of running, and mutations queue \u2014 perfect for testing offline UI. It does NOT touch the real network stack, so plain fetch/axios calls outside React Query still go through. BLAST RADIUS IS THE WHOLE APP: leave it false and every query looks hung, so always restore it with online:true when you're done and say so to the user. Persistence is unreliable \u2014 the value only gets saved when the on-device React Query panel is open, and a reload restores whatever the WiFi toggle last saved.",
|
|
1282
1282
|
"requires": [
|
|
1283
1283
|
"QueryClientProvider above <FloatingDevTools/>"
|
|
1284
1284
|
]
|
|
1285
1285
|
}
|
|
1286
1286
|
],
|
|
1287
|
-
"unavailableWhen": "There is no QueryClientProvider above <FloatingDevTools/>
|
|
1287
|
+
"unavailableWhen": "There is no QueryClientProvider above <FloatingDevTools/> \u2014 the adapter factory returns null and the \"query\" tool is never registered (older apps advertise it under the legacy id \"react-query\"). Separately, in a RELEASE JS bundle (__DEV__ === false) the whole external-sync socket only mounts when the app passed externalSync.enableInRelease AND holds a real Pro license, so no query action is reachable at all in a normal shipped build \u2014 that gate is on the transport (FloatingDevTools.tsx / externalSyncGate), not on these handlers."
|
|
1288
1288
|
},
|
|
1289
1289
|
{
|
|
1290
1290
|
"toolId": "events",
|
|
1291
1291
|
"title": "Events",
|
|
1292
|
-
"summary": "The cross-tool activity timeline: one chronological ring buffer (max 200, newest-first) that aggregates events from every other installed Buoy tool
|
|
1292
|
+
"summary": "The cross-tool activity timeline: one chronological ring buffer (max 200, newest-first) that aggregates events from every other installed Buoy tool \u2014 network requests, redux/zustand/jotai state changes, react-query query/mutation updates, AsyncStorage/MMKV writes, route navigations, and component renders. Reach for it first for any \"what just happened in the app?\" question, before drilling into a single-tool reader. It exposes 3 actions: exportEvents (formatted read), setEnabledSources (choose what the device records), clearEvents (wipe the timeline). CRITICAL: it only records while something is watching \u2014 a cold exportEvents on a freshly connected session usually returns 0 events because nothing has ever armed capture, which is NOT the same as \"the app did nothing\". Swift captures network, storage-async, storage-mmkv, and route. React state and render sources are unavailable. Native export accepts format, includeEventData, includeSource, includeStatus, includeTitle, includeSubtitle, includeSummaryHeader, filterMode, filterSources, dataSizeThreshold, and timestampFormat; other settings are rejected. Native detail inherits source snapshot payload limits.",
|
|
1293
1293
|
"actions": [
|
|
1294
1294
|
{
|
|
1295
1295
|
"action": "exportEvents",
|
|
@@ -1331,7 +1331,7 @@
|
|
|
1331
1331
|
"render"
|
|
1332
1332
|
]
|
|
1333
1333
|
},
|
|
1334
|
-
"description": "Only these sources reach the output. Empty/omitted = all. GRANULAR values only: no bare 'storage' or friendly aliases. Applied AFTER limit
|
|
1334
|
+
"description": "Only these sources reach the output. Empty/omitted = all. GRANULAR values only: no bare 'storage' or friendly aliases. Applied AFTER limit \u2014 pair with a large limit."
|
|
1335
1335
|
},
|
|
1336
1336
|
"filterMode": {
|
|
1337
1337
|
"type": "string",
|
|
@@ -1441,17 +1441,17 @@
|
|
|
1441
1441
|
},
|
|
1442
1442
|
"effect": "read",
|
|
1443
1443
|
"release": "works",
|
|
1444
|
-
"description": "Runs the device's own \"Copy Settings\" formatter over the unified store and returns {output: string, returned: number, totalAvailable: number, includedData: boolean, format: string}. The store is a 200-event ring, newest-first. Defaults are compact: includeEventData=false, so heavy raw payloads (network bodies, redux state trees, query data) stay on the device
|
|
1444
|
+
"description": "Runs the device's own \"Copy Settings\" formatter over the unified store and returns {output: string, returned: number, totalAvailable: number, includedData: boolean, format: string}. The store is a 200-event ring, newest-first. Defaults are compact: includeEventData=false, so heavy raw payloads (network bodies, redux state trees, query data) stay on the device \u2014 pass settings.includeEventData=true only when you actually need them.\n\nTHREE traps a caller must know:\n(1) COLD READS ARE EMPTY. Sources are only subscribed while a consumer holds a watch on toolId \"events\" (the adapter's subscribe() calls ensureRemoteSourcesDefault) or the on-device Events modal is open. A bare call_action does NOT arm capture \u2014 the MCP SyncClient only watches on getSnapshot, never on callAction (packages/mcp/src/tools/packs/events.ts:80). If totalAvailable is 0, say \"nothing was being recorded\", not \"nothing happened\": arm it first (read the events snapshot, or call setEnabledSources), reproduce, then export.\n(2) limit IS APPLIED BEFORE FILTERING. The adapter does all.slice(0, limit) and only then does generateExport filter by settings.filterSources / filterMode (packages/events/src/sync/eventsSyncAdapter.ts:378-381 + eventExportFormatter.ts:616). Filtering to one source with limit:25 can produce an EMPTY output while `returned` still reports 25 \u2014 `returned` counts events handed to the formatter, not events in the output. When filtering by source, pass a large limit (or omit it, which uses the whole buffer).\n(3) filterSources takes GRANULAR EventSource values, not friendly names. \"storage\" matches nothing \u2014 use [\"storage-async\",\"storage-mmkv\"]; react-query is three values ([\"react-query\",\"react-query-query\",\"react-query-mutation\"]). The MCP get_events wrapper expands those aliases for you; a direct call_action does not.\n\nAn unrecognized preset name silently falls back to DEFAULT_COPY_SETTINGS instead of erroring. settings is shallow-merged OVER the preset. Swift captures network, storage-async, storage-mmkv, and route. React state and render sources are unavailable. Native export accepts format, includeEventData, includeSource, includeStatus, includeTitle, includeSubtitle, includeSummaryHeader, filterMode, filterSources, dataSizeThreshold, and timestampFormat; other settings are rejected. Native detail inherits source snapshot payload limits. A source records only while it is enabled (setEnabledSources), so enabling one now cannot show what happened before. For storage writes, read storage getSnapshot instead \u2014 the storage tool keeps its own timeline.",
|
|
1445
1445
|
"requires": [
|
|
1446
1446
|
"@buoy-gg/events installed and auto-discovered by FloatingDevTools",
|
|
1447
|
-
"a live watch on toolId \"events\" (an events snapshot read, a desktop Events panel, or a prior setEnabledSources call) before the window you want recorded
|
|
1447
|
+
"a live watch on toolId \"events\" (an events snapshot read, a desktop Events panel, or a prior setEnabledSources call) before the window you want recorded \u2014 otherwise the buffer is empty",
|
|
1448
1448
|
"the source packages you want in the timeline (@buoy-gg/network, /redux, /storage, /react-query, /route-events, /zustand, /jotai); the /highlight-updates 'render' source produces NOTHING in a release build"
|
|
1449
1449
|
],
|
|
1450
1450
|
"armsCapture": true
|
|
1451
1451
|
},
|
|
1452
1452
|
{
|
|
1453
1453
|
"action": "setEnabledSources",
|
|
1454
|
-
"summary": "Arm/narrow which event sources the device records for this dashboard
|
|
1454
|
+
"summary": "Arm/narrow which event sources the device records for this dashboard \u2014 the way to START capture before reproducing a bug.",
|
|
1455
1455
|
"params": {
|
|
1456
1456
|
"type": "object",
|
|
1457
1457
|
"properties": {
|
|
@@ -1473,14 +1473,14 @@
|
|
|
1473
1473
|
"render"
|
|
1474
1474
|
]
|
|
1475
1475
|
},
|
|
1476
|
-
"description": "Sources this dashboard wants the device to capture. REQUIRED in practice: omitting it (or passing []) unsubscribes everything and stops recording. Use granular values
|
|
1476
|
+
"description": "Sources this dashboard wants the device to capture. REQUIRED in practice: omitting it (or passing []) unsubscribes everything and stops recording. Use granular values \u2014 'storage' alone is not valid; react-query needs its three variants. 'render' is high-frequency and floods the 200-event buffer (and does nothing in a release build)."
|
|
1477
1477
|
}
|
|
1478
1478
|
},
|
|
1479
1479
|
"additionalProperties": false
|
|
1480
1480
|
},
|
|
1481
1481
|
"effect": "write",
|
|
1482
1482
|
"release": "works",
|
|
1483
|
-
"description": "Declares this remote consumer's wanted source set. Subscriptions are ref-counted in unifiedEventStore, so narrowing here never tears down what the on-device Events modal is also using. Returns {enabledSources: <exactly what you passed>}
|
|
1483
|
+
"description": "Declares this remote consumer's wanted source set. Subscriptions are ref-counted in unifiedEventStore, so narrowing here never tears down what the on-device Events modal is also using. Returns {enabledSources: <exactly what you passed>} \u2014 a pure echo, NOT a confirmation that the source exists or actually subscribed: an uninstalled or misspelled source silently subscribes nothing and still comes back in the echo. Verify by exporting and seeing events appear.\n\nDANGER: sources is `?? []`. Calling it with no params (or sources: []) unsubscribes EVERY source this consumer had requested and the device stops recording (packages/events/src/sync/eventsSyncAdapter.ts:357-362; the adapter test at __tests__/eventsSyncAdapter.test.ts:351 pins the {enabledSources: []} echo). Never call it bare to 'reset' \u2014 pass the full list you want.\n\nPass GRANULAR EventSource values; they are mapped to parent discovery ids by EVENT_SOURCE_TO_DISCOVERY_ID (storage-async and storage-mmkv both -> 'storage'; the three react-query values -> 'react-query'; route -> 'route-events'). The default when a dashboard starts watching is 'all' minus the high-frequency 'render' source, which is deliberately excluded because it fires per component render and would flush the 200-event buffer (packages/events/src/stores/unifiedEventStore.ts:35). The underlying store also accepts the literal string \"all\" even though the adapter types this as an array \u2014 prefer an explicit list.\n\nRELEASE CAVEAT: asking for \"render\" is inert in a release build. HighlightUpdatesController.enableBackgroundTracking() returns immediately when !__DEV__ (packages/highlight-updates/src/highlight-updates/utils/HighlightUpdatesController.ts:1905), and React Native only installs __REACT_DEVTOOLS_GLOBAL_HOOK__ under __DEV__, so no render events are ever produced \u2014 the echo still lists it. Every other source (network, storage, redux, zustand, jotai, route, react-query) records normally in release.",
|
|
1484
1484
|
"requires": [
|
|
1485
1485
|
"@buoy-gg/events installed and auto-discovered by FloatingDevTools",
|
|
1486
1486
|
"the package behind each requested source installed in the app (otherwise the request is silently a no-op)"
|
|
@@ -1497,23 +1497,23 @@
|
|
|
1497
1497
|
},
|
|
1498
1498
|
"effect": "destructive",
|
|
1499
1499
|
"release": "works",
|
|
1500
|
-
"description": "Calls unifiedEventStore.clearEvents(): empties the event array, clears the activeSources set and the network request->unified id map, then notifies listeners and any onClear subscribers (packages/events/src/stores/unifiedEventStore.ts:440). Returns nothing. There is no undo and no snapshot to restore from
|
|
1500
|
+
"description": "Calls unifiedEventStore.clearEvents(): empties the event array, clears the activeSources set and the network request->unified id map, then notifies listeners and any onClear subscribers (packages/events/src/stores/unifiedEventStore.ts:440). Returns nothing. There is no undo and no snapshot to restore from \u2014 export first if the buffer might matter.\n\nScope: it clears only the AGGREGATED timeline. The per-tool stores keep their own logs, so network requests, redux actions and storage writes are still readable through their own tools after this. Conversely, clearing a source tool does not clear this timeline.\n\nUse it to get a clean baseline right before reproducing a bug (clearEvents -> reproduce -> exportEvents), not as routine cleanup.",
|
|
1501
1501
|
"requires": [
|
|
1502
1502
|
"@buoy-gg/events installed and auto-discovered by FloatingDevTools"
|
|
1503
1503
|
]
|
|
1504
1504
|
}
|
|
1505
1505
|
],
|
|
1506
|
-
"unavailableWhen": "The app doesn't have @buoy-gg/events installed/mounted (then toolId \"events\" is absent from the device's adapter map), or the device is not connected to the broker at all. In a release bundle (__DEV__ === false) the whole sync channel is off unless the app passed externalSync.enableInRelease AND holds a real Pro license (packages/devtools-floating-menu/src/floatingMenu/FloatingDevTools.tsx:707)
|
|
1506
|
+
"unavailableWhen": "The app doesn't have @buoy-gg/events installed/mounted (then toolId \"events\" is absent from the device's adapter map), or the device is not connected to the broker at all. In a release bundle (__DEV__ === false) the whole sync channel is off unless the app passed externalSync.enableInRelease AND holds a real Pro license (packages/devtools-floating-menu/src/floatingMenu/FloatingDevTools.tsx:707) \u2014 that gates every Buoy action, not just these. Individual sources are also absent when their package isn't installed: auto-discovery require()s @buoy-gg/storage, /redux, /network, /react-query, /route-events, /zustand, /jotai, /highlight-updates and silently skips whichever are missing (packages/events/src/utils/autoDiscoverEventSources.ts:935)."
|
|
1507
1507
|
},
|
|
1508
1508
|
{
|
|
1509
1509
|
"toolId": "network",
|
|
1510
1510
|
"title": "Network",
|
|
1511
|
-
"summary": "Read and act on the app's captured HTTP traffic
|
|
1511
|
+
"summary": "Read and act on the app's captured HTTP traffic \u2014 getSnapshot lists the requests, getEventBody fetches one body, and author response-override rules that force matching requests to return a chosen status/body, fail, or arrive late. 17 actions. Two things gate honesty: (1) the device only RECORDS while something holds a capture subscription, so a cold read can be legitimately empty \u2014 call getCaptureStatus before telling anyone \"no requests happened\"; (2) in a RELEASE build every override write still returns ok:true and the rule still persists and appears in listOverrideRules, but engine.ts:74 refuses to apply it to real traffic, so nothing changes \u2014 call debugOverrides and read engine.devFlag before claiming an override took effect. Boot-time capture (requests fired before anything subscribed) is also DEV-only (preset.tsx:52).",
|
|
1512
1512
|
"actions": [
|
|
1513
1513
|
{
|
|
1514
1514
|
"action": "getSnapshot",
|
|
1515
|
-
"summary": "List the app's captured HTTP requests
|
|
1516
|
-
"description": "Returns `{ totalCaptured, shown, requests: [{id, method, url, status, durationMs, error}] }`, newest last. Bodies are NOT included
|
|
1515
|
+
"summary": "List the app's captured HTTP requests \u2014 method, url, status, duration, error. The read half of the network tool; there is no action that lists requests.",
|
|
1516
|
+
"description": "Returns `{ totalCaptured, shown, requests: [{id, method, url, status, durationMs, error}] }`, newest last. Bodies are NOT included \u2014 take an `id` from here and call `getEventBody` for one response. Narrow with `failedOnly`, `pattern` (substring of the url) and `limit`. The device only RECORDS while something holds a capture subscription, so an empty list can be legitimate: call `getCaptureStatus` before telling anyone no requests happened.",
|
|
1517
1517
|
"params": {
|
|
1518
1518
|
"type": "object",
|
|
1519
1519
|
"properties": {
|
|
@@ -1537,7 +1537,7 @@
|
|
|
1537
1537
|
},
|
|
1538
1538
|
{
|
|
1539
1539
|
"action": "getCaptureStatus",
|
|
1540
|
-
"summary": "Is the device actually recording right now, and why not
|
|
1540
|
+
"summary": "Is the device actually recording right now, and why not \u2014 call this before reporting an empty request list.",
|
|
1541
1541
|
"params": {
|
|
1542
1542
|
"type": "object",
|
|
1543
1543
|
"properties": {},
|
|
@@ -1545,10 +1545,10 @@
|
|
|
1545
1545
|
},
|
|
1546
1546
|
"effect": "read",
|
|
1547
1547
|
"release": "works",
|
|
1548
|
-
"description": "Returns {capturing:boolean, subscribers:number, interceptorInstalled:boolean|null, interceptorLive:boolean|null, listenerCount:number|null, eventCount:number}. `capturing` is driven by the event store's subscriber count, NOT by whether the interceptor is installed: an enabled override rule pins the interceptor without anything recording, so installed:true + capturing:false is a real and common state. installed:true + live:false means something re-assigned fetch/XHR over the interceptor (it self-heals on the next snapshot). null for the interceptor fields means there is no interceptable runtime here. Nothing is recorded while no one is subscribed, so an empty list usually means capture was not armed
|
|
1548
|
+
"description": "Returns {capturing:boolean, subscribers:number, interceptorInstalled:boolean|null, interceptorLive:boolean|null, listenerCount:number|null, eventCount:number}. `capturing` is driven by the event store's subscriber count, NOT by whether the interceptor is installed: an enabled override rule pins the interceptor without anything recording, so installed:true + capturing:false is a real and common state. installed:true + live:false means something re-assigned fetch/XHR over the interceptor (it self-heals on the next snapshot). null for the interceptor fields means there is no interceptable runtime here. Nothing is recorded while no one is subscribed, so an empty list usually means capture was not armed \u2014 reading arms it, so trigger the traffic and read again. Separately, requests fired before anything subscribed (session bootstrap, first queries) are only buffered in a DEV build.",
|
|
1549
1549
|
"requires": [
|
|
1550
1550
|
"@buoy-gg/network mounted in the app",
|
|
1551
|
-
"in a release build there is NO boot capture
|
|
1551
|
+
"in a release build there is NO boot capture \u2014 packages/network/src/preset.tsx:52 gates startBootCapture on __DEV__, so requests fired before the first watch are unrecoverable"
|
|
1552
1552
|
]
|
|
1553
1553
|
},
|
|
1554
1554
|
{
|
|
@@ -1569,9 +1569,9 @@
|
|
|
1569
1569
|
},
|
|
1570
1570
|
"effect": "read",
|
|
1571
1571
|
"release": "works",
|
|
1572
|
-
"description": "Returns {requestData, responseData, requestHeaders, responseHeaders} (each null when absent), or null when no event with that id exists in either the live store or the saved store. Needed because the snapshot withholds bodies over 16KB, header values over 64 chars, and (past a 1.25MB per-snapshot budget) the bodies of older rows
|
|
1572
|
+
"description": "Returns {requestData, responseData, requestHeaders, responseHeaders} (each null when absent), or null when no event with that id exists in either the live store or the saved store. Needed because the snapshot withholds bodies over 16KB, header values over 64 chars, and (past a 1.25MB per-snapshot budget) the bodies of older rows \u2014 those rows carry requestBodyOmitted/responseBodyOmitted/headersOmitted:true, which is the signal to call this. Accepts a live id or a `saved:<key>` id from a previous app run.",
|
|
1573
1573
|
"requires": [
|
|
1574
|
-
"the event must still exist
|
|
1574
|
+
"the event must still exist \u2014 live capture list (500-request cap) or the saved store"
|
|
1575
1575
|
],
|
|
1576
1576
|
"armsCapture": true
|
|
1577
1577
|
},
|
|
@@ -1587,7 +1587,7 @@
|
|
|
1587
1587
|
},
|
|
1588
1588
|
"pinned": {
|
|
1589
1589
|
"type": "boolean",
|
|
1590
|
-
"description": "true pins. false OR OMITTED unpins
|
|
1590
|
+
"description": "true pins. false OR OMITTED unpins \u2014 always send this explicitly."
|
|
1591
1591
|
}
|
|
1592
1592
|
},
|
|
1593
1593
|
"required": [
|
|
@@ -1597,7 +1597,7 @@
|
|
|
1597
1597
|
},
|
|
1598
1598
|
"effect": "write",
|
|
1599
1599
|
"release": "works",
|
|
1600
|
-
"description": "Pinning writes a persisted snapshot of the request, held at the top of the developer's Network list regardless of their filters. This is the human<->agent handoff channel: a human pins the call they think is broken, or you pin one for them to look at. WARNING on optionality: the handler reads `pinned` with a truthiness cast, so omitting it (or sending false) means UNPIN. Returns null when no request with that id exists (cleared or aged out
|
|
1600
|
+
"description": "Pinning writes a persisted snapshot of the request, held at the top of the developer's Network list regardless of their filters. This is the human<->agent handoff channel: a human pins the call they think is broken, or you pin one for them to look at. WARNING on optionality: the handler reads `pinned` with a truthiness cast, so omitting it (or sending false) means UNPIN. Returns null when no request with that id exists (cleared or aged out \u2014 re-list), {ok:true} when already in the requested state, {ok:true,active:boolean} on a real toggle, or {ok:false,reason:\"pin-cap\"|\"invalid-event\"} \u2014 pin-cap is the 25-pin ceiling (lower on some license tiers); clear some first.",
|
|
1601
1601
|
"requires": [
|
|
1602
1602
|
"the event must still exist in the live or saved store"
|
|
1603
1603
|
],
|
|
@@ -1615,7 +1615,7 @@
|
|
|
1615
1615
|
},
|
|
1616
1616
|
"saved": {
|
|
1617
1617
|
"type": "boolean",
|
|
1618
|
-
"description": "true saves. false OR OMITTED unsaves
|
|
1618
|
+
"description": "true saves. false OR OMITTED unsaves \u2014 always send this explicitly."
|
|
1619
1619
|
}
|
|
1620
1620
|
},
|
|
1621
1621
|
"required": [
|
|
@@ -1625,7 +1625,7 @@
|
|
|
1625
1625
|
},
|
|
1626
1626
|
"effect": "write",
|
|
1627
1627
|
"release": "works",
|
|
1628
|
-
"description": "Same mechanics as setPinned but writes the 'saved'/bookmark flag instead of the pin. Same optionality hazard: omitting `saved` (or sending false) means UNSAVE. Returns null when no request with that id exists, {ok:true} when already in the requested state, {ok:true,active:boolean} on a toggle, or {ok:false,reason:\"saved-cap\"|\"invalid-event\"}
|
|
1628
|
+
"description": "Same mechanics as setPinned but writes the 'saved'/bookmark flag instead of the pin. Same optionality hazard: omitting `saved` (or sending false) means UNSAVE. Returns null when no request with that id exists, {ok:true} when already in the requested state, {ok:true,active:boolean} on a toggle, or {ok:false,reason:\"saved-cap\"|\"invalid-event\"} \u2014 saved-cap is the 50-record ceiling (lower on some license tiers).",
|
|
1629
1629
|
"requires": [
|
|
1630
1630
|
"the event must still exist in the live or saved store"
|
|
1631
1631
|
],
|
|
@@ -1649,7 +1649,7 @@
|
|
|
1649
1649
|
},
|
|
1650
1650
|
"effect": "destructive",
|
|
1651
1651
|
"release": "works",
|
|
1652
|
-
"description": "Takes the record `key` from the snapshot's `saved` list
|
|
1652
|
+
"description": "Takes the record `key` from the snapshot's `saved` list \u2014 NOT a request id; passing a request id silently matches nothing. Deletes the persisted snapshot outright whatever its pin/save flags, and it cannot be recovered: this may be the only remaining copy of a request the live list already dropped. Returns {ok:true} (also when the key matched nothing), or null when `key` is missing."
|
|
1653
1653
|
},
|
|
1654
1654
|
{
|
|
1655
1655
|
"action": "clearSavedRequests",
|
|
@@ -1661,7 +1661,7 @@
|
|
|
1661
1661
|
},
|
|
1662
1662
|
"effect": "destructive",
|
|
1663
1663
|
"release": "works",
|
|
1664
|
-
"description": "Clears the 'saved'/favorites flag across the persisted store. Records that carry only the saved flag are deleted for good
|
|
1664
|
+
"description": "Clears the 'saved'/favorites flag across the persisted store. Records that carry only the saved flag are deleted for good \u2014 those snapshots may be the last copy of requests the live list already dropped, and there is no undo. Records that are also pinned survive with saved:false. Returns {ok:true}. This is the developer's curated list, not scratch data \u2014 do not call it to tidy up."
|
|
1665
1665
|
},
|
|
1666
1666
|
{
|
|
1667
1667
|
"action": "clearPinnedRequests",
|
|
@@ -1673,7 +1673,7 @@
|
|
|
1673
1673
|
},
|
|
1674
1674
|
"effect": "destructive",
|
|
1675
1675
|
"release": "works",
|
|
1676
|
-
"description": "Clears the pin flag across the persisted store. Pin-only records are deleted permanently, with no undo
|
|
1676
|
+
"description": "Clears the pin flag across the persisted store. Pin-only records are deleted permanently, with no undo \u2014 and a pin is how a human flags 'this is the broken call', so clearing them destroys that signal. Records that are also saved survive with pinned:false. Returns {ok:true}. Use this only when told to, or to free room after a pin-cap rejection."
|
|
1677
1677
|
},
|
|
1678
1678
|
{
|
|
1679
1679
|
"action": "clearEvents",
|
|
@@ -1685,7 +1685,7 @@
|
|
|
1685
1685
|
},
|
|
1686
1686
|
"effect": "destructive",
|
|
1687
1687
|
"release": "works",
|
|
1688
|
-
"description": "Empties the in-memory event list and its pending-request map. Irreversible
|
|
1688
|
+
"description": "Empties the in-memory event list and its pending-request map. Irreversible \u2014 anything not pinned or saved is gone. Useful to get a clean baseline before reproducing a bug: clear, drive the app, then read. Returns undefined (no receipt)."
|
|
1689
1689
|
},
|
|
1690
1690
|
{
|
|
1691
1691
|
"action": "listOverrideRules",
|
|
@@ -1697,7 +1697,7 @@
|
|
|
1697
1697
|
},
|
|
1698
1698
|
"effect": "read",
|
|
1699
1699
|
"release": "works",
|
|
1700
|
-
"description": "Returns the whole OverrideRulesState: {enabled:boolean, autoPaused:boolean, rules:[{id, name, urlPattern, methods, kind, status, statusText, headers, body, failKind, delayMs, times, alternate, enabled, hits, seen, createdAt}]}. Unlike the periodic snapshot, this includes full rule bodies and un-coalesced hit counts. Read `enabled:false` carefully: `autoPaused:true` means the launch-safety guard turned overrides off by ITSELF after 3 launches with rules armed and untouched
|
|
1700
|
+
"description": "Returns the whole OverrideRulesState: {enabled:boolean, autoPaused:boolean, rules:[{id, name, urlPattern, methods, kind, status, statusText, headers, body, failKind, delayMs, times, alternate, enabled, hits, seen, createdAt}]}. Unlike the periodic snapshot, this includes full rule bodies and un-coalesced hit counts. Read `enabled:false` carefully: `autoPaused:true` means the launch-safety guard turned overrides off by ITSELF after 3 launches with rules armed and untouched \u2014 nobody flipped that switch, and it must be re-armed with setOverridesEnabled({enabled:true}). A rule where `seen` > `hits` matched but did not apply (an `alternate` rule in its off phase, or a spent `times` budget) \u2014 that is the explanation for 'it matched but nothing happened'."
|
|
1701
1701
|
},
|
|
1702
1702
|
{
|
|
1703
1703
|
"action": "getOverrideRuleBody",
|
|
@@ -1717,34 +1717,34 @@
|
|
|
1717
1717
|
},
|
|
1718
1718
|
"effect": "read",
|
|
1719
1719
|
"release": "works",
|
|
1720
|
-
"description": "Returns {body:string|null}, or null when no rule has that id. Needed because rule bodies over 16KB are stripped from every snapshot and flagged with bodyOmitted:true
|
|
1720
|
+
"description": "Returns {body:string|null}, or null when no rule has that id. Needed because rule bodies over 16KB are stripped from every snapshot and flagged with bodyOmitted:true \u2014 a rule seeded from a real response (fromRequestId) is routinely hundreds of KB. Reads the raw rule list, so it works even while the master switch is off."
|
|
1721
1721
|
},
|
|
1722
1722
|
{
|
|
1723
1723
|
"action": "debugOverrides",
|
|
1724
|
-
"summary": "Why a rule is or isn't firing
|
|
1724
|
+
"summary": "Why a rule is or isn't firing \u2014 and the ONLY action that reveals whether overrides can work in this build at all.",
|
|
1725
1725
|
"params": {
|
|
1726
1726
|
"type": "object",
|
|
1727
1727
|
"properties": {
|
|
1728
1728
|
"url": {
|
|
1729
1729
|
"type": "string",
|
|
1730
|
-
"description": "Concrete URL to test the rules against, query string included. Defaults to 'https://example.com/', which matches nothing useful
|
|
1730
|
+
"description": "Concrete URL to test the rules against, query string included. Defaults to 'https://example.com/', which matches nothing useful \u2014 always pass the real URL you expect to be overridden."
|
|
1731
1731
|
}
|
|
1732
1732
|
},
|
|
1733
1733
|
"additionalProperties": false
|
|
1734
1734
|
},
|
|
1735
1735
|
"effect": "read",
|
|
1736
1736
|
"release": "works",
|
|
1737
|
-
"description": "Probes the engine against one URL (method is always GET) without recording a hit, so it never burns a rule's `times` budget or nudges an alternating rule's phase. Returns {interceptorInstalled, listenerCount, engine:{hooksInstalled, devFlag, ruleCount, matches}, store:{enabled, ruleCount, rulesVisibleToEngine}}. READ engine.devFlag FIRST: if it is `false`, this is a release build and NO rule will ever be applied to real traffic no matter what the add/enable actions reported. `rulesVisibleToEngine` < `ruleCount` means the master switch is off. `matches:false` almost always means the glob missed the query string
|
|
1737
|
+
"description": "Probes the engine against one URL (method is always GET) without recording a hit, so it never burns a rule's `times` budget or nudges an alternating rule's phase. Returns {interceptorInstalled, listenerCount, engine:{hooksInstalled, devFlag, ruleCount, matches}, store:{enabled, ruleCount, rulesVisibleToEngine}}. READ engine.devFlag FIRST: if it is `false`, this is a release build and NO rule will ever be applied to real traffic no matter what the add/enable actions reported. `rulesVisibleToEngine` < `ruleCount` means the master switch is off. `matches:false` almost always means the glob missed the query string \u2014 a pattern must match the WHOLE url. Caution: unlike getCaptureStatus this probes the listener unguarded, so it throws in a runtime with no XMLHttpRequest."
|
|
1738
1738
|
},
|
|
1739
1739
|
{
|
|
1740
1740
|
"action": "upsertOverrideRule",
|
|
1741
|
-
"summary": "Create (or replace by id) a rule that forces matching requests to return a chosen status/body, fail outright, or arrive late. Pick this over query.setQueryData when the change must survive a refetch or reload, or when the ask is about what the API returns; follow it with query.invalidate so the mounted screen refetches through the rule. To change ONE value of the real response send fromRequestId plus `bodyPath` and `bodyValue` (bodyPath:\"sprites.front_default\", or \"stats[stat.name=hp].base_stat\" for one row of a list)
|
|
1741
|
+
"summary": "Create (or replace by id) a rule that forces matching requests to return a chosen status/body, fail outright, or arrive late. Pick this over query.setQueryData when the change must survive a refetch or reload, or when the ask is about what the API returns; follow it with query.invalidate so the mounted screen refetches through the rule. To change ONE value of the real response send fromRequestId plus `bodyPath` and `bodyValue` (bodyPath:\"sprites.front_default\", or \"stats[stat.name=hp].base_stat\" for one row of a list) \u2014 there is no nesting to get wrong, which matters most here because you read that body through a wire that truncates at 24,000 characters. For several fields use `bodyPatch` with ONLY those fields; never paste a whole captured body back. For \"just the NEXT call\" / \"once, then let it work again\", send rule.times:1 \u2014 the rule auto-disables after one match. Without times it overrides EVERY matching request until deleted. Never fake one-shot by triggering the request yourself and deleting the rule: that spends the failure the user wanted to see.",
|
|
1742
1742
|
"params": {
|
|
1743
1743
|
"type": "object",
|
|
1744
1744
|
"properties": {
|
|
1745
1745
|
"fromRequestId": {
|
|
1746
1746
|
"type": "string",
|
|
1747
|
-
"description": "Build the rule from a captured request id
|
|
1747
|
+
"description": "Build the rule from a captured request id \u2014 seeds urlPattern (query string replaced with `*`), method, status and the REAL response body. Anything you send explicitly wins over the prefill."
|
|
1748
1748
|
},
|
|
1749
1749
|
"rule": {
|
|
1750
1750
|
"type": "object",
|
|
@@ -1788,14 +1788,14 @@
|
|
|
1788
1788
|
},
|
|
1789
1789
|
"statusText": {
|
|
1790
1790
|
"type": "string",
|
|
1791
|
-
"description": "kind 'respond': cosmetic only
|
|
1791
|
+
"description": "kind 'respond': cosmetic only \u2014 React Native's XMLHttpRequest has no statusText, so the app never sees it."
|
|
1792
1792
|
},
|
|
1793
1793
|
"headers": {
|
|
1794
1794
|
"type": "object",
|
|
1795
1795
|
"description": "kind 'respond': response headers, string values."
|
|
1796
1796
|
},
|
|
1797
1797
|
"body": {
|
|
1798
|
-
"description": "kind 'respond': the COMPLETE response body. A string is used as-is; an object or array is JSON.stringify'd for you
|
|
1798
|
+
"description": "kind 'respond': the COMPLETE response body. A string is used as-is; an object or array is JSON.stringify'd for you \u2014 do not pre-stringify. To change a few fields of the real response use bodyPatch instead \u2014 bodies you read over the wire are truncated past 24K chars, and pasting a torn body back gives the app a response missing half its fields."
|
|
1799
1799
|
},
|
|
1800
1800
|
"failKind": {
|
|
1801
1801
|
"type": "string",
|
|
@@ -1810,7 +1810,7 @@
|
|
|
1810
1810
|
},
|
|
1811
1811
|
"times": {
|
|
1812
1812
|
"type": "number",
|
|
1813
|
-
"description": "Auto-disable after N matches. **Set times:1 whenever the ask is about the NEXT call**
|
|
1813
|
+
"description": "Auto-disable after N matches. **Set times:1 whenever the ask is about the NEXT call** \u2014 \"make the next request fail\", \"just once\", \"then let it work again\". Omitted = the rule overrides EVERY matching request until something deletes it, and the result will tell you so. `once:true`, `failOnce:true` and `maxHits:N` are accepted as ways of saying this."
|
|
1814
1814
|
},
|
|
1815
1815
|
"alternate": {
|
|
1816
1816
|
"type": "boolean",
|
|
@@ -1822,11 +1822,11 @@
|
|
|
1822
1822
|
},
|
|
1823
1823
|
"bodyPatch": {
|
|
1824
1824
|
"type": "object",
|
|
1825
|
-
"description": "Fields to change in the real captured response (needs fromRequestId, or the id of an existing rule) as a TYPED LEAF EDIT
|
|
1825
|
+
"description": "Fields to change in the real captured response (needs fromRequestId, or the id of an existing rule) as a TYPED LEAF EDIT \u2014 like the devtools editor: same-type changes to existing fields, no new fields, no type changes, no null-ing a rendered list/object. A list may gain/lose items but each item must match the existing item shape (to change one item resend the whole list). Refused with the exact field on a violation. Ignored when `body` is sent. To change ONE ROW of a list in the response, address it by its own id instead of resending the array: {results:{pikachu:{name:'test123'}}} edits that row and leaves every other row exactly as the server sent it. That is the only correct form when you read the response through a view that truncated it."
|
|
1826
1826
|
},
|
|
1827
1827
|
"force": {
|
|
1828
1828
|
"type": "boolean",
|
|
1829
|
-
"description": "Bypass the typed-edit safety on bodyPatch and write the patch raw. Default false
|
|
1829
|
+
"description": "Bypass the typed-edit safety on bodyPatch and write the patch raw. Default false \u2014 a violating patch is refused."
|
|
1830
1830
|
},
|
|
1831
1831
|
"bodyPath": {
|
|
1832
1832
|
"type": "string",
|
|
@@ -1843,12 +1843,12 @@
|
|
|
1843
1843
|
},
|
|
1844
1844
|
"effect": "write",
|
|
1845
1845
|
"release": "noop",
|
|
1846
|
-
"description": "Create (or replace by id) a rule that forces matching requests to return a chosen status/body, fail outright, or arrive late. Pick this over query.setQueryData when the change must survive a refetch or reload, or when the ask is about what the API returns; follow it with query.invalidate so the mounted screen refetches through the rule. To change ONE value of the real response send fromRequestId plus `bodyPath` and `bodyValue` (bodyPath:\"sprites.front_default\", or \"stats[stat.name=hp].base_stat\" for one row of a list)
|
|
1847
|
-
"releaseNote": "packages/network/src/network/overrides/engine.ts:74
|
|
1846
|
+
"description": "Create (or replace by id) a rule that forces matching requests to return a chosen status/body, fail outright, or arrive late. Pick this over query.setQueryData when the change must survive a refetch or reload, or when the ask is about what the API returns; follow it with query.invalidate so the mounted screen refetches through the rule. To change ONE value of the real response send fromRequestId plus `bodyPath` and `bodyValue` (bodyPath:\"sprites.front_default\", or \"stats[stat.name=hp].base_stat\" for one row of a list) \u2014 there is no nesting to get wrong, which matters most here because you read that body through a wire that truncates at 24,000 characters. For several fields use `bodyPatch` with ONLY those fields; never paste a whole captured body back. For \"just the NEXT call\" / \"once, then let it work again\", send rule.times:1 \u2014 the rule auto-disables after one match. Without times it overrides EVERY matching request until deleted. Never fake one-shot by triggering the request yourself and deleting the rule: that spends the failure the user wanted to see. No captured request is needed: urlPattern + methods + status/body (or kind/delayMs) works for a call the app has not made yet \u2014 use fromRequestId only when you need the real response to edit.",
|
|
1847
|
+
"releaseNote": "packages/network/src/network/overrides/engine.ts:74 \u2014 overrideForRequest returns null when __DEV__ === false. The rule is still created, persisted and listed, and this action still returns ok:true, but it is NEVER applied to real traffic in a release build. Confirm with debugOverrides \u2192 engine.devFlag before telling anyone the override took effect.",
|
|
1848
1848
|
"requires": [
|
|
1849
1849
|
"a __DEV__ build for the rule to actually apply",
|
|
1850
1850
|
"something holding the interceptor open (an enabled rule pins it itself)",
|
|
1851
|
-
"Buoy Pro only to keep more than 1 rule
|
|
1851
|
+
"Buoy Pro only to keep more than 1 rule \u2014 and only in the on-device UI; this action is not license-gated"
|
|
1852
1852
|
]
|
|
1853
1853
|
},
|
|
1854
1854
|
{
|
|
@@ -1863,7 +1863,7 @@
|
|
|
1863
1863
|
},
|
|
1864
1864
|
"enabled": {
|
|
1865
1865
|
"type": "boolean",
|
|
1866
|
-
"description": "true arms this rule. false OR OMITTED disables it
|
|
1866
|
+
"description": "true arms this rule. false OR OMITTED disables it \u2014 always send this explicitly."
|
|
1867
1867
|
}
|
|
1868
1868
|
},
|
|
1869
1869
|
"required": [
|
|
@@ -1873,8 +1873,8 @@
|
|
|
1873
1873
|
},
|
|
1874
1874
|
"effect": "write",
|
|
1875
1875
|
"release": "noop",
|
|
1876
|
-
"description": "Flips a single rule's enabled flag without touching the other rules or the master switch. Use it to stage a rule and arm it later, or to silence one you want to keep. WARNING on optionality: `enabled` is read with a truthiness cast, so omitting it means DISABLE. Returns {ok:true} even when no rule has that id
|
|
1877
|
-
"releaseNote": "packages/network/src/network/overrides/engine.ts:74
|
|
1876
|
+
"description": "Flips a single rule's enabled flag without touching the other rules or the master switch. Use it to stage a rule and arm it later, or to silence one you want to keep. WARNING on optionality: `enabled` is read with a truthiness cast, so omitting it means DISABLE. Returns {ok:true} even when no rule has that id \u2014 it does not verify; confirm with listOverrideRules. Returns {ok:false,error:\"Missing rule id.\"} when `id` is absent. Note that arming a rule here does NOT turn the master switch on (unlike upsertOverrideRule), so a rule can read as enabled and still be dark.",
|
|
1877
|
+
"releaseNote": "packages/network/src/network/overrides/engine.ts:74 \u2014 overrideForRequest returns null when __DEV__ === false, so arming a rule in a release build changes the flag and nothing else. The action still returns ok:true.",
|
|
1878
1878
|
"requires": [
|
|
1879
1879
|
"a __DEV__ build for the rule to actually apply"
|
|
1880
1880
|
],
|
|
@@ -1885,13 +1885,13 @@
|
|
|
1885
1885
|
},
|
|
1886
1886
|
{
|
|
1887
1887
|
"action": "setOverridesEnabled",
|
|
1888
|
-
"summary": "Master switch for ALL override rules
|
|
1888
|
+
"summary": "Master switch for ALL override rules \u2014 off silences every rule without deleting any.",
|
|
1889
1889
|
"params": {
|
|
1890
1890
|
"type": "object",
|
|
1891
1891
|
"properties": {
|
|
1892
1892
|
"enabled": {
|
|
1893
1893
|
"type": "boolean",
|
|
1894
|
-
"description": "true arms all enabled rules. false OR OMITTED silences every rule (rules are kept)
|
|
1894
|
+
"description": "true arms all enabled rules. false OR OMITTED silences every rule (rules are kept) \u2014 always send this explicitly."
|
|
1895
1895
|
}
|
|
1896
1896
|
},
|
|
1897
1897
|
"required": [],
|
|
@@ -1899,8 +1899,8 @@
|
|
|
1899
1899
|
},
|
|
1900
1900
|
"effect": "write",
|
|
1901
1901
|
"release": "noop",
|
|
1902
|
-
"description": "One call changes how every matching request in the app behaves, so treat it as a broad-blast action rather than a toggle. WARNING on optionality: `enabled` is read with a truthiness cast, so calling this with no params DISABLES all overrides. Returns {ok:true, enabled:<the resulting state>}
|
|
1903
|
-
"releaseNote": "packages/network/src/network/overrides/engine.ts:74
|
|
1902
|
+
"description": "One call changes how every matching request in the app behaves, so treat it as a broad-blast action rather than a toggle. WARNING on optionality: `enabled` is read with a truthiness cast, so calling this with no params DISABLES all overrides. Returns {ok:true, enabled:<the resulting state>} \u2014 read that back rather than assuming. This is also the re-arm for the auto-pause state: when listOverrideRules reports autoPaused:true the launch guard turned overrides off by itself after 3 launches with rules armed and untouched, and only this puts them back. Individual rules keep their own enabled flags underneath.",
|
|
1903
|
+
"releaseNote": "packages/network/src/network/overrides/engine.ts:74 \u2014 overrideForRequest returns null when __DEV__ === false, so flipping the master switch in a release build changes nothing about real traffic in either direction. The action still returns ok:true with the new flag.",
|
|
1904
1904
|
"requires": [
|
|
1905
1905
|
"a __DEV__ build for rules to actually apply"
|
|
1906
1906
|
],
|
|
@@ -1911,7 +1911,7 @@
|
|
|
1911
1911
|
},
|
|
1912
1912
|
{
|
|
1913
1913
|
"action": "deleteOverrideRule",
|
|
1914
|
-
"summary": "Delete one override rule by id
|
|
1914
|
+
"summary": "Delete one override rule by id \u2014 the undo for upsertOverrideRule.",
|
|
1915
1915
|
"params": {
|
|
1916
1916
|
"type": "object",
|
|
1917
1917
|
"properties": {
|
|
@@ -1927,7 +1927,7 @@
|
|
|
1927
1927
|
},
|
|
1928
1928
|
"effect": "destructive",
|
|
1929
1929
|
"release": "works",
|
|
1930
|
-
"description": "Removes the rule from the persisted list permanently; there is no recovery, and a rule seeded from a real response cannot be reconstructed without that request still being in the store. This is what you call to clean up after driving a test
|
|
1930
|
+
"description": "Removes the rule from the persisted list permanently; there is no recovery, and a rule seeded from a real response cannot be reconstructed without that request still being in the store. This is what you call to clean up after driving a test \u2014 always remove rules you added, since a forgotten rule looks exactly like a real bug. Returns {ok:true} even when no rule has that id (it does not verify), or {ok:false,error:\"Missing rule id.\"} when `id` is absent."
|
|
1931
1931
|
},
|
|
1932
1932
|
{
|
|
1933
1933
|
"action": "clearOverrideRules",
|
|
@@ -1939,7 +1939,7 @@
|
|
|
1939
1939
|
},
|
|
1940
1940
|
"effect": "destructive",
|
|
1941
1941
|
"release": "works",
|
|
1942
|
-
"description": "Wipes the whole persisted rule list at once
|
|
1942
|
+
"description": "Wipes the whole persisted rule list at once \u2014 including rules a human authored and is mid-investigation on, which cannot be recovered. Prefer deleteOverrideRule({id}) for rules you created yourself, or setOverridesEnabled({enabled:false}) when you only need to silence them while keeping them. Returns {ok:true}."
|
|
1943
1943
|
},
|
|
1944
1944
|
{
|
|
1945
1945
|
"action": "getNetworkConditions",
|
|
@@ -1979,12 +1979,12 @@
|
|
|
1979
1979
|
"releaseNote": "Release builds refuse active conditions. Normal can still clear the profile."
|
|
1980
1980
|
}
|
|
1981
1981
|
],
|
|
1982
|
-
"unavailableWhen": "The app does not mount the network tool (the @buoy-gg/network preset is absent from the FloatingDevTools apps list), or the runtime has no XMLHttpRequest/fetch to intercept (Node/headless/desktop-mirror
|
|
1982
|
+
"unavailableWhen": "The app does not mount the network tool (the @buoy-gg/network preset is absent from the FloatingDevTools apps list), or the runtime has no XMLHttpRequest/fetch to intercept (Node/headless/desktop-mirror \u2014 there getCaptureStatus reports interceptorInstalled:null and debugOverrides throws). The override actions are additionally Pro-gated by the MCP/dashboard layer (requireProDevice)."
|
|
1983
1983
|
},
|
|
1984
1984
|
{
|
|
1985
1985
|
"toolId": "js-top",
|
|
1986
1986
|
"title": "JS TOP (JS-thread task manager)",
|
|
1987
|
-
"summary": "Task Manager for the React Native JS thread: it wraps timers/rAF/microtasks/Promise reactions/legacy-bridge call-ins and books exclusive ms per scheduling origin, plus a calibrated busy probe (thread busy%) and PerformanceObserver('longtask') freeze attribution (blocks >=50ms). Reach for it when the app feels laggy/janky and you need to know WHAT is eating the JS thread (a forgotten setInterval, a hot rAF loop, a chatty Promise chain). `sample` is the main entry point
|
|
1987
|
+
"summary": "Task Manager for the React Native JS thread: it wraps timers/rAF/microtasks/Promise reactions/legacy-bridge call-ins and books exclusive ms per scheduling origin, plus a calibrated busy probe (thread busy%) and PerformanceObserver('longtask') freeze attribution (blocks >=50ms). Reach for it when the app feels laggy/janky and you need to know WHAT is eating the JS thread (a forgotten setInterval, a hot rAF loop, a chatty Promise chain). `sample` is the main entry point \u2014 everything else is control (setEnabled/pause/resume/clear) or drill-down (getOriginDetail). NOT for \"components re-render too often\" \u2014 this measures TASKS, not renders.",
|
|
1988
1988
|
"actions": [
|
|
1989
1989
|
{
|
|
1990
1990
|
"action": "sample",
|
|
@@ -2006,7 +2006,7 @@
|
|
|
2006
2006
|
},
|
|
2007
2007
|
"effect": "write",
|
|
2008
2008
|
"release": "works",
|
|
2009
|
-
"description": "Self-arming one-shot measurement
|
|
2009
|
+
"description": "Self-arming one-shot measurement \u2014 the only action that works on a cold device. Ensures sampling is on (turning it on itself if nobody is watching), waits durationMs, returns a JsTopSnapshot, then restores the previous sampling state. Snapshot shape: { ts, paused, busyPct, busyHistory, tiers:{timers,promises,bridge,longtask}, rows[], totals:{attributedWindowMs,unattributedWindowMs,windowMs}, freezeSummary:{count,worstMs}, longtasks[] }. Each row = { key, label, api, windowMs, pctOfBusy, totalMs, calls, avgMs, maxMs, lastSeenAgoMs, freezes, worstFreezeMs } with api one of timeout|interval|immediate|raf|microtask|then|bridge|other|unattributed. IMPORTANT window semantics: busyPct, windowMs and pctOfBusy describe only the TRAILING 5s (20 x 250ms buckets), so a durationMs above 5000 does not widen them \u2014 only totalMs/calls accumulate across the whole sample (since engine start or last clear). A pinned \"unattributed\" row absorbs event/React/native-call-in work; on New Architecture devices tiers.bridge is false and that row is expected to be large. freezeSummary covers a trailing 60s window. To profile one specific interaction, pass clearFirst:true and drive the UI (tap_element / run_flow) while the sample runs. Blocks for the full durationMs.",
|
|
2010
2010
|
"requires": [
|
|
2011
2011
|
"@buoy-gg/js-top installed and imported in the app",
|
|
2012
2012
|
"device connected to the Buoy broker (external sync)"
|
|
@@ -2020,7 +2020,7 @@
|
|
|
2020
2020
|
"properties": {
|
|
2021
2021
|
"key": {
|
|
2022
2022
|
"type": "string",
|
|
2023
|
-
"description": "Origin key from a snapshot row, e.g. \"interval|pollFeed\", \"raf|anonymous\", or the literal \"unattributed\" system row. Required
|
|
2023
|
+
"description": "Origin key from a snapshot row, e.g. \"interval|pollFeed\", \"raf|anonymous\", or the literal \"unattributed\" system row. Required \u2014 a missing key throws."
|
|
2024
2024
|
}
|
|
2025
2025
|
},
|
|
2026
2026
|
"required": [
|
|
@@ -2030,23 +2030,23 @@
|
|
|
2030
2030
|
},
|
|
2031
2031
|
"effect": "read",
|
|
2032
2032
|
"release": "works",
|
|
2033
|
-
"description": "Pass a `key` copied from a snapshot row. Keys are either the cheap form `${api}|${functionName}` (e.g. \"interval|pollFeed\", \"timeout|anonymous\") or the refined form `${api}|${name}@${caller}:${file}:${line}`, plus the literal \"unattributed\" for the pinned system row (which returns a synthetic detail built from busy-probe residuals). Returns { key, label, api, caller?, file?, line?, frames?, promoted, scheduleCount, calls, totalMs, avgMs, maxMs, windowMs, pctOfBusy, lastSeenAgoMs, buckets[20], freezes, worstFreezeMs }, or NULL when the key was never tracked, was cleared, or was evicted (the registry caps at 500 origins and evicts the coldest)
|
|
2033
|
+
"description": "Pass a `key` copied from a snapshot row. Keys are either the cheap form `${api}|${functionName}` (e.g. \"interval|pollFeed\", \"timeout|anonymous\") or the refined form `${api}|${name}@${caller}:${file}:${line}`, plus the literal \"unattributed\" for the pinned system row (which returns a synthetic detail built from busy-probe residuals). Returns { key, label, api, caller?, file?, line?, frames?, promoted, scheduleCount, calls, totalMs, avgMs, maxMs, windowMs, pctOfBusy, lastSeenAgoMs, buckets[20], freezes, worstFreezeMs }, or NULL when the key was never tracked, was cleared, or was evicted (the registry caps at 500 origins and evicts the coldest) \u2014 re-sample for current keys. caller/file/line only appear once an origin is \"promoted\" (25+ schedules or 50ms+ total; intervals capture eagerly). In a RELEASE build the file/line/component labels are absent: source symbolication posts to Metro's /symbolicate and is hard-gated on __DEV__ (packages/js-top/src/js-top/engine/symbolicate.ts:42 `if (!isDev()) return;`), so labels stay as raw Hermes function names. All numeric stats are still correct.",
|
|
2034
2034
|
"requires": [
|
|
2035
2035
|
"@buoy-gg/js-top installed and imported in the app",
|
|
2036
|
-
"the engine must have been recording
|
|
2037
|
-
"a __DEV__ build with Metro reachable for symbolicated labels/file/line
|
|
2036
|
+
"the engine must have been recording \u2014 call `sample` (or setEnabled true) first",
|
|
2037
|
+
"a __DEV__ build with Metro reachable for symbolicated labels/file/line \u2014 in a release build origins come back as raw unsymbolicated frames"
|
|
2038
2038
|
],
|
|
2039
2039
|
"armsCapture": true
|
|
2040
2040
|
},
|
|
2041
2041
|
{
|
|
2042
2042
|
"action": "setEnabled",
|
|
2043
|
-
"summary": "Turn silent remote sampling on/off
|
|
2043
|
+
"summary": "Turn silent remote sampling on/off \u2014 runs the accounting engine with no visible change on the device.",
|
|
2044
2044
|
"params": {
|
|
2045
2045
|
"type": "object",
|
|
2046
2046
|
"properties": {
|
|
2047
2047
|
"enabled": {
|
|
2048
2048
|
"type": "boolean",
|
|
2049
|
-
"description": "true = start silent remote sampling (engine runs, device UI unchanged); false = stop it. Required
|
|
2049
|
+
"description": "true = start silent remote sampling (engine runs, device UI unchanged); false = stop it. Required \u2014 the handler reads params.enabled with no guard, so omitting params throws."
|
|
2050
2050
|
}
|
|
2051
2051
|
},
|
|
2052
2052
|
"required": [
|
|
@@ -2056,7 +2056,7 @@
|
|
|
2056
2056
|
},
|
|
2057
2057
|
"effect": "write",
|
|
2058
2058
|
"release": "works",
|
|
2059
|
-
"description": "Calls JsTopController.setRemoteSampling(enabled). enabled:true starts the engine (installs timer/microtask/Promise/bridge patches on first start, starts the busy probe) and keeps it running across calls so snapshots stay live; enabled:false stops it. This does NOT show or hide the device's own HUD pill
|
|
2059
|
+
"description": "Calls JsTopController.setRemoteSampling(enabled). enabled:true starts the engine (installs timer/microtask/Promise/bridge patches on first start, starts the busy probe) and keeps it running across calls so snapshots stay live; enabled:false stops it. This does NOT show or hide the device's own HUD pill \u2014 that is a separate on-device toggle not exposed over the wire. Use this when you want the engine warm across several MCP calls; prefer `sample` for a single measurement, since it arms and disarms itself. Leaving remote sampling on costs continuous instrumentation, so turn it back off when done.",
|
|
2060
2060
|
"requires": [
|
|
2061
2061
|
"@buoy-gg/js-top installed and imported in the app"
|
|
2062
2062
|
],
|
|
@@ -2072,7 +2072,7 @@
|
|
|
2072
2072
|
},
|
|
2073
2073
|
"effect": "write",
|
|
2074
2074
|
"release": "works",
|
|
2075
|
-
"description": "Sets the engine inactive and stops the busy probe, so no new time is booked to any origin; the existing rows, totals and freeze history are preserved and the snapshot reports paused:true. No-op if already paused. Use it to hold a table steady while reading it. Note: a paused engine also means a subsequent `sample` observes nothing until you `resume`
|
|
2075
|
+
"description": "Sets the engine inactive and stops the busy probe, so no new time is booked to any origin; the existing rows, totals and freeze history are preserved and the snapshot reports paused:true. No-op if already paused. Use it to hold a table steady while reading it. Note: a paused engine also means a subsequent `sample` observes nothing until you `resume` \u2014 `sample` does not auto-resume.",
|
|
2076
2076
|
"requires": [
|
|
2077
2077
|
"@buoy-gg/js-top installed and imported in the app"
|
|
2078
2078
|
]
|
|
@@ -2087,14 +2087,14 @@
|
|
|
2087
2087
|
},
|
|
2088
2088
|
"effect": "write",
|
|
2089
2089
|
"release": "works",
|
|
2090
|
-
"description": "Reactivates booking and restarts the busy probe if the engine is running (i.e. something is subscribed or remote sampling is on). No-op if not currently paused. Accumulated totals continue from where they stopped
|
|
2090
|
+
"description": "Reactivates booking and restarts the busy probe if the engine is running (i.e. something is subscribed or remote sampling is on). No-op if not currently paused. Accumulated totals continue from where they stopped \u2014 the pause gap is not counted as busy time.",
|
|
2091
2091
|
"requires": [
|
|
2092
2092
|
"@buoy-gg/js-top installed and imported in the app"
|
|
2093
2093
|
]
|
|
2094
2094
|
},
|
|
2095
2095
|
{
|
|
2096
2096
|
"action": "clear",
|
|
2097
|
-
"summary": "Wipe the whole task table, busy history and freeze history
|
|
2097
|
+
"summary": "Wipe the whole task table, busy history and freeze history \u2014 starts a fresh measurement window.",
|
|
2098
2098
|
"params": {
|
|
2099
2099
|
"type": "object",
|
|
2100
2100
|
"properties": {},
|
|
@@ -2102,22 +2102,22 @@
|
|
|
2102
2102
|
},
|
|
2103
2103
|
"effect": "destructive",
|
|
2104
2104
|
"release": "works",
|
|
2105
|
-
"description": "Resets the origin registry (all rows, keys, captured scheduling stacks and totals), the busy/attribution aggregator, and the recorded longtask/freeze entries, then emits an empty snapshot. Irreversible
|
|
2105
|
+
"description": "Resets the origin registry (all rows, keys, captured scheduling stacks and totals), the busy/attribution aggregator, and the recorded longtask/freeze entries, then emits an empty snapshot. Irreversible \u2014 there is no saved copy, so read anything you need before calling it. Any `key` you were holding for getOriginDetail becomes invalid. Prefer `sample` with clearFirst:true when the goal is to scope a measurement to one interaction, since that clears and re-measures in a single call.",
|
|
2106
2106
|
"requires": [
|
|
2107
2107
|
"@buoy-gg/js-top installed and imported in the app"
|
|
2108
2108
|
]
|
|
2109
2109
|
}
|
|
2110
2110
|
],
|
|
2111
|
-
"unavailableWhen": "@buoy-gg/js-top is not installed/imported in the app (the adapter is never registered under \"js-top\", so every action is unroutable), or the app is a release build that has not explicitly opted into desktop sync
|
|
2111
|
+
"unavailableWhen": "@buoy-gg/js-top is not installed/imported in the app (the adapter is never registered under \"js-top\", so every action is unroutable), or the app is a release build that has not explicitly opted into desktop sync \u2014 FloatingDevTools does not dial the broker when __DEV__ is false unless the app passes the allow-in-release externalSync option, so the device never connects and no action arrives at all."
|
|
2112
2112
|
},
|
|
2113
2113
|
{
|
|
2114
2114
|
"toolId": "app",
|
|
2115
2115
|
"title": "App",
|
|
2116
|
-
"summary": "Device-level pseudo-tool from @buoy-gg/core itself (packages/devtools-floating-menu/src/floatingMenu/sync/appSyncAdapter.ts)
|
|
2116
|
+
"summary": "Device-level pseudo-tool from @buoy-gg/core itself (packages/devtools-floating-menu/src/floatingMenu/sync/appSyncAdapter.ts) \u2014 registered unconditionally for EVERY connected Buoy app, so it works with no tool package and no native deps installed. Two actions: `ping` (cheap liveness probe that also returns which JS realm is answering) and `reloadApp` (restart the JS bundle). Reach for it to check the app is alive, to learn the dev-server origin, or to reload after an edit / to clear leaked in-memory state before a measurement. Its static snapshot (v3) also reports `{startedAt, devServerOrigin, device:{platform,isTV,isTVOS}, reload:{available,strategy}}` \u2014 read `reload.available` before promising a user a reload will work.",
|
|
2117
2117
|
"actions": [
|
|
2118
2118
|
{
|
|
2119
2119
|
"action": "ping",
|
|
2120
|
-
"summary": "Liveness probe: returns {ok:true, startedAt, devServerOrigin}. `startedAt` identifies WHICH JS realm answered
|
|
2120
|
+
"summary": "Liveness probe: returns {ok:true, startedAt, devServerOrigin}. `startedAt` identifies WHICH JS realm answered \u2014 the only reliable way to confirm a reload really happened.",
|
|
2121
2121
|
"params": {
|
|
2122
2122
|
"type": "object",
|
|
2123
2123
|
"properties": {},
|
|
@@ -2126,7 +2126,7 @@
|
|
|
2126
2126
|
},
|
|
2127
2127
|
"effect": "read",
|
|
2128
2128
|
"release": "works",
|
|
2129
|
-
"description": "Takes no params. Only a live JS realm can answer, so a timeout means the app is unresponsive or crashed. `startedAt` is the module-load timestamp of this JS realm; it CHANGES across a reload, so the pattern is: ping (remember startedAt) -> reloadApp -> wait ~1.2s -> ping until startedAt differs. A reply carrying the OLD startedAt means the doomed realm answered and the app is not back yet. `devServerOrigin` is the Metro/Expo dev server that served the bundle (e.g. \"http://192.168.1.20:8081\"), or null in a release bundle (file:// scriptURL) and on web
|
|
2129
|
+
"description": "Takes no params. Only a live JS realm can answer, so a timeout means the app is unresponsive or crashed. `startedAt` is the module-load timestamp of this JS realm; it CHANGES across a reload, so the pattern is: ping (remember startedAt) -> reloadApp -> wait ~1.2s -> ping until startedAt differs. A reply carrying the OLD startedAt means the doomed realm answered and the app is not back yet. `devServerOrigin` is the Metro/Expo dev server that served the bundle (e.g. \"http://192.168.1.20:8081\"), or null in a release bundle (file:// scriptURL) and on web \u2014 capture it while the app is healthy, because after a crash it is the only remaining way to recover the app.",
|
|
2130
2130
|
"requires": [
|
|
2131
2131
|
"@buoy-gg/core's <FloatingDevTools /> mounted (headless is fine)",
|
|
2132
2132
|
"external-sync socket connected to the broker on :42831"
|
|
@@ -2134,7 +2134,7 @@
|
|
|
2134
2134
|
},
|
|
2135
2135
|
{
|
|
2136
2136
|
"action": "reloadApp",
|
|
2137
|
-
"summary": "Restart the app's JS bundle (DevSettings.reload() in dev, expo-updates otherwise). ALL in-memory state is destroyed
|
|
2137
|
+
"summary": "Restart the app's JS bundle (DevSettings.reload() in dev, expo-updates otherwise). ALL in-memory state is destroyed \u2014 read anything you need first.",
|
|
2138
2138
|
"params": {
|
|
2139
2139
|
"type": "object",
|
|
2140
2140
|
"properties": {
|
|
@@ -2157,21 +2157,21 @@
|
|
|
2157
2157
|
},
|
|
2158
2158
|
"effect": "destructive",
|
|
2159
2159
|
"release": "throws",
|
|
2160
|
-
"description": "Arms a reload and returns {scheduled:true, strategy, delayMs} BEFORE it fires (default 250ms later), because the realm dies the instant it does. The ack is NOT proof the app came back
|
|
2161
|
-
"releaseNote": "packages/shared/src/utils/reloadApp.ts:80
|
|
2160
|
+
"description": "Arms a reload and returns {scheduled:true, strategy, delayMs} BEFORE it fires (default 250ms later), because the realm dies the instant it does. The ack is NOT proof the app came back \u2014 confirm with `ping` and a changed `startedAt`. Destroys every in-memory store (redux/zustand/jotai/react-query caches, console buffer, network log, unsaved form state); persisted storage survives. Read get_console / get_network_requests / get_storage BEFORE calling. Params: `strategy` (\"auto\" default \u2014 dev-settings in dev builds, expo-updates otherwise; force one only when debugging the reload itself) and `delayMs` (ack-flush window, default 250, clamped to >= 0). Check the snapshot's `reload.available` first: when no mechanism exists this THROWS rather than silently no-op'ing, which is the correct and honest outcome to report.",
|
|
2161
|
+
"releaseNote": "packages/shared/src/utils/reloadApp.ts:80 \u2014 `if (__DEV__ && loadDevSettings())` means \"auto\" can only resolve to dev-settings in a dev build; in release it falls through to `loadExpoUpdates()`, which returns null unless the host app installed `expo-updates`. resolveReloadStrategy then returns null and scheduleReloadApp throws at line 140 WITHOUT scheduling anything. So in a release build: works only if expo-updates is installed, otherwise a hard error \u2014 never a fake success. Explicit strategy \"dev-settings\" throws in release regardless.",
|
|
2162
2162
|
"requires": [
|
|
2163
2163
|
"@buoy-gg/core's <FloatingDevTools /> mounted (headless is fine)",
|
|
2164
2164
|
"A dev build (for DevSettings.reload) OR the host app has `expo-updates` installed",
|
|
2165
|
-
"Snapshot `reload.available === true`
|
|
2165
|
+
"Snapshot `reload.available === true` \u2014 check before promising a reload"
|
|
2166
2166
|
]
|
|
2167
2167
|
}
|
|
2168
2168
|
],
|
|
2169
|
-
"unavailableWhen": "The app's bundle is a release build (`__DEV__ === false`) that did not opt into `externalSync.enableInRelease` AND hold a real Pro license
|
|
2169
|
+
"unavailableWhen": "The app's bundle is a release build (`__DEV__ === false`) that did not opt into `externalSync.enableInRelease` AND hold a real Pro license \u2014 FloatingDevTools.tsx:719 then never mounts AutoExternalSync, so no `app` action reaches the device at all. Also effectively unavailable after a fatal render error: the React tree that answers sync actions is gone, so both actions time out (recovery is the dev-server reload path, using a `devServerOrigin` learned from an earlier successful ping). Remote drivers going through the Buoy MCP server additionally hit a Pro gate (SyncClient.requireProDevice: \"Buoy Pro is required to use the MCP\")."
|
|
2170
2170
|
},
|
|
2171
2171
|
{
|
|
2172
2172
|
"toolId": "time-machine",
|
|
2173
2173
|
"title": "Time Machine",
|
|
2174
|
-
"summary": "Save and restore the app's entire client-side state (device storage, redux, zustand, jotai, react-query) as named snapshots (\"restore points\"), plus the route it was captured on. Reach for it to put the app back into a known state before re-running a flow, to preview exactly what a restore would change before committing, or to wipe the app to fresh-install state. Snapshot payloads stay on-device
|
|
2174
|
+
"summary": "Save and restore the app's entire client-side state (device storage, redux, zustand, jotai, react-query) as named snapshots (\"restore points\"), plus the route it was captured on. Reach for it to put the app back into a known state before re-running a flow, to preview exactly what a restore would change before committing, or to wipe the app to fresh-install state. Snapshot payloads stay on-device \u2014 `list` returns metadata only; use `inspect` for one source's actual captured data and `preview` for item-level diffs. Swift supports live restore of registered providers, with UserDefaults and MMKV supplied by default. Keychain is excluded. Read provider detail and restoreModes. Native capabilities omit captureBaseline and wipeAll, and mode reload is rejected before writes. Snapshots include a safety copy before restoration; this is not a full process reset.",
|
|
2175
2175
|
"actions": [
|
|
2176
2176
|
{
|
|
2177
2177
|
"action": "list",
|
|
@@ -2183,7 +2183,7 @@
|
|
|
2183
2183
|
},
|
|
2184
2184
|
"effect": "read",
|
|
2185
2185
|
"release": "works",
|
|
2186
|
-
"description": "Same payload as the tool's synced snapshot. Returns { snapshots, busy, lastOutcome, providers, route }. `snapshots[]` = { id (e.g. \"snap_m1x2y3_4\"), name, createdAt, sizeBytes, baseline?, route?:{pathname,href}, restoreRoute?, scope?, excludedKeys?, sources:{ [sourceId]: { warnings:string[] } } }
|
|
2186
|
+
"description": "Same payload as the tool's synced snapshot. Returns { snapshots, busy, lastOutcome, providers, route }. `snapshots[]` = { id (e.g. \"snap_m1x2y3_4\"), name, createdAt, sizeBytes, baseline?, route?:{pathname,href}, restoreRoute?, scope?, excludedKeys?, sources:{ [sourceId]: { warnings:string[] } } } \u2014 METADATA ONLY, no captured values. `providers[]` = { id, label, canCapture, canRestore, detail } for the registered sources: \"storage\", \"redux\", \"zustand\", \"jotai\", \"query\" (plus any custom ones the app registered). Read `detail` before reporting a source as broken \u2014 e.g. redux says \"Auto-instrumented store cannot be restored\" when the app didn't wrap its root reducer. `route.available` is false on apps without expo-router (those snapshots record no route). `lastOutcome` is the previous restore's per-source result. Call this first \u2014 every other action needs an `id` from here. Swift supports live restore of registered providers, with UserDefaults and MMKV supplied by default. Keychain is excluded. Read provider detail and restoreModes. Native capabilities omit captureBaseline and wipeAll, and mode reload is rejected before writes. Snapshots include a safety copy before restoration; this is not a full process reset.",
|
|
2187
2187
|
"requires": [
|
|
2188
2188
|
"@buoy-gg/time-machine registered in FloatingDevTools"
|
|
2189
2189
|
]
|
|
@@ -2196,7 +2196,7 @@
|
|
|
2196
2196
|
"properties": {
|
|
2197
2197
|
"name": {
|
|
2198
2198
|
"type": "string",
|
|
2199
|
-
"description": "Label for the restore point. Omit or pass \"\" to auto-name it \"<current pathname>
|
|
2199
|
+
"description": "Label for the restore point. Omit or pass \"\" to auto-name it \"<current pathname> \u00b7 HH:MM\"."
|
|
2200
2200
|
},
|
|
2201
2201
|
"sourceIds": {
|
|
2202
2202
|
"type": "array",
|
|
@@ -2210,7 +2210,7 @@
|
|
|
2210
2210
|
},
|
|
2211
2211
|
"effect": "write",
|
|
2212
2212
|
"release": "works",
|
|
2213
|
-
"description": "Captures every source whose provider reports canCapture (or only `sourceIds` if given), serializes it into the on-device vault (@react_buoy_time_machine:snap:<id>), and stamps the current route on it. Returns { captured:true, snapshot: { id, name, sizeBytes, route?, sources:{[id]:{warnings}} } }. ALWAYS surface `sources[*].warnings`
|
|
2213
|
+
"description": "Captures every source whose provider reports canCapture (or only `sourceIds` if given), serializes it into the on-device vault (@react_buoy_time_machine:snap:<id>), and stamps the current route on it. Returns { captured:true, snapshot: { id, name, sizeBytes, route?, sources:{[id]:{warnings}} } }. ALWAYS surface `sources[*].warnings` \u2014 capture is lossy by design in known places (MMKV ArrayBuffer values, biometric-protected SecureStore keys, unregistered SecureStore keys) and a per-source `capture failed: ...` warning is how a silently-empty source shows up. An empty/omitted `name` auto-names it \"<pathname> \u00b7 HH:MM\". Does NOT modify app state. Do this before driving a flow you want to re-run.",
|
|
2214
2214
|
"requires": [
|
|
2215
2215
|
"at least one state source registered (storage/redux/zustand/jotai/react-query package installed and instrumented)",
|
|
2216
2216
|
"expo-router for the route stamp (optional)"
|
|
@@ -2226,7 +2226,7 @@
|
|
|
2226
2226
|
},
|
|
2227
2227
|
"effect": "write",
|
|
2228
2228
|
"release": "works",
|
|
2229
|
-
"description": "Takes no reading of current state
|
|
2229
|
+
"description": "Takes no reading of current state \u2014 it writes a zero-byte snapshot named \"Fresh install\" with baseline:true. Safe and non-destructive by itself; the destruction happens later when you `restore` its id (which clears every clearable source and reloads). Returns { captured:true, snapshot }. Use it to give a QA user a one-tap 'back to fresh install' point.",
|
|
2230
2230
|
"requires": [
|
|
2231
2231
|
"@buoy-gg/time-machine registered in FloatingDevTools"
|
|
2232
2232
|
]
|
|
@@ -2241,7 +2241,7 @@
|
|
|
2241
2241
|
},
|
|
2242
2242
|
"effect": "destructive",
|
|
2243
2243
|
"release": "works",
|
|
2244
|
-
"description": "Calls clear() on every registered provider (app AsyncStorage/MMKV/SecureStore keys, redux, zustand, jotai, react-query cache
|
|
2244
|
+
"description": "Calls clear() on every registered provider (app AsyncStorage/MMKV/SecureStore keys, redux, zustand, jotai, react-query cache \u2014 Buoy's own @react_buoy* keys are excluded) and then schedules a JS bundle reload. Returns a RestoreOutcome { snapshotId:\"wipe-all\", willReload, results:{[sourceId]:{ok,applied,skipped,warnings}} }. There is NO undo \u2014 take a `capture` first if the state matters. CHECK `willReload` in the result before telling anyone the app restarted (see releaseNote).",
|
|
2245
2245
|
"requires": [
|
|
2246
2246
|
"expo-updates installed for the reload leg to work in a release build"
|
|
2247
2247
|
]
|
|
@@ -2269,11 +2269,11 @@
|
|
|
2269
2269
|
"items": {
|
|
2270
2270
|
"type": "string"
|
|
2271
2271
|
},
|
|
2272
|
-
"description": "Items to LEAVE UNTOUCHED, as compound \"sourceId::itemKey\" strings using the item keys from `preview`: storage::async:@cart, storage::mmkv:<instanceId>/<key>, storage::secure:<key>, redux::<sliceName>, zustand::<storeName>, jotai::<atomLabel>, query::<queryHash> (e.g. query::[\"pokemon\"]). Passing this array overrides
|
|
2272
|
+
"description": "Items to LEAVE UNTOUCHED, as compound \"sourceId::itemKey\" strings using the item keys from `preview`: storage::async:@cart, storage::mmkv:<instanceId>/<key>, storage::secure:<key>, redux::<sliceName>, zustand::<storeName>, jotai::<atomLabel>, query::<queryHash> (e.g. query::[\"pokemon\"]). Passing this array overrides \u2014 and disables \u2014 the snapshot's saved exclusions and saved scope."
|
|
2273
2273
|
},
|
|
2274
2274
|
"restoreRoute": {
|
|
2275
2275
|
"type": "boolean",
|
|
2276
|
-
"description": "Also navigate back to the route the snapshot was captured on. OMITTING THIS IS NOT false
|
|
2276
|
+
"description": "Also navigate back to the route the snapshot was captured on. OMITTING THIS IS NOT false \u2014 it falls back to the snapshot's saved setting, which is ON whenever the snapshot has a route. Pass false to force the app to stay on the current screen. With mode \"reload\" the navigation is deferred to the next boot (status \"deferred\", 60s TTL)."
|
|
2277
2277
|
}
|
|
2278
2278
|
},
|
|
2279
2279
|
"required": [
|
|
@@ -2283,7 +2283,7 @@
|
|
|
2283
2283
|
},
|
|
2284
2284
|
"effect": "destructive",
|
|
2285
2285
|
"release": "works",
|
|
2286
|
-
"description": "Requires `id` (throws \"restore requires a snapshot `id`\" without it). mode \"live\" (default) replaces state in place source by source in a fixed order (storage
|
|
2286
|
+
"description": "Requires `id` (throws \"restore requires a snapshot `id`\" without it). mode \"live\" (default) replaces state in place source by source in a fixed order (storage \u2192 redux \u2192 zustand \u2192 jotai \u2192 query); mode \"reload\" restores ONLY persisted storage and then reloads the JS bundle so in-memory stores rebuild themselves. A baseline snapshot ALWAYS takes the clear+reload path regardless of mode. Storage restore is a diff \u2014 app keys missing from the snapshot are DELETED. Returns a RestoreOutcome: read `results[sourceId].applied` / `.skipped[].reason` / `.warnings` rather than assuming success (a source with canRestore:false comes back skipped with a reason, not an error), and read `willReload` and `route.status` (\"navigated\" | \"deferred\" | \"failed\" | \"unavailable\"). TWO GOTCHAS: (1) omitting `excludeKeys` makes the snapshot's own SAVED excludedKeys apply, and a saved `scope` narrows the restore to only the scoped items \u2014 passing an explicit `excludeKeys` array disables that saved scope. (2) omitting `restoreRoute` does NOT mean false: it falls back to the snapshot's own setting, which is ON whenever the snapshot captured a route. Pass restoreRoute:false to keep the app on the current screen. Swift supports live restore of registered providers, with UserDefaults and MMKV supplied by default. Keychain is excluded. Read provider detail and restoreModes. Native capabilities omit captureBaseline and wipeAll, and mode reload is rejected before writes. Snapshots include a safety copy before restoration; this is not a full process reset.",
|
|
2287
2287
|
"requires": [
|
|
2288
2288
|
"a source's provider must report canRestore:true or that source is skipped with a reason",
|
|
2289
2289
|
"expo-router for the route leg",
|
|
@@ -2292,7 +2292,7 @@
|
|
|
2292
2292
|
},
|
|
2293
2293
|
{
|
|
2294
2294
|
"action": "preview",
|
|
2295
|
-
"summary": "Compute the exact blast radius of restoring a snapshot
|
|
2295
|
+
"summary": "Compute the exact blast radius of restoring a snapshot \u2014 per-item added/removed/changed/wont-apply verdicts \u2014 WITHOUT changing anything.",
|
|
2296
2296
|
"params": {
|
|
2297
2297
|
"type": "object",
|
|
2298
2298
|
"properties": {
|
|
@@ -2302,7 +2302,7 @@
|
|
|
2302
2302
|
},
|
|
2303
2303
|
"compareTo": {
|
|
2304
2304
|
"type": "string",
|
|
2305
|
-
"description": "Another snapshot id to diff against instead of the app's live current state. Omit to compare against live state
|
|
2305
|
+
"description": "Another snapshot id to diff against instead of the app's live current state. Omit to compare against live state \u2014 which is what a restore would actually overwrite."
|
|
2306
2306
|
}
|
|
2307
2307
|
},
|
|
2308
2308
|
"required": [
|
|
@@ -2312,7 +2312,7 @@
|
|
|
2312
2312
|
},
|
|
2313
2313
|
"effect": "read",
|
|
2314
2314
|
"release": "works",
|
|
2315
|
-
"description": "Requires `id` (throws \"preview requires a snapshot `id`\"). Diffs the snapshot against the LIVE current state, or against another snapshot when `compareTo` is given. Returns { left, right, builtAt, sources:[{ id, label, notes:string[], error?, items:[{ key, label, verdict, reason?, oldValue?, newValue?, valueOmitted? }] }] }. Verdicts are the whole point: \"added\"/\"removed\"/\"changed\"/\"too-large\" WILL be applied by a restore; \"unchanged\" and \"recomputes\" won't; \"wont-apply\" means a real difference exists that restore CANNOT apply and `reason` says why (e.g. an auto-instrumented redux store, a tool that isn't installed)
|
|
2315
|
+
"description": "Requires `id` (throws \"preview requires a snapshot `id`\"). Diffs the snapshot against the LIVE current state, or against another snapshot when `compareTo` is given. Returns { left, right, builtAt, sources:[{ id, label, notes:string[], error?, items:[{ key, label, verdict, reason?, oldValue?, newValue?, valueOmitted? }] }] }. Verdicts are the whole point: \"added\"/\"removed\"/\"changed\"/\"too-large\" WILL be applied by a restore; \"unchanged\" and \"recomputes\" won't; \"wont-apply\" means a real difference exists that restore CANNOT apply and `reason` says why (e.g. an auto-instrumented redux store, a tool that isn't installed) \u2014 surfacing that reason is more useful than the count. `key` values are exactly what `restore`'s excludeKeys / `setScope` / `setExclusions` want, once prefixed with \"<sourceId>::\". Values larger than ~4KB are stripped and flagged valueOmitted:true, so an empty oldValue/newValue does not mean the value was empty. Side effect: it rebuilds the on-device preview panel, replacing whatever a human has open in the Time Machine UI. Always run this before a `restore` you cannot undo.",
|
|
2316
2316
|
"requires": [
|
|
2317
2317
|
"@buoy-gg/time-machine registered in FloatingDevTools"
|
|
2318
2318
|
]
|
|
@@ -2329,7 +2329,7 @@
|
|
|
2329
2329
|
},
|
|
2330
2330
|
"sourceId": {
|
|
2331
2331
|
"type": "string",
|
|
2332
|
-
"description": "Which captured source to read: \"storage\", \"redux\", \"zustand\", \"jotai\", \"query\", or a custom provider id. Must be a key of that snapshot's `sources`
|
|
2332
|
+
"description": "Which captured source to read: \"storage\", \"redux\", \"zustand\", \"jotai\", \"query\", or a custom provider id. Must be a key of that snapshot's `sources` \u2014 required."
|
|
2333
2333
|
}
|
|
2334
2334
|
},
|
|
2335
2335
|
"required": [
|
|
@@ -2340,7 +2340,7 @@
|
|
|
2340
2340
|
},
|
|
2341
2341
|
"effect": "read",
|
|
2342
2342
|
"release": "works",
|
|
2343
|
-
"description": "Requires both `id` and `sourceId` (throws \"inspect requires `id` and `sourceId`\"; also throws when the snapshot doesn't exist or never captured that source). Returns { id, sourceId, warnings:string[], data } where `data` is the source's full captured tree with Date/Map/Set rehydrated. This is the only way to see actual snapshot VALUES
|
|
2343
|
+
"description": "Requires both `id` and `sourceId` (throws \"inspect requires `id` and `sourceId`\"; also throws when the snapshot doesn't exist or never captured that source). Returns { id, sourceId, warnings:string[], data } where `data` is the source's full captured tree with Date/Map/Set rehydrated. This is the only way to see actual snapshot VALUES \u2014 `list` is metadata-only. Storage `data` is shaped { async:[key,value][], mmkv:{[instanceId]:[{key,value,valueType}]}, secure:{[key]:string|null} }. Payloads can be large (up to the 8MB action budget), so ask for one source at a time.",
|
|
2344
2344
|
"requires": [
|
|
2345
2345
|
"@buoy-gg/time-machine registered in FloatingDevTools"
|
|
2346
2346
|
]
|
|
@@ -2363,7 +2363,7 @@
|
|
|
2363
2363
|
},
|
|
2364
2364
|
"effect": "destructive",
|
|
2365
2365
|
"release": "works",
|
|
2366
|
-
"description": "Requires `id` (throws \"delete requires a snapshot `id`\"). Removes the snapshot's vault row and index entry. Returns { deleted:true, id }. The device keeps one in-memory copy so a HUMAN can tap Undo in the Time Machine UI, but that undo is NOT exposed as an action
|
|
2366
|
+
"description": "Requires `id` (throws \"delete requires a snapshot `id`\"). Removes the snapshot's vault row and index entry. Returns { deleted:true, id }. The device keeps one in-memory copy so a HUMAN can tap Undo in the Time Machine UI, but that undo is NOT exposed as an action \u2014 over the wire this is irreversible. A restore point can represent state that took ten minutes and a cooperative backend to build; confirm with the user before calling.",
|
|
2367
2367
|
"requires": [
|
|
2368
2368
|
"@buoy-gg/time-machine registered in FloatingDevTools"
|
|
2369
2369
|
]
|
|
@@ -2391,7 +2391,7 @@
|
|
|
2391
2391
|
},
|
|
2392
2392
|
"effect": "write",
|
|
2393
2393
|
"release": "works",
|
|
2394
|
-
"description": "Requires BOTH `id` and a non-empty `name` (throws \"rename requires `id` and `name`\"). Metadata only
|
|
2394
|
+
"description": "Requires BOTH `id` and a non-empty `name` (throws \"rename requires `id` and `name`\"). Metadata only \u2014 the captured payload is untouched. Returns { renamed:true, id, name }.",
|
|
2395
2395
|
"requires": [
|
|
2396
2396
|
"@buoy-gg/time-machine registered in FloatingDevTools"
|
|
2397
2397
|
]
|
|
@@ -2447,14 +2447,14 @@
|
|
|
2447
2447
|
},
|
|
2448
2448
|
"effect": "write",
|
|
2449
2449
|
"release": "works",
|
|
2450
|
-
"description": "Requires `id` (throws \"setExclusions requires `id`\"). Saves compound \"sourceId::itemKey\" strings onto the snapshot's metadata; a later `restore` that omits its own excludeKeys will honour them. Pass null or an empty array to clear. Returns { id, excluded:<count> }. Metadata only
|
|
2450
|
+
"description": "Requires `id` (throws \"setExclusions requires `id`\"). Saves compound \"sourceId::itemKey\" strings onto the snapshot's metadata; a later `restore` that omits its own excludeKeys will honour them. Pass null or an empty array to clear. Returns { id, excluded:<count> }. Metadata only \u2014 nothing in the app changes now; the effect lands on the next restore.",
|
|
2451
2451
|
"requires": [
|
|
2452
2452
|
"@buoy-gg/time-machine registered in FloatingDevTools"
|
|
2453
2453
|
]
|
|
2454
2454
|
},
|
|
2455
2455
|
{
|
|
2456
2456
|
"action": "setScope",
|
|
2457
|
-
"summary": "Persist a targeted-restore scope
|
|
2457
|
+
"summary": "Persist a targeted-restore scope \u2014 the ONLY items this restore point will ever touch.",
|
|
2458
2458
|
"params": {
|
|
2459
2459
|
"type": "object",
|
|
2460
2460
|
"properties": {
|
|
@@ -2470,7 +2470,7 @@
|
|
|
2470
2470
|
"items": {
|
|
2471
2471
|
"type": "string"
|
|
2472
2472
|
},
|
|
2473
|
-
"description": "Compound \"sourceId::itemKey\" strings from `preview`
|
|
2473
|
+
"description": "Compound \"sourceId::itemKey\" strings from `preview` \u2014 the only items a restore may touch. null or an empty array clears the scope (full restore)."
|
|
2474
2474
|
}
|
|
2475
2475
|
},
|
|
2476
2476
|
"required": [
|
|
@@ -2497,7 +2497,7 @@
|
|
|
2497
2497
|
},
|
|
2498
2498
|
"restoreRoute": {
|
|
2499
2499
|
"type": "boolean",
|
|
2500
|
-
"description": "true to navigate back to the captured route on restore, false to skip it. Only an explicit false turns it off
|
|
2500
|
+
"description": "true to navigate back to the captured route on restore, false to skip it. Only an explicit false turns it off \u2014 omitting it turns it ON."
|
|
2501
2501
|
}
|
|
2502
2502
|
},
|
|
2503
2503
|
"required": [
|
|
@@ -2507,13 +2507,13 @@
|
|
|
2507
2507
|
},
|
|
2508
2508
|
"effect": "write",
|
|
2509
2509
|
"release": "works",
|
|
2510
|
-
"description": "Requires `id` (throws \"setRestoreRoute requires `id`\"). Persists the flag onto the snapshot. NOTE THE COERCION: the handler stores `restoreRoute !== false`, so omitting the param
|
|
2510
|
+
"description": "Requires `id` (throws \"setRestoreRoute requires `id`\"). Persists the flag onto the snapshot. NOTE THE COERCION: the handler stores `restoreRoute !== false`, so omitting the param \u2014 or sending anything other than exactly false \u2014 turns it ON. Returns { id, restoreRoute }. Only meaningful for snapshots that captured a route (see `route` on the snapshot and `route.available` from `list`).",
|
|
2511
2511
|
"requires": [
|
|
2512
2512
|
"expo-router on the device for the flag to have any effect"
|
|
2513
2513
|
]
|
|
2514
2514
|
}
|
|
2515
2515
|
],
|
|
2516
|
-
"unavailableWhen": "The `@buoy-gg/time-machine` tool is not in the app's FloatingDevTools tool list, or the app is a release build that has not opted into `externalSync.enableInRelease` with a real Pro license (packages/devtools-floating-menu/src/floatingMenu/autoExternalSync.tsx:190
|
|
2516
|
+
"unavailableWhen": "The `@buoy-gg/time-machine` tool is not in the app's FloatingDevTools tool list, or the app is a release build that has not opted into `externalSync.enableInRelease` with a real Pro license (packages/devtools-floating-menu/src/floatingMenu/autoExternalSync.tsx:190 \u2014 no socket, so no action reaches the device at all). Individual state sources are also absent unless their tool package is installed and instrumented: check `providers[].canCapture`/`canRestore` from `list` before assuming a source is covered."
|
|
2517
2517
|
},
|
|
2518
2518
|
{
|
|
2519
2519
|
"toolId": "clock",
|
|
@@ -2764,11 +2764,872 @@
|
|
|
2764
2764
|
}
|
|
2765
2765
|
]
|
|
2766
2766
|
},
|
|
2767
|
+
{
|
|
2768
|
+
"toolId": "lifecycle",
|
|
2769
|
+
"title": "Lifecycle",
|
|
2770
|
+
"summary": "Put the app through what happens to it on a phone and see what its code does: send it to the background and back (background, returnToApp), interrupt it like the app switcher or a call (interrupt), send a memory warning, deliver a deep link while it runs (openUrl), press Android's back button (pressBack), change the color scheme, set battery level and low power mode (setPower, only when the app has expo-battery or react-native-device-info), or relaunch it and check whether it reopens where it was (relaunch). Recipes chain these (runRecipe: phone-call, quick-switch, away-31m, overnight, low-memory, dying-battery, relaunch); away-31m moves the app clock with @buoy-gg/clock to test session timeouts. Simulations emit the same events native code sends, so the app's own listeners run unchanged, but they reach JS listeners only: timers, requests, Reanimated and native code keep running, and the next real OS event ends the simulation. Every action returns the same state object: { platform, host:{isSimulator,deviceName,bundleId,os}, realAppState, away:{kind:'background'|'inactive',startedAt,returnAt,skippedMs}|null, power|null, scheme|null, listeners:{appState,focus,memoryWarning,deepLink,battery} (counts of the APP's own listeners; 0 means nothing in the app reacts to that signal), batteryLibraries, clockAvailable, report:{label,kind,collectingUntil,counts,events:[{source,title,subtitle,status,at,whileAway}],finding?,note?,unavailable?}|null, relaunch:{before,after,tookMs,restored}|null, log, recipes, settings, active }. The report collects requests, query updates, route changes and store writes from the start of a simulation until a few seconds after the app returns; `finding` names the notable result, such as network requests made while in the background. Read getState again after the window to see the full report. reset ends everything.",
|
|
2771
|
+
"unavailableWhen": "Needs @buoy-gg/lifecycle installed (FloatingDevTools auto-discovers it). The report needs @buoy-gg/events; skipping the wait needs @buoy-gg/clock; relaunch reports need expo-router to read the route.",
|
|
2772
|
+
"actions": [
|
|
2773
|
+
{
|
|
2774
|
+
"action": "getState",
|
|
2775
|
+
"summary": "Read what is simulated, the app's listener counts per signal, the last report and the relaunch check.",
|
|
2776
|
+
"description": "Returns the state object described in the tool summary, with listener counts read fresh. Read it a few seconds after returnToApp or a one-shot signal: report.collectingUntil is non-null while it is still collecting.",
|
|
2777
|
+
"effect": "read",
|
|
2778
|
+
"release": "works",
|
|
2779
|
+
"params": {
|
|
2780
|
+
"type": "object",
|
|
2781
|
+
"properties": {},
|
|
2782
|
+
"additionalProperties": false
|
|
2783
|
+
}
|
|
2784
|
+
},
|
|
2785
|
+
{
|
|
2786
|
+
"action": "background",
|
|
2787
|
+
"summary": "Send the app to the background (iOS: inactive then background; Android: blur then background), optionally for a `duration`.",
|
|
2788
|
+
"description": "Emits the platform's real sequence to the app's AppState listeners and starts a report. With `duration` it returns by itself; with `skipWait` and a duration it moves the app clock forward instead and returns at once. Throws when skipWait has no duration or @buoy-gg/clock is missing. Returns the new state.",
|
|
2789
|
+
"effect": "write",
|
|
2790
|
+
"release": "works",
|
|
2791
|
+
"undo": {
|
|
2792
|
+
"action": "returnToApp",
|
|
2793
|
+
"note": "Brings the app back to active. reset also ends it."
|
|
2794
|
+
},
|
|
2795
|
+
"params": {
|
|
2796
|
+
"type": "object",
|
|
2797
|
+
"properties": {
|
|
2798
|
+
"duration": {
|
|
2799
|
+
"type": [
|
|
2800
|
+
"string",
|
|
2801
|
+
"number"
|
|
2802
|
+
],
|
|
2803
|
+
"description": "Return by itself after this long: \"5s\", \"5m\", \"2h\", or ms. Omit to stay away until returnToApp."
|
|
2804
|
+
},
|
|
2805
|
+
"skipWait": {
|
|
2806
|
+
"type": "boolean",
|
|
2807
|
+
"description": "Move the app clock forward by `duration` and return at once instead of waiting (needs @buoy-gg/clock). Tests session timeouts in one call."
|
|
2808
|
+
}
|
|
2809
|
+
},
|
|
2810
|
+
"additionalProperties": false
|
|
2811
|
+
}
|
|
2812
|
+
},
|
|
2813
|
+
{
|
|
2814
|
+
"action": "interrupt",
|
|
2815
|
+
"summary": "Make the app inactive, like the app switcher, Control Center or an incoming call (Android: window blur).",
|
|
2816
|
+
"description": "iOS emits 'inactive'; Android takes window focus away without changing AppState. Same `duration` and `skipWait` as background. Returns the new state.",
|
|
2817
|
+
"effect": "write",
|
|
2818
|
+
"release": "works",
|
|
2819
|
+
"undo": {
|
|
2820
|
+
"action": "returnToApp",
|
|
2821
|
+
"note": "Brings the app back to active. reset also ends it."
|
|
2822
|
+
},
|
|
2823
|
+
"params": {
|
|
2824
|
+
"type": "object",
|
|
2825
|
+
"properties": {
|
|
2826
|
+
"duration": {
|
|
2827
|
+
"type": [
|
|
2828
|
+
"string",
|
|
2829
|
+
"number"
|
|
2830
|
+
],
|
|
2831
|
+
"description": "Return by itself after this long: \"5s\", \"5m\", \"2h\", or ms. Omit to stay away until returnToApp."
|
|
2832
|
+
},
|
|
2833
|
+
"skipWait": {
|
|
2834
|
+
"type": "boolean",
|
|
2835
|
+
"description": "Move the app clock forward by `duration` and return at once instead of waiting (needs @buoy-gg/clock). Tests session timeouts in one call."
|
|
2836
|
+
}
|
|
2837
|
+
},
|
|
2838
|
+
"additionalProperties": false
|
|
2839
|
+
}
|
|
2840
|
+
},
|
|
2841
|
+
{
|
|
2842
|
+
"action": "returnToApp",
|
|
2843
|
+
"summary": "Bring the app back to active after background or interrupt.",
|
|
2844
|
+
"description": "Emits 'active' (and focus on Android) and keeps the report collecting for the report window. Does nothing when the app is not away. Returns the new state.",
|
|
2845
|
+
"effect": "write",
|
|
2846
|
+
"release": "works",
|
|
2847
|
+
"params": {
|
|
2848
|
+
"type": "object",
|
|
2849
|
+
"properties": {},
|
|
2850
|
+
"additionalProperties": false
|
|
2851
|
+
}
|
|
2852
|
+
},
|
|
2853
|
+
{
|
|
2854
|
+
"action": "memoryWarning",
|
|
2855
|
+
"summary": "Send a memory warning to the app's AppState 'memoryWarning' listeners.",
|
|
2856
|
+
"description": "Starts a report. Android never sends this event to React Native, so on Android the report carries a note saying the listeners ran anyway. Returns the new state.",
|
|
2857
|
+
"effect": "write",
|
|
2858
|
+
"release": "works",
|
|
2859
|
+
"params": {
|
|
2860
|
+
"type": "object",
|
|
2861
|
+
"properties": {},
|
|
2862
|
+
"additionalProperties": false
|
|
2863
|
+
}
|
|
2864
|
+
},
|
|
2865
|
+
{
|
|
2866
|
+
"action": "openUrl",
|
|
2867
|
+
"summary": "Deliver a deep link to the running app, as the OS would (Linking 'url').",
|
|
2868
|
+
"description": "Reaches Linking.addEventListener('url'), expo-router and React Navigation linking. getInitialURL stays unchanged. Throws when `url` has no scheme. Returns the new state with a report of what the app did (usually a route change).",
|
|
2869
|
+
"effect": "write",
|
|
2870
|
+
"release": "works",
|
|
2871
|
+
"params": {
|
|
2872
|
+
"type": "object",
|
|
2873
|
+
"properties": {
|
|
2874
|
+
"url": {
|
|
2875
|
+
"type": "string",
|
|
2876
|
+
"description": "The link with its scheme, e.g. \"myapp://orders/42\"."
|
|
2877
|
+
}
|
|
2878
|
+
},
|
|
2879
|
+
"additionalProperties": false,
|
|
2880
|
+
"required": [
|
|
2881
|
+
"url"
|
|
2882
|
+
]
|
|
2883
|
+
}
|
|
2884
|
+
},
|
|
2885
|
+
{
|
|
2886
|
+
"action": "pressBack",
|
|
2887
|
+
"summary": "Press Android's hardware back button without ever leaving the app; reports whether a screen handled it.",
|
|
2888
|
+
"description": "Android only (throws on iOS). The result has `handled`: false means no screen took the press and a real press would have exited the app. Returns the new state plus `handled`.",
|
|
2889
|
+
"effect": "write",
|
|
2890
|
+
"release": "works",
|
|
2891
|
+
"params": {
|
|
2892
|
+
"type": "object",
|
|
2893
|
+
"properties": {},
|
|
2894
|
+
"additionalProperties": false
|
|
2895
|
+
}
|
|
2896
|
+
},
|
|
2897
|
+
{
|
|
2898
|
+
"action": "setColorScheme",
|
|
2899
|
+
"summary": "Switch the app to `scheme` light or dark as if the system setting changed, or back to the system value.",
|
|
2900
|
+
"description": "mode 'js' (default) emits the change to Appearance and useColorScheme; mode 'native' calls Appearance.setColorScheme so native colors change too. Returns the new state.",
|
|
2901
|
+
"effect": "write",
|
|
2902
|
+
"release": "works",
|
|
2903
|
+
"undo": {
|
|
2904
|
+
"action": "reset",
|
|
2905
|
+
"note": "reset ends every simulation and puts the real values back."
|
|
2906
|
+
},
|
|
2907
|
+
"params": {
|
|
2908
|
+
"type": "object",
|
|
2909
|
+
"properties": {
|
|
2910
|
+
"scheme": {
|
|
2911
|
+
"type": "string",
|
|
2912
|
+
"enum": [
|
|
2913
|
+
"light",
|
|
2914
|
+
"dark",
|
|
2915
|
+
"system"
|
|
2916
|
+
],
|
|
2917
|
+
"description": "The scheme to switch to. 'system' ends the simulation and puts the real value back."
|
|
2918
|
+
},
|
|
2919
|
+
"mode": {
|
|
2920
|
+
"type": "string",
|
|
2921
|
+
"enum": [
|
|
2922
|
+
"js",
|
|
2923
|
+
"native"
|
|
2924
|
+
],
|
|
2925
|
+
"description": "'js' (default) or 'native'."
|
|
2926
|
+
}
|
|
2927
|
+
},
|
|
2928
|
+
"additionalProperties": false,
|
|
2929
|
+
"required": [
|
|
2930
|
+
"scheme"
|
|
2931
|
+
]
|
|
2932
|
+
}
|
|
2933
|
+
},
|
|
2934
|
+
{
|
|
2935
|
+
"action": "setPower",
|
|
2936
|
+
"summary": "Set battery `level`, charging `state` and `lowPowerMode` for expo-battery and react-native-device-info listeners.",
|
|
2937
|
+
"description": "Emits each library's own events and wraps its getters so reads agree until resetPower. Fields you omit keep their current simulated value. Throws when neither library is installed. Returns the new state; power.realGetters lists getters that could not be wrapped.",
|
|
2938
|
+
"effect": "write",
|
|
2939
|
+
"release": "works",
|
|
2940
|
+
"undo": {
|
|
2941
|
+
"action": "resetPower",
|
|
2942
|
+
"note": "resetPower restores the real getters and re-emits the real values."
|
|
2943
|
+
},
|
|
2944
|
+
"params": {
|
|
2945
|
+
"type": "object",
|
|
2946
|
+
"properties": {
|
|
2947
|
+
"level": {
|
|
2948
|
+
"type": "number",
|
|
2949
|
+
"description": "Battery level 0-1 (a value above 1 is read as a percentage)."
|
|
2950
|
+
},
|
|
2951
|
+
"lowPowerMode": {
|
|
2952
|
+
"type": "boolean",
|
|
2953
|
+
"description": "Low power mode on or off."
|
|
2954
|
+
},
|
|
2955
|
+
"state": {
|
|
2956
|
+
"type": "string",
|
|
2957
|
+
"enum": [
|
|
2958
|
+
"unplugged",
|
|
2959
|
+
"charging",
|
|
2960
|
+
"full",
|
|
2961
|
+
"unknown"
|
|
2962
|
+
],
|
|
2963
|
+
"description": "Charging state."
|
|
2964
|
+
}
|
|
2965
|
+
},
|
|
2966
|
+
"additionalProperties": false
|
|
2967
|
+
}
|
|
2968
|
+
},
|
|
2969
|
+
{
|
|
2970
|
+
"action": "resetPower",
|
|
2971
|
+
"summary": "Put the real battery values back.",
|
|
2972
|
+
"description": "Restores wrapped getters and re-emits the real values. Returns the new state.",
|
|
2973
|
+
"effect": "write",
|
|
2974
|
+
"release": "works",
|
|
2975
|
+
"params": {
|
|
2976
|
+
"type": "object",
|
|
2977
|
+
"properties": {},
|
|
2978
|
+
"additionalProperties": false
|
|
2979
|
+
}
|
|
2980
|
+
},
|
|
2981
|
+
{
|
|
2982
|
+
"action": "relaunch",
|
|
2983
|
+
"summary": "Restart the app's JS and check whether it reopens the screen it was on.",
|
|
2984
|
+
"description": "Records the current route, ends every simulation and reloads the JS runtime about 250 ms later. The native process survives, so this tests JS state restoration and persisted storage. After the reload, getState's relaunch shows before, after and restored (about 2.5 s after start). Unsaved in-memory state is lost.",
|
|
2985
|
+
"effect": "destructive",
|
|
2986
|
+
"release": "works",
|
|
2987
|
+
"params": {
|
|
2988
|
+
"type": "object",
|
|
2989
|
+
"properties": {},
|
|
2990
|
+
"additionalProperties": false
|
|
2991
|
+
}
|
|
2992
|
+
},
|
|
2993
|
+
{
|
|
2994
|
+
"action": "runRecipe",
|
|
2995
|
+
"summary": "Run the interruption named by `id`: phone-call, quick-switch, away-31m, overnight, low-memory, dying-battery or relaunch.",
|
|
2996
|
+
"description": "Each recipe is a short sequence of the actions above; getState lists them with `unavailable` when a dependency is missing (away-31m and overnight need @buoy-gg/clock, dying-battery needs a battery library). Returns the new state.",
|
|
2997
|
+
"effect": "write",
|
|
2998
|
+
"release": "works",
|
|
2999
|
+
"undo": {
|
|
3000
|
+
"action": "reset",
|
|
3001
|
+
"note": "reset ends every simulation and puts the real values back."
|
|
3002
|
+
},
|
|
3003
|
+
"params": {
|
|
3004
|
+
"type": "object",
|
|
3005
|
+
"properties": {
|
|
3006
|
+
"id": {
|
|
3007
|
+
"type": "string",
|
|
3008
|
+
"description": "Recipe id, e.g. \"away-31m\"."
|
|
3009
|
+
}
|
|
3010
|
+
},
|
|
3011
|
+
"additionalProperties": false,
|
|
3012
|
+
"required": [
|
|
3013
|
+
"id"
|
|
3014
|
+
]
|
|
3015
|
+
}
|
|
3016
|
+
},
|
|
3017
|
+
{
|
|
3018
|
+
"action": "clearReport",
|
|
3019
|
+
"summary": "Stop and clear the current report.",
|
|
3020
|
+
"description": "Returns the new state.",
|
|
3021
|
+
"effect": "write",
|
|
3022
|
+
"release": "works",
|
|
3023
|
+
"params": {
|
|
3024
|
+
"type": "object",
|
|
3025
|
+
"properties": {},
|
|
3026
|
+
"additionalProperties": false
|
|
3027
|
+
}
|
|
3028
|
+
},
|
|
3029
|
+
{
|
|
3030
|
+
"action": "reset",
|
|
3031
|
+
"summary": "End every simulation: return to the app, restore battery and color scheme.",
|
|
3032
|
+
"description": "Does not undo a Clock tool change made by skipWait or a recipe; reset the clock with the clock tool. Returns the new state.",
|
|
3033
|
+
"effect": "write",
|
|
3034
|
+
"release": "works",
|
|
3035
|
+
"params": {
|
|
3036
|
+
"type": "object",
|
|
3037
|
+
"properties": {},
|
|
3038
|
+
"additionalProperties": false
|
|
3039
|
+
}
|
|
3040
|
+
}
|
|
3041
|
+
]
|
|
3042
|
+
},
|
|
3043
|
+
{
|
|
3044
|
+
"toolId": "location",
|
|
3045
|
+
"title": "Location",
|
|
3046
|
+
"summary": "Tell the app it is somewhere else without touching the device: pin it to a place (setLocation), move it along a route with real heading and speed (startRoute, pauseRoute, resumeRoute, seekRoute, setSpeed, stopRoute), cross a geofence (enterRegion/exitRegion), set the signal quality (setConditions: good, weak, no-signal, off), drop the GPS signal (setSignal) or switch location services off (setServices), then go back to the real location (reset). Reach for it to test anything location-based on the client: nearest-store lists and distances, 'you are here' labels, delivery radius checks, geofence check-ins, background tracking, and how screens handle no signal or services off. What changes: every expo-location call (getCurrentPositionAsync, watchPositionAsync, getLastKnownPositionAsync, heading, hasServicesEnabledAsync, geofencing and background location tasks via expo-task-manager) and @react-native-community/geolocation / react-native-geolocation-service. What stays real: permission (the device or the Permissions tool decides; a denied permission still fails, and approximate location snaps positions to ~3 km), native map views' own blue dot, native SDKs, and the backend. The override survives reloads. Every action returns the same state object as getState: { active, simulating, override:{mode:'real'|'fixed'|'route', place:{latitude,longitude,label}, accuracy, jitter, route:{name,id,points,speed,loop,gaps}|null, playing, signal, services, interval}, current:{fix:{latitude,longitude,altitude,accuracy,heading,speed}|null, reason:'real'|'signal'|'gap'|'services'|null, precision:'full'|'reduced'}, permission:{status:'granted'|'denied'|'undetermined', background, precision}|null, position:{latitude,longitude}|null, progress:{along,length,finished,leg,etaMs}|null, watches:[{library,id,kind,updates}], tasks:[{name,kind:'geofencing'|'locationUpdates',intercepted,runs,regions:[{identifier,label,latitude,longitude,radius,state:'unknown'|'inside'|'outside',distance}]}], log:[{at,kind,library,text,detail}], libraries, places, routes, builtInPlaces, presets, settings:{persist,showChip} }. Distances are meters, speeds meters per second.",
|
|
3047
|
+
"unavailableWhen": "Needs @buoy-gg/location installed (FloatingDevTools auto-discovers it) and a supported location library in the app. Pro only. In release builds the libraries are patched when the Location tool is first opened rather than at app start.",
|
|
3048
|
+
"actions": [
|
|
3049
|
+
{
|
|
3050
|
+
"action": "getState",
|
|
3051
|
+
"summary": "Read where the app thinks it is, its live location watches, its geofence regions and recent location events.",
|
|
3052
|
+
"description": "Returns the state object described in the tool summary. active false means the app gets the device's real location. current.fix is what the app receives right now; null with a reason means updates are stopped. tasks lists geofencing regions with inside/outside state and the distance from the current position, which is where to find a region identifier for enterRegion. places and routes are the app's own named places and routes; builtInPlaces and presets are always available.",
|
|
3053
|
+
"effect": "read",
|
|
3054
|
+
"release": "works",
|
|
3055
|
+
"params": {
|
|
3056
|
+
"type": "object",
|
|
3057
|
+
"properties": {},
|
|
3058
|
+
"additionalProperties": false
|
|
3059
|
+
}
|
|
3060
|
+
},
|
|
3061
|
+
{
|
|
3062
|
+
"action": "setLocation",
|
|
3063
|
+
"summary": "Pin the app to one place: `latitude` + `longitude` (with an optional `label`), a `query` (\"lat, lon\" or a map link), or a `place` name.",
|
|
3064
|
+
"description": "Stops any route. `place` matches the app's registered places first, then built-ins (Apple Park, Times Square, Trafalgar Square, Eiffel Tower, Shibuya Crossing, Sydney Opera House, Null Island at 0,0). Live watches get the new position within one update interval, and geofence tasks fire for any region the move crosses. Throws with the list of place names when `place` doesn't match.",
|
|
3065
|
+
"effect": "write",
|
|
3066
|
+
"release": "works",
|
|
3067
|
+
"params": {
|
|
3068
|
+
"type": "object",
|
|
3069
|
+
"properties": {
|
|
3070
|
+
"latitude": {
|
|
3071
|
+
"type": "number",
|
|
3072
|
+
"description": "Latitude, -90 to 90."
|
|
3073
|
+
},
|
|
3074
|
+
"longitude": {
|
|
3075
|
+
"type": "number",
|
|
3076
|
+
"description": "Longitude, -180 to 180."
|
|
3077
|
+
},
|
|
3078
|
+
"label": {
|
|
3079
|
+
"type": "string",
|
|
3080
|
+
"description": "Name to show for the pinned place."
|
|
3081
|
+
},
|
|
3082
|
+
"query": {
|
|
3083
|
+
"type": "string",
|
|
3084
|
+
"description": "\"37.3349, -122.0090\" or a Google/Apple Maps link."
|
|
3085
|
+
},
|
|
3086
|
+
"place": {
|
|
3087
|
+
"type": "string",
|
|
3088
|
+
"description": "A place name from getState places or builtInPlaces."
|
|
3089
|
+
}
|
|
3090
|
+
},
|
|
3091
|
+
"additionalProperties": false
|
|
3092
|
+
}
|
|
3093
|
+
},
|
|
3094
|
+
{
|
|
3095
|
+
"action": "startRoute",
|
|
3096
|
+
"summary": "Move the app along `points` (optionally `loop`), a preset or app `route` (presets head north unless `heading` is set), or in a straight line `to` a place, at `speed` m/s.",
|
|
3097
|
+
"description": "Presets start where the app is now: walk (1 km), run (3 km), drive (4 km with turns), highway (30 km), tunnel (3 km with a 600 m stretch of no signal), loop (400 m square, repeats). Heading and speed in each update follow the route; a finished route parks at its last point. Geofence tasks fire as the route crosses regions, so a route that passes a store is how to test an enter then exit.",
|
|
3098
|
+
"effect": "write",
|
|
3099
|
+
"release": "works",
|
|
3100
|
+
"params": {
|
|
3101
|
+
"type": "object",
|
|
3102
|
+
"properties": {
|
|
3103
|
+
"points": {
|
|
3104
|
+
"type": "array",
|
|
3105
|
+
"items": {
|
|
3106
|
+
"type": "object",
|
|
3107
|
+
"properties": {
|
|
3108
|
+
"latitude": {
|
|
3109
|
+
"type": "number"
|
|
3110
|
+
},
|
|
3111
|
+
"longitude": {
|
|
3112
|
+
"type": "number"
|
|
3113
|
+
}
|
|
3114
|
+
},
|
|
3115
|
+
"required": [
|
|
3116
|
+
"latitude",
|
|
3117
|
+
"longitude"
|
|
3118
|
+
],
|
|
3119
|
+
"additionalProperties": false
|
|
3120
|
+
},
|
|
3121
|
+
"minItems": 2,
|
|
3122
|
+
"description": "Two or more points to travel through."
|
|
3123
|
+
},
|
|
3124
|
+
"route": {
|
|
3125
|
+
"type": "string",
|
|
3126
|
+
"description": "A preset id (walk, run, drive, highway, tunnel, loop) or an app route id from getState routes."
|
|
3127
|
+
},
|
|
3128
|
+
"to": {
|
|
3129
|
+
"type": "string",
|
|
3130
|
+
"description": "Travel in a straight line to this place name or \"lat, lon\"."
|
|
3131
|
+
},
|
|
3132
|
+
"speed": {
|
|
3133
|
+
"type": "number",
|
|
3134
|
+
"description": "Meters per second: 1.4 walking, 4 cycling, 13.4 city driving, 30 highway."
|
|
3135
|
+
},
|
|
3136
|
+
"heading": {
|
|
3137
|
+
"type": "number",
|
|
3138
|
+
"description": "For a preset: direction to head, degrees from north. Default 0 (north)."
|
|
3139
|
+
},
|
|
3140
|
+
"loop": {
|
|
3141
|
+
"type": "boolean",
|
|
3142
|
+
"description": "With points: start over after the last point."
|
|
3143
|
+
}
|
|
3144
|
+
},
|
|
3145
|
+
"additionalProperties": false
|
|
3146
|
+
}
|
|
3147
|
+
},
|
|
3148
|
+
{
|
|
3149
|
+
"action": "pauseRoute",
|
|
3150
|
+
"summary": "Stop moving along the route; the app keeps the current position.",
|
|
3151
|
+
"description": "Updates stop changing until resumeRoute. No-op when no route is playing.",
|
|
3152
|
+
"effect": "write",
|
|
3153
|
+
"release": "works",
|
|
3154
|
+
"params": {
|
|
3155
|
+
"type": "object",
|
|
3156
|
+
"properties": {},
|
|
3157
|
+
"additionalProperties": false
|
|
3158
|
+
}
|
|
3159
|
+
},
|
|
3160
|
+
{
|
|
3161
|
+
"action": "resumeRoute",
|
|
3162
|
+
"summary": "Continue a paused route, or play a finished one again from the start.",
|
|
3163
|
+
"description": "No-op when no route is loaded.",
|
|
3164
|
+
"effect": "write",
|
|
3165
|
+
"release": "works",
|
|
3166
|
+
"params": {
|
|
3167
|
+
"type": "object",
|
|
3168
|
+
"properties": {},
|
|
3169
|
+
"additionalProperties": false
|
|
3170
|
+
}
|
|
3171
|
+
},
|
|
3172
|
+
{
|
|
3173
|
+
"action": "stopRoute",
|
|
3174
|
+
"summary": "End the route and keep the app pinned where it got to.",
|
|
3175
|
+
"description": "Switches from the route to a fixed position at the route's current point, including inside a dead zone. No-op when no route is loaded.",
|
|
3176
|
+
"effect": "write",
|
|
3177
|
+
"release": "works",
|
|
3178
|
+
"params": {
|
|
3179
|
+
"type": "object",
|
|
3180
|
+
"properties": {},
|
|
3181
|
+
"additionalProperties": false
|
|
3182
|
+
}
|
|
3183
|
+
},
|
|
3184
|
+
{
|
|
3185
|
+
"action": "seekRoute",
|
|
3186
|
+
"summary": "Jump to a point along the route by `meters` from the start or `fraction` (0 to 1).",
|
|
3187
|
+
"description": "Keeps playing or paused as it was. Throws when no route is loaded.",
|
|
3188
|
+
"effect": "write",
|
|
3189
|
+
"release": "works",
|
|
3190
|
+
"params": {
|
|
3191
|
+
"type": "object",
|
|
3192
|
+
"properties": {
|
|
3193
|
+
"meters": {
|
|
3194
|
+
"type": "number",
|
|
3195
|
+
"description": "Meters from the route's start."
|
|
3196
|
+
},
|
|
3197
|
+
"fraction": {
|
|
3198
|
+
"type": "number",
|
|
3199
|
+
"description": "0 = start, 1 = end."
|
|
3200
|
+
}
|
|
3201
|
+
},
|
|
3202
|
+
"additionalProperties": false
|
|
3203
|
+
}
|
|
3204
|
+
},
|
|
3205
|
+
{
|
|
3206
|
+
"action": "setSpeed",
|
|
3207
|
+
"summary": "Change the route's speed to `speed` meters per second without losing progress.",
|
|
3208
|
+
"description": "Throws when no route is loaded or speed is not positive.",
|
|
3209
|
+
"effect": "write",
|
|
3210
|
+
"release": "works",
|
|
3211
|
+
"params": {
|
|
3212
|
+
"type": "object",
|
|
3213
|
+
"properties": {
|
|
3214
|
+
"speed": {
|
|
3215
|
+
"type": "number",
|
|
3216
|
+
"description": "Meters per second."
|
|
3217
|
+
}
|
|
3218
|
+
},
|
|
3219
|
+
"additionalProperties": false,
|
|
3220
|
+
"required": [
|
|
3221
|
+
"speed"
|
|
3222
|
+
]
|
|
3223
|
+
}
|
|
3224
|
+
},
|
|
3225
|
+
{
|
|
3226
|
+
"action": "enterRegion",
|
|
3227
|
+
"summary": "Move just inside the geofence region `identifier`; the app's geofencing task runs with an enter event.",
|
|
3228
|
+
"description": "Pins the position at the region's center. Region identifiers come from getState tasks[].regions. Pass `task` when two geofencing tasks use the same identifier. Throws when no monitored region has that identifier.",
|
|
3229
|
+
"effect": "write",
|
|
3230
|
+
"release": "works",
|
|
3231
|
+
"params": {
|
|
3232
|
+
"type": "object",
|
|
3233
|
+
"properties": {
|
|
3234
|
+
"identifier": {
|
|
3235
|
+
"type": "string",
|
|
3236
|
+
"description": "The region identifier."
|
|
3237
|
+
},
|
|
3238
|
+
"task": {
|
|
3239
|
+
"type": "string",
|
|
3240
|
+
"description": "The geofencing task name, when identifiers repeat."
|
|
3241
|
+
}
|
|
3242
|
+
},
|
|
3243
|
+
"additionalProperties": false,
|
|
3244
|
+
"required": [
|
|
3245
|
+
"identifier"
|
|
3246
|
+
]
|
|
3247
|
+
}
|
|
3248
|
+
},
|
|
3249
|
+
{
|
|
3250
|
+
"action": "exitRegion",
|
|
3251
|
+
"summary": "Move just outside the geofence region `identifier`; the app's geofencing task runs with an exit event.",
|
|
3252
|
+
"description": "Pins the position past the region's edge on the side of the current position. Same rules as enterRegion.",
|
|
3253
|
+
"effect": "write",
|
|
3254
|
+
"release": "works",
|
|
3255
|
+
"params": {
|
|
3256
|
+
"type": "object",
|
|
3257
|
+
"properties": {
|
|
3258
|
+
"identifier": {
|
|
3259
|
+
"type": "string",
|
|
3260
|
+
"description": "The region identifier."
|
|
3261
|
+
},
|
|
3262
|
+
"task": {
|
|
3263
|
+
"type": "string",
|
|
3264
|
+
"description": "The geofencing task name, when identifiers repeat."
|
|
3265
|
+
}
|
|
3266
|
+
},
|
|
3267
|
+
"additionalProperties": false,
|
|
3268
|
+
"required": [
|
|
3269
|
+
"identifier"
|
|
3270
|
+
]
|
|
3271
|
+
}
|
|
3272
|
+
},
|
|
3273
|
+
{
|
|
3274
|
+
"action": "setConditions",
|
|
3275
|
+
"summary": "Set the signal quality in one step: `conditions` good, weak, no-signal or off.",
|
|
3276
|
+
"description": "good: \u00b15 m fixes, no drift. weak: \u00b165 m fixes that wander by up to 50 m (indoors, city canyons). no-signal: same as setSignal on=false. off: same as setServices on=false. no-signal and off keep the current accuracy and drift, so switching back restores them. weak only changes simulated positions.",
|
|
3277
|
+
"effect": "write",
|
|
3278
|
+
"release": "works",
|
|
3279
|
+
"params": {
|
|
3280
|
+
"type": "object",
|
|
3281
|
+
"properties": {
|
|
3282
|
+
"conditions": {
|
|
3283
|
+
"type": "string",
|
|
3284
|
+
"enum": [
|
|
3285
|
+
"good",
|
|
3286
|
+
"weak",
|
|
3287
|
+
"no-signal",
|
|
3288
|
+
"off"
|
|
3289
|
+
],
|
|
3290
|
+
"description": "Which signal conditions the app should see."
|
|
3291
|
+
}
|
|
3292
|
+
},
|
|
3293
|
+
"additionalProperties": false,
|
|
3294
|
+
"required": [
|
|
3295
|
+
"conditions"
|
|
3296
|
+
]
|
|
3297
|
+
}
|
|
3298
|
+
},
|
|
3299
|
+
{
|
|
3300
|
+
"action": "setSignal",
|
|
3301
|
+
"summary": "Turn the GPS signal off (`on` false) or back on.",
|
|
3302
|
+
"description": "Off: live watches stop receiving updates and position requests fail with a location-unavailable error after about a second. The position mode stays as it was.",
|
|
3303
|
+
"effect": "write",
|
|
3304
|
+
"release": "works",
|
|
3305
|
+
"params": {
|
|
3306
|
+
"type": "object",
|
|
3307
|
+
"properties": {
|
|
3308
|
+
"on": {
|
|
3309
|
+
"type": "boolean",
|
|
3310
|
+
"description": "true = signal, false = no signal."
|
|
3311
|
+
}
|
|
3312
|
+
},
|
|
3313
|
+
"additionalProperties": false,
|
|
3314
|
+
"required": [
|
|
3315
|
+
"on"
|
|
3316
|
+
]
|
|
3317
|
+
}
|
|
3318
|
+
},
|
|
3319
|
+
{
|
|
3320
|
+
"action": "setServices",
|
|
3321
|
+
"summary": "Report location services as disabled (`on` false) or enabled.",
|
|
3322
|
+
"description": "Off: hasServicesEnabledAsync returns false, provider status reports locationServicesEnabled false, requests fail with a services-disabled error, and live watches get one error event.",
|
|
3323
|
+
"effect": "write",
|
|
3324
|
+
"release": "works",
|
|
3325
|
+
"params": {
|
|
3326
|
+
"type": "object",
|
|
3327
|
+
"properties": {
|
|
3328
|
+
"on": {
|
|
3329
|
+
"type": "boolean",
|
|
3330
|
+
"description": "true = enabled, false = disabled."
|
|
3331
|
+
}
|
|
3332
|
+
},
|
|
3333
|
+
"additionalProperties": false,
|
|
3334
|
+
"required": [
|
|
3335
|
+
"on"
|
|
3336
|
+
]
|
|
3337
|
+
}
|
|
3338
|
+
},
|
|
3339
|
+
{
|
|
3340
|
+
"action": "tune",
|
|
3341
|
+
"summary": "Set the reported `accuracy`, GPS `jitter` (drift), `altitude`, pinned `heading`, or update `interval`.",
|
|
3342
|
+
"description": "All fields optional; send only what changes. jitter moves each update up to that many meters from the true point. interval is how often live watches get an update, in ms (default 1000).",
|
|
3343
|
+
"effect": "write",
|
|
3344
|
+
"release": "works",
|
|
3345
|
+
"params": {
|
|
3346
|
+
"type": "object",
|
|
3347
|
+
"properties": {
|
|
3348
|
+
"accuracy": {
|
|
3349
|
+
"type": "number",
|
|
3350
|
+
"description": "Reported accuracy radius in meters."
|
|
3351
|
+
},
|
|
3352
|
+
"jitter": {
|
|
3353
|
+
"type": "number",
|
|
3354
|
+
"description": "Drift in meters; 0 turns it off."
|
|
3355
|
+
},
|
|
3356
|
+
"altitude": {
|
|
3357
|
+
"type": "number",
|
|
3358
|
+
"description": "Meters above sea level."
|
|
3359
|
+
},
|
|
3360
|
+
"heading": {
|
|
3361
|
+
"type": "number",
|
|
3362
|
+
"description": "Heading reported while pinned, degrees from north."
|
|
3363
|
+
},
|
|
3364
|
+
"interval": {
|
|
3365
|
+
"type": "number",
|
|
3366
|
+
"description": "Milliseconds between updates, 100 to 60000."
|
|
3367
|
+
}
|
|
3368
|
+
},
|
|
3369
|
+
"additionalProperties": false
|
|
3370
|
+
}
|
|
3371
|
+
},
|
|
3372
|
+
{
|
|
3373
|
+
"action": "useRealLocation",
|
|
3374
|
+
"summary": "Go back to the device's real position but keep the signal and services switches.",
|
|
3375
|
+
"description": "Use reset to end every override at once.",
|
|
3376
|
+
"effect": "write",
|
|
3377
|
+
"release": "works",
|
|
3378
|
+
"params": {
|
|
3379
|
+
"type": "object",
|
|
3380
|
+
"properties": {},
|
|
3381
|
+
"additionalProperties": false
|
|
3382
|
+
}
|
|
3383
|
+
},
|
|
3384
|
+
{
|
|
3385
|
+
"action": "reset",
|
|
3386
|
+
"summary": "End every location override; the app gets the device's real location again.",
|
|
3387
|
+
"description": "Watches and geofence or background tasks the app started under the override are started natively with the app's own options.",
|
|
3388
|
+
"effect": "write",
|
|
3389
|
+
"release": "works",
|
|
3390
|
+
"params": {
|
|
3391
|
+
"type": "object",
|
|
3392
|
+
"properties": {},
|
|
3393
|
+
"additionalProperties": false
|
|
3394
|
+
}
|
|
3395
|
+
},
|
|
3396
|
+
{
|
|
3397
|
+
"action": "updateSettings",
|
|
3398
|
+
"summary": "Set `persist` (keep the override across reloads) and `showChip` (floating status while overridden).",
|
|
3399
|
+
"description": "Both default to true.",
|
|
3400
|
+
"effect": "write",
|
|
3401
|
+
"release": "works",
|
|
3402
|
+
"params": {
|
|
3403
|
+
"type": "object",
|
|
3404
|
+
"properties": {
|
|
3405
|
+
"persist": {
|
|
3406
|
+
"type": "boolean",
|
|
3407
|
+
"description": "Keep the override across reloads."
|
|
3408
|
+
},
|
|
3409
|
+
"showChip": {
|
|
3410
|
+
"type": "boolean",
|
|
3411
|
+
"description": "Show the floating location chip while overridden."
|
|
3412
|
+
}
|
|
3413
|
+
},
|
|
3414
|
+
"additionalProperties": false
|
|
3415
|
+
}
|
|
3416
|
+
},
|
|
3417
|
+
{
|
|
3418
|
+
"action": "clearLog",
|
|
3419
|
+
"summary": "Clear the recent location events shown in getState log.",
|
|
3420
|
+
"description": "Does not change the override.",
|
|
3421
|
+
"effect": "write",
|
|
3422
|
+
"release": "works",
|
|
3423
|
+
"params": {
|
|
3424
|
+
"type": "object",
|
|
3425
|
+
"properties": {},
|
|
3426
|
+
"additionalProperties": false
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
3429
|
+
]
|
|
3430
|
+
},
|
|
3431
|
+
{
|
|
3432
|
+
"toolId": "permissions",
|
|
3433
|
+
"title": "Permissions",
|
|
3434
|
+
"summary": "Override what the app sees when it asks for a permission, without touching the device: make location, camera, notifications, photos, contacts and others read as not asked, allowed, limited (approximate location, selected photos or contacts, provisional notifications), denied, blocked (Android 'don't ask again') or restricted (iOS parental controls). Reach for it to test the permission flows on the client: the pre-prompt screen, what happens after 'Don't Allow', the 'turn it on in Settings' path, approximate location, and a returning user who already refused. Covers Expo modules (expo-location, expo-camera, expo-notifications, expo-image-picker, expo-media-library, expo-contacts, expo-calendar, expo-tracking-transparency, expo-audio, expo-sensors, expo-maps), React Native's PermissionsAndroid and react-native-permissions. While a permission is overridden the real system prompt never shows: a request from 'not asked' is answered by the requestAnswer setting (ask shows Buoy's own prompt on the device and waits for a tap; allow, limited and deny answer at once) and moves the state the way the OS would (iOS: one refusal is final; Android: a second refusal blocks). Linking.openSettings and app-settings: URLs open Buoy's stand-in for Settings. A change sends the app background then active, like a return from Settings, so screens that re-check on foreground update; hooks that read only on mount update on their next read. With enforce on, calls that need a refused permission (current or last known position, launching the camera, media library reads) fail with the native module's error; approximate location coarsens positions. Native code that checks the OS itself still sees the real state, and requestReal shows the real system prompt once so the OS can grant what the override pretends. Every action returns the same state object as getState: { platform, host:{isSimulator,deviceName,bundleId}, libraries:[string], permissions:[{ id, label, states:[state], override, real, effective, sources:[string], calls, warning, canRequestReal }], overriddenCount, settings:{ requestAnswer, notifyApp, interceptSettings, enforce, persist, showChip }, log:[{ at, source, method, permission, kind, real, returned, overridden, answer?, error? }] }.",
|
|
3435
|
+
"unavailableWhen": "Needs @buoy-gg/permissions installed (FloatingDevTools auto-discovers it). Rows appear only for permission libraries the app has. In release builds the overrides install when the Permissions tool is first opened rather than at app start.",
|
|
3436
|
+
"actions": [
|
|
3437
|
+
{
|
|
3438
|
+
"action": "getState",
|
|
3439
|
+
"summary": "Read each permission the app can reach: the override, what the OS says, which libraries asked, and the recent permission calls.",
|
|
3440
|
+
"description": "Returns the state object described in the tool summary, after re-reading the OS state from each Expo module. `effective` is what the app sees now. `states` lists the states this platform allows for that permission. `log` is newest first and shows what each call returned and whether the override answered it.",
|
|
3441
|
+
"effect": "read",
|
|
3442
|
+
"release": "works",
|
|
3443
|
+
"params": {
|
|
3444
|
+
"type": "object",
|
|
3445
|
+
"properties": {},
|
|
3446
|
+
"additionalProperties": false
|
|
3447
|
+
}
|
|
3448
|
+
},
|
|
3449
|
+
{
|
|
3450
|
+
"action": "setOverride",
|
|
3451
|
+
"summary": "Make `permission` read as `state` for the app, without changing the OS.",
|
|
3452
|
+
"description": "`state` is one of undetermined, granted, limited, denied, blocked, restricted, and must be in that permission's `states` (limited only exists for location, photos, contacts, and iOS notifications; iOS has no blocked, so blocked becomes denied; Android has no restricted). Throws naming the allowed states otherwise. The app sees it on its next permission read; with notifyApp on, the app also gets a background \u2192 active round trip right away. Returns the new state.",
|
|
3453
|
+
"effect": "write",
|
|
3454
|
+
"release": "works",
|
|
3455
|
+
"undo": {
|
|
3456
|
+
"action": "clearOverride",
|
|
3457
|
+
"note": "clearOverride with the same permission returns it to the OS answer. If it was already overridden before this call, read getState first and set the old state again instead."
|
|
3458
|
+
},
|
|
3459
|
+
"params": {
|
|
3460
|
+
"type": "object",
|
|
3461
|
+
"properties": {
|
|
3462
|
+
"permission": {
|
|
3463
|
+
"type": "string",
|
|
3464
|
+
"description": "Permission id: location, locationBackground, camera, microphone, notifications, photos, contacts, calendar, reminders (iOS), tracking (iOS), motion, bluetooth, or android:<android.permission.NAME>."
|
|
3465
|
+
},
|
|
3466
|
+
"state": {
|
|
3467
|
+
"type": "string",
|
|
3468
|
+
"enum": [
|
|
3469
|
+
"undetermined",
|
|
3470
|
+
"granted",
|
|
3471
|
+
"limited",
|
|
3472
|
+
"denied",
|
|
3473
|
+
"blocked",
|
|
3474
|
+
"restricted"
|
|
3475
|
+
],
|
|
3476
|
+
"description": "What the app sees for this permission."
|
|
3477
|
+
}
|
|
3478
|
+
},
|
|
3479
|
+
"required": [
|
|
3480
|
+
"permission",
|
|
3481
|
+
"state"
|
|
3482
|
+
],
|
|
3483
|
+
"additionalProperties": false
|
|
3484
|
+
}
|
|
3485
|
+
},
|
|
3486
|
+
{
|
|
3487
|
+
"action": "clearOverride",
|
|
3488
|
+
"summary": "Stop overriding `permission`; the app sees the OS answer again.",
|
|
3489
|
+
"description": "Returns the new state.",
|
|
3490
|
+
"effect": "write",
|
|
3491
|
+
"release": "works",
|
|
3492
|
+
"params": {
|
|
3493
|
+
"type": "object",
|
|
3494
|
+
"properties": {
|
|
3495
|
+
"permission": {
|
|
3496
|
+
"type": "string",
|
|
3497
|
+
"description": "Permission id: location, locationBackground, camera, microphone, notifications, photos, contacts, calendar, reminders (iOS), tracking (iOS), motion, bluetooth, or android:<android.permission.NAME>."
|
|
3498
|
+
}
|
|
3499
|
+
},
|
|
3500
|
+
"required": [
|
|
3501
|
+
"permission"
|
|
3502
|
+
],
|
|
3503
|
+
"additionalProperties": false
|
|
3504
|
+
}
|
|
3505
|
+
},
|
|
3506
|
+
{
|
|
3507
|
+
"action": "resetAll",
|
|
3508
|
+
"summary": "Stop every override.",
|
|
3509
|
+
"description": "Every permission reads the OS answer again. Returns the new state.",
|
|
3510
|
+
"effect": "write",
|
|
3511
|
+
"release": "works",
|
|
3512
|
+
"params": {
|
|
3513
|
+
"type": "object",
|
|
3514
|
+
"properties": {},
|
|
3515
|
+
"additionalProperties": false
|
|
3516
|
+
}
|
|
3517
|
+
},
|
|
3518
|
+
{
|
|
3519
|
+
"action": "requestReal",
|
|
3520
|
+
"summary": "Show the real system prompt for `permission`, ignoring the override, so the OS can grant it.",
|
|
3521
|
+
"description": "Use when the override says granted or limited but `real` is undetermined, so native calls (a position, the camera) would fail. The system only prompts from not asked; after that it answers without a prompt. Someone has to tap the real prompt on the device. Returns the new state with the updated `real`.",
|
|
3522
|
+
"effect": "write",
|
|
3523
|
+
"release": "works",
|
|
3524
|
+
"params": {
|
|
3525
|
+
"type": "object",
|
|
3526
|
+
"properties": {
|
|
3527
|
+
"permission": {
|
|
3528
|
+
"type": "string",
|
|
3529
|
+
"description": "Permission id: location, locationBackground, camera, microphone, notifications, photos, contacts, calendar, reminders (iOS), tracking (iOS), motion, bluetooth, or android:<android.permission.NAME>."
|
|
3530
|
+
}
|
|
3531
|
+
},
|
|
3532
|
+
"required": [
|
|
3533
|
+
"permission"
|
|
3534
|
+
],
|
|
3535
|
+
"additionalProperties": false
|
|
3536
|
+
}
|
|
3537
|
+
},
|
|
3538
|
+
{
|
|
3539
|
+
"action": "updateSettings",
|
|
3540
|
+
"summary": "Change how overrides behave: requestAnswer (how requests are answered), notifyApp (return from Settings on change), interceptSettings (the Settings stand-in), enforce, showChip and persist.",
|
|
3541
|
+
"description": "Pass only the fields to change. requestAnswer 'ask' shows Buoy's prompt on the device and waits for a person to tap; use allow, limited or deny when nobody is at the device. Returns the new state.",
|
|
3542
|
+
"effect": "write",
|
|
3543
|
+
"release": "works",
|
|
3544
|
+
"params": {
|
|
3545
|
+
"type": "object",
|
|
3546
|
+
"properties": {
|
|
3547
|
+
"requestAnswer": {
|
|
3548
|
+
"type": "string",
|
|
3549
|
+
"enum": [
|
|
3550
|
+
"ask",
|
|
3551
|
+
"allow",
|
|
3552
|
+
"limited",
|
|
3553
|
+
"deny"
|
|
3554
|
+
],
|
|
3555
|
+
"description": "How a request from 'not asked' (or Android 'denied') is answered while overridden."
|
|
3556
|
+
},
|
|
3557
|
+
"notifyApp": {
|
|
3558
|
+
"type": "boolean",
|
|
3559
|
+
"description": "Send background \u2192 active after each change, like a return from Settings."
|
|
3560
|
+
},
|
|
3561
|
+
"interceptSettings": {
|
|
3562
|
+
"type": "boolean",
|
|
3563
|
+
"description": "Show Buoy's stand-in when the app opens its Settings page while overridden."
|
|
3564
|
+
},
|
|
3565
|
+
"enforce": {
|
|
3566
|
+
"type": "boolean",
|
|
3567
|
+
"description": "Fail calls that need a refused permission with the native error, and coarsen approximate positions."
|
|
3568
|
+
},
|
|
3569
|
+
"persist": {
|
|
3570
|
+
"type": "boolean",
|
|
3571
|
+
"description": "Keep overrides across reloads."
|
|
3572
|
+
},
|
|
3573
|
+
"showChip": {
|
|
3574
|
+
"type": "boolean",
|
|
3575
|
+
"description": "Show the floating chip while any override is on."
|
|
3576
|
+
}
|
|
3577
|
+
},
|
|
3578
|
+
"additionalProperties": false
|
|
3579
|
+
}
|
|
3580
|
+
},
|
|
3581
|
+
{
|
|
3582
|
+
"action": "simulateReturnFromSettings",
|
|
3583
|
+
"summary": "Send the app background \u2192 active, as a real return from Settings does.",
|
|
3584
|
+
"description": "Screens that re-check permissions when the app comes to the foreground run their check. Returns the state.",
|
|
3585
|
+
"effect": "write",
|
|
3586
|
+
"release": "works",
|
|
3587
|
+
"params": {
|
|
3588
|
+
"type": "object",
|
|
3589
|
+
"properties": {},
|
|
3590
|
+
"additionalProperties": false
|
|
3591
|
+
}
|
|
3592
|
+
},
|
|
3593
|
+
{
|
|
3594
|
+
"action": "clearLog",
|
|
3595
|
+
"summary": "Clear the recent permission calls and their counts.",
|
|
3596
|
+
"description": "Returns the state.",
|
|
3597
|
+
"effect": "write",
|
|
3598
|
+
"release": "works",
|
|
3599
|
+
"params": {
|
|
3600
|
+
"type": "object",
|
|
3601
|
+
"properties": {},
|
|
3602
|
+
"additionalProperties": false
|
|
3603
|
+
}
|
|
3604
|
+
}
|
|
3605
|
+
]
|
|
3606
|
+
},
|
|
2767
3607
|
{
|
|
2768
3608
|
"toolId": "storage",
|
|
2769
3609
|
"title": "Storage",
|
|
2770
|
-
"summary": "Read and write the app's persisted state across all three backends
|
|
3610
|
+
"summary": "Read and write the app's persisted state across all three backends \u2014 AsyncStorage, every registered MMKV instance, and registered Expo SecureStore keys \u2014 plus a recorded timeline of storage writes with per-event undo/jump. Reach for this first when a value is wrong, stale, missing, or only broken after a restart/upgrade: the bad value is usually sitting in storage. Buoy's own devtool keys (@react_buoy*, @buoy*, buoy-*) are stripped from every read path, so results are app data only. Nothing here is __DEV__-gated \u2014 every action really runs in a release build. When a saved value looks wrong, compare it with getRequiredKeys: a key written by an older app version shows up there with the wrong type (a bare \"1856\" where the app expects an object). The storage write timeline is this tool's getSnapshot (newest events, each with its key, value and the value it replaced) \u2014 read it for \"what changed in storage?\" and to find the event to undo. Do not use the Events tool for that: it records a source only after setEnabledSources, so earlier writes are not there.",
|
|
2771
3611
|
"actions": [
|
|
3612
|
+
{
|
|
3613
|
+
"action": "getSnapshot",
|
|
3614
|
+
"summary": "Read the storage write timeline: the app's recent AsyncStorage and MMKV writes, newest first, each with its key, the value written and the value it replaced. Answers \"what changed in storage?\" and finds the event to undo.",
|
|
3615
|
+
"description": "Returns `{ events: [{ id, at, action, storage, key, value, prevValue }], total, returned }` newest first. Values are cut to 300 characters (read the key for the full value). `id` is what timeTravel.undo and timeTravel.jump take. Recording starts when something first watches the storage tool, so writes before that are not listed. Pass `limit` for how many writes (default 20) and `key` to list only writes whose key contains that text.",
|
|
3616
|
+
"params": {
|
|
3617
|
+
"type": "object",
|
|
3618
|
+
"properties": {
|
|
3619
|
+
"limit": {
|
|
3620
|
+
"type": "number",
|
|
3621
|
+
"description": "Most-recent N writes. Default 20."
|
|
3622
|
+
},
|
|
3623
|
+
"key": {
|
|
3624
|
+
"type": "string",
|
|
3625
|
+
"description": "Only writes whose key contains this text."
|
|
3626
|
+
}
|
|
3627
|
+
},
|
|
3628
|
+
"additionalProperties": false
|
|
3629
|
+
},
|
|
3630
|
+
"effect": "read",
|
|
3631
|
+
"release": "works"
|
|
3632
|
+
},
|
|
2772
3633
|
{
|
|
2773
3634
|
"action": "getRequiredKeys",
|
|
2774
3635
|
"summary": "List the storage keys the app declares as required, with their expected types and backends.",
|
|
@@ -2807,7 +3668,7 @@
|
|
|
2807
3668
|
"items": {
|
|
2808
3669
|
"type": "string"
|
|
2809
3670
|
},
|
|
2810
|
-
"description": "Keys to read, e.g. [\"session\",\"user.prefs\"]. Required
|
|
3671
|
+
"description": "Keys to read, e.g. [\"session\",\"user.prefs\"]. Required \u2014 the handler reads params.keys with no guard and throws if params is missing."
|
|
2811
3672
|
}
|
|
2812
3673
|
},
|
|
2813
3674
|
"required": [
|
|
@@ -2817,7 +3678,7 @@
|
|
|
2817
3678
|
},
|
|
2818
3679
|
"effect": "read",
|
|
2819
3680
|
"release": "works",
|
|
2820
|
-
"description": "Values are always strings (or null when unset)
|
|
3681
|
+
"description": "Values are always strings (or null when unset) \u2014 JSON.parse them yourself. Works on async-storage v2 and v3 (translated to getMany internally) and always preserves the requested key order. Any requested key that is a Buoy devtool key is dropped from the RESULT entirely, so the returned array can be shorter than `keys`.",
|
|
2821
3682
|
"requires": [
|
|
2822
3683
|
"@react-native-async-storage/async-storage installed in the app"
|
|
2823
3684
|
]
|
|
@@ -2840,7 +3701,7 @@
|
|
|
2840
3701
|
},
|
|
2841
3702
|
"effect": "read",
|
|
2842
3703
|
"release": "works",
|
|
2843
|
-
"description": "Prefer async.multiGet when you want more than one key. Returns null (never an error) for a key that is unset AND for any Buoy devtool key (@react_buoy*, @buoy*, buoy-*) even when that key really is set
|
|
3704
|
+
"description": "Prefer async.multiGet when you want more than one key. Returns null (never an error) for a key that is unset AND for any Buoy devtool key (@react_buoy*, @buoy*, buoy-*) even when that key really is set \u2014 so a null here does not prove the app never wrote it if the key is Buoy-prefixed.",
|
|
2844
3705
|
"requires": [
|
|
2845
3706
|
"@react-native-async-storage/async-storage installed in the app"
|
|
2846
3707
|
]
|
|
@@ -2868,7 +3729,7 @@
|
|
|
2868
3729
|
},
|
|
2869
3730
|
"effect": "write",
|
|
2870
3731
|
"release": "works",
|
|
2871
|
-
"description": "Values are strings only
|
|
3732
|
+
"description": "Values are strings only \u2014 JSON.stringify objects yourself, or the app will read back garbage. Unlike the read paths this is NOT key-filtered: it will happily overwrite Buoy's own @react_buoy*/@buoy*/buoy-* settings keys, so never point it at one. Emits a setItem event (with the old value as prevValue) while a dashboard is subscribed, which makes it undoable via timeTravel.undo. TRAP: if this key is a live store's saved copy (the grounding marks these as \"persists to <key>\", and zustand's listStores reports it as persistName), do NOT write it here. The app keeps that state in memory and only reads the key at startup, so the screen will not change and the store will overwrite you the next time it saves. Use the store's own tool instead \u2014 zustand.setState, redux.dispatch, jotai.setAtom.",
|
|
2872
3733
|
"requires": [
|
|
2873
3734
|
"@react-native-async-storage/async-storage installed in the app"
|
|
2874
3735
|
]
|
|
@@ -2891,7 +3752,7 @@
|
|
|
2891
3752
|
},
|
|
2892
3753
|
"effect": "destructive",
|
|
2893
3754
|
"release": "works",
|
|
2894
|
-
"description": "Permanently removes the key from the device. No confirmation and no result payload
|
|
3755
|
+
"description": "Permanently removes the key from the device. No confirmation and no result payload \u2014 it resolves to undefined whether or not the key existed. Read the value first if you might need it back.",
|
|
2895
3756
|
"requires": [
|
|
2896
3757
|
"@react-native-async-storage/async-storage installed in the app"
|
|
2897
3758
|
]
|
|
@@ -2917,7 +3778,7 @@
|
|
|
2917
3778
|
},
|
|
2918
3779
|
"effect": "destructive",
|
|
2919
3780
|
"release": "works",
|
|
2920
|
-
"description": "Batch form of async.removeItem (translated to removeMany on async-storage v3). Deletes exactly the keys you name
|
|
3781
|
+
"description": "Batch form of async.removeItem (translated to removeMany on async-storage v3). Deletes exactly the keys you name \u2014 it does NOT filter Buoy devtool keys, so do not pass @react_buoy*/@buoy*/buoy-* keys. To wipe app data wholesale use clearAppStorage instead, which protects those.",
|
|
2921
3782
|
"requires": [
|
|
2922
3783
|
"@react-native-async-storage/async-storage installed in the app",
|
|
2923
3784
|
"for undoAction timeTravel.undo: storage capture must be held open at the moment of the write, or no prevPairs event exists to undo"
|
|
@@ -2949,7 +3810,7 @@
|
|
|
2949
3810
|
},
|
|
2950
3811
|
"effect": "write",
|
|
2951
3812
|
"release": "works",
|
|
2952
|
-
"description": "Takes v2-shaped tuples [[key, value], ...] and translates to setMany on async-storage v3. All values must be strings. Same caveat as async.setItem: not key-filtered, so never include a Buoy devtool key. TRAP: if this key is a live store's saved copy (the grounding marks these as \"persists to <key>\", and zustand's listStores reports it as persistName), do NOT write it here. The app keeps that state in memory and only reads the key at startup, so the screen will not change and the store will overwrite you the next time it saves. Use the store's own tool instead
|
|
3813
|
+
"description": "Takes v2-shaped tuples [[key, value], ...] and translates to setMany on async-storage v3. All values must be strings. Same caveat as async.setItem: not key-filtered, so never include a Buoy devtool key. TRAP: if this key is a live store's saved copy (the grounding marks these as \"persists to <key>\", and zustand's listStores reports it as persistName), do NOT write it here. The app keeps that state in memory and only reads the key at startup, so the screen will not change and the store will overwrite you the next time it saves. Use the store's own tool instead \u2014 zustand.setState, redux.dispatch, jotai.setAtom.",
|
|
2953
3814
|
"requires": [
|
|
2954
3815
|
"@react-native-async-storage/async-storage installed in the app",
|
|
2955
3816
|
"for undoAction timeTravel.undo: storage capture must be held open at the moment of the write, or no prevPairs event exists to undo"
|
|
@@ -2965,10 +3826,10 @@
|
|
|
2965
3826
|
},
|
|
2966
3827
|
"effect": "destructive",
|
|
2967
3828
|
"release": "works",
|
|
2968
|
-
"description": "Raw AsyncStorage.clear()
|
|
3829
|
+
"description": "Raw AsyncStorage.clear() \u2014 the nuclear option. It destroys Buoy's own @react_buoy*/@buoy*/buoy-* keys too, resetting the dev tools' own settings along with app data. Almost always the wrong choice: use clearAppStorage, which does the same thing to app data while preserving Buoy's keys. Prefer it unless someone explicitly asked to reset the dev tools as well.",
|
|
2969
3830
|
"requires": [
|
|
2970
3831
|
"@react-native-async-storage/async-storage installed in the app",
|
|
2971
|
-
"for undoAction timeTravel.undo: storage capture must be held open (a storage events read/watch) AT THE MOMENT OF THE WRITE
|
|
3832
|
+
"for undoAction timeTravel.undo: storage capture must be held open (a storage events read/watch) AT THE MOMENT OF THE WRITE \u2014 otherwise no event with prevPairs is recorded and the undo is impossible"
|
|
2972
3833
|
]
|
|
2973
3834
|
},
|
|
2974
3835
|
{
|
|
@@ -2984,7 +3845,7 @@
|
|
|
2984
3845
|
"description": "The safe reset: getAllKeys, drop anything matching @react_buoy*/@buoy*/buoy-*/legacy dev prefixes, removeMany the rest. This is what a 'clear all storage' / 'reset the app's data' request means. Resolves to undefined and reports no count. Use it instead of async.clear.",
|
|
2985
3846
|
"requires": [
|
|
2986
3847
|
"@react-native-async-storage/async-storage installed in the app",
|
|
2987
|
-
"for undoAction timeTravel.undo: storage capture must be held open at the moment of the call
|
|
3848
|
+
"for undoAction timeTravel.undo: storage capture must be held open at the moment of the call \u2014 the wipe goes through removeMany, so with no subscriber there is no multiRemove event and the wipe is unrecoverable"
|
|
2988
3849
|
]
|
|
2989
3850
|
},
|
|
2990
3851
|
{
|
|
@@ -3005,7 +3866,7 @@
|
|
|
3005
3866
|
},
|
|
3006
3867
|
"effect": "read",
|
|
3007
3868
|
"release": "works",
|
|
3008
|
-
"description": "The streamed event timeline replaces any value over 16KB with the marker object {__buoyValueOnDevice:true}; this is the on-demand channel for the real payload (itself capped at 8MB, above which it returns {__buoyTruncated:true}). Call it before timeTravel.undo/jump on an event whose values are still markers
|
|
3869
|
+
"description": "The streamed event timeline replaces any value over 16KB with the marker object {__buoyValueOnDevice:true}; this is the on-demand channel for the real payload (itself capped at 8MB, above which it returns {__buoyTruncated:true}). Call it before timeTravel.undo/jump on an event whose values are still markers \u2014 those actions refuse to replay markers. Never throws: returns {found:false, reason:\"missing id\"} or {found:false, reason:\"unknown id\"}. Event ids look like \"se-1755000000000-42\".",
|
|
3009
3870
|
"armsCapture": true
|
|
3010
3871
|
},
|
|
3011
3872
|
{
|
|
@@ -3018,7 +3879,7 @@
|
|
|
3018
3879
|
},
|
|
3019
3880
|
"effect": "destructive",
|
|
3020
3881
|
"release": "works",
|
|
3021
|
-
"description": "Empties the 500-event ring buffer. No device storage is written or deleted
|
|
3882
|
+
"description": "Empties the 500-event ring buffer. No device storage is written or deleted \u2014 but every event id disappears, so getEventDetail and timeTravel.undo/jump lose all their targets, permanently. Worth knowing: the initial key scan that seeds the timeline with the app's PRE-EXISTING keys runs only once per store lifetime, so after clearEvents the timeline does not re-list existing keys \u2014 it only refills with new writes. Use async.getAllKeys to see current keys instead.",
|
|
3022
3883
|
"armsCapture": true
|
|
3023
3884
|
},
|
|
3024
3885
|
{
|
|
@@ -3039,7 +3900,7 @@
|
|
|
3039
3900
|
},
|
|
3040
3901
|
"effect": "destructive",
|
|
3041
3902
|
"release": "works",
|
|
3042
|
-
"description": "Addresses a single event by id and restores its prevValue/prevPairs (removing the key when it did not exist before). AsyncStorage events only
|
|
3903
|
+
"description": "Addresses a single event by id and restores its prevValue/prevPairs (removing the key when it did not exist before). AsyncStorage events only \u2014 throws \"Time travel supports AsyncStorage events only\" for MMKV events, throws when the event has no captured previous value, and throws when prevValue is still an on-device marker (fetch getEventDetail first). Unlike the in-app UNDO button, failures throw with the real reason instead of silently doing nothing. The restore itself emits a normal storage event, so it is visible and re-undoable.",
|
|
3043
3904
|
"armsCapture": true
|
|
3044
3905
|
},
|
|
3045
3906
|
{
|
|
@@ -3068,7 +3929,7 @@
|
|
|
3068
3929
|
},
|
|
3069
3930
|
"effect": "destructive",
|
|
3070
3931
|
"release": "works",
|
|
3071
|
-
"description": "Replays the captured AsyncStorage timeline up to and including `id`, then reconciles the keys that timeline governs
|
|
3932
|
+
"description": "Replays the captured AsyncStorage timeline up to and including `id`, then reconciles the keys that timeline governs \u2014 writing the replayed values and DELETING keys that only exist later than the target. scope:\"key\" (the default, matching the in-app JUMP button) replays only the target event's own key. scope:\"all\" replays every captured AsyncStorage event, so it can rewrite and delete many unrelated keys at once \u2014 treat that as a bulk data change and confirm before using it. Buoy's own devtool keys are never touched. Throws for an unknown id, for an id not in the chosen timeline, or when any replayed event still holds an on-device value marker (fetch getEventDetail first). Returns {jumped, scope, replayed, of, key}.",
|
|
3072
3933
|
"armsCapture": true
|
|
3073
3934
|
},
|
|
3074
3935
|
{
|
|
@@ -3081,7 +3942,7 @@
|
|
|
3081
3942
|
},
|
|
3082
3943
|
"effect": "read",
|
|
3083
3944
|
"release": "works",
|
|
3084
|
-
"description": "Returns [{id, encrypted, readOnly, entries:[{key, value, valueType}]}] where valueType is string|number|boolean|buffer. One round trip for all instances
|
|
3945
|
+
"description": "Returns [{id, encrypted, readOnly, entries:[{key, value, valueType}]}] where valueType is string|number|boolean|buffer. One round trip for all instances \u2014 the right MMKV read unless you need a single oversized value. Values over 16KB are replaced with {__buoyValueOnDevice:true}; fetch those via mmkv.get. Buoy devtool keys are stripped. Returns [] when the app never called registerMMKVInstance(); an instance that throws on read comes back with entries: [] rather than failing the call.",
|
|
3085
3946
|
"requires": [
|
|
3086
3947
|
"registerMMKVInstance(...) called by the app (react-native-mmkv)"
|
|
3087
3948
|
]
|
|
@@ -3109,7 +3970,7 @@
|
|
|
3109
3970
|
},
|
|
3110
3971
|
"effect": "read",
|
|
3111
3972
|
"release": "works",
|
|
3112
|
-
"description": "The size-guarded single-key channel for values mmkv.snapshot omitted. Returns {found:true, instanceId, key, value, valueType}
|
|
3973
|
+
"description": "The size-guarded single-key channel for values mmkv.snapshot omitted. Returns {found:true, instanceId, key, value, valueType} \u2014 or, without throwing, {found:false, reason:\"missing instanceId/key\"} or {found:false, reason:\"unknown instance\"}. valueType is auto-detected as string|number|boolean|buffer. instanceId is the id the app registered, e.g. \"mmkv.default\" or \"user-prefs\" \u2014 get the exact ids from mmkv.snapshot.",
|
|
3113
3974
|
"requires": [
|
|
3114
3975
|
"registerMMKVInstance(...) called by the app (react-native-mmkv)"
|
|
3115
3976
|
]
|
|
@@ -3134,7 +3995,7 @@
|
|
|
3134
3995
|
"number",
|
|
3135
3996
|
"boolean"
|
|
3136
3997
|
],
|
|
3137
|
-
"description": "Typed value
|
|
3998
|
+
"description": "Typed value \u2014 stored as string, number, or boolean exactly as passed."
|
|
3138
3999
|
}
|
|
3139
4000
|
},
|
|
3140
4001
|
"required": [
|
|
@@ -3146,7 +4007,7 @@
|
|
|
3146
4007
|
},
|
|
3147
4008
|
"effect": "write",
|
|
3148
4009
|
"release": "works",
|
|
3149
|
-
"description": "Unlike AsyncStorage, MMKV is typed: pass a real number or boolean and it is stored as that type
|
|
4010
|
+
"description": "Unlike AsyncStorage, MMKV is typed: pass a real number or boolean and it is stored as that type \u2014 do not stringify. Throws 'No MMKV instance registered as \"<id>\"' for an unknown instance and 'MMKV instance \"<id>\" is registered read-only' for one the app declared immutable, so a success means the write really landed. Returns nothing. MMKV writes are recorded in the event timeline but are NOT undoable via timeTravel (AsyncStorage only). TRAP: if this key is a live store's saved copy (the grounding marks these as \"persists to <key>\", and zustand's listStores reports it as persistName), do NOT write it here. The app keeps that state in memory and only reads the key at startup, so the screen will not change and the store will overwrite you the next time it saves. Use the store's own tool instead \u2014 zustand.setState, redux.dispatch, jotai.setAtom.",
|
|
3150
4011
|
"requires": [
|
|
3151
4012
|
"registerMMKVInstance(...) called by the app (react-native-mmkv)"
|
|
3152
4013
|
]
|
|
@@ -3174,7 +4035,7 @@
|
|
|
3174
4035
|
},
|
|
3175
4036
|
"effect": "destructive",
|
|
3176
4037
|
"release": "works",
|
|
3177
|
-
"description": "Calls remove() on react-native-mmkv v4 and falls back to delete() on older versions. Throws for an unknown instance id or one registered read-only; otherwise returns nothing, whether or not the key existed. Not recoverable through timeTravel
|
|
4038
|
+
"description": "Calls remove() on react-native-mmkv v4 and falls back to delete() on older versions. Throws for an unknown instance id or one registered read-only; otherwise returns nothing, whether or not the key existed. Not recoverable through timeTravel \u2014 that covers AsyncStorage only.",
|
|
3178
4039
|
"requires": [
|
|
3179
4040
|
"registerMMKVInstance(...) called by the app (react-native-mmkv)"
|
|
3180
4041
|
]
|
|
@@ -3189,7 +4050,7 @@
|
|
|
3189
4050
|
},
|
|
3190
4051
|
"effect": "read",
|
|
3191
4052
|
"release": "works",
|
|
3192
|
-
"description": "Returns [{key, description, keychainService, requireAuthentication}]. SecureStore has no key-enumeration API, so only keys the app declared via registerSecureStoreKeys(...) are visible
|
|
4053
|
+
"description": "Returns [{key, description, keychainService, requireAuthentication}]. SecureStore has no key-enumeration API, so only keys the app declared via registerSecureStoreKeys(...) are visible \u2014 an empty [] means nothing was registered, NOT that the keychain is empty. requireAuthentication:true marks a biometric-protected key whose value Buoy never reads. Each key also has `hasValue` (true / false; null for a biometric-protected key, which is never read) \u2014 so you can tell an empty key from a set one, or see that the login moved from one key to another, without the value ever leaving the device.",
|
|
3193
4054
|
"requires": [
|
|
3194
4055
|
"registerSecureStoreKeys(SecureStore, [...]) called by the app (expo-secure-store)"
|
|
3195
4056
|
]
|
|
@@ -3204,7 +4065,7 @@
|
|
|
3204
4065
|
},
|
|
3205
4066
|
"effect": "read",
|
|
3206
4067
|
"release": "works",
|
|
3207
|
-
"description": "The SecureStore counterpart to mmkv.snapshot, and the right read when you want values
|
|
4068
|
+
"description": "The SecureStore counterpart to mmkv.snapshot, and the right read when you want values \u2014 one call instead of secure.keys plus N secure.get. Returns [{key, description, keychainService, requireAuthentication, value}]. value is null when unset, when the read failed, and always for biometric-protected keys (reading those would fire an auth prompt on the user's device, so they are skipped). Values over 16KB come back as {__buoyValueOnDevice:true}.",
|
|
3208
4069
|
"requires": [
|
|
3209
4070
|
"registerSecureStoreKeys(SecureStore, [...]) called by the app (expo-secure-store)"
|
|
3210
4071
|
]
|
|
@@ -3227,7 +4088,7 @@
|
|
|
3227
4088
|
},
|
|
3228
4089
|
"effect": "read",
|
|
3229
4090
|
"release": "works",
|
|
3230
|
-
"description": "Reads with the exact options (keychainService) the key was registered with, which is required or the read returns null. Resolves to null
|
|
4091
|
+
"description": "Reads with the exact options (keychainService) the key was registered with, which is required or the read returns null. Resolves to null \u2014 never an error \u2014 in four different cases: the value is unset, the key is not registered, no SecureStore module was registered, or the key is biometric-protected (requireAuthentication, deliberately never read). Check secure.keys before concluding from a null that the app never stored anything. Prefer secure.snapshot for more than one key.",
|
|
3231
4092
|
"requires": [
|
|
3232
4093
|
"registerSecureStoreKeys(SecureStore, [...]) called by the app (expo-secure-store)"
|
|
3233
4094
|
]
|
|
@@ -3255,7 +4116,7 @@
|
|
|
3255
4116
|
},
|
|
3256
4117
|
"effect": "write",
|
|
3257
4118
|
"release": "works",
|
|
3258
|
-
"description": "Goes through the registry so the value is written with the SAME options it was registered with
|
|
4119
|
+
"description": "Goes through the registry so the value is written with the SAME options it was registered with \u2014 writing with a different keychainService silently creates a second entry the app cannot read. Throws rather than no-ops: 'No SecureStore module is registered', 'SecureStore key \"<k>\" is not registered', or '...is biometric-protected \u2014 DevTools never writes it'. Values are strings; JSON-encode objects yourself. SecureStore changes are NOT in the event timeline, so there is no timeTravel undo.",
|
|
3259
4120
|
"requires": [
|
|
3260
4121
|
"registerSecureStoreKeys(SecureStore, [...]) called by the app (expo-secure-store)"
|
|
3261
4122
|
]
|
|
@@ -3278,36 +4139,36 @@
|
|
|
3278
4139
|
},
|
|
3279
4140
|
"effect": "destructive",
|
|
3280
4141
|
"release": "works",
|
|
3281
|
-
"description": "Permanently deletes the keychain entry using the options the key was registered with. Beware the silent path: if the key is not registered or no SecureStore module was registered it resolves with NO error and NO deletion
|
|
4142
|
+
"description": "Permanently deletes the keychain entry using the options the key was registered with. Beware the silent path: if the key is not registered or no SecureStore module was registered it resolves with NO error and NO deletion \u2014 a success here is not proof anything was deleted, so verify with secure.get/secure.snapshot afterwards. Deleting an auth token or session key logs the user out; not recoverable (SecureStore is not in the event timeline).",
|
|
3282
4143
|
"requires": [
|
|
3283
4144
|
"registerSecureStoreKeys(SecureStore, [...]) called by the app (expo-secure-store)"
|
|
3284
4145
|
]
|
|
3285
4146
|
}
|
|
3286
4147
|
],
|
|
3287
|
-
"unavailableWhen": "The app does not have @buoy-gg/storage installed alongside <FloatingDevTools/>
|
|
4148
|
+
"unavailableWhen": "The app does not have @buoy-gg/storage installed alongside <FloatingDevTools/> \u2014 autoExternalSync only registers the \"storage\" adapter when that module resolves (packages/devtools-floating-menu/src/floatingMenu/autoExternalSync.tsx:320). Individual backends degrade instead of erroring: with no registerMMKVInstance() call mmkv.snapshot returns [], and with no registerSecureStoreKeys() call secure.keys/secure.snapshot return []."
|
|
3288
4149
|
},
|
|
3289
4150
|
{
|
|
3290
4151
|
"toolId": "highlight-updates",
|
|
3291
4152
|
"title": "Highlight Updates",
|
|
3292
|
-
"summary": "Read and drive the live screen with describeScreen, tapElement, and waitFor. RN also exposes React render tracking; native Swift does not. Inspect the connected device
|
|
4153
|
+
"summary": "Read and drive the live screen with describeScreen, tapElement, and waitFor. RN also exposes React render tracking; native Swift does not. Inspect the connected device\u2019s actions before calling a platform-specific operation. The touch-capture actions support Scenarios recording. Native capture is restricted to development builds and excludes Buoy controls and secure text inputs.",
|
|
3293
4154
|
"actions": [
|
|
3294
4155
|
{
|
|
3295
4156
|
"action": "describeScreen",
|
|
3296
|
-
"summary": "List every meaningful/interactive element currently on screen, with normalized tap points
|
|
4157
|
+
"summary": "List every meaningful/interactive element currently on screen, with normalized tap points \u2014 the read half of driving the app. The ONLY way to read what is on screen but in no store: text, values and names held in component state, which no other Buoy tool can see.",
|
|
3297
4158
|
"params": {
|
|
3298
4159
|
"type": "object",
|
|
3299
4160
|
"properties": {
|
|
3300
4161
|
"includeBuoy": {
|
|
3301
4162
|
"type": "boolean",
|
|
3302
|
-
"description": "Also list Buoy's own overlay (the dial, tool sheets, Ask Buoy's own chat). Default false
|
|
4163
|
+
"description": "Also list Buoy's own overlay (the dial, tool sheets, Ask Buoy's own chat). Default false \u2014 those are the tool, not the app."
|
|
3303
4164
|
}
|
|
3304
4165
|
},
|
|
3305
4166
|
"additionalProperties": false
|
|
3306
4167
|
},
|
|
3307
4168
|
"effect": "read",
|
|
3308
4169
|
"release": "empty",
|
|
3309
|
-
"description": "Walks the live React fiber tree across ALL renderers and measures each candidate. Returns {screen:{width,height}, count, elements[]} where each element has: nativeTag, name (owning component, not the host View), role, testID, label, text, interactive, control ('toggle'|'slider'|'text', omitted for a plain press), value (current value for toggle/slider/text), longPressable, tap:{x,y} and frame:{x,y,width,height} both normalized to 0-1. Sorted top-to-bottom then left-to-right. Prunes offscreen/inactive react-native-screens and hidden Offscreen subtrees, so it reflects the CURRENT screen only
|
|
3310
|
-
"releaseNote": "packages/highlight-updates/src/highlight-updates/utils/screenElements.ts:57
|
|
4170
|
+
"description": "Walks the live React fiber tree across ALL renderers and measures each candidate. Returns {screen:{width,height}, count, elements[]} where each element has: nativeTag, name (owning component, not the host View), role, testID, label, text, interactive, control ('toggle'|'slider'|'text', omitted for a plain press), value (current value for toggle/slider/text), longPressable, tap:{x,y} and frame:{x,y,width,height} both normalized to 0-1. Sorted top-to-bottom then left-to-right. Prunes offscreen/inactive react-native-screens and hidden Offscreen subtrees, so it reflects the CURRENT screen only \u2014 re-run after every navigation, positions and tags move. No screenshot and no tracking needed; it does not require the highlight overlay to be enabled. Call this before tapElement to get an exact testID/nativeTag instead of guessing a fuzzy query. Buoy's OWN overlay is left out \u2014 the dial, tool sheets and your own chat sheet are the tool, not the app under test \u2014 and `hiddenBuoy` counts what was omitted; pass includeBuoy:true only when the task is about Buoy itself.",
|
|
4171
|
+
"releaseNote": "packages/highlight-updates/src/highlight-updates/utils/screenElements.ts:57 \u2014 getReactDevToolsHook() reads __REACT_DEVTOOLS_GLOBAL_HOOK__, which RN installs only under __DEV__; getAllFiberRoots() then returns [] and collectCandidates() short-circuits at line 539, so the result is a valid-looking {count:0, elements:[]} rather than an error.",
|
|
3311
4172
|
"requires": [
|
|
3312
4173
|
"@buoy-gg/highlight-updates installed and registered with <FloatingDevTools>",
|
|
3313
4174
|
"dev build (__DEV__ === true)"
|
|
@@ -3315,7 +4176,7 @@
|
|
|
3315
4176
|
},
|
|
3316
4177
|
{
|
|
3317
4178
|
"action": "tapElement",
|
|
3318
|
-
"summary": "Interact with one on-screen element by invoking its real handler in JS: press, long-press, toggle/slider, or type into a text input. This is how you make the app FETCH MORE
|
|
4179
|
+
"summary": "Interact with one on-screen element by invoking its real handler in JS: press, long-press, toggle/slider, or type into a text input. This is how you make the app FETCH MORE \u2014 press its own \"next\", \"load more\" or a list row, waitFor, then read: the app fetches through its own code, so the data arrives in the shape its own screens render.",
|
|
3319
4180
|
"params": {
|
|
3320
4181
|
"type": "object",
|
|
3321
4182
|
"properties": {
|
|
@@ -3329,7 +4190,7 @@
|
|
|
3329
4190
|
},
|
|
3330
4191
|
"query": {
|
|
3331
4192
|
"type": "string",
|
|
3332
|
-
"description": "Fuzzy match against testID / accessibilityLabel / visible text / component name, e.g. 'Sign in'. Least precise
|
|
4193
|
+
"description": "Fuzzy match against testID / accessibilityLabel / visible text / component name, e.g. 'Sign in'. Least precise \u2014 verify with describeScreen first."
|
|
3333
4194
|
},
|
|
3334
4195
|
"value": {
|
|
3335
4196
|
"type": [
|
|
@@ -3348,7 +4209,7 @@
|
|
|
3348
4209
|
},
|
|
3349
4210
|
"scrollIntoView": {
|
|
3350
4211
|
"type": "boolean",
|
|
3351
|
-
"description": "Scroll an ancestor ScrollView so the target is visible before acting. Default true
|
|
4212
|
+
"description": "Scroll an ancestor ScrollView so the target is visible before acting. Default true \u2014 set false to avoid moving the user's screen."
|
|
3352
4213
|
},
|
|
3353
4214
|
"includeBuoy": {
|
|
3354
4215
|
"type": "boolean",
|
|
@@ -3359,8 +4220,8 @@
|
|
|
3359
4220
|
},
|
|
3360
4221
|
"effect": "write",
|
|
3361
4222
|
"release": "empty",
|
|
3362
|
-
"description": "WARNING: this fires the app's ACTUAL handler, so it can trigger irreversible flows (delete, purchase, logout, submit). Matching by fuzzy `query` can select the wrong element
|
|
3363
|
-
"releaseNote": "packages/highlight-updates/src/highlight-updates/utils/screenElements.ts:793
|
|
4223
|
+
"description": "WARNING: this fires the app's ACTUAL handler, so it can trigger irreversible flows (delete, purchase, logout, submit). Matching by fuzzy `query` can select the wrong element \u2014 prefer an exact nativeTag or testID from describeScreen, and confirm the target before firing anything consequential. Resolution order: the element's OWN onValueChange (toggle/slider) or onChangeText (text input) wins; otherwise the nearest onPress walking up the fiber chain (up to 25 levels). Returns {tapped, reason?, scrolled?, matched:{nativeTag,name,testID,label,text,via,value}, candidates?} \u2014 `via` is onPress|onLongPress|onValueChange|onChangeText. Trap: passing `text` to something that is not a text input silently runs its onPress instead, so check `matched.via` in the result. Elements driven only by react-native-gesture-handler GestureDetector have no JS handler and return tapped:false with a clear reason. If the target is off-screen it is scrolled into view first (this moves the user's screen). Provide exactly one of nativeTag / testID / query. A fuzzy query never matches Buoy's own overlay unless includeBuoy:true (your chat sheet echoes the words you search for; that echo used to be the best match). READ THE RESULT BEFORE TRUSTING THE TAP: `effect.commits` is how many React commits followed inside the settle window, and 0 means the handler ran but NOTHING re-rendered \u2014 the match was probably a screen still mounted underneath the current one (a stack keeps them), or an inert control \u2014 so treat it as not done and pick another target from describeScreen. `alsoMatched` lists elements that matched equally well; non-empty means the choice was a coin toss, so re-tap by nativeTag.",
|
|
4224
|
+
"releaseNote": "packages/highlight-updates/src/highlight-updates/utils/screenElements.ts:793 \u2014 collectCandidates() returns [] with no DevTools hook, so it returns {tapped:false, reason:\"No React fiber roots / elements found on screen.\"}; it reports the failure honestly rather than claiming a tap.",
|
|
3364
4225
|
"requires": [
|
|
3365
4226
|
"@buoy-gg/highlight-updates installed and registered with <FloatingDevTools>",
|
|
3366
4227
|
"dev build (__DEV__ === true)"
|
|
@@ -3369,7 +4230,7 @@
|
|
|
3369
4230
|
{
|
|
3370
4231
|
"action": "waitFor",
|
|
3371
4232
|
"summary": "Block until an element is on screen (or gone), then report how long it took. Use it between navigating and tapping.",
|
|
3372
|
-
"description": "Returns {ok, waitedMs, polls, matched?, reason?}. USE THIS AFTER ANY ACTION THAT STARTS A LOAD
|
|
4233
|
+
"description": "Returns {ok, waitedMs, polls, matched?, reason?}. USE THIS AFTER ANY ACTION THAT STARTS A LOAD \u2014 `route-events.navigate` returns the moment the route is pushed, not when the screen has data, so tapping straight after it hits a loading skeleton and fails for a reason that looks like a bad selector. Matching is `tapElement`'s exactly (same fields, same ranking), so a wait can never resolve on an element the tap then cannot find. Presence means ON SCREEN: a candidate matching by name is measured before it counts, so a previous screen still mounted behind this one does not satisfy the wait. `gone:true` waits for absence instead \u2014 and returns immediately when nothing matched in the first place, which is a real answer, not a failure. Like tapElement, a fuzzy query ignores Buoy's own overlay unless includeBuoy:true; an exact nativeTag/testID always counts. Defaults: timeoutMs 5000 (capped at 30000), pollMs 150 (floored at 50, because this polls on the JS thread the app renders on). Provide exactly one of nativeTag / testID / query.",
|
|
3373
4234
|
"params": {
|
|
3374
4235
|
"type": "object",
|
|
3375
4236
|
"properties": {
|
|
@@ -3387,7 +4248,7 @@
|
|
|
3387
4248
|
},
|
|
3388
4249
|
"gone": {
|
|
3389
4250
|
"type": "boolean",
|
|
3390
|
-
"description": "Wait for the element to DISAPPEAR instead of appear
|
|
4251
|
+
"description": "Wait for the element to DISAPPEAR instead of appear \u2014 a spinner, a skeleton, a modal. Default false."
|
|
3391
4252
|
},
|
|
3392
4253
|
"timeoutMs": {
|
|
3393
4254
|
"type": "number",
|
|
@@ -3414,7 +4275,7 @@
|
|
|
3414
4275
|
},
|
|
3415
4276
|
{
|
|
3416
4277
|
"action": "beginMeasurement",
|
|
3417
|
-
"summary": "Open an invisible render-capture window (CommitProfiler)
|
|
4278
|
+
"summary": "Open an invisible render-capture window (CommitProfiler) \u2014 pair with endMeasurement around the interaction you want to measure.",
|
|
3418
4279
|
"params": {
|
|
3419
4280
|
"type": "object",
|
|
3420
4281
|
"properties": {},
|
|
@@ -3422,8 +4283,8 @@
|
|
|
3422
4283
|
},
|
|
3423
4284
|
"effect": "write",
|
|
3424
4285
|
"release": "empty",
|
|
3425
|
-
"description": "Starts a commit-level capture with detail:true. Backed by CommitProfiler, NOT the highlight overlay's RenderTracker, and that difference is the point: it walks COMPOSITE fibers, so it answers about the component you named (which of ITS props changed, by value or identity only, and which parent dragged it along) instead of about the native View inside it. Touches none of the user's state
|
|
3426
|
-
"releaseNote": "packages/highlight-updates/src/highlight-updates/utils/CommitProfiler.ts:458
|
|
4286
|
+
"description": "Starts a commit-level capture with detail:true. Backed by CommitProfiler, NOT the highlight overlay's RenderTracker, and that difference is the point: it walks COMPOSITE fibers, so it answers about the component you named (which of ITS props changed, by value or identity only, and which parent dragged it along) instead of about the native View inside it. Touches none of the user's state \u2014 nothing is enabled, nothing is cleared, nothing is drawn on screen; the user sees no change. Discards any previous unclaimed capture. Returns {ok:true} or {ok:false, reason} \u2014 always check `ok` before driving the interaction. Usage: beginMeasurement -> wait ~400ms to settle -> drive the UI with tapElement -> endMeasurement.",
|
|
4287
|
+
"releaseNote": "packages/highlight-updates/src/highlight-updates/utils/CommitProfiler.ts:458 \u2014 isSupported() returns false when !__DEV__, so this returns {ok:false, reason:\"render capture needs a dev build with the React DevTools hook available\"}. Honest self-report, unlike the toggle actions.",
|
|
3427
4288
|
"requires": [
|
|
3428
4289
|
"@buoy-gg/highlight-updates at adapter version 3+ (older apps have no beginMeasurement)",
|
|
3429
4290
|
"dev build (__DEV__ === true)"
|
|
@@ -3439,8 +4300,8 @@
|
|
|
3439
4300
|
},
|
|
3440
4301
|
"effect": "write",
|
|
3441
4302
|
"release": "empty",
|
|
3442
|
-
"description": "Returns {summary: RenderCaptureSummary | null}. The summary carries totalCommits, totalRenders, totalRenderMs, wastedRenders (parent cascades + identity-only prop churn
|
|
3443
|
-
"releaseNote": "packages/highlight-updates/src/highlight-updates/utils/CommitProfiler.ts
|
|
4303
|
+
"description": "Returns {summary: RenderCaptureSummary | null}. The summary carries totalCommits, totalRenders, totalRenderMs, wastedRenders (parent cascades + identity-only prop churn \u2014 the removable share), aggregate `causes`, and topComponents[] (capped) with {name, renders, totalMs, maxMs, avgMs, causes, changedProps, parentName}. Cause labels: mount (first render), hooks (own state changed), props (props changed by value), propsUnstable (props changed by IDENTITY only \u2014 a fresh function/object from the parent, fix upstream), parent (pure cascade, nothing of its own changed). Read topComponents sorted by totalMs, not by render count: a component rendering 40x for 0.4ms is noise; one rendering 4x for 38ms is the answer. summary is null when beginMeasurement was never called or the app reloaded mid-window.",
|
|
4304
|
+
"releaseNote": "packages/highlight-updates/src/highlight-updates/utils/CommitProfiler.ts \u2014 stopCapture() returns null when the window was never started, and beginMeasurement can never start one in release (isSupported() false at line 458), so this always yields {summary:null}.",
|
|
3444
4305
|
"requires": [
|
|
3445
4306
|
"@buoy-gg/highlight-updates at adapter version 3+",
|
|
3446
4307
|
"a prior successful beginMeasurement on the same device"
|
|
@@ -3448,7 +4309,7 @@
|
|
|
3448
4309
|
},
|
|
3449
4310
|
{
|
|
3450
4311
|
"action": "locateComponent",
|
|
3451
|
-
"summary": "Resolve a component to a fresh on-screen rectangle (in points) plus pixel scale
|
|
4312
|
+
"summary": "Resolve a component to a fresh on-screen rectangle (in points) plus pixel scale \u2014 used to crop a simulator screenshot to exactly that component.",
|
|
3452
4313
|
"params": {
|
|
3453
4314
|
"type": "object",
|
|
3454
4315
|
"properties": {
|
|
@@ -3458,11 +4319,11 @@
|
|
|
3458
4319
|
},
|
|
3459
4320
|
"nativeTag": {
|
|
3460
4321
|
"type": "number",
|
|
3461
|
-
"description": "Exact native tag
|
|
4322
|
+
"description": "Exact native tag \u2014 skips fuzzy matching and wins over query."
|
|
3462
4323
|
},
|
|
3463
4324
|
"scrollIntoView": {
|
|
3464
4325
|
"type": "boolean",
|
|
3465
|
-
"description": "Scroll an ancestor ScrollView to bring the component into view before measuring. Default true
|
|
4326
|
+
"description": "Scroll an ancestor ScrollView to bring the component into view before measuring. Default true \u2014 this visibly moves the user's screen."
|
|
3466
4327
|
},
|
|
3467
4328
|
"margin": {
|
|
3468
4329
|
"type": "number",
|
|
@@ -3474,7 +4335,7 @@
|
|
|
3474
4335
|
"effect": "write",
|
|
3475
4336
|
"release": "empty",
|
|
3476
4337
|
"description": "BEWARE the side effect: by default this SCROLLS the user's app so the target sits near the top of its ScrollView before measuring. Pass scrollIntoView:false for a pure read. Returns {matched, reason?, live?, scrolled?, inView?, rect:{x,y,width,height}, scale (PixelRatio, multiply points by it for pixels), screen:{width,height}, nativeTag, componentName, testID, candidates[]}. Two resolution paths: if the render tracker has data (highlights or silent tracking were on) it ranks tracked components by testID/nativeID/accessibilityLabel/componentName/viewType/nativeTag and re-measures the live node; if the tracker is empty it falls back to the same fiber walk as describeScreen, so it still works cold. `reason` is one of no-renders | no-match | no-measurement. When matched is false, read `candidates` and retry with an exact nativeTag.",
|
|
3477
|
-
"releaseNote": "HighlightUpdatesController.ts:1744
|
|
4338
|
+
"releaseNote": "HighlightUpdatesController.ts:1744 \u2014 initialize() bails when !__DEV__ so the tracker is empty, and the describeScreen fallback finds no fiber roots; result is {matched:false, reason:\"no-match\", candidates:[]}.",
|
|
3478
4339
|
"requires": [
|
|
3479
4340
|
"@buoy-gg/highlight-updates installed and registered with <FloatingDevTools>",
|
|
3480
4341
|
"dev build (__DEV__ === true)"
|
|
@@ -3498,8 +4359,8 @@
|
|
|
3498
4359
|
},
|
|
3499
4360
|
"effect": "read",
|
|
3500
4361
|
"release": "empty",
|
|
3501
|
-
"description": "Snapshots strip the per-component history buffer (up to 20 events) and hookChanges to keep the ~5x/sec sync small; this is the on-demand fetch for a single row the user clicked. Returns {found:true, nativeTag, render} or {found:false, reason} where reason is 'missing nativeTag' or 'unknown nativeTag'. Keyed by nativeTag
|
|
3502
|
-
"releaseNote": "HighlightUpdatesController.ts:1819/1854
|
|
4362
|
+
"description": "Snapshots strip the per-component history buffer (up to 20 events) and hookChanges to keep the ~5x/sec sync small; this is the on-demand fetch for a single row the user clicked. Returns {found:true, nativeTag, render} or {found:false, reason} where reason is 'missing nativeTag' or 'unknown nativeTag'. Keyed by nativeTag \u2014 get one from the snapshot's renders[] or from describeScreen. Only returns data for components the RenderTracker has actually seen, which means highlights (setEnabled/toggle) or setSilentTracking must have been on while the component rendered; a cold call on a fresh app returns found:false even though the component exists on screen.",
|
|
4363
|
+
"releaseNote": "HighlightUpdatesController.ts:1819/1854 \u2014 enable()/disable() no-op when !__DEV__, so RenderTracker never records and every lookup returns {found:false, reason:\"unknown nativeTag\"}.",
|
|
3503
4364
|
"requires": [
|
|
3504
4365
|
"@buoy-gg/highlight-updates installed and registered with <FloatingDevTools>",
|
|
3505
4366
|
"dev build (__DEV__ === true)",
|
|
@@ -3524,8 +4385,8 @@
|
|
|
3524
4385
|
},
|
|
3525
4386
|
"effect": "write",
|
|
3526
4387
|
"release": "noop",
|
|
3527
|
-
"description": "Prefer this over `toggle`
|
|
3528
|
-
"releaseNote": "HighlightUpdatesController.ts:1819 (enable) and :1854 (disable) both `if (!__DEV__) return;` before doing anything. The wire still reports ok:true, so this action LIES in a release build
|
|
4388
|
+
"description": "Prefer this over `toggle` \u2014 it is idempotent, so the agent never has to know the current state. Enabling starts RenderTracker (populating the synced renders[] list and enabling getRenderDetail) AND draws colored boxes on the DEVICE screen: cyan for few renders through yellow for many, with a count badge. It also clears any leftover highlight suppression from a silent-tracking session. This is visible to whoever is holding the phone \u2014 say so before enabling. Returns undefined on success. Always send a params object: the handler reads params.enabled without a null guard, so calling with no params throws (surfaced as ok:false with a TypeError message).",
|
|
4389
|
+
"releaseNote": "HighlightUpdatesController.ts:1819 (enable) and :1854 (disable) both `if (!__DEV__) return;` before doing anything. The wire still reports ok:true, so this action LIES in a release build \u2014 never tell a QA user highlighting is on without confirming a dev build.",
|
|
3529
4390
|
"requires": [
|
|
3530
4391
|
"@buoy-gg/highlight-updates installed and registered with <FloatingDevTools>",
|
|
3531
4392
|
"dev build (__DEV__ === true)"
|
|
@@ -3533,7 +4394,7 @@
|
|
|
3533
4394
|
},
|
|
3534
4395
|
{
|
|
3535
4396
|
"action": "toggle",
|
|
3536
|
-
"summary": "Flip render highlighting on/off
|
|
4397
|
+
"summary": "Flip render highlighting on/off \u2014 same visible effect as setEnabled but state-dependent.",
|
|
3537
4398
|
"params": {
|
|
3538
4399
|
"type": "object",
|
|
3539
4400
|
"properties": {},
|
|
@@ -3541,8 +4402,8 @@
|
|
|
3541
4402
|
},
|
|
3542
4403
|
"effect": "write",
|
|
3543
4404
|
"release": "noop",
|
|
3544
|
-
"description": "Calls enable() when off, disable() when on. Prefer setEnabled({enabled}) unless you genuinely want a flip, because this action's outcome depends on state you may not have read. Read the synced snapshot's `enabled` flag first if the distinction matters. Initializes the controller on first use. Draws colored boxes on the DEVICE screen
|
|
3545
|
-
"releaseNote": "HighlightUpdatesController.ts:1954
|
|
4405
|
+
"description": "Calls enable() when off, disable() when on. Prefer setEnabled({enabled}) unless you genuinely want a flip, because this action's outcome depends on state you may not have read. Read the synced snapshot's `enabled` flag first if the distinction matters. Initializes the controller on first use. Draws colored boxes on the DEVICE screen \u2014 visible to the person holding the phone. Takes no params. Returns undefined.",
|
|
4406
|
+
"releaseNote": "HighlightUpdatesController.ts:1954 \u2014 `if (!__DEV__) return;` at the top of toggle(). Reports ok:true and does nothing.",
|
|
3546
4407
|
"requires": [
|
|
3547
4408
|
"@buoy-gg/highlight-updates installed and registered with <FloatingDevTools>",
|
|
3548
4409
|
"dev build (__DEV__ === true)"
|
|
@@ -3550,7 +4411,7 @@
|
|
|
3550
4411
|
},
|
|
3551
4412
|
{
|
|
3552
4413
|
"action": "setSilentTracking",
|
|
3553
|
-
"summary": "Track and measure renders WITHOUT drawing any highlight boxes
|
|
4414
|
+
"summary": "Track and measure renders WITHOUT drawing any highlight boxes \u2014 invisible tracking for screenshot/locate flows.",
|
|
3554
4415
|
"params": {
|
|
3555
4416
|
"type": "object",
|
|
3556
4417
|
"properties": {
|
|
@@ -3563,7 +4424,7 @@
|
|
|
3563
4424
|
},
|
|
3564
4425
|
"effect": "write",
|
|
3565
4426
|
"release": "noop",
|
|
3566
|
-
"description": "Turns the render tracker on (so renders[] populates, measurements sync, and getRenderDetail/locateComponent have data) while keeping the visual overlay hidden, so nothing appears on the user's screen. This is the right way to arm tracking when you only need data
|
|
4427
|
+
"description": "Turns the render tracker on (so renders[] populates, measurements sync, and getRenderDetail/locateComponent have data) while keeping the visual overlay hidden, so nothing appears on the user's screen. This is the right way to arm tracking when you only need data \u2014 use setEnabled only when the user actually wants to SEE the boxes. Idempotent and safe to call before every locateComponent. Disabling restores normal state (also disables tracking). Note: enabling any visual highlighting afterwards clears the suppression, so the boxes come back. Returns undefined. Always send a params object \u2014 the handler reads params.enabled without a null guard and throws if params is omitted; `{}` is treated as enabled:false.",
|
|
3567
4428
|
"releaseNote": "Reaches HighlightUpdatesController.setSilentTracking -> enable(), which returns early at HighlightUpdatesController.ts:1819 when !__DEV__. Reports ok:true; no tracking is armed and every later getRenderDetail/locateComponent comes back empty.",
|
|
3568
4429
|
"requires": [
|
|
3569
4430
|
"@buoy-gg/highlight-updates installed and registered with <FloatingDevTools>",
|
|
@@ -3580,7 +4441,7 @@
|
|
|
3580
4441
|
},
|
|
3581
4442
|
"effect": "write",
|
|
3582
4443
|
"release": "noop",
|
|
3583
|
-
"description": "Freeze mode keeps existing highlight boxes on the device screen instead of letting them fade, so a human can read which components lit up during a burst. New renders are still captured. Unfreezing clears whatever boxes are currently drawn. State-dependent: read the snapshot's `frozen` flag before calling if you need a specific end state. Freeze is a view state on a LIVE overlay
|
|
4444
|
+
"description": "Freeze mode keeps existing highlight boxes on the device screen instead of letting them fade, so a human can read which components lit up during a burst. New renders are still captured. Unfreezing clears whatever boxes are currently drawn. State-dependent: read the snapshot's `frozen` flag before calling if you need a specific end state. Freeze is a view state on a LIVE overlay \u2014 disabling highlighting entirely also clears it. Takes no params. Returns undefined.",
|
|
3584
4445
|
"releaseNote": "toggleFreeze() itself has no gate, but both branches do: freeze() at HighlightUpdatesController.ts:2068 and unfreeze() at :2082 return early when !__DEV__. Reports ok:true and nothing changes.",
|
|
3585
4446
|
"requires": [
|
|
3586
4447
|
"@buoy-gg/highlight-updates installed and registered with <FloatingDevTools>",
|
|
@@ -3609,7 +4470,7 @@
|
|
|
3609
4470
|
},
|
|
3610
4471
|
"effect": "write",
|
|
3611
4472
|
"release": "noop",
|
|
3612
|
-
"description": "Used when someone is browsing a component's detail from the desktop dashboard and wants to see WHICH component that row is, physically on the phone. Draws on the DEVICE screen
|
|
4473
|
+
"description": "Used when someone is browsing a component's detail from the desktop dashboard and wants to see WHICH component that row is, physically on the phone. Draws on the DEVICE screen \u2014 visible to whoever is holding it. Pass nativeTag:null to clear the spotlight; always clear it when you're done, or a stale box stays on the user's screen. Requires the highlight overlay to be mounted (i.e. highlighting enabled) for anything to appear. Returns undefined. Always send a params object \u2014 the handler reads params.nativeTag without a null guard and throws if params is omitted entirely.",
|
|
3613
4474
|
"releaseNote": "setSpotlight has no __DEV__ gate of its own, but the overlay that renders the spotlight only exists once the tool is enabled, and enable() is gated at HighlightUpdatesController.ts:1819. It sets a variable, draws nothing, and reports ok:true.",
|
|
3614
4475
|
"requires": [
|
|
3615
4476
|
"@buoy-gg/highlight-updates installed and registered with <FloatingDevTools>",
|
|
@@ -3619,7 +4480,7 @@
|
|
|
3619
4480
|
},
|
|
3620
4481
|
{
|
|
3621
4482
|
"action": "clearRenderCounts",
|
|
3622
|
-
"summary": "Wipe ALL tracked render data and per-component counters
|
|
4483
|
+
"summary": "Wipe ALL tracked render data and per-component counters \u2014 irreversible, and it destroys data someone may be collecting.",
|
|
3623
4484
|
"params": {
|
|
3624
4485
|
"type": "object",
|
|
3625
4486
|
"properties": {},
|
|
@@ -3627,7 +4488,7 @@
|
|
|
3627
4488
|
},
|
|
3628
4489
|
"effect": "destructive",
|
|
3629
4490
|
"release": "noop",
|
|
3630
|
-
"description": "Clears the nativeTag->count map and empties RenderTracker's entire renders store. There is no undo and no snapshot: any render history a human was watching accumulate, or that a colleague armed tracking to collect, is gone. Only call this when the user explicitly asks to reset counts, typically to get a clean baseline before measuring an interaction. Prefer beginMeasurement/endMeasurement for measuring
|
|
4491
|
+
"description": "Clears the nativeTag->count map and empties RenderTracker's entire renders store. There is no undo and no snapshot: any render history a human was watching accumulate, or that a colleague armed tracking to collect, is gone. Only call this when the user explicitly asks to reset counts, typically to get a clean baseline before measuring an interaction. Prefer beginMeasurement/endMeasurement for measuring \u2014 that opens its own window and never touches the user's overlay data. Takes no params. Returns undefined.",
|
|
3631
4492
|
"releaseNote": "packages/highlight-updates/src/highlight-updates/utils/HighlightUpdatesController.ts:1744 (initialize), :1819 (enable), :1905 (enableBackgroundTracking) all return early when __DEV__===false, and the only writers of nodeRenderCounts/RenderTracker are the DevTools-hook interceptors (ProfilerInterceptor.ts:107, CommitProfiler.ts:74). In a release build those stores are permanently empty, so clearRen",
|
|
3632
4493
|
"requires": [
|
|
3633
4494
|
"@buoy-gg/highlight-updates installed and registered with <FloatingDevTools>"
|
|
@@ -3643,7 +4504,8 @@
|
|
|
3643
4504
|
"additionalProperties": false
|
|
3644
4505
|
},
|
|
3645
4506
|
"effect": "write",
|
|
3646
|
-
"release": "empty"
|
|
4507
|
+
"release": "empty",
|
|
4508
|
+
"releaseNote": "touchCapture.ts startTouchCapture() subscribes to RawEventEmitter and reports status \"capturing\", but each touch is named through resolveTouchTarget() in screenElements.ts:1484, which needs fiber roots from __REACT_DEVTOOLS_GLOBAL_HOOK__ (installed only under __DEV__). In a release build every target resolves to null, so nothing is recorded and readTouchCapture stays empty."
|
|
3647
4509
|
},
|
|
3648
4510
|
{
|
|
3649
4511
|
"action": "stopTouchCapture",
|
|
@@ -3687,16 +4549,16 @@
|
|
|
3687
4549
|
"release": "works"
|
|
3688
4550
|
}
|
|
3689
4551
|
],
|
|
3690
|
-
"unavailableWhen": "RN requires @buoy-gg/highlight-updates registered with FloatingDevTools; React inspection and render tracking depend on a development build. Swift exposes native interaction and touch-capture actions under this ID, without React render tracking. Inspect the connected device
|
|
4552
|
+
"unavailableWhen": "RN requires @buoy-gg/highlight-updates registered with FloatingDevTools; React inspection and render tracking depend on a development build. Swift exposes native interaction and touch-capture actions under this ID, without React render tracking. Inspect the connected device\u2019s available actions. Swift touch capture refuses production builds."
|
|
3691
4553
|
},
|
|
3692
4554
|
{
|
|
3693
4555
|
"toolId": "scenarios",
|
|
3694
4556
|
"title": "Scenarios",
|
|
3695
|
-
"summary": "Named, parameterized app states (\"out of stock at store 220\", \"expired token\") stored as a list of steps that each call one other Buoy tool action; running one bends the app into that state deterministically and `deactivate` reverses what is reversible. Reach for it to put the app in a known state BEFORE driving a flow, and to check whether what is on screen is real or simulated
|
|
4557
|
+
"summary": "Named, parameterized app states (\"out of stock at store 220\", \"expired token\") stored as a list of steps that each call one other Buoy tool action; running one bends the app into that state deterministically and `deactivate` reverses what is reversible. Reach for it to put the app in a known state BEFORE driving a flow, and to check whether what is on screen is real or simulated \u2014 if `active` is non-null, the data the user is looking at is partly fake. Authoring flow: save (always lands as an inert DRAFT) \u2192 a human accepts it on the device (or acceptDraft) \u2192 preview \u2192 run \u2192 deactivate.",
|
|
3696
4558
|
"actions": [
|
|
3697
4559
|
{
|
|
3698
4560
|
"action": "listScenarios",
|
|
3699
|
-
"summary": "List every scenario on the device
|
|
4561
|
+
"summary": "List every scenario on the device \u2014 code, device-saved, and inert drafts \u2014 plus which one is currently active.",
|
|
3700
4562
|
"params": {
|
|
3701
4563
|
"type": "object",
|
|
3702
4564
|
"properties": {},
|
|
@@ -3704,7 +4566,7 @@
|
|
|
3704
4566
|
},
|
|
3705
4567
|
"effect": "read",
|
|
3706
4568
|
"release": "works",
|
|
3707
|
-
"description": "Returns {code[], device[], drafts[], active, runState, hasLoaded}. Each entry has id, name, description, version, source, author, tags, vars, stepCount, expectedOutcome, authoredAgainst, usage.runs, plus `steps` (raw {tool,action,params} objects; any step whose params exceed 8KB is sent with params dropped and paramsOmitted:true) and `stepLines` [{label, durability}]
|
|
4569
|
+
"description": "Returns {code[], device[], drafts[], active, runState, hasLoaded}. Each entry has id, name, description, version, source, author, tags, vars, stepCount, expectedOutcome, authoredAgainst, usage.runs, plus `steps` (raw {tool,action,params} objects; any step whose params exceed 8KB is sent with params dropped and paramsOmitted:true) and `stepLines` [{label, durability}] \u2014 the same plain-English sentences the device UI shows. durability is one of durable | session | transient | one-shot. `active` non-null means the app is showing SIMULATED state: say so before reporting anything observed on the device. `drafts` are INERT \u2014 they cannot run until accepted. Identical payload to the tool's sync snapshot. HYDRATION TRAP: the store loads @react_buoy_scenarios_library lazily on first subscribe and this handler does not await it, so if hasLoaded is false, empty `device`/`drafts` means \"not loaded yet\", not \"none saved\" \u2014 read again once the Scenarios panel or a dashboard has subscribed.",
|
|
3708
4570
|
"requires": [
|
|
3709
4571
|
"Scenarios tool installed in the app"
|
|
3710
4572
|
]
|
|
@@ -3727,14 +4589,14 @@
|
|
|
3727
4589
|
},
|
|
3728
4590
|
"effect": "read",
|
|
3729
4591
|
"release": "works",
|
|
3730
|
-
"description": "Returns {scenario}. Looks in code scenarios first, then device-saved, then drafts (a code scenario wins an id clash). Use this when listScenarios truncated a step (paramsOmitted:true) or you need undoSteps/vars in full. THROWS `No scenario \"<id>\"` if the id is unknown
|
|
4592
|
+
"description": "Returns {scenario}. Looks in code scenarios first, then device-saved, then drafts (a code scenario wins an id clash). Use this when listScenarios truncated a step (paramsOmitted:true) or you need undoSteps/vars in full. THROWS `No scenario \"<id>\"` if the id is unknown \u2014 and can throw that on a cold device purely because the persisted library has not hydrated yet (see listScenarios).",
|
|
3731
4593
|
"requires": [
|
|
3732
4594
|
"Scenarios tool installed in the app"
|
|
3733
4595
|
]
|
|
3734
4596
|
},
|
|
3735
4597
|
{
|
|
3736
4598
|
"action": "save",
|
|
3737
|
-
"summary": "Write a new scenario onto the device
|
|
4599
|
+
"summary": "Write a new scenario onto the device \u2014 it ALWAYS lands in the draft inbox and cannot run until accepted.",
|
|
3738
4600
|
"params": {
|
|
3739
4601
|
"type": "object",
|
|
3740
4602
|
"properties": {
|
|
@@ -3755,7 +4617,7 @@
|
|
|
3755
4617
|
},
|
|
3756
4618
|
"expectedOutcome": {
|
|
3757
4619
|
"type": "string",
|
|
3758
|
-
"description": "What SHOULD happen once applied
|
|
4620
|
+
"description": "What SHOULD happen once applied \u2014 how a non-developer tells a bug from expected behavior without asking an engineer. Write it."
|
|
3759
4621
|
},
|
|
3760
4622
|
"version": {
|
|
3761
4623
|
"type": "number",
|
|
@@ -3832,7 +4694,7 @@
|
|
|
3832
4694
|
},
|
|
3833
4695
|
"tool": {
|
|
3834
4696
|
"type": "string",
|
|
3835
|
-
"description": "Adapter tool id: network | storage | impersonate | route-events | query | time-machine, or 'scenario' for the engine-local 'wait' step. Never 'scenarios'
|
|
4697
|
+
"description": "Adapter tool id: network | storage | impersonate | route-events | query | time-machine, or 'scenario' for the engine-local 'wait' step. Never 'scenarios' \u2014 recursion is refused at pre-flight."
|
|
3836
4698
|
},
|
|
3837
4699
|
"action": {
|
|
3838
4700
|
"type": "string",
|
|
@@ -3892,7 +4754,7 @@
|
|
|
3892
4754
|
},
|
|
3893
4755
|
"folder": {
|
|
3894
4756
|
"type": "string",
|
|
3895
|
-
"description": "The flow this belongs to
|
|
4757
|
+
"description": "The flow this belongs to \u2014 \"Checkout\", \"Login\". Reuse a name from `folders` in listScenarios; a scenario nobody can find is a scenario nobody runs."
|
|
3896
4758
|
}
|
|
3897
4759
|
},
|
|
3898
4760
|
"required": [
|
|
@@ -3903,7 +4765,7 @@
|
|
|
3903
4765
|
},
|
|
3904
4766
|
"accept": {
|
|
3905
4767
|
"type": "boolean",
|
|
3906
|
-
"description": "IGNORED
|
|
4768
|
+
"description": "IGNORED \u2014 the handler never reads it. The save always lands as a draft."
|
|
3907
4769
|
}
|
|
3908
4770
|
},
|
|
3909
4771
|
"required": [
|
|
@@ -3913,14 +4775,14 @@
|
|
|
3913
4775
|
},
|
|
3914
4776
|
"effect": "write",
|
|
3915
4777
|
"release": "works",
|
|
3916
|
-
"description": "Returns {ok:true, savedAsDraft:true, id, message} on success, or {ok:false, error} / {ok:false, errors:[...]} on rejection
|
|
4778
|
+
"description": "Returns {ok:true, savedAsDraft:true, id, message} on success, or {ok:false, error} / {ok:false, errors:[...]} on rejection \u2014 it does not throw for validation. Rejects unless: `id` is a string, `name` is a string, `steps` is a non-empty array, every step has both `tool` and `action`, `vars` (if present) is an array, each step's params serialize under 64KB, and the whole scenario is under 256KB. Source is forced to \"draft\", author defaults to \"remote\", unsavedToRepo is set true; saving over an existing DRAFT id replaces it and bumps its version. Tell the user the draft is inert and someone must open Scenarios on the device and tap Accept to library (or call acceptDraft). The `accept` field is read into the params type but never used by the handler \u2014 passing it does nothing. Prefer a network `upsertOverrideRule` step over a `query.setQueryData` poke: the override survives refetch and restart, the poke dies on the next fetch. Supply `undoSteps` for anything that writes or removes storage, or deactivation will honestly report the key as unrestored.",
|
|
3917
4779
|
"requires": [
|
|
3918
4780
|
"Scenarios tool installed in the app"
|
|
3919
4781
|
]
|
|
3920
4782
|
},
|
|
3921
4783
|
{
|
|
3922
4784
|
"action": "acceptDraft",
|
|
3923
|
-
"summary": "Promote a reviewed draft into the runnable device library
|
|
4785
|
+
"summary": "Promote a reviewed draft into the runnable device library \u2014 this is the human-consent gate, so only do it when the user says to.",
|
|
3924
4786
|
"params": {
|
|
3925
4787
|
"type": "object",
|
|
3926
4788
|
"properties": {
|
|
@@ -3936,7 +4798,7 @@
|
|
|
3936
4798
|
},
|
|
3937
4799
|
"effect": "write",
|
|
3938
4800
|
"release": "works",
|
|
3939
|
-
"description": "Moves the draft out of the inbox with source:\"device\" and unsavedToRepo:true, making it runnable. Returns {ok:true, id}. THROWS `No draft \"<id>\"`. Drafts exist precisely so a scenario pushed from chat never becomes runnable on someone's phone without them seeing what it will do
|
|
4801
|
+
"description": "Moves the draft out of the inbox with source:\"device\" and unsavedToRepo:true, making it runnable. Returns {ok:true, id}. THROWS `No draft \"<id>\"`. Drafts exist precisely so a scenario pushed from chat never becomes runnable on someone's phone without them seeing what it will do \u2014 accepting on their behalf skips that review. Ask first, or tell them to tap \"Accept to library\" on the device instead.",
|
|
3940
4802
|
"requires": [
|
|
3941
4803
|
"Scenarios tool installed in the app"
|
|
3942
4804
|
]
|
|
@@ -3959,7 +4821,7 @@
|
|
|
3959
4821
|
},
|
|
3960
4822
|
"effect": "destructive",
|
|
3961
4823
|
"release": "works",
|
|
3962
|
-
"description": "Removes the draft and rewrites the persisted library. Returns {ok:true, id} even when no draft with that id existed
|
|
4824
|
+
"description": "Removes the draft and rewrites the persisted library. Returns {ok:true, id} even when no draft with that id existed \u2014 a success here is not proof anything was deleted. There is no undo: the steps are gone unless you exported the JSON first.",
|
|
3963
4825
|
"requires": [
|
|
3964
4826
|
"Scenarios tool installed in the app"
|
|
3965
4827
|
]
|
|
@@ -3976,7 +4838,7 @@
|
|
|
3976
4838
|
},
|
|
3977
4839
|
"params": {
|
|
3978
4840
|
"type": "object",
|
|
3979
|
-
"description": "Variable values, e.g. {storeId:'220', outOfStock:true}. Only string/number/boolean values are kept
|
|
4841
|
+
"description": "Variable values, e.g. {storeId:'220', outOfStock:true}. Only string/number/boolean values are kept \u2014 objects and arrays are silently dropped. Declared defaults fill in the rest.",
|
|
3980
4842
|
"additionalProperties": {
|
|
3981
4843
|
"type": [
|
|
3982
4844
|
"string",
|
|
@@ -3993,14 +4855,14 @@
|
|
|
3993
4855
|
},
|
|
3994
4856
|
"effect": "read",
|
|
3995
4857
|
"release": "works",
|
|
3996
|
-
"description": "Returns {willDo:[{label,durability}], values, ok, errors, warnings, conflict, expectedOutcome}. `values` is declared defaults merged with the values you supplied (supplied wins), and the labels are rendered THROUGH those values, so no {{placeholder}} survives. `conflict` is non-null when the currently-active scenario touches an overlapping URL pattern / storage key / the impersonation session
|
|
4858
|
+
"description": "Returns {willDo:[{label,durability}], values, ok, errors, warnings, conflict, expectedOutcome}. `values` is declared defaults merged with the values you supplied (supplied wins), and the labels are rendered THROUGH those values, so no {{placeholder}} survives. `conflict` is non-null when the currently-active scenario touches an overlapping URL pattern / storage key / the impersonation session \u2014 running anyway SWAPS (the active one is deactivated first). ok:false lists every blocking reason at once: draft not yet accepted, a step whose tool is not installed in this app, an unknown action, an undeclared or unset {{variable}}, a reload that is not the last step. Always run this before running an unfamiliar scenario. Changes nothing.",
|
|
3997
4859
|
"requires": [
|
|
3998
4860
|
"Scenarios tool installed in the app"
|
|
3999
4861
|
]
|
|
4000
4862
|
},
|
|
4001
4863
|
{
|
|
4002
4864
|
"action": "run",
|
|
4003
|
-
"summary": "Apply a scenario for real
|
|
4865
|
+
"summary": "Apply a scenario for real \u2014 installs network overrides, writes storage, starts impersonation, navigates \u2014 and resolve only once every step has landed.",
|
|
4004
4866
|
"params": {
|
|
4005
4867
|
"type": "object",
|
|
4006
4868
|
"properties": {
|
|
@@ -4010,7 +4872,7 @@
|
|
|
4010
4872
|
},
|
|
4011
4873
|
"params": {
|
|
4012
4874
|
"type": "object",
|
|
4013
|
-
"description": "Variable values, e.g. {storeId:'220'}. Only string/number/boolean values are kept
|
|
4875
|
+
"description": "Variable values, e.g. {storeId:'220'}. Only string/number/boolean values are kept \u2014 objects and arrays are silently dropped. Declared defaults fill in the rest.",
|
|
4014
4876
|
"additionalProperties": {
|
|
4015
4877
|
"type": [
|
|
4016
4878
|
"string",
|
|
@@ -4027,12 +4889,12 @@
|
|
|
4027
4889
|
},
|
|
4028
4890
|
"effect": "destructive",
|
|
4029
4891
|
"release": "throws",
|
|
4030
|
-
"description": "Atomic pre-flight runs first: on failure it returns {ok:false, preflightErrors:[...], conflict} and NOTHING is applied. Otherwise returns a RunReport {ok, scenarioId, name, steps:[{label, ok, error, skipped, ms}], effects:[{kind,label,...}]}. Steps run in order and STOP at the first failure
|
|
4031
|
-
"releaseNote": "packages/scenarios/src/store/scenariosStore.ts:98-104 (isRunnableInThisBuild)
|
|
4892
|
+
"description": "Atomic pre-flight runs first: on failure it returns {ok:false, preflightErrors:[...], conflict} and NOTHING is applied. Otherwise returns a RunReport {ok, scenarioId, name, steps:[{label, ok, error, skipped, ms}], effects:[{kind,label,...}]}. Steps run in order and STOP at the first failure \u2014 later steps are marked skipped, and the steps before it already applied, leaving the device in a partial state (call deactivate). Exclusive activation: running a different scenario while one is active deactivates the active one first. Budgets are 10s per step and 60s overall. Effects vary in reversibility \u2014 a network override rule is removed on deactivate, but a storage write has NO automatic inverse and will be reported as unrestored. Drafts cannot run. After this, the device shows a SIMULATED banner: anything the user observes is partly fake until deactivate.",
|
|
4893
|
+
"releaseNote": "packages/scenarios/src/store/scenariosStore.ts:98-104 (isRunnableInThisBuild) \u2014 in a release bundle checkBeforeRun injects \"Scenarios cannot run in a production build.\", so the adapter returns {ok:false, preflightErrors:[...]} and applies nothing; scenariosStore.run() at :346-348 throws the same message. Verified by packages/scenarios/src/__tests__/runGate.release.test.ts. Report the refusal \u2014 never claim the scenario was applied.",
|
|
4032
4894
|
"requires": [
|
|
4033
4895
|
"Scenarios tool installed in the app",
|
|
4034
4896
|
"every tool a step targets must be installed on the device (network, storage, impersonate, route-events, query, time-machine)",
|
|
4035
|
-
"a development build
|
|
4897
|
+
"a development build \u2014 release builds refuse"
|
|
4036
4898
|
]
|
|
4037
4899
|
},
|
|
4038
4900
|
{
|
|
@@ -4045,8 +4907,8 @@
|
|
|
4045
4907
|
},
|
|
4046
4908
|
"effect": "write",
|
|
4047
4909
|
"release": "empty",
|
|
4048
|
-
"description": "Returns {ok:true, report:{removedRules, stoppedImpersonation, clearedTransients, unrestoredStorageKeys[], undoStepsRan, errors[]}}. Deletes the override rules the run created, stops impersonation, and runs the author's undoSteps if any (which clears unrestoredStorageKeys). `unrestoredStorageKeys` names keys the app still holds scenario values for
|
|
4049
|
-
"releaseNote": "Not itself __DEV__-gated, but run is (packages/scenarios/src/store/scenariosStore.ts:98-104), so a release build can never have an active scenario
|
|
4910
|
+
"description": "Returns {ok:true, report:{removedRules, stoppedImpersonation, clearedTransients, unrestoredStorageKeys[], undoStepsRan, errors[]}}. Deletes the override rules the run created, stops impersonation, and runs the author's undoSteps if any (which clears unrestoredStorageKeys). `unrestoredStorageKeys` names keys the app still holds scenario values for \u2014 surface those verbatim to the user, they are real leftover state. Safe when nothing is active: returns an all-zero report. Also the right call after a failed/partial run.",
|
|
4911
|
+
"releaseNote": "Not itself __DEV__-gated, but run is (packages/scenarios/src/store/scenariosStore.ts:98-104), so a release build can never have an active scenario \u2014 expect an all-zero report rather than a real reversal.",
|
|
4050
4912
|
"requires": [
|
|
4051
4913
|
"Scenarios tool installed in the app"
|
|
4052
4914
|
]
|
|
@@ -4059,7 +4921,7 @@
|
|
|
4059
4921
|
"properties": {
|
|
4060
4922
|
"id": {
|
|
4061
4923
|
"type": "string",
|
|
4062
|
-
"description": "Scenario id from listScenarios `device`
|
|
4924
|
+
"description": "Scenario id from listScenarios `device` \u2014 code scenarios and drafts are unaffected."
|
|
4063
4925
|
}
|
|
4064
4926
|
},
|
|
4065
4927
|
"required": [
|
|
@@ -4069,7 +4931,7 @@
|
|
|
4069
4931
|
},
|
|
4070
4932
|
"effect": "destructive",
|
|
4071
4933
|
"release": "works",
|
|
4072
|
-
"description": "Deactivates it first if it happens to be the active scenario, then drops it from the device list and rewrites persistent storage. Returns {ok:true, id}. Only touches DEVICE-saved scenarios: an id that is a code scenario (registered via defineScenario in app source) or a draft still returns ok:true while deleting nothing
|
|
4934
|
+
"description": "Deactivates it first if it happens to be the active scenario, then drops it from the device list and rewrites persistent storage. Returns {ok:true, id}. Only touches DEVICE-saved scenarios: an id that is a code scenario (registered via defineScenario in app source) or a draft still returns ok:true while deleting nothing \u2014 code scenarios come back from the app bundle, drafts need discardDraft. Not recoverable; call export first if the definition matters.",
|
|
4073
4935
|
"requires": [
|
|
4074
4936
|
"Scenarios tool installed in the app"
|
|
4075
4937
|
]
|
|
@@ -4096,7 +4958,7 @@
|
|
|
4096
4958
|
},
|
|
4097
4959
|
"effect": "write",
|
|
4098
4960
|
"release": "works",
|
|
4099
|
-
"description": "Returns {ok:true, id, folder} where folder is the name it actually landed in, or null. A folder is just a string on the scenario
|
|
4961
|
+
"description": "Returns {ok:true, id, folder} where folder is the name it actually landed in, or null. A folder is just a string on the scenario \u2014 there is no folder record, so it exists exactly as long as something is filed in it and un-filing the last member makes it disappear. A name matching an existing folder case-insensitively snaps to that folder's spelling, so \"checkout\" joins \"Checkout\" instead of splitting it; read `folders` from listScenarios and reuse a name rather than inventing a near-duplicate. THROWS for a `code` scenario: those are filed by the `folder` field in defineScenario() in app source, so the QA menu looks the same on every device. Does not bump `version` \u2014 filing is organization, not an edit to what the scenario does.",
|
|
4100
4962
|
"requires": [
|
|
4101
4963
|
"Scenarios tool installed in the app"
|
|
4102
4964
|
]
|
|
@@ -4111,7 +4973,7 @@
|
|
|
4111
4973
|
},
|
|
4112
4974
|
"effect": "read",
|
|
4113
4975
|
"release": "works",
|
|
4114
|
-
"description": "Returns {folders:[...]}
|
|
4976
|
+
"description": "Returns {folders:[...]} \u2014 the same derived, case-insensitively deduped list the device's folder bar renders, across code, device AND draft scenarios. listScenarios already includes this as `folders`; call this only when the folder names are all you need.",
|
|
4115
4977
|
"requires": [
|
|
4116
4978
|
"Scenarios tool installed in the app"
|
|
4117
4979
|
]
|
|
@@ -4126,7 +4988,7 @@
|
|
|
4126
4988
|
},
|
|
4127
4989
|
"effect": "read",
|
|
4128
4990
|
"release": "works",
|
|
4129
|
-
"description": "Returns {active}
|
|
4991
|
+
"description": "Returns {active} \u2014 null, or {scenarioId, name, version, source, runAt, varValues, effects[{kind,label,...}], expiresAt}. Call this before trusting any data read off the device: if active is non-null, prices/inventory/user/session may be bent by the listed effects, and the answer to \"is this a bug?\" is probably \"a scenario is running\". Same hydration caveat as listScenarios \u2014 a cold read before the store has loaded @react_buoy_scenarios_active reports null.",
|
|
4130
4992
|
"requires": [
|
|
4131
4993
|
"Scenarios tool installed in the app"
|
|
4132
4994
|
]
|
|
@@ -4149,7 +5011,7 @@
|
|
|
4149
5011
|
},
|
|
4150
5012
|
"effect": "read",
|
|
4151
5013
|
"release": "works",
|
|
4152
|
-
"description": "Returns {json}
|
|
5014
|
+
"description": "Returns {json} \u2014 JSON.stringify of the full Scenario (steps, undoSteps, vars, metadata) with 2-space indent. Use it to move a device-authored scenario into app source as a defineScenario(...) entry, or to back one up before delete/discardDraft. THROWS `No scenario \"<id>\"` for an unknown id, including when the library has not hydrated yet.",
|
|
4153
5015
|
"requires": [
|
|
4154
5016
|
"Scenarios tool installed in the app"
|
|
4155
5017
|
]
|
|
@@ -4160,7 +5022,7 @@
|
|
|
4160
5022
|
{
|
|
4161
5023
|
"toolId": "perf-monitor",
|
|
4162
5024
|
"title": "Bench",
|
|
4163
|
-
"summary": "Measures runtime performance on the device: live JS/UI FPS, CPU and memory sampled every 250ms, plus recorded \"benchmark runs\" saved to disk and an automation mode that navigates a screen with different query params and ranks the variants. Reach for it to answer \"is this screen slow / which variant is faster / did my fix land\", not to inspect data
|
|
5025
|
+
"summary": "Measures runtime performance on the device: live JS/UI FPS, CPU and memory sampled every 250ms, plus recorded \"benchmark runs\" saved to disk and an automation mode that navigates a screen with different query params and ranks the variants. Reach for it to answer \"is this screen slow / which variant is faster / did my fix land\", not to inspect data \u2014 it reads no app state. Two traps: live metrics are all zeros until `setEnabled {enabled:true}` arms sampling, and `startAutomation` swallows every validation error, so a bad config returns ok and simply never runs.",
|
|
4164
5026
|
"actions": [
|
|
4165
5027
|
{
|
|
4166
5028
|
"action": "setEnabled",
|
|
@@ -4180,7 +5042,7 @@
|
|
|
4180
5042
|
},
|
|
4181
5043
|
"effect": "write",
|
|
4182
5044
|
"release": "works",
|
|
4183
|
-
"description": "Starts SILENT remote sampling: the device samples every 250ms and streams live.snapshot (current values + a 120-sample / 30s history ring) but its own on-device HUD stays hidden. This is the arming step for every live read
|
|
5045
|
+
"description": "Starts SILENT remote sampling: the device samples every 250ms and streams live.snapshot (current values + a 120-sample / 30s history ring) but its own on-device HUD stays hidden. This is the arming step for every live read \u2014 before it, live.snapshot is zeros with an empty history, and after setEnabled{false} the values freeze at their last tick. Always call with enabled:true before reading live metrics, and turn it back off when done (sampling costs a 250ms JS timer). Works with or without react-native-performance-toolkit; without the native module it falls back to a pure-JS sampler and cpuUsage reads 0."
|
|
4184
5046
|
},
|
|
4185
5047
|
{
|
|
4186
5048
|
"action": "startRecording",
|
|
@@ -4192,7 +5054,7 @@
|
|
|
4192
5054
|
},
|
|
4193
5055
|
"effect": "write",
|
|
4194
5056
|
"release": "works",
|
|
4195
|
-
"description": "Begins accumulating perf samples into an in-memory run. The device auto-names it and tags the current route (when @buoy-gg/route-events is installed); the desktop/agent cannot pass a name here
|
|
5057
|
+
"description": "Begins accumulating perf samples into an in-memory run. The device auto-names it and tags the current route (when @buoy-gg/route-events is installed); the desktop/agent cannot pass a name here \u2014 naming happens at savePending. Self-arms sampling, so setEnabled is not required first. No-op if a recording is already active. Per-component render capture rides along only when @buoy-gg/highlight-updates is installed AND the build is a dev build \u2014 in a release build CommitProfiler.isSupported() returns false (packages/highlight-updates/src/highlight-updates/utils/CommitProfiler.ts:458), so the saved report has FPS/CPU/memory but renders:null plus a diagnostic explaining why. Pair with stopRecording + savePending.",
|
|
4196
5058
|
"requires": [
|
|
4197
5059
|
"@buoy-gg/highlight-updates for render-commit data (dev builds only)"
|
|
4198
5060
|
]
|
|
@@ -4207,7 +5069,7 @@
|
|
|
4207
5069
|
},
|
|
4208
5070
|
"effect": "write",
|
|
4209
5071
|
"release": "works",
|
|
4210
|
-
"description": "Stops sampling for the run and parks the report in memory instead of persisting it. Returns { defaultName, sampleCount } so you can prompt for a name, or null when there was nothing to keep (no active recording, or zero samples because it was stopped almost immediately). The held run also shows up as live.pendingSave on the next snapshot. Nothing is on disk until savePending
|
|
5072
|
+
"description": "Stops sampling for the run and parks the report in memory instead of persisting it. Returns { defaultName, sampleCount } so you can prompt for a name, or null when there was nothing to keep (no active recording, or zero samples because it was stopped almost immediately). The held run also shows up as live.pendingSave on the next snapshot. Nothing is on disk until savePending \u2014 a stopRecording followed by neither savePending nor discardPending leaves the run dangling and it is lost on reload."
|
|
4211
5073
|
},
|
|
4212
5074
|
{
|
|
4213
5075
|
"action": "savePending",
|
|
@@ -4240,7 +5102,7 @@
|
|
|
4240
5102
|
},
|
|
4241
5103
|
"effect": "destructive",
|
|
4242
5104
|
"release": "works",
|
|
4243
|
-
"description": "Drops the report held by stopRecording and clears live.pendingSave. The samples are gone
|
|
5105
|
+
"description": "Drops the report held by stopRecording and clears live.pendingSave. The samples are gone \u2014 they were never written to disk and cannot be recovered. Use when the user cancels the name prompt or the run was junk."
|
|
4244
5106
|
},
|
|
4245
5107
|
{
|
|
4246
5108
|
"action": "mark",
|
|
@@ -4257,7 +5119,7 @@
|
|
|
4257
5119
|
},
|
|
4258
5120
|
"effect": "write",
|
|
4259
5121
|
"release": "works",
|
|
4260
|
-
"description": "Appends { timestamp, label } to the active run so the timeline view can line a spike up with an action (\"tapped submit\", \"list scrolled\"). SILENT NO-OP when no recording is active
|
|
5122
|
+
"description": "Appends { timestamp, label } to the active run so the timeline view can line a spike up with an action (\"tapped submit\", \"list scrolled\"). SILENT NO-OP when no recording is active \u2014 BenchmarkRecorder.mark returns immediately if !isRecording, so this returns ok even when nothing was recorded. Check live.isRecording first before telling a user a marker landed."
|
|
4261
5123
|
},
|
|
4262
5124
|
{
|
|
4263
5125
|
"action": "startAutomation",
|
|
@@ -4267,7 +5129,7 @@
|
|
|
4267
5129
|
"properties": {
|
|
4268
5130
|
"config": {
|
|
4269
5131
|
"type": "object",
|
|
4270
|
-
"description": "Full batch config. Not merged with the device's saved settings
|
|
5132
|
+
"description": "Full batch config. Not merged with the device's saved settings \u2014 whatever you omit takes the runner's own fallback, so read getAutomationConfig first if you want the user's tuned profile.",
|
|
4271
5133
|
"properties": {
|
|
4272
5134
|
"targetRoute": {
|
|
4273
5135
|
"type": "string",
|
|
@@ -4326,7 +5188,7 @@
|
|
|
4326
5188
|
},
|
|
4327
5189
|
"coolDownMs": {
|
|
4328
5190
|
"type": "number",
|
|
4329
|
-
"description": "Idle between runs and cases so thermals recover. Device default 8000
|
|
5191
|
+
"description": "Idle between runs and cases so thermals recover. Device default 8000 \u2014 raising this is the fix when later cases score worse than earlier ones."
|
|
4330
5192
|
},
|
|
4331
5193
|
"discardWarmupRuns": {
|
|
4332
5194
|
"type": "number",
|
|
@@ -4380,12 +5242,12 @@
|
|
|
4380
5242
|
},
|
|
4381
5243
|
"effect": "destructive",
|
|
4382
5244
|
"release": "works",
|
|
4383
|
-
"description": "Fire-and-forget. The DEVICE owns the loop: for each case it bounces to bounceRoute, navigates to targetRoute with that case's query params, settles, records for perCaseDurationMs, saves the run tagged with a shared batchId, cools down, and repeats runsPerCase times. Returns immediately with no result
|
|
5245
|
+
"description": "Fire-and-forget. The DEVICE owns the loop: for each case it bounces to bounceRoute, navigates to targetRoute with that case's query params, settles, records for perCaseDurationMs, saves the run tagged with a shared batchId, cools down, and repeats runsPerCase times. Returns immediately with no result \u2014 poll live.automation.phase (idle/navigating/recording/reloading/done/cancelled), live.automationCompleted, or the growing index to follow it; a batch takes minutes. THE BIG TRAP: every failure is swallowed by the adapter (`void AutomationRunner.start(config).catch(() => {})`), so it returns ok and nothing happens when config is missing, cases is empty, expo-router is absent, targetRoute is empty, or any case route equals bounceRoute (validateConfig throws on that \u2014 no remount would happen). If live.automation.phase never leaves \"idle\", the config was rejected, not slow. reloadBetweenCases tears down the JS realm between cases and needs <AutomationResumer/> mounted at the router root; in a release build without expo-updates the reload throws and the batch quietly finishes in-process instead. captureRenders yields no render data in a release build (React DevTools hook is dev-only).",
|
|
4384
5246
|
"requires": [
|
|
4385
|
-
"expo-router (navigation)
|
|
5247
|
+
"expo-router (navigation) \u2014 without it this is a silent no-op",
|
|
4386
5248
|
"@buoy-gg/route-events for reliable navigation waits (otherwise it falls back to a fixed sleep)",
|
|
4387
5249
|
"<AutomationResumer/> mounted at the router root when reloadBetweenCases is true",
|
|
4388
|
-
"a dev build (DevSettings) OR expo-updates installed when reloadBetweenCases is true
|
|
5250
|
+
"a dev build (DevSettings) OR expo-updates installed when reloadBetweenCases is true \u2014 in a release build without expo-updates the reload silently fails and the batch continues in-process with leak risk",
|
|
4389
5251
|
"@buoy-gg/highlight-updates + a dev build for captureRenders data"
|
|
4390
5252
|
]
|
|
4391
5253
|
},
|
|
@@ -4399,7 +5261,7 @@
|
|
|
4399
5261
|
},
|
|
4400
5262
|
"effect": "write",
|
|
4401
5263
|
"release": "works",
|
|
4402
|
-
"description": "Asks the runner to stop after the current step and clears the persisted pending-batch state so a reload cannot resume it. No-op when no batch is running. Runs already saved remain on disk under the batch's batchId
|
|
5264
|
+
"description": "Asks the runner to stop after the current step and clears the persisted pending-batch state so a reload cannot resume it. No-op when no batch is running. Runs already saved remain on disk under the batch's batchId \u2014 delete them with deleteBatch if you want the batch gone. The status settles to phase \"cancelled\" and stays sticky in live.automationCompleted until acknowledgeAutomation."
|
|
4403
5265
|
},
|
|
4404
5266
|
{
|
|
4405
5267
|
"action": "acknowledgeAutomation",
|
|
@@ -4411,7 +5273,7 @@
|
|
|
4411
5273
|
},
|
|
4412
5274
|
"effect": "write",
|
|
4413
5275
|
"release": "works",
|
|
4414
|
-
"description": "The handshake that says \"I have seen this batch finish\": clears live.automationCompleted and puts live.automation back to phase \"idle\" so the same batch cannot re-trigger a navigation. Call it once after you have read the results, and on tool-open to discard a stale prior completion. Careful: it is global
|
|
5276
|
+
"description": "The handshake that says \"I have seen this batch finish\": clears live.automationCompleted and puts live.automation back to phase \"idle\" so the same batch cannot re-trigger a navigation. Call it once after you have read the results, and on tool-open to discard a stale prior completion. Careful: it is global \u2014 clearing it also blinds any other connected dashboard that was waiting on that flag, which is why index-based completion detection (watching new batchIds appear) is more reliable than polling automationCompleted."
|
|
4415
5277
|
},
|
|
4416
5278
|
{
|
|
4417
5279
|
"action": "refreshIndex",
|
|
@@ -4423,7 +5285,7 @@
|
|
|
4423
5285
|
},
|
|
4424
5286
|
"effect": "read",
|
|
4425
5287
|
"release": "works",
|
|
4426
|
-
"description": "Re-reads @react_buoy/perf-monitor/index from disk and re-emits the snapshot. Returns nothing
|
|
5288
|
+
"description": "Re-reads @react_buoy/perf-monitor/index from disk and re-emits the snapshot. Returns nothing \u2014 read the result from snapshot.index (newest first: id, name, createdAt, route, durationMs, sampleCount, jsFpsAvg, uiFpsAvg, cpuAvg, memMaxMb, jank counts, plus batchId/batchIndex/caseId/runIndex/isMedianRun and renderCommits/renderWasted/topRenderers for batch runs). Call it on tool-open: if the device booted before storage was ready the cached index can be empty, and without this it stays empty until the next save or delete."
|
|
4427
5289
|
},
|
|
4428
5290
|
{
|
|
4429
5291
|
"action": "getAutomationConfig",
|
|
@@ -4435,7 +5297,7 @@
|
|
|
4435
5297
|
},
|
|
4436
5298
|
"effect": "read",
|
|
4437
5299
|
"release": "works",
|
|
4438
|
-
"description": "Loads and returns the persisted AutomationConfig from @react_buoy/perf-monitor/automation
|
|
5300
|
+
"description": "Loads and returns the persisted AutomationConfig from @react_buoy/perf-monitor/automation \u2014 perCaseDurationMs, settleMs, navTimeoutMs, runsPerCase, coolDownMs, discardWarmupRuns/Case, shuffleCases, reloadBetweenCases, reloadStrategy, captureRenders, plus the user's last cases and targetRoute. Do this before startAutomation so a run inherits the user's tuned pacing instead of invented defaults, and ALWAYS before setAutomationConfig so you can merge rather than clobber. Note the snapshot's own automationConfig field omits `cases`; this action returns them."
|
|
4439
5301
|
},
|
|
4440
5302
|
{
|
|
4441
5303
|
"action": "setAutomationConfig",
|
|
@@ -4445,7 +5307,7 @@
|
|
|
4445
5307
|
"properties": {
|
|
4446
5308
|
"config": {
|
|
4447
5309
|
"type": "object",
|
|
4448
|
-
"description": "The COMPLETE config to persist
|
|
5310
|
+
"description": "The COMPLETE config to persist \u2014 merge your changes onto the getAutomationConfig result, since omitted fields are reset to defaults (and omitted `cases` are erased).",
|
|
4449
5311
|
"properties": {
|
|
4450
5312
|
"targetRoute": {
|
|
4451
5313
|
"type": "string",
|
|
@@ -4549,7 +5411,7 @@
|
|
|
4549
5411
|
},
|
|
4550
5412
|
"effect": "destructive",
|
|
4551
5413
|
"release": "works",
|
|
4552
|
-
"description": "Sanitizes and writes the whole config to @react_buoy/perf-monitor/automation, then returns the stored result. THIS IS A FULL REPLACE: sanitize() rebuilds every field, so a config passed without `cases` wipes the user's saved case matrix and one without `targetRoute` blanks it (packages/perf-monitor/src/perf-monitor/utils/automationSettings.ts sanitize + saveAutomationConfig). Read getAutomationConfig, spread your overrides onto it, and send the merged object. Numbers are clamped (perCaseDurationMs 500-120000, runsPerCase 1-10, coolDownMs 0-30000, settleMs 0-30000, navTimeoutMs 500-60000). Only for changes the user wants to STICK
|
|
5414
|
+
"description": "Sanitizes and writes the whole config to @react_buoy/perf-monitor/automation, then returns the stored result. THIS IS A FULL REPLACE: sanitize() rebuilds every field, so a config passed without `cases` wipes the user's saved case matrix and one without `targetRoute` blanks it (packages/perf-monitor/src/perf-monitor/utils/automationSettings.ts sanitize + saveAutomationConfig). Read getAutomationConfig, spread your overrides onto it, and send the merged object. Numbers are clamped (perCaseDurationMs 500-120000, runsPerCase 1-10, coolDownMs 0-30000, settleMs 0-30000, navTimeoutMs 500-60000). Only for changes the user wants to STICK \u2014 for a one-off, pass overrides to startAutomation instead. Passing no config is a no-op that just returns the current one."
|
|
4553
5415
|
},
|
|
4554
5416
|
{
|
|
4555
5417
|
"action": "loadReport",
|
|
@@ -4569,7 +5431,7 @@
|
|
|
4569
5431
|
},
|
|
4570
5432
|
"effect": "read",
|
|
4571
5433
|
"release": "works",
|
|
4572
|
-
"description": "Reads @react_buoy/perf-monitor/report/<id> and returns the whole BenchmarkReport: metadata (name, route, batch fields, environmentSignals like thermalState/battery/network), every 250ms sample, markers, diagnostics, aggregate stats (avg/p95 FPS-CPU-memory, jank counts) and `renders` when render capture ran. Returns null for an unknown id. Payloads are large
|
|
5434
|
+
"description": "Reads @react_buoy/perf-monitor/report/<id> and returns the whole BenchmarkReport: metadata (name, route, batch fields, environmentSignals like thermalState/battery/network), every 250ms sample, markers, diagnostics, aggregate stats (avg/p95 FPS-CPU-memory, jank counts) and `renders` when render capture ran. Returns null for an unknown id. Payloads are large \u2014 use the index summary fields for lists and only load a report when you need the timeline or a head-to-head comparison."
|
|
4573
5435
|
},
|
|
4574
5436
|
{
|
|
4575
5437
|
"action": "deleteReport",
|
|
@@ -4589,7 +5451,7 @@
|
|
|
4589
5451
|
},
|
|
4590
5452
|
"effect": "destructive",
|
|
4591
5453
|
"release": "works",
|
|
4592
|
-
"description": "Removes @react_buoy/perf-monitor/report/<id> and drops its index entry, then re-emits the index. Irreversible
|
|
5454
|
+
"description": "Removes @react_buoy/perf-monitor/report/<id> and drops its index entry, then re-emits the index. Irreversible \u2014 the samples are gone. Deleting one run of a multi-run case leaves the rest of the batch in place, which can skew a later re-ranking of that batchId; prefer deleteBatch for a whole batch."
|
|
4593
5455
|
},
|
|
4594
5456
|
{
|
|
4595
5457
|
"action": "deleteBatch",
|
|
@@ -4609,7 +5471,7 @@
|
|
|
4609
5471
|
},
|
|
4610
5472
|
"effect": "destructive",
|
|
4611
5473
|
"release": "works",
|
|
4612
|
-
"description": "Cascade-deletes all reports whose index entry carries this batchId and returns how many were removed (0 when the batchId matches nothing). Irreversible. This is the right cleanup after a botched or cancelled batch
|
|
5474
|
+
"description": "Cascade-deletes all reports whose index entry carries this batchId and returns how many were removed (0 when the batchId matches nothing). Irreversible. This is the right cleanup after a botched or cancelled batch \u2014 it removes every run and failure placeholder in one serialized index mutation, instead of fanning out parallel deleteReport calls."
|
|
4613
5475
|
},
|
|
4614
5476
|
{
|
|
4615
5477
|
"action": "clearAll",
|
|
@@ -4621,15 +5483,15 @@
|
|
|
4621
5483
|
},
|
|
4622
5484
|
"effect": "destructive",
|
|
4623
5485
|
"release": "works",
|
|
4624
|
-
"description": "Wipes all @react_buoy/perf-monitor/report/* blobs and empties the index. Irreversible and total
|
|
5486
|
+
"description": "Wipes all @react_buoy/perf-monitor/report/* blobs and empties the index. Irreversible and total \u2014 every manual recording and every batch, including baselines someone may be comparing against. Only run on an explicit, unambiguous request to clear all recordings; deleteReport or deleteBatch cover every narrower case."
|
|
4625
5487
|
}
|
|
4626
5488
|
],
|
|
4627
|
-
"unavailableWhen": "The host app does not have @buoy-gg/perf-monitor installed
|
|
5489
|
+
"unavailableWhen": "The host app does not have @buoy-gg/perf-monitor installed \u2014 FloatingDevTools only registers this adapter when the package resolves (packages/devtools-floating-menu/src/floatingMenu/autoExternalSync.tsx:382). Automation actions additionally need expo-router; without it startAutomation is a silent no-op."
|
|
4628
5490
|
},
|
|
4629
5491
|
{
|
|
4630
5492
|
"toolId": "assets",
|
|
4631
5493
|
"title": "Assets",
|
|
4632
|
-
"summary": "Inventory of what the app SHIPS
|
|
5494
|
+
"summary": "Inventory of what the app SHIPS \u2014 every bundled asset (images, fonts, video, audio, data) with dimensions, @1x/@2x/@3x scale coverage, content hash, byte size, loaded-vs-never-loaded state, audit findings (duplicates, unused, WebP candidates, over-decode, heavy GIFs/fonts, scale gaps), and a diff vs a saved baseline. Reach for it for bundle-size questions (\"what's making the app big\", \"did this branch add megabytes\", \"which images ship but are never used\"); use the images tool instead for what the app actually RENDERS. Nothing here is __DEV__-gated (zero `__DEV__` in packages/assets/src), but the Metro dev server is what supplies the full-bundle graph and real byte sizes \u2014 in an embedded/release bundle the inventory shrinks to registry-only records with estimated sizes. Swift inventories loose resources in the main bundle and explicitly registered app resource bundles. Buoy resource bundles are excluded. Compiled Assets.car files are aggregate records, not per-image inventory. Native loaded status requires host calls to BuoyAssets.markLoaded; unobserved does not mean unused. Native scale and decoded-memory insight arrays are currently empty. Check scanStatus.coverage and warnings.",
|
|
4633
5495
|
"actions": [
|
|
4634
5496
|
{
|
|
4635
5497
|
"action": "list",
|
|
@@ -4655,7 +5517,7 @@
|
|
|
4655
5517
|
},
|
|
4656
5518
|
"loadedOnly": {
|
|
4657
5519
|
"type": "boolean",
|
|
4658
|
-
"description": "true keeps ONLY assets loaded at runtime. There is no 'unusedOnly' param here
|
|
5520
|
+
"description": "true keeps ONLY assets loaded at runtime. There is no 'unusedOnly' param here \u2014 for shipped-but-never-loaded assets read insights.neverLoaded (ids) or fetch with a high limit and filter loaded===false."
|
|
4659
5521
|
}
|
|
4660
5522
|
},
|
|
4661
5523
|
"required": [],
|
|
@@ -4663,7 +5525,7 @@
|
|
|
4663
5525
|
},
|
|
4664
5526
|
"effect": "read",
|
|
4665
5527
|
"release": "works",
|
|
4666
|
-
"description": "Returns { stats, scanStatus, insights, diff, total, records }. `records` is sorted by size descending and sliced to `limit`; `total` is the count AFTER kind/loadedOnly filtering but BEFORE the slice. Each record: { id, key, registryId, name, type, kind, hash, location, width, height, scales, uri, loaded, sizeBytes, sizeSource }. `sizeSource` is \"measured\" (real bytes from the Metro dev server) or \"estimate\" (decoded RGBA memory, width*scale*height*scale*4)
|
|
5528
|
+
"description": "Returns { stats, scanStatus, insights, diff, total, records }. `records` is sorted by size descending and sliced to `limit`; `total` is the count AFTER kind/loadedOnly filtering but BEFORE the slice. Each record: { id, key, registryId, name, type, kind, hash, location, width, height, scales, uri, loaded, sizeBytes, sizeSource }. `sizeSource` is \"measured\" (real bytes from the Metro dev server) or \"estimate\" (decoded RGBA memory, width*scale*height*scale*4) \u2014 never present them as the same number. `insights` carries id lists: duplicates (grouped by content hash, with wastedBytes), neverLoaded, webpCandidates (png/jpg >= 50KB), overDecode (decodes to >2 screenfuls), heavyGifs (>= 100KB), heavyFonts (>= 150KB), scaleGaps, scaleAnomalies. If `scanStatus.lastScanAt` is null or `graphCount` is null, run `rescan` first \u2014 the inventory is registry-only until the Metro graph is merged. If `scanStatus.measuring` is true, byte totals are partial; re-run shortly. Swift inventories loose resources in the main bundle and explicitly registered app resource bundles. Buoy resource bundles are excluded. Compiled Assets.car files are aggregate records, not per-image inventory. Native loaded status requires host calls to BuoyAssets.markLoaded; unobserved does not mean unused. Native scale and decoded-memory insight arrays are currently empty. Check scanStatus.coverage and warnings.",
|
|
4667
5529
|
"requires": [
|
|
4668
5530
|
"@buoy-gg/assets imported in the app",
|
|
4669
5531
|
"Metro dev server (for full-bundle coverage, never-loaded detection and measured bytes)"
|
|
@@ -4677,7 +5539,7 @@
|
|
|
4677
5539
|
"properties": {
|
|
4678
5540
|
"id": {
|
|
4679
5541
|
"type": "number",
|
|
4680
|
-
"description": "Numeric record id from a `list` response. A non-number throws 'Missing numeric `id` param'; an unknown id throws 'Asset record N not found
|
|
5542
|
+
"description": "Numeric record id from a `list` response. A non-number throws 'Missing numeric `id` param'; an unknown id throws 'Asset record N not found \u2014 re-run the list action for current ids.'"
|
|
4681
5543
|
}
|
|
4682
5544
|
},
|
|
4683
5545
|
"required": [
|
|
@@ -4687,7 +5549,7 @@
|
|
|
4687
5549
|
},
|
|
4688
5550
|
"effect": "read",
|
|
4689
5551
|
"release": "works",
|
|
4690
|
-
"description": "Everything `list` returns for that record plus origin (\"registry\" | \"graph\"), fileSystemLocation (the source directory on the dev machine
|
|
5552
|
+
"description": "Everything `list` returns for that record plus origin (\"registry\" | \"graph\"), fileSystemLocation (the source directory on the dev machine \u2014 Metro-graph only), files (per-scale source file paths), sizesByScale ({ \"1\": bytes, \"2\": bytes, ... }), and estDecodedBytes. Use it to answer \"where does this file live\" and \"which scale variant is the fat one\". Ids come from `list` and are per-session: they are stable across a `rescan` (records are keyed by location/name.type) but NOT across `clearRecords`, which renumbers \u2014 re-run `list` before calling this if anything was cleared.",
|
|
4691
5553
|
"requires": [
|
|
4692
5554
|
"@buoy-gg/assets imported in the app",
|
|
4693
5555
|
"a prior `list` call for a valid id"
|
|
@@ -4703,7 +5565,7 @@
|
|
|
4703
5565
|
},
|
|
4704
5566
|
"effect": "write",
|
|
4705
5567
|
"release": "works",
|
|
4706
|
-
"description": "Run this FIRST when the inventory looks empty, stale, or has no never-loaded data. Returns { ok: true, scanStatus } immediately; the measurement pass is fired with `void measureSizes()` and keeps running after the response, so scanStatus.measuring is usually true on return
|
|
5568
|
+
"description": "Run this FIRST when the inventory looks empty, stale, or has no never-loaded data. Returns { ok: true, scanStatus } immediately; the measurement pass is fired with `void measureSizes()` and keeps running after the response, so scanStatus.measuring is usually true on return \u2014 call `list` or `getScanStatus` again a moment later for final byte totals. Merges into existing records (ids and any measured sizes are preserved); it never wipes. `ok:true` only means the scan ran \u2014 read scanStatus.graphError and graphCount to see whether the bundle graph actually loaded."
|
|
4707
5569
|
},
|
|
4708
5570
|
{
|
|
4709
5571
|
"action": "measureSizes",
|
|
@@ -4715,8 +5577,8 @@
|
|
|
4715
5577
|
},
|
|
4716
5578
|
"effect": "write",
|
|
4717
5579
|
"release": "empty",
|
|
4718
|
-
"description": "Returns { ok, measured, total } where ok is `measured > 0`. Fetches every scale variant (HEAD for Content-Length, GET-blob fallback) 4 at a time and sums them onto the record as sizeSource:\"measured\". Records it cannot fetch are marked sizeSource:\"estimate\". Safe to re-run
|
|
4719
|
-
"releaseNote": "packages/assets/src/capture/measure.ts:74-75
|
|
5580
|
+
"description": "Returns { ok, measured, total } where ok is `measured > 0`. Fetches every scale variant (HEAD for Content-Length, GET-blob fallback) 4 at a time and sums them onto the record as sizeSource:\"measured\". Records it cannot fetch are marked sizeSource:\"estimate\". Safe to re-run \u2014 it only visits records that aren't measured yet. If a call is already in flight it returns the current counts immediately without starting a second pass.",
|
|
5581
|
+
"releaseNote": "packages/assets/src/capture/measure.ts:74-75 \u2014 variantURLs() needs an http(s) resolved URI or a live dev-server origin; in a release bundle assets resolve to file:// paths or Android resource ids, so it returns [] and measure.ts:118 marks every record \"estimate\". The result is { ok:false, measured:0, total:N }. Byte sizes are simply not obtainable from JS in a release build \u2014 say that instead of retrying.",
|
|
4720
5582
|
"requires": [
|
|
4721
5583
|
"Metro dev server reachable from the device"
|
|
4722
5584
|
]
|
|
@@ -4731,7 +5593,7 @@
|
|
|
4731
5593
|
},
|
|
4732
5594
|
"effect": "write",
|
|
4733
5595
|
"release": "works",
|
|
4734
|
-
"description": "Returns { ok: true, savedAt, assets }. Writes a slim {hash, sizeBytes, name, type} snapshot per asset to persistentStorage under '@react_buoy/assets/baseline' (FileSystem -> AsyncStorage -> memory). The workflow is: measure sizes, save baseline, make the change, then `list`/`getDiff` reports added / removed / grown (>=1KB) / netBytes. Two warnings worth surfacing before calling: (1) it silently replaces a previously saved baseline and the old one cannot be recovered
|
|
5596
|
+
"description": "Returns { ok: true, savedAt, assets }. Writes a slim {hash, sizeBytes, name, type} snapshot per asset to persistentStorage under '@react_buoy/assets/baseline' (FileSystem -> AsyncStorage -> memory). The workflow is: measure sizes, save baseline, make the change, then `list`/`getDiff` reports added / removed / grown (>=1KB) / netBytes. Two warnings worth surfacing before calling: (1) it silently replaces a previously saved baseline and the old one cannot be recovered \u2014 ask first if a comparison may already be in progress; (2) only sizeSource===\"measured\" bytes are stored, so saving before a measurement pass finishes produces a baseline whose later netBytes is null."
|
|
4735
5597
|
},
|
|
4736
5598
|
{
|
|
4737
5599
|
"action": "clearBaseline",
|
|
@@ -4743,7 +5605,7 @@
|
|
|
4743
5605
|
},
|
|
4744
5606
|
"effect": "destructive",
|
|
4745
5607
|
"release": "works",
|
|
4746
|
-
"description": "Returns { ok: true, message: \"Baseline cleared\" }. Removes '@react_buoy/assets/baseline' from persistent storage and nulls it in memory; after this `getDiff` returns null and `list().diff` is null. Irreversible
|
|
5608
|
+
"description": "Returns { ok: true, message: \"Baseline cleared\" }. Removes '@react_buoy/assets/baseline' from persistent storage and nulls it in memory; after this `getDiff` returns null and `list().diff` is null. Irreversible \u2014 the saved snapshot is gone, and re-saving captures TODAY's inventory, not the one you deleted. Only call it when the user explicitly wants to stop comparing or start a fresh comparison."
|
|
4747
5609
|
},
|
|
4748
5610
|
{
|
|
4749
5611
|
"action": "getDiff",
|
|
@@ -4755,7 +5617,7 @@
|
|
|
4755
5617
|
},
|
|
4756
5618
|
"effect": "read",
|
|
4757
5619
|
"release": "works",
|
|
4758
|
-
"description": "Returns { baselineSavedAt, added: number[], removed: [{key, name, type}], grown: [{id, deltaBytes}] sorted biggest-first, netBytes } or null if nothing was ever saved with saveBaseline. `grown` only counts increases of >=1KB (below that is codec noise) and only for assets measured both then and now. netBytes is null whenever no asset pair was comparable
|
|
5620
|
+
"description": "Returns { baselineSavedAt, added: number[], removed: [{key, name, type}], grown: [{id, deltaBytes}] sorted biggest-first, netBytes } or null if nothing was ever saved with saveBaseline. `grown` only counts increases of >=1KB (below that is codec noise) and only for assets measured both then and now. netBytes is null whenever no asset pair was comparable \u2014 that means 'sizes unknown', NOT 'no change'; `list` already embeds this same object as `diff`, so calling both is redundant."
|
|
4759
5621
|
},
|
|
4760
5622
|
{
|
|
4761
5623
|
"action": "clearRecords",
|
|
@@ -4767,7 +5629,7 @@
|
|
|
4767
5629
|
},
|
|
4768
5630
|
"effect": "destructive",
|
|
4769
5631
|
"release": "works",
|
|
4770
|
-
"description": "Returns { ok: true, message: \"Inventory cleared
|
|
5632
|
+
"description": "Returns { ok: true, message: \"Inventory cleared \u2014 rescan to rebuild\" }. Empties records/byKey/byId and resets measuredCount and graphCount. Everything measured is lost and must be re-fetched from the dev server, and because the id counter is NOT reset, a following `rescan` assigns brand-new ids \u2014 any id from an earlier `list` is dead afterwards. Never a diagnostic step; only run it when the user asks for a clean slate. The saved baseline survives (use clearBaseline for that)."
|
|
4771
5633
|
},
|
|
4772
5634
|
{
|
|
4773
5635
|
"action": "getScanStatus",
|
|
@@ -4779,15 +5641,15 @@
|
|
|
4779
5641
|
},
|
|
4780
5642
|
"effect": "read",
|
|
4781
5643
|
"release": "works",
|
|
4782
|
-
"description": "Returns { registryAvailable, patched, registryCount, graphSupported, graphCount, graphError, lastScanAt, measuring, measuredCount, fontFamilies: string[], localAssetCount }. Use it to explain a thin or surprising inventory before drawing conclusions: registryAvailable:false means the RN asset registry could not be required at all; graphCount:null with a graphError means only runtime-registered assets are known (so 'never loaded' is unanswerable); measuring:true means byte totals are still landing. lastScanAt:null means nothing has scanned yet
|
|
5644
|
+
"description": "Returns { registryAvailable, patched, registryCount, graphSupported, graphCount, graphError, lastScanAt, measuring, measuredCount, fontFamilies: string[], localAssetCount }. Use it to explain a thin or surprising inventory before drawing conclusions: registryAvailable:false means the RN asset registry could not be required at all; graphCount:null with a graphError means only runtime-registered assets are known (so 'never loaded' is unanswerable); measuring:true means byte totals are still landing. lastScanAt:null means nothing has scanned yet \u2014 call rescan. Swift inventories loose resources in the main bundle and explicitly registered app resource bundles. Buoy resource bundles are excluded. Compiled Assets.car files are aggregate records, not per-image inventory. Native loaded status requires host calls to BuoyAssets.markLoaded; unobserved does not mean unused. Native scale and decoded-memory insight arrays are currently empty. Check scanStatus.coverage and warnings."
|
|
4783
5645
|
}
|
|
4784
5646
|
],
|
|
4785
|
-
"unavailableWhen": "The app doesn't import @buoy-gg/assets (autoExternalSync only registers the adapter when the module resolves
|
|
5647
|
+
"unavailableWhen": "The app doesn't import @buoy-gg/assets (autoExternalSync only registers the adapter when the module resolves \u2014 packages/devtools-floating-menu/src/floatingMenu/autoExternalSync.tsx:391), or the app is a release/production JS bundle that didn't opt in with `externalSync.enableInRelease` plus a real Pro license, in which case the device never connects to the broker at all."
|
|
4786
5648
|
},
|
|
4787
5649
|
{
|
|
4788
5650
|
"toolId": "tv-remote",
|
|
4789
5651
|
"title": "TV Remote",
|
|
4790
|
-
"summary": "Capture-only observer for Apple TV / Android TV apps: it reports the D-pad, select, menu, media and long-press events the app ACTUALLY received, which is how you tell \"the app handled that press\" apart from \"something swallowed it\". It cannot press anything
|
|
5652
|
+
"summary": "Capture-only observer for Apple TV / Android TV apps: it reports the D-pad, select, menu, media and long-press events the app ACTUALLY received, which is how you tell \"the app handled that press\" apart from \"something swallowed it\". It cannot press anything \u2014 presses are injected host-side by Buoy Desktop (`adb shell input keyevent` / `idb ui key`), so never tell a user this tool moved focus. Reach for it to arm capture before a remote press happens, then poll getEventsSince for the echo. Inert on phones/tablets/web.",
|
|
4791
5653
|
"actions": [
|
|
4792
5654
|
{
|
|
4793
5655
|
"action": "arm",
|
|
@@ -4800,7 +5662,7 @@
|
|
|
4800
5662
|
},
|
|
4801
5663
|
"effect": "write",
|
|
4802
5664
|
"release": "works",
|
|
4803
|
-
"description": "Attaches the native TVEventHandler listener and returns the fresh TvRemoteState: { supported, platform, captureArmed, listening, menuCaptureArmed, seq, recent[] }. CHECK `listening`, not `captureArmed`: on a non-TV build or an RN build without TVEventHandler, arm() still sets captureArmed:true while listening stays false and no event will ever arrive
|
|
5665
|
+
"description": "Attaches the native TVEventHandler listener and returns the fresh TvRemoteState: { supported, platform, captureArmed, listening, menuCaptureArmed, seq, recent[] }. CHECK `listening`, not `captureArmed`: on a non-TV build or an RN build without TVEventHandler, arm() still sets captureArmed:true while listening stays false and no event will ever arrive \u2014 reporting 'armed' there would make every replay step look swallowed. Buffer holds the last 50 events; key-DOWN (eventKeyAction 0) and continuous gestures (pan/swipeUp/swipeDown/swipeLeft/swipeRight) are dropped so one press = exactly one echoed event. Arming renders nothing on screen and costs one listener. This does NOT press any button.",
|
|
4804
5666
|
"requires": [
|
|
4805
5667
|
"@buoy-gg/tv-remote installed in the app",
|
|
4806
5668
|
"FloatingDevTools mounted (auto-discovery registers the adapter)",
|
|
@@ -4809,7 +5671,7 @@
|
|
|
4809
5671
|
},
|
|
4810
5672
|
{
|
|
4811
5673
|
"action": "getEventsSince",
|
|
4812
|
-
"summary": "Cursor-paged read of captured remote events newer than `seq`. The echo poll
|
|
5674
|
+
"summary": "Cursor-paged read of captured remote events newer than `seq`. The echo poll \u2014 use this, not the snapshot, to time whether a press landed.",
|
|
4813
5675
|
"params": {
|
|
4814
5676
|
"type": "object",
|
|
4815
5677
|
"properties": {
|
|
@@ -4823,9 +5685,9 @@
|
|
|
4823
5685
|
},
|
|
4824
5686
|
"effect": "read",
|
|
4825
5687
|
"release": "works",
|
|
4826
|
-
"description": "Returns { seq, events: [{ seq, t, eventType, eventKeyAction }] }, where `events` are strictly newer than the passed cursor and `seq` is the store's current counter. eventType is the raw RN name: up | down | left | right | select | playPause | menu | longSelect | longLeft |
|
|
5688
|
+
"description": "Returns { seq, events: [{ seq, t, eventType, eventKeyAction }] }, where `events` are strictly newer than the passed cursor and `seq` is the store's current counter. eventType is the raw RN name: up | down | left | right | select | playPause | menu | longSelect | longLeft | \u2026 . Pure read \u2014 it never consumes or advances the buffer. Pattern: call with seq: 9007199254740991 (Number.MAX_SAFE_INTEGER) to grab the current cursor with zero events, have the press injected, then poll with that cursor. Omitted seq (or 0) returns everything buffered, max 50. TRAPS: a non-number (a string \"5\") silently falls back to 0 and returns EVERYTHING; a non-finite value (Infinity/NaN) also returns everything rather than nothing. Injected TEXT and Home never echo at all (they ride the platform keyboard/button path, not the TV event pipe) \u2014 absence of an event for those is expected, not a bug.",
|
|
4827
5689
|
"requires": [
|
|
4828
|
-
"arm() must have been called first
|
|
5690
|
+
"arm() must have been called first \u2014 an unarmed store records nothing and returns an empty list"
|
|
4829
5691
|
]
|
|
4830
5692
|
},
|
|
4831
5693
|
{
|
|
@@ -4844,7 +5706,7 @@
|
|
|
4844
5706
|
},
|
|
4845
5707
|
"effect": "write",
|
|
4846
5708
|
"release": "works",
|
|
4847
|
-
"description": "Calls TVEventControl.enableTVMenuKey/disableTVMenuKey and returns the fresh TvRemoteState. Pass { on: true } to capture Menu; ANY other value (omitted, \"true\", 1, null) disables it
|
|
5709
|
+
"description": "Calls TVEventControl.enableTVMenuKey/disableTVMenuKey and returns the fresh TvRemoteState. Pass { on: true } to capture Menu; ANY other value (omitted, \"true\", 1, null) disables it \u2014 the handler is a strict `params?.on === true`. WARN THE USER BEFORE ENABLING: with capture on, Menu no longer navigates back, so at the root of the app the remote's Back/Menu button stops working the way they expect until it is turned off. Hard no-op (returns ok, changes nothing) on Android TV, on non-TV builds, and on RN builds without TVEventControl \u2014 check `menuCaptureArmed` in the returned state to see whether it actually took. Always undone automatically by disarm().",
|
|
4848
5710
|
"requires": [
|
|
4849
5711
|
"tvOS (Platform.OS === \"ios\") with Platform.isTV === true",
|
|
4850
5712
|
"react-native-tvos TVEventControl present"
|
|
@@ -4861,11 +5723,11 @@
|
|
|
4861
5723
|
},
|
|
4862
5724
|
"effect": "write",
|
|
4863
5725
|
"release": "works",
|
|
4864
|
-
"description": "Removes the native listener, turns menu capture off if it was on, and returns the fresh TvRemoteState. The 50-event buffer is NOT wiped
|
|
5726
|
+
"description": "Removes the native listener, turns menu capture off if it was on, and returns the fresh TvRemoteState. The 50-event buffer is NOT wiped \u2014 previously captured events stay readable via getEventsSince. Do not call this while someone is recording a macro or a replay is running on this device: an unarmed device echoes nothing, which reads as every press being swallowed. Idempotent and safe to call on a non-TV app."
|
|
4865
5727
|
},
|
|
4866
5728
|
{
|
|
4867
5729
|
"action": "clear",
|
|
4868
|
-
"summary": "Wipe the captured-event ring buffer. Irreversible
|
|
5730
|
+
"summary": "Wipe the captured-event ring buffer. Irreversible \u2014 the recorded presses are gone.",
|
|
4869
5731
|
"params": {
|
|
4870
5732
|
"type": "object",
|
|
4871
5733
|
"properties": {},
|
|
@@ -4874,15 +5736,15 @@
|
|
|
4874
5736
|
},
|
|
4875
5737
|
"effect": "destructive",
|
|
4876
5738
|
"release": "works",
|
|
4877
|
-
"description": "Empties the in-memory buffer and returns the fresh TvRemoteState (recent: []). The `seq` counter is NOT reset, so cursors held by an in-flight replay stay valid but their events vanish. This destroys the evidence a recording-from-the-real-remote session just collected
|
|
5739
|
+
"description": "Empties the in-memory buffer and returns the fresh TvRemoteState (recent: []). The `seq` counter is NOT reset, so cursors held by an in-flight replay stay valid but their events vanish. This destroys the evidence a recording-from-the-real-remote session just collected \u2014 never call it to 'tidy up' while a macro is being recorded or a replay is polling for echoes; ask first. Capture state is untouched: still armed after clearing. No-ops when the buffer is already empty."
|
|
4878
5740
|
}
|
|
4879
5741
|
],
|
|
4880
|
-
"unavailableWhen": "The app is not a react-native-tvos TV build (`Platform.isTV !== true`): every action still returns ok, but the snapshot says `supported: false`, `listening` stays false, and no event is ever captured. Also absent if `@buoy-gg/tv-remote` isn't installed, or if FloatingDevTools isn't mounted (auto-discovery registers the adapter). In a release build the whole Buoy sync transport only connects when the app passes `externalSync={{ enableInRelease: true }}` AND holds a real Pro license (packages/devtools-floating-menu/src/floatingMenu/externalSyncGate.ts:38)
|
|
5742
|
+
"unavailableWhen": "The app is not a react-native-tvos TV build (`Platform.isTV !== true`): every action still returns ok, but the snapshot says `supported: false`, `listening` stays false, and no event is ever captured. Also absent if `@buoy-gg/tv-remote` isn't installed, or if FloatingDevTools isn't mounted (auto-discovery registers the adapter). In a release build the whole Buoy sync transport only connects when the app passes `externalSync={{ enableInRelease: true }}` AND holds a real Pro license (packages/devtools-floating-menu/src/floatingMenu/externalSyncGate.ts:38) \u2014 but if an action is reachable at all, its code path works in release."
|
|
4881
5743
|
},
|
|
4882
5744
|
{
|
|
4883
5745
|
"toolId": "focus-inspector",
|
|
4884
5746
|
"title": "TV Focus Inspector",
|
|
4885
|
-
"summary": "Debugs D-pad/remote focus on Android TV and tvOS: what holds focus right now, the observed focus-transition history, and detected dead ends, traps, invisible stops (TVFocusGuideView) and traversal coverage. Reach for it when a user says the remote won't move, focus is stuck inside one row, focus skips a button, or focus \"disappeared\". Unlike most timeline tools it needs NO arming
|
|
5747
|
+
"summary": "Debugs D-pad/remote focus on Android TV and tvOS: what holds focus right now, the observed focus-transition history, and detected dead ends, traps, invisible stops (TVFocusGuideView) and traversal coverage. Reach for it when a user says the remote won't move, focus is stuck inside one row, focus skips a button, or focus \"disappeared\". Unlike most timeline tools it needs NO arming \u2014 the focus/key observers attach at app launch, so history is already there when you first read it; but it is TV-ONLY (on a phone/tablet/web build snapshot.supported is false and nothing is ever observed), and in a RELEASE build the fiber half dies while the observation half survives (see per-action release notes).",
|
|
4886
5748
|
"actions": [
|
|
4887
5749
|
{
|
|
4888
5750
|
"action": "rescan",
|
|
@@ -4895,11 +5757,11 @@
|
|
|
4895
5757
|
},
|
|
4896
5758
|
"effect": "read",
|
|
4897
5759
|
"release": "empty",
|
|
4898
|
-
"description": "Returns {ok:true, focusables:N}. Refreshes snapshot.focusables / screen / scannedAt and the internal nativeTag->instance table that focusElement depends on. Call it after the app navigates to a new screen, and always before a focusElement. Inventory is capped at 400 nodes, sorted in reading order, frames normalized to [0,1]; off-window nodes are KEPT (a tile below a ScrollView fold is still a real D-pad target). Two gotchas: (1) it CLEARS the inventory and instance table before rebuilding, so a rescan fired while nothing is mounted leaves focusables:0 and breaks focusElement until you rescan again; (2) detected flags are judged against this scan's timestamp, so rescanning discards trap/invisible-stop evidence collected before it. Concurrent calls are coalesced
|
|
4899
|
-
"releaseNote": "packages/focus-inspector/src/scan/focusableScanner.ts:109 reads global.__REACT_DEVTOOLS_GLOBAL_HOOK__, which React Native installs ONLY under __DEV__ (node_modules/react-native/Libraries/Core/setUpReactDevTools.js:32). In a release build getAllFiberRoots() returns [], collectCandidates() returns [], and the action still answers {ok:true, focusables:0}
|
|
5760
|
+
"description": "Returns {ok:true, focusables:N}. Refreshes snapshot.focusables / screen / scannedAt and the internal nativeTag->instance table that focusElement depends on. Call it after the app navigates to a new screen, and always before a focusElement. Inventory is capped at 400 nodes, sorted in reading order, frames normalized to [0,1]; off-window nodes are KEPT (a tile below a ScrollView fold is still a real D-pad target). Two gotchas: (1) it CLEARS the inventory and instance table before rebuilding, so a rescan fired while nothing is mounted leaves focusables:0 and breaks focusElement until you rescan again; (2) detected flags are judged against this scan's timestamp, so rescanning discards trap/invisible-stop evidence collected before it. Concurrent calls are coalesced \u2014 a rescan while one is in flight returns the previous scan.",
|
|
5761
|
+
"releaseNote": "packages/focus-inspector/src/scan/focusableScanner.ts:109 reads global.__REACT_DEVTOOLS_GLOBAL_HOOK__, which React Native installs ONLY under __DEV__ (node_modules/react-native/Libraries/Core/setUpReactDevTools.js:32). In a release build getAllFiberRoots() returns [], collectCandidates() returns [], and the action still answers {ok:true, focusables:0} \u2014 do not report that as an app with no focusable elements. Knock-on effects in release: snapshot.focusables is empty, current.node is null (only the raw tag is known), coverage.focusables is 0, and flags.traps / flags.invisibleStops are always empty because both need frames or a non-empty inventory.",
|
|
4900
5762
|
"requires": [
|
|
4901
5763
|
"Platform.isTV === true for the results to mean anything (the scan itself runs on a phone but nothing is ever observed there)",
|
|
4902
|
-
"A __DEV__ build
|
|
5764
|
+
"A __DEV__ build \u2014 a release build returns focusables:0"
|
|
4903
5765
|
]
|
|
4904
5766
|
},
|
|
4905
5767
|
{
|
|
@@ -4920,8 +5782,8 @@
|
|
|
4920
5782
|
},
|
|
4921
5783
|
"effect": "write",
|
|
4922
5784
|
"release": "empty",
|
|
4923
|
-
"description": "The tool's ONLY write into the host app
|
|
4924
|
-
"releaseNote": "packages/focus-inspector/src/scan/focusableScanner.ts:550 looks the tag up in instancesByTag, which is populated ONLY by the DEV-gated fiber scan (focusableScanner.ts:454, reached via the __REACT_DEVTOOLS_GLOBAL_HOOK__ read at :109). In a release build that map is permanently empty, so every call returns {ok:false, reason:'Unknown tag
|
|
5785
|
+
"description": "The tool's ONLY write into the host app \u2014 it calls the element's requestTVFocus(). Send {nativeTag: 166}, where the tag comes from snapshot.focusables[].nativeTag or snapshot.current.tag AND from the most recent rescan (the instance table is rebuilt on every scan, so a tag from an older scan is stale). Returns {ok:true} or {ok:false, reason} and the reason strings are exact and worth relaying verbatim: 'nativeTag is required.' (missing or non-number param), 'Not a TV build \u2014 there is no focus engine.' (Platform.isTV false), 'Unknown tag \u2014 rescan and try again.' (tag not in the current scan's instance table), or 'This element exposes no requestTVFocus().' (the host node is not a View \u2014 Text and Image never get one). Note the adapter hand-casts this as (params as {nativeTag?: number}), i.e. structurally optional on the wire, but the handler hard-rejects when it is absent.",
|
|
5786
|
+
"releaseNote": "packages/focus-inspector/src/scan/focusableScanner.ts:550 looks the tag up in instancesByTag, which is populated ONLY by the DEV-gated fiber scan (focusableScanner.ts:454, reached via the __REACT_DEVTOOLS_GLOBAL_HOOK__ read at :109). In a release build that map is permanently empty, so every call returns {ok:false, reason:'Unknown tag \u2014 rescan and try again.'} no matter how many times you rescan. Do not loop on the rescan advice in the reason string \u2014 in release it can never succeed; say so and drive the app with the actual remote instead.",
|
|
4925
5787
|
"requires": [
|
|
4926
5788
|
"Platform.isTV === true",
|
|
4927
5789
|
"A __DEV__ build",
|
|
@@ -4936,7 +5798,7 @@
|
|
|
4936
5798
|
"properties": {
|
|
4937
5799
|
"enabled": {
|
|
4938
5800
|
"type": "boolean",
|
|
4939
|
-
"description": "true resumes recording, false pauses it. OMITTING THIS MEANS TRUE (resume)
|
|
5801
|
+
"description": "true resumes recording, false pauses it. OMITTING THIS MEANS TRUE (resume) \u2014 always send it explicitly."
|
|
4940
5802
|
}
|
|
4941
5803
|
},
|
|
4942
5804
|
"required": [],
|
|
@@ -4944,11 +5806,11 @@
|
|
|
4944
5806
|
},
|
|
4945
5807
|
"effect": "write",
|
|
4946
5808
|
"release": "works",
|
|
4947
|
-
"description": "Returns {tracking:boolean}
|
|
5809
|
+
"description": "Returns {tracking:boolean} \u2014 the value actually in effect. GOTCHA THAT WILL BITE: `enabled` defaults to TRUE. The handler is `setTracking(params?.enabled !== false)`, so calling setTracking with no params, or with anything other than exactly false, RESUMES recording. To pause you must explicitly send {enabled:false}. Pausing leaves the RawEventEmitter and TVEventHandler listeners attached and keeps the currently-focused element, so nothing is forgotten; it only stops new focus/blur/key events from being appended to the history. Use it when a person is driving the app by hand and does not want that traversal scored. The current value is also visible as snapshot.tracking."
|
|
4948
5810
|
},
|
|
4949
5811
|
{
|
|
4950
5812
|
"action": "clearHistory",
|
|
4951
|
-
"summary": "Permanently wipe the recorded focus history
|
|
5813
|
+
"summary": "Permanently wipe the recorded focus history \u2014 transitions, D-pad probes, visited tags and counters.",
|
|
4952
5814
|
"params": {
|
|
4953
5815
|
"type": "object",
|
|
4954
5816
|
"properties": {},
|
|
@@ -4957,15 +5819,15 @@
|
|
|
4957
5819
|
},
|
|
4958
5820
|
"effect": "destructive",
|
|
4959
5821
|
"release": "works",
|
|
4960
|
-
"description": "Returns {ok:true}. Resets transitions, probes, visited, lostCount and the seq counters to zero, which also blanks the desktop timeline and zeroes every derived number (coverage.visited, stats.transitions, all dead-end/trap/invisible-stop flags, since they are computed from that stream). IRREVERSIBLE
|
|
5822
|
+
"description": "Returns {ok:true}. Resets transitions, probes, visited, lostCount and the seq counters to zero, which also blanks the desktop timeline and zeroes every derived number (coverage.visited, stats.transitions, all dead-end/trap/invisible-stop flags, since they are computed from that stream). IRREVERSIBLE \u2014 there is no snapshot of the old history anywhere. It deliberately KEEPS the currently focused element as the only visited tag, so the Now card does not blank out. The legitimate use is starting a clean traversal run: clear, then have the QA user walk the screen with the remote, then read the flags. Do not call it just to tidy up \u2014 you are destroying the evidence the tool exists to collect, and focus history cannot be re-derived because focus can only be watched arriving, never queried."
|
|
4961
5823
|
}
|
|
4962
5824
|
],
|
|
4963
|
-
"unavailableWhen": "The app is not a TV build
|
|
5825
|
+
"unavailableWhen": "The app is not a TV build \u2014 `Platform.isTV !== true` means the focus and D-pad observers are never attached (focusInspectorSyncAdapter.ts:117), so `snapshot.supported` is false, `presence` stays \"never-observed\", transitions/current stay empty forever, and focusElement refuses. Also absent entirely if `@buoy-gg/focus-inspector` isn't installed, since @buoy-gg/core only registers the \"focus-inspector\" capability when that optional require resolves (autoExternalSync.tsx:397). There is deliberately NO on-device UI \u2014 a focusable overlay would insert itself into the host app's focus order and corrupt the measurement \u2014 so everything is read through this adapter."
|
|
4964
5826
|
},
|
|
4965
5827
|
{
|
|
4966
5828
|
"toolId": "images",
|
|
4967
5829
|
"title": "Images",
|
|
4968
|
-
"summary": "Live registry of every image the app has loaded
|
|
5830
|
+
"summary": "Live registry of every image the app has loaded \u2014 RN core <Image> and expo-image \u2014 with cache verdict (memory/disk/network), load ms, decoded-vs-displayed pixel size plus oversize/wasted-KB math, error codes, and cross-record insights (duplicate URLs, retry storms, missing alt text, layout shifters). Also drives per-image and app-wide simulation: force error / force loading / blank / URL swap / offline / cold-start, plus locate-flash and cache clears. Reach for it whenever an image is broken, blank, blurry, slow, or suspected of memory bloat: image HTTP never passes through the JS network stack, so this is the ONLY visibility into image loading.",
|
|
4969
5831
|
"actions": [
|
|
4970
5832
|
{
|
|
4971
5833
|
"action": "list",
|
|
@@ -4993,7 +5855,7 @@
|
|
|
4993
5855
|
},
|
|
4994
5856
|
"effect": "read",
|
|
4995
5857
|
"release": "works",
|
|
4996
|
-
"description": "Returns { stats, captureStatus, globalModes, insights, total, records }. stats = {total, loading, loaded, errors, networkLoads, estDecodedBytes, estWastedBytes}. Each record carries id, lib ('rn'|'expo'), uri, kind ('network'|'asset'|'file'|'data'|'other'), status, mounted, cache verdict, ms, intrinsic px, layout dp, neededPx, oversizeFactor, decodedKB, wastedKB, error/errorCode, loadCount, overrideLabel, hasAltText, layoutShifts, ageMs. `data:` URIs and URIs over 2KB arrive truncated to a stub, never in full. insights flags duplicate URLs, retry storms (loadCount >= 5), iOS queue saturation (>4 RN images loading), missing alt text and layout shifters. The registry keeps the last 500 records and only holds images that mounted AFTER capture installed
|
|
5858
|
+
"description": "Returns { stats, captureStatus, globalModes, insights, total, records }. stats = {total, loading, loaded, errors, networkLoads, estDecodedBytes, estWastedBytes}. Each record carries id, lib ('rn'|'expo'), uri, kind ('network'|'asset'|'file'|'data'|'other'), status, mounted, cache verdict, ms, intrinsic px, layout dp, neededPx, oversizeFactor, decodedKB, wastedKB, error/errorCode, loadCount, overrideLabel, hasAltText, layoutShifts, ageMs. `data:` URIs and URIs over 2KB arrive truncated to a stub, never in full. insights flags duplicate URLs, retry storms (loadCount >= 5), iOS queue saturation (>4 RN images loading), missing alt text and layout shifters. The registry keeps the last 500 records and only holds images that mounted AFTER capture installed \u2014 an empty result means either nothing rendered yet or capture is not wired (call getCaptureStatus). Use the returned ids for every other action.",
|
|
4997
5859
|
"requires": [
|
|
4998
5860
|
"@buoy-gg/images installed in the app",
|
|
4999
5861
|
"capture installed (import \"@buoy-gg/images/register\" as the first entry import, or <ImagesRoot/> mounted)"
|
|
@@ -5017,7 +5879,7 @@
|
|
|
5017
5879
|
},
|
|
5018
5880
|
"effect": "read",
|
|
5019
5881
|
"release": "works",
|
|
5020
|
-
"description": "Same fields as a `list` record plus `errorHeaders` (iOS RN core only
|
|
5882
|
+
"description": "Same fields as a `list` record plus `errorHeaders` (iOS RN core only \u2014 the HTTP response headers captured from onError, the way to see a 403 body/auth header on a failing CDN image). Throws 'Missing numeric `id` param' if id is not a number, and throws 'Image record <id> not found (cleared or evicted)' when the id aged out of the 500-record buffer or was dropped by clearRecords \u2014 re-run `list` for current ids.",
|
|
5021
5883
|
"requires": [
|
|
5022
5884
|
"@buoy-gg/images installed in the app"
|
|
5023
5885
|
]
|
|
@@ -5039,7 +5901,7 @@
|
|
|
5039
5901
|
},
|
|
5040
5902
|
{
|
|
5041
5903
|
"action": "retry",
|
|
5042
|
-
"summary": "Plain fresh load attempt for one mounted image
|
|
5904
|
+
"summary": "Plain fresh load attempt for one mounted image \u2014 no cache bypass, nothing cleared.",
|
|
5043
5905
|
"params": {
|
|
5044
5906
|
"type": "object",
|
|
5045
5907
|
"properties": {
|
|
@@ -5055,7 +5917,7 @@
|
|
|
5055
5917
|
},
|
|
5056
5918
|
"effect": "write",
|
|
5057
5919
|
"release": "works",
|
|
5058
|
-
"description": "RN core: bumps the wrapper's key so the native view remounts and re-runs the load. expo-image: calls the live instance's reloadAsync(). The safe 'try loading it again' action
|
|
5920
|
+
"description": "RN core: bumps the wrapper's key so the native view remounts and re-runs the load. expo-image: calls the live instance's reloadAsync(). The safe 'try loading it again' action \u2014 prefer it over hardReload unless you specifically need to prove a cache is stale. Returns { ok:false, message:'Instance unmounted \u2014 cannot reload' } when the image left the screen, or 'Live instance not reachable' for an expo record whose instance was not registered. Give it ~1.5s before re-reading the record.",
|
|
5059
5921
|
"requires": [
|
|
5060
5922
|
"@buoy-gg/images installed in the app",
|
|
5061
5923
|
"the image must still be mounted on screen"
|
|
@@ -5079,7 +5941,7 @@
|
|
|
5079
5941
|
},
|
|
5080
5942
|
"effect": "write",
|
|
5081
5943
|
"release": "works",
|
|
5082
|
-
"description": "Purely visual and self-reverting (the border clears itself after ~2.5s, no undo needed). Returns { ok:true, message:'Flashing for 2.5s' } unconditionally
|
|
5944
|
+
"description": "Purely visual and self-reverting (the border clears itself after ~2.5s, no undo needed). Returns { ok:true, message:'Flashing for 2.5s' } unconditionally \u2014 including for an unmounted record, where nothing will actually be visible; check `mounted` on the record first. The right way to answer 'which image on screen is #42?'.",
|
|
5083
5945
|
"requires": [
|
|
5084
5946
|
"@buoy-gg/images installed in the app",
|
|
5085
5947
|
"the image must be mounted and on screen to be visible"
|
|
@@ -5143,7 +6005,7 @@
|
|
|
5143
6005
|
},
|
|
5144
6006
|
"effect": "write",
|
|
5145
6007
|
"release": "works",
|
|
5146
|
-
"description": "kind:'error' points the source at a nonexistent file:// so native fires onError instantly (offline-safe). kind:'hang' points at a blackhole IP so the load never settles (permanent skeleton/spinner state). kind:'blank' renders expo-image with source=null; RN core has no safe empty source, so it blanks via opacity:0 (visual only
|
|
6008
|
+
"description": "kind:'error' points the source at a nonexistent file:// so native fires onError instantly (offline-safe). kind:'hang' points at a blackhole IP so the load never settles (permanent skeleton/spinner state). kind:'blank' renders expo-image with source=null; RN core has no safe empty source, so it blanks via opacity:0 (visual only \u2014 the decoded bitmap stays resident). kind:'url' requires `uri` and swaps the source in place. IMPORTANT: for error/hang/blank the action returns { ok:false, message:'Instance unmounted \u2014 overrides need the image on screen' } and does nothing when the image is not mounted; the 'url' path does NOT perform that check and reports ok:true even for an unmounted record. Unknown kinds throw. The override sticks until clearOverride (or massAction 'restore') \u2014 always tell the user how to undo it.",
|
|
5147
6009
|
"requires": [
|
|
5148
6010
|
"@buoy-gg/images installed in the app",
|
|
5149
6011
|
"the image must be mounted on screen for kinds error/hang/blank"
|
|
@@ -5167,7 +6029,7 @@
|
|
|
5167
6029
|
},
|
|
5168
6030
|
"effect": "write",
|
|
5169
6031
|
"release": "works",
|
|
5170
|
-
"description": "Deletes the per-record override and clears the record's overrideLabel, then re-renders just that image. Always returns { ok:true, message:'Override removed
|
|
6032
|
+
"description": "Deletes the per-record override and clears the record's overrideLabel, then re-renders just that image. Always returns { ok:true, message:'Override removed \u2014 original source restored' }, even for an id that had no override. This is the undo for setOverride.",
|
|
5171
6033
|
"requires": [
|
|
5172
6034
|
"@buoy-gg/images installed in the app"
|
|
5173
6035
|
]
|
|
@@ -5195,7 +6057,7 @@
|
|
|
5195
6057
|
},
|
|
5196
6058
|
"effect": "write",
|
|
5197
6059
|
"release": "works",
|
|
5198
|
-
"description": "'offline' swaps every NETWORK-kind source for a nonexistent file:// so it fails immediately on both libs
|
|
6060
|
+
"description": "'offline' swaps every NETWORK-kind source for a nonexistent file:// so it fails immediately on both libs \u2014 bundled/local assets keep loading, exactly like a real offline device showing shipped images. 'cold' injects RN source.cache:'reload' / expo cachePolicy:'none' so every load refetches: first-launch behavior WITHOUT clearing any cache. 'normal' resets. Any other value throws. Applies to every image in the app, persists until reset, and already-displayed images need a remount/navigation (or massAction 'reload') before the effect is visible. Returns { ok:true, modes:{ network, blank } }. list/getSnapshot surface the active mode in globalModes \u2014 say so out loud, since a stuck 'offline' looks exactly like a real app bug.",
|
|
5199
6061
|
"requires": [
|
|
5200
6062
|
"@buoy-gg/images installed in the app"
|
|
5201
6063
|
]
|
|
@@ -5216,14 +6078,14 @@
|
|
|
5216
6078
|
},
|
|
5217
6079
|
"effect": "write",
|
|
5218
6080
|
"release": "works",
|
|
5219
|
-
"description": "expo-image gets source=null (its placeholder keeps showing); RN core gets opacity:0 because RN has no safe empty source (visual only
|
|
6081
|
+
"description": "expo-image gets source=null (its placeholder keeps showing); RN core gets opacity:0 because RN has no safe empty source (visual only \u2014 the bitmap is still decoded and resident, so this does NOT prove memory savings). Good for checking layout/alt-text without imagery. Returns { ok:true, modes:{ network, blank } }. NOTE the param cast is `enabled === true`: calling with no params, or with anything other than boolean true, turns the mode OFF \u2014 always pass `enabled` explicitly.",
|
|
5220
6082
|
"requires": [
|
|
5221
6083
|
"@buoy-gg/images installed in the app"
|
|
5222
6084
|
]
|
|
5223
6085
|
},
|
|
5224
6086
|
{
|
|
5225
6087
|
"action": "hardReload",
|
|
5226
|
-
"summary": "Cache-busting reload of one image
|
|
6088
|
+
"summary": "Cache-busting reload of one image \u2014 for expo records this ALSO clears the app's entire expo-image memory cache.",
|
|
5227
6089
|
"params": {
|
|
5228
6090
|
"type": "object",
|
|
5229
6091
|
"properties": {
|
|
@@ -5239,7 +6101,7 @@
|
|
|
5239
6101
|
},
|
|
5240
6102
|
"effect": "destructive",
|
|
5241
6103
|
"release": "works",
|
|
5242
|
-
"description": "RN core: remounts with source.cache:'reload' injected (honored on both platforms) so the HTTP cache is bypassed. expo-image: deletes that URI's disk-cache file, then calls Image.clearMemoryCache() which wipes the WHOLE app's expo-image memory cache (no per-entry memory eviction exists), then reloads. Non-network sources (asset/file/data) have no HTTP cache and silently fall back to a plain `retry`. Returns { ok:false, message:'Instance unmounted
|
|
6104
|
+
"description": "RN core: remounts with source.cache:'reload' injected (honored on both platforms) so the HTTP cache is bypassed. expo-image: deletes that URI's disk-cache file, then calls Image.clearMemoryCache() which wipes the WHOLE app's expo-image memory cache (no per-entry memory eviction exists), then reloads. Non-network sources (asset/file/data) have no HTTP cache and silently fall back to a plain `retry`. Returns { ok:false, message:'Instance unmounted \u2014 cannot reload' } when the image is off screen. Prefer `retry` unless the point is to prove a stale cache; re-read the record after ~1.5s to see the new cache verdict.",
|
|
5243
6105
|
"requires": [
|
|
5244
6106
|
"@buoy-gg/images installed in the app",
|
|
5245
6107
|
"the image must be mounted on screen",
|
|
@@ -5248,7 +6110,7 @@
|
|
|
5248
6110
|
},
|
|
5249
6111
|
{
|
|
5250
6112
|
"action": "evictDisk",
|
|
5251
|
-
"summary": "Delete this image's expo-image disk-cache file. Cache data only
|
|
6113
|
+
"summary": "Delete this image's expo-image disk-cache file. Cache data only \u2014 irreversible, the image refetches next load.",
|
|
5252
6114
|
"params": {
|
|
5253
6115
|
"type": "object",
|
|
5254
6116
|
"properties": {
|
|
@@ -5264,7 +6126,7 @@
|
|
|
5264
6126
|
},
|
|
5265
6127
|
"effect": "destructive",
|
|
5266
6128
|
"release": "works",
|
|
5267
|
-
"description": "Resolves the entry via expo-image's getCachePathAsync and deletes the file with expo-file-system. Returns { ok:true, message:'Disk cache entry deleted' } or { ok:false, message:'No disk entry found (expo-image + expo-file-system required)' }
|
|
6129
|
+
"description": "Resolves the entry via expo-image's getCachePathAsync and deletes the file with expo-file-system. Returns { ok:true, message:'Disk cache entry deleted' } or { ok:false, message:'No disk entry found (expo-image + expo-file-system required)' } \u2014 that same ok:false covers 'neither package installed', 'not an expo-image record', and 'nothing was cached', so do not read it as a hard error. No effect at all on RN core <Image> records or on the memory cache.",
|
|
5268
6130
|
"requires": [
|
|
5269
6131
|
"expo-image installed",
|
|
5270
6132
|
"expo-file-system installed (legacy or main entry)",
|
|
@@ -5297,10 +6159,10 @@
|
|
|
5297
6159
|
},
|
|
5298
6160
|
"effect": "destructive",
|
|
5299
6161
|
"release": "works",
|
|
5300
|
-
"description": "kind 'error'|'loading'|'blank' set that override on every mounted record (note: per-record 'hang' is spelled 'loading' here) and return { ok: n>0, message:'... on N images' }. 'flash' red-borders everything the tool tracks for 2.5s. 'restore' clears every active override and is the undo for the three simulation kinds. 'reload' fans `hardReload` out across every mounted image
|
|
6162
|
+
"description": "kind 'error'|'loading'|'blank' set that override on every mounted record (note: per-record 'hang' is spelled 'loading' here) and return { ok: n>0, message:'... on N images' }. 'flash' red-borders everything the tool tracks for 2.5s. 'restore' clears every active override and is the undo for the three simulation kinds. 'reload' fans `hardReload` out across every mounted image \u2014 which for expo records means disk-entry evictions plus a full expo-image memory-cache clear, hence the destructive rating on this whole action. Unknown kinds throw. ok:false just means zero images were mounted. Whole-screen effect: confirm with the user before firing, and always report how to restore.",
|
|
5301
6163
|
"requires": [
|
|
5302
6164
|
"@buoy-gg/images installed in the app",
|
|
5303
|
-
"images must be mounted on screen
|
|
6165
|
+
"images must be mounted on screen \u2014 a background screen yields ok:false with 0 affected"
|
|
5304
6166
|
]
|
|
5305
6167
|
},
|
|
5306
6168
|
{
|
|
@@ -5313,7 +6175,7 @@
|
|
|
5313
6175
|
},
|
|
5314
6176
|
"effect": "destructive",
|
|
5315
6177
|
"release": "works",
|
|
5316
|
-
"description": "Drops every record that is unmounted or already settled (loaded/error); records that are still mounted and still loading survive so in-flight events do not orphan. Returns { ok:true, message:'Registry cleared' }. The captured history cannot be recovered
|
|
6178
|
+
"description": "Drops every record that is unmounted or already settled (loaded/error); records that are still mounted and still loading survive so in-flight events do not orphan. Returns { ok:true, message:'Registry cleared' }. The captured history cannot be recovered \u2014 take a `list` first if the user might need it. Legitimate use: clear, then have the user re-do the broken step, so the registry contains only the repro.",
|
|
5317
6179
|
"requires": [
|
|
5318
6180
|
"@buoy-gg/images installed in the app"
|
|
5319
6181
|
]
|
|
@@ -5338,18 +6200,18 @@
|
|
|
5338
6200
|
},
|
|
5339
6201
|
"effect": "destructive",
|
|
5340
6202
|
"release": "works",
|
|
5341
|
-
"description": "Calls expo-image's Image.clearMemoryCache() and Image.clearDiskCache(). Both default to true
|
|
6203
|
+
"description": "Calls expo-image's Image.clearMemoryCache() and Image.clearDiskCache(). Both default to true \u2014 the cast is `p.memory !== false`, so omitting params clears BOTH; pass false explicitly to skip one. Returns { ok:true, message:'Cleared expo-image memory + disk cache' } or { ok:false, message:'expo-image not installed' }. Affects the whole app, not one image, and does nothing for RN core <Image> (whose caches are native/Fresco/NSURLCache and unreachable from JS). Use it to make the next loads genuinely cold; `setNetworkMode {mode:'cold'}` simulates the same thing without deleting anything.",
|
|
5342
6204
|
"requires": [
|
|
5343
6205
|
"expo-image installed (otherwise returns ok:false and does nothing)"
|
|
5344
6206
|
]
|
|
5345
6207
|
}
|
|
5346
6208
|
],
|
|
5347
|
-
"unavailableWhen": "@buoy-gg/images is not installed in the app, or capture never installed (no `import \"@buoy-gg/images/register\"`, no <ImagesRoot/>, and the Images tool UI never opened). RN core <Image> specifically goes uncaptured when that register import is not the FIRST import of the entry file
|
|
6209
|
+
"unavailableWhen": "@buoy-gg/images is not installed in the app, or capture never installed (no `import \"@buoy-gg/images/register\"`, no <ImagesRoot/>, and the Images tool UI never opened). RN core <Image> specifically goes uncaptured when that register import is not the FIRST import of the entry file \u2014 expo-image is captured regardless; check getCaptureStatus.rnDecoratorTooLate before concluding \"no images\". Separately, in a release build the sync transport itself is off unless the app passes externalSync={{enableInRelease:true}} with a real Pro license (packages/devtools-floating-menu/src/floatingMenu/externalSyncGate.ts), in which case no action reaches the device at all."
|
|
5348
6210
|
},
|
|
5349
6211
|
{
|
|
5350
6212
|
"toolId": "ask-buoy",
|
|
5351
6213
|
"title": "Ask Buoy",
|
|
5352
|
-
"summary": "Ask Buoy's OWN session
|
|
6214
|
+
"summary": "Ask Buoy's OWN session \u2014 the changes it has made in this conversation, and the undo for them. Call listChanges when the user asks \"what did you change?\"; call undoChange with the id of the change they mean when they say \"undo that\" about one change, and undoAll when they say \"undo everything\" / \"put it all back\" / \"clean up\". Undo restores exactly what the ledger captured at write time: override rules Ask Buoy created are deleted, impersonation is stopped, storage keys are restored to their pre-write values, a query cache edit (setQueryData) is put back to the data read just before the write \u2014 unless the app has refetched it since, in which case the server's data is already back and undo says so \u2014 and a store edit (zustand.setState) has exactly the values it changed put back, unless something changed those same values again since. To undo ONE change and keep the rest, call listChanges and pass that change's id to undoChange. Changes listed with reversible:false (state writes, wipes, one-shot actions) cannot be automatically reversed \u2014 say so honestly. NEVER improvise a reverse-write with a guessed shape instead of calling undoAll; a hand-rolled \"undo\" that writes invented data is worse than telling the user a change is permanent. Its retrieve action re-reads any earlier tool result in FULL: every result over 24,000 characters is cut with a `[truncated \u2014 \u2026; ref ev_N]` marker and every result compressed out of memory leaves an `[earlier result \u2014 \u2026; ref ev_N]` marker \u2014 call retrieve with that ref (and a path or pattern) instead of asking the user for a narrower slice or calling the tool again for a value you already had.",
|
|
5353
6215
|
"actions": [
|
|
5354
6216
|
{
|
|
5355
6217
|
"action": "listChanges",
|
|
@@ -5362,11 +6224,33 @@
|
|
|
5362
6224
|
},
|
|
5363
6225
|
"effect": "read",
|
|
5364
6226
|
"release": "works",
|
|
5365
|
-
"description": "Returns `{changes:[{toolId, action, kind, label, reversible}], returned}`
|
|
6227
|
+
"description": "Returns `{changes:[{id, toolId, action, kind, label, reversible}], returned}` \u2014 outstanding (not yet undone) changes only, oldest first. `reversible:true` means undoChange (with that `id`) or undoAll can restore that change exactly. `kind` \"transient\" is a one-shot action (a navigation, a tap) that changed no persistent state."
|
|
6228
|
+
},
|
|
6229
|
+
{
|
|
6230
|
+
"action": "undoChange",
|
|
6231
|
+
"summary": "Undo ONE change Ask Buoy made this conversation, by the id listChanges gave it. Use it for \"undo that\" when the user means a single change (usually the latest one) and other changes should stay.",
|
|
6232
|
+
"params": {
|
|
6233
|
+
"type": "object",
|
|
6234
|
+
"properties": {
|
|
6235
|
+
"id": {
|
|
6236
|
+
"type": "string",
|
|
6237
|
+
"description": "The change's id from listChanges, exactly as listChanges returned it, e.g. \"fx-3-1759330000000\"."
|
|
6238
|
+
}
|
|
6239
|
+
},
|
|
6240
|
+
"required": [
|
|
6241
|
+
"id"
|
|
6242
|
+
],
|
|
6243
|
+
"additionalProperties": false,
|
|
6244
|
+
"description": "`{id}` from listChanges."
|
|
6245
|
+
},
|
|
6246
|
+
"effect": "write",
|
|
6247
|
+
"release": "works",
|
|
6248
|
+
"description": "Returns `{ok:true, undone:{id, label}}`, or `{ok:false, error}` when the id is unknown, already undone, or the change can't be put back (it is permanent, or the same value was changed again since \u2014 the error says which). Report a refusal honestly; do not write the old value back by hand over a newer one.",
|
|
6249
|
+
"servedBy": "engine"
|
|
5366
6250
|
},
|
|
5367
6251
|
{
|
|
5368
6252
|
"action": "undoAll",
|
|
5369
|
-
"summary": "Undo every reversible change Ask Buoy made this conversation
|
|
6253
|
+
"summary": "Undo every reversible change Ask Buoy made this conversation \u2014 deletes override rules it created, stops impersonation, restores storage keys, cache edits and store edits to their prior values. Use it when the user asks to undo everything or clean up; for one change use undoChange.",
|
|
5370
6254
|
"params": {
|
|
5371
6255
|
"type": "object",
|
|
5372
6256
|
"properties": {},
|
|
@@ -5375,11 +6259,11 @@
|
|
|
5375
6259
|
},
|
|
5376
6260
|
"effect": "write",
|
|
5377
6261
|
"release": "works",
|
|
5378
|
-
"description": "Returns `{ok, reverted, failed:[{label, error}], skippedTransients, permanent}`. Report the numbers honestly: `failed` entries were attempted and could not be restored (tell the user which, using the labels); `permanent` is how many changes were never undoable (state writes, refetches) and are still applied
|
|
6262
|
+
"description": "Returns `{ok, reverted, failed:[{label, error}], skippedTransients, permanent}`. Report the numbers honestly: `failed` entries were attempted and could not be restored (tell the user which, using the labels); `permanent` is how many changes were never undoable (state writes, refetches) and are still applied \u2014 say so, they are not failures; `skippedTransients` are one-shot actions that never needed undoing. This undoes ALL reversible changes from this conversation, newest first. To undo one change and keep the others, use undoChange instead."
|
|
5379
6263
|
},
|
|
5380
6264
|
{
|
|
5381
6265
|
"action": "retrieve",
|
|
5382
|
-
"summary": "Re-read part of an earlier tool result by its ref (ev_N from a [truncated
|
|
6266
|
+
"summary": "Re-read part of an earlier tool result by its ref (ev_N from a [truncated \u2026] or [earlier result \u2026] marker). With only `ref` it returns the result's SHAPE (top-level keys with types and sizes); add `path` to get one value (\"stats.0.base_stat\", \"moves.3.move.name\"), `slice` for a window of an array or string, or `pattern` for a literal case-insensitive substring search with `contextLines` of surrounding text around each match. The kept copy is exactly what you were shown when it arrived \u2014 for the app's CURRENT value call the original tool again.",
|
|
5383
6267
|
"params": {
|
|
5384
6268
|
"type": "object",
|
|
5385
6269
|
"properties": {
|
|
@@ -5413,12 +6297,12 @@
|
|
|
5413
6297
|
"ref"
|
|
5414
6298
|
],
|
|
5415
6299
|
"additionalProperties": false,
|
|
5416
|
-
"description": "ref is required; add exactly what you need
|
|
6300
|
+
"description": "ref is required; add exactly what you need \u2014 a bare ref shows the shape, then path/slice/pattern read a part."
|
|
5417
6301
|
},
|
|
5418
6302
|
"effect": "read",
|
|
5419
6303
|
"release": "works",
|
|
5420
6304
|
"servedBy": "engine",
|
|
5421
|
-
"description": "Returns `{ref, from, capturedAt, totalChars,
|
|
6305
|
+
"description": "Returns `{ref, from, capturedAt, totalChars, \u2026}` plus `shape` (no selector), `value` (path), `value`+`sliced`+`of` (slice), or `matches:[{line,text}]`+`totalMatches` (pattern). `{ok:false, error}` when the ref is unknown or was evicted to make room (the store keeps the most recent ~4 MB) \u2014 then call the original tool again. Results are capped at 24,000 characters like any other; narrow with path or pattern rather than asking for the whole thing."
|
|
5422
6306
|
},
|
|
5423
6307
|
{
|
|
5424
6308
|
"action": "openProcedure",
|
|
@@ -5443,7 +6327,7 @@
|
|
|
5443
6327
|
"description": "Returns `{id, title, version?, requires?, body, note}`; `{ok:false, error}` naming the available ids when the id is unknown or the app has none. A procedure grants nothing: each step still runs through the same catalog, policy, approval card and undo as any other call."
|
|
5444
6328
|
}
|
|
5445
6329
|
],
|
|
5446
|
-
"unavailableWhen": "Only present while an Ask Buoy session is running
|
|
6330
|
+
"unavailableWhen": "Only present while an Ask Buoy session is running \u2014 which is exactly when this catalog is in use, so in practice always available to you."
|
|
5447
6331
|
},
|
|
5448
6332
|
{
|
|
5449
6333
|
"toolId": "push-notifications",
|
|
@@ -5651,7 +6535,7 @@
|
|
|
5651
6535
|
},
|
|
5652
6536
|
{
|
|
5653
6537
|
"action": "selectTarget",
|
|
5654
|
-
"summary": "Attach the overlay to
|
|
6538
|
+
"summary": "Attach the overlay to the visible target with this `id`.",
|
|
5655
6539
|
"params": {
|
|
5656
6540
|
"type": "object",
|
|
5657
6541
|
"properties": {
|
|
@@ -5670,7 +6554,7 @@
|
|
|
5670
6554
|
},
|
|
5671
6555
|
{
|
|
5672
6556
|
"action": "loadImage",
|
|
5673
|
-
"summary": "Start loading a design image
|
|
6557
|
+
"summary": "Start loading a design image from `url`; returns {scheduled:true}. Check getSnapshot for completion or error.",
|
|
5674
6558
|
"params": {
|
|
5675
6559
|
"type": "object",
|
|
5676
6560
|
"properties": {
|
|
@@ -5689,7 +6573,7 @@
|
|
|
5689
6573
|
},
|
|
5690
6574
|
{
|
|
5691
6575
|
"action": "setSettings",
|
|
5692
|
-
"summary": "Update overlay settings
|
|
6576
|
+
"summary": "Update overlay settings: `visible`, `locked`, `flipped`, `flippedY`, `showOutline`, `autoTrack`, `opacity`, `scale`, `offsetX`, `offsetY`. Invalid batches fail before any settings change.",
|
|
5693
6577
|
"params": {
|
|
5694
6578
|
"type": "object",
|
|
5695
6579
|
"properties": {
|