@qontinui/ui-bridge-native 0.2.1 → 0.3.1

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 (39) hide show
  1. package/dist/{useUIBridgeToast-DD13oF3k.d.ts → StateAnnotation--zm0Jfqj.d.ts} +35 -6
  2. package/dist/{useUIBridgeToast-BPIEbsEc.d.mts → StateAnnotation-BgFoVWFy.d.mts} +35 -6
  3. package/dist/control/index.d.mts +4 -4
  4. package/dist/control/index.d.ts +4 -4
  5. package/dist/core/index.d.mts +3 -3
  6. package/dist/core/index.d.ts +3 -3
  7. package/dist/core/index.js +89 -6
  8. package/dist/core/index.js.map +1 -1
  9. package/dist/core/index.mjs +89 -6
  10. package/dist/core/index.mjs.map +1 -1
  11. package/dist/design/index.d.mts +1 -1
  12. package/dist/design/index.d.ts +1 -1
  13. package/dist/{http-server-D2U1k48d.d.mts → http-server--550eD1F.d.mts} +106 -4
  14. package/dist/{http-server-ck8clUsZ.d.ts → http-server-C9NaNBEP.d.ts} +106 -4
  15. package/dist/index.d.mts +5 -5
  16. package/dist/index.d.ts +5 -5
  17. package/dist/index.js +584 -49
  18. package/dist/index.js.map +1 -1
  19. package/dist/index.mjs +584 -51
  20. package/dist/index.mjs.map +1 -1
  21. package/dist/react/index.d.mts +5 -5
  22. package/dist/react/index.d.ts +5 -5
  23. package/dist/react/index.js +584 -49
  24. package/dist/react/index.js.map +1 -1
  25. package/dist/react/index.mjs +584 -51
  26. package/dist/react/index.mjs.map +1 -1
  27. package/dist/{registry-KdUW_koV.d.mts → registry-B5JadBN9.d.mts} +49 -2
  28. package/dist/{registry-BxXaxmsT.d.ts → registry-pAUpQq2O.d.ts} +49 -2
  29. package/dist/server/index.d.mts +65 -6
  30. package/dist/server/index.d.ts +65 -6
  31. package/dist/server/index.js +438 -2
  32. package/dist/server/index.js.map +1 -1
  33. package/dist/server/index.mjs +437 -3
  34. package/dist/server/index.mjs.map +1 -1
  35. package/dist/{types-DjB0zJJH.d.mts → types-B0XA4vOF.d.mts} +1 -1
  36. package/dist/{types-BwdPNpII.d.mts → types-CGVut7mc.d.mts} +43 -1
  37. package/dist/{types-BwdPNpII.d.ts → types-CGVut7mc.d.ts} +43 -1
  38. package/dist/{types-DCEv5qb6.d.ts → types-CjEKTDYT.d.ts} +1 -1
  39. package/package.json +1 -1
package/dist/index.mjs CHANGED
@@ -1,6 +1,6 @@
1
1
  import { createContext, useRef, useState, useCallback, useEffect, useMemo, useContext } from 'react';
2
2
  import { Dimensions, StyleSheet, Platform, TouchableOpacity, Text, Modal, View, ScrollView } from 'react-native';
3
- import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3
+ import { jsx, Fragment, jsxs } from 'react/jsx-runtime';
4
4
 
5
5
  var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
6
6
  get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
