@qontinui/ui-bridge-native 0.6.8 → 0.6.10

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/index.mjs CHANGED
@@ -26,7 +26,7 @@ function inferActions(type) {
26
26
  case "pressable":
27
27
  return [...baseActions, "click", "press", "longPress", "doubleTap"];
28
28
  case "input":
29
- return [...baseActions, "click", "press", "type", "clear"];
29
+ return [...baseActions, "click", "press", "type", "setValue", "clear"];
30
30
  case "text":
31
31
  return [...baseActions, "click", "press", "longPress"];
32
32
  case "view":
@@ -985,9 +985,10 @@ var DefaultNativeActionExecutor = class {
985
985
  requestId: request.requestId,
986
986
  durationMs: successTime - startTime
987
987
  });
988
+ const fresh = this.registry.getElement(registered.id) ?? registered;
988
989
  return {
989
990
  success: true,
990
- elementState: registered.getState(),
991
+ elementState: fresh.getState(),
991
992
  result,
992
993
  durationMs: successTime - startTime,
993
994
  timestamp: successTime,
@@ -1040,6 +1041,8 @@ var DefaultNativeActionExecutor = class {
1040
1041
  return this.performDoubleTap(props);
1041
1042
  case "type":
1042
1043
  return this.performType(element, props, params);
1044
+ case "setValue":
1045
+ return this.performSetValue(element, props, params);
1043
1046
  case "clear":
1044
1047
  return this.performClear(element, props);
1045
1048
  case "focus":
@@ -1132,6 +1135,38 @@ var DefaultNativeActionExecutor = class {
1132
1135
  this.registry.updateElementState(element.id, { value: params.text });
1133
1136
  }
1134
1137
  }
1138
+ /**
1139
+ * Perform setValue action.
1140
+ *
1141
+ * Web/runner UI Bridge parity: `setValue` replaces the input's value
1142
+ * atomically in a single call (no per-character keystroke simulation),
1143
+ * mirroring the web bridge's `setValue` semantics. On native there is no
1144
+ * DOM value to assign, so we drive the controlled-input handler
1145
+ * (`onChangeText`, falling back to `onChange`) with the full string. The
1146
+ * registry's `state.value` is then synced so a subsequent GET on the
1147
+ * element reflects the new value.
1148
+ *
1149
+ * Unlike `type`, this does not append to existing content and ignores
1150
+ * `delay` — it is the deterministic "set the field to exactly this" action.
1151
+ */
1152
+ async performSetValue(element, props, params) {
1153
+ const next = typeof params?.text === "string" ? params.text : typeof params?.value === "string" ? params.value : void 0;
1154
+ if (typeof next !== "string") {
1155
+ throw new Error('setValue action requires a string "text" (or "value") parameter');
1156
+ }
1157
+ if (typeof props.onChangeText === "function") {
1158
+ props.onChangeText(next);
1159
+ } else if (typeof props.onChange === "function") {
1160
+ props.onChange({
1161
+ nativeEvent: { text: next }
1162
+ });
1163
+ } else {
1164
+ throw new Error("No text change handler found on element");
1165
+ }
1166
+ if (element) {
1167
+ this.registry.updateElementState(element.id, { value: next });
1168
+ }
1169
+ }
1135
1170
  /**
1136
1171
  * Perform clear action
1137
1172
  */
@@ -2124,6 +2159,13 @@ function matchElementsByText(registry, text, opts) {
2124
2159
  scored.sort((a, b) => a.rank - b.rank);
2125
2160
  return scored.map((s) => s.element);
2126
2161
  }
2162
+ function resolveElementState(element) {
2163
+ const state = element.getState();
2164
+ if (element.type === "input" && state.value === void 0 && typeof element.props?.value === "string") {
2165
+ return { ...state, value: element.props.value };
2166
+ }
2167
+ return state;
2168
+ }
2127
2169
  async function executeWorkflowStep(step, registry, executor) {
2128
2170
  const startTime = Date.now();
2129
2171
  try {
@@ -2360,7 +2402,7 @@ function createServerHandlers(registry, executor, config) {
2360
2402
  type: element.type,
2361
2403
  label: element.label,
2362
2404
  identifier: element.getIdentifier(),
2363
- state: element.getState(),
2405
+ state: resolveElementState(element),
2364
2406
  actions: element.actions,
2365
2407
  customActions: element.customActions ? Object.keys(element.customActions) : void 0,
2366
2408
  registeredHandlers: handlers.length > 0 ? handlers : void 0,
@@ -2374,18 +2416,19 @@ function createServerHandlers(registry, executor, config) {
2374
2416
  if (!element) {
2375
2417
  return error2(`Element not found: ${id}`, "ELEMENT_NOT_FOUND");
2376
2418
  }
2377
- return success2({ state: element.getState() });
2419
+ return success2({ state: resolveElementState(element) });
2378
2420
  },
2379
2421
  executeAction: async (ctx) => {
2380
- const { id } = ctx.params;
2381
- const body = ctx.body;
2382
- if (!body?.action) {
2422
+ const rawBody = ctx.body ?? {};
2423
+ const envelope = rawBody.request && typeof rawBody.request === "object" ? rawBody.request : rawBody;
2424
+ const id = ctx.params.id ?? rawBody.id ?? "";
2425
+ if (!envelope?.action) {
2383
2426
  return error2("Action is required", "INVALID_REQUEST");
2384
2427
  }
2385
2428
  const response = await executor.executeAction(id, {
2386
- action: body.action,
2387
- params: body.params,
2388
- waitOptions: body.waitOptions
2429
+ action: envelope.action,
2430
+ params: envelope.params,
2431
+ waitOptions: envelope.waitOptions
2389
2432
  });
2390
2433
  if (!response.success) {
2391
2434
  return error2(response.error || "Action failed", "ACTION_FAILED");
@@ -2897,7 +2940,8 @@ function createServerHandlers(registry, executor, config) {
2897
2940
  return success2({
2898
2941
  entries: [],
2899
2942
  count: 0,
2900
- bufferSize: 0
2943
+ bufferSize: 0,
2944
+ installed: false
2901
2945
  });
2902
2946
  }
2903
2947
  const entries = buf.entries({
@@ -2907,7 +2951,8 @@ function createServerHandlers(registry, executor, config) {
2907
2951
  return success2({
2908
2952
  entries,
2909
2953
  count: entries.length,
2910
- bufferSize: buf.size()
2954
+ bufferSize: buf.size(),
2955
+ installed: buf.isInstalled()
2911
2956
  });
2912
2957
  },
2913
2958
  getNetworkRequests: async (ctx) => {
@@ -2918,7 +2963,8 @@ function createServerHandlers(registry, executor, config) {
2918
2963
  return success2({
2919
2964
  entries: [],
2920
2965
  count: 0,
2921
- bufferSize: 0
2966
+ bufferSize: 0,
2967
+ installed: false
2922
2968
  });
2923
2969
  }
2924
2970
  const entries = buf.entries({
@@ -2928,7 +2974,8 @@ function createServerHandlers(registry, executor, config) {
2928
2974
  return success2({
2929
2975
  entries,
2930
2976
  count: entries.length,
2931
- bufferSize: buf.size()
2977
+ bufferSize: buf.size(),
2978
+ installed: buf.isInstalled()
2932
2979
  });
2933
2980
  },
2934
2981
  // Test hooks — modal stack manipulation
@@ -3020,7 +3067,7 @@ function createServerHandlers(registry, executor, config) {
3020
3067
  };
3021
3068
  if (config?.appInfo) {
3022
3069
  response.uiBridge = {
3023
- version: "0.3.0",
3070
+ version: "0.6.10",
3024
3071
  ...config.appInfo,
3025
3072
  capabilities: ["elements", "components", "actions", "design"],
3026
3073
  elementCount: stats.elements,
@@ -3347,7 +3394,10 @@ var WS_ROUTES = [
3347
3394
  // same handler at both paths to keep agents portable across platforms.
3348
3395
  { pattern: "ai/find", handler: "find", httpMethods: ["POST"] },
3349
3396
  { pattern: "control/snapshot", handler: "getSnapshot", httpMethods: ["GET"] },
3350
- { pattern: "control/discover", handler: "getSnapshot" },
3397
+ // Discovery alias — web/runner parity with `POST /control/discover` (a
3398
+ // legacy alias for snapshot/find). Mounted on HTTP (GET+POST) so mobile
3399
+ // doesn't 404 where web returns 200; delegates to the snapshot handler.
3400
+ { pattern: "control/discover", handler: "getSnapshot", httpMethods: ["GET", "POST"] },
3351
3401
  // Page health — accepts both GET and POST so the canonical skill invocation
3352
3402
  // (`POST .../control/page-health` with an empty body) and ad-hoc curls both
3353
3403
  // work without a body-shape gotcha.
@@ -3441,19 +3491,26 @@ var WS_ROUTES = [
3441
3491
  httpMethods: ["POST"],
3442
3492
  requiresTestHooks: true
3443
3493
  },
3444
- // Observability — last-N console errors + network requests. testHooks-gated
3445
- // because patching `console.error` / `fetch` is invasive in production.
3494
+ // Observability — last-N console errors + network requests.
3495
+ //
3496
+ // Always mounted (not testHooks-gated) so they return a schema-valid 200
3497
+ // on every build instead of a 404 that agents can't distinguish from a
3498
+ // missing route. The INVASIVE part — monkey-patching `console.error` /
3499
+ // `fetch` / `XMLHttpRequest` — stays gated behind `testHooks` (see
3500
+ // NativeUIBridgeServer.start → buffer.install()). When the patches aren't
3501
+ // installed the handlers report `installed: false` + empty entries, so
3502
+ // production builds expose the contract without observing real users'
3503
+ // console/network traffic. Set `features.testHooks` (typically on
3504
+ // `__DEV__`) to populate the buffers with live data.
3446
3505
  {
3447
3506
  pattern: "control/console-errors",
3448
3507
  handler: "getConsoleErrors",
3449
- httpMethods: ["GET"],
3450
- requiresTestHooks: true
3508
+ httpMethods: ["GET"]
3451
3509
  },
3452
3510
  {
3453
3511
  pattern: "sdk/network-requests",
3454
3512
  handler: "getNetworkRequests",
3455
- httpMethods: ["GET"],
3456
- requiresTestHooks: true
3513
+ httpMethods: ["GET"]
3457
3514
  }
3458
3515
  ];
3459
3516
  var HANDLER_DESCRIPTIONS = {
@@ -3500,8 +3557,8 @@ var HANDLER_DESCRIPTIONS = {
3500
3557
  typeInto: "App-agnostic: type into the input matching a label: body { label, text, clear?, visibleOnly? }. (selector form is web-only)",
3501
3558
  readValue: "STUB \u2014 selector-based; NOT_SUPPORTED on native (no DOM). Use /control/element/:id/state to read a value.",
3502
3559
  findByText: "App-agnostic: list elements matching free-text (no press): body { text, visibleOnly? }",
3503
- getConsoleErrors: "TEST HOOK \u2014 last-N console.error/console.warn entries; ?since=<ms>&limit=<n>",
3504
- getNetworkRequests: "TEST HOOK \u2014 last-N fetch/XHR entries with status + duration; ?since=<ms>&limit=<n>",
3560
+ getConsoleErrors: "Last-N console.error/console.warn entries; ?since=<ms>&limit=<n>. `installed:false` when capture is off (no testHooks) \u2014 entries empty but schema-valid.",
3561
+ getNetworkRequests: "Last-N fetch/XHR entries with status + duration; ?since=<ms>&limit=<n>. `installed:false` when capture is off (no testHooks) \u2014 entries empty but schema-valid.",
3505
3562
  pushModal: "TEST HOOK \u2014 push a modal onto the snapshot.modalStack via the registry ModalDetector",
3506
3563
  dismissModal: "TEST HOOK \u2014 dismiss a modal by id from the snapshot.modalStack via the registry ModalDetector",
3507
3564
  keepAwake: "TEST HOOK \u2014 keep the device screen awake via the injected keepAwakeProvider: body { enabled: boolean, durationMs?: number }"
@@ -5672,7 +5729,8 @@ function useUIElement(options) {
5672
5729
  parentPath,
5673
5730
  style,
5674
5731
  stateStyles: stateStylesProp,
5675
- handlers: handlersProp
5732
+ handlers: handlersProp,
5733
+ value: valueProp
5676
5734
  } = options;
5677
5735
  const treePath = parentPath ? `${parentPath}/${id}` : id;
5678
5736
  const bridgeProps = useMemo(
@@ -5698,9 +5756,12 @@ function useUIElement(options) {
5698
5756
  });
5699
5757
  registeredRef.current = true;
5700
5758
  registeredIdRef.current = id;
5759
+ if (typeof valueProp === "string") {
5760
+ bridge.registry.updateElementState(id, { value: valueProp });
5761
+ }
5701
5762
  setRegistered(true);
5702
5763
  scheduleDeadButtonWarning(bridge, id, type);
5703
- }, [bridge, id, type, label, actions, customActions, treePath, style, stateStylesProp]);
5764
+ }, [bridge, id, type, label, actions, customActions, treePath, style, stateStylesProp, valueProp]);
5704
5765
  const unregister = useCallback(() => {
5705
5766
  if (!bridge || !registeredRef.current) return;
5706
5767
  bridge.registry.unregisterElement(registeredIdRef.current ?? id);
@@ -5830,6 +5891,13 @@ function useUIElement(options) {
5830
5891
  }
5831
5892
  };
5832
5893
  }, [bridge, registered, id, handlersProp]);
5894
+ useEffect(() => {
5895
+ if (!bridge || !registered) return;
5896
+ if (typeof valueProp !== "string") return;
5897
+ const current = bridge.registry.getElement(id)?.getState().value;
5898
+ if (current === valueProp) return;
5899
+ bridge.registry.updateElementState(id, { value: valueProp });
5900
+ }, [bridge, registered, id, valueProp]);
5833
5901
  useCallback(
5834
5902
  (props) => {
5835
5903
  propsRef.current = { ...propsRef.current, ...props };
@@ -6221,6 +6289,11 @@ var UI_BRIDGE_NATIVE_ROUTES = {
6221
6289
  path: "/ui-bridge/control/snapshot",
6222
6290
  description: "Get full bridge snapshot"
6223
6291
  },
6292
+ DISCOVER: {
6293
+ method: "GET",
6294
+ path: "/ui-bridge/control/discover",
6295
+ description: "Discover elements (alias for snapshot; web/runner parity)"
6296
+ },
6224
6297
  PAGE_HEALTH: {
6225
6298
  method: "POST",
6226
6299
  path: "/ui-bridge/control/page-health",
@@ -6348,6 +6421,18 @@ var UI_BRIDGE_NATIVE_ROUTES = {
6348
6421
  path: "/ui-bridge/control/modal/dismiss/:id",
6349
6422
  description: "Dismiss a modal by id (testHooks)"
6350
6423
  },
6424
+ // Observability — last-N ring buffers (always mounted; `installed:false`
6425
+ // when the console/network patches are off, i.e. no testHooks).
6426
+ CONSOLE_ERRORS: {
6427
+ method: "GET",
6428
+ path: "/ui-bridge/control/console-errors",
6429
+ description: "Last-N captured console.error/console.warn entries"
6430
+ },
6431
+ NETWORK_REQUESTS: {
6432
+ method: "GET",
6433
+ path: "/ui-bridge/sdk/network-requests",
6434
+ description: "Last-N captured fetch/XHR requests with status + duration"
6435
+ },
6351
6436
  // Health
6352
6437
  HEALTH: {
6353
6438
  method: "GET",
@@ -6554,6 +6639,46 @@ function decodeTextPayload(payload) {
6554
6639
  return new TextDecoder().decode(payload);
6555
6640
  }
6556
6641
 
6642
+ // src/server/bind-retry.ts
6643
+ function isAddrInUseError(err) {
6644
+ const message = err instanceof Error ? err.message : typeof err === "string" ? err : String(err);
6645
+ const code = err?.code;
6646
+ return message.includes("EADDRINUSE") || message.toLowerCase().includes("address already in use") || code === "EADDRINUSE";
6647
+ }
6648
+ var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
6649
+ async function bindWithRetry(attempt, options = {}) {
6650
+ const {
6651
+ minTotalMs = 3e4,
6652
+ initialDelayMs = 500,
6653
+ maxDelayMs = 5e3,
6654
+ backoffFactor = 2,
6655
+ logger = console,
6656
+ sleep: sleep2 = defaultSleep,
6657
+ now = Date.now
6658
+ } = options;
6659
+ const startedAt = now();
6660
+ let delay = initialDelayMs;
6661
+ let attemptIndex = 0;
6662
+ for (; ; ) {
6663
+ try {
6664
+ await attempt(attemptIndex);
6665
+ return;
6666
+ } catch (err) {
6667
+ const elapsed = now() - startedAt;
6668
+ const budgetExhausted = elapsed + delay >= minTotalMs;
6669
+ if (!isAddrInUseError(err) || budgetExhausted) {
6670
+ throw err;
6671
+ }
6672
+ logger.warn(
6673
+ `[ui-bridge-native] bind failed with EADDRINUSE (attempt ${attemptIndex + 1}); retrying in ${delay}ms (elapsed ${elapsed}ms / budget ${minTotalMs === Infinity ? "\u221E" : minTotalMs}ms)`
6674
+ );
6675
+ await sleep2(delay);
6676
+ delay = Math.min(delay * backoffFactor, maxDelayMs);
6677
+ attemptIndex += 1;
6678
+ }
6679
+ }
6680
+ }
6681
+
6557
6682
  // src/server/ws-connection.ts
6558
6683
  var WebSocketConnection = class {
6559
6684
  constructor(id, socket, onMessage, onClose) {
@@ -7120,6 +7245,6 @@ var styles = StyleSheet.create({
7120
7245
  }
7121
7246
  });
7122
7247
 
7123
- export { CloudRelayClient, DefaultNativeActionExecutor, DeviceAnnouncer, ModalDetector, NativeUIBridgeRegistry, NativeUIBridgeServer, State, ToastCapture, TransitionTo, UIBridgeInspector, UIBridgeNativeProvider, UI_BRIDGE_NATIVE_ROUTES, UndoTracker, WebSocketConnection, WebSocketEventBridge, buildTreePath, buildUpgradeResponse, captureNativeResponsiveSnapshot, captureNativeStateVariations, computeAcceptKey, createDesignHandlers, createNativeActionExecutor, createNativeElementIdentifier, createNativeServer, createServerHandlers, extractHandlerNames, findAllByPattern, findElementByIdentifier, getGlobalRegistry, getNativeElementDesignData, mapNativeStyleToExtended, matchesIdentifier, parseTreePath, resetGlobalRegistry, setGlobalRegistry, useToastRecorder, useUIBridge, useUIBridgeModal, useUIBridgeNative, useUIBridgeNativeOptional, useUIBridgeNativeRequired, useUIBridgeRequired, useUIBridgeToast, useUIComponent, useUIComponentAction, useUIElement, useUIElementWithProps };
7248
+ export { CloudRelayClient, DefaultNativeActionExecutor, DeviceAnnouncer, ModalDetector, NativeUIBridgeRegistry, NativeUIBridgeServer, State, ToastCapture, TransitionTo, UIBridgeInspector, UIBridgeNativeProvider, UI_BRIDGE_NATIVE_ROUTES, UndoTracker, WebSocketConnection, WebSocketEventBridge, bindWithRetry, buildTreePath, buildUpgradeResponse, captureNativeResponsiveSnapshot, captureNativeStateVariations, computeAcceptKey, createDesignHandlers, createNativeActionExecutor, createNativeElementIdentifier, createNativeServer, createServerHandlers, extractHandlerNames, findAllByPattern, findElementByIdentifier, getGlobalRegistry, getNativeElementDesignData, isAddrInUseError, mapNativeStyleToExtended, matchesIdentifier, parseTreePath, resetGlobalRegistry, setGlobalRegistry, useToastRecorder, useUIBridge, useUIBridgeModal, useUIBridgeNative, useUIBridgeNativeOptional, useUIBridgeNativeRequired, useUIBridgeRequired, useUIBridgeToast, useUIComponent, useUIComponentAction, useUIElement, useUIElementWithProps };
7124
7249
  //# sourceMappingURL=index.mjs.map
7125
7250
  //# sourceMappingURL=index.mjs.map