@qontinui/ui-bridge-native 0.4.0 → 0.6.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/core/index.js +38 -4
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +38 -4
- package/dist/core/index.mjs.map +1 -1
- package/dist/core/registry.d.ts +30 -1
- package/dist/core/registry.d.ts.map +1 -1
- package/dist/core/types.d.ts +18 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/debug/index.js.map +1 -1
- package/dist/debug/index.mjs.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +232 -30
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +232 -30
- package/dist/index.mjs.map +1 -1
- package/dist/react/UIBridgeNativeProvider.d.ts +9 -2
- package/dist/react/UIBridgeNativeProvider.d.ts.map +1 -1
- package/dist/react/index.js +207 -30
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +207 -30
- package/dist/react/index.mjs.map +1 -1
- package/dist/react/useUIElement.d.ts +73 -35
- package/dist/react/useUIElement.d.ts.map +1 -1
- package/dist/server/handlers.d.ts.map +1 -1
- package/dist/server/http-server.d.ts +8 -1
- package/dist/server/http-server.d.ts.map +1 -1
- package/dist/server/index.js +135 -4
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +135 -4
- package/dist/server/index.mjs.map +1 -1
- package/dist/server/types.d.ts +13 -0
- package/dist/server/types.d.ts.map +1 -1
- package/package.json +3 -2
package/dist/index.js
CHANGED
|
@@ -256,7 +256,18 @@ var NativeUIBridgeRegistry = class {
|
|
|
256
256
|
return Array.from(this.elements.values());
|
|
257
257
|
}
|
|
258
258
|
/**
|
|
259
|
-
* Get all registered elements that are visible
|
|
259
|
+
* Get all registered elements that are visible AND have a measured layout.
|
|
260
|
+
*
|
|
261
|
+
* This is the *strict* visibility filter: requires `state.visible === true`
|
|
262
|
+
* AND `state.layout !== null`. Use this when you need both signals
|
|
263
|
+
* (e.g. coord-based tap, where you need an actual rect to hit-test against).
|
|
264
|
+
*
|
|
265
|
+
* Callers driving an LLM agent typically want
|
|
266
|
+
* {@link getMountedVisibleElements} instead — that variant accepts
|
|
267
|
+
* elements that *should* be on screen (visible:true) but whose first
|
|
268
|
+
* `onLayout` has not yet fired. Without it, a snapshot taken in the gap
|
|
269
|
+
* between mount and the first layout pass returns 0 elements on a fully
|
|
270
|
+
* populated screen.
|
|
260
271
|
*/
|
|
261
272
|
getVisibleElements() {
|
|
262
273
|
return this.getAllElements().filter((e) => {
|
|
@@ -264,6 +275,26 @@ var NativeUIBridgeRegistry = class {
|
|
|
264
275
|
return state.visible && state.layout !== null;
|
|
265
276
|
});
|
|
266
277
|
}
|
|
278
|
+
/**
|
|
279
|
+
* Get all registered elements marked as visible (regardless of layout).
|
|
280
|
+
*
|
|
281
|
+
* Looser than {@link getVisibleElements}: includes elements whose
|
|
282
|
+
* `onLayout` callback has not yet fired (so `layout === null`). React
|
|
283
|
+
* Native's `onLayout` is async — there's a one-tick window after mount
|
|
284
|
+
* where elements are in the tree, the user *sees* them, but the
|
|
285
|
+
* registry hasn't received their measurements yet. Excluding those
|
|
286
|
+
* elements makes `getSnapshot?visibleOnly=true` return 0 on a populated
|
|
287
|
+
* screen, which is a worse failure mode than including a few elements
|
|
288
|
+
* we can't yet locate spatially.
|
|
289
|
+
*
|
|
290
|
+
* Used by `createSnapshot`'s `visibleOnly` filter and by the
|
|
291
|
+
* `getSnapshot` / `getElements` handlers. Each emitted element carries a
|
|
292
|
+
* `visibility` discriminator (`"visible"` / `"likely-visible"`) so
|
|
293
|
+
* agents that *do* require a known rect can branch downstream.
|
|
294
|
+
*/
|
|
295
|
+
getMountedVisibleElements() {
|
|
296
|
+
return this.getAllElements().filter((e) => e.getState().visible);
|
|
297
|
+
}
|
|
267
298
|
/**
|
|
268
299
|
* Get elements registered on a specific route (for page-scoped filtering)
|
|
269
300
|
*/
|
|
@@ -538,7 +569,7 @@ var NativeUIBridgeRegistry = class {
|
|
|
538
569
|
}
|
|
539
570
|
return { currentRoute: null, segments: void 0 };
|
|
540
571
|
})();
|
|
541
|
-
let elements = options?.visibleOnly ? this.
|
|
572
|
+
let elements = options?.visibleOnly ? this.getMountedVisibleElements() : this.getAllElements();
|
|
542
573
|
if (options?.currentRouteOnly && resolvedRoute.currentRoute) {
|
|
543
574
|
const currentRoute = resolvedRoute.currentRoute;
|
|
544
575
|
elements = elements.filter((e) => e.registrationRoute === currentRoute);
|
|
@@ -547,16 +578,19 @@ var NativeUIBridgeRegistry = class {
|
|
|
547
578
|
timestamp: Date.now(),
|
|
548
579
|
elements: elements.map((e) => {
|
|
549
580
|
const handlers = extractHandlerNames(e.props);
|
|
581
|
+
const state = e.getState();
|
|
582
|
+
const visibility = !state.visible ? "hidden" : state.layout !== null ? "visible" : "likely-visible";
|
|
550
583
|
return {
|
|
551
584
|
id: e.id,
|
|
552
585
|
type: e.type,
|
|
553
586
|
label: e.label,
|
|
554
587
|
identifier: e.getIdentifier(),
|
|
555
|
-
state
|
|
588
|
+
state,
|
|
556
589
|
actions: e.actions,
|
|
557
590
|
customActions: e.customActions ? Object.keys(e.customActions) : void 0,
|
|
558
591
|
registeredHandlers: handlers.length > 0 ? handlers : void 0,
|
|
559
|
-
registrationRoute: e.registrationRoute
|
|
592
|
+
registrationRoute: e.registrationRoute,
|
|
593
|
+
visibility
|
|
560
594
|
};
|
|
561
595
|
}),
|
|
562
596
|
components: this.getAllComponents().map((c) => ({
|
|
@@ -1950,22 +1984,25 @@ function createServerHandlers(registry, executor, config) {
|
|
|
1950
1984
|
getElements: async (ctx) => {
|
|
1951
1985
|
const visibleOnly = ctx.query?.visibleOnly === "true" || ctx.body?.visibleOnly === true;
|
|
1952
1986
|
const forRoute = ctx.query?.route || ctx.body?.route;
|
|
1953
|
-
let allElements = visibleOnly ? registry.
|
|
1987
|
+
let allElements = visibleOnly ? registry.getMountedVisibleElements() : registry.getAllElements();
|
|
1954
1988
|
if (forRoute) {
|
|
1955
1989
|
allElements = allElements.filter((e) => e.registrationRoute === forRoute);
|
|
1956
1990
|
}
|
|
1957
1991
|
const elements = allElements.map((e) => {
|
|
1958
1992
|
const handlers = extractHandlerNames(e.props);
|
|
1993
|
+
const state = e.getState();
|
|
1994
|
+
const visibility = !state.visible ? "hidden" : state.layout !== null ? "visible" : "likely-visible";
|
|
1959
1995
|
return {
|
|
1960
1996
|
id: e.id,
|
|
1961
1997
|
type: e.type,
|
|
1962
1998
|
label: e.label,
|
|
1963
1999
|
identifier: e.getIdentifier(),
|
|
1964
|
-
state
|
|
2000
|
+
state,
|
|
1965
2001
|
actions: e.actions,
|
|
1966
2002
|
customActions: e.customActions ? Object.keys(e.customActions) : void 0,
|
|
1967
2003
|
registeredHandlers: handlers.length > 0 ? handlers : void 0,
|
|
1968
|
-
registrationRoute: e.registrationRoute
|
|
2004
|
+
registrationRoute: e.registrationRoute,
|
|
2005
|
+
visibility
|
|
1969
2006
|
};
|
|
1970
2007
|
});
|
|
1971
2008
|
return success2({ elements });
|
|
@@ -2220,6 +2257,45 @@ function createServerHandlers(registry, executor, config) {
|
|
|
2220
2257
|
"NOT_SUPPORTED"
|
|
2221
2258
|
);
|
|
2222
2259
|
},
|
|
2260
|
+
// Keep-awake (stub — apps should override via setKeepAwakeProvider, which
|
|
2261
|
+
// the provider wires when a keepAwakeProvider prop is supplied)
|
|
2262
|
+
keepAwake: async () => {
|
|
2263
|
+
return error2(
|
|
2264
|
+
"keep-awake not configured. Provide a keepAwakeProvider to UIBridgeNativeProvider.",
|
|
2265
|
+
"NOT_SUPPORTED"
|
|
2266
|
+
);
|
|
2267
|
+
},
|
|
2268
|
+
// AI helpers — runner-only endpoints that have no React Native analog.
|
|
2269
|
+
//
|
|
2270
|
+
// The mobile bridge mounts explicit NOT_SUPPORTED stubs at the runner's
|
|
2271
|
+
// cheatsheet paths so callers (and the /manual-test skill in particular)
|
|
2272
|
+
// get a structured envelope explaining the gap rather than a confusing
|
|
2273
|
+
// HTTP 404. Each error message names an in-tree mobile replacement so
|
|
2274
|
+
// operators aren't left guessing what to use instead.
|
|
2275
|
+
aiForms: async () => {
|
|
2276
|
+
return error2(
|
|
2277
|
+
'ai/forms (form discovery) is runner-only; mobile React Native has no DOM to walk. Use GET /ui-bridge/control/snapshot and filter for type === "input" instead.',
|
|
2278
|
+
"NOT_SUPPORTED"
|
|
2279
|
+
);
|
|
2280
|
+
},
|
|
2281
|
+
aiIdleStatus: async () => {
|
|
2282
|
+
return error2(
|
|
2283
|
+
'ai/idle-status (page-idle signal) is runner-only; mobile React Native has no DOM-mutation / network-idle signals. Use the registry event stream (WS subscribe to "element:registered"/"element:stateChanged") or poll /control/snapshot for stability.',
|
|
2284
|
+
"NOT_SUPPORTED"
|
|
2285
|
+
);
|
|
2286
|
+
},
|
|
2287
|
+
aiChangeBufferEnable: async () => {
|
|
2288
|
+
return error2(
|
|
2289
|
+
'ai/change-buffer (DOM mutation diff buffer) is runner-only; mobile React Native has no DOM. Use the WS event bridge (subscribe to "element:registered"/"element:unregistered"/"element:stateChanged") for change notifications.',
|
|
2290
|
+
"NOT_SUPPORTED"
|
|
2291
|
+
);
|
|
2292
|
+
},
|
|
2293
|
+
aiWaitForElement: async () => {
|
|
2294
|
+
return error2(
|
|
2295
|
+
'POST /ai/wait-for-element is runner-only on mobile. Use the WS "waitForElement" JSON-RPC method or "waitForCondition" instead \u2014 same semantics, no HTTP polling overhead.',
|
|
2296
|
+
"NOT_SUPPORTED"
|
|
2297
|
+
);
|
|
2298
|
+
},
|
|
2223
2299
|
// Meta / Introspection (stub — server constructor overrides with real route table)
|
|
2224
2300
|
getMethods: async () => {
|
|
2225
2301
|
return error2("Methods introspection not configured", "NOT_SUPPORTED");
|
|
@@ -2875,6 +2951,24 @@ var WS_ROUTES = [
|
|
|
2875
2951
|
{ pattern: "meta/methods", handler: "getMethods", httpMethods: ["GET"] },
|
|
2876
2952
|
// AI helpers — mobile parity with the runner's `/ui-bridge/ai/*` family.
|
|
2877
2953
|
{ pattern: "ai/fill-form", handler: "fillForm", httpMethods: ["POST"] },
|
|
2954
|
+
// AI helpers — NOT_SUPPORTED stubs. These endpoints exist on the runner-side
|
|
2955
|
+
// UI Bridge surface (and the /manual-test skill's cheatsheet documents them
|
|
2956
|
+
// as if they're universally available). Mobile React Native has no DOM, so
|
|
2957
|
+
// the mobile bridge mounts explicit stubs so callers get a structured
|
|
2958
|
+
// NOT_SUPPORTED envelope (with a hint pointing at the mobile-native
|
|
2959
|
+
// replacement) instead of a confusing HTTP 404.
|
|
2960
|
+
{ pattern: "ai/forms", handler: "aiForms", httpMethods: ["GET"] },
|
|
2961
|
+
{ pattern: "ai/idle-status", handler: "aiIdleStatus", httpMethods: ["GET"] },
|
|
2962
|
+
{
|
|
2963
|
+
pattern: "ai/change-buffer/enable",
|
|
2964
|
+
handler: "aiChangeBufferEnable",
|
|
2965
|
+
httpMethods: ["POST"]
|
|
2966
|
+
},
|
|
2967
|
+
{
|
|
2968
|
+
pattern: "ai/wait-for-element",
|
|
2969
|
+
handler: "aiWaitForElement",
|
|
2970
|
+
httpMethods: ["POST"]
|
|
2971
|
+
},
|
|
2878
2972
|
// Coord-based tap — synthesizes a press at the given screen coords by
|
|
2879
2973
|
// matching against registered elements' layout rects.
|
|
2880
2974
|
{ pattern: "control/tap", handler: "tapAt", httpMethods: ["POST"] },
|
|
@@ -2893,6 +2987,12 @@ var WS_ROUTES = [
|
|
|
2893
2987
|
httpMethods: ["POST"],
|
|
2894
2988
|
requiresTestHooks: true
|
|
2895
2989
|
},
|
|
2990
|
+
{
|
|
2991
|
+
pattern: "control/keep-awake",
|
|
2992
|
+
handler: "keepAwake",
|
|
2993
|
+
httpMethods: ["POST"],
|
|
2994
|
+
requiresTestHooks: true
|
|
2995
|
+
},
|
|
2896
2996
|
// Observability — last-N console errors + network requests. testHooks-gated
|
|
2897
2997
|
// because patching `console.error` / `fetch` is invasive in production.
|
|
2898
2998
|
{
|
|
@@ -2941,11 +3041,16 @@ var HANDLER_DESCRIPTIONS = {
|
|
|
2941
3041
|
diffBaseline: "Diff current UI against a saved baseline",
|
|
2942
3042
|
getMethods: "List every handler name (legacy introspection)",
|
|
2943
3043
|
fillForm: "Fill multiple inputs in one call: body { fields: [{elementId, value}] } \u2014 each field dispatches `type` + `clear:true`",
|
|
3044
|
+
aiForms: 'STUB \u2014 runner-only; mobile returns NOT_SUPPORTED. Use /control/snapshot + filter type==="input" instead.',
|
|
3045
|
+
aiIdleStatus: "STUB \u2014 runner-only; mobile returns NOT_SUPPORTED. Subscribe to registry events over WS for change signals.",
|
|
3046
|
+
aiChangeBufferEnable: "STUB \u2014 runner-only; mobile returns NOT_SUPPORTED. Use WS event subscriptions for change notifications.",
|
|
3047
|
+
aiWaitForElement: "STUB \u2014 runner-only; mobile returns NOT_SUPPORTED. Use the WS waitForElement / waitForCondition JSON-RPC methods.",
|
|
2944
3048
|
tapAt: "Synthesize a press at given screen coords by matching registered element layout rects: body { x, y, action? }",
|
|
2945
3049
|
getConsoleErrors: "TEST HOOK \u2014 last-N console.error/console.warn entries; ?since=<ms>&limit=<n>",
|
|
2946
3050
|
getNetworkRequests: "TEST HOOK \u2014 last-N fetch/XHR entries with status + duration; ?since=<ms>&limit=<n>",
|
|
2947
3051
|
pushModal: "TEST HOOK \u2014 push a modal onto the snapshot.modalStack via the registry ModalDetector",
|
|
2948
|
-
dismissModal: "TEST HOOK \u2014 dismiss a modal by id from the snapshot.modalStack via the registry ModalDetector"
|
|
3052
|
+
dismissModal: "TEST HOOK \u2014 dismiss a modal by id from the snapshot.modalStack via the registry ModalDetector",
|
|
3053
|
+
keepAwake: "TEST HOOK \u2014 keep the device screen awake via the injected keepAwakeProvider: body { enabled: boolean, durationMs?: number }"
|
|
2949
3054
|
};
|
|
2950
3055
|
var ACTION_REFERENCE = [
|
|
2951
3056
|
{ name: "press", aliases: ["click"], params: "PressActionParams (optional position)" },
|
|
@@ -3213,6 +3318,35 @@ var NativeUIBridgeServer = class {
|
|
|
3213
3318
|
}
|
|
3214
3319
|
};
|
|
3215
3320
|
}
|
|
3321
|
+
/**
|
|
3322
|
+
* Set a keep-awake provider for native screen-wake control.
|
|
3323
|
+
* This enables `control/keep-awake` and arms the auto-arm hook in
|
|
3324
|
+
* handleRequest (every request keeps the screen alive for 60s).
|
|
3325
|
+
*/
|
|
3326
|
+
setKeepAwakeProvider(provider) {
|
|
3327
|
+
this.keepAwakeProvider = provider;
|
|
3328
|
+
this.handlers.keepAwake = async (ctx) => {
|
|
3329
|
+
const { enabled, durationMs } = ctx.body ?? {};
|
|
3330
|
+
if (typeof enabled !== "boolean") {
|
|
3331
|
+
return {
|
|
3332
|
+
success: false,
|
|
3333
|
+
error: "enabled must be boolean",
|
|
3334
|
+
code: "INVALID_REQUEST",
|
|
3335
|
+
timestamp: Date.now()
|
|
3336
|
+
};
|
|
3337
|
+
}
|
|
3338
|
+
if (enabled) {
|
|
3339
|
+
provider.request("ui-bridge", typeof durationMs === "number" ? durationMs : void 0);
|
|
3340
|
+
} else {
|
|
3341
|
+
provider.release("ui-bridge");
|
|
3342
|
+
}
|
|
3343
|
+
return {
|
|
3344
|
+
success: true,
|
|
3345
|
+
data: { enabled, durationMs: typeof durationMs === "number" ? durationMs : null },
|
|
3346
|
+
timestamp: Date.now()
|
|
3347
|
+
};
|
|
3348
|
+
};
|
|
3349
|
+
}
|
|
3216
3350
|
/**
|
|
3217
3351
|
* Start the HTTP server
|
|
3218
3352
|
*/
|
|
@@ -3805,6 +3939,12 @@ var NativeUIBridgeServer = class {
|
|
|
3805
3939
|
if (request.method === "OPTIONS") {
|
|
3806
3940
|
return { status: 204, headers, body: "" };
|
|
3807
3941
|
}
|
|
3942
|
+
if (this.config.testHooks === true && this.keepAwakeProvider) {
|
|
3943
|
+
try {
|
|
3944
|
+
this.keepAwakeProvider.request("auto-arm", 6e4);
|
|
3945
|
+
} catch {
|
|
3946
|
+
}
|
|
3947
|
+
}
|
|
3808
3948
|
try {
|
|
3809
3949
|
const response = await this.routeRequest(request);
|
|
3810
3950
|
return {
|
|
@@ -4705,6 +4845,7 @@ function UIBridgeNativeProvider({
|
|
|
4705
4845
|
serverAdapter,
|
|
4706
4846
|
navigationProvider,
|
|
4707
4847
|
screenshotProvider,
|
|
4848
|
+
keepAwakeProvider,
|
|
4708
4849
|
routeProvider,
|
|
4709
4850
|
cloudRelayConfig,
|
|
4710
4851
|
enableMdnsAnnounce,
|
|
@@ -4759,6 +4900,9 @@ function UIBridgeNativeProvider({
|
|
|
4759
4900
|
if (screenshotProvider) {
|
|
4760
4901
|
server.setScreenshotProvider(screenshotProvider);
|
|
4761
4902
|
}
|
|
4903
|
+
if (keepAwakeProvider) {
|
|
4904
|
+
server.setKeepAwakeProvider(keepAwakeProvider);
|
|
4905
|
+
}
|
|
4762
4906
|
if (routeProvider) {
|
|
4763
4907
|
server.setRouteProvider(routeProvider);
|
|
4764
4908
|
}
|
|
@@ -4812,6 +4956,7 @@ function UIBridgeNativeProvider({
|
|
|
4812
4956
|
serverAdapter,
|
|
4813
4957
|
navigationProvider,
|
|
4814
4958
|
screenshotProvider,
|
|
4959
|
+
keepAwakeProvider,
|
|
4815
4960
|
routeProvider
|
|
4816
4961
|
]);
|
|
4817
4962
|
const stopServer = react.useCallback(() => {
|
|
@@ -4845,6 +4990,7 @@ function UIBridgeNativeProvider({
|
|
|
4845
4990
|
});
|
|
4846
4991
|
if (navigationProvider) bareServer.setNavigationProvider(navigationProvider);
|
|
4847
4992
|
if (screenshotProvider) bareServer.setScreenshotProvider(screenshotProvider);
|
|
4993
|
+
if (keepAwakeProvider) bareServer.setKeepAwakeProvider(keepAwakeProvider);
|
|
4848
4994
|
if (routeProvider) bareServer.setRouteProvider(routeProvider);
|
|
4849
4995
|
serverRef.current = bareServer;
|
|
4850
4996
|
} catch (err) {
|
|
@@ -5019,6 +5165,24 @@ function flattenStateStyles(stateStyles) {
|
|
|
5019
5165
|
if (stateStyles.disabled != null) result.disabled = flattenStyle(stateStyles.disabled);
|
|
5020
5166
|
return Object.keys(result).length > 0 ? result : void 0;
|
|
5021
5167
|
}
|
|
5168
|
+
var warnedDeadButtonIds = /* @__PURE__ */ new Set();
|
|
5169
|
+
function scheduleDeadButtonWarning(bridge, id, type) {
|
|
5170
|
+
if (type !== "button" && type !== "pressable") return;
|
|
5171
|
+
if (warnedDeadButtonIds.has(id)) return;
|
|
5172
|
+
if (!bridge) return;
|
|
5173
|
+
setTimeout(() => {
|
|
5174
|
+
if (warnedDeadButtonIds.has(id)) return;
|
|
5175
|
+
const element = bridge.registry.getElement(id);
|
|
5176
|
+
if (!element) return;
|
|
5177
|
+
const props = element.props ?? {};
|
|
5178
|
+
const hasPress = typeof props.onPress === "function" || typeof props.onPressIn === "function" || typeof props.onResponderRelease === "function";
|
|
5179
|
+
if (hasPress) return;
|
|
5180
|
+
warnedDeadButtonIds.add(id);
|
|
5181
|
+
console.warn(
|
|
5182
|
+
`[UI Bridge] Registered \`${type}\` element "${id}" without an \`onPress\` handler \u2014 UI Bridge \`press\` actions on this element will fail with "No press handler found on element". Use \`useUIElementWithProps\` and \`captureProps({onPress})\` instead.`
|
|
5183
|
+
);
|
|
5184
|
+
}, 0);
|
|
5185
|
+
}
|
|
5022
5186
|
function useUIElement(options) {
|
|
5023
5187
|
const bridge = useUIBridgeNativeOptional();
|
|
5024
5188
|
const ref = react.useRef(null);
|
|
@@ -5065,6 +5229,7 @@ function useUIElement(options) {
|
|
|
5065
5229
|
registeredRef.current = true;
|
|
5066
5230
|
registeredIdRef.current = id;
|
|
5067
5231
|
setRegistered(true);
|
|
5232
|
+
scheduleDeadButtonWarning(bridge, id, type);
|
|
5068
5233
|
}, [bridge, id, type, label, actions, customActions, treePath, style, stateStylesProp]);
|
|
5069
5234
|
const unregister = react.useCallback(() => {
|
|
5070
5235
|
if (!bridge || !registeredRef.current) return;
|
|
@@ -5124,30 +5289,42 @@ function useUIElement(options) {
|
|
|
5124
5289
|
const node = ref.current;
|
|
5125
5290
|
if (!node || typeof node.measureInWindow !== "function") return;
|
|
5126
5291
|
let cancelled = false;
|
|
5127
|
-
|
|
5128
|
-
|
|
5129
|
-
|
|
5130
|
-
|
|
5131
|
-
|
|
5132
|
-
|
|
5133
|
-
|
|
5134
|
-
|
|
5135
|
-
|
|
5136
|
-
|
|
5137
|
-
|
|
5138
|
-
|
|
5139
|
-
|
|
5140
|
-
|
|
5141
|
-
|
|
5142
|
-
|
|
5143
|
-
|
|
5144
|
-
|
|
5145
|
-
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
|
|
5292
|
+
let pendingTimer = null;
|
|
5293
|
+
const backoffMs = [50, 200, 500, 1e3, 2500];
|
|
5294
|
+
let attempt = 0;
|
|
5295
|
+
const tryMeasure = () => {
|
|
5296
|
+
if (cancelled || !node.measureInWindow) return;
|
|
5297
|
+
node.measureInWindow((pageX, pageY, w, h) => {
|
|
5298
|
+
if (cancelled) return;
|
|
5299
|
+
if (w > 0 && h > 0) {
|
|
5300
|
+
const newLayout = {
|
|
5301
|
+
x: pageX,
|
|
5302
|
+
y: pageY,
|
|
5303
|
+
width: w,
|
|
5304
|
+
height: h,
|
|
5305
|
+
pageX,
|
|
5306
|
+
pageY
|
|
5307
|
+
};
|
|
5308
|
+
setLayout(newLayout);
|
|
5309
|
+
const newState = {
|
|
5310
|
+
mounted: true,
|
|
5311
|
+
visible: true,
|
|
5312
|
+
enabled: true,
|
|
5313
|
+
focused: false,
|
|
5314
|
+
layout: newLayout
|
|
5315
|
+
};
|
|
5316
|
+
bridge.registry.updateElementState(id, newState);
|
|
5317
|
+
onStateChangeRef.current?.(newState);
|
|
5318
|
+
return;
|
|
5319
|
+
}
|
|
5320
|
+
if (attempt >= backoffMs.length) return;
|
|
5321
|
+
pendingTimer = setTimeout(tryMeasure, backoffMs[attempt++]);
|
|
5322
|
+
});
|
|
5323
|
+
};
|
|
5324
|
+
tryMeasure();
|
|
5149
5325
|
return () => {
|
|
5150
5326
|
cancelled = true;
|
|
5327
|
+
if (pendingTimer != null) clearTimeout(pendingTimer);
|
|
5151
5328
|
};
|
|
5152
5329
|
}, [bridge, registered, id]);
|
|
5153
5330
|
const registeredHandlerKeysRef = react.useRef([]);
|
|
@@ -5640,6 +5817,31 @@ var UI_BRIDGE_NATIVE_ROUTES = {
|
|
|
5640
5817
|
path: "/ui-bridge/ai/fill-form",
|
|
5641
5818
|
description: "Fill multiple input elements in a single call"
|
|
5642
5819
|
},
|
|
5820
|
+
// AI helpers (NOT_SUPPORTED stubs) — these endpoints exist on the runner-side
|
|
5821
|
+
// UI Bridge surface but have no analog on mobile React Native. The mobile
|
|
5822
|
+
// bridge registers explicit stubs so callers get a structured NOT_SUPPORTED
|
|
5823
|
+
// envelope instead of a confusing HTTP 404. The mobile bridge's snapshot +
|
|
5824
|
+
// `/control/find` already cover what an operator would reach for these for.
|
|
5825
|
+
AI_FORMS: {
|
|
5826
|
+
method: "GET",
|
|
5827
|
+
path: "/ui-bridge/ai/forms",
|
|
5828
|
+
description: "Discover forms (runner-only; mobile returns NOT_SUPPORTED)"
|
|
5829
|
+
},
|
|
5830
|
+
AI_IDLE_STATUS: {
|
|
5831
|
+
method: "GET",
|
|
5832
|
+
path: "/ui-bridge/ai/idle-status",
|
|
5833
|
+
description: "Get page-idle signal (runner-only; mobile returns NOT_SUPPORTED)"
|
|
5834
|
+
},
|
|
5835
|
+
AI_CHANGE_BUFFER_ENABLE: {
|
|
5836
|
+
method: "POST",
|
|
5837
|
+
path: "/ui-bridge/ai/change-buffer/enable",
|
|
5838
|
+
description: "Enable DOM change buffer (runner-only; mobile returns NOT_SUPPORTED)"
|
|
5839
|
+
},
|
|
5840
|
+
AI_WAIT_FOR_ELEMENT: {
|
|
5841
|
+
method: "POST",
|
|
5842
|
+
path: "/ui-bridge/ai/wait-for-element",
|
|
5843
|
+
description: "Wait for element predicate via HTTP (runner-only; mobile returns NOT_SUPPORTED \u2014 use WS waitForElement instead)"
|
|
5844
|
+
},
|
|
5643
5845
|
// Test hooks — drive ModalDetector state from outside the React tree
|
|
5644
5846
|
// (gated by `features.testHooks`). Mirrors `pushModal` / `dismissModal`
|
|
5645
5847
|
// calls that components normally make in-process.
|