@qontinui/ui-bridge-native 0.3.0 → 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/{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.mjs
CHANGED
|
@@ -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);
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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;
|