@@ -55,6 +55,29 @@ var NativeUIBridgeRegistry = class {
55
55
  this.eventListeners = /* @__PURE__ */ new Map();
56
56
  this.enrichers = {};
57
57
  this.snapshotExtras = /* @__PURE__ */ new Map();
58
+ /**
59
+ * App identification metadata (appId/appName/appType/framework). Set via
60
+ * `setAppInfo` — typically wired in the server constructor from the
61
+ * `NativeUIBridgeConfig` the host passes to `UIBridgeNativeProvider`.
62
+ *
63
+ * When set, `createSnapshot` includes it on the resulting snapshot so
64
+ * agents can identify the app from snapshot bytes alone (parity with the
65
+ * `uiBridge` block on the `health` response). Snapshots created before
66
+ * this is wired simply omit the field.
67
+ */
68
+ this.appInfo = null;
69
+ /**
70
+ * Optional route provider. When set, `createSnapshot` reads
71
+ * `currentRoute`/`segments` from it whenever the caller does not pass an
72
+ * explicit `routeInfo` argument. This lets the default `getSnapshot` HTTP
73
+ * handler — which has no direct reference to the `NativeUIBridgeServer`
74
+ * instance — still emit a populated `currentRoute` field.
75
+ *
76
+ * Wired by `NativeUIBridgeServer.setRouteProvider`, which forwards the
77
+ * provider into the registry alongside the per-handler overrides it
78
+ * already installs.
79
+ */
80
+ this.routeProvider = null;
58
81
  /**
59
82
  * Sticky flag: flips `true` the first time any element is registered and
60
83
  * stays `true` even after elements are unregistered. Lets agents distinguish
@@ -65,6 +88,34 @@ var NativeUIBridgeRegistry = class {
65
88
  this.config = config;
66
89
  }
67
90
  // ============================================================================
91
+ // App Info & Route Provider
92
+ // ============================================================================
93
+ /**
94
+ * Set the app identification metadata returned alongside snapshots.
95
+ * Idempotent — re-calling with the same shape is a no-op.
96
+ * Pass `null` (or omit a previous call) to clear.
97
+ */
98
+ setAppInfo(info) {
99
+ this.appInfo = info ?? null;
100
+ }
101
+ /** Read-only accessor for the registered app info, if any. */
102
+ getAppInfo() {
103
+ return this.appInfo;
104
+ }
105
+ /**
106
+ * Set a route provider whose `getCurrentRoute`/`getSegments` are read by
107
+ * `createSnapshot` when no explicit `routeInfo` argument is supplied.
108
+ *
109
+ * Call with `null` to detach (e.g. when the host swaps providers on HMR).
110
+ */
111
+ setRouteProvider(provider) {
112
+ this.routeProvider = provider ?? null;
113
+ }
114
+ /** Read-only accessor for the registered route provider, if any. */
115
+ getRouteProvider() {
116
+ return this.routeProvider;
117
+ }
118
+ // ============================================================================
68
119
  // Snapshot Enricher Slots
69
120
  // ============================================================================
70
121
  /**
@@ -453,12 +504,41 @@ var NativeUIBridgeRegistry = class {
453
504
  // Snapshots
454
505
  // ============================================================================
455
506
  /**
456
- * Create a snapshot of the current state
507
+ * Create a snapshot of the current state.
508
+ *
509
+ * Route resolution: `routeInfo` wins when provided. Otherwise — and this
510
+ * is the path the default `getSnapshot` HTTP handler takes — we fall back
511
+ * to the registered `routeProvider`. This means callers that never wire a
512
+ * server-level override (the bare-server / cloud-relay path) still get a
513
+ * populated `currentRoute` field as long as `setRouteProvider` was called
514
+ * on the registry.
457
515
  */
458
516
  createSnapshot(routeInfo, options) {
517
+ const resolvedRoute = (() => {
518
+ if (routeInfo !== void 0) {
519
+ return {
520
+ currentRoute: routeInfo.currentRoute ?? null,
521
+ segments: routeInfo.segments
522
+ };
523
+ }
524
+ if (this.routeProvider) {
525
+ try {
526
+ return {
527
+ currentRoute: this.routeProvider.getCurrentRoute() ?? null,
528
+ segments: this.routeProvider.getSegments?.()
529
+ };
530
+ } catch (error3) {
531
+ if (this.config.verbose) {
532
+ console.warn(`[ui-bridge-native] routeProvider getCurrentRoute threw:`, error3);
533
+ }
534
+ return { currentRoute: null, segments: void 0 };
535
+ }
536
+ }
537
+ return { currentRoute: null, segments: void 0 };
538
+ })();
459
539
  let elements = options?.visibleOnly ? this.getVisibleElements() : this.getAllElements();
460
- if (options?.currentRouteOnly && routeInfo?.currentRoute) {
461
- const currentRoute = routeInfo.currentRoute;
540
+ if (options?.currentRouteOnly && resolvedRoute.currentRoute) {
541
+ const currentRoute = resolvedRoute.currentRoute;
462
542
  elements = elements.filter((e) => e.registrationRoute === currentRoute);
463
543
  }
464
544
  const snapshot = {
@@ -490,10 +570,13 @@ var NativeUIBridgeRegistry = class {
490
570
  description: w.description,
491
571
  stepCount: w.steps.length
492
572
  })),
493
- currentRoute: routeInfo?.currentRoute ?? null,
494
- segments: routeInfo?.segments,
573
+ currentRoute: resolvedRoute.currentRoute,
574
+ segments: resolvedRoute.segments,
495
575
  registration: this.getRegistrationCoverage()
496
576
  };
577
+ if (this.appInfo) {
578
+ snapshot.appInfo = this.appInfo;
579
+ }
497
580
  if (this.enrichers.modalDetector) {
498
581
  try {
499
582
  snapshot.modalStack = this.enrichers.modalDetector.getSnapshotModalContext();
@@ -522,7 +605,7 @@ var NativeUIBridgeRegistry = class {
522
605
  }
523
606
  }
524
607
  if (this.snapshotExtras.size > 0) {
525
- const ctx = { elements, currentRoute: routeInfo?.currentRoute ?? null };
608
+ const ctx = { elements, currentRoute: resolvedRoute.currentRoute };
526
609
  for (const [name, fn] of this.snapshotExtras) {
527
610
  try {
528
611
  const extra = fn(ctx);
@@ -2041,7 +2124,8 @@ function createServerHandlers(registry, executor, config) {
2041
2124
  },
2042
2125
  getSnapshot: async (ctx) => {
2043
2126
  const visibleOnly = ctx.query?.visibleOnly === "true" || ctx.body?.visibleOnly === true;
2044
- const snapshot = registry.createSnapshot(void 0, { visibleOnly });
2127
+ const currentRouteOnly = ctx.query?.currentRouteOnly === "true" || ctx.body?.currentRouteOnly === true;
2128
+ const snapshot = registry.createSnapshot(void 0, { visibleOnly, currentRouteOnly });
2045
2129
  return success2(snapshot);
2046
2130
  },
2047
2131
  // Workflows
@@ -2208,6 +2292,134 @@ function createServerHandlers(registry, executor, config) {
2208
2292
  const failedCount = results.length - succeededCount;
2209
2293
  return success2({ results, succeededCount, failedCount });
2210
2294
  },
2295
+ // Coord-based tap
2296
+ //
2297
+ // Synthesizes a press at the given screen coordinates by scanning the
2298
+ // registered elements' layout rects for a containing match. We prefer the
2299
+ // *topmost* match (smallest area = innermost child) since native UIs
2300
+ // generally render parents before children, and the user's tap should
2301
+ // logically hit the leaf-most interactive element.
2302
+ //
2303
+ // Coords are interpreted in the absolute (screen) space published by
2304
+ // `measureInWindow`; we compare against `pageX/pageY` when available and
2305
+ // fall back to the layout's relative `x/y` so this works in test fixtures
2306
+ // that don't measure (the fallback only matches single-rooted screens but
2307
+ // is good enough for the unit suite).
2308
+ tapAt: async (ctx) => {
2309
+ const body = ctx.body;
2310
+ const x = body?.x;
2311
+ const y = body?.y;
2312
+ if (typeof x !== "number" || !Number.isFinite(x)) {
2313
+ return error2('"x" must be a finite number', "INVALID_REQUEST");
2314
+ }
2315
+ if (typeof y !== "number" || !Number.isFinite(y)) {
2316
+ return error2('"y" must be a finite number', "INVALID_REQUEST");
2317
+ }
2318
+ const action = body?.action ?? "press";
2319
+ const allowedActions = [
2320
+ "press",
2321
+ "longPress",
2322
+ "doubleTap"
2323
+ ];
2324
+ if (!allowedActions.includes(action)) {
2325
+ return error2(
2326
+ `"action" must be one of: ${allowedActions.join(", ")}`,
2327
+ "INVALID_REQUEST"
2328
+ );
2329
+ }
2330
+ const candidates = [];
2331
+ for (const el of registry.getAllElements()) {
2332
+ const state = el.getState();
2333
+ const layout = state?.layout;
2334
+ if (!layout) continue;
2335
+ if (typeof layout.width !== "number" || typeof layout.height !== "number" || layout.width <= 0 || layout.height <= 0) {
2336
+ continue;
2337
+ }
2338
+ const hasPage = typeof layout.pageX === "number" && typeof layout.pageY === "number" && Number.isFinite(layout.pageX) && Number.isFinite(layout.pageY);
2339
+ const left = hasPage ? layout.pageX : layout.x;
2340
+ const top = hasPage ? layout.pageY : layout.y;
2341
+ if (x >= left && x <= left + layout.width && y >= top && y <= top + layout.height) {
2342
+ candidates.push({
2343
+ elementId: el.id,
2344
+ layout,
2345
+ area: layout.width * layout.height
2346
+ });
2347
+ }
2348
+ }
2349
+ if (candidates.length === 0) {
2350
+ return error2(
2351
+ `No element registered at (${x}, ${y})`,
2352
+ "no_element_at_coords"
2353
+ );
2354
+ }
2355
+ candidates.sort((a, b) => a.area - b.area);
2356
+ const target = candidates[0];
2357
+ const response = await executor.executeAction(target.elementId, { action });
2358
+ if (!response.success) {
2359
+ return error2(response.error || "Action failed", "ACTION_FAILED");
2360
+ }
2361
+ return success2({
2362
+ elementId: target.elementId,
2363
+ action,
2364
+ layout: {
2365
+ x: target.layout.x,
2366
+ y: target.layout.y,
2367
+ width: target.layout.width,
2368
+ height: target.layout.height,
2369
+ pageX: target.layout.pageX,
2370
+ pageY: target.layout.pageY
2371
+ }
2372
+ });
2373
+ },
2374
+ // Observability — console error / network request ring buffers.
2375
+ //
2376
+ // The buffers are owned by NativeUIBridgeServer and patched onto the
2377
+ // globals on `start()` (gated behind `features.testHooks`). When no
2378
+ // buffer is provided (no testHooks, or a stripped-down server fixture),
2379
+ // these handlers gracefully return empty arrays so callers can probe
2380
+ // without 500ing.
2381
+ getConsoleErrors: async (ctx) => {
2382
+ const buf = config?.consoleErrorBuffer;
2383
+ const since = ctx.query?.since !== void 0 ? Number(ctx.query.since) : void 0;
2384
+ const limit = ctx.query?.limit !== void 0 ? Number(ctx.query.limit) : void 0;
2385
+ if (!buf) {
2386
+ return success2({
2387
+ entries: [],
2388
+ count: 0,
2389
+ bufferSize: 0
2390
+ });
2391
+ }
2392
+ const entries = buf.entries({
2393
+ since: typeof since === "number" && Number.isFinite(since) ? since : void 0,
2394
+ limit: typeof limit === "number" && Number.isFinite(limit) ? limit : void 0
2395
+ });
2396
+ return success2({
2397
+ entries,
2398
+ count: entries.length,
2399
+ bufferSize: buf.size()
2400
+ });
2401
+ },
2402
+ getNetworkRequests: async (ctx) => {
2403
+ const buf = config?.networkRequestBuffer;
2404
+ const since = ctx.query?.since !== void 0 ? Number(ctx.query.since) : void 0;
2405
+ const limit = ctx.query?.limit !== void 0 ? Number(ctx.query.limit) : void 0;
2406
+ if (!buf) {
2407
+ return success2({
2408
+ entries: [],
2409
+ count: 0,
2410
+ bufferSize: 0
2411
+ });
2412
+ }
2413
+ const entries = buf.entries({
2414
+ since: typeof since === "number" && Number.isFinite(since) ? since : void 0,
2415
+ limit: typeof limit === "number" && Number.isFinite(limit) ? limit : void 0
2416
+ });
2417
+ return success2({
2418
+ entries,
2419
+ count: entries.length,
2420
+ bufferSize: buf.size()
2421
+ });
2422
+ },
2211
2423
  // Test hooks — modal stack manipulation
2212
2424
  //
2213
2425
  // These two endpoints exist so external automation runners can drive
@@ -2309,6 +2521,266 @@ function createServerHandlers(registry, executor, config) {
2309
2521
  };
2310
2522
  }
2311
2523
 
2524
+ // src/server/observability.ts
2525
+ var RingBuffer = class {
2526
+ constructor(capacity) {
2527
+ this.capacity = capacity;
2528
+ this.items = [];
2529
+ }
2530
+ push(entry) {
2531
+ this.items.push(entry);
2532
+ if (this.items.length > this.capacity) {
2533
+ this.items.splice(0, this.items.length - this.capacity);
2534
+ }
2535
+ }
2536
+ /**
2537
+ * Return entries optionally filtered by `since` (timestamp ms) and capped
2538
+ * at `limit`. Newest-end is preserved when `limit` would otherwise truncate.
2539
+ */
2540
+ entries(opts = {}) {
2541
+ const { since, limit } = opts;
2542
+ let filtered = this.items;
2543
+ if (typeof since === "number") {
2544
+ filtered = filtered.filter((e) => e.timestamp >= since);
2545
+ }
2546
+ if (typeof limit === "number" && limit >= 0 && filtered.length > limit) {
2547
+ filtered = filtered.slice(filtered.length - limit);
2548
+ }
2549
+ return filtered.slice();
2550
+ }
2551
+ size() {
2552
+ return this.items.length;
2553
+ }
2554
+ clear() {
2555
+ this.items.length = 0;
2556
+ }
2557
+ };
2558
+ function formatConsoleArgs(args) {
2559
+ return args.map((a) => {
2560
+ if (a instanceof Error) return a.message;
2561
+ if (typeof a === "string") return a;
2562
+ try {
2563
+ return JSON.stringify(a);
2564
+ } catch {
2565
+ return String(a);
2566
+ }
2567
+ }).join(" ");
2568
+ }
2569
+ function extractStack(args) {
2570
+ for (const a of args) {
2571
+ if (a instanceof Error && a.stack) return a.stack;
2572
+ }
2573
+ return void 0;
2574
+ }
2575
+ var ConsoleErrorBuffer = class {
2576
+ constructor(capacity = 100) {
2577
+ this.installed = false;
2578
+ this.buffer = new RingBuffer(capacity);
2579
+ }
2580
+ install() {
2581
+ if (this.installed) return;
2582
+ if (typeof console === "undefined") return;
2583
+ this.originalError = console.error;
2584
+ this.originalWarn = console.warn;
2585
+ const buffer = this.buffer;
2586
+ const origError = this.originalError;
2587
+ const origWarn = this.originalWarn;
2588
+ console.error = (...args) => {
2589
+ try {
2590
+ buffer.push({
2591
+ timestamp: Date.now(),
2592
+ level: "error",
2593
+ message: formatConsoleArgs(args),
2594
+ stack: extractStack(args)
2595
+ });
2596
+ } catch {
2597
+ }
2598
+ origError.apply(console, args);
2599
+ };
2600
+ console.warn = (...args) => {
2601
+ try {
2602
+ buffer.push({
2603
+ timestamp: Date.now(),
2604
+ level: "warn",
2605
+ message: formatConsoleArgs(args),
2606
+ stack: extractStack(args)
2607
+ });
2608
+ } catch {
2609
+ }
2610
+ origWarn.apply(console, args);
2611
+ };
2612
+ this.installed = true;
2613
+ }
2614
+ uninstall() {
2615
+ if (!this.installed) return;
2616
+ if (typeof console === "undefined") return;
2617
+ if (this.originalError) console.error = this.originalError;
2618
+ if (this.originalWarn) console.warn = this.originalWarn;
2619
+ this.originalError = void 0;
2620
+ this.originalWarn = void 0;
2621
+ this.installed = false;
2622
+ }
2623
+ entries(opts = {}) {
2624
+ return this.buffer.entries(opts);
2625
+ }
2626
+ size() {
2627
+ return this.buffer.size();
2628
+ }
2629
+ clear() {
2630
+ this.buffer.clear();
2631
+ }
2632
+ isInstalled() {
2633
+ return this.installed;
2634
+ }
2635
+ };
2636
+ function extractUrl(input) {
2637
+ if (typeof input === "string") return input;
2638
+ if (input && typeof input === "object") {
2639
+ const obj = input;
2640
+ if (typeof obj.url === "string") return obj.url;
2641
+ try {
2642
+ return String(input);
2643
+ } catch {
2644
+ return "";
2645
+ }
2646
+ }
2647
+ return "";
2648
+ }
2649
+ function extractMethod(input, init) {
2650
+ if (init && typeof init.method === "string") return init.method.toUpperCase();
2651
+ if (input && typeof input === "object") {
2652
+ const obj = input;
2653
+ if (typeof obj.method === "string") return obj.method.toUpperCase();
2654
+ }
2655
+ return "GET";
2656
+ }
2657
+ var NetworkRequestBuffer = class {
2658
+ constructor(capacity = 100) {
2659
+ this.installed = false;
2660
+ this.buffer = new RingBuffer(capacity);
2661
+ }
2662
+ install() {
2663
+ if (this.installed) return;
2664
+ const g = globalThis;
2665
+ if (typeof g.fetch === "function") {
2666
+ this.originalFetch = g.fetch;
2667
+ const buffer = this.buffer;
2668
+ const original = this.originalFetch;
2669
+ const patched = async (input, init) => {
2670
+ const start = Date.now();
2671
+ const url = extractUrl(input);
2672
+ const method = extractMethod(input, init);
2673
+ try {
2674
+ const res = await original.call(globalThis, input, init);
2675
+ buffer.push({
2676
+ timestamp: start,
2677
+ method,
2678
+ url,
2679
+ status: res.status,
2680
+ durationMs: Date.now() - start,
2681
+ ok: res.ok
2682
+ });
2683
+ return res;
2684
+ } catch (err) {
2685
+ buffer.push({
2686
+ timestamp: start,
2687
+ method,
2688
+ url,
2689
+ status: 0,
2690
+ durationMs: Date.now() - start,
2691
+ ok: false,
2692
+ error: err instanceof Error ? err.message : String(err)
2693
+ });
2694
+ throw err;
2695
+ }
2696
+ };
2697
+ g.fetch = patched;
2698
+ }
2699
+ if (typeof g.XMLHttpRequest === "function") {
2700
+ const Xhr = g.XMLHttpRequest;
2701
+ this.originalXhrOpen = Xhr.prototype.open;
2702
+ this.originalXhrSend = Xhr.prototype.send;
2703
+ const buffer = this.buffer;
2704
+ const origOpen = this.originalXhrOpen;
2705
+ Xhr.prototype.open = function(method, url, ...rest) {
2706
+ this.__uiBridgeMethod = method.toUpperCase();
2707
+ this.__uiBridgeUrl = typeof url === "string" ? url : String(url);
2708
+ return origOpen.call(
2709
+ this,
2710
+ method,
2711
+ url,
2712
+ ...rest
2713
+ );
2714
+ };
2715
+ const origSend = this.originalXhrSend;
2716
+ Xhr.prototype.send = function(body) {
2717
+ const start = Date.now();
2718
+ const method = this.__uiBridgeMethod ?? "GET";
2719
+ const url = this.__uiBridgeUrl ?? "";
2720
+ const finalize = (status, ok, error3) => {
2721
+ buffer.push({
2722
+ timestamp: start,
2723
+ method,
2724
+ url,
2725
+ status,
2726
+ durationMs: Date.now() - start,
2727
+ ok,
2728
+ error: error3
2729
+ });
2730
+ };
2731
+ const onLoadEnd = () => {
2732
+ this.removeEventListener("loadend", onLoadEnd);
2733
+ this.removeEventListener("error", onError);
2734
+ finalize(this.status, this.status >= 200 && this.status < 300);
2735
+ };
2736
+ const onError = () => {
2737
+ this.removeEventListener("loadend", onLoadEnd);
2738
+ this.removeEventListener("error", onError);
2739
+ finalize(0, false, "XMLHttpRequest error");
2740
+ };
2741
+ this.addEventListener("loadend", onLoadEnd);
2742
+ this.addEventListener("error", onError);
2743
+ return origSend.call(
2744
+ this,
2745
+ body
2746
+ );
2747
+ };
2748
+ }
2749
+ this.installed = true;
2750
+ }
2751
+ uninstall() {
2752
+ if (!this.installed) return;
2753
+ const g = globalThis;
2754
+ if (this.originalFetch) {
2755
+ g.fetch = this.originalFetch;
2756
+ this.originalFetch = void 0;
2757
+ }
2758
+ if (typeof g.XMLHttpRequest === "function") {
2759
+ if (this.originalXhrOpen) {
2760
+ g.XMLHttpRequest.prototype.open = this.originalXhrOpen;
2761
+ }
2762
+ if (this.originalXhrSend) {
2763
+ g.XMLHttpRequest.prototype.send = this.originalXhrSend;
2764
+ }
2765
+ }
2766
+ this.originalXhrOpen = void 0;
2767
+ this.originalXhrSend = void 0;
2768
+ this.installed = false;
2769
+ }
2770
+ entries(opts = {}) {
2771
+ return this.buffer.entries(opts);
2772
+ }
2773
+ size() {
2774
+ return this.buffer.size();
2775
+ }
2776
+ clear() {
2777
+ this.buffer.clear();
2778
+ }
2779
+ isInstalled() {
2780
+ return this.installed;
2781
+ }
2782
+ };
2783
+
2312
2784
  // src/server/ws-types.ts
2313
2785
  function isBatchRequest(msg) {
2314
2786
  return typeof msg === "object" && msg !== null && "batch" in msg && Array.isArray(msg.batch);
@@ -2359,6 +2831,10 @@ var WS_ROUTES = [
2359
2831
  },
2360
2832
  // Discovery
2361
2833
  { pattern: "control/find", handler: "find", httpMethods: ["POST"] },
2834
+ // Runner-parity alias for `control/find`; the runner exposes its
2835
+ // AI-powered element search at `/ui-bridge/ai/find`, so mobile mounts the
2836
+ // same handler at both paths to keep agents portable across platforms.
2837
+ { pattern: "ai/find", handler: "find", httpMethods: ["POST"] },
2362
2838
  { pattern: "control/snapshot", handler: "getSnapshot", httpMethods: ["GET"] },
2363
2839
  { pattern: "control/discover", handler: "getSnapshot" },
2364
2840
  // Workflows
@@ -2397,6 +2873,9 @@ var WS_ROUTES = [
2397
2873
  { pattern: "meta/methods", handler: "getMethods", httpMethods: ["GET"] },
2398
2874
  // AI helpers — mobile parity with the runner's `/ui-bridge/ai/*` family.
2399
2875
  { pattern: "ai/fill-form", handler: "fillForm", httpMethods: ["POST"] },
2876
+ // Coord-based tap — synthesizes a press at the given screen coords by
2877
+ // matching against registered elements' layout rects.
2878
+ { pattern: "control/tap", handler: "tapAt", httpMethods: ["POST"] },
2400
2879
  // Test hooks — gated behind `features.testHooks`. Let external runners
2401
2880
  // drive the ModalDetector over HTTP so tests can flip `snapshot.modalStack`
2402
2881
  // without mounting React components.
@@ -2411,6 +2890,20 @@ var WS_ROUTES = [
2411
2890
  handler: "dismissModal",
2412
2891
  httpMethods: ["POST"],
2413
2892
  requiresTestHooks: true
2893
+ },
2894
+ // Observability — last-N console errors + network requests. testHooks-gated
2895
+ // because patching `console.error` / `fetch` is invasive in production.
2896
+ {
2897
+ pattern: "control/console-errors",
2898
+ handler: "getConsoleErrors",
2899
+ httpMethods: ["GET"],
2900
+ requiresTestHooks: true
2901
+ },
2902
+ {
2903
+ pattern: "sdk/network-requests",
2904
+ handler: "getNetworkRequests",
2905
+ httpMethods: ["GET"],
2906
+ requiresTestHooks: true
2414
2907
  }
2415
2908
  ];
2416
2909
  var HANDLER_DESCRIPTIONS = {
@@ -2446,6 +2939,9 @@ var HANDLER_DESCRIPTIONS = {
2446
2939
  diffBaseline: "Diff current UI against a saved baseline",
2447
2940
  getMethods: "List every handler name (legacy introspection)",
2448
2941
  fillForm: "Fill multiple inputs in one call: body { fields: [{elementId, value}] } \u2014 each field dispatches `type` + `clear:true`",
2942
+ tapAt: "Synthesize a press at given screen coords by matching registered element layout rects: body { x, y, action? }",
2943
+ getConsoleErrors: "TEST HOOK \u2014 last-N console.error/console.warn entries; ?since=<ms>&limit=<n>",
2944
+ getNetworkRequests: "TEST HOOK \u2014 last-N fetch/XHR entries with status + duration; ?since=<ms>&limit=<n>",
2449
2945
  pushModal: "TEST HOOK \u2014 push a modal onto the snapshot.modalStack via the registry ModalDetector",
2450
2946
  dismissModal: "TEST HOOK \u2014 dismiss a modal by id from the snapshot.modalStack via the registry ModalDetector"
2451
2947
  };
@@ -2514,8 +3010,15 @@ var NativeUIBridgeServer = class {
2514
3010
  cors: true,
2515
3011
  ...config
2516
3012
  };
3013
+ this.consoleErrorBuffer = new ConsoleErrorBuffer();
3014
+ this.networkRequestBuffer = new NetworkRequestBuffer();
3015
+ if (this.config.appInfo) {
3016
+ this.registry.setAppInfo(this.config.appInfo);
3017
+ }
2517
3018
  this.handlers = createServerHandlers(registry, executor, {
2518
- appInfo: this.config.appInfo
3019
+ appInfo: this.config.appInfo,
3020
+ consoleErrorBuffer: this.consoleErrorBuffer,
3021
+ networkRequestBuffer: this.networkRequestBuffer
2519
3022
  });
2520
3023
  this.handlers.getMethods = async () => {
2521
3024
  const testHooks = this.config.testHooks === true;
@@ -2643,8 +3146,16 @@ var NativeUIBridgeServer = class {
2643
3146
  /**
2644
3147
  * Set a route provider for reporting the current navigation route.
2645
3148
  * When set, `control/snapshot` responses include `currentRoute` and `segments`.
3149
+ *
3150
+ * Also forwards the provider into the registry so the *default*
3151
+ * `getSnapshot` handler (and any other call site of
3152
+ * `registry.createSnapshot`) reads the route too — without this the
3153
+ * cloud-relay/bare-server path returned `currentRoute: null` even when
3154
+ * a `RouteTracker` was wired, because that path never installed the
3155
+ * server-level handler override below.
2646
3156
  */
2647
3157
  setRouteProvider(provider) {
3158
+ this.registry.setRouteProvider(provider);
2648
3159
  const baseGetElements = this.handlers.getElements;
2649
3160
  this.handlers.getElements = async (ctx) => {
2650
3161
  const currentRouteOnly = ctx.query?.currentRouteOnly === "true" || ctx.body?.currentRouteOnly === true;
@@ -2713,6 +3224,10 @@ var NativeUIBridgeServer = class {
2713
3224
  console.warn("[ui-bridge-native] See documentation for supported adapters.");
2714
3225
  return;
2715
3226
  }
3227
+ if (this.config.testHooks === true) {
3228
+ this.consoleErrorBuffer.install();
3229
+ this.networkRequestBuffer.install();
3230
+ }
2716
3231
  await this.adapter.start(this.config.serverPort, this.handleRequest.bind(this));
2717
3232
  this.running = true;
2718
3233
  console.log(`[ui-bridge-native] HTTP server started on port ${this.config.serverPort}`);
@@ -2726,6 +3241,8 @@ var NativeUIBridgeServer = class {
2726
3241
  }
2727
3242
  await this.adapter.stop();
2728
3243
  this.running = false;
3244
+ this.consoleErrorBuffer.uninstall();
3245
+ this.networkRequestBuffer.uninstall();
2729
3246
  console.log("[ui-bridge-native] HTTP server stopped");
2730
3247
  }
2731
3248
  /**
@@ -4554,56 +5071,35 @@ function useUIElement(options) {
4554
5071
  registeredIdRef.current = null;
4555
5072
  setRegistered(false);
4556
5073
  }, [bridge, id]);
5074
+ const onStateChangeRef = useRef(onStateChange);
5075
+ useEffect(() => {
5076
+ onStateChangeRef.current = onStateChange;
5077
+ }, [onStateChange]);
4557
5078
  const onLayout = useCallback(
4558
5079
  (event) => {
4559
5080
  const { x, y, width, height } = event.nativeEvent.layout;
5081
+ const writeLayout = (newLayout) => {
5082
+ setLayout(newLayout);
5083
+ if (!bridge) return;
5084
+ const newState = {
5085
+ mounted: true,
5086
+ visible: width > 0 && height > 0,
5087
+ enabled: true,
5088
+ focused: false,
5089
+ layout: newLayout
5090
+ };
5091
+ bridge.registry.updateElementState(id, newState);
5092
+ onStateChangeRef.current?.(newState);
5093
+ };
4560
5094
  if (ref.current && "measureInWindow" in ref.current) {
4561
5095
  ref.current.measureInWindow((pageX, pageY) => {
4562
- const newLayout = {
4563
- x,
4564
- y,
4565
- width,
4566
- height,
4567
- pageX,
4568
- pageY
4569
- };
4570
- setLayout(newLayout);
4571
- if (bridge && registered) {
4572
- const newState = {
4573
- mounted: true,
4574
- visible: width > 0 && height > 0,
4575
- enabled: true,
4576
- focused: false,
4577
- layout: newLayout
4578
- };
4579
- bridge.registry.updateElementState(id, newState);
4580
- onStateChange?.(newState);
4581
- }
5096
+ writeLayout({ x, y, width, height, pageX, pageY });
4582
5097
  });
4583
5098
  } else {
4584
- const newLayout = {
4585
- x,
4586
- y,
4587
- width,
4588
- height,
4589
- pageX: x,
4590
- pageY: y
4591
- };
4592
- setLayout(newLayout);
4593
- if (bridge && registered) {
4594
- const newState = {
4595
- mounted: true,
4596
- visible: width > 0 && height > 0,
4597
- enabled: true,
4598
- focused: false,
4599
- layout: newLayout
4600
- };
4601
- bridge.registry.updateElementState(id, newState);
4602
- onStateChange?.(newState);
4603
- }
5099
+ writeLayout({ x, y, width, height, pageX: x, pageY: y });
4604
5100
  }
4605
5101
  },
4606
- [bridge, registered, id, onStateChange]
5102
+ [bridge, id]
4607
5103
  );
4608
5104
  const registerRef = useRef(register);
4609
5105
  const unregisterRef = useRef(unregister);
@@ -4621,6 +5117,37 @@ function useUIElement(options) {
4621
5117
  }
4622
5118
  };
4623
5119
  }, [autoRegister, bridge]);
5120
+ useEffect(() => {
5121
+ if (!bridge || !registered) return;
5122
+ const node = ref.current;
5123
+ if (!node || typeof node.measureInWindow !== "function") return;
5124
+ let cancelled = false;
5125
+ node.measureInWindow((pageX, pageY, w, h) => {
5126
+ if (cancelled) return;
5127
+ if (w <= 0 || h <= 0) return;
5128
+ const newLayout = {
5129
+ x: pageX,
5130
+ y: pageY,
5131
+ width: w,
5132
+ height: h,
5133
+ pageX,
5134
+ pageY
5135
+ };
5136
+ setLayout(newLayout);
5137
+ const newState = {
5138
+ mounted: true,
5139
+ visible: true,
5140
+ enabled: true,
5141
+ focused: false,
5142
+ layout: newLayout
5143
+ };
5144
+ bridge.registry.updateElementState(id, newState);
5145
+ onStateChangeRef.current?.(newState);
5146
+ });
5147
+ return () => {
5148
+ cancelled = true;
5149
+ };
5150
+ }, [bridge, registered, id]);
4624
5151
  const registeredHandlerKeysRef = useRef([]);
4625
5152
  useEffect(() => {
4626
5153
  if (!bridge || !registered || !handlersProp) return;
@@ -4970,6 +5497,12 @@ function useToastRecorder() {
4970
5497
  const dismissToast = useCallback((id) => toastCapture.dismissToast(id), [toastCapture]);
4971
5498
  return { recordToast, dismissToast };
4972
5499
  }
5500
+ function State(props) {
5501
+ return /* @__PURE__ */ jsx(Fragment, { children: props.children });
5502
+ }
5503
+ function TransitionTo(props) {
5504
+ return /* @__PURE__ */ jsx(Fragment, { children: props.children });
5505
+ }
4973
5506
 
4974
5507
  // src/server/types.ts
4975
5508
  var UI_BRIDGE_NATIVE_ROUTES = {
@@ -5890,6 +6423,6 @@ var styles = StyleSheet.create({
5890
6423
  }
5891
6424
  });
5892
6425
 
5893
- export { CloudRelayClient, DefaultNativeActionExecutor, DeviceAnnouncer, ModalDetector, NativeUIBridgeRegistry, NativeUIBridgeServer, ToastCapture, 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 };
6426
+ 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 };
5894
6427
  //# sourceMappingURL=index.mjs.map
5895
6428
  //# sourceMappingURL=index.mjs.map