@qontinui/ui-bridge-native 0.5.1 → 0.6.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/core/index.js +83 -4
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +83 -4
- package/dist/core/index.mjs.map +1 -1
- package/dist/core/registry.d.ts +49 -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/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +240 -30
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +240 -30
- package/dist/index.mjs.map +1 -1
- package/dist/react/index.js +215 -30
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +215 -30
- package/dist/react/index.mjs.map +1 -1
- package/dist/react/useUIElement.d.ts +92 -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.map +1 -1
- package/dist/server/index.js +84 -3
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +84 -3
- package/dist/server/index.mjs.map +1 -1
- package/dist/server/types.d.ts +4 -0
- package/dist/server/types.d.ts.map +1 -1
- package/package.json +3 -2
package/dist/index.mjs
CHANGED
|
@@ -254,7 +254,18 @@ var NativeUIBridgeRegistry = class {
|
|
|
254
254
|
return Array.from(this.elements.values());
|
|
255
255
|
}
|
|
256
256
|
/**
|
|
257
|
-
* Get all registered elements that are visible
|
|
257
|
+
* Get all registered elements that are visible AND have a measured layout.
|
|
258
|
+
*
|
|
259
|
+
* This is the *strict* visibility filter: requires `state.visible === true`
|
|
260
|
+
* AND `state.layout !== null`. Use this when you need both signals
|
|
261
|
+
* (e.g. coord-based tap, where you need an actual rect to hit-test against).
|
|
262
|
+
*
|
|
263
|
+
* Callers driving an LLM agent typically want
|
|
264
|
+
* {@link getMountedVisibleElements} instead — that variant accepts
|
|
265
|
+
* elements that *should* be on screen (visible:true) but whose first
|
|
266
|
+
* `onLayout` has not yet fired. Without it, a snapshot taken in the gap
|
|
267
|
+
* between mount and the first layout pass returns 0 elements on a fully
|
|
268
|
+
* populated screen.
|
|
258
269
|
*/
|
|
259
270
|
getVisibleElements() {
|
|
260
271
|
return this.getAllElements().filter((e) => {
|
|
@@ -262,6 +273,26 @@ var NativeUIBridgeRegistry = class {
|
|
|
262
273
|
return state.visible && state.layout !== null;
|
|
263
274
|
});
|
|
264
275
|
}
|
|
276
|
+
/**
|
|
277
|
+
* Get all registered elements marked as visible (regardless of layout).
|
|
278
|
+
*
|
|
279
|
+
* Looser than {@link getVisibleElements}: includes elements whose
|
|
280
|
+
* `onLayout` callback has not yet fired (so `layout === null`). React
|
|
281
|
+
* Native's `onLayout` is async — there's a one-tick window after mount
|
|
282
|
+
* where elements are in the tree, the user *sees* them, but the
|
|
283
|
+
* registry hasn't received their measurements yet. Excluding those
|
|
284
|
+
* elements makes `getSnapshot?visibleOnly=true` return 0 on a populated
|
|
285
|
+
* screen, which is a worse failure mode than including a few elements
|
|
286
|
+
* we can't yet locate spatially.
|
|
287
|
+
*
|
|
288
|
+
* Used by `createSnapshot`'s `visibleOnly` filter and by the
|
|
289
|
+
* `getSnapshot` / `getElements` handlers. Each emitted element carries a
|
|
290
|
+
* `visibility` discriminator (`"visible"` / `"likely-visible"`) so
|
|
291
|
+
* agents that *do* require a known rect can branch downstream.
|
|
292
|
+
*/
|
|
293
|
+
getMountedVisibleElements() {
|
|
294
|
+
return this.getAllElements().filter((e) => e.getState().visible);
|
|
295
|
+
}
|
|
265
296
|
/**
|
|
266
297
|
* Get elements registered on a specific route (for page-scoped filtering)
|
|
267
298
|
*/
|
|
@@ -318,6 +349,51 @@ var NativeUIBridgeRegistry = class {
|
|
|
318
349
|
this.emit("element:stateChanged", { id, state: newState });
|
|
319
350
|
}
|
|
320
351
|
}
|
|
352
|
+
/**
|
|
353
|
+
* Update an element's descriptive metadata (label / accessibilityLabel /
|
|
354
|
+
* testId). Sister to {@link updateElementState} (which carries layout +
|
|
355
|
+
* visibility) and {@link updateElementProps} (which carries event handlers).
|
|
356
|
+
*
|
|
357
|
+
* Idempotent — if every passed field equals the existing value, the call
|
|
358
|
+
* returns without mutating the entry or emitting an event. This keeps
|
|
359
|
+
* `useUIElement().updateLabel(...)` cheap when consumers re-publish from a
|
|
360
|
+
* render path that produces the same string on every tick.
|
|
361
|
+
*
|
|
362
|
+
* Returns `true` when the entry was mutated, `false` otherwise. Callers
|
|
363
|
+
* that want to gate a `console.warn` on no-op invocations can use the
|
|
364
|
+
* return value instead of recomputing equality.
|
|
365
|
+
*/
|
|
366
|
+
updateElementMeta(id, meta) {
|
|
367
|
+
const element = this.elements.get(id);
|
|
368
|
+
if (!element) return false;
|
|
369
|
+
const currentIdentifier = element.getIdentifier();
|
|
370
|
+
const nextLabel = meta.label !== void 0 ? meta.label : element.label;
|
|
371
|
+
const nextAccessibilityLabel = meta.accessibilityLabel !== void 0 ? meta.accessibilityLabel : currentIdentifier.accessibilityLabel;
|
|
372
|
+
const nextTestId = meta.testId !== void 0 ? meta.testId : currentIdentifier.testId;
|
|
373
|
+
const labelChanged = nextLabel !== element.label;
|
|
374
|
+
const accessibilityLabelChanged = nextAccessibilityLabel !== currentIdentifier.accessibilityLabel;
|
|
375
|
+
const testIdChanged = nextTestId !== currentIdentifier.testId;
|
|
376
|
+
if (!labelChanged && !accessibilityLabelChanged && !testIdChanged) {
|
|
377
|
+
return false;
|
|
378
|
+
}
|
|
379
|
+
const updated = {
|
|
380
|
+
...element,
|
|
381
|
+
label: nextLabel,
|
|
382
|
+
getIdentifier: () => ({
|
|
383
|
+
uiId: id,
|
|
384
|
+
testId: nextTestId,
|
|
385
|
+
accessibilityLabel: nextAccessibilityLabel,
|
|
386
|
+
treePath: currentIdentifier.treePath
|
|
387
|
+
})
|
|
388
|
+
};
|
|
389
|
+
this.elements.set(id, updated);
|
|
390
|
+
this.emit("element:registered", {
|
|
391
|
+
id,
|
|
392
|
+
type: element.type,
|
|
393
|
+
label: nextLabel
|
|
394
|
+
});
|
|
395
|
+
return true;
|
|
396
|
+
}
|
|
321
397
|
/**
|
|
322
398
|
* Update element props (for action execution)
|
|
323
399
|
*/
|
|
@@ -536,7 +612,7 @@ var NativeUIBridgeRegistry = class {
|
|
|
536
612
|
}
|
|
537
613
|
return { currentRoute: null, segments: void 0 };
|
|
538
614
|
})();
|
|
539
|
-
let elements = options?.visibleOnly ? this.
|
|
615
|
+
let elements = options?.visibleOnly ? this.getMountedVisibleElements() : this.getAllElements();
|
|
540
616
|
if (options?.currentRouteOnly && resolvedRoute.currentRoute) {
|
|
541
617
|
const currentRoute = resolvedRoute.currentRoute;
|
|
542
618
|
elements = elements.filter((e) => e.registrationRoute === currentRoute);
|
|
@@ -545,16 +621,19 @@ var NativeUIBridgeRegistry = class {
|
|
|
545
621
|
timestamp: Date.now(),
|
|
546
622
|
elements: elements.map((e) => {
|
|
547
623
|
const handlers = extractHandlerNames(e.props);
|
|
624
|
+
const state = e.getState();
|
|
625
|
+
const visibility = !state.visible ? "hidden" : state.layout !== null ? "visible" : "likely-visible";
|
|
548
626
|
return {
|
|
549
627
|
id: e.id,
|
|
550
628
|
type: e.type,
|
|
551
629
|
label: e.label,
|
|
552
630
|
identifier: e.getIdentifier(),
|
|
553
|
-
state
|
|
631
|
+
state,
|
|
554
632
|
actions: e.actions,
|
|
555
633
|
customActions: e.customActions ? Object.keys(e.customActions) : void 0,
|
|
556
634
|
registeredHandlers: handlers.length > 0 ? handlers : void 0,
|
|
557
|
-
registrationRoute: e.registrationRoute
|
|
635
|
+
registrationRoute: e.registrationRoute,
|
|
636
|
+
visibility
|
|
558
637
|
};
|
|
559
638
|
}),
|
|
560
639
|
components: this.getAllComponents().map((c) => ({
|
|
@@ -1948,22 +2027,25 @@ function createServerHandlers(registry, executor, config) {
|
|
|
1948
2027
|
getElements: async (ctx) => {
|
|
1949
2028
|
const visibleOnly = ctx.query?.visibleOnly === "true" || ctx.body?.visibleOnly === true;
|
|
1950
2029
|
const forRoute = ctx.query?.route || ctx.body?.route;
|
|
1951
|
-
let allElements = visibleOnly ? registry.
|
|
2030
|
+
let allElements = visibleOnly ? registry.getMountedVisibleElements() : registry.getAllElements();
|
|
1952
2031
|
if (forRoute) {
|
|
1953
2032
|
allElements = allElements.filter((e) => e.registrationRoute === forRoute);
|
|
1954
2033
|
}
|
|
1955
2034
|
const elements = allElements.map((e) => {
|
|
1956
2035
|
const handlers = extractHandlerNames(e.props);
|
|
2036
|
+
const state = e.getState();
|
|
2037
|
+
const visibility = !state.visible ? "hidden" : state.layout !== null ? "visible" : "likely-visible";
|
|
1957
2038
|
return {
|
|
1958
2039
|
id: e.id,
|
|
1959
2040
|
type: e.type,
|
|
1960
2041
|
label: e.label,
|
|
1961
2042
|
identifier: e.getIdentifier(),
|
|
1962
|
-
state
|
|
2043
|
+
state,
|
|
1963
2044
|
actions: e.actions,
|
|
1964
2045
|
customActions: e.customActions ? Object.keys(e.customActions) : void 0,
|
|
1965
2046
|
registeredHandlers: handlers.length > 0 ? handlers : void 0,
|
|
1966
|
-
registrationRoute: e.registrationRoute
|
|
2047
|
+
registrationRoute: e.registrationRoute,
|
|
2048
|
+
visibility
|
|
1967
2049
|
};
|
|
1968
2050
|
});
|
|
1969
2051
|
return success2({ elements });
|
|
@@ -2226,6 +2308,37 @@ function createServerHandlers(registry, executor, config) {
|
|
|
2226
2308
|
"NOT_SUPPORTED"
|
|
2227
2309
|
);
|
|
2228
2310
|
},
|
|
2311
|
+
// AI helpers — runner-only endpoints that have no React Native analog.
|
|
2312
|
+
//
|
|
2313
|
+
// The mobile bridge mounts explicit NOT_SUPPORTED stubs at the runner's
|
|
2314
|
+
// cheatsheet paths so callers (and the /manual-test skill in particular)
|
|
2315
|
+
// get a structured envelope explaining the gap rather than a confusing
|
|
2316
|
+
// HTTP 404. Each error message names an in-tree mobile replacement so
|
|
2317
|
+
// operators aren't left guessing what to use instead.
|
|
2318
|
+
aiForms: async () => {
|
|
2319
|
+
return error2(
|
|
2320
|
+
'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.',
|
|
2321
|
+
"NOT_SUPPORTED"
|
|
2322
|
+
);
|
|
2323
|
+
},
|
|
2324
|
+
aiIdleStatus: async () => {
|
|
2325
|
+
return error2(
|
|
2326
|
+
'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.',
|
|
2327
|
+
"NOT_SUPPORTED"
|
|
2328
|
+
);
|
|
2329
|
+
},
|
|
2330
|
+
aiChangeBufferEnable: async () => {
|
|
2331
|
+
return error2(
|
|
2332
|
+
'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.',
|
|
2333
|
+
"NOT_SUPPORTED"
|
|
2334
|
+
);
|
|
2335
|
+
},
|
|
2336
|
+
aiWaitForElement: async () => {
|
|
2337
|
+
return error2(
|
|
2338
|
+
'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.',
|
|
2339
|
+
"NOT_SUPPORTED"
|
|
2340
|
+
);
|
|
2341
|
+
},
|
|
2229
2342
|
// Meta / Introspection (stub — server constructor overrides with real route table)
|
|
2230
2343
|
getMethods: async () => {
|
|
2231
2344
|
return error2("Methods introspection not configured", "NOT_SUPPORTED");
|
|
@@ -2881,6 +2994,24 @@ var WS_ROUTES = [
|
|
|
2881
2994
|
{ pattern: "meta/methods", handler: "getMethods", httpMethods: ["GET"] },
|
|
2882
2995
|
// AI helpers — mobile parity with the runner's `/ui-bridge/ai/*` family.
|
|
2883
2996
|
{ pattern: "ai/fill-form", handler: "fillForm", httpMethods: ["POST"] },
|
|
2997
|
+
// AI helpers — NOT_SUPPORTED stubs. These endpoints exist on the runner-side
|
|
2998
|
+
// UI Bridge surface (and the /manual-test skill's cheatsheet documents them
|
|
2999
|
+
// as if they're universally available). Mobile React Native has no DOM, so
|
|
3000
|
+
// the mobile bridge mounts explicit stubs so callers get a structured
|
|
3001
|
+
// NOT_SUPPORTED envelope (with a hint pointing at the mobile-native
|
|
3002
|
+
// replacement) instead of a confusing HTTP 404.
|
|
3003
|
+
{ pattern: "ai/forms", handler: "aiForms", httpMethods: ["GET"] },
|
|
3004
|
+
{ pattern: "ai/idle-status", handler: "aiIdleStatus", httpMethods: ["GET"] },
|
|
3005
|
+
{
|
|
3006
|
+
pattern: "ai/change-buffer/enable",
|
|
3007
|
+
handler: "aiChangeBufferEnable",
|
|
3008
|
+
httpMethods: ["POST"]
|
|
3009
|
+
},
|
|
3010
|
+
{
|
|
3011
|
+
pattern: "ai/wait-for-element",
|
|
3012
|
+
handler: "aiWaitForElement",
|
|
3013
|
+
httpMethods: ["POST"]
|
|
3014
|
+
},
|
|
2884
3015
|
// Coord-based tap — synthesizes a press at the given screen coords by
|
|
2885
3016
|
// matching against registered elements' layout rects.
|
|
2886
3017
|
{ pattern: "control/tap", handler: "tapAt", httpMethods: ["POST"] },
|
|
@@ -2953,6 +3084,10 @@ var HANDLER_DESCRIPTIONS = {
|
|
|
2953
3084
|
diffBaseline: "Diff current UI against a saved baseline",
|
|
2954
3085
|
getMethods: "List every handler name (legacy introspection)",
|
|
2955
3086
|
fillForm: "Fill multiple inputs in one call: body { fields: [{elementId, value}] } \u2014 each field dispatches `type` + `clear:true`",
|
|
3087
|
+
aiForms: 'STUB \u2014 runner-only; mobile returns NOT_SUPPORTED. Use /control/snapshot + filter type==="input" instead.',
|
|
3088
|
+
aiIdleStatus: "STUB \u2014 runner-only; mobile returns NOT_SUPPORTED. Subscribe to registry events over WS for change signals.",
|
|
3089
|
+
aiChangeBufferEnable: "STUB \u2014 runner-only; mobile returns NOT_SUPPORTED. Use WS event subscriptions for change notifications.",
|
|
3090
|
+
aiWaitForElement: "STUB \u2014 runner-only; mobile returns NOT_SUPPORTED. Use the WS waitForElement / waitForCondition JSON-RPC methods.",
|
|
2956
3091
|
tapAt: "Synthesize a press at given screen coords by matching registered element layout rects: body { x, y, action? }",
|
|
2957
3092
|
getConsoleErrors: "TEST HOOK \u2014 last-N console.error/console.warn entries; ?since=<ms>&limit=<n>",
|
|
2958
3093
|
getNetworkRequests: "TEST HOOK \u2014 last-N fetch/XHR entries with status + duration; ?since=<ms>&limit=<n>",
|
|
@@ -5073,6 +5208,25 @@ function flattenStateStyles(stateStyles) {
|
|
|
5073
5208
|
if (stateStyles.disabled != null) result.disabled = flattenStyle(stateStyles.disabled);
|
|
5074
5209
|
return Object.keys(result).length > 0 ? result : void 0;
|
|
5075
5210
|
}
|
|
5211
|
+
var warnedDeadButtonIds = /* @__PURE__ */ new Set();
|
|
5212
|
+
var warnedRedundantUpdateLabelIds = /* @__PURE__ */ new Set();
|
|
5213
|
+
function scheduleDeadButtonWarning(bridge, id, type) {
|
|
5214
|
+
if (type !== "button" && type !== "pressable") return;
|
|
5215
|
+
if (warnedDeadButtonIds.has(id)) return;
|
|
5216
|
+
if (!bridge) return;
|
|
5217
|
+
setTimeout(() => {
|
|
5218
|
+
if (warnedDeadButtonIds.has(id)) return;
|
|
5219
|
+
const element = bridge.registry.getElement(id);
|
|
5220
|
+
if (!element) return;
|
|
5221
|
+
const props = element.props ?? {};
|
|
5222
|
+
const hasPress = typeof props.onPress === "function" || typeof props.onPressIn === "function" || typeof props.onResponderRelease === "function";
|
|
5223
|
+
if (hasPress) return;
|
|
5224
|
+
warnedDeadButtonIds.add(id);
|
|
5225
|
+
console.warn(
|
|
5226
|
+
`[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.`
|
|
5227
|
+
);
|
|
5228
|
+
}, 0);
|
|
5229
|
+
}
|
|
5076
5230
|
function useUIElement(options) {
|
|
5077
5231
|
const bridge = useUIBridgeNativeOptional();
|
|
5078
5232
|
const ref = useRef(null);
|
|
@@ -5119,6 +5273,7 @@ function useUIElement(options) {
|
|
|
5119
5273
|
registeredRef.current = true;
|
|
5120
5274
|
registeredIdRef.current = id;
|
|
5121
5275
|
setRegistered(true);
|
|
5276
|
+
scheduleDeadButtonWarning(bridge, id, type);
|
|
5122
5277
|
}, [bridge, id, type, label, actions, customActions, treePath, style, stateStylesProp]);
|
|
5123
5278
|
const unregister = useCallback(() => {
|
|
5124
5279
|
if (!bridge || !registeredRef.current) return;
|
|
@@ -5178,30 +5333,42 @@ function useUIElement(options) {
|
|
|
5178
5333
|
const node = ref.current;
|
|
5179
5334
|
if (!node || typeof node.measureInWindow !== "function") return;
|
|
5180
5335
|
let cancelled = false;
|
|
5181
|
-
|
|
5182
|
-
|
|
5183
|
-
|
|
5184
|
-
|
|
5185
|
-
|
|
5186
|
-
|
|
5187
|
-
|
|
5188
|
-
|
|
5189
|
-
|
|
5190
|
-
|
|
5191
|
-
|
|
5192
|
-
|
|
5193
|
-
|
|
5194
|
-
|
|
5195
|
-
|
|
5196
|
-
|
|
5197
|
-
|
|
5198
|
-
|
|
5199
|
-
|
|
5200
|
-
|
|
5201
|
-
|
|
5202
|
-
|
|
5336
|
+
let pendingTimer = null;
|
|
5337
|
+
const backoffMs = [50, 200, 500, 1e3, 2500];
|
|
5338
|
+
let attempt = 0;
|
|
5339
|
+
const tryMeasure = () => {
|
|
5340
|
+
if (cancelled || !node.measureInWindow) return;
|
|
5341
|
+
node.measureInWindow((pageX, pageY, w, h) => {
|
|
5342
|
+
if (cancelled) return;
|
|
5343
|
+
if (w > 0 && h > 0) {
|
|
5344
|
+
const newLayout = {
|
|
5345
|
+
x: pageX,
|
|
5346
|
+
y: pageY,
|
|
5347
|
+
width: w,
|
|
5348
|
+
height: h,
|
|
5349
|
+
pageX,
|
|
5350
|
+
pageY
|
|
5351
|
+
};
|
|
5352
|
+
setLayout(newLayout);
|
|
5353
|
+
const newState = {
|
|
5354
|
+
mounted: true,
|
|
5355
|
+
visible: true,
|
|
5356
|
+
enabled: true,
|
|
5357
|
+
focused: false,
|
|
5358
|
+
layout: newLayout
|
|
5359
|
+
};
|
|
5360
|
+
bridge.registry.updateElementState(id, newState);
|
|
5361
|
+
onStateChangeRef.current?.(newState);
|
|
5362
|
+
return;
|
|
5363
|
+
}
|
|
5364
|
+
if (attempt >= backoffMs.length) return;
|
|
5365
|
+
pendingTimer = setTimeout(tryMeasure, backoffMs[attempt++]);
|
|
5366
|
+
});
|
|
5367
|
+
};
|
|
5368
|
+
tryMeasure();
|
|
5203
5369
|
return () => {
|
|
5204
5370
|
cancelled = true;
|
|
5371
|
+
if (pendingTimer != null) clearTimeout(pendingTimer);
|
|
5205
5372
|
};
|
|
5206
5373
|
}, [bridge, registered, id]);
|
|
5207
5374
|
const registeredHandlerKeysRef = useRef([]);
|
|
@@ -5271,6 +5438,23 @@ function useUIElement(options) {
|
|
|
5271
5438
|
},
|
|
5272
5439
|
[bridge, id]
|
|
5273
5440
|
);
|
|
5441
|
+
const updateLabel = useCallback(
|
|
5442
|
+
(newLabel) => {
|
|
5443
|
+
if (!bridge || !registered) return;
|
|
5444
|
+
const changed = bridge.registry.updateElementMeta(id, {
|
|
5445
|
+
label: newLabel,
|
|
5446
|
+
accessibilityLabel: newLabel,
|
|
5447
|
+
testId: id
|
|
5448
|
+
});
|
|
5449
|
+
if (!changed && !warnedRedundantUpdateLabelIds.has(id)) {
|
|
5450
|
+
warnedRedundantUpdateLabelIds.add(id);
|
|
5451
|
+
console.warn(
|
|
5452
|
+
`[UI Bridge] updateLabel("${newLabel}") on element "${id}" was a no-op (label unchanged). Gate the call behind a useEffect with [label] deps to avoid redundant publishes.`
|
|
5453
|
+
);
|
|
5454
|
+
}
|
|
5455
|
+
},
|
|
5456
|
+
[bridge, registered, id]
|
|
5457
|
+
);
|
|
5274
5458
|
const updateStyle = useCallback(
|
|
5275
5459
|
(newStyle, newStateStyles) => {
|
|
5276
5460
|
if (!bridge || !registered) return;
|
|
@@ -5296,7 +5480,8 @@ function useUIElement(options) {
|
|
|
5296
5480
|
register,
|
|
5297
5481
|
unregister,
|
|
5298
5482
|
registeredElement,
|
|
5299
|
-
updateStyle
|
|
5483
|
+
updateStyle,
|
|
5484
|
+
updateLabel
|
|
5300
5485
|
};
|
|
5301
5486
|
}
|
|
5302
5487
|
function useUIElementWithProps(options) {
|
|
@@ -5694,6 +5879,31 @@ var UI_BRIDGE_NATIVE_ROUTES = {
|
|
|
5694
5879
|
path: "/ui-bridge/ai/fill-form",
|
|
5695
5880
|
description: "Fill multiple input elements in a single call"
|
|
5696
5881
|
},
|
|
5882
|
+
// AI helpers (NOT_SUPPORTED stubs) — these endpoints exist on the runner-side
|
|
5883
|
+
// UI Bridge surface but have no analog on mobile React Native. The mobile
|
|
5884
|
+
// bridge registers explicit stubs so callers get a structured NOT_SUPPORTED
|
|
5885
|
+
// envelope instead of a confusing HTTP 404. The mobile bridge's snapshot +
|
|
5886
|
+
// `/control/find` already cover what an operator would reach for these for.
|
|
5887
|
+
AI_FORMS: {
|
|
5888
|
+
method: "GET",
|
|
5889
|
+
path: "/ui-bridge/ai/forms",
|
|
5890
|
+
description: "Discover forms (runner-only; mobile returns NOT_SUPPORTED)"
|
|
5891
|
+
},
|
|
5892
|
+
AI_IDLE_STATUS: {
|
|
5893
|
+
method: "GET",
|
|
5894
|
+
path: "/ui-bridge/ai/idle-status",
|
|
5895
|
+
description: "Get page-idle signal (runner-only; mobile returns NOT_SUPPORTED)"
|
|
5896
|
+
},
|
|
5897
|
+
AI_CHANGE_BUFFER_ENABLE: {
|
|
5898
|
+
method: "POST",
|
|
5899
|
+
path: "/ui-bridge/ai/change-buffer/enable",
|
|
5900
|
+
description: "Enable DOM change buffer (runner-only; mobile returns NOT_SUPPORTED)"
|
|
5901
|
+
},
|
|
5902
|
+
AI_WAIT_FOR_ELEMENT: {
|
|
5903
|
+
method: "POST",
|
|
5904
|
+
path: "/ui-bridge/ai/wait-for-element",
|
|
5905
|
+
description: "Wait for element predicate via HTTP (runner-only; mobile returns NOT_SUPPORTED \u2014 use WS waitForElement instead)"
|
|
5906
|
+
},
|
|
5697
5907
|
// Test hooks — drive ModalDetector state from outside the React tree
|
|
5698
5908
|
// (gated by `features.testHooks`). Mirrors `pushModal` / `dismissModal`
|
|
5699
5909
|
// calls that components normally make in-process.
|