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