@mobileaidev/ai-app-bridge-web 0.3.4 → 0.3.6

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.
Files changed (3) hide show
  1. package/README.md +11 -7
  2. package/package.json +1 -1
  3. package/src/index.js +44 -5
package/README.md CHANGED
@@ -115,10 +115,11 @@ For SSR frameworks, initialize it only in client/browser code.
115
115
  </script>
116
116
  ```
117
117
 
118
- ## Continuous UI Observation
118
+ ## Bounded UI Observation
119
119
 
120
- `capture.ui` is opt-in. Set it to `true` for defaults, or pass an object to
121
- bound its work:
120
+ UI observation is off by default, including after `bridge.start()`.
121
+ `capture.ui` configures an explicitly requested window; `false` disables this
122
+ capability. Set it to `true` for defaults, or pass an object to bound its work:
122
123
 
123
124
  ```js
124
125
  capture: {
@@ -142,10 +143,13 @@ only `changed`, `length`, and `sensitive: true`. Non-sensitive input values are
142
143
  limited to 300 characters. DOM body text contributes only to the fingerprint
143
144
  hash and is not included in the UI event payload.
144
145
 
145
- This observer does not continuously capture screenshots or video. Call
146
- `bridge.stop()` (or the existing `bridge.disconnect()`) to flush pending UI
147
- events, remove listeners and observers, restore patched History methods, and
148
- close the connection. A later `bridge.start()` installs a fresh observer.
146
+ Open a window with `bridge.uiObservation({ operation: 'start', durationMs: 1500 })`
147
+ or the Host `web-ui-observation` command. Durations must be 100–5000 ms.
148
+ `{ operation: 'stop', leaseId }` releases that owner's window early;
149
+ `{ operation: 'status' }` reads its state. Expiry, disconnect and SDK stop flush
150
+ pending events, remove UI listeners and restore History methods. A later SDK
151
+ start leaves observation off. Missing events outside a window cannot establish
152
+ UI stability or action success. No screenshots or video are captured by this API.
149
153
 
150
154
  This package is intended for debug and test builds. Keep command handlers
151
155
  whitelisted and do not enable it in production without a deliberate security
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mobileaidev/ai-app-bridge-web",
3
- "version": "0.3.4",
3
+ "version": "0.3.6",
4
4
  "description": "Browser SDK for AI App Bridge Web sessions.",
5
5
  "main": "src/index.js",
6
6
  "browser": "src/index.js",
package/src/index.js CHANGED
@@ -27,13 +27,13 @@
27
27
  sessionId: options.sessionId ?? storedSessionId(options.storageKey ?? 'ai_app_bridge_web_session_id'),
28
28
  actions: new Map(), stateProviders: new Map(), restores: [], active: null, completion: null,
29
29
  dropped: 0, rejected: 0, lastReceipt: null, lastError: null, reconnectDelay: 1000,
30
- captureActionId: null, flushUi: null,
30
+ captureActionId: null, flushUi: null, uiWindow: null,
31
31
  captures: Object.fromEntries(captureStreams.map(stream => [stream, { sequence: 0, losses: 0, pending: 0 }])),
32
32
  };
33
33
  if (typeof state.sessionId !== 'string' || !state.sessionId || state.sessionId.length > 1024)
34
34
  throw new Error('invalid_web_session_id');
