@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
|
@@ -8,7 +8,7 @@ import type { NativeUIBridgeFeatures, NativeUIBridgeConfig, RegisteredNativeElem
|
|
|
8
8
|
import { NativeUIBridgeRegistry } from '../core/registry';
|
|
9
9
|
import type { NativeActionExecutor } from '../control/types';
|
|
10
10
|
import { type ServerAdapter } from '../server/http-server';
|
|
11
|
-
import type { RouteProvider } from '../server/types';
|
|
11
|
+
import type { RouteProvider, KeepAwakeProvider } from '../server/types';
|
|
12
12
|
import { type CloudRelayConfig } from '../transport/CloudRelayClient';
|
|
13
13
|
import { ModalDetector } from '../modal/modal-detector';
|
|
14
14
|
import { ToastCapture } from '../toast/toast-capture';
|
|
@@ -89,6 +89,13 @@ export interface UIBridgeNativeProviderProps {
|
|
|
89
89
|
height: number;
|
|
90
90
|
}>;
|
|
91
91
|
};
|
|
92
|
+
/**
|
|
93
|
+
* Keep-awake provider for native screen-wake control via UI Bridge.
|
|
94
|
+
* Pass expo-keep-awake's activateKeepAwakeAsync/deactivateKeepAwake (wrapped
|
|
95
|
+
* to the request/release shape) so an external runner never loses the device
|
|
96
|
+
* to a screen lock. The SDK never imports expo-keep-awake directly.
|
|
97
|
+
*/
|
|
98
|
+
keepAwakeProvider?: KeepAwakeProvider;
|
|
92
99
|
/**
|
|
93
100
|
* Route provider for exposing the current navigation route in snapshots.
|
|
94
101
|
* Wire this to Expo Router's `usePathname()` / `useSegments()` via a module-level ref.
|
|
@@ -134,7 +141,7 @@ export interface UIBridgeNativeProviderProps {
|
|
|
134
141
|
* }
|
|
135
142
|
* ```
|
|
136
143
|
*/
|
|
137
|
-
export declare function UIBridgeNativeProvider({ children, features, config, onEvent, serverAdapter, navigationProvider, screenshotProvider, routeProvider, cloudRelayConfig, enableMdnsAnnounce, deviceId, }: UIBridgeNativeProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
144
|
+
export declare function UIBridgeNativeProvider({ children, features, config, onEvent, serverAdapter, navigationProvider, screenshotProvider, keepAwakeProvider, routeProvider, cloudRelayConfig, enableMdnsAnnounce, deviceId, }: UIBridgeNativeProviderProps): import("react/jsx-runtime").JSX.Element;
|
|
138
145
|
/**
|
|
139
146
|
* useUIBridgeNative hook
|
|
140
147
|
*
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"UIBridgeNativeProvider.d.ts","sourceRoot":"","sources":["../../src/react/UIBridgeNativeProvider.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAQN,MAAM,OAAO,CAAC;AACf,OAAO,KAAK,EACV,sBAAsB,EACtB,oBAAoB,EACpB,uBAAuB,EACvB,yBAAyB,EACzB,oBAAoB,EACpB,eAAe,EACf,mBAAmB,EACpB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,sBAAsB,EAA0C,MAAM,kBAAkB,CAAC;AAElG,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAC7D,OAAO,EAGL,KAAK,aAAa,EAEnB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"UIBridgeNativeProvider.d.ts","sourceRoot":"","sources":["../../src/react/UIBridgeNativeProvider.tsx"],"names":[],"mappings":"AAAA;;;;GAIG;AAEH,OAAO,KAQN,MAAM,OAAO,CAAC;AACf,OAAO,KAAK,EACV,sBAAsB,EACtB,oBAAoB,EACpB,uBAAuB,EACvB,yBAAyB,EACzB,oBAAoB,EACpB,eAAe,EACf,mBAAmB,EACpB,MAAM,eAAe,CAAC;AACvB,OAAO,EAAE,sBAAsB,EAA0C,MAAM,kBAAkB,CAAC;AAElG,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAC7D,OAAO,EAGL,KAAK,aAAa,EAEnB,MAAM,uBAAuB,CAAC;AAC/B,OAAO,KAAK,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAGxE,OAAO,EAAoB,KAAK,gBAAgB,EAAE,MAAM,+BAA+B,CAAC;AACxF,OAAO,EAAE,aAAa,EAAE,MAAM,yBAAyB,CAAC;AACxD,OAAO,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AACtD,OAAO,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AAEnD;;GAEG;AACH,MAAM,WAAW,0BAA0B;IACzC,oBAAoB;IACpB,QAAQ,EAAE,sBAAsB,CAAC;IACjC,oBAAoB;IACpB,MAAM,EAAE,oBAAoB,CAAC;IAC7B,uBAAuB;IACvB,QAAQ,EAAE,sBAAsB,CAAC;IACjC,sBAAsB;IACtB,QAAQ,EAAE,oBAAoB,CAAC;IAC/B,+DAA+D;IAC/D,aAAa,EAAE,aAAa,CAAC;IAC7B,oEAAoE;IACpE,YAAY,EAAE,YAAY,CAAC;IAC3B,2CAA2C;IAC3C,WAAW,EAAE,WAAW,CAAC;IACzB,kCAAkC;IAClC,WAAW,EAAE,MAAM,uBAAuB,EAAE,CAAC;IAC7C,oCAAoC;IACpC,aAAa,EAAE,MAAM,yBAAyB,EAAE,CAAC;IACjD,wBAAwB;IACxB,cAAc,EAAE,MAAM,oBAAoB,CAAC;IAC3C,0BAA0B;IAC1B,EAAE,EAAE,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,eAAe,EAAE,QAAQ,EAAE,mBAAmB,CAAC,CAAC,CAAC,KAAK,MAAM,IAAI,CAAC;IACzF,8BAA8B;IAC9B,GAAG,EAAE,CAAC,CAAC,GAAG,OAAO,EAAE,IAAI,EAAE,eAAe,EAAE,QAAQ,EAAE,mBAAmB,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC;IACpF,0CAA0C;IAC1C,WAAW,EAAE,OAAO,CAAC;IACrB,4BAA4B;IAC5B,aAAa,EAAE,OAAO,CAAC;IACvB,4BAA4B;IAC5B,WAAW,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;IACjC,2BAA2B;IAC3B,UAAU,EAAE,MAAM,IAAI,CAAC;IACvB,0EAA0E;IAC1E,eAAe,EAAE,MAAM,MAAM,GAAG,IAAI,CAAC;CACtC;AAOD;;GAEG;AACH,MAAM,WAAW,2BAA2B;IAC1C,uBAAuB;IACvB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,oBAAoB;IACpB,QAAQ,CAAC,EAAE,sBAAsB,CAAC;IAClC,oBAAoB;IACpB,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B,oBAAoB;IACpB,OAAO,CAAC,EAAE,mBAAmB,CAAC;IAC9B;;;;OAIG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B;;;OAGG;IACH,kBAAkB,CAAC,EAAE;QAAE,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,IAAI,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,IAAI,CAAA;KAAE,CAAC;IAC5E;;;OAGG;IACH,kBAAkB,CAAC,EAAE;QACnB,OAAO,EAAE,MAAM,OAAO,CAAC;YAAE,MAAM,EAAE,MAAM,CAAC;YAAC,KAAK,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAA;SAAE,CAAC,CAAC;KAC3E,CAAC;IACF;;;;;OAKG;IACH,iBAAiB,CAAC,EAAE,iBAAiB,CAAC;IACtC;;;OAGG;IACH,aAAa,CAAC,EAAE,aAAa,CAAC;IAC9B;;;;OAIG;IACH,gBAAgB,CAAC,EAAE,IAAI,CAAC,gBAAgB,EAAE,gBAAgB,CAAC,CAAC;IAC5D;;;OAGG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B;;;;OAIG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,sBAAsB,CAAC,EACrC,QAAQ,EACR,QAAa,EACb,MAAW,EACX,OAAO,EACP,aAAa,EACb,kBAAkB,EAClB,kBAAkB,EAClB,iBAAiB,EACjB,aAAa,EACb,gBAAgB,EAChB,kBAAkB,EAClB,QAAQ,GACT,EAAE,2BAA2B,2CA4X7B;AAED;;;;GAIG;AACH,wBAAgB,iBAAiB,IAAI,0BAA0B,CAM9D;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,IAAI,0BAA0B,GAAG,IAAI,CAE7E;AAED;;;;GAIG;AACH,eAAO,MAAM,yBAAyB,0BAAoB,CAAC"}
|
package/dist/react/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) => ({
|
|
@@ -1898,22 +1932,25 @@ function createServerHandlers(registry, executor, config) {
|
|
|
1898
1932
|
getElements: async (ctx) => {
|
|
1899
1933
|
const visibleOnly = ctx.query?.visibleOnly === "true" || ctx.body?.visibleOnly === true;
|
|
1900
1934
|
const forRoute = ctx.query?.route || ctx.body?.route;
|
|
1901
|
-
let allElements = visibleOnly ? registry.
|
|
1935
|
+
let allElements = visibleOnly ? registry.getMountedVisibleElements() : registry.getAllElements();
|
|
1902
1936
|
if (forRoute) {
|
|
1903
1937
|
allElements = allElements.filter((e) => e.registrationRoute === forRoute);
|
|
1904
1938
|
}
|
|
1905
1939
|
const elements = allElements.map((e) => {
|
|
1906
1940
|
const handlers = extractHandlerNames(e.props);
|
|
1941
|
+
const state = e.getState();
|
|
1942
|
+
const visibility = !state.visible ? "hidden" : state.layout !== null ? "visible" : "likely-visible";
|
|
1907
1943
|
return {
|
|
1908
1944
|
id: e.id,
|
|
1909
1945
|
type: e.type,
|
|
1910
1946
|
label: e.label,
|
|
1911
1947
|
identifier: e.getIdentifier(),
|
|
1912
|
-
state
|
|
1948
|
+
state,
|
|
1913
1949
|
actions: e.actions,
|
|
1914
1950
|
customActions: e.customActions ? Object.keys(e.customActions) : void 0,
|
|
1915
1951
|
registeredHandlers: handlers.length > 0 ? handlers : void 0,
|
|
1916
|
-
registrationRoute: e.registrationRoute
|
|
1952
|
+
registrationRoute: e.registrationRoute,
|
|
1953
|
+
visibility
|
|
1917
1954
|
};
|
|
1918
1955
|
});
|
|
1919
1956
|
return success2({ elements });
|
|
@@ -2168,6 +2205,45 @@ function createServerHandlers(registry, executor, config) {
|
|
|
2168
2205
|
"NOT_SUPPORTED"
|
|
2169
2206
|
);
|
|
2170
2207
|
},
|
|
2208
|
+
// Keep-awake (stub — apps should override via setKeepAwakeProvider, which
|
|
2209
|
+
// the provider wires when a keepAwakeProvider prop is supplied)
|
|
2210
|
+
keepAwake: async () => {
|
|
2211
|
+
return error2(
|
|
2212
|
+
"keep-awake not configured. Provide a keepAwakeProvider to UIBridgeNativeProvider.",
|
|
2213
|
+
"NOT_SUPPORTED"
|
|
2214
|
+
);
|
|
2215
|
+
},
|
|
2216
|
+
// AI helpers — runner-only endpoints that have no React Native analog.
|
|
2217
|
+
//
|
|
2218
|
+
// The mobile bridge mounts explicit NOT_SUPPORTED stubs at the runner's
|
|
2219
|
+
// cheatsheet paths so callers (and the /manual-test skill in particular)
|
|
2220
|
+
// get a structured envelope explaining the gap rather than a confusing
|
|
2221
|
+
// HTTP 404. Each error message names an in-tree mobile replacement so
|
|
2222
|
+
// operators aren't left guessing what to use instead.
|
|
2223
|
+
aiForms: async () => {
|
|
2224
|
+
return error2(
|
|
2225
|
+
'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.',
|
|
2226
|
+
"NOT_SUPPORTED"
|
|
2227
|
+
);
|
|
2228
|
+
},
|
|
2229
|
+
aiIdleStatus: async () => {
|
|
2230
|
+
return error2(
|
|
2231
|
+
'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.',
|
|
2232
|
+
"NOT_SUPPORTED"
|
|
2233
|
+
);
|
|
2234
|
+
},
|
|
2235
|
+
aiChangeBufferEnable: async () => {
|
|
2236
|
+
return error2(
|
|
2237
|
+
'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.',
|
|
2238
|
+
"NOT_SUPPORTED"
|
|
2239
|
+
);
|
|
2240
|
+
},
|
|
2241
|
+
aiWaitForElement: async () => {
|
|
2242
|
+
return error2(
|
|
2243
|
+
'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.',
|
|
2244
|
+
"NOT_SUPPORTED"
|
|
2245
|
+
);
|
|
2246
|
+
},
|
|
2171
2247
|
// Meta / Introspection (stub — server constructor overrides with real route table)
|
|
2172
2248
|
getMethods: async () => {
|
|
2173
2249
|
return error2("Methods introspection not configured", "NOT_SUPPORTED");
|
|
@@ -2823,6 +2899,24 @@ var WS_ROUTES = [
|
|
|
2823
2899
|
{ pattern: "meta/methods", handler: "getMethods", httpMethods: ["GET"] },
|
|
2824
2900
|
// AI helpers — mobile parity with the runner's `/ui-bridge/ai/*` family.
|
|
2825
2901
|
{ pattern: "ai/fill-form", handler: "fillForm", httpMethods: ["POST"] },
|
|
2902
|
+
// AI helpers — NOT_SUPPORTED stubs. These endpoints exist on the runner-side
|
|
2903
|
+
// UI Bridge surface (and the /manual-test skill's cheatsheet documents them
|
|
2904
|
+
// as if they're universally available). Mobile React Native has no DOM, so
|
|
2905
|
+
// the mobile bridge mounts explicit stubs so callers get a structured
|
|
2906
|
+
// NOT_SUPPORTED envelope (with a hint pointing at the mobile-native
|
|
2907
|
+
// replacement) instead of a confusing HTTP 404.
|
|
2908
|
+
{ pattern: "ai/forms", handler: "aiForms", httpMethods: ["GET"] },
|
|
2909
|
+
{ pattern: "ai/idle-status", handler: "aiIdleStatus", httpMethods: ["GET"] },
|
|
2910
|
+
{
|
|
2911
|
+
pattern: "ai/change-buffer/enable",
|
|
2912
|
+
handler: "aiChangeBufferEnable",
|
|
2913
|
+
httpMethods: ["POST"]
|
|
2914
|
+
},
|
|
2915
|
+
{
|
|
2916
|
+
pattern: "ai/wait-for-element",
|
|
2917
|
+
handler: "aiWaitForElement",
|
|
2918
|
+
httpMethods: ["POST"]
|
|
2919
|
+
},
|
|
2826
2920
|
// Coord-based tap — synthesizes a press at the given screen coords by
|
|
2827
2921
|
// matching against registered elements' layout rects.
|
|
2828
2922
|
{ pattern: "control/tap", handler: "tapAt", httpMethods: ["POST"] },
|
|
@@ -2841,6 +2935,12 @@ var WS_ROUTES = [
|
|
|
2841
2935
|
httpMethods: ["POST"],
|
|
2842
2936
|
requiresTestHooks: true
|
|
2843
2937
|
},
|
|
2938
|
+
{
|
|
2939
|
+
pattern: "control/keep-awake",
|
|
2940
|
+
handler: "keepAwake",
|
|
2941
|
+
httpMethods: ["POST"],
|
|
2942
|
+
requiresTestHooks: true
|
|
2943
|
+
},
|
|
2844
2944
|
// Observability — last-N console errors + network requests. testHooks-gated
|
|
2845
2945
|
// because patching `console.error` / `fetch` is invasive in production.
|
|
2846
2946
|
{
|
|
@@ -2889,11 +2989,16 @@ var HANDLER_DESCRIPTIONS = {
|
|
|
2889
2989
|
diffBaseline: "Diff current UI against a saved baseline",
|
|
2890
2990
|
getMethods: "List every handler name (legacy introspection)",
|
|
2891
2991
|
fillForm: "Fill multiple inputs in one call: body { fields: [{elementId, value}] } \u2014 each field dispatches `type` + `clear:true`",
|
|
2992
|
+
aiForms: 'STUB \u2014 runner-only; mobile returns NOT_SUPPORTED. Use /control/snapshot + filter type==="input" instead.',
|
|
2993
|
+
aiIdleStatus: "STUB \u2014 runner-only; mobile returns NOT_SUPPORTED. Subscribe to registry events over WS for change signals.",
|
|
2994
|
+
aiChangeBufferEnable: "STUB \u2014 runner-only; mobile returns NOT_SUPPORTED. Use WS event subscriptions for change notifications.",
|
|
2995
|
+
aiWaitForElement: "STUB \u2014 runner-only; mobile returns NOT_SUPPORTED. Use the WS waitForElement / waitForCondition JSON-RPC methods.",
|
|
2892
2996
|
tapAt: "Synthesize a press at given screen coords by matching registered element layout rects: body { x, y, action? }",
|
|
2893
2997
|
getConsoleErrors: "TEST HOOK \u2014 last-N console.error/console.warn entries; ?since=<ms>&limit=<n>",
|
|
2894
2998
|
getNetworkRequests: "TEST HOOK \u2014 last-N fetch/XHR entries with status + duration; ?since=<ms>&limit=<n>",
|
|
2895
2999
|
pushModal: "TEST HOOK \u2014 push a modal onto the snapshot.modalStack via the registry ModalDetector",
|
|
2896
|
-
dismissModal: "TEST HOOK \u2014 dismiss a modal by id from the snapshot.modalStack via the registry ModalDetector"
|
|
3000
|
+
dismissModal: "TEST HOOK \u2014 dismiss a modal by id from the snapshot.modalStack via the registry ModalDetector",
|
|
3001
|
+
keepAwake: "TEST HOOK \u2014 keep the device screen awake via the injected keepAwakeProvider: body { enabled: boolean, durationMs?: number }"
|
|
2897
3002
|
};
|
|
2898
3003
|
var ACTION_REFERENCE = [
|
|
2899
3004
|
{ name: "press", aliases: ["click"], params: "PressActionParams (optional position)" },
|
|
@@ -3161,6 +3266,35 @@ var NativeUIBridgeServer = class {
|
|
|
3161
3266
|
}
|
|
3162
3267
|
};
|
|
3163
3268
|
}
|
|
3269
|
+
/**
|
|
3270
|
+
* Set a keep-awake provider for native screen-wake control.
|
|
3271
|
+
* This enables `control/keep-awake` and arms the auto-arm hook in
|
|
3272
|
+
* handleRequest (every request keeps the screen alive for 60s).
|
|
3273
|
+
*/
|
|
3274
|
+
setKeepAwakeProvider(provider) {
|
|
3275
|
+
this.keepAwakeProvider = provider;
|
|
3276
|
+
this.handlers.keepAwake = async (ctx) => {
|
|
3277
|
+
const { enabled, durationMs } = ctx.body ?? {};
|
|
3278
|
+
if (typeof enabled !== "boolean") {
|
|
3279
|
+
return {
|
|
3280
|
+
success: false,
|
|
3281
|
+
error: "enabled must be boolean",
|
|
3282
|
+
code: "INVALID_REQUEST",
|
|
3283
|
+
timestamp: Date.now()
|
|
3284
|
+
};
|
|
3285
|
+
}
|
|
3286
|
+
if (enabled) {
|
|
3287
|
+
provider.request("ui-bridge", typeof durationMs === "number" ? durationMs : void 0);
|
|
3288
|
+
} else {
|
|
3289
|
+
provider.release("ui-bridge");
|
|
3290
|
+
}
|
|
3291
|
+
return {
|
|
3292
|
+
success: true,
|
|
3293
|
+
data: { enabled, durationMs: typeof durationMs === "number" ? durationMs : null },
|
|
3294
|
+
timestamp: Date.now()
|
|
3295
|
+
};
|
|
3296
|
+
};
|
|
3297
|
+
}
|
|
3164
3298
|
/**
|
|
3165
3299
|
* Start the HTTP server
|
|
3166
3300
|
*/
|
|
@@ -3753,6 +3887,12 @@ var NativeUIBridgeServer = class {
|
|
|
3753
3887
|
if (request.method === "OPTIONS") {
|
|
3754
3888
|
return { status: 204, headers, body: "" };
|
|
3755
3889
|
}
|
|
3890
|
+
if (this.config.testHooks === true && this.keepAwakeProvider) {
|
|
3891
|
+
try {
|
|
3892
|
+
this.keepAwakeProvider.request("auto-arm", 6e4);
|
|
3893
|
+
} catch {
|
|
3894
|
+
}
|
|
3895
|
+
}
|
|
3756
3896
|
try {
|
|
3757
3897
|
const response = await this.routeRequest(request);
|
|
3758
3898
|
return {
|
|
@@ -4653,6 +4793,7 @@ function UIBridgeNativeProvider({
|
|
|
4653
4793
|
serverAdapter,
|
|
4654
4794
|
navigationProvider,
|
|
4655
4795
|
screenshotProvider,
|
|
4796
|
+
keepAwakeProvider,
|
|
4656
4797
|
routeProvider,
|
|
4657
4798
|
cloudRelayConfig,
|
|
4658
4799
|
enableMdnsAnnounce,
|
|
@@ -4707,6 +4848,9 @@ function UIBridgeNativeProvider({
|
|
|
4707
4848
|
if (screenshotProvider) {
|
|
4708
4849
|
server.setScreenshotProvider(screenshotProvider);
|
|
4709
4850
|
}
|
|
4851
|
+
if (keepAwakeProvider) {
|
|
4852
|
+
server.setKeepAwakeProvider(keepAwakeProvider);
|
|
4853
|
+
}
|
|
4710
4854
|
if (routeProvider) {
|
|
4711
4855
|
server.setRouteProvider(routeProvider);
|
|
4712
4856
|
}
|
|
@@ -4760,6 +4904,7 @@ function UIBridgeNativeProvider({
|
|
|
4760
4904
|
serverAdapter,
|
|
4761
4905
|
navigationProvider,
|
|
4762
4906
|
screenshotProvider,
|
|
4907
|
+
keepAwakeProvider,
|
|
4763
4908
|
routeProvider
|
|
4764
4909
|
]);
|
|
4765
4910
|
const stopServer = react.useCallback(() => {
|
|
@@ -4793,6 +4938,7 @@ function UIBridgeNativeProvider({
|
|
|
4793
4938
|
});
|
|
4794
4939
|
if (navigationProvider) bareServer.setNavigationProvider(navigationProvider);
|
|
4795
4940
|
if (screenshotProvider) bareServer.setScreenshotProvider(screenshotProvider);
|
|
4941
|
+
if (keepAwakeProvider) bareServer.setKeepAwakeProvider(keepAwakeProvider);
|
|
4796
4942
|
if (routeProvider) bareServer.setRouteProvider(routeProvider);
|
|
4797
4943
|
serverRef.current = bareServer;
|
|
4798
4944
|
} catch (err) {
|
|
@@ -4967,6 +5113,24 @@ function flattenStateStyles(stateStyles) {
|
|
|
4967
5113
|
if (stateStyles.disabled != null) result.disabled = flattenStyle(stateStyles.disabled);
|
|
4968
5114
|
return Object.keys(result).length > 0 ? result : void 0;
|
|
4969
5115
|
}
|
|
5116
|
+
var warnedDeadButtonIds = /* @__PURE__ */ new Set();
|
|
5117
|
+
function scheduleDeadButtonWarning(bridge, id, type) {
|
|
5118
|
+
if (type !== "button" && type !== "pressable") return;
|
|
5119
|
+
if (warnedDeadButtonIds.has(id)) return;
|
|
5120
|
+
if (!bridge) return;
|
|
5121
|
+
setTimeout(() => {
|
|
5122
|
+
if (warnedDeadButtonIds.has(id)) return;
|
|
5123
|
+
const element = bridge.registry.getElement(id);
|
|
5124
|
+
if (!element) return;
|
|
5125
|
+
const props = element.props ?? {};
|
|
5126
|
+
const hasPress = typeof props.onPress === "function" || typeof props.onPressIn === "function" || typeof props.onResponderRelease === "function";
|
|
5127
|
+
if (hasPress) return;
|
|
5128
|
+
warnedDeadButtonIds.add(id);
|
|
5129
|
+
console.warn(
|
|
5130
|
+
`[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.`
|
|
5131
|
+
);
|
|
5132
|
+
}, 0);
|
|
5133
|
+
}
|
|
4970
5134
|
function useUIElement(options) {
|
|
4971
5135
|
const bridge = useUIBridgeNativeOptional();
|
|
4972
5136
|
const ref = react.useRef(null);
|
|
@@ -5013,6 +5177,7 @@ function useUIElement(options) {
|
|
|
5013
5177
|
registeredRef.current = true;
|
|
5014
5178
|
registeredIdRef.current = id;
|
|
5015
5179
|
setRegistered(true);
|
|
5180
|
+
scheduleDeadButtonWarning(bridge, id, type);
|
|
5016
5181
|
}, [bridge, id, type, label, actions, customActions, treePath, style, stateStylesProp]);
|
|
5017
5182
|
const unregister = react.useCallback(() => {
|
|
5018
5183
|
if (!bridge || !registeredRef.current) return;
|
|
@@ -5072,30 +5237,42 @@ function useUIElement(options) {
|
|
|
5072
5237
|
const node = ref.current;
|
|
5073
5238
|
if (!node || typeof node.measureInWindow !== "function") return;
|
|
5074
5239
|
let cancelled = false;
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
|
|
5080
|
-
|
|
5081
|
-
|
|
5082
|
-
|
|
5083
|
-
|
|
5084
|
-
|
|
5085
|
-
|
|
5086
|
-
|
|
5087
|
-
|
|
5088
|
-
|
|
5089
|
-
|
|
5090
|
-
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
5094
|
-
|
|
5095
|
-
|
|
5096
|
-
|
|
5240
|
+
let pendingTimer = null;
|
|
5241
|
+
const backoffMs = [50, 200, 500, 1e3, 2500];
|
|
5242
|
+
let attempt = 0;
|
|
5243
|
+
const tryMeasure = () => {
|
|
5244
|
+
if (cancelled || !node.measureInWindow) return;
|
|
5245
|
+
node.measureInWindow((pageX, pageY, w, h) => {
|
|
5246
|
+
if (cancelled) return;
|
|
5247
|
+
if (w > 0 && h > 0) {
|
|
5248
|
+
const newLayout = {
|
|
5249
|
+
x: pageX,
|
|
5250
|
+
y: pageY,
|
|
5251
|
+
width: w,
|
|
5252
|
+
height: h,
|
|
5253
|
+
pageX,
|
|
5254
|
+
pageY
|
|
5255
|
+
};
|
|
5256
|
+
setLayout(newLayout);
|
|
5257
|
+
const newState = {
|
|
5258
|
+
mounted: true,
|
|
5259
|
+
visible: true,
|
|
5260
|
+
enabled: true,
|
|
5261
|
+
focused: false,
|
|
5262
|
+
layout: newLayout
|
|
5263
|
+
};
|
|
5264
|
+
bridge.registry.updateElementState(id, newState);
|
|
5265
|
+
onStateChangeRef.current?.(newState);
|
|
5266
|
+
return;
|
|
5267
|
+
}
|
|
5268
|
+
if (attempt >= backoffMs.length) return;
|
|
5269
|
+
pendingTimer = setTimeout(tryMeasure, backoffMs[attempt++]);
|
|
5270
|
+
});
|
|
5271
|
+
};
|
|
5272
|
+
tryMeasure();
|
|
5097
5273
|
return () => {
|
|
5098
5274
|
cancelled = true;
|
|
5275
|
+
if (pendingTimer != null) clearTimeout(pendingTimer);
|
|
5099
5276
|
};
|
|
5100
5277
|
}, [bridge, registered, id]);
|
|
5101
5278
|
const registeredHandlerKeysRef = react.useRef([]);
|