@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.
- package/dist/{useUIBridgeToast-DD13oF3k.d.ts → StateAnnotation--zm0Jfqj.d.ts} +35 -6
- package/dist/{useUIBridgeToast-BPIEbsEc.d.mts → StateAnnotation-BgFoVWFy.d.mts} +35 -6
- package/dist/control/index.d.mts +4 -4
- package/dist/control/index.d.ts +4 -4
- package/dist/core/index.d.mts +3 -3
- package/dist/core/index.d.ts +3 -3
- package/dist/core/index.js +89 -6
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +89 -6
- package/dist/core/index.mjs.map +1 -1
- package/dist/design/index.d.mts +1 -1
- package/dist/design/index.d.ts +1 -1
- package/dist/{http-server-D2U1k48d.d.mts → http-server--550eD1F.d.mts} +106 -4
- package/dist/{http-server-ck8clUsZ.d.ts → http-server-C9NaNBEP.d.ts} +106 -4
- package/dist/index.d.mts +5 -5
- package/dist/index.d.ts +5 -5
- package/dist/index.js +584 -49
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +584 -51
- package/dist/index.mjs.map +1 -1
- package/dist/react/index.d.mts +5 -5
- package/dist/react/index.d.ts +5 -5
- package/dist/react/index.js +584 -49
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +584 -51
- package/dist/react/index.mjs.map +1 -1
- package/dist/{registry-KdUW_koV.d.mts → registry-B5JadBN9.d.mts} +49 -2
- package/dist/{registry-BxXaxmsT.d.ts → registry-pAUpQq2O.d.ts} +49 -2
- package/dist/server/index.d.mts +65 -6
- package/dist/server/index.d.ts +65 -6
- package/dist/server/index.js +438 -2
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +437 -3
- package/dist/server/index.mjs.map +1 -1
- package/dist/{types-DjB0zJJH.d.mts → types-B0XA4vOF.d.mts} +1 -1
- package/dist/{types-BwdPNpII.d.mts → types-CGVut7mc.d.mts} +43 -1
- package/dist/{types-BwdPNpII.d.ts → types-CGVut7mc.d.ts} +43 -1
- package/dist/{types-DCEv5qb6.d.ts → types-CjEKTDYT.d.ts} +1 -1
- package/package.json +1 -1
package/dist/react/index.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createContext, useRef, useState, useCallback, useEffect, useMemo, useContext } from 'react';
|
|
2
2
|
import { StyleSheet, Platform } from 'react-native';
|
|
3
|
-
import { jsx } from 'react/jsx-runtime';
|
|
3
|
+
import { jsx, Fragment } 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 &&
|
|
461
|
-
const 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:
|
|
494
|
-
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:
|
|
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);
|
|
@@ -1989,7 +2072,8 @@ function createServerHandlers(registry, executor, config) {
|
|
|
1989
2072
|
},
|
|
1990
2073
|
getSnapshot: async (ctx) => {
|
|
1991
2074
|
const visibleOnly = ctx.query?.visibleOnly === "true" || ctx.body?.visibleOnly === true;
|
|
1992
|
-
const
|
|
2075
|
+
const currentRouteOnly = ctx.query?.currentRouteOnly === "true" || ctx.body?.currentRouteOnly === true;
|
|
2076
|
+
const snapshot = registry.createSnapshot(void 0, { visibleOnly, currentRouteOnly });
|
|
1993
2077
|
return success2(snapshot);
|
|
1994
2078
|
},
|
|
1995
2079
|
// Workflows
|
|
@@ -2156,6 +2240,134 @@ function createServerHandlers(registry, executor, config) {
|
|
|
2156
2240
|
const failedCount = results.length - succeededCount;
|
|
2157
2241
|
return success2({ results, succeededCount, failedCount });
|
|
2158
2242
|
},
|
|
2243
|
+
// Coord-based tap
|
|
2244
|
+
//
|
|
2245
|
+
// Synthesizes a press at the given screen coordinates by scanning the
|
|
2246
|
+
// registered elements' layout rects for a containing match. We prefer the
|
|
2247
|
+
// *topmost* match (smallest area = innermost child) since native UIs
|
|
2248
|
+
// generally render parents before children, and the user's tap should
|
|
2249
|
+
// logically hit the leaf-most interactive element.
|
|
2250
|
+
//
|
|
2251
|
+
// Coords are interpreted in the absolute (screen) space published by
|
|
2252
|
+
// `measureInWindow`; we compare against `pageX/pageY` when available and
|
|
2253
|
+
// fall back to the layout's relative `x/y` so this works in test fixtures
|
|
2254
|
+
// that don't measure (the fallback only matches single-rooted screens but
|
|
2255
|
+
// is good enough for the unit suite).
|
|
2256
|
+
tapAt: async (ctx) => {
|
|
2257
|
+
const body = ctx.body;
|
|
2258
|
+
const x = body?.x;
|
|
2259
|
+
const y = body?.y;
|
|
2260
|
+
if (typeof x !== "number" || !Number.isFinite(x)) {
|
|
2261
|
+
return error2('"x" must be a finite number', "INVALID_REQUEST");
|
|
2262
|
+
}
|
|
2263
|
+
if (typeof y !== "number" || !Number.isFinite(y)) {
|
|
2264
|
+
return error2('"y" must be a finite number', "INVALID_REQUEST");
|
|
2265
|
+
}
|
|
2266
|
+
const action = body?.action ?? "press";
|
|
2267
|
+
const allowedActions = [
|
|
2268
|
+
"press",
|
|
2269
|
+
"longPress",
|
|
2270
|
+
"doubleTap"
|
|
2271
|
+
];
|
|
2272
|
+
if (!allowedActions.includes(action)) {
|
|
2273
|
+
return error2(
|
|
2274
|
+
`"action" must be one of: ${allowedActions.join(", ")}`,
|
|
2275
|
+
"INVALID_REQUEST"
|
|
2276
|
+
);
|
|
2277
|
+
}
|
|
2278
|
+
const candidates = [];
|
|
2279
|
+
for (const el of registry.getAllElements()) {
|
|
2280
|
+
const state = el.getState();
|
|
2281
|
+
const layout = state?.layout;
|
|
2282
|
+
if (!layout) continue;
|
|
2283
|
+
if (typeof layout.width !== "number" || typeof layout.height !== "number" || layout.width <= 0 || layout.height <= 0) {
|
|
2284
|
+
continue;
|
|
2285
|
+
}
|
|
2286
|
+
const hasPage = typeof layout.pageX === "number" && typeof layout.pageY === "number" && Number.isFinite(layout.pageX) && Number.isFinite(layout.pageY);
|
|
2287
|
+
const left = hasPage ? layout.pageX : layout.x;
|
|
2288
|
+
const top = hasPage ? layout.pageY : layout.y;
|
|
2289
|
+
if (x >= left && x <= left + layout.width && y >= top && y <= top + layout.height) {
|
|
2290
|
+
candidates.push({
|
|
2291
|
+
elementId: el.id,
|
|
2292
|
+
layout,
|
|
2293
|
+
area: layout.width * layout.height
|
|
2294
|
+
});
|
|
2295
|
+
}
|
|
2296
|
+
}
|
|
2297
|
+
if (candidates.length === 0) {
|
|
2298
|
+
return error2(
|
|
2299
|
+
`No element registered at (${x}, ${y})`,
|
|
2300
|
+
"no_element_at_coords"
|
|
2301
|
+
);
|
|
2302
|
+
}
|
|
2303
|
+
candidates.sort((a, b) => a.area - b.area);
|
|
2304
|
+
const target = candidates[0];
|
|
2305
|
+
const response = await executor.executeAction(target.elementId, { action });
|
|
2306
|
+
if (!response.success) {
|
|
2307
|
+
return error2(response.error || "Action failed", "ACTION_FAILED");
|
|
2308
|
+
}
|
|
2309
|
+
return success2({
|
|
2310
|
+
elementId: target.elementId,
|
|
2311
|
+
action,
|
|
2312
|
+
layout: {
|
|
2313
|
+
x: target.layout.x,
|
|
2314
|
+
y: target.layout.y,
|
|
2315
|
+
width: target.layout.width,
|
|
2316
|
+
height: target.layout.height,
|
|
2317
|
+
pageX: target.layout.pageX,
|
|
2318
|
+
pageY: target.layout.pageY
|
|
2319
|
+
}
|
|
2320
|
+
});
|
|
2321
|
+
},
|
|
2322
|
+
// Observability — console error / network request ring buffers.
|
|
2323
|
+
//
|
|
2324
|
+
// The buffers are owned by NativeUIBridgeServer and patched onto the
|
|
2325
|
+
// globals on `start()` (gated behind `features.testHooks`). When no
|
|
2326
|
+
// buffer is provided (no testHooks, or a stripped-down server fixture),
|
|
2327
|
+
// these handlers gracefully return empty arrays so callers can probe
|
|
2328
|
+
// without 500ing.
|
|
2329
|
+
getConsoleErrors: async (ctx) => {
|
|
2330
|
+
const buf = config?.consoleErrorBuffer;
|
|
2331
|
+
const since = ctx.query?.since !== void 0 ? Number(ctx.query.since) : void 0;
|
|
2332
|
+
const limit = ctx.query?.limit !== void 0 ? Number(ctx.query.limit) : void 0;
|
|
2333
|
+
if (!buf) {
|
|
2334
|
+
return success2({
|
|
2335
|
+
entries: [],
|
|
2336
|
+
count: 0,
|
|
2337
|
+
bufferSize: 0
|
|
2338
|
+
});
|
|
2339
|
+
}
|
|
2340
|
+
const entries = buf.entries({
|
|
2341
|
+
since: typeof since === "number" && Number.isFinite(since) ? since : void 0,
|
|
2342
|
+
limit: typeof limit === "number" && Number.isFinite(limit) ? limit : void 0
|
|
2343
|
+
});
|
|
2344
|
+
return success2({
|
|
2345
|
+
entries,
|
|
2346
|
+
count: entries.length,
|
|
2347
|
+
bufferSize: buf.size()
|
|
2348
|
+
});
|
|
2349
|
+
},
|
|
2350
|
+
getNetworkRequests: async (ctx) => {
|
|
2351
|
+
const buf = config?.networkRequestBuffer;
|
|
2352
|
+
const since = ctx.query?.since !== void 0 ? Number(ctx.query.since) : void 0;
|
|
2353
|
+
const limit = ctx.query?.limit !== void 0 ? Number(ctx.query.limit) : void 0;
|
|
2354
|
+
if (!buf) {
|
|
2355
|
+
return success2({
|
|
2356
|
+
entries: [],
|
|
2357
|
+
count: 0,
|
|
2358
|
+
bufferSize: 0
|
|
2359
|
+
});
|
|
2360
|
+
}
|
|
2361
|
+
const entries = buf.entries({
|
|
2362
|
+
since: typeof since === "number" && Number.isFinite(since) ? since : void 0,
|
|
2363
|
+
limit: typeof limit === "number" && Number.isFinite(limit) ? limit : void 0
|
|
2364
|
+
});
|
|
2365
|
+
return success2({
|
|
2366
|
+
entries,
|
|
2367
|
+
count: entries.length,
|
|
2368
|
+
bufferSize: buf.size()
|
|
2369
|
+
});
|
|
2370
|
+
},
|
|
2159
2371
|
// Test hooks — modal stack manipulation
|
|
2160
2372
|
//
|
|
2161
2373
|
// These two endpoints exist so external automation runners can drive
|
|
@@ -2257,6 +2469,266 @@ function createServerHandlers(registry, executor, config) {
|
|
|
2257
2469
|
};
|
|
2258
2470
|
}
|
|
2259
2471
|
|
|
2472
|
+
// src/server/observability.ts
|
|
2473
|
+
var RingBuffer = class {
|
|
2474
|
+
constructor(capacity) {
|
|
2475
|
+
this.capacity = capacity;
|
|
2476
|
+
this.items = [];
|
|
2477
|
+
}
|
|
2478
|
+
push(entry) {
|
|
2479
|
+
this.items.push(entry);
|
|
2480
|
+
if (this.items.length > this.capacity) {
|
|
2481
|
+
this.items.splice(0, this.items.length - this.capacity);
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
/**
|
|
2485
|
+
* Return entries optionally filtered by `since` (timestamp ms) and capped
|
|
2486
|
+
* at `limit`. Newest-end is preserved when `limit` would otherwise truncate.
|
|
2487
|
+
*/
|
|
2488
|
+
entries(opts = {}) {
|
|
2489
|
+
const { since, limit } = opts;
|
|
2490
|
+
let filtered = this.items;
|
|
2491
|
+
if (typeof since === "number") {
|
|
2492
|
+
filtered = filtered.filter((e) => e.timestamp >= since);
|
|
2493
|
+
}
|
|
2494
|
+
if (typeof limit === "number" && limit >= 0 && filtered.length > limit) {
|
|
2495
|
+
filtered = filtered.slice(filtered.length - limit);
|
|
2496
|
+
}
|
|
2497
|
+
return filtered.slice();
|
|
2498
|
+
}
|
|
2499
|
+
size() {
|
|
2500
|
+
return this.items.length;
|
|
2501
|
+
}
|
|
2502
|
+
clear() {
|
|
2503
|
+
this.items.length = 0;
|
|
2504
|
+
}
|
|
2505
|
+
};
|
|
2506
|
+
function formatConsoleArgs(args) {
|
|
2507
|
+
return args.map((a) => {
|
|
2508
|
+
if (a instanceof Error) return a.message;
|
|
2509
|
+
if (typeof a === "string") return a;
|
|
2510
|
+
try {
|
|
2511
|
+
return JSON.stringify(a);
|
|
2512
|
+
} catch {
|
|
2513
|
+
return String(a);
|
|
2514
|
+
}
|
|
2515
|
+
}).join(" ");
|
|
2516
|
+
}
|
|
2517
|
+
function extractStack(args) {
|
|
2518
|
+
for (const a of args) {
|
|
2519
|
+
if (a instanceof Error && a.stack) return a.stack;
|
|
2520
|
+
}
|
|
2521
|
+
return void 0;
|
|
2522
|
+
}
|
|
2523
|
+
var ConsoleErrorBuffer = class {
|
|
2524
|
+
constructor(capacity = 100) {
|
|
2525
|
+
this.installed = false;
|
|
2526
|
+
this.buffer = new RingBuffer(capacity);
|
|
2527
|
+
}
|
|
2528
|
+
install() {
|
|
2529
|
+
if (this.installed) return;
|
|
2530
|
+
if (typeof console === "undefined") return;
|
|
2531
|
+
this.originalError = console.error;
|
|
2532
|
+
this.originalWarn = console.warn;
|
|
2533
|
+
const buffer = this.buffer;
|
|
2534
|
+
const origError = this.originalError;
|
|
2535
|
+
const origWarn = this.originalWarn;
|
|
2536
|
+
console.error = (...args) => {
|
|
2537
|
+
try {
|
|
2538
|
+
buffer.push({
|
|
2539
|
+
timestamp: Date.now(),
|
|
2540
|
+
level: "error",
|
|
2541
|
+
message: formatConsoleArgs(args),
|
|
2542
|
+
stack: extractStack(args)
|
|
2543
|
+
});
|
|
2544
|
+
} catch {
|
|
2545
|
+
}
|
|
2546
|
+
origError.apply(console, args);
|
|
2547
|
+
};
|
|
2548
|
+
console.warn = (...args) => {
|
|
2549
|
+
try {
|
|
2550
|
+
buffer.push({
|
|
2551
|
+
timestamp: Date.now(),
|
|
2552
|
+
level: "warn",
|
|
2553
|
+
message: formatConsoleArgs(args),
|
|
2554
|
+
stack: extractStack(args)
|
|
2555
|
+
});
|
|
2556
|
+
} catch {
|
|
2557
|
+
}
|
|
2558
|
+
origWarn.apply(console, args);
|
|
2559
|
+
};
|
|
2560
|
+
this.installed = true;
|
|
2561
|
+
}
|
|
2562
|
+
uninstall() {
|
|
2563
|
+
if (!this.installed) return;
|
|
2564
|
+
if (typeof console === "undefined") return;
|
|
2565
|
+
if (this.originalError) console.error = this.originalError;
|
|
2566
|
+
if (this.originalWarn) console.warn = this.originalWarn;
|
|
2567
|
+
this.originalError = void 0;
|
|
2568
|
+
this.originalWarn = void 0;
|
|
2569
|
+
this.installed = false;
|
|
2570
|
+
}
|
|
2571
|
+
entries(opts = {}) {
|
|
2572
|
+
return this.buffer.entries(opts);
|
|
2573
|
+
}
|
|
2574
|
+
size() {
|
|
2575
|
+
return this.buffer.size();
|
|
2576
|
+
}
|
|
2577
|
+
clear() {
|
|
2578
|
+
this.buffer.clear();
|
|
2579
|
+
}
|
|
2580
|
+
isInstalled() {
|
|
2581
|
+
return this.installed;
|
|
2582
|
+
}
|
|
2583
|
+
};
|
|
2584
|
+
function extractUrl(input) {
|
|
2585
|
+
if (typeof input === "string") return input;
|
|
2586
|
+
if (input && typeof input === "object") {
|
|
2587
|
+
const obj = input;
|
|
2588
|
+
if (typeof obj.url === "string") return obj.url;
|
|
2589
|
+
try {
|
|
2590
|
+
return String(input);
|
|
2591
|
+
} catch {
|
|
2592
|
+
return "";
|
|
2593
|
+
}
|
|
2594
|
+
}
|
|
2595
|
+
return "";
|
|
2596
|
+
}
|
|
2597
|
+
function extractMethod(input, init) {
|
|
2598
|
+
if (init && typeof init.method === "string") return init.method.toUpperCase();
|
|
2599
|
+
if (input && typeof input === "object") {
|
|
2600
|
+
const obj = input;
|
|
2601
|
+
if (typeof obj.method === "string") return obj.method.toUpperCase();
|
|
2602
|
+
}
|
|
2603
|
+
return "GET";
|
|
2604
|
+
}
|
|
2605
|
+
var NetworkRequestBuffer = class {
|
|
2606
|
+
constructor(capacity = 100) {
|
|
2607
|
+
this.installed = false;
|
|
2608
|
+
this.buffer = new RingBuffer(capacity);
|
|
2609
|
+
}
|
|
2610
|
+
install() {
|
|
2611
|
+
if (this.installed) return;
|
|
2612
|
+
const g = globalThis;
|
|
2613
|
+
if (typeof g.fetch === "function") {
|
|
2614
|
+
this.originalFetch = g.fetch;
|
|
2615
|
+
const buffer = this.buffer;
|
|
2616
|
+
const original = this.originalFetch;
|
|
2617
|
+
const patched = async (input, init) => {
|
|
2618
|
+
const start = Date.now();
|
|
2619
|
+
const url = extractUrl(input);
|
|
2620
|
+
const method = extractMethod(input, init);
|
|
2621
|
+
try {
|
|
2622
|
+
const res = await original.call(globalThis, input, init);
|
|
2623
|
+
buffer.push({
|
|
2624
|
+
timestamp: start,
|
|
2625
|
+
method,
|
|
2626
|
+
url,
|
|
2627
|
+
status: res.status,
|
|
2628
|
+
durationMs: Date.now() - start,
|
|
2629
|
+
ok: res.ok
|
|
2630
|
+
});
|
|
2631
|
+
return res;
|
|
2632
|
+
} catch (err) {
|
|
2633
|
+
buffer.push({
|
|
2634
|
+
timestamp: start,
|
|
2635
|
+
method,
|
|
2636
|
+
url,
|
|
2637
|
+
status: 0,
|
|
2638
|
+
durationMs: Date.now() - start,
|
|
2639
|
+
ok: false,
|
|
2640
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2641
|
+
});
|
|
2642
|
+
throw err;
|
|
2643
|
+
}
|
|
2644
|
+
};
|
|
2645
|
+
g.fetch = patched;
|
|
2646
|
+
}
|
|
2647
|
+
if (typeof g.XMLHttpRequest === "function") {
|
|
2648
|
+
const Xhr = g.XMLHttpRequest;
|
|
2649
|
+
this.originalXhrOpen = Xhr.prototype.open;
|
|
2650
|
+
this.originalXhrSend = Xhr.prototype.send;
|
|
2651
|
+
const buffer = this.buffer;
|
|
2652
|
+
const origOpen = this.originalXhrOpen;
|
|
2653
|
+
Xhr.prototype.open = function(method, url, ...rest) {
|
|
2654
|
+
this.__uiBridgeMethod = method.toUpperCase();
|
|
2655
|
+
this.__uiBridgeUrl = typeof url === "string" ? url : String(url);
|
|
2656
|
+
return origOpen.call(
|
|
2657
|
+
this,
|
|
2658
|
+
method,
|
|
2659
|
+
url,
|
|
2660
|
+
...rest
|
|
2661
|
+
);
|
|
2662
|
+
};
|
|
2663
|
+
const origSend = this.originalXhrSend;
|
|
2664
|
+
Xhr.prototype.send = function(body) {
|
|
2665
|
+
const start = Date.now();
|
|
2666
|
+
const method = this.__uiBridgeMethod ?? "GET";
|
|
2667
|
+
const url = this.__uiBridgeUrl ?? "";
|
|
2668
|
+
const finalize = (status, ok, error3) => {
|
|
2669
|
+
buffer.push({
|
|
2670
|
+
timestamp: start,
|
|
2671
|
+
method,
|
|
2672
|
+
url,
|
|
2673
|
+
status,
|
|
2674
|
+
durationMs: Date.now() - start,
|
|
2675
|
+
ok,
|
|
2676
|
+
error: error3
|
|
2677
|
+
});
|
|
2678
|
+
};
|
|
2679
|
+
const onLoadEnd = () => {
|
|
2680
|
+
this.removeEventListener("loadend", onLoadEnd);
|
|
2681
|
+
this.removeEventListener("error", onError);
|
|
2682
|
+
finalize(this.status, this.status >= 200 && this.status < 300);
|
|
2683
|
+
};
|
|
2684
|
+
const onError = () => {
|
|
2685
|
+
this.removeEventListener("loadend", onLoadEnd);
|
|
2686
|
+
this.removeEventListener("error", onError);
|
|
2687
|
+
finalize(0, false, "XMLHttpRequest error");
|
|
2688
|
+
};
|
|
2689
|
+
this.addEventListener("loadend", onLoadEnd);
|
|
2690
|
+
this.addEventListener("error", onError);
|
|
2691
|
+
return origSend.call(
|
|
2692
|
+
this,
|
|
2693
|
+
body
|
|
2694
|
+
);
|
|
2695
|
+
};
|
|
2696
|
+
}
|
|
2697
|
+
this.installed = true;
|
|
2698
|
+
}
|
|
2699
|
+
uninstall() {
|
|
2700
|
+
if (!this.installed) return;
|
|
2701
|
+
const g = globalThis;
|
|
2702
|
+
if (this.originalFetch) {
|
|
2703
|
+
g.fetch = this.originalFetch;
|
|
2704
|
+
this.originalFetch = void 0;
|
|
2705
|
+
}
|
|
2706
|
+
if (typeof g.XMLHttpRequest === "function") {
|
|
2707
|
+
if (this.originalXhrOpen) {
|
|
2708
|
+
g.XMLHttpRequest.prototype.open = this.originalXhrOpen;
|
|
2709
|
+
}
|
|
2710
|
+
if (this.originalXhrSend) {
|
|
2711
|
+
g.XMLHttpRequest.prototype.send = this.originalXhrSend;
|
|
2712
|
+
}
|
|
2713
|
+
}
|
|
2714
|
+
this.originalXhrOpen = void 0;
|
|
2715
|
+
this.originalXhrSend = void 0;
|
|
2716
|
+
this.installed = false;
|
|
2717
|
+
}
|
|
2718
|
+
entries(opts = {}) {
|
|
2719
|
+
return this.buffer.entries(opts);
|
|
2720
|
+
}
|
|
2721
|
+
size() {
|
|
2722
|
+
return this.buffer.size();
|
|
2723
|
+
}
|
|
2724
|
+
clear() {
|
|
2725
|
+
this.buffer.clear();
|
|
2726
|
+
}
|
|
2727
|
+
isInstalled() {
|
|
2728
|
+
return this.installed;
|
|
2729
|
+
}
|
|
2730
|
+
};
|
|
2731
|
+
|
|
2260
2732
|
// src/server/ws-types.ts
|
|
2261
2733
|
function isBatchRequest(msg) {
|
|
2262
2734
|
return typeof msg === "object" && msg !== null && "batch" in msg && Array.isArray(msg.batch);
|
|
@@ -2307,6 +2779,10 @@ var WS_ROUTES = [
|
|
|
2307
2779
|
},
|
|
2308
2780
|
// Discovery
|
|
2309
2781
|
{ pattern: "control/find", handler: "find", httpMethods: ["POST"] },
|
|
2782
|
+
// Runner-parity alias for `control/find`; the runner exposes its
|
|
2783
|
+
// AI-powered element search at `/ui-bridge/ai/find`, so mobile mounts the
|
|
2784
|
+
// same handler at both paths to keep agents portable across platforms.
|
|
2785
|
+
{ pattern: "ai/find", handler: "find", httpMethods: ["POST"] },
|
|
2310
2786
|
{ pattern: "control/snapshot", handler: "getSnapshot", httpMethods: ["GET"] },
|
|
2311
2787
|
{ pattern: "control/discover", handler: "getSnapshot" },
|
|
2312
2788
|
// Workflows
|
|
@@ -2345,6 +2821,9 @@ var WS_ROUTES = [
|
|
|
2345
2821
|
{ pattern: "meta/methods", handler: "getMethods", httpMethods: ["GET"] },
|
|
2346
2822
|
// AI helpers — mobile parity with the runner's `/ui-bridge/ai/*` family.
|
|
2347
2823
|
{ pattern: "ai/fill-form", handler: "fillForm", httpMethods: ["POST"] },
|
|
2824
|
+
// Coord-based tap — synthesizes a press at the given screen coords by
|
|
2825
|
+
// matching against registered elements' layout rects.
|
|
2826
|
+
{ pattern: "control/tap", handler: "tapAt", httpMethods: ["POST"] },
|
|
2348
2827
|
// Test hooks — gated behind `features.testHooks`. Let external runners
|
|
2349
2828
|
// drive the ModalDetector over HTTP so tests can flip `snapshot.modalStack`
|
|
2350
2829
|
// without mounting React components.
|
|
@@ -2359,6 +2838,20 @@ var WS_ROUTES = [
|
|
|
2359
2838
|
handler: "dismissModal",
|
|
2360
2839
|
httpMethods: ["POST"],
|
|
2361
2840
|
requiresTestHooks: true
|
|
2841
|
+
},
|
|
2842
|
+
// Observability — last-N console errors + network requests. testHooks-gated
|
|
2843
|
+
// because patching `console.error` / `fetch` is invasive in production.
|
|
2844
|
+
{
|
|
2845
|
+
pattern: "control/console-errors",
|
|
2846
|
+
handler: "getConsoleErrors",
|
|
2847
|
+
httpMethods: ["GET"],
|
|
2848
|
+
requiresTestHooks: true
|
|
2849
|
+
},
|
|
2850
|
+
{
|
|
2851
|
+
pattern: "sdk/network-requests",
|
|
2852
|
+
handler: "getNetworkRequests",
|
|
2853
|
+
httpMethods: ["GET"],
|
|
2854
|
+
requiresTestHooks: true
|
|
2362
2855
|
}
|
|
2363
2856
|
];
|
|
2364
2857
|
var HANDLER_DESCRIPTIONS = {
|
|
@@ -2394,6 +2887,9 @@ var HANDLER_DESCRIPTIONS = {
|
|
|
2394
2887
|
diffBaseline: "Diff current UI against a saved baseline",
|
|
2395
2888
|
getMethods: "List every handler name (legacy introspection)",
|
|
2396
2889
|
fillForm: "Fill multiple inputs in one call: body { fields: [{elementId, value}] } \u2014 each field dispatches `type` + `clear:true`",
|
|
2890
|
+
tapAt: "Synthesize a press at given screen coords by matching registered element layout rects: body { x, y, action? }",
|
|
2891
|
+
getConsoleErrors: "TEST HOOK \u2014 last-N console.error/console.warn entries; ?since=<ms>&limit=<n>",
|
|
2892
|
+
getNetworkRequests: "TEST HOOK \u2014 last-N fetch/XHR entries with status + duration; ?since=<ms>&limit=<n>",
|
|
2397
2893
|
pushModal: "TEST HOOK \u2014 push a modal onto the snapshot.modalStack via the registry ModalDetector",
|
|
2398
2894
|
dismissModal: "TEST HOOK \u2014 dismiss a modal by id from the snapshot.modalStack via the registry ModalDetector"
|
|
2399
2895
|
};
|
|
@@ -2462,8 +2958,15 @@ var NativeUIBridgeServer = class {
|
|
|
2462
2958
|
cors: true,
|
|
2463
2959
|
...config
|
|
2464
2960
|
};
|
|
2961
|
+
this.consoleErrorBuffer = new ConsoleErrorBuffer();
|
|
2962
|
+
this.networkRequestBuffer = new NetworkRequestBuffer();
|
|
2963
|
+
if (this.config.appInfo) {
|
|
2964
|
+
this.registry.setAppInfo(this.config.appInfo);
|
|
2965
|
+
}
|
|
2465
2966
|
this.handlers = createServerHandlers(registry, executor, {
|
|
2466
|
-
appInfo: this.config.appInfo
|
|
2967
|
+
appInfo: this.config.appInfo,
|
|
2968
|
+
consoleErrorBuffer: this.consoleErrorBuffer,
|
|
2969
|
+
networkRequestBuffer: this.networkRequestBuffer
|
|
2467
2970
|
});
|
|
2468
2971
|
this.handlers.getMethods = async () => {
|
|
2469
2972
|
const testHooks = this.config.testHooks === true;
|
|
@@ -2591,8 +3094,16 @@ var NativeUIBridgeServer = class {
|
|
|
2591
3094
|
/**
|
|
2592
3095
|
* Set a route provider for reporting the current navigation route.
|
|
2593
3096
|
* When set, `control/snapshot` responses include `currentRoute` and `segments`.
|
|
3097
|
+
*
|
|
3098
|
+
* Also forwards the provider into the registry so the *default*
|
|
3099
|
+
* `getSnapshot` handler (and any other call site of
|
|
3100
|
+
* `registry.createSnapshot`) reads the route too — without this the
|
|
3101
|
+
* cloud-relay/bare-server path returned `currentRoute: null` even when
|
|
3102
|
+
* a `RouteTracker` was wired, because that path never installed the
|
|
3103
|
+
* server-level handler override below.
|
|
2594
3104
|
*/
|
|
2595
3105
|
setRouteProvider(provider) {
|
|
3106
|
+
this.registry.setRouteProvider(provider);
|
|
2596
3107
|
const baseGetElements = this.handlers.getElements;
|
|
2597
3108
|
this.handlers.getElements = async (ctx) => {
|
|
2598
3109
|
const currentRouteOnly = ctx.query?.currentRouteOnly === "true" || ctx.body?.currentRouteOnly === true;
|
|
@@ -2661,6 +3172,10 @@ var NativeUIBridgeServer = class {
|
|
|
2661
3172
|
console.warn("[ui-bridge-native] See documentation for supported adapters.");
|
|
2662
3173
|
return;
|
|
2663
3174
|
}
|
|
3175
|
+
if (this.config.testHooks === true) {
|
|
3176
|
+
this.consoleErrorBuffer.install();
|
|
3177
|
+
this.networkRequestBuffer.install();
|
|
3178
|
+
}
|
|
2664
3179
|
await this.adapter.start(this.config.serverPort, this.handleRequest.bind(this));
|
|
2665
3180
|
this.running = true;
|
|
2666
3181
|
console.log(`[ui-bridge-native] HTTP server started on port ${this.config.serverPort}`);
|
|
@@ -2674,6 +3189,8 @@ var NativeUIBridgeServer = class {
|
|
|
2674
3189
|
}
|
|
2675
3190
|
await this.adapter.stop();
|
|
2676
3191
|
this.running = false;
|
|
3192
|
+
this.consoleErrorBuffer.uninstall();
|
|
3193
|
+
this.networkRequestBuffer.uninstall();
|
|
2677
3194
|
console.log("[ui-bridge-native] HTTP server stopped");
|
|
2678
3195
|
}
|
|
2679
3196
|
/**
|
|
@@ -4502,56 +5019,35 @@ function useUIElement(options) {
|
|
|
4502
5019
|
registeredIdRef.current = null;
|
|
4503
5020
|
setRegistered(false);
|
|
4504
5021
|
}, [bridge, id]);
|
|
5022
|
+
const onStateChangeRef = useRef(onStateChange);
|
|
5023
|
+
useEffect(() => {
|
|
5024
|
+
onStateChangeRef.current = onStateChange;
|
|
5025
|
+
}, [onStateChange]);
|
|
4505
5026
|
const onLayout = useCallback(
|
|
4506
5027
|
(event) => {
|
|
4507
5028
|
const { x, y, width, height } = event.nativeEvent.layout;
|
|
5029
|
+
const writeLayout = (newLayout) => {
|
|
5030
|
+
setLayout(newLayout);
|
|
5031
|
+
if (!bridge) return;
|
|
5032
|
+
const newState = {
|
|
5033
|
+
mounted: true,
|
|
5034
|
+
visible: width > 0 && height > 0,
|
|
5035
|
+
enabled: true,
|
|
5036
|
+
focused: false,
|
|
5037
|
+
layout: newLayout
|
|
5038
|
+
};
|
|
5039
|
+
bridge.registry.updateElementState(id, newState);
|
|
5040
|
+
onStateChangeRef.current?.(newState);
|
|
5041
|
+
};
|
|
4508
5042
|
if (ref.current && "measureInWindow" in ref.current) {
|
|
4509
5043
|
ref.current.measureInWindow((pageX, pageY) => {
|
|
4510
|
-
|
|
4511
|
-
x,
|
|
4512
|
-
y,
|
|
4513
|
-
width,
|
|
4514
|
-
height,
|
|
4515
|
-
pageX,
|
|
4516
|
-
pageY
|
|
4517
|
-
};
|
|
4518
|
-
setLayout(newLayout);
|
|
4519
|
-
if (bridge && registered) {
|
|
4520
|
-
const newState = {
|
|
4521
|
-
mounted: true,
|
|
4522
|
-
visible: width > 0 && height > 0,
|
|
4523
|
-
enabled: true,
|
|
4524
|
-
focused: false,
|
|
4525
|
-
layout: newLayout
|
|
4526
|
-
};
|
|
4527
|
-
bridge.registry.updateElementState(id, newState);
|
|
4528
|
-
onStateChange?.(newState);
|
|
4529
|
-
}
|
|
5044
|
+
writeLayout({ x, y, width, height, pageX, pageY });
|
|
4530
5045
|
});
|
|
4531
5046
|
} else {
|
|
4532
|
-
|
|
4533
|
-
x,
|
|
4534
|
-
y,
|
|
4535
|
-
width,
|
|
4536
|
-
height,
|
|
4537
|
-
pageX: x,
|
|
4538
|
-
pageY: y
|
|
4539
|
-
};
|
|
4540
|
-
setLayout(newLayout);
|
|
4541
|
-
if (bridge && registered) {
|
|
4542
|
-
const newState = {
|
|
4543
|
-
mounted: true,
|
|
4544
|
-
visible: width > 0 && height > 0,
|
|
4545
|
-
enabled: true,
|
|
4546
|
-
focused: false,
|
|
4547
|
-
layout: newLayout
|
|
4548
|
-
};
|
|
4549
|
-
bridge.registry.updateElementState(id, newState);
|
|
4550
|
-
onStateChange?.(newState);
|
|
4551
|
-
}
|
|
5047
|
+
writeLayout({ x, y, width, height, pageX: x, pageY: y });
|
|
4552
5048
|
}
|
|
4553
5049
|
},
|
|
4554
|
-
[bridge,
|
|
5050
|
+
[bridge, id]
|
|
4555
5051
|
);
|
|
4556
5052
|
const registerRef = useRef(register);
|
|
4557
5053
|
const unregisterRef = useRef(unregister);
|
|
@@ -4569,6 +5065,37 @@ function useUIElement(options) {
|
|
|
4569
5065
|
}
|
|
4570
5066
|
};
|
|
4571
5067
|
}, [autoRegister, bridge]);
|
|
5068
|
+
useEffect(() => {
|
|
5069
|
+
if (!bridge || !registered) return;
|
|
5070
|
+
const node = ref.current;
|
|
5071
|
+
if (!node || typeof node.measureInWindow !== "function") return;
|
|
5072
|
+
let cancelled = false;
|
|
5073
|
+
node.measureInWindow((pageX, pageY, w, h) => {
|
|
5074
|
+
if (cancelled) return;
|
|
5075
|
+
if (w <= 0 || h <= 0) return;
|
|
5076
|
+
const newLayout = {
|
|
5077
|
+
x: pageX,
|
|
5078
|
+
y: pageY,
|
|
5079
|
+
width: w,
|
|
5080
|
+
height: h,
|
|
5081
|
+
pageX,
|
|
5082
|
+
pageY
|
|
5083
|
+
};
|
|
5084
|
+
setLayout(newLayout);
|
|
5085
|
+
const newState = {
|
|
5086
|
+
mounted: true,
|
|
5087
|
+
visible: true,
|
|
5088
|
+
enabled: true,
|
|
5089
|
+
focused: false,
|
|
5090
|
+
layout: newLayout
|
|
5091
|
+
};
|
|
5092
|
+
bridge.registry.updateElementState(id, newState);
|
|
5093
|
+
onStateChangeRef.current?.(newState);
|
|
5094
|
+
});
|
|
5095
|
+
return () => {
|
|
5096
|
+
cancelled = true;
|
|
5097
|
+
};
|
|
5098
|
+
}, [bridge, registered, id]);
|
|
4572
5099
|
const registeredHandlerKeysRef = useRef([]);
|
|
4573
5100
|
useEffect(() => {
|
|
4574
5101
|
if (!bridge || !registered || !handlersProp) return;
|
|
@@ -4956,7 +5483,13 @@ function useToastRecorder() {
|
|
|
4956
5483
|
const dismissToast = useCallback((id) => toastCapture.dismissToast(id), [toastCapture]);
|
|
4957
5484
|
return { recordToast, dismissToast };
|
|
4958
5485
|
}
|
|
5486
|
+
function State(props) {
|
|
5487
|
+
return /* @__PURE__ */ jsx(Fragment, { children: props.children });
|
|
5488
|
+
}
|
|
5489
|
+
function TransitionTo(props) {
|
|
5490
|
+
return /* @__PURE__ */ jsx(Fragment, { children: props.children });
|
|
5491
|
+
}
|
|
4959
5492
|
|
|
4960
|
-
export { UIBridgeNativeProvider, useAutoRegister, useToastRecorder, useUIBridge, useUIBridgeModal, useUIBridgeNative, useUIBridgeNativeOptional, useUIBridgeNativeRequired, useUIBridgeRequired, useUIBridgeToast, useUIComponent, useUIComponentAction, useUIElement, useUIElementWithProps };
|
|
5493
|
+
export { State, TransitionTo, UIBridgeNativeProvider, useAutoRegister, useToastRecorder, useUIBridge, useUIBridgeModal, useUIBridgeNative, useUIBridgeNativeOptional, useUIBridgeNativeRequired, useUIBridgeRequired, useUIBridgeToast, useUIComponent, useUIComponentAction, useUIElement, useUIElementWithProps };
|
|
4961
5494
|
//# sourceMappingURL=index.mjs.map
|
|
4962
5495
|
//# sourceMappingURL=index.mjs.map
|