@reticlehq/browser 2.3.0 → 2.4.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/dist/commands/commands.js +12 -0
- package/dist/connect-options.d.ts +97 -0
- package/dist/connect-options.js +1 -0
- package/dist/index.d.ts +3 -3
- package/dist/index.js +2 -2
- package/dist/registry/capabilities.d.ts +2 -0
- package/dist/registry/capabilities.js +16 -0
- package/dist/registry/store-adapters.d.ts +127 -1
- package/dist/registry/store-adapters.js +178 -0
- package/dist/reticle.d.ts +2 -57
- package/dist/reticle.js +28 -3
- package/dist/transport/transport.d.ts +10 -0
- package/dist/transport/transport.js +14 -0
- package/package.json +7 -2
|
@@ -3,6 +3,7 @@ import { buildSnapshot } from '../dom/snapshot.js';
|
|
|
3
3
|
import { matchQuery, runQuery } from '../dom/query.js';
|
|
4
4
|
import { executeAction, executeSequence, dispatchWebMcp, } from '../actions/actions.js';
|
|
5
5
|
import { describe } from '../dom/a11y.js';
|
|
6
|
+
import { sourceFor, formatSource } from '../dom/source.js';
|
|
6
7
|
import { themeReport } from '../dom/theme.js';
|
|
7
8
|
import { refs } from '../dom/refs.js';
|
|
8
9
|
import { isButton, isInput } from '../dom/realm.js';
|
|
@@ -74,8 +75,15 @@ function inspect(ref) {
|
|
|
74
75
|
visibility: cs.visibility,
|
|
75
76
|
}
|
|
76
77
|
: null;
|
|
78
|
+
// `describe()` carries the CHEAP source (the nearest babel-stamped ancestor), because it runs per
|
|
79
|
+
// element on paths that describe hundreds at once. inspect is a single-element path, so it takes
|
|
80
|
+
// the adapter's answer first — the component that actually RENDERED this element, not the nearest
|
|
81
|
+
// stamped host. `act` already did this; inspect did not, so the two tools disagreed about the same
|
|
82
|
+
// ref and inspect — the tool you reach for to ask where something lives — was the one saying null.
|
|
83
|
+
const source = formatSource(sourceFor(el, component?.source));
|
|
77
84
|
return {
|
|
78
85
|
...describe(el),
|
|
86
|
+
...(source !== undefined ? { source } : {}),
|
|
79
87
|
tag: el.tagName.toLowerCase(),
|
|
80
88
|
href: el.getAttribute('href') ?? undefined,
|
|
81
89
|
formAction: isButton(el) || isInput(el) ? (el.form?.getAttribute('action') ?? undefined) : undefined,
|
|
@@ -132,6 +140,10 @@ function readState(ref, store, path, depth) {
|
|
|
132
140
|
// what keeps "the list is short" distinguishable from "I shortened the list".
|
|
133
141
|
...(projected.truncation === undefined ? {} : { truncation: projected.truncation }),
|
|
134
142
|
...('availableKeys' in selection ? { availableKeys: selection.availableKeys } : {}),
|
|
143
|
+
// How many keys there REALLY were, when the near-miss list is a sample. Without it, 50 names
|
|
144
|
+
// and no marker reads as "the key you asked for does not exist" — the strongest negative
|
|
145
|
+
// signal there is, and a false one when the key is simply number 51.
|
|
146
|
+
...('totalKeys' in selection ? { totalKeys: selection.totalKeys } : {}),
|
|
135
147
|
storeNames: names,
|
|
136
148
|
};
|
|
137
149
|
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import type { RedactionConfig } from '@reticlehq/core';
|
|
2
|
+
/**
|
|
3
|
+
* What `reticle.connect()` accepts.
|
|
4
|
+
*
|
|
5
|
+
* Split out of reticle.ts when that file crossed the 600-line cap — the same move `session-info.ts`
|
|
6
|
+
* made for the server's Session. A pure description of the SDK's public entry contract has no reason
|
|
7
|
+
* to live inside the class that happens to consume it, and separating them keeps the surface every
|
|
8
|
+
* integrating app actually reads reviewable on its own.
|
|
9
|
+
*/
|
|
10
|
+
export interface ReticleConnectOptions {
|
|
11
|
+
/** WS endpoint of the local bridge. Defaults to ws://localhost:<port><path>. */
|
|
12
|
+
url?: string;
|
|
13
|
+
/** Human-friendly session label so the agent can target the right tab. */
|
|
14
|
+
session?: string;
|
|
15
|
+
/**
|
|
16
|
+
* Stable project identity, normally stamped by the build plugin (e.g. "acme-web-9f3c1d"). Lets the
|
|
17
|
+
* agent scope to the right app even when its dev server boots on an unexpected port. Optional.
|
|
18
|
+
*/
|
|
19
|
+
projectId?: string;
|
|
20
|
+
/** Browser/bridge pairing token. Required when either endpoint is non-localhost. */
|
|
21
|
+
token?: string;
|
|
22
|
+
/** Explicitly allow Reticle on a non-localhost page or bridge. Requires token. */
|
|
23
|
+
allowNonLocalhost?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Escape hatch for the production backstop. Reticle is dev-only and refuses to connect when the
|
|
26
|
+
* build reports NODE_ENV=production (an SSR healthcheck or a prod bundle opened locally would
|
|
27
|
+
* otherwise activate). The real fix is to gate the import behind `import.meta.env.DEV` so it's
|
|
28
|
+
* tree-shaken out; this flag only exists for the rare intentional prod diagnostic.
|
|
29
|
+
*/
|
|
30
|
+
allowInProduction?: boolean;
|
|
31
|
+
/** Show a small in-page status chip (connection + event count). */
|
|
32
|
+
overlay?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Capture request/response bodies on net.request events (dev-only; text-like content only,
|
|
35
|
+
* sensitive keys redacted, per-body capped). Off by default — bodies cost tokens and can carry PII.
|
|
36
|
+
*/
|
|
37
|
+
captureNetworkBodies?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* The project root, so source paths report repo-relative instead of absolute.
|
|
40
|
+
*
|
|
41
|
+
* React's `_debugSource.fileName` is absolute; the babel stamp is repo-relative. Build plugins that
|
|
42
|
+
* can `define` a global set it themselves; frameworks that cannot (Next passes it through env,
|
|
43
|
+
* Astro through its own config) hand it here instead. Taking it as a connect OPTION is what keeps
|
|
44
|
+
* the generated app-side code plain JavaScript — the previous shape made the caller assign a global,
|
|
45
|
+
* and the TypeScript cast that needed shipped into a `.jsx` file and broke the build.
|
|
46
|
+
*/
|
|
47
|
+
root?: string;
|
|
48
|
+
/** Presenter mode: glow border, animated cursor, click/hover effects, narration HUD. */
|
|
49
|
+
present?: boolean;
|
|
50
|
+
/** Per-action pacing (ms) in presenter mode so a human can follow. Default 450. */
|
|
51
|
+
pace?: number;
|
|
52
|
+
/** Min ms each narration line stays visible before the next replaces it (presenter). Default 3000. */
|
|
53
|
+
narrationDwellMs?: number;
|
|
54
|
+
/**
|
|
55
|
+
* Border behavior in presenter mode: 'session' (default) persists the border for the whole
|
|
56
|
+
* session; 'busy' restores the fade-after-idle behavior.
|
|
57
|
+
*/
|
|
58
|
+
border?: 'session' | 'busy';
|
|
59
|
+
/** Max accumulated activity-log rows before the oldest are pruned (presenter). Default 50. */
|
|
60
|
+
logMax?: number;
|
|
61
|
+
/**
|
|
62
|
+
* Mount the floating human-recorder toolbar (Record/Stop/Annotate).
|
|
63
|
+
* Default off — purely additive, dev-only.
|
|
64
|
+
*/
|
|
65
|
+
recorder?: boolean;
|
|
66
|
+
/**
|
|
67
|
+
* Mount the "Flag a bug" annotator: the human clicks an element that looks wrong, types what's
|
|
68
|
+
* wrong, and Reticle emits a HUMAN_MARK the agent drains via reticle_review. Defaults to ON with the
|
|
69
|
+
* presenter (it's the human's side of the look→act→assert loop); pass `annotate: false` to suppress.
|
|
70
|
+
*/
|
|
71
|
+
annotate?: boolean;
|
|
72
|
+
/** Live-control: overridable ended-border fade delay (native timer). Default 4000. */
|
|
73
|
+
endedFadeMs?: number;
|
|
74
|
+
/** Session auto-end after this much agent idle (presenter). Default 5min; agent-tunable via reticle_session. */
|
|
75
|
+
idleEndMs?: number;
|
|
76
|
+
/**
|
|
77
|
+
* Extend the redaction rules with your app's own vocabulary.
|
|
78
|
+
*
|
|
79
|
+
* ```ts
|
|
80
|
+
* reticle.connect({
|
|
81
|
+
* redact: {
|
|
82
|
+
* keys: ['deviceSecretRef', /^partner[-_]?code$/i], // also redact these
|
|
83
|
+
* allow: ['designToken'], // stop redacting this false positive
|
|
84
|
+
* },
|
|
85
|
+
* });
|
|
86
|
+
* ```
|
|
87
|
+
*
|
|
88
|
+
* Additive only — there is no way to replace the built-in rule, because a config that could would
|
|
89
|
+
* eventually ship an app that leaks. `keys` strings match a key name exactly (case-insensitively);
|
|
90
|
+
* a RegExp is tested against it. `allow` exempts a key from the DEFAULT rule and loses to `keys`.
|
|
91
|
+
*
|
|
92
|
+
* Literal `keys` strings also cross the bridge, so the daemon redacts them on the driven path,
|
|
93
|
+
* where request bodies are captured raw from the network stack and never pass through this SDK.
|
|
94
|
+
* RegExp entries and `allow` do NOT cross — see docs/usage.md ("Extending the redaction rules") for why, and what that means.
|
|
95
|
+
*/
|
|
96
|
+
redact?: RedactionConfig;
|
|
97
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -8,9 +8,9 @@ export { registerAdapter, identifyComponent, readComponentState, elementHasHover
|
|
|
8
8
|
export type { ReticleAdapter, ComponentInfo, ComponentSource } from './registry/adapters.js';
|
|
9
9
|
export { registerStore, unregisterStore, storeNames, readStores, readStoresWithTruncation, } from './registry/stores.js';
|
|
10
10
|
export type { StoreGetter, StoreLike, StoreSubscribe } from './registry/stores.js';
|
|
11
|
-
export { tanstackQueryStore, jotaiStore, xstateStore, valtioStore, mobxStore, pushStore, } from './registry/store-adapters.js';
|
|
12
|
-
export type { QuerySnapshot } from './registry/store-adapters.js';
|
|
13
|
-
export { registerCapabilities, getCapabilities, hasCapabilities } from './registry/capabilities.js';
|
|
11
|
+
export { tanstackQueryStore, jotaiStore, xstateStore, valtioStore, mobxStore, recoilStore, svelteStore, piniaStore, pushStore, } from './registry/store-adapters.js';
|
|
12
|
+
export type { QuerySnapshot, RecoilAtomSnapshot } from './registry/store-adapters.js';
|
|
13
|
+
export { registerCapabilities, getCapabilities, hasCapabilities, setCapabilitiesListener, } from './registry/capabilities.js';
|
|
14
14
|
export type { Capabilities, CapabilitiesInput, CapabilityFlow } from './registry/capabilities.js';
|
|
15
15
|
export { createReticleEmitter } from './registry/emitter.js';
|
|
16
16
|
export type { ReticleEmitter, EmitterTarget, CreateReticleEmitterOptions, } from './registry/emitter.js';
|
package/dist/index.js
CHANGED
|
@@ -18,9 +18,9 @@ export { registerStore, unregisterStore, storeNames, readStores, readStoresWithT
|
|
|
18
18
|
// Adapters for state libraries that do not natively expose `{getState, subscribe}`. zustand and Redux
|
|
19
19
|
// need none of these; TanStack Query is the one that matters most, because a stale-cache bug fires no
|
|
20
20
|
// request and renders a plausible value, so the cache is the only witness.
|
|
21
|
-
export { tanstackQueryStore, jotaiStore, xstateStore, valtioStore, mobxStore, pushStore, } from './registry/store-adapters.js';
|
|
21
|
+
export { tanstackQueryStore, jotaiStore, xstateStore, valtioStore, mobxStore, recoilStore, svelteStore, piniaStore, pushStore, } from './registry/store-adapters.js';
|
|
22
22
|
// Capability registry: the app self-describes its testable surface via reticle.describe.
|
|
23
|
-
export { registerCapabilities, getCapabilities, hasCapabilities } from './registry/capabilities.js';
|
|
23
|
+
export { registerCapabilities, getCapabilities, hasCapabilities, setCapabilitiesListener, } from './registry/capabilities.js';
|
|
24
24
|
// SDK helpers: adopt the recommended integration patterns without boilerplate.
|
|
25
25
|
export { createReticleEmitter } from './registry/emitter.js';
|
|
26
26
|
export { commitAndSignal } from './registry/commit-and-signal.js';
|
|
@@ -16,6 +16,8 @@ export interface CapabilitiesInput {
|
|
|
16
16
|
stores?: string[];
|
|
17
17
|
flows?: CapabilityFlow[];
|
|
18
18
|
}
|
|
19
|
+
/** Set by the SDK at connect. Idempotent; the last connect wins. */
|
|
20
|
+
export declare function setCapabilitiesListener(cb: (() => void) | undefined): void;
|
|
19
21
|
/** Called by the host app via reticle.describe. Merges (idempotent), never replaces wholesale. */
|
|
20
22
|
export declare function registerCapabilities(input: CapabilitiesInput): void;
|
|
21
23
|
/** Snapshot copy of the registered capabilities (defensive — never hand out the live arrays). */
|
|
@@ -12,6 +12,21 @@ function mergeUnique(into, add) {
|
|
|
12
12
|
if (!into.includes(v))
|
|
13
13
|
into.push(v);
|
|
14
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Notified whenever capabilities change, so the SDK can re-announce them to the bridge.
|
|
17
|
+
*
|
|
18
|
+
* `hasCapabilities` rides in the HELLO, which goes out at connect() — and registering deliberately
|
|
19
|
+
* happens AFTER connect, because `registerStore` needs a live SDK to subscribe through. Without a
|
|
20
|
+
* notification, an app that declared its whole testable surface still appeared to the agent as
|
|
21
|
+
* having none, permanently. The hook lives HERE rather than on `reticle.describe` because the
|
|
22
|
+
* documented entry point is this bare function; only wiring `describe` would have fixed the path
|
|
23
|
+
* almost nobody uses.
|
|
24
|
+
*/
|
|
25
|
+
let onChanged;
|
|
26
|
+
/** Set by the SDK at connect. Idempotent; the last connect wins. */
|
|
27
|
+
export function setCapabilitiesListener(cb) {
|
|
28
|
+
onChanged = cb;
|
|
29
|
+
}
|
|
15
30
|
/** Called by the host app via reticle.describe. Merges (idempotent), never replaces wholesale. */
|
|
16
31
|
export function registerCapabilities(input) {
|
|
17
32
|
mergeUnique(capabilities.testids, input.testids);
|
|
@@ -28,6 +43,7 @@ export function registerCapabilities(input) {
|
|
|
28
43
|
}
|
|
29
44
|
}
|
|
30
45
|
}
|
|
46
|
+
onChanged?.();
|
|
31
47
|
}
|
|
32
48
|
/** Snapshot copy of the registered capabilities (defensive — never hand out the live arrays). */
|
|
33
49
|
export function getCapabilities() {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { StoreLike } from './stores.js';
|
|
1
|
+
import type { StoreLike, StoreSubscribe } from './stores.js';
|
|
2
2
|
/**
|
|
3
3
|
* Adapters that give non-`{getState, subscribe}` state libraries the shape `registerStore` wants.
|
|
4
4
|
*
|
|
@@ -119,6 +119,132 @@ export declare function valtioStore<T extends object>(proxy: T, snapshot: (p: T)
|
|
|
119
119
|
* ```
|
|
120
120
|
*/
|
|
121
121
|
export declare function mobxStore<T>(observable: T, toJS: (value: T) => unknown, reaction: (track: () => unknown, effect: () => void) => () => void): StoreLike;
|
|
122
|
+
/** The minimum of a Recoil `Loadable` this adapter touches. */
|
|
123
|
+
interface RecoilLoadableLike {
|
|
124
|
+
/** 'hasValue' | 'loading' | 'hasError'. Widened to string: it arrives from outside this package. */
|
|
125
|
+
state: string;
|
|
126
|
+
/** The value, the pending promise, or the thrown error — which one depends on `state`. */
|
|
127
|
+
contents: unknown;
|
|
128
|
+
}
|
|
129
|
+
/** The minimum of a Recoil `Snapshot` this adapter touches, generic in the ATOM type. */
|
|
130
|
+
interface RecoilSnapshotLike<A> {
|
|
131
|
+
getLoadable: (atom: A) => RecoilLoadableLike;
|
|
132
|
+
}
|
|
133
|
+
/** How one Recoil atom is projected: the value never travels without the load state beside it. */
|
|
134
|
+
export interface RecoilAtomSnapshot {
|
|
135
|
+
status: string;
|
|
136
|
+
value: unknown;
|
|
137
|
+
error: string | null;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Expose a chosen set of Recoil atoms as one Reticle store.
|
|
141
|
+
*
|
|
142
|
+
* Three things about Recoil's API shape this adapter is built around, none of them optional:
|
|
143
|
+
*
|
|
144
|
+
* - **The atom map is forced, not an ergonomic choice.** Recoil has no enumerable registry of live
|
|
145
|
+
* atoms, so "the whole store" is not a thing that exists. Naming the atoms is the only way to
|
|
146
|
+
* snapshot them, and it doubles as a declaration of what matters — the same reason `jotaiStore`
|
|
147
|
+
* takes one.
|
|
148
|
+
* - **`.state`/`.contents`, never `.getValue()`.** On a pending async selector `getValue()` THROWS
|
|
149
|
+
* the pending promise, and on a failed one it throws the error. Either would take down the whole
|
|
150
|
+
* state read over one slow atom — so a loading atom reports `status: 'loading'` with a null value
|
|
151
|
+
* rather than silently becoming an empty object or an exception.
|
|
152
|
+
* - **One subscription, not one per atom.** Recoil exposes no per-atom subscription outside React
|
|
153
|
+
* (`jotaiStore` gets `store.sub(atom, …)`; there is no equivalent here). The transaction stream is
|
|
154
|
+
* the only public change signal, so the caller passes it in — the same "the library ships free
|
|
155
|
+
* functions, so hand them over" shape as `valtioStore` and `mobxStore`.
|
|
156
|
+
*
|
|
157
|
+
* Wire it from a bridge component, which is where `useRecoilTransactionObserver_UNSTABLE` lives:
|
|
158
|
+
*
|
|
159
|
+
* ```tsx
|
|
160
|
+
* const latest = useRef(snapshot_UNSTABLE());
|
|
161
|
+
* const listeners = useRef(new Set<() => void>()).current;
|
|
162
|
+
* useRecoilTransactionObserver_UNSTABLE(({ snapshot }) => {
|
|
163
|
+
* latest.current = snapshot;
|
|
164
|
+
* for (const l of listeners) l();
|
|
165
|
+
* });
|
|
166
|
+
* registerStore('recoil', recoilStore({ cart: cartAtom }, () => latest.current, (l) => {
|
|
167
|
+
* listeners.add(l);
|
|
168
|
+
* return () => listeners.delete(l);
|
|
169
|
+
* }));
|
|
170
|
+
* ```
|
|
171
|
+
*/
|
|
172
|
+
export declare function recoilStore<A>(atoms: Record<string, A>, snapshot: () => RecoilSnapshotLike<A>, subscribe: StoreSubscribe): StoreLike;
|
|
173
|
+
/**
|
|
174
|
+
* The minimum of a Svelte store this adapter touches.
|
|
175
|
+
*
|
|
176
|
+
* `subscribe` may return either the unsubscribe function or an object carrying one — the store
|
|
177
|
+
* contract permits both, and an RxJS-shaped source returns the object form.
|
|
178
|
+
*/
|
|
179
|
+
interface SvelteReadable {
|
|
180
|
+
subscribe: (run: (value: unknown) => void) => (() => void) | {
|
|
181
|
+
unsubscribe: () => void;
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* Expose a Svelte store as a Reticle store.
|
|
186
|
+
*
|
|
187
|
+
* Structurally unlike every other adapter in this file: they all wrap something that can be PULLED
|
|
188
|
+
* (`getState`, `getSnapshot`, `snapshot(proxy)`, `toJS`). A Svelte store has no pull side at all —
|
|
189
|
+
* `{ subscribe }` is the entire contract. What makes a pull possible anyway is the part of that
|
|
190
|
+
* contract which says `subscribe` must call back **immediately and synchronously** with the current
|
|
191
|
+
* value: subscribe, catch the value, unsubscribe, return it. That is precisely how `svelte/store`'s
|
|
192
|
+
* own `get()` is implemented, so it is a blessed read rather than a trick.
|
|
193
|
+
*
|
|
194
|
+
* Doing it that way is what keeps this adapter free of the `dispose` the obvious implementation
|
|
195
|
+
* needs. Caching the latest value would mean holding a permanent subscription, which means a
|
|
196
|
+
* teardown the other five adapters don't have, which means a leak the moment a caller forgets it.
|
|
197
|
+
* A transient subscription per read owns nothing and leaks nothing.
|
|
198
|
+
*
|
|
199
|
+
* ```ts
|
|
200
|
+
* registerStore('cart', svelteStore(cartStore));
|
|
201
|
+
* ```
|
|
202
|
+
*/
|
|
203
|
+
export declare function svelteStore(readable: SvelteReadable, warn?: (message: string) => void): StoreLike;
|
|
204
|
+
/**
|
|
205
|
+
* The minimum of a Pinia store this adapter touches.
|
|
206
|
+
*
|
|
207
|
+
* Two things here look over-specified and are not, both found by compiling against the real library
|
|
208
|
+
* rather than against a fake written to the same belief as the adapter:
|
|
209
|
+
*
|
|
210
|
+
* - **`$state: object`, not `Record<string, unknown>`.** A Pinia store's `$state` is a concrete
|
|
211
|
+
* object type with named keys and no index signature, and TypeScript rejects those for a
|
|
212
|
+
* `Record<string, unknown>` parameter. `piniaStore(useCartStore())` did not compile — the same
|
|
213
|
+
* shape of failure as the Jotai contravariance note above, and equally invisible to a fake.
|
|
214
|
+
* - **`flush` as the literal union**, not `string`. The option object is checked contravariantly, so
|
|
215
|
+
* a widened `string` here makes the real `$subscribe` unassignable to this shape and the whole
|
|
216
|
+
* store stops matching.
|
|
217
|
+
*/
|
|
218
|
+
interface PiniaStoreLike {
|
|
219
|
+
$state: object;
|
|
220
|
+
$subscribe: (callback: (mutation: unknown, state: unknown) => void, options?: {
|
|
221
|
+
detached?: boolean;
|
|
222
|
+
flush?: 'pre' | 'post' | 'sync';
|
|
223
|
+
}) => () => void;
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Expose a Pinia store as a Reticle store. `$state` / `$subscribe` are close to `{getState,
|
|
227
|
+
* subscribe}` but not close enough for `registerStore`'s duck-type, which is the whole reason a Vue
|
|
228
|
+
* app currently connects and then reports no state at all.
|
|
229
|
+
*
|
|
230
|
+
* Both `$subscribe` options are load-bearing:
|
|
231
|
+
*
|
|
232
|
+
* - **`detached`** keeps the subscription alive past the component that happened to register the
|
|
233
|
+
* store. Without it Pinia tears the listener down on unmount, so a store registered from inside a
|
|
234
|
+
* component goes permanently silent after the first route change — readable, but never again
|
|
235
|
+
* emitting a STATE_CHANGE, which reads exactly like an app that stopped changing.
|
|
236
|
+
* - **`flush: 'sync'`** puts the notification inside the action's attribution window. The default
|
|
237
|
+
* (`'post'`) defers it past the Vue update tick, and a state change that lands after the window
|
|
238
|
+
* closed is no longer linked to the click that caused it in the causal summary.
|
|
239
|
+
*
|
|
240
|
+
* `$state` carries state, not getters — a Pinia getter is derived, so asserting on the state it
|
|
241
|
+
* derives from is the stronger assertion anyway.
|
|
242
|
+
*
|
|
243
|
+
* ```ts
|
|
244
|
+
* registerStore('cart', piniaStore(useCartStore()));
|
|
245
|
+
* ```
|
|
246
|
+
*/
|
|
247
|
+
export declare function piniaStore(store: PiniaStoreLike): StoreLike;
|
|
122
248
|
/**
|
|
123
249
|
* Build a store whose value is PUSHED in rather than pulled — for state that has no object to read.
|
|
124
250
|
*
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { nativeWarn } from '../timers/native-console.js';
|
|
1
2
|
/**
|
|
2
3
|
* Expose a TanStack Query cache as a Reticle store, keyed by query key.
|
|
3
4
|
*
|
|
@@ -106,6 +107,183 @@ export function mobxStore(observable, toJS, reaction) {
|
|
|
106
107
|
subscribe: (listener) => reaction(() => toJS(observable), () => listener()),
|
|
107
108
|
};
|
|
108
109
|
}
|
|
110
|
+
/**
|
|
111
|
+
* The load states a Recoil `Loadable` reports. Named because the projection below branches on them
|
|
112
|
+
* and a bare 'hasValue' in three places is exactly the free string the rules forbid.
|
|
113
|
+
*/
|
|
114
|
+
const RecoilLoadState = {
|
|
115
|
+
HAS_VALUE: 'hasValue',
|
|
116
|
+
LOADING: 'loading',
|
|
117
|
+
HAS_ERROR: 'hasError',
|
|
118
|
+
};
|
|
119
|
+
function projectLoadable(loadable) {
|
|
120
|
+
if (loadable.state === RecoilLoadState.HAS_ERROR) {
|
|
121
|
+
const contents = loadable.contents;
|
|
122
|
+
return {
|
|
123
|
+
status: loadable.state,
|
|
124
|
+
value: null,
|
|
125
|
+
error: contents instanceof Error ? contents.message : String(contents),
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
// A LOADING loadable's `contents` is the pending promise. Sending it would serialize to `{}` and
|
|
129
|
+
// read as an empty value — so the value stays null and the status carries the truth.
|
|
130
|
+
if (loadable.state !== RecoilLoadState.HAS_VALUE) {
|
|
131
|
+
return { status: loadable.state, value: null, error: null };
|
|
132
|
+
}
|
|
133
|
+
return { status: loadable.state, value: loadable.contents, error: null };
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Expose a chosen set of Recoil atoms as one Reticle store.
|
|
137
|
+
*
|
|
138
|
+
* Three things about Recoil's API shape this adapter is built around, none of them optional:
|
|
139
|
+
*
|
|
140
|
+
* - **The atom map is forced, not an ergonomic choice.** Recoil has no enumerable registry of live
|
|
141
|
+
* atoms, so "the whole store" is not a thing that exists. Naming the atoms is the only way to
|
|
142
|
+
* snapshot them, and it doubles as a declaration of what matters — the same reason `jotaiStore`
|
|
143
|
+
* takes one.
|
|
144
|
+
* - **`.state`/`.contents`, never `.getValue()`.** On a pending async selector `getValue()` THROWS
|
|
145
|
+
* the pending promise, and on a failed one it throws the error. Either would take down the whole
|
|
146
|
+
* state read over one slow atom — so a loading atom reports `status: 'loading'` with a null value
|
|
147
|
+
* rather than silently becoming an empty object or an exception.
|
|
148
|
+
* - **One subscription, not one per atom.** Recoil exposes no per-atom subscription outside React
|
|
149
|
+
* (`jotaiStore` gets `store.sub(atom, …)`; there is no equivalent here). The transaction stream is
|
|
150
|
+
* the only public change signal, so the caller passes it in — the same "the library ships free
|
|
151
|
+
* functions, so hand them over" shape as `valtioStore` and `mobxStore`.
|
|
152
|
+
*
|
|
153
|
+
* Wire it from a bridge component, which is where `useRecoilTransactionObserver_UNSTABLE` lives:
|
|
154
|
+
*
|
|
155
|
+
* ```tsx
|
|
156
|
+
* const latest = useRef(snapshot_UNSTABLE());
|
|
157
|
+
* const listeners = useRef(new Set<() => void>()).current;
|
|
158
|
+
* useRecoilTransactionObserver_UNSTABLE(({ snapshot }) => {
|
|
159
|
+
* latest.current = snapshot;
|
|
160
|
+
* for (const l of listeners) l();
|
|
161
|
+
* });
|
|
162
|
+
* registerStore('recoil', recoilStore({ cart: cartAtom }, () => latest.current, (l) => {
|
|
163
|
+
* listeners.add(l);
|
|
164
|
+
* return () => listeners.delete(l);
|
|
165
|
+
* }));
|
|
166
|
+
* ```
|
|
167
|
+
*/
|
|
168
|
+
export function recoilStore(atoms, snapshot, subscribe) {
|
|
169
|
+
return {
|
|
170
|
+
// Resolved per call, not captured once: a Recoil snapshot is IMMUTABLE, so an adapter holding
|
|
171
|
+
// one would keep answering from the transaction it was built in and never see another write.
|
|
172
|
+
getState: () => {
|
|
173
|
+
const current = snapshot();
|
|
174
|
+
const out = {};
|
|
175
|
+
for (const [name, atom] of Object.entries(atoms)) {
|
|
176
|
+
out[name] = projectLoadable(current.getLoadable(atom));
|
|
177
|
+
}
|
|
178
|
+
return out;
|
|
179
|
+
},
|
|
180
|
+
subscribe,
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
function stopSubscription(handle) {
|
|
184
|
+
if (typeof handle === 'function') {
|
|
185
|
+
handle();
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
handle.unsubscribe();
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Expose a Svelte store as a Reticle store.
|
|
192
|
+
*
|
|
193
|
+
* Structurally unlike every other adapter in this file: they all wrap something that can be PULLED
|
|
194
|
+
* (`getState`, `getSnapshot`, `snapshot(proxy)`, `toJS`). A Svelte store has no pull side at all —
|
|
195
|
+
* `{ subscribe }` is the entire contract. What makes a pull possible anyway is the part of that
|
|
196
|
+
* contract which says `subscribe` must call back **immediately and synchronously** with the current
|
|
197
|
+
* value: subscribe, catch the value, unsubscribe, return it. That is precisely how `svelte/store`'s
|
|
198
|
+
* own `get()` is implemented, so it is a blessed read rather than a trick.
|
|
199
|
+
*
|
|
200
|
+
* Doing it that way is what keeps this adapter free of the `dispose` the obvious implementation
|
|
201
|
+
* needs. Caching the latest value would mean holding a permanent subscription, which means a
|
|
202
|
+
* teardown the other five adapters don't have, which means a leak the moment a caller forgets it.
|
|
203
|
+
* A transient subscription per read owns nothing and leaks nothing.
|
|
204
|
+
*
|
|
205
|
+
* ```ts
|
|
206
|
+
* registerStore('cart', svelteStore(cartStore));
|
|
207
|
+
* ```
|
|
208
|
+
*/
|
|
209
|
+
export function svelteStore(readable, warn = nativeWarn) {
|
|
210
|
+
let warnedAboutLazyStore = false;
|
|
211
|
+
return {
|
|
212
|
+
getState: () => {
|
|
213
|
+
let value;
|
|
214
|
+
let called = false;
|
|
215
|
+
stopSubscription(readable.subscribe((next) => {
|
|
216
|
+
value = next;
|
|
217
|
+
called = true;
|
|
218
|
+
}));
|
|
219
|
+
// A store that did NOT call back synchronously (an RxJS Observable that is not a
|
|
220
|
+
// BehaviorSubject, a hand-rolled store that breaks the contract) leaves `value` undefined —
|
|
221
|
+
// indistinguishable from a store legitimately holding undefined. Reading empty when the answer
|
|
222
|
+
// is "unknown" is the false green this project exists to prevent, so say it once, out loud.
|
|
223
|
+
if (!called && !warnedAboutLazyStore) {
|
|
224
|
+
warnedAboutLazyStore = true;
|
|
225
|
+
warn('[reticle] a store passed to svelteStore did not call its subscriber synchronously, so ' +
|
|
226
|
+
'its current value cannot be read. reticle_state will report undefined for it. Svelte ' +
|
|
227
|
+
'stores always call back immediately; an RxJS Observable does not unless it is a ' +
|
|
228
|
+
'BehaviorSubject.');
|
|
229
|
+
}
|
|
230
|
+
return value;
|
|
231
|
+
},
|
|
232
|
+
subscribe: (listener) => {
|
|
233
|
+
// Swallow whatever arrives DURING the subscribe call. Forwarding it would emit a STATE_CHANGE
|
|
234
|
+
// at REGISTRATION time for a change that never happened — a diff of nothing that shows up in
|
|
235
|
+
// causal summaries and that a {kind:'state'} predicate could satisfy without the app doing
|
|
236
|
+
// anything at all.
|
|
237
|
+
//
|
|
238
|
+
// The window is what makes this correct, rather than "swallow the first callback whenever it
|
|
239
|
+
// arrives". A store that does NOT call back synchronously (an RxJS Observable that is not a
|
|
240
|
+
// BehaviorSubject — the exact store `getState` warns about) has no registration callback to
|
|
241
|
+
// drop, so counting callbacks ate its first REAL change: no STATE_CHANGE, and a
|
|
242
|
+
// {kind:'state'} assertion with nothing to match. A silently missed state change is the false
|
|
243
|
+
// green this project exists to prevent, so the rule is positional, not ordinal.
|
|
244
|
+
let inSubscribeCall = true;
|
|
245
|
+
const handle = readable.subscribe(() => {
|
|
246
|
+
if (!inSubscribeCall)
|
|
247
|
+
listener();
|
|
248
|
+
});
|
|
249
|
+
inSubscribeCall = false;
|
|
250
|
+
return () => stopSubscription(handle);
|
|
251
|
+
},
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
/** Pinia `$subscribe` options. `sync` and `detached` are library option values, not free strings. */
|
|
255
|
+
const PINIA_SUBSCRIBE_OPTIONS = { detached: true, flush: 'sync' };
|
|
256
|
+
/**
|
|
257
|
+
* Expose a Pinia store as a Reticle store. `$state` / `$subscribe` are close to `{getState,
|
|
258
|
+
* subscribe}` but not close enough for `registerStore`'s duck-type, which is the whole reason a Vue
|
|
259
|
+
* app currently connects and then reports no state at all.
|
|
260
|
+
*
|
|
261
|
+
* Both `$subscribe` options are load-bearing:
|
|
262
|
+
*
|
|
263
|
+
* - **`detached`** keeps the subscription alive past the component that happened to register the
|
|
264
|
+
* store. Without it Pinia tears the listener down on unmount, so a store registered from inside a
|
|
265
|
+
* component goes permanently silent after the first route change — readable, but never again
|
|
266
|
+
* emitting a STATE_CHANGE, which reads exactly like an app that stopped changing.
|
|
267
|
+
* - **`flush: 'sync'`** puts the notification inside the action's attribution window. The default
|
|
268
|
+
* (`'post'`) defers it past the Vue update tick, and a state change that lands after the window
|
|
269
|
+
* closed is no longer linked to the click that caused it in the causal summary.
|
|
270
|
+
*
|
|
271
|
+
* `$state` carries state, not getters — a Pinia getter is derived, so asserting on the state it
|
|
272
|
+
* derives from is the stronger assertion anyway.
|
|
273
|
+
*
|
|
274
|
+
* ```ts
|
|
275
|
+
* registerStore('cart', piniaStore(useCartStore()));
|
|
276
|
+
* ```
|
|
277
|
+
*/
|
|
278
|
+
export function piniaStore(store) {
|
|
279
|
+
return {
|
|
280
|
+
// `$state` is a live reactive proxy, so this reads through to the current value rather than
|
|
281
|
+
// snapshotting at registration. The transport serializer walks it like any object and guards
|
|
282
|
+
// each key, which is what keeps a throwing reactive trap from costing the whole read.
|
|
283
|
+
getState: () => store.$state,
|
|
284
|
+
subscribe: (listener) => store.$subscribe(() => listener(), { ...PINIA_SUBSCRIBE_OPTIONS }),
|
|
285
|
+
};
|
|
286
|
+
}
|
|
109
287
|
/**
|
|
110
288
|
* Build a store whose value is PUSHED in rather than pulled — for state that has no object to read.
|
|
111
289
|
*
|
package/dist/reticle.d.ts
CHANGED
|
@@ -1,62 +1,7 @@
|
|
|
1
1
|
import { EventType, RETICLE_URL_PARAM, type ReticleEvent } from '@reticlehq/core';
|
|
2
2
|
import { type CapabilitiesInput } from './registry/capabilities.js';
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
url?: string;
|
|
6
|
-
/** Human-friendly session label so the agent can target the right tab. */
|
|
7
|
-
session?: string;
|
|
8
|
-
/**
|
|
9
|
-
* Stable project identity, normally stamped by the build plugin (e.g. "acme-web-9f3c1d"). Lets the
|
|
10
|
-
* agent scope to the right app even when its dev server boots on an unexpected port. Optional.
|
|
11
|
-
*/
|
|
12
|
-
projectId?: string;
|
|
13
|
-
/** Browser/bridge pairing token. Required when either endpoint is non-localhost. */
|
|
14
|
-
token?: string;
|
|
15
|
-
/** Explicitly allow Reticle on a non-localhost page or bridge. Requires token. */
|
|
16
|
-
allowNonLocalhost?: boolean;
|
|
17
|
-
/**
|
|
18
|
-
* Escape hatch for the production backstop. Reticle is dev-only and refuses to connect when the
|
|
19
|
-
* build reports NODE_ENV=production (an SSR healthcheck or a prod bundle opened locally would
|
|
20
|
-
* otherwise activate). The real fix is to gate the import behind `import.meta.env.DEV` so it's
|
|
21
|
-
* tree-shaken out; this flag only exists for the rare intentional prod diagnostic.
|
|
22
|
-
*/
|
|
23
|
-
allowInProduction?: boolean;
|
|
24
|
-
/** Show a small in-page status chip (connection + event count). */
|
|
25
|
-
overlay?: boolean;
|
|
26
|
-
/**
|
|
27
|
-
* Capture request/response bodies on net.request events (dev-only; text-like content only,
|
|
28
|
-
* sensitive keys redacted, per-body capped). Off by default — bodies cost tokens and can carry PII.
|
|
29
|
-
*/
|
|
30
|
-
captureNetworkBodies?: boolean;
|
|
31
|
-
/** Presenter mode: glow border, animated cursor, click/hover effects, narration HUD. */
|
|
32
|
-
present?: boolean;
|
|
33
|
-
/** Per-action pacing (ms) in presenter mode so a human can follow. Default 450. */
|
|
34
|
-
pace?: number;
|
|
35
|
-
/** Min ms each narration line stays visible before the next replaces it (presenter). Default 3000. */
|
|
36
|
-
narrationDwellMs?: number;
|
|
37
|
-
/**
|
|
38
|
-
* Border behavior in presenter mode: 'session' (default) persists the border for the whole
|
|
39
|
-
* session; 'busy' restores the fade-after-idle behavior.
|
|
40
|
-
*/
|
|
41
|
-
border?: 'session' | 'busy';
|
|
42
|
-
/** Max accumulated activity-log rows before the oldest are pruned (presenter). Default 50. */
|
|
43
|
-
logMax?: number;
|
|
44
|
-
/**
|
|
45
|
-
* Mount the floating human-recorder toolbar (Record/Stop/Annotate).
|
|
46
|
-
* Default off — purely additive, dev-only.
|
|
47
|
-
*/
|
|
48
|
-
recorder?: boolean;
|
|
49
|
-
/**
|
|
50
|
-
* Mount the "Flag a bug" annotator: the human clicks an element that looks wrong, types what's
|
|
51
|
-
* wrong, and Reticle emits a HUMAN_MARK the agent drains via reticle_review. Defaults to ON with the
|
|
52
|
-
* presenter (it's the human's side of the look→act→assert loop); pass `annotate: false` to suppress.
|
|
53
|
-
*/
|
|
54
|
-
annotate?: boolean;
|
|
55
|
-
/** Live-control: overridable ended-border fade delay (native timer). Default 4000. */
|
|
56
|
-
endedFadeMs?: number;
|
|
57
|
-
/** Session auto-end after this much agent idle (presenter). Default 5min; agent-tunable via reticle_session. */
|
|
58
|
-
idleEndMs?: number;
|
|
59
|
-
}
|
|
3
|
+
import type { ReticleConnectOptions } from './connect-options.js';
|
|
4
|
+
export type { ReticleConnectOptions };
|
|
60
5
|
/**
|
|
61
6
|
* Runtime backstop for the dev-only SDK: block connecting when the build reports production, unless
|
|
62
7
|
* explicitly overridden. Pure so it's testable; connect reads NODE_ENV safely (process may be absent
|
package/dist/reticle.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { EventType, RETICLE_DEFAULT_PORT, RETICLE_PROTOCOL_VERSION, RETICLE_URL_PARAM, bridgeWsUrl, ReticleCommand, MessageKind, SESSION_AUTO, SessionState, TRANSPORT_LIMITS, isLoopbackHostname, isLocalPage, } from '@reticlehq/core';
|
|
1
|
+
import { EventType, RETICLE_DEFAULT_PORT, RETICLE_PROTOCOL_VERSION, RETICLE_URL_PARAM, bridgeWsUrl, ReticleCommand, MessageKind, SESSION_AUTO, SessionState, TRANSPORT_LIMITS, buildRedactionPolicy, isLoopbackHostname, isLocalPage, setActiveRedactionPolicy, wireRedactionKeys, RETICLE_ROOT_GLOBAL, } from '@reticlehq/core';
|
|
2
2
|
import { createCommandRegistry, RELOAD_CACHE_BUST_PARAM, } from './commands/commands.js';
|
|
3
3
|
import { Transport } from './transport/transport.js';
|
|
4
4
|
import { adapterNames } from './registry/adapters.js';
|
|
5
|
-
import { registerCapabilities, hasCapabilities, } from './registry/capabilities.js';
|
|
5
|
+
import { registerCapabilities, setCapabilitiesListener, hasCapabilities, } from './registry/capabilities.js';
|
|
6
6
|
import { installAllObservers } from './observers/install-all.js';
|
|
7
7
|
import { installOverlay } from './presenter/overlay.js';
|
|
8
8
|
import { Presenter, LOG_KIND, LOG_RESULT, } from './presenter/presenter.js';
|
|
@@ -146,6 +146,8 @@ export class Reticle {
|
|
|
146
146
|
#eventCount = 0;
|
|
147
147
|
#token;
|
|
148
148
|
#projectId;
|
|
149
|
+
/** App-declared extra redaction keys, announced in hello so the driven path honours them too. */
|
|
150
|
+
#redactKeys = [];
|
|
149
151
|
/** Act-row log handle for the in-flight act/act_sequence, so its outcome stamps the right row. */
|
|
150
152
|
#actHandle;
|
|
151
153
|
connect(options = {}) {
|
|
@@ -172,6 +174,11 @@ export class Reticle {
|
|
|
172
174
|
globalThis.console.warn(`[Reticle] ${policy.reason ?? 'connection blocked'}`);
|
|
173
175
|
return;
|
|
174
176
|
}
|
|
177
|
+
// Publish the project root for the adapters' source relativisation. Done HERE, in library code,
|
|
178
|
+
// so the app-side connect stays plain JavaScript — see ReticleConnectOptions.root.
|
|
179
|
+
if (options.root !== undefined && options.root.length > 0) {
|
|
180
|
+
globalThis[RETICLE_ROOT_GLOBAL] = options.root;
|
|
181
|
+
}
|
|
175
182
|
// A pooled/headless launcher can stamp identity via namespaced URL params; explicit (non-auto)
|
|
176
183
|
// options win, but the `auto` sentinel defers to the URL param so leases correlate.
|
|
177
184
|
const identity = resolveConnectIdentity(options, window.location.search);
|
|
@@ -181,6 +188,10 @@ export class Reticle {
|
|
|
181
188
|
this.#token =
|
|
182
189
|
options.token !== undefined && options.token.length > 0 ? options.token : undefined;
|
|
183
190
|
this.#projectId = identity.projectId;
|
|
191
|
+
// Install the redaction rule BEFORE anything can emit. Every observer, the serializer and the
|
|
192
|
+
// snapshot path read it ambiently, so a policy set even one line after installAllObservers would
|
|
193
|
+
// leave the first events of the session redacted by a different rule than the rest.
|
|
194
|
+
this.#applyRedaction(options.redact);
|
|
184
195
|
this.#start = performance.now();
|
|
185
196
|
this.#registry = createCommandRegistry();
|
|
186
197
|
this.#transport = new Transport({
|
|
@@ -213,6 +224,9 @@ export class Reticle {
|
|
|
213
224
|
`or reticle.connect({ url })). Still retrying…`);
|
|
214
225
|
},
|
|
215
226
|
});
|
|
227
|
+
// Capabilities registered AFTER connect (which is when they are registered, by design) must
|
|
228
|
+
// reach the bridge, or an app that declared its whole testable surface reads as having none.
|
|
229
|
+
setCapabilitiesListener(() => this.#transport?.reannounce());
|
|
216
230
|
const emit = this.#emit;
|
|
217
231
|
this.#teardowns = installAllObservers(emit, {
|
|
218
232
|
captureBodies: options.captureNetworkBodies === true,
|
|
@@ -288,7 +302,7 @@ export class Reticle {
|
|
|
288
302
|
}
|
|
289
303
|
/** Advertise the app's testable surface so the agent learns it without reading source. */
|
|
290
304
|
describe(input) {
|
|
291
|
-
registerCapabilities(input);
|
|
305
|
+
registerCapabilities(input); // the registry notifies the transport — see setCapabilitiesListener
|
|
292
306
|
}
|
|
293
307
|
/** Live-control: end the session programmatically from the host app (drives the panel to ended). */
|
|
294
308
|
endSession() {
|
|
@@ -340,6 +354,16 @@ export class Reticle {
|
|
|
340
354
|
/* observation is best-effort; the host app's control flow is not */
|
|
341
355
|
}
|
|
342
356
|
};
|
|
357
|
+
/**
|
|
358
|
+
* Resolve the app's redaction config into the ambient policy, and remember the part of it that
|
|
359
|
+
* travels. A config of `undefined` is not "no policy" — it is the DEFAULT policy, installed
|
|
360
|
+
* explicitly so a second connect() in the same page (HMR, a re-mount) cannot inherit a rule the
|
|
361
|
+
* previous one set.
|
|
362
|
+
*/
|
|
363
|
+
#applyRedaction(config) {
|
|
364
|
+
setActiveRedactionPolicy(buildRedactionPolicy(config, nativeWarn));
|
|
365
|
+
this.#redactKeys = wireRedactionKeys(config);
|
|
366
|
+
}
|
|
343
367
|
#hello() {
|
|
344
368
|
return {
|
|
345
369
|
kind: MessageKind.HELLO,
|
|
@@ -351,6 +375,7 @@ export class Reticle {
|
|
|
351
375
|
adapters: adapterNames(),
|
|
352
376
|
...(this.#token === undefined ? {} : { token: this.#token }),
|
|
353
377
|
hasCapabilities: hasCapabilities(),
|
|
378
|
+
...(this.#redactKeys.length === 0 ? {} : { redactKeys: this.#redactKeys }),
|
|
354
379
|
};
|
|
355
380
|
}
|
|
356
381
|
async #handleCommand(command) {
|
|
@@ -45,6 +45,16 @@ export declare class Transport {
|
|
|
45
45
|
constructor(deps: TransportDeps);
|
|
46
46
|
connect(): void;
|
|
47
47
|
sendEvent(event: ReticleEvent): void;
|
|
48
|
+
/**
|
|
49
|
+
* Re-send the HELLO on a live socket.
|
|
50
|
+
*
|
|
51
|
+
* The hello carries `hasCapabilities`, and it goes out at connect() — before the app has had a
|
|
52
|
+
* chance to call `registerCapabilities`, which by design happens AFTER connect so `registerStore`
|
|
53
|
+
* has a live SDK to subscribe through. So an app that declared its whole testable surface still
|
|
54
|
+
* appeared to the agent as having none, and nothing ever corrected it. Cheap and idempotent: the
|
|
55
|
+
* bridge treats a hello as the session's current identity, not as a new session.
|
|
56
|
+
*/
|
|
57
|
+
reannounce(): void;
|
|
48
58
|
close(): void;
|
|
49
59
|
}
|
|
50
60
|
export {};
|
|
@@ -243,6 +243,20 @@ export class Transport {
|
|
|
243
243
|
t: event.t,
|
|
244
244
|
});
|
|
245
245
|
}
|
|
246
|
+
/**
|
|
247
|
+
* Re-send the HELLO on a live socket.
|
|
248
|
+
*
|
|
249
|
+
* The hello carries `hasCapabilities`, and it goes out at connect() — before the app has had a
|
|
250
|
+
* chance to call `registerCapabilities`, which by design happens AFTER connect so `registerStore`
|
|
251
|
+
* has a live SDK to subscribe through. So an app that declared its whole testable surface still
|
|
252
|
+
* appeared to the agent as having none, and nothing ever corrected it. Cheap and idempotent: the
|
|
253
|
+
* bridge treats a hello as the session's current identity, not as a new session.
|
|
254
|
+
*/
|
|
255
|
+
reannounce() {
|
|
256
|
+
if (this.#ws === undefined || this.#ws.readyState !== WebSocket.OPEN)
|
|
257
|
+
return;
|
|
258
|
+
this.#ws.send(JSON.stringify(this.#deps.hello()));
|
|
259
|
+
}
|
|
246
260
|
#sendRaw(text, event) {
|
|
247
261
|
if (this.#ws !== undefined && this.#ws.readyState === WebSocket.OPEN) {
|
|
248
262
|
this.#ws.send(text);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reticlehq/browser",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "Reticle browser SDK — installs observers, builds semantic snapshots, executes actions, talks to the bridge.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
},
|
|
14
14
|
"dependencies": {
|
|
15
15
|
"@testing-library/dom": "^10.4.1",
|
|
16
|
-
"@reticlehq/core": "2.
|
|
16
|
+
"@reticlehq/core": "2.4.0"
|
|
17
17
|
},
|
|
18
18
|
"devDependencies": {
|
|
19
19
|
"@tanstack/query-core": "^5.101.4",
|
|
@@ -21,7 +21,12 @@
|
|
|
21
21
|
"jotai": "^2.20.2",
|
|
22
22
|
"jsdom": "^29.1.1",
|
|
23
23
|
"mobx": "^7.0.0",
|
|
24
|
+
"pinia": "^4.0.2",
|
|
25
|
+
"react": "^19.2.8",
|
|
26
|
+
"recoil": "^0.7.7",
|
|
27
|
+
"svelte": "^5.56.8",
|
|
24
28
|
"valtio": "^2.3.2",
|
|
29
|
+
"vue": "^3.5.41",
|
|
25
30
|
"xstate": "^5.32.5"
|
|
26
31
|
},
|
|
27
32
|
"license": "Apache-2.0",
|