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