@avasapp/agent-bridge 0.4.2 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -14
- package/dist/adapters/ably.cjs +79 -3
- package/dist/adapters/ably.d.cts +2 -1
- package/dist/adapters/async-storage.cjs +158 -0
- package/dist/adapters/async-storage.d.cts +14 -0
- package/dist/adapters/device.cjs +1110 -0
- package/dist/adapters/device.d.cts +49 -0
- package/dist/adapters/expo-router.cjs +112 -6
- package/dist/adapters/expo-router.d.cts +1 -1
- package/dist/adapters/jotai.cjs +266 -0
- package/dist/adapters/jotai.d.cts +19 -0
- package/dist/adapters/react-native-mmkv.cjs +80 -0
- package/dist/adapters/react-native-mmkv.d.cts +3 -1
- package/dist/adapters/react-native-webview.d.cts +1 -1
- package/dist/adapters/react-navigation.cjs +293 -0
- package/dist/adapters/react-navigation.d.cts +55 -0
- package/dist/adapters/redux.cjs +322 -0
- package/dist/adapters/redux.d.cts +28 -0
- package/dist/adapters/secure-store.cjs +169 -0
- package/dist/adapters/secure-store.d.cts +28 -0
- package/dist/adapters/socket-io.cjs +77 -3
- package/dist/adapters/socket-io.d.cts +2 -1
- package/dist/adapters/tanstack-query.cjs +3 -0
- package/dist/adapters/tanstack-query.d.cts +13 -4
- package/dist/adapters/zustand.cjs +193 -22
- package/dist/adapters/zustand.d.cts +11 -15
- package/dist/{chunk-TA2OLPPT.js → chunk-Y6QFU5ON.js} +477 -165
- package/dist/cli.js +2370 -103
- package/dist/client/index.cjs +441 -136
- package/dist/client/index.d.ts +67 -4
- package/dist/client/index.js +5 -3
- package/dist/expo/index.d.cts +1 -1
- package/dist/network/index.cjs +320 -73
- package/dist/network/index.d.cts +18 -3
- package/dist/noop/async-storage.cjs +26 -0
- package/dist/noop/device.cjs +26 -0
- package/dist/noop/jotai.cjs +26 -0
- package/dist/noop/react-navigation.cjs +26 -0
- package/dist/noop/realtime.cjs +6 -1
- package/dist/noop/redux.cjs +28 -0
- package/dist/noop/secure-store.cjs +26 -0
- package/dist/realtime/index.cjs +71 -4
- package/dist/realtime/index.d.cts +21 -2
- package/dist/runtime/index.cjs +838 -128
- package/dist/runtime/index.d.cts +4 -2
- package/dist/store-common-j0cHHvBr.d.cts +23 -0
- package/dist/{types-BmpykP1P.d.cts → types-qlwdcGjt.d.cts} +22 -0
- package/entries/async-storage.cjs +9 -0
- package/entries/device.cjs +9 -0
- package/entries/jotai.cjs +9 -0
- package/entries/react-navigation.cjs +9 -0
- package/entries/redux.cjs +9 -0
- package/entries/secure-store.cjs +9 -0
- package/package.json +34 -2
- package/skills/agent-bridge/SKILL.md +70 -22
- package/skills/agent-bridge/references/device.md +21 -0
- package/skills/agent-bridge/references/flows.md +19 -8
- package/skills/agent-bridge/references/large-data.md +6 -6
- package/skills/agent-bridge/references/network.md +25 -7
- package/skills/agent-bridge/references/realtime.md +5 -5
- package/skills/agent-bridge/references/restore.md +25 -7
- package/skills/agent-bridge/references/screen.md +25 -11
- package/skills/agent-bridge/references/state.md +31 -11
- package/skills/agent-bridge/references/webview.md +9 -9
- package/skills/agent-bridge/references/writing-scenarios.md +3 -3
package/dist/runtime/index.d.cts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { a as Transport, A as AgentBridgeOptions } from '../types-
|
|
2
|
-
export { C as CallMessage, D as DeviceInfo, L as LogEntry, O as OptionsSchema, R as RUNTIME_MARKER, b as ResultMessage, S as Scenario, c as ScenarioContext, d as Scenarios, e as ToolDefinition, f as ToolFn, g as ToolInfo, T as Tools } from '../types-
|
|
1
|
+
import { a as Transport, A as AgentBridgeOptions } from '../types-qlwdcGjt.cjs';
|
|
2
|
+
export { C as CallMessage, D as DeviceInfo, L as LogEntry, O as OptionsSchema, R as RUNTIME_MARKER, b as ResultMessage, S as Scenario, c as ScenarioContext, d as Scenarios, e as ToolDefinition, f as ToolFn, g as ToolInfo, T as Tools } from '../types-qlwdcGjt.cjs';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
5
|
* Works in any React Native app served by Metro. The client evaluates one fixed
|
|
@@ -82,6 +82,8 @@ type ScreenElement = {
|
|
|
82
82
|
expanded?: boolean;
|
|
83
83
|
/** A WebView registered with the react-native-webview adapter: drive it with `webview.*`. */
|
|
84
84
|
webview?: string;
|
|
85
|
+
/** `accessibilityActions` it handles with `onAccessibilityAction`, e.g. `activate` on a swipe button. */
|
|
86
|
+
actions?: string[];
|
|
85
87
|
rect: Rect | null;
|
|
86
88
|
/** Only in `screen.snapshot({ all: true })`. */
|
|
87
89
|
onScreen?: boolean;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Keeps secrets out of what store.* prints. Either dotted paths per store
|
|
3
|
+
* (`{ auth: ['accessToken', 'user.phone'] }`), or a hook called for the store
|
|
4
|
+
* value and every nested value with its dotted path; what it returns is
|
|
5
|
+
* printed instead.
|
|
6
|
+
*
|
|
7
|
+
* Paths are store paths. A `store.call` result that is a piece of the store's
|
|
8
|
+
* state (same object, e.g. `() => get().user`) is matched by where it lives; a
|
|
9
|
+
* copy or derived value (`() => ({ ...get().user })`) is not, so its paths are
|
|
10
|
+
* matched relative to the result and store paths like `user.phone` won't apply.
|
|
11
|
+
* Use the hook, or don't return secrets from actions.
|
|
12
|
+
*/
|
|
13
|
+
type Redact = Record<string, string[]> | ((store: string, path: string, value: unknown) => unknown);
|
|
14
|
+
/**
|
|
15
|
+
* Keeps secrets out of what an adapter prints, by the name of the slice or
|
|
16
|
+
* atom: dotted paths inside it (`{ auth: ['token', 'user.phone'] }`), or `true`
|
|
17
|
+
* for all of it (`{ authToken: true }`). Or a hook called for the whole value
|
|
18
|
+
* (path `''`) and every nested value with its dotted path; what it returns is
|
|
19
|
+
* printed instead.
|
|
20
|
+
*/
|
|
21
|
+
type NamedRedact = Record<string, string[] | true> | ((name: string, path: string, value: unknown) => unknown);
|
|
22
|
+
|
|
23
|
+
export type { NamedRedact as N, Redact as R };
|
|
@@ -6,6 +6,10 @@ type CallMessage = {
|
|
|
6
6
|
args?: unknown[];
|
|
7
7
|
/** Device to run on. Expo's socket broadcasts, so every app sees every call. */
|
|
8
8
|
to?: string;
|
|
9
|
+
/** A session's own: the journal tag it holds, so the reply carries the journal only when it is stale. */
|
|
10
|
+
journalAck?: string;
|
|
11
|
+
/** A session's own secret; the runtime keeps the first one it gets and ignores the journal calls of anyone else. */
|
|
12
|
+
journalKey?: string;
|
|
9
13
|
};
|
|
10
14
|
/** An error or warning the app logged, threw or left unhandled. */
|
|
11
15
|
type LogEntry = {
|
|
@@ -39,6 +43,10 @@ type ResultMessage = ({
|
|
|
39
43
|
loadId?: string;
|
|
40
44
|
/** Areas (`store`, `query`, ...) whose `*.restore` has something to undo. */
|
|
41
45
|
pending?: string[];
|
|
46
|
+
/** Goes up when something persistent changed that a restore could undo. */
|
|
47
|
+
journal?: number;
|
|
48
|
+
/** What each area could undo from storage; sent only to a session that proved it holds the key and a stale tag. */
|
|
49
|
+
journalData?: Record<string, unknown>;
|
|
42
50
|
};
|
|
43
51
|
type ToolInfo = {
|
|
44
52
|
name: string;
|
|
@@ -82,6 +90,20 @@ type ToolDefinition = ToolFn | {
|
|
|
82
90
|
pending?: (options?: {
|
|
83
91
|
detail?: boolean;
|
|
84
92
|
}) => unknown;
|
|
93
|
+
/**
|
|
94
|
+
* On a `*.restore` tool: what it could undo that lives in persistent
|
|
95
|
+
* storage (so it outlives a reload), as JSON, or undefined. A session
|
|
96
|
+
* keeps it, and hands it to `adopt` after the app reloads.
|
|
97
|
+
*/
|
|
98
|
+
journal?: () => unknown;
|
|
99
|
+
/**
|
|
100
|
+
* Takes what `journal` returned before a reload and re-arms the undo for
|
|
101
|
+
* it in the new load, without changing the app. Returns an
|
|
102
|
+
* `AdoptResult`.
|
|
103
|
+
*/
|
|
104
|
+
adopt?: (data: unknown) => unknown;
|
|
105
|
+
/** Goes up when `journal` may have changed. Not shared state: keep it in the adapter's own module. */
|
|
106
|
+
journalRev?: () => number;
|
|
85
107
|
};
|
|
86
108
|
/** Tools by name. Namespace them with a dot, e.g. `query.pin`. */
|
|
87
109
|
type Tools = Record<string, ToolDefinition>;
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Metro inlines NODE_ENV and drops the dead branch, so a release bundle
|
|
4
|
+
// never includes the bridge, the same way react/index.js works.
|
|
5
|
+
if (process.env.NODE_ENV === 'production') {
|
|
6
|
+
module.exports = require('../dist/noop/async-storage.cjs')
|
|
7
|
+
} else {
|
|
8
|
+
module.exports = require('../dist/adapters/async-storage.cjs')
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Metro inlines NODE_ENV and drops the dead branch, so a release bundle
|
|
4
|
+
// never includes the bridge, the same way react/index.js works.
|
|
5
|
+
if (process.env.NODE_ENV === 'production') {
|
|
6
|
+
module.exports = require('../dist/noop/device.cjs')
|
|
7
|
+
} else {
|
|
8
|
+
module.exports = require('../dist/adapters/device.cjs')
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Metro inlines NODE_ENV and drops the dead branch, so a release bundle
|
|
4
|
+
// never includes the bridge, the same way react/index.js works.
|
|
5
|
+
if (process.env.NODE_ENV === 'production') {
|
|
6
|
+
module.exports = require('../dist/noop/jotai.cjs')
|
|
7
|
+
} else {
|
|
8
|
+
module.exports = require('../dist/adapters/jotai.cjs')
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Metro inlines NODE_ENV and drops the dead branch, so a release bundle
|
|
4
|
+
// never includes the bridge, the same way react/index.js works.
|
|
5
|
+
if (process.env.NODE_ENV === 'production') {
|
|
6
|
+
module.exports = require('../dist/noop/react-navigation.cjs')
|
|
7
|
+
} else {
|
|
8
|
+
module.exports = require('../dist/adapters/react-navigation.cjs')
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Metro inlines NODE_ENV and drops the dead branch, so a release bundle
|
|
4
|
+
// never includes the bridge, the same way react/index.js works.
|
|
5
|
+
if (process.env.NODE_ENV === 'production') {
|
|
6
|
+
module.exports = require('../dist/noop/redux.cjs')
|
|
7
|
+
} else {
|
|
8
|
+
module.exports = require('../dist/adapters/redux.cjs')
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
// Metro inlines NODE_ENV and drops the dead branch, so a release bundle
|
|
4
|
+
// never includes the bridge, the same way react/index.js works.
|
|
5
|
+
if (process.env.NODE_ENV === 'production') {
|
|
6
|
+
module.exports = require('../dist/noop/secure-store.cjs')
|
|
7
|
+
} else {
|
|
8
|
+
module.exports = require('../dist/adapters/secure-store.cjs')
|
|
9
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@avasapp/agent-bridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Let coding agents drive a running React Native app directly: seed state, flip flags, navigate and check the screen in about a millisecond per call, with no taps.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -38,6 +38,14 @@
|
|
|
38
38
|
"types": "./dist/adapters/zustand.d.cts",
|
|
39
39
|
"default": "./entries/zustand.cjs"
|
|
40
40
|
},
|
|
41
|
+
"./redux": {
|
|
42
|
+
"types": "./dist/adapters/redux.d.cts",
|
|
43
|
+
"default": "./entries/redux.cjs"
|
|
44
|
+
},
|
|
45
|
+
"./jotai": {
|
|
46
|
+
"types": "./dist/adapters/jotai.d.cts",
|
|
47
|
+
"default": "./entries/jotai.cjs"
|
|
48
|
+
},
|
|
41
49
|
"./react-native-mmkv": {
|
|
42
50
|
"types": "./dist/adapters/react-native-mmkv.d.cts",
|
|
43
51
|
"default": "./entries/react-native-mmkv.cjs"
|
|
@@ -46,6 +54,10 @@
|
|
|
46
54
|
"types": "./dist/adapters/expo-router.d.cts",
|
|
47
55
|
"default": "./entries/expo-router.cjs"
|
|
48
56
|
},
|
|
57
|
+
"./react-navigation": {
|
|
58
|
+
"types": "./dist/adapters/react-navigation.d.cts",
|
|
59
|
+
"default": "./entries/react-navigation.cjs"
|
|
60
|
+
},
|
|
49
61
|
"./react-native-webview": {
|
|
50
62
|
"types": "./dist/adapters/react-native-webview.d.cts",
|
|
51
63
|
"default": "./entries/react-native-webview.cjs"
|
|
@@ -66,6 +78,18 @@
|
|
|
66
78
|
"types": "./dist/adapters/socket-io.d.cts",
|
|
67
79
|
"default": "./entries/socket.io.cjs"
|
|
68
80
|
},
|
|
81
|
+
"./async-storage": {
|
|
82
|
+
"types": "./dist/adapters/async-storage.d.cts",
|
|
83
|
+
"default": "./entries/async-storage.cjs"
|
|
84
|
+
},
|
|
85
|
+
"./secure-store": {
|
|
86
|
+
"types": "./dist/adapters/secure-store.d.cts",
|
|
87
|
+
"default": "./entries/secure-store.cjs"
|
|
88
|
+
},
|
|
89
|
+
"./device": {
|
|
90
|
+
"types": "./dist/adapters/device.d.cts",
|
|
91
|
+
"default": "./entries/device.cjs"
|
|
92
|
+
},
|
|
69
93
|
"./client": {
|
|
70
94
|
"types": "./dist/client/index.d.ts",
|
|
71
95
|
"import": "./dist/client/index.js",
|
|
@@ -92,7 +116,11 @@
|
|
|
92
116
|
"cdp",
|
|
93
117
|
"tanstack-query",
|
|
94
118
|
"zustand",
|
|
119
|
+
"redux",
|
|
120
|
+
"jotai",
|
|
95
121
|
"mmkv",
|
|
122
|
+
"async-storage",
|
|
123
|
+
"secure-store",
|
|
96
124
|
"realtime",
|
|
97
125
|
"ably",
|
|
98
126
|
"socket.io",
|
|
@@ -103,8 +131,9 @@
|
|
|
103
131
|
"build": "bun run generate && rm -rf dist && tsup",
|
|
104
132
|
"test": "bun run generate && bun test src/",
|
|
105
133
|
"typecheck": "bun run generate && tsc -p tsconfig.json --noEmit",
|
|
106
|
-
"check": "bun run typecheck && bun run test && bun run build && bun run check:gates",
|
|
134
|
+
"check": "bun run typecheck && bun run test && bun run build && bun run check:gates && bun run check:consumer-types",
|
|
107
135
|
"check:gates": "node scripts/check-gates.mjs",
|
|
136
|
+
"check:consumer-types": "node scripts/check-consumer-types.mjs",
|
|
108
137
|
"changeset": "changeset",
|
|
109
138
|
"release": "bun run check && changeset publish",
|
|
110
139
|
"prepublishOnly": "bun run check"
|
|
@@ -134,12 +163,15 @@
|
|
|
134
163
|
"devDependencies": {
|
|
135
164
|
"@changesets/cli": "^3.0.3",
|
|
136
165
|
"@expo/devtools": "^57.0.1",
|
|
166
|
+
"@reduxjs/toolkit": "^2.14.0",
|
|
137
167
|
"@tanstack/query-core": "^5.90.0",
|
|
138
168
|
"@types/bun": "^1.4.2",
|
|
139
169
|
"@types/react": "^19.3.0",
|
|
140
170
|
"@types/ws": "^8.18.1",
|
|
141
171
|
"ably": "^2.29.0",
|
|
142
172
|
"happy-dom": "^20.14.5",
|
|
173
|
+
"jotai": "^3.0.2",
|
|
174
|
+
"redux-persist": "^6.0.0",
|
|
143
175
|
"socket.io-client": "^4.8.4",
|
|
144
176
|
"tsup": "^8.5.1",
|
|
145
177
|
"typescript": "~6.0.3",
|
|
@@ -26,28 +26,49 @@ bridge call can set.
|
|
|
26
26
|
|
|
27
27
|
## Run the CLI directly
|
|
28
28
|
|
|
29
|
-
The examples say `npx agent-bridge
|
|
30
|
-
|
|
29
|
+
The examples say `npx @avasapp/agent-bridge`; never drop the scope, because
|
|
30
|
+
`agent-bridge` on npm is an unrelated package. Prefer the installed bin,
|
|
31
|
+
`./node_modules/.bin/agent-bridge` (with bun too): `npx` loads npm's
|
|
31
32
|
config on every call, so a project `.npmrc` with keys npm doesn't know prints
|
|
32
33
|
`npm warn Unknown project config` into your output each time, and it adds
|
|
33
34
|
roughly 200 ms more startup per call (machine-dependent).
|
|
34
35
|
|
|
36
|
+
If the app's tools are already in your tool list as an MCP server
|
|
37
|
+
(`agent-bridge mcp`; names like `screen_snapshot` for `screen.snapshot`), call
|
|
38
|
+
those: each takes `args`, an array of the tool's arguments, and `full: true` for
|
|
39
|
+
a result over 32 KB. If the list is empty or a call fails, call `agent_bridge_status`. A failed call keeps the session's own error (`may or may not have run`): check state before repeating a call that changes it. The rest of this skill applies the same way.
|
|
40
|
+
|
|
35
41
|
## Start a session
|
|
36
42
|
|
|
37
43
|
```sh
|
|
38
|
-
npx agent-bridge session start # add --metro host:port if Metro isn't on localhost:8081
|
|
39
|
-
npx agent-bridge tools # what this app exposes
|
|
44
|
+
npx @avasapp/agent-bridge session start # add --metro host:port if Metro isn't on localhost:8081
|
|
45
|
+
npx @avasapp/agent-bridge tools # what this app exposes
|
|
40
46
|
```
|
|
41
47
|
|
|
48
|
+
If Metro runs on another port, pass the same `--metro` (or set
|
|
49
|
+
`AGENT_BRIDGE_METRO`) on every command that doesn't go through the session. A
|
|
50
|
+
`Nothing is listening on …` error names the Metro it found instead and its apps;
|
|
51
|
+
check it's yours before switching to it.
|
|
52
|
+
|
|
53
|
+
Calls take `--timeout <ms>` (default 10000) for a slow call. To drive two apps
|
|
54
|
+
at once, start named sessions (`session start --name ios`, `--name android`) and
|
|
55
|
+
pass `--session <name>` on each call. A session is found from the nearest
|
|
56
|
+
`package.json` above where you run: a command at a monorepo root also finds the
|
|
57
|
+
one session started in an app folder under it (downward only; a nested checkout
|
|
58
|
+
with its own `.git` doesn't count). With several app sessions under the root it
|
|
59
|
+
connects directly, so pass `--session`.
|
|
60
|
+
|
|
42
61
|
A session holds one connection for all your calls. It stops itself after 15
|
|
43
62
|
minutes without calls and runs `bridge.restore` when it does.
|
|
44
63
|
|
|
45
|
-
A JS reload (an error screen's Retry, or `app.reload`) throws away
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
64
|
+
A JS reload (an error screen's Retry, or `app.reload`) throws away the app's
|
|
65
|
+
undo state, except what lives in storage: string `mmkv.set`/`mmkv.delete` and
|
|
66
|
+
persisted zustand stores are kept by the session and still undone by
|
|
67
|
+
`bridge.restore`. The session reports what it kept, and
|
|
68
|
+
`app reloaded; N pending restores lost: query, ...` (the areas that had
|
|
69
|
+
something to undo) for the rest, as a warning on the next call (also on a
|
|
70
|
+
failed one) and again in `session stop`. For the lost ones, redo your setup:
|
|
71
|
+
they will not be undone.
|
|
51
72
|
|
|
52
73
|
If a call fails with `app not connected; reconnecting` or `The app is gone`, the
|
|
53
74
|
session lost the app (Metro or the app is down, or mid-reload). It retries on
|
|
@@ -55,17 +76,32 @@ its 5 s health check and on your next call: wait a few seconds and call again.
|
|
|
55
76
|
`session stop` still works. A session never moves to another app: `<app> is
|
|
56
77
|
not connected; connected: …` means its app is gone and something else is there.
|
|
57
78
|
|
|
79
|
+
If a call fails with `the app reloaded during <tool>; it may or may not have run`
|
|
80
|
+
(or `disconnected`, `the connection dropped`), the connection dropped mid-call
|
|
81
|
+
and the session did not repeat it (only read-only tools are repeated). Check the screen or state before you call again.
|
|
82
|
+
|
|
83
|
+
A `timeoutMs` in the options of `screen.waitFor` or `webview.waitFor` longer
|
|
84
|
+
than the call timeout stretches it, so you don't need `--timeout` for that.
|
|
85
|
+
|
|
58
86
|
If `tools` fails with "agent-bridge isn't running", the hook isn't mounted in
|
|
59
87
|
this build. Say so; don't fall back to tapping silently.
|
|
60
88
|
|
|
89
|
+
If the app doesn't have the package or a bridge file yet, run
|
|
90
|
+
`npx @avasapp/agent-bridge init --json` in the app (`--dir apps/x` from a
|
|
91
|
+
monorepo root). It writes `src/dev/agent-bridge.tsx` but never edits the root
|
|
92
|
+
layout: paste `mount.snippet` into `mount.file` as `mount.note` says. Ask the
|
|
93
|
+
user about each entry in `choices` it reports as `open` (such as network
|
|
94
|
+
tools), then rerun with the `flag` it names, or add its `lines` by hand when
|
|
95
|
+
there's no flag. Then follow `next`, from `runFrom`.
|
|
96
|
+
|
|
61
97
|
## Start from a scenario
|
|
62
98
|
|
|
63
99
|
Apps can define named setups, such as a locally signed-in user. Use one
|
|
64
100
|
instead of signing in by hand or seeding auth state yourself:
|
|
65
101
|
|
|
66
102
|
```sh
|
|
67
|
-
npx agent-bridge scenarios # what this app defines
|
|
68
|
-
npx agent-bridge call scenario.apply '["signedIn", {"user": {"name": "Ada"}}]'
|
|
103
|
+
npx @avasapp/agent-bridge scenarios # what this app defines
|
|
104
|
+
npx @avasapp/agent-bridge call scenario.apply '["signedIn", {"user": {"name": "Ada"}}]'
|
|
69
105
|
```
|
|
70
106
|
|
|
71
107
|
`scenarios` shows each one's options as a JSON Schema. Bad options fail
|
|
@@ -85,29 +121,37 @@ real account instead. If you're asked to add one, read
|
|
|
85
121
|
## See and use the screen
|
|
86
122
|
|
|
87
123
|
```sh
|
|
88
|
-
npx agent-bridge call screen.snapshot #
|
|
89
|
-
npx agent-bridge call screen.press '"add-plant"' # by testID, label or text
|
|
90
|
-
npx agent-bridge call screen.fill '["plant-name", "Fern"]' # runs the input's handlers
|
|
91
|
-
npx agent-bridge call screen.waitFor '"Name is required"' # {"gone": true} waits for it to go
|
|
92
|
-
npx agent-bridge call router.navigate /inbox
|
|
124
|
+
npx @avasapp/agent-bridge call screen.snapshot # one line per button, input, text on the focused screen (--json for rects)
|
|
125
|
+
npx @avasapp/agent-bridge call screen.press '"add-plant"' # by testID, label or text
|
|
126
|
+
npx @avasapp/agent-bridge call screen.fill '["plant-name", "Fern"]' # runs the input's handlers
|
|
127
|
+
npx @avasapp/agent-bridge call screen.waitFor '"Name is required"' # {"gone": true} waits for it to go
|
|
128
|
+
npx @avasapp/agent-bridge call router.navigate /inbox
|
|
93
129
|
```
|
|
94
130
|
|
|
131
|
+
Check `tools` for `router.*` (expo-router) or `nav.*` (React Navigation). `nav.*`
|
|
132
|
+
takes route names (`nav.navigate Inbox`, `nav.popTo Home`), and paths only if the
|
|
133
|
+
app passed `linking`; read the returned `warning`.
|
|
134
|
+
|
|
95
135
|
`press` and `fill` return once the app has rendered the result, so the next
|
|
96
|
-
check sees it.
|
|
136
|
+
check sees it. `press` on plain text presses the nearest pressable ancestor.
|
|
137
|
+
Unfocused tabs (native tabs included) and screens under others count as hidden.
|
|
138
|
+
Screen tools need React Native's New Architecture (Fabric); where nothing can
|
|
139
|
+
be measured they fail with a message saying so (`snapshot --all` and
|
|
140
|
+
`findText` still return text, without rects). Arguments are a JSON array (or a single JSON value): the array is
|
|
97
141
|
spread into the tool's arguments.
|
|
98
142
|
|
|
99
143
|
## Watch for errors
|
|
100
144
|
|
|
101
145
|
Every reply carries errors the app logged or threw since the previous reply,
|
|
102
146
|
printed as `! error during <tool>` or `! error after <tool>`. Treat them as
|
|
103
|
-
failures of the step they name. `npx agent-bridge call bridge.logs`
|
|
104
|
-
last 200 errors and warnings.
|
|
147
|
+
failures of the step they name. `npx @avasapp/agent-bridge call bridge.logs`
|
|
148
|
+
shows the last 200 errors and warnings.
|
|
105
149
|
|
|
106
150
|
## Clean up
|
|
107
151
|
|
|
108
152
|
```sh
|
|
109
|
-
npx agent-bridge call bridge.pending # what restore will put back, per area
|
|
110
|
-
npx agent-bridge session stop # runs bridge.restore; --keep leaves the app as is
|
|
153
|
+
npx @avasapp/agent-bridge call bridge.pending # what restore will put back, per area
|
|
154
|
+
npx @avasapp/agent-bridge session stop # runs bridge.restore; --keep leaves the app as is
|
|
111
155
|
```
|
|
112
156
|
|
|
113
157
|
`bridge.restore` unpins queries, puts back store values, removes network mocks,
|
|
@@ -123,6 +167,7 @@ from the real state.
|
|
|
123
167
|
| Read or set query data, store values, routes, modals; get argument shapes right | [references/state.md](references/state.md) |
|
|
124
168
|
| Mock or inspect requests, or deal with strict network mode | [references/network.md](references/network.md) |
|
|
125
169
|
| See or fake realtime messages (`realtime.*` in `tools`) | [references/realtime.md](references/realtime.md) |
|
|
170
|
+
| Fake a deep link, background, offline, a push or the clock (`device.*` in `tools`) | [references/device.md](references/device.md) |
|
|
126
171
|
| Handle a result over 32 KB or an argument over ~128 KB | [references/large-data.md](references/large-data.md) |
|
|
127
172
|
| Run many calls at once, write a flow file, or measure timing | [references/flows.md](references/flows.md) |
|
|
128
173
|
| Understand what restore undoes, or fix a value restore got wrong | [references/restore.md](references/restore.md) |
|
|
@@ -138,4 +183,7 @@ The app may expose its own tools for hooks the bridge has no tool for
|
|
|
138
183
|
overlap. Finish a flow with one real UI check or screenshot from agent-device.
|
|
139
184
|
- `fill` skips the keyboard: autocorrect and native-only input behaviour need
|
|
140
185
|
one real typing step from agent-device.
|
|
186
|
+
- Swipe and drag gestures need agent-device, unless the control is screen-reader
|
|
187
|
+
operable: `press` sends its `activate` accessibility action (see
|
|
188
|
+
[references/screen.md](references/screen.md)).
|
|
141
189
|
- With several apps on one Metro, pass `--device`.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Device events (`device.*`)
|
|
2
|
+
|
|
3
|
+
Only there if the app added `deviceTools`; check `bridge.tools`.
|
|
4
|
+
|
|
5
|
+
```sh
|
|
6
|
+
npx @avasapp/agent-bridge call device.openUrl '"myapp://orders/5"' # deep link into the running app; returns the route when the app passes navigation
|
|
7
|
+
npx @avasapp/agent-bridge call device.appState '"background"' # then '"active"'
|
|
8
|
+
npx @avasapp/agent-bridge call device.offline true # fetch, XHR and NetInfo; false goes back
|
|
9
|
+
npx @avasapp/agent-bridge call device.notification '[{"title":"Hi","data":{"id":5}},{"tapped":true}]'
|
|
10
|
+
npx @avasapp/agent-bridge call device.setTime '"2030-01-02T03:04:05Z"' # only with time: true
|
|
11
|
+
npx @avasapp/agent-bridge call device.restore
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
- `device.openUrl` is for a running app. A cold start reads its URL before the bridge can fake it: open that link with agent-device.
|
|
15
|
+
- `device.offline` fails mocked requests too. Metro stays reachable.
|
|
16
|
+
- Without `{tapped:true}` a notification calls the received listeners (app open); with it, the response listeners.
|
|
17
|
+
- The clock moves `Date.now()` and `new Date()` only; timers do not move. After `device.setTime`, call `device.appState '"background"'` then `'"active"'` so query caches recheck staleness (`active` alone does nothing when the app is already active). JS-driven `Animated` animations and Reanimated worklets are not shifted safely.
|
|
18
|
+
- `device.offline` shows in `net.log` as `(device offline)`. NetInfo is faked only for listeners, `useNetInfo` and `NetInfo.refresh`; `fetch('wifi')` and a real native network event are not.
|
|
19
|
+
- `device.notification` returns `listeners`; 0 means nothing heard it.
|
|
20
|
+
- `device.openUrl` also reaches `useLinkingURL()`, but `getLinkingURL()` still reads the native value.
|
|
21
|
+
- `bridge.pending` lists what is set under `device`, and `bridge.restore` undoes it.
|
|
@@ -8,7 +8,7 @@ one JSON line back per call.
|
|
|
8
8
|
|
|
9
9
|
```sh
|
|
10
10
|
printf '%s\n' 'router.navigate /inbox' 'screen.waitFor "Inbox"' 'screen.snapshot' \
|
|
11
|
-
| npx agent-bridge call --batch --stop-on-error
|
|
11
|
+
| npx @avasapp/agent-bridge call --batch --stop-on-error
|
|
12
12
|
# {"tool":"router.navigate","ok":true,"ms":1.8,"appMs":1,"value":...}
|
|
13
13
|
# {"tool":"screen.waitFor","ok":false,"ms":2003.1,"error":"..."} (plus "logs"/"notice" when present)
|
|
14
14
|
```
|
|
@@ -29,23 +29,23 @@ reading the output.
|
|
|
29
29
|
## A flow file
|
|
30
30
|
|
|
31
31
|
Write the steps into a flow file and run it when a step depends on an earlier
|
|
32
|
-
result, and for timing checks.
|
|
32
|
+
result, and for timing checks. Name it `flows/<name>.flow.mjs` in the app: plain
|
|
33
|
+
`.mjs`, never under `app/`, never `*.test.*`.
|
|
33
34
|
|
|
34
35
|
```js
|
|
35
36
|
export const scenario = 'signedIn' // applied first, undone after, even on failure
|
|
36
37
|
|
|
37
|
-
export default async ({ step }) => {
|
|
38
|
+
export default async ({ step, expect }) => {
|
|
38
39
|
await step('open form', 'screen.press', 'add-plant')
|
|
39
40
|
await step('save empty', 'screen.press', 'save-plant')
|
|
40
|
-
await
|
|
41
|
-
await step('undo', 'bridge.restore')
|
|
41
|
+
await expect.text('Name is required') // also expect.gone, expect.route, expect.result
|
|
42
42
|
}
|
|
43
43
|
```
|
|
44
44
|
|
|
45
45
|
```sh
|
|
46
|
-
npx agent-bridge run flow.mjs --strict # --strict fails if the app logged an error
|
|
47
|
-
npx agent-bridge run flow.mjs --scenario signedIn # add a scenario the flow doesn't declare
|
|
48
|
-
npx agent-bridge run flow.mjs --scenario 'signedIn={"user":{"name":"Ada"}}' # with options
|
|
46
|
+
npx @avasapp/agent-bridge run flows/check.flow.mjs --strict # --strict fails if the app logged an error
|
|
47
|
+
npx @avasapp/agent-bridge run flows/check.flow.mjs --scenario signedIn # add a scenario the flow doesn't declare
|
|
48
|
+
npx @avasapp/agent-bridge run flows/check.flow.mjs --scenario 'signedIn={"user":{"name":"Ada"}}' # with options
|
|
49
49
|
```
|
|
50
50
|
|
|
51
51
|
A flow can declare several scenarios, with options:
|
|
@@ -55,6 +55,17 @@ when the flow fails. The flow gets what each scenario's `apply` returned as
|
|
|
55
55
|
`scenarios.signedIn`. A failed restorer makes the run exit non-zero.
|
|
56
56
|
`--scenario` replaces a declared one of the same name.
|
|
57
57
|
|
|
58
|
+
Run several at once: `run flows/` (a directory's `*.flow.mjs`), several files, or a
|
|
59
|
+
glob, in order on one connection, with a pass/fail line each and a summary; exit 1 if
|
|
60
|
+
any failed. `--bail` stops at the first failure, `--grep <text>` filters by name or path
|
|
61
|
+
(relative to here), `--reporter json|junit` is for CI: report on stdout with all
|
|
62
|
+
other output on stderr, or `--out report.xml`. Globs only match `*.flow.mjs`. Every flow ends with
|
|
63
|
+
`bridge.restore`, pass or fail (it is global: through a session it also undoes what
|
|
64
|
+
you set up by hand first); `--keep` skips it to debug. A failed flow prints the
|
|
65
|
+
screen elements and route. `expect.text(t)`, `expect.gone(t)`, `expect.route(path)`
|
|
66
|
+
and `expect.result(tool, args, match)` (a RegExp matches a string) throw readable
|
|
67
|
+
messages.
|
|
68
|
+
|
|
58
69
|
`step` takes the same argument shapes as `call`; see [state.md](state.md).
|
|
59
70
|
|
|
60
71
|
## Timing
|
|
@@ -11,9 +11,9 @@ and `connect()` always get the full value.
|
|
|
11
11
|
Narrow the call before reaching for `--out`:
|
|
12
12
|
|
|
13
13
|
```sh
|
|
14
|
-
npx agent-bridge call query.get '["feed", {"pages": [0, 2]}]' # first two pages of an infinite query, plus totalPages
|
|
15
|
-
npx agent-bridge call query.get '["feed", {"path": "pages.0.items"}]' # just that value
|
|
16
|
-
npx agent-bridge call net.mocks '[{"full": true}]' # net.mocks cuts bodies over ~2 KB unless full
|
|
14
|
+
npx @avasapp/agent-bridge call query.get '["feed", {"pages": [0, 2]}]' # first two pages of an infinite query, plus totalPages
|
|
15
|
+
npx @avasapp/agent-bridge call query.get '["feed", {"path": "pages.0.items"}]' # just that value
|
|
16
|
+
npx @avasapp/agent-bridge call net.mocks '[{"full": true}]' # net.mocks cuts bodies over ~2 KB unless full
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
`pages` is `[from, to]` with `to` exclusive, like `Array.slice`; `path` applies
|
|
@@ -25,9 +25,9 @@ The shell caps one argument at about 128 KB on Linux. For a bigger value (a long
|
|
|
25
25
|
feed, a large `net.mock` body), write JSON to a file and pass `@file`:
|
|
26
26
|
|
|
27
27
|
```sh
|
|
28
|
-
npx agent-bridge call query.pin @args.json # the file is the argument list, like the inline form
|
|
29
|
-
npx agent-bridge call net.mock '"/feed"' @feed.json # several words: each is one argument; @file is its JSON as-is
|
|
30
|
-
some-generator | npx agent-bridge call store.set @- # @- reads stdin
|
|
28
|
+
npx @avasapp/agent-bridge call query.pin @args.json # the file is the argument list, like the inline form
|
|
29
|
+
npx @avasapp/agent-bridge call net.mock '"/feed"' @feed.json # several words: each is one argument; @file is its JSON as-is
|
|
30
|
+
some-generator | npx @avasapp/agent-bridge call store.set @- # @- reads stdin
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
A file holding an array is the argument list; any other JSON value is one
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
# Network mocks and logs
|
|
2
2
|
|
|
3
3
|
```sh
|
|
4
|
-
npx agent-bridge call net.mock '["/inbox", {"status": 500}]' # or {"offline": true}
|
|
5
|
-
npx agent-bridge call net.log '[{"since": 1700000000000}]' # entries carry startedAt (epoch ms)
|
|
6
|
-
npx agent-bridge call net.entry 3 # one request
|
|
7
|
-
npx agent-bridge call net.
|
|
8
|
-
npx agent-bridge call net.
|
|
4
|
+
npx @avasapp/agent-bridge call net.mock '["/inbox", {"status": 500}]' # or {"offline": true}
|
|
5
|
+
npx @avasapp/agent-bridge call net.log '[{"since": 1700000000000}]' # entries carry startedAt (epoch ms)
|
|
6
|
+
npx @avasapp/agent-bridge call net.entry 3 # one request: bodies whole, headers (credential headers such as Authorization, Cookie, *-token, *-api-key cut unless {"full": true} second)
|
|
7
|
+
npx @avasapp/agent-bridge call net.waitFor '[{"path": "/orders", "method": "POST"}, {"timeoutMs": 5000}]' # wait for it to finish
|
|
8
|
+
npx @avasapp/agent-bridge call net.mockFromLog '[3, {"user": {"name": "Moss"}}]' # a real response, patched
|
|
9
|
+
npx @avasapp/agent-bridge call net.mocks '[{"full": true}]' # active mocks; bodies over ~2 KB are cut unless full
|
|
9
10
|
```
|
|
10
11
|
|
|
11
12
|
`net.log` cuts bodies at about 2 KB; use `net.entry <id>` (or `{"full": true}`)
|
|
@@ -15,6 +16,23 @@ The newest mock is tried first, so a later broad mock shadows an earlier
|
|
|
15
16
|
specific one: give the specific one `{"priority": 1}` as `net.mock`'s third
|
|
16
17
|
argument, or read `shadows` in its result.
|
|
17
18
|
|
|
19
|
+
## What a mock matches
|
|
20
|
+
|
|
21
|
+
A string is a URL substring: `"/orders"` also answers `/orders/5/items`. For
|
|
22
|
+
exact matches use the object form (every key given must match):
|
|
23
|
+
|
|
24
|
+
```sh
|
|
25
|
+
call net.mock '[{"path": "/orders/:id"}, {"json": {"id": 5}}]' # whole path, any host, query ignored; * is the rest
|
|
26
|
+
call net.mock '[{"graphql": "GetUser"}, {"json": {"data": {}}}]' # operationName in the POST body or ?operationName=
|
|
27
|
+
call net.mock '[{"path": "/search", "body": {"filter": {"status": "open"}}}, {"json": []}]' # JSON body contains this
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`path` ignores case and the query, takes `:name` only as a whole segment (`/items:batchGet` is literal), and a trailing slash is optional; spaces are refused (write `%20`). A mock with only `body` or `graphql` looks at every request. `body` matches objects by key (extra keys are fine) and arrays by length and
|
|
31
|
+
position; form and binary bodies never match. A batched GraphQL array body
|
|
32
|
+
isn't matched by `graphql`; use `body`. `net.mockFromLog` returns a `warning` when a POST body wasn't kept and the mock may answer other operations. The reply's `matches` says it in words.
|
|
33
|
+
`net.waitFor` takes the same match; pass `{"sinceId": <id from net.log>}` so an
|
|
34
|
+
older request doesn't satisfy it.
|
|
35
|
+
|
|
18
36
|
For a mock body too big for the command line, pass it as `@file`; see
|
|
19
37
|
[large-data.md](large-data.md).
|
|
20
38
|
|
|
@@ -25,8 +43,8 @@ answers fails with a 501, and the reply carries an error that names it
|
|
|
25
43
|
(`! error during …: agent-bridge strict network: no mock for GET https://…`).
|
|
26
44
|
Add a mock for it (`net.mock`), don't turn strict mode off.
|
|
27
45
|
|
|
28
|
-
`npx agent-bridge call net.strict` lists what it blocked. Strict mode
|
|
29
|
-
`fetch`/XHR only, not images, WebSockets or native SDKs.
|
|
46
|
+
`npx @avasapp/agent-bridge call net.strict` lists what it blocked. Strict mode
|
|
47
|
+
sees JS `fetch`/XHR only, not images, WebSockets or native SDKs.
|
|
30
48
|
|
|
31
49
|
## Mocks and cached queries
|
|
32
50
|
|
|
@@ -4,11 +4,11 @@ If the app has `realtime.*` tools, you can see its realtime messages and fake
|
|
|
4
4
|
them instead of waiting for a backend to send one:
|
|
5
5
|
|
|
6
6
|
```sh
|
|
7
|
-
npx agent-bridge call realtime.channels # what the app listens on
|
|
8
|
-
npx agent-bridge call realtime.log '{"channel": "chat"}' # recent messages
|
|
9
|
-
npx agent-bridge call realtime.mute '"chat"' # stop real ones overwriting your state
|
|
10
|
-
npx agent-bridge call realtime.emit '["chat", {"name": "message", "data": {"text": "hi"}}]'
|
|
11
|
-
npx agent-bridge call realtime.connection '"disconnected"' # null goes back to the real state
|
|
7
|
+
npx @avasapp/agent-bridge call realtime.channels # what the app listens on
|
|
8
|
+
npx @avasapp/agent-bridge call realtime.log '{"channel": "chat"}' # recent messages: real, faked, and `sent: true` ones the app sent (credential keys are [redacted] unless `{"full":true}`)
|
|
9
|
+
npx @avasapp/agent-bridge call realtime.mute '"chat"' # stop real ones overwriting your state
|
|
10
|
+
npx @avasapp/agent-bridge call realtime.emit '["chat", {"name": "message", "data": {"text": "hi"}}]'
|
|
11
|
+
npx @avasapp/agent-bridge call realtime.connection '"disconnected"' # null goes back to the real state
|
|
12
12
|
```
|
|
13
13
|
|
|
14
14
|
Copy the shape of a real message from `realtime.log` before you emit one. With
|
|
@@ -1,20 +1,27 @@
|
|
|
1
1
|
# What restore undoes
|
|
2
2
|
|
|
3
3
|
```sh
|
|
4
|
-
npx agent-bridge call bridge.pending # what restore will put back, per area
|
|
5
|
-
npx agent-bridge session stop --dry-run # the same, and keeps the session
|
|
6
|
-
npx agent-bridge session stop # runs bridge.restore
|
|
7
|
-
npx agent-bridge session stop --keep # leaves the app as is; warns about anything still pending
|
|
4
|
+
npx @avasapp/agent-bridge call bridge.pending # what restore will put back, per area
|
|
5
|
+
npx @avasapp/agent-bridge session stop --dry-run # the same, and keeps the session
|
|
6
|
+
npx @avasapp/agent-bridge session stop # runs bridge.restore
|
|
7
|
+
npx @avasapp/agent-bridge session stop --keep # leaves the app as is; warns about anything still pending
|
|
8
8
|
```
|
|
9
9
|
|
|
10
|
-
`bridge.restore` unpins queries, puts back store and
|
|
10
|
+
`bridge.restore` unpins queries, puts back store, MMKV, AsyncStorage and SecureStore values, removes
|
|
11
11
|
network mocks, unmutes realtime channels and ends a faked connection state,
|
|
12
12
|
then undoes active scenarios (newest first), so the next run starts from the
|
|
13
13
|
real state. With `session stop --keep`, a later restore will still apply what
|
|
14
14
|
is pending.
|
|
15
15
|
|
|
16
|
-
A JS reload throws away
|
|
17
|
-
|
|
16
|
+
A JS reload throws away the app's undo state. In a session, what lives in
|
|
17
|
+
storage is kept: `mmkv.set`/`mmkv.delete` of string values and `store.set`/
|
|
18
|
+
`store.call` on a persisted zustand store (one made with `persist`) are still
|
|
19
|
+
undone by `bridge.restore` after the reload, and the next call says
|
|
20
|
+
`app reloaded; kept N undos across the reload (...)`. A value the app changed
|
|
21
|
+
after your write is left alone and named. Everything else (stores that are not
|
|
22
|
+
persisted, redux, jotai, AsyncStorage and SecureStore changes, queries, mocks, `onRestore`) is lost: the session warns
|
|
23
|
+
`app reloaded; N pending restores lost: ...`. Redo that setup when you see it. After a reload that also lost non-persisted changes, check `bridge.pending`: `kept N undos` does not list what was lost in the same area. `not kept across reload: store:key` means that key (a Set, Date, class instance, one `partialize` renames, or one `redact` hides) is not journaled, so the reload keeps the agent's value for it; `could not re-arm after the reload: ...` means a store or MMKV instance never became ready and its undo was dropped; `<tool> was cut off; ... that undo may be lost` means a write's reply was lost and may not be journaled.
|
|
24
|
+
AsyncStorage and SecureStore values stay on disk across a reload with their old values gone, so run `bridge.restore` before `app.reload`. A key the app wrote after the agent did is left alone and returned under `skipped`.
|
|
18
25
|
|
|
19
26
|
## Mocked queries
|
|
20
27
|
|
|
@@ -38,6 +45,17 @@ with `store.set` snapshots the broken one.
|
|
|
38
45
|
keeps the current value and drops the snapshot.
|
|
39
46
|
- `store.set <store> {}` arms the snapshot before you drive the UI.
|
|
40
47
|
|
|
48
|
+
`redux.*` and `jotai.*` work the same way, under their own names. `redux.set`
|
|
49
|
+
snapshots each slice on its first change and restore puts back only those
|
|
50
|
+
slices; `redux.dispatch` snapshots the whole state on its first call, so restore
|
|
51
|
+
puts all of it back, including what the app changed in other slices after that.
|
|
52
|
+
A slice or atom the app changed after the agent is left alone and returned in
|
|
53
|
+
`{restored, skipped}`. `redux.commit` drops the snapshots. Jotai snapshots each
|
|
54
|
+
atom on its first `jotai.set`; `jotai.commit <atom>` drops one. These snapshots
|
|
55
|
+
live in memory, so **restore before `app.reload`**: with redux-persist or
|
|
56
|
+
`atomWithStorage` the agent's values stay saved until then. With an async
|
|
57
|
+
`atomWithStorage`, `jotai.get` the atom before the first set.
|
|
58
|
+
|
|
41
59
|
## WebViews
|
|
42
60
|
|
|
43
61
|
`webview.press`, `fill` and `send` change the page and app, and restore does
|