@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.js CHANGED
@@ -28,7 +28,7 @@ function inferActions(type) {
28
28
  case "pressable":
29
29
  return [...baseActions, "click", "press", "longPress", "doubleTap"];
30
30
  case "input":
31
- return [...baseActions, "click", "press", "type", "clear"];
31
+ return [...baseActions, "click", "press", "type", "setValue", "clear"];
32
32
  case "text":
33
33
  return [...baseActions, "click", "press", "longPress"];
34
34
  case "view":
@@ -987,9 +987,10 @@ var DefaultNativeActionExecutor = class {
987
987
  requestId: request.requestId,
988
988
  durationMs: successTime - startTime
989
989
  });
990
+ const fresh = this.registry.getElement(registered.id) ?? registered;
990
991
  return {
991
992
  success: true,
992
- elementState: registered.getState(),
993
+ elementState: fresh.getState(),
993
994
  result,
994
995
  durationMs: successTime - startTime,
995
996
  timestamp: successTime,
@@ -1042,6 +1043,8 @@ var DefaultNativeActionExecutor = class {
1042
1043
  return this.performDoubleTap(props);
1043
1044
  case "type":
1044
1045
  return this.performType(element, props, params);
1046
+ case "setValue":
1047
+ return this.performSetValue(element, props, params);
1045
1048
  case "clear":
1046
1049
  return this.performClear(element, props);
1047
1050
  case "focus":
@@ -1134,6 +1137,38 @@ var DefaultNativeActionExecutor = class {
1134
1137
  this.registry.updateElementState(element.id, { value: params.text });
1135
1138
  }
1136
1139
  }
1140
+ /**
1141
+ * Perform setValue action.
1142
+ *
1143
+ * Web/runner UI Bridge parity: `setValue` replaces the input's value
1144
+ * atomically in a single call (no per-character keystroke simulation),
1145
+ * mirroring the web bridge's `setValue` semantics. On native there is no
1146
+ * DOM value to assign, so we drive the controlled-input handler
1147
+ * (`onChangeText`, falling back to `onChange`) with the full string. The
1148
+ * registry's `state.value` is then synced so a subsequent GET on the
1149
+ * element reflects the new value.
1150
+ *
1151
+ * Unlike `type`, this does not append to existing content and ignores
1152
+ * `delay` — it is the deterministic "set the field to exactly this" action.
1153
+ */
1154
+ async performSetValue(element, props, params) {
1155
+ const next = typeof params?.text === "string" ? params.text : typeof params?.value === "string" ? params.value : void 0;
1156
+ if (typeof next !== "string") {
1157
+ throw new Error('setValue action requires a string "text" (or "value") parameter');
1158
+ }
1159
+ if (typeof props.onChangeText === "function") {
1160
+ props.onChangeText(next);
1161
+ } else if (typeof props.onChange === "function") {
1162
+ props.onChange({
1163
+ nativeEvent: { text: next }
1164
+ });
1165
+ } else {
1166
+ throw new Error("No text change handler found on element");
1167
+ }
1168
+ if (element) {
1169
+ this.registry.updateElementState(element.id, { value: next });
1170
+ }
1171
+ }
1137
1172
  /**
1138
1173
  * Perform clear action
1139
1174
  */
@@ -2126,6 +2161,13 @@ function matchElementsByText(registry, text, opts) {
2126
2161
  scored.sort((a, b) => a.rank - b.rank);
2127
2162
  return scored.map((s) => s.element);
2128
2163
  }
2164
+ function resolveElementState(element) {
2165
+ const state = element.getState();
2166
+ if (element.type === "input" && state.value === void 0 && typeof element.props?.value === "string") {
2167
+ return { ...state, value: element.props.value };
2168
+ }
2169
+ return state;
2170
+ }
2129
2171
  async function executeWorkflowStep(step, registry, executor) {
2130
2172
  const startTime = Date.now();
2131
2173
  try {
@@ -2362,7 +2404,7 @@ function createServerHandlers(registry, executor, config) {
2362
2404
  type: element.type,
2363
2405
  label: element.label,
2364
2406
  identifier: element.getIdentifier(),
2365
- state: element.getState(),
2407
+ state: resolveElementState(element),
2366
2408
  actions: element.actions,
2367
2409
  customActions: element.customActions ? Object.keys(element.customActions) : void 0,
2368
2410
  registeredHandlers: handlers.length > 0 ? handlers : void 0,
@@ -2376,18 +2418,19 @@ function createServerHandlers(registry, executor, config) {
2376
2418
  if (!element) {
2377
2419
  return error2(`Element not found: ${id}`, "ELEMENT_NOT_FOUND");
2378
2420
  }
2379
- return success2({ state: element.getState() });
2421
+ return success2({ state: resolveElementState(element) });
2380
2422
  },
2381
2423
  executeAction: async (ctx) => {
2382
- const { id } = ctx.params;
2383
- const body = ctx.body;
2384
- if (!body?.action) {
2424
+ const rawBody = ctx.body ?? {};
2425
+ const envelope = rawBody.request && typeof rawBody.request === "object" ? rawBody.request : rawBody;
2426
+ const id = ctx.params.id ?? rawBody.id ?? "";
2427
+ if (!envelope?.action) {
2385
2428
  return error2("Action is required", "INVALID_REQUEST");
2386
2429
  }
2387
2430
  const response = await executor.executeAction(id, {
2388
- action: body.action,
2389
- params: body.params,
2390
- waitOptions: body.waitOptions
2431
+ action: envelope.action,
2432
+ params: envelope.params,
2433
+ waitOptions: envelope.waitOptions
2391
2434
  });
2392
2435
  if (!response.success) {
2393
2436
  return error2(response.error || "Action failed", "ACTION_FAILED");
@@ -2899,7 +2942,8 @@ function createServerHandlers(registry, executor, config) {
2899
2942
  return success2({
2900
2943
  entries: [],
2901
2944
  count: 0,
2902
- bufferSize: 0
2945
+ bufferSize: 0,
2946
+ installed: false
2903
2947
  });
2904
2948
  }
2905
2949
  const entries = buf.entries({
@@ -2909,7 +2953,8 @@ function createServerHandlers(registry, executor, config) {
2909
2953
  return success2({
2910
2954
  entries,
2911
2955
  count: entries.length,
2912
- bufferSize: buf.size()
2956
+ bufferSize: buf.size(),
2957
+ installed: buf.isInstalled()
2913
2958
  });
2914
2959
  },
2915
2960
  getNetworkRequests: async (ctx) => {
@@ -2920,7 +2965,8 @@ function createServerHandlers(registry, executor, config) {
2920
2965
  return success2({
2921
2966
  entries: [],
2922
2967
  count: 0,
2923
- bufferSize: 0
2968
+ bufferSize: 0,
2969
+ installed: false
2924
2970
  });
2925
2971
  }
2926
2972
  const entries = buf.entries({
@@ -2930,7 +2976,8 @@ function createServerHandlers(registry, executor, config) {
2930
2976
  return success2({
2931
2977
  entries,
2932
2978
  count: entries.length,
2933
- bufferSize: buf.size()
2979
+ bufferSize: buf.size(),
2980
+ installed: buf.isInstalled()
2934
2981
  });
2935
2982
  },
2936
2983
  // Test hooks — modal stack manipulation
@@ -3022,7 +3069,7 @@ function createServerHandlers(registry, executor, config) {
3022
3069
  };
3023
3070
  if (config?.appInfo) {
3024
3071
  response.uiBridge = {
3025
- version: "0.3.0",
3072
+ version: "0.6.10",
3026
3073
  ...config.appInfo,
3027
3074
  capabilities: ["elements", "components", "actions", "design"],
3028
3075
  elementCount: stats.elements,
@@ -3349,7 +3396,10 @@ var WS_ROUTES = [
3349
3396
  // same handler at both paths to keep agents portable across platforms.
3350
3397
  { pattern: "ai/find", handler: "find", httpMethods: ["POST"] },
3351
3398
  { pattern: "control/snapshot", handler: "getSnapshot", httpMethods: ["GET"] },
3352
- { pattern: "control/discover", handler: "getSnapshot" },
3399
+ // Discovery alias — web/runner parity with `POST /control/discover` (a
3400
+ // legacy alias for snapshot/find). Mounted on HTTP (GET+POST) so mobile
3401
+ // doesn't 404 where web returns 200; delegates to the snapshot handler.
3402
+ { pattern: "control/discover", handler: "getSnapshot", httpMethods: ["GET", "POST"] },
3353
3403
  // Page health — accepts both GET and POST so the canonical skill invocation
3354
3404
  // (`POST .../control/page-health` with an empty body) and ad-hoc curls both
3355
3405
  // work without a body-shape gotcha.
@@ -3443,19 +3493,26 @@ var WS_ROUTES = [
3443
3493
  httpMethods: ["POST"],
3444
3494
  requiresTestHooks: true
3445
3495
  },
3446
- // Observability — last-N console errors + network requests. testHooks-gated
3447
- // because patching `console.error` / `fetch` is invasive in production.
3496
+ // Observability — last-N console errors + network requests.
3497
+ //
3498
+ // Always mounted (not testHooks-gated) so they return a schema-valid 200
3499
+ // on every build instead of a 404 that agents can't distinguish from a
3500
+ // missing route. The INVASIVE part — monkey-patching `console.error` /
3501
+ // `fetch` / `XMLHttpRequest` — stays gated behind `testHooks` (see
3502
+ // NativeUIBridgeServer.start → buffer.install()). When the patches aren't
3503
+ // installed the handlers report `installed: false` + empty entries, so
3504
+ // production builds expose the contract without observing real users'
3505
+ // console/network traffic. Set `features.testHooks` (typically on
3506
+ // `__DEV__`) to populate the buffers with live data.
3448
3507
  {
3449
3508
  pattern: "control/console-errors",
3450
3509
  handler: "getConsoleErrors",
3451
- httpMethods: ["GET"],
3452
- requiresTestHooks: true
3510
+ httpMethods: ["GET"]
3453
3511
  },
3454
3512
  {
3455
3513
  pattern: "sdk/network-requests",
3456
3514
  handler: "getNetworkRequests",
3457
- httpMethods: ["GET"],
3458
- requiresTestHooks: true
3515
+ httpMethods: ["GET"]
3459
3516
  }
3460
3517
  ];
3461
3518
  var HANDLER_DESCRIPTIONS = {
@@ -3502,8 +3559,8 @@ var HANDLER_DESCRIPTIONS = {
3502
3559
  typeInto: "App-agnostic: type into the input matching a label: body { label, text, clear?, visibleOnly? }. (selector form is web-only)",
3503
3560
  readValue: "STUB \u2014 selector-based; NOT_SUPPORTED on native (no DOM). Use /control/element/:id/state to read a value.",
3504
3561
  findByText: "App-agnostic: list elements matching free-text (no press): body { text, visibleOnly? }",
3505
- getConsoleErrors: "TEST HOOK \u2014 last-N console.error/console.warn entries; ?since=<ms>&limit=<n>",
3506
- getNetworkRequests: "TEST HOOK \u2014 last-N fetch/XHR entries with status + duration; ?since=<ms>&limit=<n>",
3562
+ 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.",
3563
+ 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.",
3507
3564
  pushModal: "TEST HOOK \u2014 push a modal onto the snapshot.modalStack via the registry ModalDetector",
3508
3565
  dismissModal: "TEST HOOK \u2014 dismiss a modal by id from the snapshot.modalStack via the registry ModalDetector",
3509
3566
  keepAwake: "TEST HOOK \u2014 keep the device screen awake via the injected keepAwakeProvider: body { enabled: boolean, durationMs?: number }"
@@ -5674,7 +5731,8 @@ function useUIElement(options) {
5674
5731
  parentPath,
5675
5732
  style,
5676
5733
  stateStyles: stateStylesProp,
5677
- handlers: handlersProp
5734
+ handlers: handlersProp,
5735
+ value: valueProp
5678
5736
  } = options;
5679
5737
  const treePath = parentPath ? `${parentPath}/${id}` : id;
5680
5738
  const bridgeProps = react.useMemo(
@@ -5700,9 +5758,12 @@ function useUIElement(options) {
5700
5758
  });
5701
5759
  registeredRef.current = true;
5702
5760
  registeredIdRef.current = id;
5761
+ if (typeof valueProp === "string") {
5762
+ bridge.registry.updateElementState(id, { value: valueProp });
5763
+ }
5703
5764
  setRegistered(true);
5704
5765
  scheduleDeadButtonWarning(bridge, id, type);
5705
- }, [bridge, id, type, label, actions, customActions, treePath, style, stateStylesProp]);
5766
+ }, [bridge, id, type, label, actions, customActions, treePath, style, stateStylesProp, valueProp]);
5706
5767
  const unregister = react.useCallback(() => {
5707
5768
  if (!bridge || !registeredRef.current) return;
5708
5769
  bridge.registry.unregisterElement(registeredIdRef.current ?? id);
@@ -5832,6 +5893,13 @@ function useUIElement(options) {
5832
5893
  }
5833
5894
  };
5834
5895
  }, [bridge, registered, id, handlersProp]);
5896
+ react.useEffect(() => {
5897
+ if (!bridge || !registered) return;
5898
+ if (typeof valueProp !== "string") return;
5899
+ const current = bridge.registry.getElement(id)?.getState().value;
5900
+ if (current === valueProp) return;
5901
+ bridge.registry.updateElementState(id, { value: valueProp });
5902
+ }, [bridge, registered, id, valueProp]);
5835
5903
  react.useCallback(
5836
5904
  (props) => {
5837
5905
  propsRef.current = { ...propsRef.current, ...props };
@@ -6223,6 +6291,11 @@ var UI_BRIDGE_NATIVE_ROUTES = {
6223
6291
  path: "/ui-bridge/control/snapshot",
6224
6292
  description: "Get full bridge snapshot"
6225
6293
  },
6294
+ DISCOVER: {
6295
+ method: "GET",
6296
+ path: "/ui-bridge/control/discover",
6297
+ description: "Discover elements (alias for snapshot; web/runner parity)"
6298
+ },
6226
6299
  PAGE_HEALTH: {
6227
6300
  method: "POST",
6228
6301
  path: "/ui-bridge/control/page-health",
@@ -6350,6 +6423,18 @@ var UI_BRIDGE_NATIVE_ROUTES = {
6350
6423
  path: "/ui-bridge/control/modal/dismiss/:id",
6351
6424
  description: "Dismiss a modal by id (testHooks)"
6352
6425
  },
6426
+ // Observability — last-N ring buffers (always mounted; `installed:false`
6427
+ // when the console/network patches are off, i.e. no testHooks).
6428
+ CONSOLE_ERRORS: {
6429
+ method: "GET",
6430
+ path: "/ui-bridge/control/console-errors",
6431
+ description: "Last-N captured console.error/console.warn entries"
6432
+ },
6433
+ NETWORK_REQUESTS: {
6434
+ method: "GET",
6435
+ path: "/ui-bridge/sdk/network-requests",
6436
+ description: "Last-N captured fetch/XHR requests with status + duration"
6437
+ },
6353
6438
  // Health
6354
6439
  HEALTH: {
6355
6440
  method: "GET",
@@ -6556,6 +6641,46 @@ function decodeTextPayload(payload) {
6556
6641
  return new TextDecoder().decode(payload);
6557
6642
  }
6558
6643
 
6644
+ // src/server/bind-retry.ts
6645
+ function isAddrInUseError(err) {
6646
+ const message = err instanceof Error ? err.message : typeof err === "string" ? err : String(err);
6647
+ const code = err?.code;
6648
+ return message.includes("EADDRINUSE") || message.toLowerCase().includes("address already in use") || code === "EADDRINUSE";
6649
+ }
6650
+ var defaultSleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
6651
+ async function bindWithRetry(attempt, options = {}) {
6652
+ const {
6653
+ minTotalMs = 3e4,
6654
+ initialDelayMs = 500,
6655
+ maxDelayMs = 5e3,
6656
+ backoffFactor = 2,
6657
+ logger = console,
6658
+ sleep: sleep2 = defaultSleep,
6659
+ now = Date.now
6660
+ } = options;
6661
+ const startedAt = now();
6662
+ let delay = initialDelayMs;
6663
+ let attemptIndex = 0;
6664
+ for (; ; ) {
6665
+ try {
6666
+ await attempt(attemptIndex);
6667
+ return;
6668
+ } catch (err) {
6669
+ const elapsed = now() - startedAt;
6670
+ const budgetExhausted = elapsed + delay >= minTotalMs;
6671
+ if (!isAddrInUseError(err) || budgetExhausted) {
6672
+ throw err;
6673
+ }
6674
+ logger.warn(
6675
+ `[ui-bridge-native] bind failed with EADDRINUSE (attempt ${attemptIndex + 1}); retrying in ${delay}ms (elapsed ${elapsed}ms / budget ${minTotalMs === Infinity ? "\u221E" : minTotalMs}ms)`
6676
+ );
6677
+ await sleep2(delay);
6678
+ delay = Math.min(delay * backoffFactor, maxDelayMs);
6679
+ attemptIndex += 1;
6680
+ }
6681
+ }
6682
+ }
6683
+
6559
6684
  // src/server/ws-connection.ts
6560
6685
  var WebSocketConnection = class {
6561
6686
  constructor(id, socket, onMessage, onClose) {
@@ -7137,6 +7262,7 @@ exports.UI_BRIDGE_NATIVE_ROUTES = UI_BRIDGE_NATIVE_ROUTES;
7137
7262
  exports.UndoTracker = UndoTracker;
7138
7263
  exports.WebSocketConnection = WebSocketConnection;
7139
7264
  exports.WebSocketEventBridge = WebSocketEventBridge;
7265
+ exports.bindWithRetry = bindWithRetry;
7140
7266
  exports.buildTreePath = buildTreePath;
7141
7267
  exports.buildUpgradeResponse = buildUpgradeResponse;
7142
7268
  exports.captureNativeResponsiveSnapshot = captureNativeResponsiveSnapshot;
@@ -7152,6 +7278,7 @@ exports.findAllByPattern = findAllByPattern;
7152
7278
  exports.findElementByIdentifier = findElementByIdentifier;
7153
7279
  exports.getGlobalRegistry = getGlobalRegistry;
7154
7280
  exports.getNativeElementDesignData = getNativeElementDesignData;
7281
+ exports.isAddrInUseError = isAddrInUseError;
7155
7282
  exports.mapNativeStyleToExtended = mapNativeStyleToExtended;
7156
7283
  exports.matchesIdentifier = matchesIdentifier;
7157
7284
  exports.parseTreePath = parseTreePath;