35
35
  const api = { start, stop: disconnect, disconnect, recordLog, recordNetwork, recordState, recordEvent,
36
- registerAction, unregisterAction, registerStateProvider, snapshotDom: observeDom,
36
+ registerAction, unregisterAction, registerStateProvider, snapshotDom: observeDom, uiObservation,
37
37
  sessionId: () => state.sessionId, runtimeEpoch: () => state.runtimeEpoch,
38
38
  isConnected: () => state.connected,
39
39
  transportStatus: () => ({ connected: state.connected, queuedRecords: state.queue.length, queuedBytes: state.queueBytes,
@@ -47,6 +47,7 @@
47
47
  state.started = true; state.restores.push(trackDocument()); installCaptures(); connect(); return api;
48
48
  }
49
49
  function disconnect() {
50
+ stopUiObservation();
50
51
  state.active?.cancel('web_sdk_stopped');
51
52
  for (const restore of state.restores.splice(0).reverse()) {
52
53
  try { restore(); } catch (error) { state.lastError = error.message; }
@@ -73,6 +74,7 @@
73
74
  };
74
75
  socket.onclose = event => {
75
76
  if (state.socket !== socket) return;
77
+ stopUiObservation();
76
78
  state.connected = false; state.binding = null; state.socket = null;
77
79
  if (event?.code === 1008) { state.lastError = 'web_protocol_rejected'; disconnect(); return; }
78
80
  if (state.started && options.reconnect !== false && state.reconnectTimer === null) {
@@ -174,6 +176,7 @@
174
176
  return;
175
177
  }
176
178
  if (message.type === 'read') {
179
+ if (message.payload?.name === 'uiObservation') { reply(message, uiObservation(message.payload.args)); return; }
177
180
  if (message.payload?.name === 'captureBarrier') {
178
181
  const stream = message.payload.args?.stream;
179
182
  if (!captureStreams.includes(stream)) { reply(message, { ok: false, error: 'invalid_web_capture_stream' }); return; }
@@ -275,13 +278,43 @@
275
278
  const dom = snapshotDom(options);
276
279
  return dom.ok ? { ...dom, pageRef: documentPageRef(target()) } : dom;
277
280
  }
281
+ function uiObservation(request = {}) {
282
+ if (!state.started) return { ok: false, error: 'web_sdk_stopped' };
283
+ if (state.uiWindow && performance.now() - state.uiWindow.startedAt >= state.uiWindow.durationMs) stopUiObservation();
284
+ const fields = { start: ['operation', 'durationMs'], stop: ['operation', 'leaseId'], status: ['operation'] }[request.operation];
285
+ if (!fields || Object.keys(request).length !== fields.length || !Object.keys(request).every(key => fields.includes(key)))
286
+ return { ok: false, error: 'invalid_ui_observation_request' };
287
+ if (request.operation === 'start') {
288
+ if (!Number.isInteger(request.durationMs) || request.durationMs < 100 || request.durationMs > 5000)
289
+ return { ok: false, error: 'invalid_ui_observation_duration' };
290
+ if (state.uiWindow) return { ok: false, error: 'ui_observation_busy' };
291
+ const startedAt = performance.now();
292
+ const cleanup = installUiCapture(state, recordEvent, options.capture?.ui ?? {});
293
+ if (!cleanup) return { ok: false, error: 'web_ui_observation_unavailable' };
294
+ state.uiWindow = { leaseId: newIdentity(), startedAt, durationMs: request.durationMs,
295
+ cleanup, timer: setTimeout(stopUiObservation, Math.max(0, request.durationMs - (performance.now() - startedAt))) };
296
+ } else if (request.operation === 'stop') {
297
+ if (typeof request.leaseId !== 'string' || request.leaseId !== state.uiWindow?.leaseId)
298
+ return { ok: false, error: 'ui_observation_lease_mismatch' };
299
+ stopUiObservation();
300
+ }
301
+ return { ok: true, schemaVersion: 'aab.ui-observation/v1', mode: 'on-demand', active: Boolean(state.uiWindow),
302
+ leaseId: state.uiWindow?.leaseId ?? null,
303
+ remainingMs: state.uiWindow ? Math.max(0, Math.ceil(state.uiWindow.durationMs - (performance.now() - state.uiWindow.startedAt))) : 0,
304
+ maxDurationMs: 5000 };
305
+ }
306
+ function stopUiObservation() {
307
+ const window = state.uiWindow;
308
+ if (!window) return;
309
+ state.uiWindow = null; clearTimeout(window.timer); window.cleanup();
310
+ }
278
311
  function installCaptures() {
279
312
  const captureOptions = options.capture ?? {};
280
313
  if (captureOptions.console) installConsoleCapture(state, recordLog);
281
314
  if (captureOptions.errors) installErrorCapture(state, recordLog);
282
315
  if (captureOptions.fetch) installFetchCapture(state, recordNetwork, options);
283
316
  if (captureOptions.xhr) installXhrCapture(state, recordNetwork, options);
284
- if (captureOptions.ui) installUiCapture(state, recordEvent, captureOptions.ui);
317
+ // capture.ui configures explicit observation windows; start() never scans DOM.
285
318
  }
286
319
  return api;
287
320
  }
@@ -759,6 +792,8 @@
759
792
  }
760
793
 
761
794
  function installUiCapture(state, recordEvent, captureOption) {
795
+ const restores = [];
796
+ let capturing = true;
762
797
  const options = captureOption === true ? {} : captureOption;
763
798
  if (!options || options.enabled === false) return;
764
799
  const doc = options.document || globalValue('document');
@@ -808,6 +843,7 @@
808
843
  if (pending.length) scheduleFlush();
809
844
  };
810
845
  const enqueue = (event) => {
846
+ if (!capturing) return;
811
847
  if (!state.started) return;
812
848
  if (pending.length >= maxPendingEvents) {
813
849
  pending.shift();
@@ -824,7 +860,7 @@
824
860
  scheduleFlush();
825
861
  };
826
862
  state.flushUi = () => flush(true);
827
- state.restores.push(() => { state.flushUi = null; });
863
+ restores.push(() => { state.flushUi = null; });
828
864
  const onClick = (event) => {
829
865
  enqueue({
830
866
  type: 'interaction.click',
@@ -869,6 +905,8 @@
869
905
  const onHashChange = () => onRoute('hashchange');
870
906
  const onMutations = (records) => {
871
907
  if (!state.started) return;
908
+ const window = state.uiWindow;
909
+ if (!window || performance.now() - window.startedAt >= window.durationMs) return;
872
910
  const previousFingerprint = domFingerprint;
873
911
  const nextFingerprint = fingerprintDom(doc, win, options, fingerprintSalt);
874
912
  const fingerprintChanged = previousFingerprint.hash !== nextFingerprint.hash;
@@ -947,7 +985,7 @@
947
985
  win.addEventListener('popstate', onPopState);
948
986
  win.addEventListener('hashchange', onHashChange);
949
987
  }
950
- state.restores.push(() => {
988
+ restores.push(() => {
951
989
  doc.removeEventListener('click', onClick, true);
952
990
  doc.removeEventListener('input', onInput, true);
953
991
  doc.removeEventListener('change', onChange, true);
@@ -965,6 +1003,7 @@
965
1003
  if (mutationObserver) mutationObserver.disconnect();
966
1004
  flush(true);
967
1005
  });
1006
+ return () => { capturing = false; for (const restore of restores.reverse()) restore(); };
968
1007
  }
969
1008
 
970
1009
  function fingerprintDom(doc, win, options, salt) {