@qontinui/ui-bridge-native 0.6.11 → 0.6.13
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/README.md +36 -16
- package/dist/control/action-executor.d.ts +42 -2
- package/dist/control/action-executor.d.ts.map +1 -1
- package/dist/control/index.js +723 -4
- package/dist/control/index.js.map +1 -1
- package/dist/control/index.mjs +723 -5
- package/dist/control/index.mjs.map +1 -1
- package/dist/control/types.d.ts +82 -1
- package/dist/control/types.d.ts.map +1 -1
- package/dist/core/abortable.d.ts +151 -0
- package/dist/core/abortable.d.ts.map +1 -0
- package/dist/core/action-effect.d.ts +74 -0
- package/dist/core/action-effect.d.ts.map +1 -0
- package/dist/core/element-actions.d.ts +38 -0
- package/dist/core/element-actions.d.ts.map +1 -0
- package/dist/core/index.d.ts +4 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/index.js +853 -15
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +835 -16
- package/dist/core/index.mjs.map +1 -1
- package/dist/core/param-schema.d.ts +282 -0
- package/dist/core/param-schema.d.ts.map +1 -0
- package/dist/core/registry.d.ts +317 -2
- package/dist/core/registry.d.ts.map +1 -1
- package/dist/core/types.d.ts +231 -32
- 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 +10 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1603 -118
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1585 -119
- package/dist/index.mjs.map +1 -1
- package/dist/react/UIBridgeNativeProvider.d.ts +1 -23
- package/dist/react/UIBridgeNativeProvider.d.ts.map +1 -1
- package/dist/react/index.js +1420 -113
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +1420 -113
- package/dist/react/index.mjs.map +1 -1
- package/dist/react/useUIBridge.d.ts +19 -22
- package/dist/react/useUIBridge.d.ts.map +1 -1
- package/dist/react/useUIComponent.d.ts +113 -8
- package/dist/react/useUIComponent.d.ts.map +1 -1
- package/dist/react/useUIElement.d.ts +34 -8
- 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 +15 -2
- package/dist/server/http-server.d.ts.map +1 -1
- package/dist/server/index.js +348 -43
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +348 -44
- package/dist/server/index.mjs.map +1 -1
- package/dist/server/observability.d.ts +40 -0
- package/dist/server/observability.d.ts.map +1 -1
- package/dist/server/types.d.ts +57 -1
- package/dist/server/types.d.ts.map +1 -1
- package/dist/transport/CloudRelayClient.d.ts.map +1 -1
- package/dist/transport/DeviceAnnouncer.d.ts.map +1 -1
- package/dist/transport/relay-logging.d.ts +50 -0
- package/dist/transport/relay-logging.d.ts.map +1 -0
- package/package.json +2 -2
package/dist/server/index.js
CHANGED
|
@@ -9,12 +9,31 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
|
|
|
9
9
|
|
|
10
10
|
// src/server/types.ts
|
|
11
11
|
var UI_BRIDGE_NATIVE_ROUTES = {
|
|
12
|
+
// Discovery / introspection — the two endpoints an agent hits first to learn
|
|
13
|
+
// the rest of this table without reading docs. Special-cased in
|
|
14
|
+
// `routeRequest` rather than living in `WS_ROUTES`, and appended by
|
|
15
|
+
// `buildRoutesPayload` so `_routes` describes itself.
|
|
16
|
+
HELP: {
|
|
17
|
+
method: "GET",
|
|
18
|
+
path: "/ui-bridge/_help",
|
|
19
|
+
description: "Route index + action reference + tips (rich discovery payload)"
|
|
20
|
+
},
|
|
21
|
+
ROUTES: {
|
|
22
|
+
method: "GET",
|
|
23
|
+
path: "/ui-bridge/_routes",
|
|
24
|
+
description: "Minimal {method, path} route list, matching the runner /_routes shape"
|
|
25
|
+
},
|
|
12
26
|
// Control - Elements
|
|
13
27
|
GET_ELEMENTS: {
|
|
14
28
|
method: "GET",
|
|
15
29
|
path: "/ui-bridge/control/elements",
|
|
16
30
|
description: "List all registered elements"
|
|
17
31
|
},
|
|
32
|
+
GET_ELEMENTS_FLAT: {
|
|
33
|
+
method: "GET",
|
|
34
|
+
path: "/ui-bridge/elements",
|
|
35
|
+
description: "List all registered elements (flat alias matching the runner path)"
|
|
36
|
+
},
|
|
18
37
|
GET_ELEMENT: {
|
|
19
38
|
method: "GET",
|
|
20
39
|
path: "/ui-bridge/control/element/:id",
|
|
@@ -52,6 +71,11 @@ var UI_BRIDGE_NATIVE_ROUTES = {
|
|
|
52
71
|
path: "/ui-bridge/control/find",
|
|
53
72
|
description: "Find elements matching criteria"
|
|
54
73
|
},
|
|
74
|
+
AI_FIND: {
|
|
75
|
+
method: "POST",
|
|
76
|
+
path: "/ui-bridge/ai/find",
|
|
77
|
+
description: "Find elements matching criteria (runner-parity alias for /control/find)"
|
|
78
|
+
},
|
|
55
79
|
GET_SNAPSHOT: {
|
|
56
80
|
method: "GET",
|
|
57
81
|
path: "/ui-bridge/control/snapshot",
|
|
@@ -62,11 +86,78 @@ var UI_BRIDGE_NATIVE_ROUTES = {
|
|
|
62
86
|
path: "/ui-bridge/control/discover",
|
|
63
87
|
description: "Discover elements (alias for snapshot; web/runner parity)"
|
|
64
88
|
},
|
|
89
|
+
DISCOVER_POST: {
|
|
90
|
+
method: "POST",
|
|
91
|
+
path: "/ui-bridge/control/discover",
|
|
92
|
+
description: "Discover elements over POST (same handler as the GET form; web/runner parity)"
|
|
93
|
+
},
|
|
65
94
|
PAGE_HEALTH: {
|
|
66
95
|
method: "POST",
|
|
67
96
|
path: "/ui-bridge/control/page-health",
|
|
68
97
|
description: "Holistic page health diagnostic: spatial coverage, layout regions, text signals, interactive readiness, anomalies + ASCII heatmap"
|
|
69
98
|
},
|
|
99
|
+
PAGE_HEALTH_GET: {
|
|
100
|
+
method: "GET",
|
|
101
|
+
path: "/ui-bridge/control/page-health",
|
|
102
|
+
description: "Page health diagnostic over GET, for ad-hoc curls with no body"
|
|
103
|
+
},
|
|
104
|
+
// Screenshots — GET and POST both accepted so a curl works either way.
|
|
105
|
+
//
|
|
106
|
+
// Both keys carry an explicit verb suffix. The pre-existing multi-verb pairs
|
|
107
|
+
// do not agree on what an UNSUFFIXED key means — `DISCOVER` is the GET and
|
|
108
|
+
// `PAGE_HEALTH` is the POST — so a consumer reading `ROUTES.<NAME>.method`
|
|
109
|
+
// off a bare name gets opposite verbs from equally unmarked keys. Those two
|
|
110
|
+
// names are load-bearing for existing consumers and are left alone; new
|
|
111
|
+
// multi-verb entries name their verb rather than entrenching the ambiguity.
|
|
112
|
+
SCREENSHOT_GET: {
|
|
113
|
+
method: "GET",
|
|
114
|
+
path: "/ui-bridge/control/screenshot",
|
|
115
|
+
description: "PNG screenshot of the current screen"
|
|
116
|
+
},
|
|
117
|
+
SCREENSHOT_POST: {
|
|
118
|
+
method: "POST",
|
|
119
|
+
path: "/ui-bridge/control/screenshot",
|
|
120
|
+
description: "PNG screenshot of the current screen (POST form)"
|
|
121
|
+
},
|
|
122
|
+
// Coordinate-based interaction
|
|
123
|
+
CONTROL_TAP: {
|
|
124
|
+
method: "POST",
|
|
125
|
+
path: "/ui-bridge/control/tap",
|
|
126
|
+
description: "Synthesize a press at screen coords by matching registered element layout rects: body { x, y, action? }"
|
|
127
|
+
},
|
|
128
|
+
// App-agnostic interaction parity with the web/runner surface. The
|
|
129
|
+
// selector-based pair has no RN analog and answers NOT_SUPPORTED (501).
|
|
130
|
+
PAGE_CLICK_BY_TEXT: {
|
|
131
|
+
method: "POST",
|
|
132
|
+
path: "/ui-bridge/control/page/click-by-text",
|
|
133
|
+
description: "Press the first element matching free text: body { text, visibleOnly? }"
|
|
134
|
+
},
|
|
135
|
+
PAGE_CLICK_BY_SELECTOR: {
|
|
136
|
+
method: "POST",
|
|
137
|
+
path: "/ui-bridge/control/page/click-by-selector",
|
|
138
|
+
description: "Selector-based press (web-only; mobile returns NOT_SUPPORTED)"
|
|
139
|
+
},
|
|
140
|
+
PAGE_TYPE_INTO: {
|
|
141
|
+
method: "POST",
|
|
142
|
+
path: "/ui-bridge/control/page/type-into",
|
|
143
|
+
description: "Type into the input matching a label: body { label, text, clear?, visibleOnly? }"
|
|
144
|
+
},
|
|
145
|
+
PAGE_READ_VALUE: {
|
|
146
|
+
method: "POST",
|
|
147
|
+
path: "/ui-bridge/control/page/read-value",
|
|
148
|
+
description: "Selector-based value read (web-only; mobile returns NOT_SUPPORTED)"
|
|
149
|
+
},
|
|
150
|
+
PAGE_FIND_BY_TEXT: {
|
|
151
|
+
method: "POST",
|
|
152
|
+
path: "/ui-bridge/control/page/find-by-text",
|
|
153
|
+
description: "List elements matching free text without pressing: body { text, visibleOnly? }"
|
|
154
|
+
},
|
|
155
|
+
// Meta
|
|
156
|
+
META_METHODS: {
|
|
157
|
+
method: "GET",
|
|
158
|
+
path: "/ui-bridge/meta/methods",
|
|
159
|
+
description: "List every handler name (legacy introspection; prefer /_routes)"
|
|
160
|
+
},
|
|
70
161
|
// Workflows
|
|
71
162
|
GET_WORKFLOWS: {
|
|
72
163
|
method: "GET",
|
|
@@ -145,6 +236,27 @@ var UI_BRIDGE_NATIVE_ROUTES = {
|
|
|
145
236
|
path: "/ui-bridge/design/style-guide",
|
|
146
237
|
description: "Clear loaded style guide"
|
|
147
238
|
},
|
|
239
|
+
// Quality Evaluation
|
|
240
|
+
DESIGN_EVALUATE: {
|
|
241
|
+
method: "POST",
|
|
242
|
+
path: "/ui-bridge/design/evaluate",
|
|
243
|
+
description: "Score the current UI against a quality context"
|
|
244
|
+
},
|
|
245
|
+
DESIGN_EVALUATE_CONTEXTS: {
|
|
246
|
+
method: "GET",
|
|
247
|
+
path: "/ui-bridge/design/evaluate/contexts",
|
|
248
|
+
description: "List available quality evaluation contexts"
|
|
249
|
+
},
|
|
250
|
+
DESIGN_EVALUATE_BASELINE: {
|
|
251
|
+
method: "POST",
|
|
252
|
+
path: "/ui-bridge/design/evaluate/baseline",
|
|
253
|
+
description: "Save a quality baseline snapshot"
|
|
254
|
+
},
|
|
255
|
+
DESIGN_EVALUATE_DIFF: {
|
|
256
|
+
method: "POST",
|
|
257
|
+
path: "/ui-bridge/design/evaluate/diff",
|
|
258
|
+
description: "Diff the current UI against a saved baseline"
|
|
259
|
+
},
|
|
148
260
|
// AI helpers
|
|
149
261
|
AI_FILL_FORM: {
|
|
150
262
|
method: "POST",
|
|
@@ -176,18 +288,34 @@ var UI_BRIDGE_NATIVE_ROUTES = {
|
|
|
176
288
|
path: "/ui-bridge/ai/wait-for-element",
|
|
177
289
|
description: "Wait for element predicate via HTTP (runner-only; mobile returns NOT_SUPPORTED \u2014 use WS waitForElement instead)"
|
|
178
290
|
},
|
|
179
|
-
// Test hooks — drive ModalDetector state
|
|
180
|
-
// (gated by `features.testHooks`). Mirrors `pushModal` /
|
|
181
|
-
// calls that components normally make in-process.
|
|
291
|
+
// Test hooks — drive ModalDetector state and the screen lock from outside
|
|
292
|
+
// the React tree (gated by `features.testHooks`). Mirrors `pushModal` /
|
|
293
|
+
// `dismissModal` calls that components normally make in-process.
|
|
294
|
+
//
|
|
295
|
+
// These are LISTED unconditionally — this table is a static constant with no
|
|
296
|
+
// config to consult — but each carries `requiresTestHooks: true`, which is
|
|
297
|
+
// the machine-readable form of the `(testHooks)` in its description. With the
|
|
298
|
+
// flag off `routeRequest` and `buildRoutesPayload` both drop them and the
|
|
299
|
+
// route answers a bare 404. `route-table-parity.test.ts` compares the whole
|
|
300
|
+
// table against `_routes` at `testHooks: true`, and the flagged subset
|
|
301
|
+
// against `_routes` at `testHooks: false`, so both surfaces are pinned.
|
|
182
302
|
PUSH_MODAL: {
|
|
183
303
|
method: "POST",
|
|
184
304
|
path: "/ui-bridge/control/modal/push",
|
|
185
|
-
description: "Push a modal onto the modal stack (testHooks)"
|
|
305
|
+
description: "Push a modal onto the modal stack (testHooks)",
|
|
306
|
+
requiresTestHooks: true
|
|
186
307
|
},
|
|
187
308
|
DISMISS_MODAL: {
|
|
188
309
|
method: "POST",
|
|
189
310
|
path: "/ui-bridge/control/modal/dismiss/:id",
|
|
190
|
-
description: "Dismiss a modal by id (testHooks)"
|
|
311
|
+
description: "Dismiss a modal by id (testHooks)",
|
|
312
|
+
requiresTestHooks: true
|
|
313
|
+
},
|
|
314
|
+
KEEP_AWAKE: {
|
|
315
|
+
method: "POST",
|
|
316
|
+
path: "/ui-bridge/control/keep-awake",
|
|
317
|
+
description: "Keep the device screen awake via the injected keepAwakeProvider: body { enabled, durationMs? } (testHooks)",
|
|
318
|
+
requiresTestHooks: true
|
|
191
319
|
},
|
|
192
320
|
// Observability — last-N ring buffers (always mounted; `installed:false`
|
|
193
321
|
// when the console/network patches are off, i.e. no testHooks).
|
|
@@ -382,6 +510,21 @@ function projectVisionFields(args) {
|
|
|
382
510
|
return fields;
|
|
383
511
|
}
|
|
384
512
|
|
|
513
|
+
// src/core/element-actions.ts
|
|
514
|
+
function serializeElementCustomActions(customActions) {
|
|
515
|
+
if (!customActions) return void 0;
|
|
516
|
+
const out = [];
|
|
517
|
+
for (const [id, action] of Object.entries(customActions)) {
|
|
518
|
+
if (!action) continue;
|
|
519
|
+
const entry = { id };
|
|
520
|
+
if (action.label !== void 0) entry.label = action.label;
|
|
521
|
+
if (action.description !== void 0) entry.description = action.description;
|
|
522
|
+
if (action.effect !== void 0) entry.effect = action.effect;
|
|
523
|
+
out.push(entry);
|
|
524
|
+
}
|
|
525
|
+
return out;
|
|
526
|
+
}
|
|
527
|
+
|
|
385
528
|
// src/core/registry.ts
|
|
386
529
|
function extractHandlerNames(props) {
|
|
387
530
|
if (!props) return [];
|
|
@@ -410,6 +553,46 @@ function projectBbox(state, visibility, pixelRatio = 1) {
|
|
|
410
553
|
h: toPx(layout.height)
|
|
411
554
|
};
|
|
412
555
|
}
|
|
556
|
+
function pageRectOf(state) {
|
|
557
|
+
const layout = state.layout;
|
|
558
|
+
if (!layout) return null;
|
|
559
|
+
if (layout.pageOriginUnmeasured === true) return null;
|
|
560
|
+
const hasPage = typeof layout.pageX === "number" && typeof layout.pageY === "number" && Number.isFinite(layout.pageX) && Number.isFinite(layout.pageY);
|
|
561
|
+
if (!hasPage) return null;
|
|
562
|
+
const left = layout.pageX;
|
|
563
|
+
const top = layout.pageY;
|
|
564
|
+
if (typeof layout.width !== "number" || typeof layout.height !== "number" || !Number.isFinite(layout.width) || !Number.isFinite(layout.height)) {
|
|
565
|
+
return null;
|
|
566
|
+
}
|
|
567
|
+
return { left, top, right: left + layout.width, bottom: top + layout.height };
|
|
568
|
+
}
|
|
569
|
+
function spansOverlap(aMin, aMax, bMin, bMax) {
|
|
570
|
+
if (aMin === aMax) return aMin >= bMin && aMin <= bMax;
|
|
571
|
+
return aMin < bMax && aMax > bMin;
|
|
572
|
+
}
|
|
573
|
+
function isEmptyRect(rect) {
|
|
574
|
+
return rect.right <= rect.left || rect.bottom <= rect.top;
|
|
575
|
+
}
|
|
576
|
+
function computeVisibility(state, clip) {
|
|
577
|
+
if (!state.visible) {
|
|
578
|
+
return { visibility: "hidden", visibilityReason: "hidden" };
|
|
579
|
+
}
|
|
580
|
+
if (state.layout === null) {
|
|
581
|
+
return { visibility: "likely-visible", visibilityReason: "no-layout" };
|
|
582
|
+
}
|
|
583
|
+
if (!clip) {
|
|
584
|
+
return { visibility: "visible" };
|
|
585
|
+
}
|
|
586
|
+
const rect = pageRectOf(state);
|
|
587
|
+
if (!rect) {
|
|
588
|
+
return { visibility: "visible" };
|
|
589
|
+
}
|
|
590
|
+
if (isEmptyRect(clip)) {
|
|
591
|
+
return { visibility: "hidden", visibilityReason: "off-screen" };
|
|
592
|
+
}
|
|
593
|
+
const onScreen = spansOverlap(rect.left, rect.right, clip.left, clip.right) && spansOverlap(rect.top, rect.bottom, clip.top, clip.bottom);
|
|
594
|
+
return onScreen ? { visibility: "visible" } : { visibility: "hidden", visibilityReason: "off-screen" };
|
|
595
|
+
}
|
|
413
596
|
|
|
414
597
|
// src/design/design-inspector-native.ts
|
|
415
598
|
function toCssValue(value) {
|
|
@@ -1357,16 +1540,21 @@ function createServerHandlers(registry, executor, config) {
|
|
|
1357
1540
|
return {
|
|
1358
1541
|
// Elements
|
|
1359
1542
|
getElements: async (ctx) => {
|
|
1543
|
+
await registry.refreshMeasurements();
|
|
1360
1544
|
const visibleOnly = ctx.query?.visibleOnly === "true" || ctx.body?.visibleOnly === true;
|
|
1361
1545
|
const forRoute = ctx.query?.route || ctx.body?.route;
|
|
1362
1546
|
let allElements = visibleOnly ? registry.getMountedVisibleElements() : registry.getAllElements();
|
|
1363
1547
|
if (forRoute) {
|
|
1364
1548
|
allElements = allElements.filter((e) => matchesCurrentRoute(e.registrationRoute, forRoute));
|
|
1365
1549
|
}
|
|
1550
|
+
const viewport = registry.getViewportRect();
|
|
1366
1551
|
const elements = allElements.map((e) => {
|
|
1367
1552
|
const handlers = extractHandlerNames(e.props);
|
|
1368
1553
|
const state = e.getState();
|
|
1369
|
-
const visibility
|
|
1554
|
+
const { visibility, visibilityReason } = computeVisibility(
|
|
1555
|
+
state,
|
|
1556
|
+
registry.getClipRectFor(e, viewport)
|
|
1557
|
+
);
|
|
1370
1558
|
const bbox = projectBbox(state, visibility, registry.getPixelRatio());
|
|
1371
1559
|
const vision = projectVisionFields({
|
|
1372
1560
|
type: e.type,
|
|
@@ -1384,10 +1572,11 @@ function createServerHandlers(registry, executor, config) {
|
|
|
1384
1572
|
identifier: e.getIdentifier(),
|
|
1385
1573
|
state,
|
|
1386
1574
|
actions: e.actions,
|
|
1387
|
-
customActions:
|
|
1575
|
+
customActions: serializeElementCustomActions(e.customActions),
|
|
1388
1576
|
registeredHandlers: handlers.length > 0 ? handlers : void 0,
|
|
1389
1577
|
registrationRoute: e.registrationRoute,
|
|
1390
1578
|
visibility,
|
|
1579
|
+
...visibilityReason ? { visibilityReason } : {},
|
|
1391
1580
|
...bbox ? { bbox } : {},
|
|
1392
1581
|
...vision
|
|
1393
1582
|
};
|
|
@@ -1409,7 +1598,7 @@ function createServerHandlers(registry, executor, config) {
|
|
|
1409
1598
|
identifier: element.getIdentifier(),
|
|
1410
1599
|
state: resolveElementState(element),
|
|
1411
1600
|
actions: element.actions,
|
|
1412
|
-
customActions:
|
|
1601
|
+
customActions: serializeElementCustomActions(element.customActions),
|
|
1413
1602
|
registeredHandlers: handlers.length > 0 ? handlers : void 0,
|
|
1414
1603
|
registrationRoute: element.registrationRoute
|
|
1415
1604
|
}
|
|
@@ -1430,13 +1619,9 @@ function createServerHandlers(registry, executor, config) {
|
|
|
1430
1619
|
if (!envelope?.action) {
|
|
1431
1620
|
return error2("Action is required", "INVALID_REQUEST");
|
|
1432
1621
|
}
|
|
1433
|
-
const response = await executor.executeAction(id,
|
|
1434
|
-
action: envelope.action,
|
|
1435
|
-
params: envelope.params,
|
|
1436
|
-
waitOptions: envelope.waitOptions
|
|
1437
|
-
});
|
|
1622
|
+
const response = await executor.executeAction(id, envelope);
|
|
1438
1623
|
if (!response.success) {
|
|
1439
|
-
return error2(response.error || "Action failed", "ACTION_FAILED");
|
|
1624
|
+
return error2(response.error || "Action failed", response.code ?? "ACTION_FAILED");
|
|
1440
1625
|
}
|
|
1441
1626
|
return success2(response);
|
|
1442
1627
|
},
|
|
@@ -1446,7 +1631,11 @@ function createServerHandlers(registry, executor, config) {
|
|
|
1446
1631
|
id: c.id,
|
|
1447
1632
|
name: c.name,
|
|
1448
1633
|
description: c.description,
|
|
1449
|
-
|
|
1634
|
+
// Phase 4 (plan 2026-08-20-ui-bridge-action-declaration-shape):
|
|
1635
|
+
// carry the safety annotation onto the native wire too — a walker
|
|
1636
|
+
// driving a React Native app needs the same `destructive` exclusion
|
|
1637
|
+
// the web channel gets.
|
|
1638
|
+
actions: c.actions.map((a) => ({ id: a.id, label: a.label, effect: a.effect })),
|
|
1450
1639
|
elementIds: c.elementIds
|
|
1451
1640
|
}));
|
|
1452
1641
|
return success2({ components });
|
|
@@ -1465,7 +1654,9 @@ function createServerHandlers(registry, executor, config) {
|
|
|
1465
1654
|
actions: component.actions.map((a) => ({
|
|
1466
1655
|
id: a.id,
|
|
1467
1656
|
label: a.label,
|
|
1468
|
-
description: a.description
|
|
1657
|
+
description: a.description,
|
|
1658
|
+
// Phase 4 — see the listing twin above.
|
|
1659
|
+
effect: a.effect
|
|
1469
1660
|
})),
|
|
1470
1661
|
elementIds: component.elementIds
|
|
1471
1662
|
}
|
|
@@ -1476,7 +1667,8 @@ function createServerHandlers(registry, executor, config) {
|
|
|
1476
1667
|
const body = ctx.body;
|
|
1477
1668
|
const response = await executor.executeComponentAction(id, {
|
|
1478
1669
|
action: actionId,
|
|
1479
|
-
params: body?.params
|
|
1670
|
+
params: body?.params,
|
|
1671
|
+
timeoutMs: body?.timeoutMs
|
|
1480
1672
|
});
|
|
1481
1673
|
if (!response.success) {
|
|
1482
1674
|
return error2(response.error || "Action failed", "ACTION_FAILED");
|
|
@@ -1550,6 +1742,7 @@ function createServerHandlers(registry, executor, config) {
|
|
|
1550
1742
|
});
|
|
1551
1743
|
},
|
|
1552
1744
|
getSnapshot: async (ctx) => {
|
|
1745
|
+
await registry.refreshMeasurements();
|
|
1553
1746
|
const visibleOnly = ctx.query?.visibleOnly === "true" || ctx.body?.visibleOnly === true;
|
|
1554
1747
|
const currentRouteOnly = ctx.query?.currentRouteOnly === "true" || ctx.body?.currentRouteOnly === true;
|
|
1555
1748
|
const snapshot = registry.createSnapshot(void 0, { visibleOnly, currentRouteOnly });
|
|
@@ -1667,9 +1860,12 @@ function createServerHandlers(registry, executor, config) {
|
|
|
1667
1860
|
//
|
|
1668
1861
|
// The mobile bridge mounts explicit NOT_SUPPORTED stubs at the runner's
|
|
1669
1862
|
// cheatsheet paths so callers (and the /manual-test skill in particular)
|
|
1670
|
-
// get a structured envelope explaining the gap rather than a
|
|
1671
|
-
//
|
|
1672
|
-
//
|
|
1863
|
+
// get a structured envelope explaining the gap rather than a bare
|
|
1864
|
+
// "no such route". These answer **HTTP 501** — `httpStatusForResponse` in
|
|
1865
|
+
// server/http-server.ts maps NOT_SUPPORTED → 501, so the status line and
|
|
1866
|
+
// the envelope agree; an unmounted path answers 404 instead. Each error
|
|
1867
|
+
// message names an in-tree mobile replacement so operators aren't left
|
|
1868
|
+
// guessing what to use instead.
|
|
1673
1869
|
aiForms: async () => {
|
|
1674
1870
|
return error2(
|
|
1675
1871
|
'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.',
|
|
@@ -1865,6 +2061,7 @@ function createServerHandlers(registry, executor, config) {
|
|
|
1865
2061
|
// that don't measure (the fallback only matches single-rooted screens but
|
|
1866
2062
|
// is good enough for the unit suite).
|
|
1867
2063
|
tapAt: async (ctx) => {
|
|
2064
|
+
await registry.refreshMeasurements();
|
|
1868
2065
|
const body = ctx.body;
|
|
1869
2066
|
const x = body?.x;
|
|
1870
2067
|
const y = body?.y;
|
|
@@ -1995,18 +2192,12 @@ function createServerHandlers(registry, executor, config) {
|
|
|
1995
2192
|
pushModal: async (ctx) => {
|
|
1996
2193
|
const detector = registry.getEnrichers().modalDetector;
|
|
1997
2194
|
if (!detector) {
|
|
1998
|
-
return error2(
|
|
1999
|
-
"modal detector not registered",
|
|
2000
|
-
"ENRICHER_UNAVAILABLE"
|
|
2001
|
-
);
|
|
2195
|
+
return error2("modal detector not registered", "ENRICHER_UNAVAILABLE");
|
|
2002
2196
|
}
|
|
2003
2197
|
const body = ctx.body;
|
|
2004
2198
|
const id = body?.id;
|
|
2005
2199
|
if (typeof id !== "string" || id.length === 0) {
|
|
2006
|
-
return error2(
|
|
2007
|
-
'pushModal requires a non-empty "id"',
|
|
2008
|
-
"INVALID_REQUEST"
|
|
2009
|
-
);
|
|
2200
|
+
return error2('pushModal requires a non-empty "id"', "INVALID_REQUEST");
|
|
2010
2201
|
}
|
|
2011
2202
|
const allowedTypes = ["modal", "sheet", "drawer", "popover", "alertdialog", "dialog"];
|
|
2012
2203
|
const requestedType = body?.type;
|
|
@@ -2039,10 +2230,7 @@ function createServerHandlers(registry, executor, config) {
|
|
|
2039
2230
|
dismissModal: async (ctx) => {
|
|
2040
2231
|
const detector = registry.getEnrichers().modalDetector;
|
|
2041
2232
|
if (!detector) {
|
|
2042
|
-
return error2(
|
|
2043
|
-
"modal detector not registered",
|
|
2044
|
-
"ENRICHER_UNAVAILABLE"
|
|
2045
|
-
);
|
|
2233
|
+
return error2("modal detector not registered", "ENRICHER_UNAVAILABLE");
|
|
2046
2234
|
}
|
|
2047
2235
|
const { id } = ctx.params;
|
|
2048
2236
|
if (typeof id !== "string" || id.length === 0) {
|
|
@@ -2072,7 +2260,7 @@ function createServerHandlers(registry, executor, config) {
|
|
|
2072
2260
|
};
|
|
2073
2261
|
if (config?.appInfo) {
|
|
2074
2262
|
response.uiBridge = {
|
|
2075
|
-
version: "0.6.
|
|
2263
|
+
version: "0.6.13",
|
|
2076
2264
|
...config.appInfo,
|
|
2077
2265
|
capabilities: ["elements", "components", "actions", "design"],
|
|
2078
2266
|
elementCount: stats.elements,
|
|
@@ -2220,8 +2408,48 @@ function extractMethod(input, init) {
|
|
|
2220
2408
|
var NetworkRequestBuffer = class {
|
|
2221
2409
|
constructor(capacity = 100) {
|
|
2222
2410
|
this.installed = false;
|
|
2411
|
+
/**
|
|
2412
|
+
* Synchronous re-entrancy depth: non-zero while the ORIGINAL `fetch` is
|
|
2413
|
+
* executing, which is exactly the window in which React Native's whatwg-fetch
|
|
2414
|
+
* polyfill constructs and opens its internal `XMLHttpRequest`.
|
|
2415
|
+
*
|
|
2416
|
+
* A counter rather than a boolean so a `fetch` called from inside a `fetch`
|
|
2417
|
+
* (an interceptor, a retry wrapper) still unwinds to zero. Safe as a plain
|
|
2418
|
+
* number: JS is single-threaded and the only writes are the paired
|
|
2419
|
+
* increment/decrement around one synchronous call.
|
|
2420
|
+
*/
|
|
2421
|
+
this.fetchDepth = { value: 0 };
|
|
2223
2422
|
this.buffer = new RingBuffer(capacity);
|
|
2224
2423
|
}
|
|
2424
|
+
/**
|
|
2425
|
+
* Patch `fetch` and `XMLHttpRequest` so `GET /sdk/network-requests` can
|
|
2426
|
+
* report what the app talked to.
|
|
2427
|
+
*
|
|
2428
|
+
* ONE REQUEST MUST PRODUCE ONE ENTRY. React Native implements `fetch` **on
|
|
2429
|
+
* top of** XHR (whatwg-fetch), so both patches observe the same request and
|
|
2430
|
+
* the buffer used to carry every call twice — same url, timestamps ~1ms
|
|
2431
|
+
* apart, and on failure one `"Network request failed"` beside one
|
|
2432
|
+
* `"XMLHttpRequest error"`. A tester reads that as a double-fetch bug in the
|
|
2433
|
+
* app and chases a defect that does not exist.
|
|
2434
|
+
*
|
|
2435
|
+
* The de-dup: `fetchDepth` is raised across the synchronous call into the
|
|
2436
|
+
* original `fetch`, which is when the polyfill builds its XHR; `open` stamps
|
|
2437
|
+
* any XHR created in that window with `__uiBridgeFromFetch`, and `send`
|
|
2438
|
+
* skips recording those. The `fetch` patch keeps the entry, because it is
|
|
2439
|
+
* the layer the app actually called and the only one that sees `Response.ok`.
|
|
2440
|
+
*
|
|
2441
|
+
* Uninstalling both patches together keeps this sound: an XHR can only be
|
|
2442
|
+
* marked while the fetch patch is live.
|
|
2443
|
+
*
|
|
2444
|
+
* KNOWN LIMIT — it is coupled to the polyfill building its XHR SYNCHRONOUSLY,
|
|
2445
|
+
* which whatwg-fetch does (its work happens in the Promise executor). If the
|
|
2446
|
+
* host's `fetch` is wrapped by an interceptor that defers the real call to a
|
|
2447
|
+
* microtask or `setTimeout`, the depth is back to 0 by the time `open` runs
|
|
2448
|
+
* and every request duplicates again — the original symptom, with no signal
|
|
2449
|
+
* that the mechanism disengaged. Duplicates in `/sdk/network-requests` are
|
|
2450
|
+
* the thing to look for; they mean this assumption stopped holding, not that
|
|
2451
|
+
* the app double-fetched.
|
|
2452
|
+
*/
|
|
2225
2453
|
install() {
|
|
2226
2454
|
if (this.installed) return;
|
|
2227
2455
|
const g = globalThis;
|
|
@@ -2229,12 +2457,20 @@ var NetworkRequestBuffer = class {
|
|
|
2229
2457
|
this.originalFetch = g.fetch;
|
|
2230
2458
|
const buffer = this.buffer;
|
|
2231
2459
|
const original = this.originalFetch;
|
|
2460
|
+
const fetchDepth = this.fetchDepth;
|
|
2232
2461
|
const patched = async (input, init) => {
|
|
2233
2462
|
const start = Date.now();
|
|
2234
2463
|
const url = extractUrl(input);
|
|
2235
2464
|
const method = extractMethod(input, init);
|
|
2236
2465
|
try {
|
|
2237
|
-
|
|
2466
|
+
let pending;
|
|
2467
|
+
fetchDepth.value += 1;
|
|
2468
|
+
try {
|
|
2469
|
+
pending = original.call(globalThis, input, init);
|
|
2470
|
+
} finally {
|
|
2471
|
+
fetchDepth.value -= 1;
|
|
2472
|
+
}
|
|
2473
|
+
const res = await pending;
|
|
2238
2474
|
buffer.push({
|
|
2239
2475
|
timestamp: start,
|
|
2240
2476
|
method,
|
|
@@ -2264,10 +2500,14 @@ var NetworkRequestBuffer = class {
|
|
|
2264
2500
|
this.originalXhrOpen = Xhr.prototype.open;
|
|
2265
2501
|
this.originalXhrSend = Xhr.prototype.send;
|
|
2266
2502
|
const buffer = this.buffer;
|
|
2503
|
+
const fetchDepth = this.fetchDepth;
|
|
2267
2504
|
const origOpen = this.originalXhrOpen;
|
|
2268
2505
|
Xhr.prototype.open = function(method, url, ...rest) {
|
|
2269
2506
|
this.__uiBridgeMethod = method.toUpperCase();
|
|
2270
2507
|
this.__uiBridgeUrl = typeof url === "string" ? url : String(url);
|
|
2508
|
+
if (fetchDepth.value > 0) {
|
|
2509
|
+
this.__uiBridgeFromFetch = true;
|
|
2510
|
+
}
|
|
2271
2511
|
return origOpen.call(
|
|
2272
2512
|
this,
|
|
2273
2513
|
method,
|
|
@@ -2277,6 +2517,9 @@ var NetworkRequestBuffer = class {
|
|
|
2277
2517
|
};
|
|
2278
2518
|
const origSend = this.originalXhrSend;
|
|
2279
2519
|
Xhr.prototype.send = function(body) {
|
|
2520
|
+
if (this.__uiBridgeFromFetch === true) {
|
|
2521
|
+
return origSend.call(this, body);
|
|
2522
|
+
}
|
|
2280
2523
|
const start = Date.now();
|
|
2281
2524
|
const method = this.__uiBridgeMethod ?? "GET";
|
|
2282
2525
|
const url = this.__uiBridgeUrl ?? "";
|
|
@@ -2356,6 +2599,16 @@ function isUnsubscribe(msg) {
|
|
|
2356
2599
|
}
|
|
2357
2600
|
|
|
2358
2601
|
// src/server/http-server.ts
|
|
2602
|
+
var ERROR_CODE_HTTP_STATUS = /* @__PURE__ */ new Map([
|
|
2603
|
+
["NOT_SUPPORTED", 501],
|
|
2604
|
+
["NOT_FOUND", 404],
|
|
2605
|
+
["METHOD_NOT_ALLOWED", 405]
|
|
2606
|
+
]);
|
|
2607
|
+
function httpStatusForResponse(response) {
|
|
2608
|
+
if (response.success) return 200;
|
|
2609
|
+
const mapped = response.code ? ERROR_CODE_HTTP_STATUS.get(response.code) : void 0;
|
|
2610
|
+
return mapped ?? 400;
|
|
2611
|
+
}
|
|
2359
2612
|
function parsePath(pattern, actual) {
|
|
2360
2613
|
const patternParts = pattern.split("/");
|
|
2361
2614
|
const actualParts = actual.split("/");
|
|
@@ -2518,6 +2771,23 @@ var WS_ROUTES = [
|
|
|
2518
2771
|
httpMethods: ["GET"]
|
|
2519
2772
|
}
|
|
2520
2773
|
];
|
|
2774
|
+
var DISCOVERY_PATHS = /* @__PURE__ */ new Set(["_help", "_routes"]);
|
|
2775
|
+
function allowedMethodsForPath(path, testHooks) {
|
|
2776
|
+
const PREFIX = "/ui-bridge/";
|
|
2777
|
+
if (!path.startsWith(PREFIX)) return [];
|
|
2778
|
+
const stripped = path.slice(PREFIX.length);
|
|
2779
|
+
const methods = [];
|
|
2780
|
+
if (DISCOVERY_PATHS.has(stripped)) methods.push("GET");
|
|
2781
|
+
for (const route of WS_ROUTES) {
|
|
2782
|
+
if (!route.httpMethods) continue;
|
|
2783
|
+
if (route.requiresTestHooks && !testHooks) continue;
|
|
2784
|
+
if (parsePath(route.pattern, stripped) === null) continue;
|
|
2785
|
+
for (const verb of route.httpMethods) {
|
|
2786
|
+
if (!methods.includes(verb)) methods.push(verb);
|
|
2787
|
+
}
|
|
2788
|
+
}
|
|
2789
|
+
return methods;
|
|
2790
|
+
}
|
|
2521
2791
|
var HANDLER_DESCRIPTIONS = {
|
|
2522
2792
|
health: "Liveness probe; returns app metadata + element count",
|
|
2523
2793
|
getElements: "Flat list of registered UI elements",
|
|
@@ -2580,6 +2850,10 @@ var ACTION_REFERENCE = [
|
|
|
2580
2850
|
{ name: "focus", params: "none" },
|
|
2581
2851
|
{ name: "blur", params: "none" },
|
|
2582
2852
|
{ name: "scroll", params: "ScrollActionParams { offset? {x,y}, to? }" },
|
|
2853
|
+
{
|
|
2854
|
+
name: "scrollIntoView",
|
|
2855
|
+
params: "ScrollIntoViewActionParams { padding? (dp, default 16) } \u2014 scrolls the element's DECLARED scrollAncestorId ScrollView; returns { alreadyVisible, scrolled }; NOT_SUPPORTED (501) without a usable declared ancestor"
|
|
2856
|
+
},
|
|
2583
2857
|
{ name: "swipe", params: "SwipeActionParams { direction, distance }" },
|
|
2584
2858
|
{ name: "toggle", params: "none \u2014 flips boolean controls" }
|
|
2585
2859
|
];
|
|
@@ -2777,7 +3051,9 @@ var NativeUIBridgeServer = class {
|
|
|
2777
3051
|
}
|
|
2778
3052
|
/**
|
|
2779
3053
|
* Set a route provider for reporting the current navigation route.
|
|
2780
|
-
* When set, `control/snapshot` responses include `currentRoute` and
|
|
3054
|
+
* When set, `control/snapshot` responses include `currentRoute` and
|
|
3055
|
+
* `segments`, plus the canonical `route` / `activeTab` aliases the web SDK's
|
|
3056
|
+
* `BridgeSnapshot` spells the same way.
|
|
2781
3057
|
*
|
|
2782
3058
|
* Also forwards the provider into the registry so the *default*
|
|
2783
3059
|
* `getSnapshot` handler (and any other call site of
|
|
@@ -2792,7 +3068,12 @@ var NativeUIBridgeServer = class {
|
|
|
2792
3068
|
this.handlers.getElements = async (ctx) => {
|
|
2793
3069
|
const currentRouteOnly = ctx.query?.currentRouteOnly === "true" || ctx.body?.currentRouteOnly === true;
|
|
2794
3070
|
if (currentRouteOnly && !ctx.query?.route && !ctx.body?.route) {
|
|
2795
|
-
|
|
3071
|
+
let currentRoute;
|
|
3072
|
+
try {
|
|
3073
|
+
currentRoute = provider.getCurrentRoute();
|
|
3074
|
+
} catch {
|
|
3075
|
+
currentRoute = null;
|
|
3076
|
+
}
|
|
2796
3077
|
if (currentRoute) {
|
|
2797
3078
|
ctx.query = { ...ctx.query, route: currentRoute };
|
|
2798
3079
|
}
|
|
@@ -2802,11 +3083,26 @@ var NativeUIBridgeServer = class {
|
|
|
2802
3083
|
this.handlers.getSnapshot = async (ctx) => {
|
|
2803
3084
|
const visibleOnly = ctx?.query?.visibleOnly === "true" || ctx?.body?.visibleOnly === true;
|
|
2804
3085
|
const currentRouteOnly = ctx?.query?.currentRouteOnly === "true" || ctx?.body?.currentRouteOnly === true;
|
|
3086
|
+
let currentRoute;
|
|
3087
|
+
try {
|
|
3088
|
+
currentRoute = provider.getCurrentRoute() ?? null;
|
|
3089
|
+
} catch {
|
|
3090
|
+
currentRoute = null;
|
|
3091
|
+
}
|
|
3092
|
+
let segments;
|
|
3093
|
+
try {
|
|
3094
|
+
segments = provider.getSegments?.();
|
|
3095
|
+
} catch {
|
|
3096
|
+
segments = void 0;
|
|
3097
|
+
}
|
|
3098
|
+
let activeTab;
|
|
3099
|
+
try {
|
|
3100
|
+
activeTab = provider.getActiveTab?.();
|
|
3101
|
+
} catch {
|
|
3102
|
+
activeTab = void 0;
|
|
3103
|
+
}
|
|
2805
3104
|
const snapshot = this.registry.createSnapshot(
|
|
2806
|
-
{
|
|
2807
|
-
currentRoute: provider.getCurrentRoute(),
|
|
2808
|
-
segments: provider.getSegments?.()
|
|
2809
|
-
},
|
|
3105
|
+
{ currentRoute, segments, activeTab },
|
|
2810
3106
|
{ visibleOnly, currentRouteOnly }
|
|
2811
3107
|
);
|
|
2812
3108
|
return { success: true, data: snapshot, timestamp: Date.now() };
|
|
@@ -3472,8 +3768,15 @@ var NativeUIBridgeServer = class {
|
|
|
3472
3768
|
}
|
|
3473
3769
|
try {
|
|
3474
3770
|
const response = await this.routeRequest(request);
|
|
3771
|
+
if (response.code === "METHOD_NOT_ALLOWED") {
|
|
3772
|
+
const allow = allowedMethodsForPath(request.path, this.config.testHooks === true);
|
|
3773
|
+
if (allow.length > 0) {
|
|
3774
|
+
headers["Allow"] = allow.join(", ");
|
|
3775
|
+
if (this.config.cors) headers["Access-Control-Expose-Headers"] = "Allow";
|
|
3776
|
+
}
|
|
3777
|
+
}
|
|
3475
3778
|
return {
|
|
3476
|
-
status: response
|
|
3779
|
+
status: httpStatusForResponse(response),
|
|
3477
3780
|
headers,
|
|
3478
3781
|
body: JSON.stringify(response)
|
|
3479
3782
|
};
|
|
@@ -3530,7 +3833,7 @@ var NativeUIBridgeServer = class {
|
|
|
3530
3833
|
timestamp: Date.now()
|
|
3531
3834
|
};
|
|
3532
3835
|
}
|
|
3533
|
-
let patternMatchedButWrongVerb =
|
|
3836
|
+
let patternMatchedButWrongVerb = DISCOVERY_PATHS.has(stripped);
|
|
3534
3837
|
for (const route of WS_ROUTES) {
|
|
3535
3838
|
const pathParams = parsePath(route.pattern, stripped);
|
|
3536
3839
|
if (!pathParams) continue;
|
|
@@ -3547,9 +3850,10 @@ var NativeUIBridgeServer = class {
|
|
|
3547
3850
|
const ctx = { params: pathParams, query, body };
|
|
3548
3851
|
return this.handlers[route.handler](ctx);
|
|
3549
3852
|
}
|
|
3853
|
+
const allowed = patternMatchedButWrongVerb ? allowedMethodsForPath(path, testHooks) : [];
|
|
3550
3854
|
return {
|
|
3551
3855
|
success: false,
|
|
3552
|
-
error: patternMatchedButWrongVerb ? `Method not allowed: ${method} ${path}` : `Route not found: ${method} ${path}`,
|
|
3856
|
+
error: patternMatchedButWrongVerb ? `Method not allowed: ${method} ${path}${allowed.length > 0 ? ` (allowed: ${allowed.join(", ")})` : ""}` : `Route not found: ${method} ${path}`,
|
|
3553
3857
|
code: patternMatchedButWrongVerb ? "METHOD_NOT_ALLOWED" : "NOT_FOUND",
|
|
3554
3858
|
timestamp: Date.now()
|
|
3555
3859
|
};
|
|
@@ -4270,6 +4574,7 @@ exports.encodeCloseFrame = encodeCloseFrame;
|
|
|
4270
4574
|
exports.encodePingFrame = encodePingFrame;
|
|
4271
4575
|
exports.encodePongFrame = encodePongFrame;
|
|
4272
4576
|
exports.encodeTextFrame = encodeTextFrame;
|
|
4577
|
+
exports.httpStatusForResponse = httpStatusForResponse;
|
|
4273
4578
|
exports.isAddrInUseError = isAddrInUseError;
|
|
4274
4579
|
exports.isBatchRequest = isBatchRequest;
|
|
4275
4580
|
exports.isSubscribe = isSubscribe;
|