@qontinui/ui-bridge-native 0.6.12 → 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 +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 +1502 -117
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1484 -118
- 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 +1319 -112
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +1319 -112
- 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.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) {
|
|
@@ -1362,10 +1545,14 @@ function createServerHandlers(registry, executor, config) {
|
|
|
1362
1545
|
if (forRoute) {
|
|
1363
1546
|
allElements = allElements.filter((e) => matchesCurrentRoute(e.registrationRoute, forRoute));
|
|
1364
1547
|
}
|
|
1548
|
+
const viewport = registry.getViewportRect();
|
|
1365
1549
|
const elements = allElements.map((e) => {
|
|
1366
1550
|
const handlers = extractHandlerNames(e.props);
|
|
1367
1551
|
const state = e.getState();
|
|
1368
|
-
const visibility
|
|
1552
|
+
const { visibility, visibilityReason } = computeVisibility(
|
|
1553
|
+
state,
|
|
1554
|
+
registry.getClipRectFor(e, viewport)
|
|
1555
|
+
);
|
|
1369
1556
|
const bbox = projectBbox(state, visibility, registry.getPixelRatio());
|
|
1370
1557
|
const vision = projectVisionFields({
|
|
1371
1558
|
type: e.type,
|
|
@@ -1383,10 +1570,11 @@ function createServerHandlers(registry, executor, config) {
|
|
|
1383
1570
|
identifier: e.getIdentifier(),
|
|
1384
1571
|
state,
|
|
1385
1572
|
actions: e.actions,
|
|
1386
|
-
customActions:
|
|
1573
|
+
customActions: serializeElementCustomActions(e.customActions),
|
|
1387
1574
|
registeredHandlers: handlers.length > 0 ? handlers : void 0,
|
|
1388
1575
|
registrationRoute: e.registrationRoute,
|
|
1389
1576
|
visibility,
|
|
1577
|
+
...visibilityReason ? { visibilityReason } : {},
|
|
1390
1578
|
...bbox ? { bbox } : {},
|
|
1391
1579
|
...vision
|
|
1392
1580
|
};
|
|
@@ -1408,7 +1596,7 @@ function createServerHandlers(registry, executor, config) {
|
|
|
1408
1596
|
identifier: element.getIdentifier(),
|
|
1409
1597
|
state: resolveElementState(element),
|
|
1410
1598
|
actions: element.actions,
|
|
1411
|
-
customActions:
|
|
1599
|
+
customActions: serializeElementCustomActions(element.customActions),
|
|
1412
1600
|
registeredHandlers: handlers.length > 0 ? handlers : void 0,
|
|
1413
1601
|
registrationRoute: element.registrationRoute
|
|
1414
1602
|
}
|
|
@@ -1429,13 +1617,9 @@ function createServerHandlers(registry, executor, config) {
|
|
|
1429
1617
|
if (!envelope?.action) {
|
|
1430
1618
|
return error2("Action is required", "INVALID_REQUEST");
|
|
1431
1619
|
}
|
|
1432
|
-
const response = await executor.executeAction(id,
|
|
1433
|
-
action: envelope.action,
|
|
1434
|
-
params: envelope.params,
|
|
1435
|
-
waitOptions: envelope.waitOptions
|
|
1436
|
-
});
|
|
1620
|
+
const response = await executor.executeAction(id, envelope);
|
|
1437
1621
|
if (!response.success) {
|
|
1438
|
-
return error2(response.error || "Action failed", "ACTION_FAILED");
|
|
1622
|
+
return error2(response.error || "Action failed", response.code ?? "ACTION_FAILED");
|
|
1439
1623
|
}
|
|
1440
1624
|
return success2(response);
|
|
1441
1625
|
},
|
|
@@ -1445,7 +1629,11 @@ function createServerHandlers(registry, executor, config) {
|
|
|
1445
1629
|
id: c.id,
|
|
1446
1630
|
name: c.name,
|
|
1447
1631
|
description: c.description,
|
|
1448
|
-
|
|
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 })),
|
|
1449
1637
|
elementIds: c.elementIds
|
|
1450
1638
|
}));
|
|
1451
1639
|
return success2({ components });
|
|
@@ -1464,7 +1652,9 @@ function createServerHandlers(registry, executor, config) {
|
|
|
1464
1652
|
actions: component.actions.map((a) => ({
|
|
1465
1653
|
id: a.id,
|
|
1466
1654
|
label: a.label,
|
|
1467
|
-
description: a.description
|
|
1655
|
+
description: a.description,
|
|
1656
|
+
// Phase 4 — see the listing twin above.
|
|
1657
|
+
effect: a.effect
|
|
1468
1658
|
})),
|
|
1469
1659
|
elementIds: component.elementIds
|
|
1470
1660
|
}
|
|
@@ -1475,7 +1665,8 @@ function createServerHandlers(registry, executor, config) {
|
|
|
1475
1665
|
const body = ctx.body;
|
|
1476
1666
|
const response = await executor.executeComponentAction(id, {
|
|
1477
1667
|
action: actionId,
|
|
1478
|
-
params: body?.params
|
|
1668
|
+
params: body?.params,
|
|
1669
|
+
timeoutMs: body?.timeoutMs
|
|
1479
1670
|
});
|
|
1480
1671
|
if (!response.success) {
|
|
1481
1672
|
return error2(response.error || "Action failed", "ACTION_FAILED");
|
|
@@ -1667,9 +1858,12 @@ function createServerHandlers(registry, executor, config) {
|
|
|
1667
1858
|
//
|
|
1668
1859
|
// The mobile bridge mounts explicit NOT_SUPPORTED stubs at the runner's
|
|
1669
1860
|
// cheatsheet paths so callers (and the /manual-test skill in particular)
|
|
1670
|
-
// get a structured envelope explaining the gap rather than a
|
|
1671
|
-
//
|
|
1672
|
-
//
|
|
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.
|
|
1673
1867
|
aiForms: async () => {
|
|
1674
1868
|
return error2(
|
|
1675
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.',
|
|
@@ -1996,18 +2190,12 @@ function createServerHandlers(registry, executor, config) {
|
|
|
1996
2190
|
pushModal: async (ctx) => {
|
|
1997
2191
|
const detector = registry.getEnrichers().modalDetector;
|
|
1998
2192
|
if (!detector) {
|
|
1999
|
-
return error2(
|
|
2000
|
-
"modal detector not registered",
|
|
2001
|
-
"ENRICHER_UNAVAILABLE"
|
|
2002
|
-
);
|
|
2193
|
+
return error2("modal detector not registered", "ENRICHER_UNAVAILABLE");
|
|
2003
2194
|
}
|
|
2004
2195
|
const body = ctx.body;
|
|
2005
2196
|
const id = body?.id;
|
|
2006
2197
|
if (typeof id !== "string" || id.length === 0) {
|
|
2007
|
-
return error2(
|
|
2008
|
-
'pushModal requires a non-empty "id"',
|
|
2009
|
-
"INVALID_REQUEST"
|
|
2010
|
-
);
|
|
2198
|
+
return error2('pushModal requires a non-empty "id"', "INVALID_REQUEST");
|
|
2011
2199
|
}
|
|
2012
2200
|
const allowedTypes = ["modal", "sheet", "drawer", "popover", "alertdialog", "dialog"];
|
|
2013
2201
|
const requestedType = body?.type;
|
|
@@ -2040,10 +2228,7 @@ function createServerHandlers(registry, executor, config) {
|
|
|
2040
2228
|
dismissModal: async (ctx) => {
|
|
2041
2229
|
const detector = registry.getEnrichers().modalDetector;
|
|
2042
2230
|
if (!detector) {
|
|
2043
|
-
return error2(
|
|
2044
|
-
"modal detector not registered",
|
|
2045
|
-
"ENRICHER_UNAVAILABLE"
|
|
2046
|
-
);
|
|
2231
|
+
return error2("modal detector not registered", "ENRICHER_UNAVAILABLE");
|
|
2047
2232
|
}
|
|
2048
2233
|
const { id } = ctx.params;
|
|
2049
2234
|
if (typeof id !== "string" || id.length === 0) {
|
|
@@ -2073,7 +2258,7 @@ function createServerHandlers(registry, executor, config) {
|
|
|
2073
2258
|
};
|
|
2074
2259
|
if (config?.appInfo) {
|
|
2075
2260
|
response.uiBridge = {
|
|
2076
|
-
version: "0.6.
|
|
2261
|
+
version: "0.6.13",
|
|
2077
2262
|
...config.appInfo,
|
|
2078
2263
|
capabilities: ["elements", "components", "actions", "design"],
|
|
2079
2264
|
elementCount: stats.elements,
|
|
@@ -2221,8 +2406,48 @@ function extractMethod(input, init) {
|
|
|
2221
2406
|
var NetworkRequestBuffer = class {
|
|
2222
2407
|
constructor(capacity = 100) {
|
|
2223
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 };
|
|
2224
2420
|
this.buffer = new RingBuffer(capacity);
|
|
2225
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
|
+
*/
|
|
2226
2451
|
install() {
|
|
2227
2452
|
if (this.installed) return;
|
|
2228
2453
|
const g = globalThis;
|
|
@@ -2230,12 +2455,20 @@ var NetworkRequestBuffer = class {
|
|
|
2230
2455
|
this.originalFetch = g.fetch;
|
|
2231
2456
|
const buffer = this.buffer;
|
|
2232
2457
|
const original = this.originalFetch;
|
|
2458
|
+
const fetchDepth = this.fetchDepth;
|
|
2233
2459
|
const patched = async (input, init) => {
|
|
2234
2460
|
const start = Date.now();
|
|
2235
2461
|
const url = extractUrl(input);
|
|
2236
2462
|
const method = extractMethod(input, init);
|
|
2237
2463
|
try {
|
|
2238
|
-
|
|
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;
|
|
2239
2472
|
buffer.push({
|
|
2240
2473
|
timestamp: start,
|
|
2241
2474
|
method,
|
|
@@ -2265,10 +2498,14 @@ var NetworkRequestBuffer = class {
|
|
|
2265
2498
|
this.originalXhrOpen = Xhr.prototype.open;
|
|
2266
2499
|
this.originalXhrSend = Xhr.prototype.send;
|
|
2267
2500
|
const buffer = this.buffer;
|
|
2501
|
+
const fetchDepth = this.fetchDepth;
|
|
2268
2502
|
const origOpen = this.originalXhrOpen;
|
|
2269
2503
|
Xhr.prototype.open = function(method, url, ...rest) {
|
|
2270
2504
|
this.__uiBridgeMethod = method.toUpperCase();
|
|
2271
2505
|
this.__uiBridgeUrl = typeof url === "string" ? url : String(url);
|
|
2506
|
+
if (fetchDepth.value > 0) {
|
|
2507
|
+
this.__uiBridgeFromFetch = true;
|
|
2508
|
+
}
|
|
2272
2509
|
return origOpen.call(
|
|
2273
2510
|
this,
|
|
2274
2511
|
method,
|
|
@@ -2278,6 +2515,9 @@ var NetworkRequestBuffer = class {
|
|
|
2278
2515
|
};
|
|
2279
2516
|
const origSend = this.originalXhrSend;
|
|
2280
2517
|
Xhr.prototype.send = function(body) {
|
|
2518
|
+
if (this.__uiBridgeFromFetch === true) {
|
|
2519
|
+
return origSend.call(this, body);
|
|
2520
|
+
}
|
|
2281
2521
|
const start = Date.now();
|
|
2282
2522
|
const method = this.__uiBridgeMethod ?? "GET";
|
|
2283
2523
|
const url = this.__uiBridgeUrl ?? "";
|
|
@@ -2357,6 +2597,16 @@ function isUnsubscribe(msg) {
|
|
|
2357
2597
|
}
|
|
2358
2598
|
|
|
2359
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
|
+
}
|
|
2360
2610
|
function parsePath(pattern, actual) {
|
|
2361
2611
|
const patternParts = pattern.split("/");
|
|
2362
2612
|
const actualParts = actual.split("/");
|
|
@@ -2519,6 +2769,23 @@ var WS_ROUTES = [
|
|
|
2519
2769
|
httpMethods: ["GET"]
|
|
2520
2770
|
}
|
|
2521
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
|
+
}
|
|
2522
2789
|
var HANDLER_DESCRIPTIONS = {
|
|
2523
2790
|
health: "Liveness probe; returns app metadata + element count",
|
|
2524
2791
|
getElements: "Flat list of registered UI elements",
|
|
@@ -2581,6 +2848,10 @@ var ACTION_REFERENCE = [
|
|
|
2581
2848
|
{ name: "focus", params: "none" },
|
|
2582
2849
|
{ name: "blur", params: "none" },
|
|
2583
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
|
+
},
|
|
2584
2855
|
{ name: "swipe", params: "SwipeActionParams { direction, distance }" },
|
|
2585
2856
|
{ name: "toggle", params: "none \u2014 flips boolean controls" }
|
|
2586
2857
|
];
|
|
@@ -2778,7 +3049,9 @@ var NativeUIBridgeServer = class {
|
|
|
2778
3049
|
}
|
|
2779
3050
|
/**
|
|
2780
3051
|
* Set a route provider for reporting the current navigation route.
|
|
2781
|
-
* 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.
|
|
2782
3055
|
*
|
|
2783
3056
|
* Also forwards the provider into the registry so the *default*
|
|
2784
3057
|
* `getSnapshot` handler (and any other call site of
|
|
@@ -2793,7 +3066,12 @@ var NativeUIBridgeServer = class {
|
|
|
2793
3066
|
this.handlers.getElements = async (ctx) => {
|
|
2794
3067
|
const currentRouteOnly = ctx.query?.currentRouteOnly === "true" || ctx.body?.currentRouteOnly === true;
|
|
2795
3068
|
if (currentRouteOnly && !ctx.query?.route && !ctx.body?.route) {
|
|
2796
|
-
|
|
3069
|
+
let currentRoute;
|
|
3070
|
+
try {
|
|
3071
|
+
currentRoute = provider.getCurrentRoute();
|
|
3072
|
+
} catch {
|
|
3073
|
+
currentRoute = null;
|
|
3074
|
+
}
|
|
2797
3075
|
if (currentRoute) {
|
|
2798
3076
|
ctx.query = { ...ctx.query, route: currentRoute };
|
|
2799
3077
|
}
|
|
@@ -2803,11 +3081,26 @@ var NativeUIBridgeServer = class {
|
|
|
2803
3081
|
this.handlers.getSnapshot = async (ctx) => {
|
|
2804
3082
|
const visibleOnly = ctx?.query?.visibleOnly === "true" || ctx?.body?.visibleOnly === true;
|
|
2805
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
|
+
}
|
|
2806
3102
|
const snapshot = this.registry.createSnapshot(
|
|
2807
|
-
{
|
|
2808
|
-
currentRoute: provider.getCurrentRoute(),
|
|
2809
|
-
segments: provider.getSegments?.()
|
|
2810
|
-
},
|
|
3103
|
+
{ currentRoute, segments, activeTab },
|
|
2811
3104
|
{ visibleOnly, currentRouteOnly }
|
|
2812
3105
|
);
|
|
2813
3106
|
return { success: true, data: snapshot, timestamp: Date.now() };
|
|
@@ -3473,8 +3766,15 @@ var NativeUIBridgeServer = class {
|
|
|
3473
3766
|
}
|
|
3474
3767
|
try {
|
|
3475
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
|
+
}
|
|
3476
3776
|
return {
|
|
3477
|
-
status: response
|
|
3777
|
+
status: httpStatusForResponse(response),
|
|
3478
3778
|
headers,
|
|
3479
3779
|
body: JSON.stringify(response)
|
|
3480
3780
|
};
|
|
@@ -3531,7 +3831,7 @@ var NativeUIBridgeServer = class {
|
|
|
3531
3831
|
timestamp: Date.now()
|
|
3532
3832
|
};
|
|
3533
3833
|
}
|
|
3534
|
-
let patternMatchedButWrongVerb =
|
|
3834
|
+
let patternMatchedButWrongVerb = DISCOVERY_PATHS.has(stripped);
|
|
3535
3835
|
for (const route of WS_ROUTES) {
|
|
3536
3836
|
const pathParams = parsePath(route.pattern, stripped);
|
|
3537
3837
|
if (!pathParams) continue;
|
|
@@ -3548,9 +3848,10 @@ var NativeUIBridgeServer = class {
|
|
|
3548
3848
|
const ctx = { params: pathParams, query, body };
|
|
3549
3849
|
return this.handlers[route.handler](ctx);
|
|
3550
3850
|
}
|
|
3851
|
+
const allowed = patternMatchedButWrongVerb ? allowedMethodsForPath(path, testHooks) : [];
|
|
3551
3852
|
return {
|
|
3552
3853
|
success: false,
|
|
3553
|
-
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}`,
|
|
3554
3855
|
code: patternMatchedButWrongVerb ? "METHOD_NOT_ALLOWED" : "NOT_FOUND",
|
|
3555
3856
|
timestamp: Date.now()
|
|
3556
3857
|
};
|
|
@@ -4247,6 +4548,6 @@ async function bindWithRetry(attempt, options = {}) {
|
|
|
4247
4548
|
}
|
|
4248
4549
|
}
|
|
4249
4550
|
|
|
4250
|
-
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 };
|
|
4251
4552
|
//# sourceMappingURL=index.mjs.map
|
|
4252
4553
|
//# sourceMappingURL=index.mjs.map
|