@qontinui/ui-bridge-native 0.6.12 → 0.6.14
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 +756 -15
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +738 -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 +272 -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 +1568 -135
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1550 -136
- package/dist/index.mjs.map +1 -1
- package/dist/react/UIBridgeNativeProvider.d.ts +18 -24
- package/dist/react/UIBridgeNativeProvider.d.ts.map +1 -1
- package/dist/react/index.js +1385 -130
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +1385 -130
- 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 +345 -43
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +345 -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 +35 -7
- package/dist/transport/DeviceAnnouncer.d.ts.map +1 -1
- package/dist/transport/index.d.ts +1 -1
- package/dist/transport/index.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) {
|
|
@@ -1364,10 +1547,14 @@ function createServerHandlers(registry, executor, config) {
|
|
|
1364
1547
|
if (forRoute) {
|
|
1365
1548
|
allElements = allElements.filter((e) => matchesCurrentRoute(e.registrationRoute, forRoute));
|
|
1366
1549
|
}
|
|
1550
|
+
const viewport = registry.getViewportRect();
|
|
1367
1551
|
const elements = allElements.map((e) => {
|
|
1368
1552
|
const handlers = extractHandlerNames(e.props);
|
|
1369
1553
|
const state = e.getState();
|
|
1370
|
-
const visibility
|
|
1554
|
+
const { visibility, visibilityReason } = computeVisibility(
|
|
1555
|
+
state,
|
|
1556
|
+
registry.getClipRectFor(e, viewport)
|
|
1557
|
+
);
|
|
1371
1558
|
const bbox = projectBbox(state, visibility, registry.getPixelRatio());
|
|
1372
1559
|
const vision = projectVisionFields({
|
|
1373
1560
|
type: e.type,
|
|
@@ -1385,10 +1572,11 @@ function createServerHandlers(registry, executor, config) {
|
|
|
1385
1572
|
identifier: e.getIdentifier(),
|
|
1386
1573
|
state,
|
|
1387
1574
|
actions: e.actions,
|
|
1388
|
-
customActions:
|
|
1575
|
+
customActions: serializeElementCustomActions(e.customActions),
|
|
1389
1576
|
registeredHandlers: handlers.length > 0 ? handlers : void 0,
|
|
1390
1577
|
registrationRoute: e.registrationRoute,
|
|
1391
1578
|
visibility,
|
|
1579
|
+
...visibilityReason ? { visibilityReason } : {},
|
|
1392
1580
|
...bbox ? { bbox } : {},
|
|
1393
1581
|
...vision
|
|
1394
1582
|
};
|
|
@@ -1410,7 +1598,7 @@ function createServerHandlers(registry, executor, config) {
|
|
|
1410
1598
|
identifier: element.getIdentifier(),
|
|
1411
1599
|
state: resolveElementState(element),
|
|
1412
1600
|
actions: element.actions,
|
|
1413
|
-
customActions:
|
|
1601
|
+
customActions: serializeElementCustomActions(element.customActions),
|
|
1414
1602
|
registeredHandlers: handlers.length > 0 ? handlers : void 0,
|
|
1415
1603
|
registrationRoute: element.registrationRoute
|
|
1416
1604
|
}
|
|
@@ -1431,13 +1619,9 @@ function createServerHandlers(registry, executor, config) {
|
|
|
1431
1619
|
if (!envelope?.action) {
|
|
1432
1620
|
return error2("Action is required", "INVALID_REQUEST");
|
|
1433
1621
|
}
|
|
1434
|
-
const response = await executor.executeAction(id,
|
|
1435
|
-
action: envelope.action,
|
|
1436
|
-
params: envelope.params,
|
|
1437
|
-
waitOptions: envelope.waitOptions
|
|
1438
|
-
});
|
|
1622
|
+
const response = await executor.executeAction(id, envelope);
|
|
1439
1623
|
if (!response.success) {
|
|
1440
|
-
return error2(response.error || "Action failed", "ACTION_FAILED");
|
|
1624
|
+
return error2(response.error || "Action failed", response.code ?? "ACTION_FAILED");
|
|
1441
1625
|
}
|
|
1442
1626
|
return success2(response);
|
|
1443
1627
|
},
|
|
@@ -1447,7 +1631,11 @@ function createServerHandlers(registry, executor, config) {
|
|
|
1447
1631
|
id: c.id,
|
|
1448
1632
|
name: c.name,
|
|
1449
1633
|
description: c.description,
|
|
1450
|
-
|
|
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 })),
|
|
1451
1639
|
elementIds: c.elementIds
|
|
1452
1640
|
}));
|
|
1453
1641
|
return success2({ components });
|
|
@@ -1466,7 +1654,9 @@ function createServerHandlers(registry, executor, config) {
|
|
|
1466
1654
|
actions: component.actions.map((a) => ({
|
|
1467
1655
|
id: a.id,
|
|
1468
1656
|
label: a.label,
|
|
1469
|
-
description: a.description
|
|
1657
|
+
description: a.description,
|
|
1658
|
+
// Phase 4 — see the listing twin above.
|
|
1659
|
+
effect: a.effect
|
|
1470
1660
|
})),
|
|
1471
1661
|
elementIds: component.elementIds
|
|
1472
1662
|
}
|
|
@@ -1477,7 +1667,8 @@ function createServerHandlers(registry, executor, config) {
|
|
|
1477
1667
|
const body = ctx.body;
|
|
1478
1668
|
const response = await executor.executeComponentAction(id, {
|
|
1479
1669
|
action: actionId,
|
|
1480
|
-
params: body?.params
|
|
1670
|
+
params: body?.params,
|
|
1671
|
+
timeoutMs: body?.timeoutMs
|
|
1481
1672
|
});
|
|
1482
1673
|
if (!response.success) {
|
|
1483
1674
|
return error2(response.error || "Action failed", "ACTION_FAILED");
|
|
@@ -1669,9 +1860,12 @@ function createServerHandlers(registry, executor, config) {
|
|
|
1669
1860
|
//
|
|
1670
1861
|
// The mobile bridge mounts explicit NOT_SUPPORTED stubs at the runner's
|
|
1671
1862
|
// cheatsheet paths so callers (and the /manual-test skill in particular)
|
|
1672
|
-
// get a structured envelope explaining the gap rather than a
|
|
1673
|
-
//
|
|
1674
|
-
//
|
|
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.
|
|
1675
1869
|
aiForms: async () => {
|
|
1676
1870
|
return error2(
|
|
1677
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.',
|
|
@@ -1998,18 +2192,12 @@ function createServerHandlers(registry, executor, config) {
|
|
|
1998
2192
|
pushModal: async (ctx) => {
|
|
1999
2193
|
const detector = registry.getEnrichers().modalDetector;
|
|
2000
2194
|
if (!detector) {
|
|
2001
|
-
return error2(
|
|
2002
|
-
"modal detector not registered",
|
|
2003
|
-
"ENRICHER_UNAVAILABLE"
|
|
2004
|
-
);
|
|
2195
|
+
return error2("modal detector not registered", "ENRICHER_UNAVAILABLE");
|
|
2005
2196
|
}
|
|
2006
2197
|
const body = ctx.body;
|
|
2007
2198
|
const id = body?.id;
|
|
2008
2199
|
if (typeof id !== "string" || id.length === 0) {
|
|
2009
|
-
return error2(
|
|
2010
|
-
'pushModal requires a non-empty "id"',
|
|
2011
|
-
"INVALID_REQUEST"
|
|
2012
|
-
);
|
|
2200
|
+
return error2('pushModal requires a non-empty "id"', "INVALID_REQUEST");
|
|
2013
2201
|
}
|
|
2014
2202
|
const allowedTypes = ["modal", "sheet", "drawer", "popover", "alertdialog", "dialog"];
|
|
2015
2203
|
const requestedType = body?.type;
|
|
@@ -2042,10 +2230,7 @@ function createServerHandlers(registry, executor, config) {
|
|
|
2042
2230
|
dismissModal: async (ctx) => {
|
|
2043
2231
|
const detector = registry.getEnrichers().modalDetector;
|
|
2044
2232
|
if (!detector) {
|
|
2045
|
-
return error2(
|
|
2046
|
-
"modal detector not registered",
|
|
2047
|
-
"ENRICHER_UNAVAILABLE"
|
|
2048
|
-
);
|
|
2233
|
+
return error2("modal detector not registered", "ENRICHER_UNAVAILABLE");
|
|
2049
2234
|
}
|
|
2050
2235
|
const { id } = ctx.params;
|
|
2051
2236
|
if (typeof id !== "string" || id.length === 0) {
|
|
@@ -2075,7 +2260,7 @@ function createServerHandlers(registry, executor, config) {
|
|
|
2075
2260
|
};
|
|
2076
2261
|
if (config?.appInfo) {
|
|
2077
2262
|
response.uiBridge = {
|
|
2078
|
-
version: "0.6.
|
|
2263
|
+
version: "0.6.14",
|
|
2079
2264
|
...config.appInfo,
|
|
2080
2265
|
capabilities: ["elements", "components", "actions", "design"],
|
|
2081
2266
|
elementCount: stats.elements,
|
|
@@ -2223,8 +2408,48 @@ function extractMethod(input, init) {
|
|
|
2223
2408
|
var NetworkRequestBuffer = class {
|
|
2224
2409
|
constructor(capacity = 100) {
|
|
2225
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 };
|
|
2226
2422
|
this.buffer = new RingBuffer(capacity);
|
|
2227
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
|
+
*/
|
|
2228
2453
|
install() {
|
|
2229
2454
|
if (this.installed) return;
|
|
2230
2455
|
const g = globalThis;
|
|
@@ -2232,12 +2457,20 @@ var NetworkRequestBuffer = class {
|
|
|
2232
2457
|
this.originalFetch = g.fetch;
|
|
2233
2458
|
const buffer = this.buffer;
|
|
2234
2459
|
const original = this.originalFetch;
|
|
2460
|
+
const fetchDepth = this.fetchDepth;
|
|
2235
2461
|
const patched = async (input, init) => {
|
|
2236
2462
|
const start = Date.now();
|
|
2237
2463
|
const url = extractUrl(input);
|
|
2238
2464
|
const method = extractMethod(input, init);
|
|
2239
2465
|
try {
|
|
2240
|
-
|
|
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;
|
|
2241
2474
|
buffer.push({
|
|
2242
2475
|
timestamp: start,
|
|
2243
2476
|
method,
|
|
@@ -2267,10 +2500,14 @@ var NetworkRequestBuffer = class {
|
|
|
2267
2500
|
this.originalXhrOpen = Xhr.prototype.open;
|
|
2268
2501
|
this.originalXhrSend = Xhr.prototype.send;
|
|
2269
2502
|
const buffer = this.buffer;
|
|
2503
|
+
const fetchDepth = this.fetchDepth;
|
|
2270
2504
|
const origOpen = this.originalXhrOpen;
|
|
2271
2505
|
Xhr.prototype.open = function(method, url, ...rest) {
|
|
2272
2506
|
this.__uiBridgeMethod = method.toUpperCase();
|
|
2273
2507
|
this.__uiBridgeUrl = typeof url === "string" ? url : String(url);
|
|
2508
|
+
if (fetchDepth.value > 0) {
|
|
2509
|
+
this.__uiBridgeFromFetch = true;
|
|
2510
|
+
}
|
|
2274
2511
|
return origOpen.call(
|
|
2275
2512
|
this,
|
|
2276
2513
|
method,
|
|
@@ -2280,6 +2517,9 @@ var NetworkRequestBuffer = class {
|
|
|
2280
2517
|
};
|
|
2281
2518
|
const origSend = this.originalXhrSend;
|
|
2282
2519
|
Xhr.prototype.send = function(body) {
|
|
2520
|
+
if (this.__uiBridgeFromFetch === true) {
|
|
2521
|
+
return origSend.call(this, body);
|
|
2522
|
+
}
|
|
2283
2523
|
const start = Date.now();
|
|
2284
2524
|
const method = this.__uiBridgeMethod ?? "GET";
|
|
2285
2525
|
const url = this.__uiBridgeUrl ?? "";
|
|
@@ -2359,6 +2599,16 @@ function isUnsubscribe(msg) {
|
|
|
2359
2599
|
}
|
|
2360
2600
|
|
|
2361
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
|
+
}
|
|
2362
2612
|
function parsePath(pattern, actual) {
|
|
2363
2613
|
const patternParts = pattern.split("/");
|
|
2364
2614
|
const actualParts = actual.split("/");
|
|
@@ -2521,6 +2771,23 @@ var WS_ROUTES = [
|
|
|
2521
2771
|
httpMethods: ["GET"]
|
|
2522
2772
|
}
|
|
2523
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
|
+
}
|
|
2524
2791
|
var HANDLER_DESCRIPTIONS = {
|
|
2525
2792
|
health: "Liveness probe; returns app metadata + element count",
|
|
2526
2793
|
getElements: "Flat list of registered UI elements",
|
|
@@ -2583,6 +2850,10 @@ var ACTION_REFERENCE = [
|
|
|
2583
2850
|
{ name: "focus", params: "none" },
|
|
2584
2851
|
{ name: "blur", params: "none" },
|
|
2585
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
|
+
},
|
|
2586
2857
|
{ name: "swipe", params: "SwipeActionParams { direction, distance }" },
|
|
2587
2858
|
{ name: "toggle", params: "none \u2014 flips boolean controls" }
|
|
2588
2859
|
];
|
|
@@ -2780,7 +3051,9 @@ var NativeUIBridgeServer = class {
|
|
|
2780
3051
|
}
|
|
2781
3052
|
/**
|
|
2782
3053
|
* Set a route provider for reporting the current navigation route.
|
|
2783
|
-
* 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.
|
|
2784
3057
|
*
|
|
2785
3058
|
* Also forwards the provider into the registry so the *default*
|
|
2786
3059
|
* `getSnapshot` handler (and any other call site of
|
|
@@ -2795,7 +3068,12 @@ var NativeUIBridgeServer = class {
|
|
|
2795
3068
|
this.handlers.getElements = async (ctx) => {
|
|
2796
3069
|
const currentRouteOnly = ctx.query?.currentRouteOnly === "true" || ctx.body?.currentRouteOnly === true;
|
|
2797
3070
|
if (currentRouteOnly && !ctx.query?.route && !ctx.body?.route) {
|
|
2798
|
-
|
|
3071
|
+
let currentRoute;
|
|
3072
|
+
try {
|
|
3073
|
+
currentRoute = provider.getCurrentRoute();
|
|
3074
|
+
} catch {
|
|
3075
|
+
currentRoute = null;
|
|
3076
|
+
}
|
|
2799
3077
|
if (currentRoute) {
|
|
2800
3078
|
ctx.query = { ...ctx.query, route: currentRoute };
|
|
2801
3079
|
}
|
|
@@ -2805,11 +3083,26 @@ var NativeUIBridgeServer = class {
|
|
|
2805
3083
|
this.handlers.getSnapshot = async (ctx) => {
|
|
2806
3084
|
const visibleOnly = ctx?.query?.visibleOnly === "true" || ctx?.body?.visibleOnly === true;
|
|
2807
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
|
+
}
|
|
2808
3104
|
const snapshot = this.registry.createSnapshot(
|
|
2809
|
-
{
|
|
2810
|
-
currentRoute: provider.getCurrentRoute(),
|
|
2811
|
-
segments: provider.getSegments?.()
|
|
2812
|
-
},
|
|
3105
|
+
{ currentRoute, segments, activeTab },
|
|
2813
3106
|
{ visibleOnly, currentRouteOnly }
|
|
2814
3107
|
);
|
|
2815
3108
|
return { success: true, data: snapshot, timestamp: Date.now() };
|
|
@@ -3475,8 +3768,15 @@ var NativeUIBridgeServer = class {
|
|
|
3475
3768
|
}
|
|
3476
3769
|
try {
|
|
3477
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
|
+
}
|
|
3478
3778
|
return {
|
|
3479
|
-
status: response
|
|
3779
|
+
status: httpStatusForResponse(response),
|
|
3480
3780
|
headers,
|
|
3481
3781
|
body: JSON.stringify(response)
|
|
3482
3782
|
};
|
|
@@ -3533,7 +3833,7 @@ var NativeUIBridgeServer = class {
|
|
|
3533
3833
|
timestamp: Date.now()
|
|
3534
3834
|
};
|
|
3535
3835
|
}
|
|
3536
|
-
let patternMatchedButWrongVerb =
|
|
3836
|
+
let patternMatchedButWrongVerb = DISCOVERY_PATHS.has(stripped);
|
|
3537
3837
|
for (const route of WS_ROUTES) {
|
|
3538
3838
|
const pathParams = parsePath(route.pattern, stripped);
|
|
3539
3839
|
if (!pathParams) continue;
|
|
@@ -3550,9 +3850,10 @@ var NativeUIBridgeServer = class {
|
|
|
3550
3850
|
const ctx = { params: pathParams, query, body };
|
|
3551
3851
|
return this.handlers[route.handler](ctx);
|
|
3552
3852
|
}
|
|
3853
|
+
const allowed = patternMatchedButWrongVerb ? allowedMethodsForPath(path, testHooks) : [];
|
|
3553
3854
|
return {
|
|
3554
3855
|
success: false,
|
|
3555
|
-
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}`,
|
|
3556
3857
|
code: patternMatchedButWrongVerb ? "METHOD_NOT_ALLOWED" : "NOT_FOUND",
|
|
3557
3858
|
timestamp: Date.now()
|
|
3558
3859
|
};
|
|
@@ -4273,6 +4574,7 @@ exports.encodeCloseFrame = encodeCloseFrame;
|
|
|
4273
4574
|
exports.encodePingFrame = encodePingFrame;
|
|
4274
4575
|
exports.encodePongFrame = encodePongFrame;
|
|
4275
4576
|
exports.encodeTextFrame = encodeTextFrame;
|
|
4577
|
+
exports.httpStatusForResponse = httpStatusForResponse;
|
|
4276
4578
|
exports.isAddrInUseError = isAddrInUseError;
|
|
4277
4579
|
exports.isBatchRequest = isBatchRequest;
|
|
4278
4580
|
exports.isSubscribe = isSubscribe;
|