@qontinui/ui-bridge-native 0.1.6 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/control/{action-executor.d.ts → index.d.mts} +14 -6
- package/dist/control/index.d.ts +95 -5
- package/dist/control/index.js +74 -11
- package/dist/control/index.js.map +1 -1
- package/dist/control/index.mjs +74 -11
- package/dist/control/index.mjs.map +1 -1
- package/dist/core/index.d.mts +46 -0
- package/dist/core/index.d.ts +43 -6
- package/dist/core/index.js +108 -2
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +108 -2
- package/dist/core/index.mjs.map +1 -1
- package/dist/debug/{inspector.d.ts → index.d.mts} +6 -3
- package/dist/debug/index.d.ts +44 -4
- package/dist/debug/index.js +2 -2
- package/dist/debug/index.js.map +1 -1
- package/dist/debug/index.mjs +2 -2
- package/dist/debug/index.mjs.map +1 -1
- package/dist/design/{design-inspector-native.d.ts → index.d.mts} +11 -7
- package/dist/design/index.d.ts +60 -5
- package/dist/{design/design-types.d.ts → design-types-BHr6Q1FX.d.mts} +11 -10
- package/dist/design-types-BHr6Q1FX.d.ts +205 -0
- package/dist/http-server-D2U1k48d.d.mts +703 -0
- package/dist/http-server-ck8clUsZ.d.ts +703 -0
- package/dist/index.d.mts +89 -0
- package/dist/index.d.ts +86 -28
- package/dist/index.js +754 -50
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +749 -51
- package/dist/index.mjs.map +1 -1
- package/dist/react/{useAutoRegister.d.ts → index.d.mts} +15 -7
- package/dist/react/index.d.ts +69 -8
- package/dist/react/index.js +733 -50
- package/dist/react/index.js.map +1 -1
- package/dist/react/index.mjs +731 -51
- package/dist/react/index.mjs.map +1 -1
- package/dist/{core/registry.d.ts → registry-BxXaxmsT.d.ts} +51 -10
- package/dist/registry-KdUW_koV.d.mts +253 -0
- package/dist/server/index.d.mts +119 -0
- package/dist/server/index.d.ts +117 -13
- package/dist/server/index.js +272 -16
- package/dist/server/index.js.map +1 -1
- package/dist/server/index.mjs +272 -16
- package/dist/server/index.mjs.map +1 -1
- package/dist/{core/types.d.ts → types-BwdPNpII.d.mts} +172 -30
- package/dist/types-BwdPNpII.d.ts +525 -0
- package/dist/{control/types.d.ts → types-DCEv5qb6.d.ts} +36 -15
- package/dist/types-DjB0zJJH.d.mts +197 -0
- package/dist/useUIBridgeToast-BPIEbsEc.d.mts +726 -0
- package/dist/useUIBridgeToast-DD13oF3k.d.ts +726 -0
- package/package.json +2 -2
- package/dist/control/action-executor.d.ts.map +0 -1
- package/dist/control/index.d.ts.map +0 -1
- package/dist/control/types.d.ts.map +0 -1
- package/dist/core/element-identifier.d.ts +0 -41
- package/dist/core/element-identifier.d.ts.map +0 -1
- package/dist/core/index.d.ts.map +0 -1
- package/dist/core/registry.d.ts.map +0 -1
- package/dist/core/types.d.ts.map +0 -1
- package/dist/debug/index.d.ts.map +0 -1
- package/dist/debug/inspector.d.ts.map +0 -1
- package/dist/design/design-inspector-native.d.ts.map +0 -1
- package/dist/design/design-types.d.ts.map +0 -1
- package/dist/design/index.d.ts.map +0 -1
- package/dist/index.d.ts.map +0 -1
- package/dist/react/UIBridgeNativeProvider.d.ts +0 -147
- package/dist/react/UIBridgeNativeProvider.d.ts.map +0 -1
- package/dist/react/index.d.ts.map +0 -1
- package/dist/react/useAutoRegister.d.ts.map +0 -1
- package/dist/react/useUIBridge.d.ts +0 -103
- package/dist/react/useUIBridge.d.ts.map +0 -1
- package/dist/react/useUIComponent.d.ts +0 -112
- package/dist/react/useUIComponent.d.ts.map +0 -1
- package/dist/react/useUIElement.d.ts +0 -162
- package/dist/react/useUIElement.d.ts.map +0 -1
- package/dist/server/design-handlers.d.ts +0 -47
- package/dist/server/design-handlers.d.ts.map +0 -1
- package/dist/server/handlers.d.ts +0 -20
- package/dist/server/handlers.d.ts.map +0 -1
- package/dist/server/http-server.d.ts +0 -247
- package/dist/server/http-server.d.ts.map +0 -1
- package/dist/server/index.d.ts.map +0 -1
- package/dist/server/types.d.ts +0 -200
- package/dist/server/types.d.ts.map +0 -1
- package/dist/server/ws-connection.d.ts +0 -48
- package/dist/server/ws-connection.d.ts.map +0 -1
- package/dist/server/ws-event-bridge.d.ts +0 -113
- package/dist/server/ws-event-bridge.d.ts.map +0 -1
- package/dist/server/ws-protocol.d.ts +0 -48
- package/dist/server/ws-protocol.d.ts.map +0 -1
- package/dist/server/ws-types.d.ts +0 -65
- package/dist/server/ws-types.d.ts.map +0 -1
- package/dist/transport/CloudRelayClient.d.ts +0 -51
- package/dist/transport/CloudRelayClient.d.ts.map +0 -1
- package/dist/transport/DeviceAnnouncer.d.ts +0 -75
- package/dist/transport/DeviceAnnouncer.d.ts.map +0 -1
- package/dist/transport/index.d.ts +0 -3
- package/dist/transport/index.d.ts.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -53,9 +53,51 @@ var NativeUIBridgeRegistry = class {
|
|
|
53
53
|
this.components = /* @__PURE__ */ new Map();
|
|
54
54
|
this.workflows = /* @__PURE__ */ new Map();
|
|
55
55
|
this.eventListeners = /* @__PURE__ */ new Map();
|
|
56
|
+
this.enrichers = {};
|
|
57
|
+
this.snapshotExtras = /* @__PURE__ */ new Map();
|
|
58
|
+
/**
|
|
59
|
+
* Sticky flag: flips `true` the first time any element is registered and
|
|
60
|
+
* stays `true` even after elements are unregistered. Lets agents distinguish
|
|
61
|
+
* "this route never wired any elements" from "this route registered then
|
|
62
|
+
* unmounted".
|
|
63
|
+
*/
|
|
64
|
+
this.everHadRegistrations = false;
|
|
56
65
|
this.config = config;
|
|
57
66
|
}
|
|
58
67
|
// ============================================================================
|
|
68
|
+
// Snapshot Enricher Slots
|
|
69
|
+
// ============================================================================
|
|
70
|
+
/**
|
|
71
|
+
* Register/replace canonical enrichers (modal/toast/undo). HMR-safe — calling
|
|
72
|
+
* with a partial set merges into existing slots instead of clobbering them.
|
|
73
|
+
*/
|
|
74
|
+
setEnrichers(e) {
|
|
75
|
+
this.enrichers = { ...this.enrichers, ...e };
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Read-only accessor for the canonical enrichers slot. Returned object is
|
|
79
|
+
* the live reference, so callers can detect "no detector wired" via
|
|
80
|
+
* `getEnrichers().modalDetector === undefined`. Used by the test-hook HTTP
|
|
81
|
+
* handlers (`control/modal/push`, `control/modal/dismiss/:id`) which need
|
|
82
|
+
* to drive the ModalDetector directly without re-instantiating it.
|
|
83
|
+
*/
|
|
84
|
+
getEnrichers() {
|
|
85
|
+
return this.enrichers;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Register a custom snapshot enricher. The returned object will be
|
|
89
|
+
* `Object.assign`ed onto the snapshot, so use unique top-level keys to avoid
|
|
90
|
+
* clobbering canonical fields. Returns a disposer.
|
|
91
|
+
*/
|
|
92
|
+
registerSnapshotEnricher(name, fn) {
|
|
93
|
+
this.snapshotExtras.set(name, fn);
|
|
94
|
+
return () => this.unregisterSnapshotEnricher(name);
|
|
95
|
+
}
|
|
96
|
+
/** Remove a custom snapshot enricher by name */
|
|
97
|
+
unregisterSnapshotEnricher(name) {
|
|
98
|
+
this.snapshotExtras.delete(name);
|
|
99
|
+
}
|
|
100
|
+
// ============================================================================
|
|
59
101
|
// Element Management
|
|
60
102
|
// ============================================================================
|
|
61
103
|
/**
|
|
@@ -121,6 +163,7 @@ var NativeUIBridgeRegistry = class {
|
|
|
121
163
|
stateStyles
|
|
122
164
|
};
|
|
123
165
|
this.elements.set(id, registered);
|
|
166
|
+
this.everHadRegistrations = true;
|
|
124
167
|
this.emit("element:registered", { id, type, label });
|
|
125
168
|
if (this.config.verbose) {
|
|
126
169
|
console.log(`[ui-bridge-native] Registered element: ${id} (${type})`);
|
|
@@ -418,7 +461,7 @@ var NativeUIBridgeRegistry = class {
|
|
|
418
461
|
const currentRoute = routeInfo.currentRoute;
|
|
419
462
|
elements = elements.filter((e) => e.registrationRoute === currentRoute);
|
|
420
463
|
}
|
|
421
|
-
|
|
464
|
+
const snapshot = {
|
|
422
465
|
timestamp: Date.now(),
|
|
423
466
|
elements: elements.map((e) => {
|
|
424
467
|
const handlers = extractHandlerNames(e.props);
|
|
@@ -448,8 +491,52 @@ var NativeUIBridgeRegistry = class {
|
|
|
448
491
|
stepCount: w.steps.length
|
|
449
492
|
})),
|
|
450
493
|
currentRoute: routeInfo?.currentRoute ?? null,
|
|
451
|
-
segments: routeInfo?.segments
|
|
494
|
+
segments: routeInfo?.segments,
|
|
495
|
+
registration: this.getRegistrationCoverage()
|
|
452
496
|
};
|
|
497
|
+
if (this.enrichers.modalDetector) {
|
|
498
|
+
try {
|
|
499
|
+
snapshot.modalStack = this.enrichers.modalDetector.getSnapshotModalContext();
|
|
500
|
+
} catch (error3) {
|
|
501
|
+
if (this.config.verbose) {
|
|
502
|
+
console.warn(`[ui-bridge-native] modalDetector enricher threw:`, error3);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
if (this.enrichers.toastCapture) {
|
|
507
|
+
try {
|
|
508
|
+
snapshot.toasts = this.enrichers.toastCapture.getSnapshotToastContext();
|
|
509
|
+
} catch (error3) {
|
|
510
|
+
if (this.config.verbose) {
|
|
511
|
+
console.warn(`[ui-bridge-native] toastCapture enricher threw:`, error3);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
if (this.enrichers.undoTracker) {
|
|
516
|
+
try {
|
|
517
|
+
snapshot.undoRedo = this.enrichers.undoTracker.getSnapshotUndoContext();
|
|
518
|
+
} catch (error3) {
|
|
519
|
+
if (this.config.verbose) {
|
|
520
|
+
console.warn(`[ui-bridge-native] undoTracker enricher threw:`, error3);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
if (this.snapshotExtras.size > 0) {
|
|
525
|
+
const ctx = { elements, currentRoute: routeInfo?.currentRoute ?? null };
|
|
526
|
+
for (const [name, fn] of this.snapshotExtras) {
|
|
527
|
+
try {
|
|
528
|
+
const extra = fn(ctx);
|
|
529
|
+
if (extra && typeof extra === "object") {
|
|
530
|
+
Object.assign(snapshot, extra);
|
|
531
|
+
}
|
|
532
|
+
} catch (error3) {
|
|
533
|
+
if (this.config.verbose) {
|
|
534
|
+
console.warn(`[ui-bridge-native] snapshot enricher "${name}" threw:`, error3);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
return snapshot;
|
|
453
540
|
}
|
|
454
541
|
/**
|
|
455
542
|
* Get registry statistics
|
|
@@ -461,6 +548,25 @@ var NativeUIBridgeRegistry = class {
|
|
|
461
548
|
workflows: this.workflows.size
|
|
462
549
|
};
|
|
463
550
|
}
|
|
551
|
+
/**
|
|
552
|
+
* Compute registration coverage metadata for the current registry state.
|
|
553
|
+
*
|
|
554
|
+
* Groups currently-registered elements by `registrationRoute`, bucketing
|
|
555
|
+
* elements without a route under `'?'`. `everHadRegistrations` is sticky —
|
|
556
|
+
* once true, it stays true for the lifetime of the registry instance.
|
|
557
|
+
*/
|
|
558
|
+
getRegistrationCoverage() {
|
|
559
|
+
const byRoute = {};
|
|
560
|
+
for (const element of this.elements.values()) {
|
|
561
|
+
const key = element.registrationRoute == null || element.registrationRoute === "" ? "?" : element.registrationRoute;
|
|
562
|
+
byRoute[key] = (byRoute[key] ?? 0) + 1;
|
|
563
|
+
}
|
|
564
|
+
return {
|
|
565
|
+
totalRegistered: this.elements.size,
|
|
566
|
+
everHadRegistrations: this.everHadRegistrations,
|
|
567
|
+
byRoute
|
|
568
|
+
};
|
|
569
|
+
}
|
|
464
570
|
/**
|
|
465
571
|
* Clear all registrations
|
|
466
572
|
*/
|
|
@@ -595,6 +701,23 @@ function sleep(ms) {
|
|
|
595
701
|
var DefaultNativeActionExecutor = class {
|
|
596
702
|
constructor(registry) {
|
|
597
703
|
this.registry = registry;
|
|
704
|
+
this.actionListeners = /* @__PURE__ */ new Set();
|
|
705
|
+
}
|
|
706
|
+
onActionExecuted(listener) {
|
|
707
|
+
this.actionListeners.add(listener);
|
|
708
|
+
return () => {
|
|
709
|
+
this.actionListeners.delete(listener);
|
|
710
|
+
};
|
|
711
|
+
}
|
|
712
|
+
/** Notify listeners out-of-band; isolate each invocation so one bad listener can't poison the rest. */
|
|
713
|
+
emitActionEvent(event) {
|
|
714
|
+
for (const listener of this.actionListeners) {
|
|
715
|
+
try {
|
|
716
|
+
listener(event);
|
|
717
|
+
} catch (error3) {
|
|
718
|
+
console.warn(`[ui-bridge-native] Action listener error:`, error3);
|
|
719
|
+
}
|
|
720
|
+
}
|
|
598
721
|
}
|
|
599
722
|
/**
|
|
600
723
|
* Execute an action on an element
|
|
@@ -608,11 +731,23 @@ var DefaultNativeActionExecutor = class {
|
|
|
608
731
|
registered = findElementByIdentifier(elementId) ?? void 0;
|
|
609
732
|
}
|
|
610
733
|
if (!registered) {
|
|
734
|
+
const failTime = Date.now();
|
|
735
|
+
const errorMsg = `Element not found: ${elementId}`;
|
|
736
|
+
this.emitActionEvent({
|
|
737
|
+
elementId,
|
|
738
|
+
action: request.action,
|
|
739
|
+
params: request.params,
|
|
740
|
+
success: false,
|
|
741
|
+
error: errorMsg,
|
|
742
|
+
timestamp: failTime,
|
|
743
|
+
requestId: request.requestId,
|
|
744
|
+
durationMs: failTime - startTime
|
|
745
|
+
});
|
|
611
746
|
return {
|
|
612
747
|
success: false,
|
|
613
|
-
error:
|
|
614
|
-
durationMs:
|
|
615
|
-
timestamp:
|
|
748
|
+
error: errorMsg,
|
|
749
|
+
durationMs: failTime - startTime,
|
|
750
|
+
timestamp: failTime,
|
|
616
751
|
requestId: request.requestId
|
|
617
752
|
};
|
|
618
753
|
}
|
|
@@ -620,33 +755,67 @@ var DefaultNativeActionExecutor = class {
|
|
|
620
755
|
const waitResult = await this.waitForElementInternal(registered.id, request.waitOptions);
|
|
621
756
|
waitDurationMs = waitResult.waitedMs;
|
|
622
757
|
if (!waitResult.met) {
|
|
758
|
+
const failTime = Date.now();
|
|
759
|
+
const errorMsg = waitResult.error || "Wait condition not met";
|
|
760
|
+
this.emitActionEvent({
|
|
761
|
+
elementId,
|
|
762
|
+
action: request.action,
|
|
763
|
+
params: request.params,
|
|
764
|
+
success: false,
|
|
765
|
+
error: errorMsg,
|
|
766
|
+
timestamp: failTime,
|
|
767
|
+
requestId: request.requestId,
|
|
768
|
+
durationMs: failTime - startTime
|
|
769
|
+
});
|
|
623
770
|
return {
|
|
624
771
|
success: false,
|
|
625
|
-
error:
|
|
626
|
-
durationMs:
|
|
627
|
-
timestamp:
|
|
772
|
+
error: errorMsg,
|
|
773
|
+
durationMs: failTime - startTime,
|
|
774
|
+
timestamp: failTime,
|
|
628
775
|
requestId: request.requestId,
|
|
629
776
|
waitDurationMs
|
|
630
777
|
};
|
|
631
778
|
}
|
|
632
779
|
}
|
|
633
780
|
const result = await this.performAction(registered, request.action, request.params);
|
|
781
|
+
const successTime = Date.now();
|
|
782
|
+
this.emitActionEvent({
|
|
783
|
+
elementId,
|
|
784
|
+
action: request.action,
|
|
785
|
+
params: request.params,
|
|
786
|
+
success: true,
|
|
787
|
+
timestamp: successTime,
|
|
788
|
+
requestId: request.requestId,
|
|
789
|
+
durationMs: successTime - startTime
|
|
790
|
+
});
|
|
634
791
|
return {
|
|
635
792
|
success: true,
|
|
636
793
|
elementState: registered.getState(),
|
|
637
794
|
result,
|
|
638
|
-
durationMs:
|
|
639
|
-
timestamp:
|
|
795
|
+
durationMs: successTime - startTime,
|
|
796
|
+
timestamp: successTime,
|
|
640
797
|
requestId: request.requestId,
|
|
641
798
|
waitDurationMs
|
|
642
799
|
};
|
|
643
800
|
} catch (error3) {
|
|
801
|
+
const failTime = Date.now();
|
|
802
|
+
const errorMsg = error3 instanceof Error ? error3.message : String(error3);
|
|
803
|
+
this.emitActionEvent({
|
|
804
|
+
elementId,
|
|
805
|
+
action: request.action,
|
|
806
|
+
params: request.params,
|
|
807
|
+
success: false,
|
|
808
|
+
error: errorMsg,
|
|
809
|
+
timestamp: failTime,
|
|
810
|
+
requestId: request.requestId,
|
|
811
|
+
durationMs: failTime - startTime
|
|
812
|
+
});
|
|
644
813
|
return {
|
|
645
814
|
success: false,
|
|
646
|
-
error:
|
|
815
|
+
error: errorMsg,
|
|
647
816
|
stack: error3 instanceof Error ? error3.stack : void 0,
|
|
648
|
-
durationMs:
|
|
649
|
-
timestamp:
|
|
817
|
+
durationMs: failTime - startTime,
|
|
818
|
+
timestamp: failTime,
|
|
650
819
|
requestId: request.requestId,
|
|
651
820
|
waitDurationMs
|
|
652
821
|
};
|
|
@@ -1805,6 +1974,20 @@ function createServerHandlers(registry, executor, config) {
|
|
|
1805
1974
|
return success2(response);
|
|
1806
1975
|
},
|
|
1807
1976
|
// Discovery
|
|
1977
|
+
//
|
|
1978
|
+
// Native parity with the runner's `POST /ui-bridge/ai/find`. The runner
|
|
1979
|
+
// matches a free-text `query` against label > text > value > aria-label >
|
|
1980
|
+
// placeholder > name. We replicate that here against the native registry's
|
|
1981
|
+
// analogues:
|
|
1982
|
+
// 1. `label` (the registered display label)
|
|
1983
|
+
// 2. identifier fields: `testId`, `accessibilityLabel`, `treePath`, `uiId`
|
|
1984
|
+
// 3. registered handler names (e.g. `onPress`, `onChangeText`)
|
|
1985
|
+
//
|
|
1986
|
+
// Match is case-insensitive substring. Empty/missing query falls back to
|
|
1987
|
+
// the structured filters (types/visibleOnly/etc.) so callers that don't
|
|
1988
|
+
// pass `query` still get the legacy behaviour. Once we've narrowed by
|
|
1989
|
+
// query the response is shaped through the executor's NativeFindResponse
|
|
1990
|
+
// envelope so the wire format stays stable.
|
|
1808
1991
|
find: async (ctx) => {
|
|
1809
1992
|
const body = ctx.body;
|
|
1810
1993
|
const response = await executor.find({
|
|
@@ -1812,9 +1995,49 @@ function createServerHandlers(registry, executor, config) {
|
|
|
1812
1995
|
testIdPattern: body?.testIdPattern,
|
|
1813
1996
|
accessibilityLabelPattern: body?.accessibilityLabelPattern,
|
|
1814
1997
|
visibleOnly: body?.visibleOnly,
|
|
1815
|
-
limit
|
|
1998
|
+
// When `query` is supplied we apply our own limit *after* ranking so
|
|
1999
|
+
// the highest-priority matches survive the cap; defer slicing to us
|
|
2000
|
+
// by widening here.
|
|
2001
|
+
limit: body?.query && body.query.length > 0 ? void 0 : body?.limit
|
|
2002
|
+
});
|
|
2003
|
+
const rawQuery = typeof body?.query === "string" ? body.query.trim() : "";
|
|
2004
|
+
if (rawQuery.length === 0) {
|
|
2005
|
+
return success2(response);
|
|
2006
|
+
}
|
|
2007
|
+
const needle = rawQuery.toLowerCase();
|
|
2008
|
+
const containsNeedle = (value) => typeof value === "string" && value.toLowerCase().includes(needle);
|
|
2009
|
+
const scored = [];
|
|
2010
|
+
for (const el of response.elements) {
|
|
2011
|
+
let priorityRank = Number.POSITIVE_INFINITY;
|
|
2012
|
+
if (containsNeedle(el.label)) {
|
|
2013
|
+
priorityRank = Math.min(priorityRank, 0);
|
|
2014
|
+
}
|
|
2015
|
+
const ident = el.identifier;
|
|
2016
|
+
if (ident) {
|
|
2017
|
+
if (containsNeedle(ident.testId)) priorityRank = Math.min(priorityRank, 1);
|
|
2018
|
+
if (containsNeedle(ident.accessibilityLabel)) priorityRank = Math.min(priorityRank, 1);
|
|
2019
|
+
if (containsNeedle(ident.treePath)) priorityRank = Math.min(priorityRank, 1);
|
|
2020
|
+
if (containsNeedle(ident.uiId)) priorityRank = Math.min(priorityRank, 1);
|
|
2021
|
+
}
|
|
2022
|
+
if (priorityRank === Number.POSITIVE_INFINITY) {
|
|
2023
|
+
const registered = registry.getElement(el.id);
|
|
2024
|
+
const handlers = extractHandlerNames(registered?.props);
|
|
2025
|
+
if (handlers.some((h) => h.toLowerCase().includes(needle))) {
|
|
2026
|
+
priorityRank = 2;
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
if (priorityRank !== Number.POSITIVE_INFINITY) {
|
|
2030
|
+
scored.push({ priorityRank, element: el });
|
|
2031
|
+
}
|
|
2032
|
+
}
|
|
2033
|
+
scored.sort((a, b) => a.priorityRank - b.priorityRank);
|
|
2034
|
+
const limit = body?.limit && body.limit > 0 ? body.limit : void 0;
|
|
2035
|
+
const filtered = limit ? scored.slice(0, limit) : scored;
|
|
2036
|
+
return success2({
|
|
2037
|
+
...response,
|
|
2038
|
+
elements: filtered.map((s) => s.element),
|
|
2039
|
+
total: filtered.length
|
|
1816
2040
|
});
|
|
1817
|
-
return success2(response);
|
|
1818
2041
|
},
|
|
1819
2042
|
getSnapshot: async (ctx) => {
|
|
1820
2043
|
const visibleOnly = ctx.query?.visibleOnly === "true" || ctx.body?.visibleOnly === true;
|
|
@@ -1915,15 +2138,162 @@ function createServerHandlers(registry, executor, config) {
|
|
|
1915
2138
|
getMethods: async () => {
|
|
1916
2139
|
return error2("Methods introspection not configured", "NOT_SUPPORTED");
|
|
1917
2140
|
},
|
|
2141
|
+
// AI helpers
|
|
2142
|
+
//
|
|
2143
|
+
// Mobile-side parity with the runner's `POST /ui-bridge/ai/fill-form`.
|
|
2144
|
+
// Each entry in `fields` is dispatched through the existing executor as a
|
|
2145
|
+
// `type` action with `clear: true` so the input value is replaced
|
|
2146
|
+
// atomically (the closest native equivalent to the web bridge's
|
|
2147
|
+
// "setValue" semantics — there is no `setValue` action on native).
|
|
2148
|
+
//
|
|
2149
|
+
// Failures do not short-circuit: every field is attempted, and the
|
|
2150
|
+
// response carries per-field success/error plus aggregate counts.
|
|
2151
|
+
fillForm: async (ctx) => {
|
|
2152
|
+
const body = ctx.body;
|
|
2153
|
+
const rawFields = body?.fields;
|
|
2154
|
+
if (!Array.isArray(rawFields)) {
|
|
2155
|
+
return error2('Request body must include "fields" array', "INVALID_REQUEST");
|
|
2156
|
+
}
|
|
2157
|
+
const results = [];
|
|
2158
|
+
for (let i = 0; i < rawFields.length; i++) {
|
|
2159
|
+
const field = rawFields[i];
|
|
2160
|
+
if (!field || typeof field !== "object") {
|
|
2161
|
+
results.push({
|
|
2162
|
+
elementId: "",
|
|
2163
|
+
success: false,
|
|
2164
|
+
error: `fields[${i}] must be an object with "elementId" and "value"`
|
|
2165
|
+
});
|
|
2166
|
+
continue;
|
|
2167
|
+
}
|
|
2168
|
+
const { elementId, value } = field;
|
|
2169
|
+
if (typeof elementId !== "string" || elementId.length === 0) {
|
|
2170
|
+
results.push({
|
|
2171
|
+
elementId: typeof elementId === "string" ? elementId : "",
|
|
2172
|
+
success: false,
|
|
2173
|
+
error: `fields[${i}] is missing a non-empty "elementId"`
|
|
2174
|
+
});
|
|
2175
|
+
continue;
|
|
2176
|
+
}
|
|
2177
|
+
if (typeof value !== "string") {
|
|
2178
|
+
results.push({
|
|
2179
|
+
elementId,
|
|
2180
|
+
success: false,
|
|
2181
|
+
error: `fields[${i}] is missing a string "value"`
|
|
2182
|
+
});
|
|
2183
|
+
continue;
|
|
2184
|
+
}
|
|
2185
|
+
try {
|
|
2186
|
+
const response = await executor.executeAction(elementId, {
|
|
2187
|
+
action: "type",
|
|
2188
|
+
params: { text: value, clear: true }
|
|
2189
|
+
});
|
|
2190
|
+
if (response.success) {
|
|
2191
|
+
results.push({ elementId, success: true });
|
|
2192
|
+
} else {
|
|
2193
|
+
results.push({
|
|
2194
|
+
elementId,
|
|
2195
|
+
success: false,
|
|
2196
|
+
error: response.error ?? "Action failed"
|
|
2197
|
+
});
|
|
2198
|
+
}
|
|
2199
|
+
} catch (err) {
|
|
2200
|
+
results.push({
|
|
2201
|
+
elementId,
|
|
2202
|
+
success: false,
|
|
2203
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2204
|
+
});
|
|
2205
|
+
}
|
|
2206
|
+
}
|
|
2207
|
+
const succeededCount = results.filter((r) => r.success).length;
|
|
2208
|
+
const failedCount = results.length - succeededCount;
|
|
2209
|
+
return success2({ results, succeededCount, failedCount });
|
|
2210
|
+
},
|
|
2211
|
+
// Test hooks — modal stack manipulation
|
|
2212
|
+
//
|
|
2213
|
+
// These two endpoints exist so external automation runners can drive
|
|
2214
|
+
// ModalDetector state via HTTP, mirroring the in-tree `pushModal` /
|
|
2215
|
+
// `dismissModal` calls that components normally make. They're gated by
|
|
2216
|
+
// `features.testHooks` at the route layer (see http-server.ts); the
|
|
2217
|
+
// handlers themselves still defend against a missing modalDetector
|
|
2218
|
+
// because the bare server path used by CloudRelayClient may run before
|
|
2219
|
+
// enrichers are wired.
|
|
2220
|
+
pushModal: async (ctx) => {
|
|
2221
|
+
const detector = registry.getEnrichers().modalDetector;
|
|
2222
|
+
if (!detector) {
|
|
2223
|
+
return error2(
|
|
2224
|
+
"modal detector not registered",
|
|
2225
|
+
"ENRICHER_UNAVAILABLE"
|
|
2226
|
+
);
|
|
2227
|
+
}
|
|
2228
|
+
const body = ctx.body;
|
|
2229
|
+
const id = body?.id;
|
|
2230
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
2231
|
+
return error2(
|
|
2232
|
+
'pushModal requires a non-empty "id"',
|
|
2233
|
+
"INVALID_REQUEST"
|
|
2234
|
+
);
|
|
2235
|
+
}
|
|
2236
|
+
const allowedTypes = ["modal", "sheet", "drawer", "popover", "alertdialog", "dialog"];
|
|
2237
|
+
const requestedType = body?.type;
|
|
2238
|
+
if (requestedType !== void 0 && (typeof requestedType !== "string" || !allowedTypes.includes(requestedType))) {
|
|
2239
|
+
return error2(
|
|
2240
|
+
`pushModal "type" must be one of: ${allowedTypes.join(", ")}`,
|
|
2241
|
+
"INVALID_REQUEST"
|
|
2242
|
+
);
|
|
2243
|
+
}
|
|
2244
|
+
detector.pushModal({
|
|
2245
|
+
id,
|
|
2246
|
+
title: body?.title,
|
|
2247
|
+
// requestedType is narrowed by the allowedTypes guard above; the cast
|
|
2248
|
+
// converts the validated string to the NativeModalInfo['type'] union.
|
|
2249
|
+
type: requestedType,
|
|
2250
|
+
blocking: body?.blocking,
|
|
2251
|
+
dismissible: body?.dismissible
|
|
2252
|
+
});
|
|
2253
|
+
const stored = detector.getActive().find((m) => m.id === id);
|
|
2254
|
+
return success2({
|
|
2255
|
+
pushed: true,
|
|
2256
|
+
id,
|
|
2257
|
+
title: stored?.title,
|
|
2258
|
+
type: stored?.type,
|
|
2259
|
+
blocking: stored?.blocking,
|
|
2260
|
+
dismissible: stored?.dismissible,
|
|
2261
|
+
pushedAt: stored?.detectedAt ?? Date.now()
|
|
2262
|
+
});
|
|
2263
|
+
},
|
|
2264
|
+
dismissModal: async (ctx) => {
|
|
2265
|
+
const detector = registry.getEnrichers().modalDetector;
|
|
2266
|
+
if (!detector) {
|
|
2267
|
+
return error2(
|
|
2268
|
+
"modal detector not registered",
|
|
2269
|
+
"ENRICHER_UNAVAILABLE"
|
|
2270
|
+
);
|
|
2271
|
+
}
|
|
2272
|
+
const { id } = ctx.params;
|
|
2273
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
2274
|
+
return error2(
|
|
2275
|
+
"dismissModal requires a modal id in the path",
|
|
2276
|
+
"INVALID_REQUEST"
|
|
2277
|
+
);
|
|
2278
|
+
}
|
|
2279
|
+
const wasPresent = detector.getActive().some((m) => m.id === id);
|
|
2280
|
+
if (!wasPresent) {
|
|
2281
|
+
return error2(`modal not found: ${id}`, "MODAL_NOT_FOUND");
|
|
2282
|
+
}
|
|
2283
|
+
detector.dismissModal(id);
|
|
2284
|
+
return success2({ dismissed: id });
|
|
2285
|
+
},
|
|
1918
2286
|
// Design Review
|
|
1919
2287
|
...designHandlers,
|
|
1920
2288
|
// Health
|
|
1921
2289
|
health: async () => {
|
|
1922
2290
|
const stats = registry.getStats();
|
|
2291
|
+
const registration = registry.getRegistrationCoverage();
|
|
1923
2292
|
const response = {
|
|
1924
2293
|
status: "healthy",
|
|
1925
2294
|
timestamp: Date.now(),
|
|
1926
|
-
...stats
|
|
2295
|
+
...stats,
|
|
2296
|
+
registration
|
|
1927
2297
|
};
|
|
1928
2298
|
if (config?.appInfo) {
|
|
1929
2299
|
response.uiBridge = {
|
|
@@ -2024,7 +2394,24 @@ var WS_ROUTES = [
|
|
|
2024
2394
|
{ pattern: "design/evaluate/baseline", handler: "saveBaseline", httpMethods: ["POST"] },
|
|
2025
2395
|
{ pattern: "design/evaluate/diff", handler: "diffBaseline", httpMethods: ["POST"] },
|
|
2026
2396
|
// Meta / Introspection
|
|
2027
|
-
{ pattern: "meta/methods", handler: "getMethods", httpMethods: ["GET"] }
|
|
2397
|
+
{ pattern: "meta/methods", handler: "getMethods", httpMethods: ["GET"] },
|
|
2398
|
+
// AI helpers — mobile parity with the runner's `/ui-bridge/ai/*` family.
|
|
2399
|
+
{ pattern: "ai/fill-form", handler: "fillForm", httpMethods: ["POST"] },
|
|
2400
|
+
// Test hooks — gated behind `features.testHooks`. Let external runners
|
|
2401
|
+
// drive the ModalDetector over HTTP so tests can flip `snapshot.modalStack`
|
|
2402
|
+
// without mounting React components.
|
|
2403
|
+
{
|
|
2404
|
+
pattern: "control/modal/push",
|
|
2405
|
+
handler: "pushModal",
|
|
2406
|
+
httpMethods: ["POST"],
|
|
2407
|
+
requiresTestHooks: true
|
|
2408
|
+
},
|
|
2409
|
+
{
|
|
2410
|
+
pattern: "control/modal/dismiss/:id",
|
|
2411
|
+
handler: "dismissModal",
|
|
2412
|
+
httpMethods: ["POST"],
|
|
2413
|
+
requiresTestHooks: true
|
|
2414
|
+
}
|
|
2028
2415
|
];
|
|
2029
2416
|
var HANDLER_DESCRIPTIONS = {
|
|
2030
2417
|
health: "Liveness probe; returns app metadata + element count",
|
|
@@ -2057,7 +2444,10 @@ var HANDLER_DESCRIPTIONS = {
|
|
|
2057
2444
|
getQualityContexts: "List available quality evaluation contexts",
|
|
2058
2445
|
saveBaseline: "Save a quality baseline snapshot",
|
|
2059
2446
|
diffBaseline: "Diff current UI against a saved baseline",
|
|
2060
|
-
getMethods: "List every handler name (legacy introspection)"
|
|
2447
|
+
getMethods: "List every handler name (legacy introspection)",
|
|
2448
|
+
fillForm: "Fill multiple inputs in one call: body { fields: [{elementId, value}] } \u2014 each field dispatches `type` + `clear:true`",
|
|
2449
|
+
pushModal: "TEST HOOK \u2014 push a modal onto the snapshot.modalStack via the registry ModalDetector",
|
|
2450
|
+
dismissModal: "TEST HOOK \u2014 dismiss a modal by id from the snapshot.modalStack via the registry ModalDetector"
|
|
2061
2451
|
};
|
|
2062
2452
|
var ACTION_REFERENCE = [
|
|
2063
2453
|
{ name: "press", aliases: ["click"], params: "PressActionParams (optional position)" },
|
|
@@ -2074,10 +2464,11 @@ var ACTION_REFERENCE = [
|
|
|
2074
2464
|
{ name: "swipe", params: "SwipeActionParams { direction, distance }" },
|
|
2075
2465
|
{ name: "toggle", params: "none \u2014 flips boolean controls" }
|
|
2076
2466
|
];
|
|
2077
|
-
function buildRoutesPayload() {
|
|
2467
|
+
function buildRoutesPayload(testHooks) {
|
|
2078
2468
|
const entries = [];
|
|
2079
2469
|
for (const r of WS_ROUTES) {
|
|
2080
2470
|
if (!r.httpMethods) continue;
|
|
2471
|
+
if (r.requiresTestHooks && !testHooks) continue;
|
|
2081
2472
|
for (const m of r.httpMethods) {
|
|
2082
2473
|
entries.push({ method: m, path: `/ui-bridge/${r.pattern}` });
|
|
2083
2474
|
}
|
|
@@ -2087,11 +2478,11 @@ function buildRoutesPayload() {
|
|
|
2087
2478
|
entries.sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method));
|
|
2088
2479
|
return { routes: entries, count: entries.length };
|
|
2089
2480
|
}
|
|
2090
|
-
function buildHelpPayload() {
|
|
2481
|
+
function buildHelpPayload(testHooks) {
|
|
2091
2482
|
return {
|
|
2092
2483
|
version: 1,
|
|
2093
2484
|
description: "UI Bridge Native HTTP surface. Prefix every path with /ui-bridge/.",
|
|
2094
|
-
routes: WS_ROUTES.map((r) => ({
|
|
2485
|
+
routes: WS_ROUTES.filter((r) => !r.requiresTestHooks || testHooks).map((r) => ({
|
|
2095
2486
|
pattern: r.pattern,
|
|
2096
2487
|
httpMethods: r.httpMethods ?? [],
|
|
2097
2488
|
wsOnly: !r.httpMethods,
|
|
@@ -2127,7 +2518,8 @@ var NativeUIBridgeServer = class {
|
|
|
2127
2518
|
appInfo: this.config.appInfo
|
|
2128
2519
|
});
|
|
2129
2520
|
this.handlers.getMethods = async () => {
|
|
2130
|
-
const
|
|
2521
|
+
const testHooks = this.config.testHooks === true;
|
|
2522
|
+
const methods = WS_ROUTES.filter((r) => !r.requiresTestHooks || testHooks).map((r) => ({
|
|
2131
2523
|
method: r.pattern,
|
|
2132
2524
|
httpMethods: r.httpMethods ? [...r.httpMethods] : void 0
|
|
2133
2525
|
}));
|
|
@@ -2170,7 +2562,11 @@ var NativeUIBridgeServer = class {
|
|
|
2170
2562
|
timestamp: Date.now()
|
|
2171
2563
|
};
|
|
2172
2564
|
} catch (e) {
|
|
2173
|
-
return {
|
|
2565
|
+
return {
|
|
2566
|
+
success: false,
|
|
2567
|
+
error: `Navigation failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2568
|
+
timestamp: Date.now()
|
|
2569
|
+
};
|
|
2174
2570
|
}
|
|
2175
2571
|
};
|
|
2176
2572
|
this.handlers.pageReplace = async (ctx) => {
|
|
@@ -2196,7 +2592,7 @@ var NativeUIBridgeServer = class {
|
|
|
2196
2592
|
} catch (e) {
|
|
2197
2593
|
return {
|
|
2198
2594
|
success: false,
|
|
2199
|
-
error: `Replace navigation failed: ${e.message}`,
|
|
2595
|
+
error: `Replace navigation failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2200
2596
|
timestamp: Date.now()
|
|
2201
2597
|
};
|
|
2202
2598
|
}
|
|
@@ -2215,7 +2611,7 @@ var NativeUIBridgeServer = class {
|
|
|
2215
2611
|
} catch (e) {
|
|
2216
2612
|
return {
|
|
2217
2613
|
success: false,
|
|
2218
|
-
error: `Back navigation failed: ${e.message}`,
|
|
2614
|
+
error: `Back navigation failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2219
2615
|
timestamp: Date.now()
|
|
2220
2616
|
};
|
|
2221
2617
|
}
|
|
@@ -2236,7 +2632,11 @@ var NativeUIBridgeServer = class {
|
|
|
2236
2632
|
timestamp: Date.now()
|
|
2237
2633
|
};
|
|
2238
2634
|
} catch (e) {
|
|
2239
|
-
return {
|
|
2635
|
+
return {
|
|
2636
|
+
success: false,
|
|
2637
|
+
error: `Refresh failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2638
|
+
timestamp: Date.now()
|
|
2639
|
+
};
|
|
2240
2640
|
}
|
|
2241
2641
|
};
|
|
2242
2642
|
}
|
|
@@ -2294,7 +2694,7 @@ var NativeUIBridgeServer = class {
|
|
|
2294
2694
|
} catch (e) {
|
|
2295
2695
|
return {
|
|
2296
2696
|
success: false,
|
|
2297
|
-
error: `Screenshot capture failed: ${e.message}`,
|
|
2697
|
+
error: `Screenshot capture failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2298
2698
|
timestamp: Date.now()
|
|
2299
2699
|
};
|
|
2300
2700
|
}
|
|
@@ -2849,9 +3249,11 @@ var NativeUIBridgeServer = class {
|
|
|
2849
3249
|
* "control/component/my-comp/action/submit" → handlers.executeComponentAction
|
|
2850
3250
|
*/
|
|
2851
3251
|
async routeMethodToHandler(method, params) {
|
|
3252
|
+
const testHooks = this.config.testHooks === true;
|
|
2852
3253
|
for (const route of WS_ROUTES) {
|
|
2853
3254
|
const pathParams = parsePath(route.pattern, method);
|
|
2854
3255
|
if (!pathParams) continue;
|
|
3256
|
+
if (route.requiresTestHooks && !testHooks) continue;
|
|
2855
3257
|
const ctx = {
|
|
2856
3258
|
params: pathParams,
|
|
2857
3259
|
query: params.query && typeof params.query === "object" ? params.query : {},
|
|
@@ -2929,17 +3331,18 @@ var NativeUIBridgeServer = class {
|
|
|
2929
3331
|
};
|
|
2930
3332
|
}
|
|
2931
3333
|
const stripped = path.slice(PREFIX.length);
|
|
3334
|
+
const testHooks = this.config.testHooks === true;
|
|
2932
3335
|
if (method === "GET" && stripped === "_help") {
|
|
2933
3336
|
return {
|
|
2934
3337
|
success: true,
|
|
2935
|
-
data: buildHelpPayload(),
|
|
3338
|
+
data: buildHelpPayload(testHooks),
|
|
2936
3339
|
timestamp: Date.now()
|
|
2937
3340
|
};
|
|
2938
3341
|
}
|
|
2939
3342
|
if (method === "GET" && stripped === "_routes") {
|
|
2940
3343
|
return {
|
|
2941
3344
|
success: true,
|
|
2942
|
-
data: buildRoutesPayload(),
|
|
3345
|
+
data: buildRoutesPayload(testHooks),
|
|
2943
3346
|
timestamp: Date.now()
|
|
2944
3347
|
};
|
|
2945
3348
|
}
|
|
@@ -2950,6 +3353,9 @@ var NativeUIBridgeServer = class {
|
|
|
2950
3353
|
if (!route.httpMethods) {
|
|
2951
3354
|
continue;
|
|
2952
3355
|
}
|
|
3356
|
+
if (route.requiresTestHooks && !testHooks) {
|
|
3357
|
+
continue;
|
|
3358
|
+
}
|
|
2953
3359
|
if (!route.httpMethods.includes(method)) {
|
|
2954
3360
|
patternMatchedButWrongVerb = true;
|
|
2955
3361
|
continue;
|
|
@@ -3243,6 +3649,27 @@ var WebSocketEventBridge = class {
|
|
|
3243
3649
|
}, this.heartbeatIntervalMs);
|
|
3244
3650
|
}
|
|
3245
3651
|
};
|
|
3652
|
+
|
|
3653
|
+
// src/transport/logger.ts
|
|
3654
|
+
function createTransportLogger() {
|
|
3655
|
+
return {
|
|
3656
|
+
silent: false,
|
|
3657
|
+
log(message, ...args) {
|
|
3658
|
+
if (!this.silent) {
|
|
3659
|
+
console.log(message, ...args);
|
|
3660
|
+
}
|
|
3661
|
+
},
|
|
3662
|
+
warn(message, ...args) {
|
|
3663
|
+
console.warn(message, ...args);
|
|
3664
|
+
},
|
|
3665
|
+
error(message, ...args) {
|
|
3666
|
+
console.error(message, ...args);
|
|
3667
|
+
}
|
|
3668
|
+
};
|
|
3669
|
+
}
|
|
3670
|
+
var transportLogger = createTransportLogger();
|
|
3671
|
+
|
|
3672
|
+
// src/transport/DeviceAnnouncer.ts
|
|
3246
3673
|
var DEFAULT_PORT = 8087;
|
|
3247
3674
|
var RECONNECT_INITIAL_DELAY_MS = 2e3;
|
|
3248
3675
|
var RECONNECT_MAX_DELAY_MS = 6e4;
|
|
@@ -3277,7 +3704,7 @@ var DeviceAnnouncer = class {
|
|
|
3277
3704
|
*/
|
|
3278
3705
|
async startMdnsAdvertise(ZeroconfCtor) {
|
|
3279
3706
|
if (!ZeroconfCtor) {
|
|
3280
|
-
|
|
3707
|
+
transportLogger.log(
|
|
3281
3708
|
"[DeviceAnnouncer] mDNS advertisement skipped (no Zeroconf constructor provided). Pass one to enable mDNS."
|
|
3282
3709
|
);
|
|
3283
3710
|
return;
|
|
@@ -3293,9 +3720,9 @@ var DeviceAnnouncer = class {
|
|
|
3293
3720
|
pairing_token: this.pairingToken
|
|
3294
3721
|
});
|
|
3295
3722
|
this.state = { ...this.state, mdnsActive: true };
|
|
3296
|
-
|
|
3723
|
+
transportLogger.log(`[DeviceAnnouncer] mDNS: advertising "${serviceName}" on port ${port}`);
|
|
3297
3724
|
} catch (err) {
|
|
3298
|
-
|
|
3725
|
+
transportLogger.warn("[DeviceAnnouncer] mDNS start failed:", err);
|
|
3299
3726
|
}
|
|
3300
3727
|
}
|
|
3301
3728
|
// ── Cloud relay ───────────────────────────────────────────────────────────
|
|
@@ -3311,7 +3738,7 @@ var DeviceAnnouncer = class {
|
|
|
3311
3738
|
}
|
|
3312
3739
|
if (this.stopped) return;
|
|
3313
3740
|
const url = `${this.config.cloudRelayUrl}?token=${encodeURIComponent(this.config.cloudToken)}`;
|
|
3314
|
-
|
|
3741
|
+
transportLogger.log("[DeviceAnnouncer] Cloud relay: connecting to", url);
|
|
3315
3742
|
const ws = new WebSocket(url);
|
|
3316
3743
|
this.cloudWs = ws;
|
|
3317
3744
|
ws.onopen = () => {
|
|
@@ -3321,7 +3748,7 @@ var DeviceAnnouncer = class {
|
|
|
3321
3748
|
}
|
|
3322
3749
|
this.reconnectDelay = RECONNECT_INITIAL_DELAY_MS;
|
|
3323
3750
|
this.state = { ...this.state, cloudConnected: true };
|
|
3324
|
-
|
|
3751
|
+
transportLogger.log("[DeviceAnnouncer] Cloud relay: connected");
|
|
3325
3752
|
ws.send(
|
|
3326
3753
|
JSON.stringify({
|
|
3327
3754
|
type: "device_register",
|
|
@@ -3336,7 +3763,7 @@ var DeviceAnnouncer = class {
|
|
|
3336
3763
|
ws.onmessage = (event) => {
|
|
3337
3764
|
try {
|
|
3338
3765
|
const msg = JSON.parse(event.data);
|
|
3339
|
-
|
|
3766
|
+
transportLogger.log("[DeviceAnnouncer] Cloud relay message:", msg.type);
|
|
3340
3767
|
} catch {
|
|
3341
3768
|
}
|
|
3342
3769
|
};
|
|
@@ -3347,7 +3774,7 @@ var DeviceAnnouncer = class {
|
|
|
3347
3774
|
}
|
|
3348
3775
|
};
|
|
3349
3776
|
ws.onerror = (err) => {
|
|
3350
|
-
|
|
3777
|
+
transportLogger.warn("[DeviceAnnouncer] Cloud relay error:", err);
|
|
3351
3778
|
};
|
|
3352
3779
|
}
|
|
3353
3780
|
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
|
@@ -3382,7 +3809,7 @@ var DeviceAnnouncer = class {
|
|
|
3382
3809
|
scheduleReconnect() {
|
|
3383
3810
|
if (this.stopped) return;
|
|
3384
3811
|
const delay = this.reconnectDelay;
|
|
3385
|
-
|
|
3812
|
+
transportLogger.log(`[DeviceAnnouncer] Cloud relay: reconnecting in ${delay}ms`);
|
|
3386
3813
|
this.reconnectTimer = setTimeout(() => {
|
|
3387
3814
|
this.reconnectTimer = null;
|
|
3388
3815
|
if (!this.stopped) {
|
|
@@ -3444,7 +3871,7 @@ var CloudRelayClient = class {
|
|
|
3444
3871
|
connect() {
|
|
3445
3872
|
if (this.stopped) return;
|
|
3446
3873
|
const url = `${this.config.relayUrl}?token=${encodeURIComponent(this.config.authToken)}`;
|
|
3447
|
-
|
|
3874
|
+
transportLogger.log("[CloudRelayClient] Connecting to", url);
|
|
3448
3875
|
const ws = new WebSocket(url);
|
|
3449
3876
|
this.ws = ws;
|
|
3450
3877
|
ws.onopen = () => {
|
|
@@ -3454,7 +3881,7 @@ var CloudRelayClient = class {
|
|
|
3454
3881
|
}
|
|
3455
3882
|
this._isConnected = true;
|
|
3456
3883
|
this.reconnectDelay = INITIAL_RECONNECT_DELAY_MS;
|
|
3457
|
-
|
|
3884
|
+
transportLogger.log("[CloudRelayClient] Connected to relay");
|
|
3458
3885
|
ws.send(
|
|
3459
3886
|
JSON.stringify({
|
|
3460
3887
|
type: "device_register",
|
|
@@ -3472,7 +3899,7 @@ var CloudRelayClient = class {
|
|
|
3472
3899
|
}
|
|
3473
3900
|
};
|
|
3474
3901
|
ws.onerror = (err) => {
|
|
3475
|
-
|
|
3902
|
+
transportLogger.warn("[CloudRelayClient] WebSocket error:", err);
|
|
3476
3903
|
};
|
|
3477
3904
|
}
|
|
3478
3905
|
async onMessage(raw) {
|
|
@@ -3480,12 +3907,12 @@ var CloudRelayClient = class {
|
|
|
3480
3907
|
try {
|
|
3481
3908
|
msg = JSON.parse(raw);
|
|
3482
3909
|
} catch {
|
|
3483
|
-
|
|
3910
|
+
transportLogger.warn("[CloudRelayClient] Received non-JSON message");
|
|
3484
3911
|
return;
|
|
3485
3912
|
}
|
|
3486
3913
|
if (msg["type"] !== "tunnel_request") return;
|
|
3487
3914
|
if (typeof msg["id"] !== "string" || typeof msg["method"] !== "string" || typeof msg["path"] !== "string") {
|
|
3488
|
-
|
|
3915
|
+
transportLogger.warn("[CloudRelayClient] Malformed tunnel_request \u2014 missing required fields");
|
|
3489
3916
|
return;
|
|
3490
3917
|
}
|
|
3491
3918
|
const req = {
|
|
@@ -3526,7 +3953,7 @@ var CloudRelayClient = class {
|
|
|
3526
3953
|
body: bodyValue
|
|
3527
3954
|
};
|
|
3528
3955
|
} catch (err) {
|
|
3529
|
-
|
|
3956
|
+
transportLogger.error("[CloudRelayClient] Error handling tunneled request:", err);
|
|
3530
3957
|
return {
|
|
3531
3958
|
id: req.id,
|
|
3532
3959
|
status: 500,
|
|
@@ -3541,7 +3968,7 @@ var CloudRelayClient = class {
|
|
|
3541
3968
|
scheduleReconnect() {
|
|
3542
3969
|
if (this.stopped) return;
|
|
3543
3970
|
const delay = this.reconnectDelay;
|
|
3544
|
-
|
|
3971
|
+
transportLogger.log(`[CloudRelayClient] Reconnecting in ${delay}ms`);
|
|
3545
3972
|
this.reconnectTimer = setTimeout(() => {
|
|
3546
3973
|
this.reconnectTimer = null;
|
|
3547
3974
|
if (!this.stopped) {
|
|
@@ -3564,6 +3991,192 @@ function parseQueryParams(path) {
|
|
|
3564
3991
|
}
|
|
3565
3992
|
return result;
|
|
3566
3993
|
}
|
|
3994
|
+
|
|
3995
|
+
// src/modal/modal-detector.ts
|
|
3996
|
+
var ModalDetector = class {
|
|
3997
|
+
constructor() {
|
|
3998
|
+
/** Map preserves O(1) idempotent replace by id; ordering comes from detectedAt. */
|
|
3999
|
+
this.modals = /* @__PURE__ */ new Map();
|
|
4000
|
+
}
|
|
4001
|
+
pushModal(modal) {
|
|
4002
|
+
const existing = this.modals.get(modal.id);
|
|
4003
|
+
const detectedAt = existing ? existing.detectedAt : Date.now();
|
|
4004
|
+
const next = {
|
|
4005
|
+
id: modal.id,
|
|
4006
|
+
title: modal.title,
|
|
4007
|
+
type: modal.type ?? "modal",
|
|
4008
|
+
blocking: modal.blocking ?? true,
|
|
4009
|
+
dismissible: modal.dismissible ?? true,
|
|
4010
|
+
detectedAt
|
|
4011
|
+
};
|
|
4012
|
+
this.modals.set(modal.id, next);
|
|
4013
|
+
}
|
|
4014
|
+
/**
|
|
4015
|
+
* Remove a modal from the stack. Returns `true` if a modal with that id
|
|
4016
|
+
* was actually deleted, `false` if nothing matched (idempotent so repeated
|
|
4017
|
+
* calls don't throw, but callers — e.g. the HTTP test-hook endpoint — can
|
|
4018
|
+
* surface the miss to clients).
|
|
4019
|
+
*/
|
|
4020
|
+
dismissModal(id) {
|
|
4021
|
+
return this.modals.delete(id);
|
|
4022
|
+
}
|
|
4023
|
+
getSnapshotModalContext() {
|
|
4024
|
+
const active = this.getActive();
|
|
4025
|
+
const ctx = {
|
|
4026
|
+
modals: active,
|
|
4027
|
+
hasBlockingModal: active.some((m) => m.blocking),
|
|
4028
|
+
count: active.length
|
|
4029
|
+
};
|
|
4030
|
+
if (active.length > 0) {
|
|
4031
|
+
ctx.topModal = active[active.length - 1];
|
|
4032
|
+
}
|
|
4033
|
+
return ctx;
|
|
4034
|
+
}
|
|
4035
|
+
getActive() {
|
|
4036
|
+
return Array.from(this.modals.values()).sort((a, b) => a.detectedAt - b.detectedAt);
|
|
4037
|
+
}
|
|
4038
|
+
clear() {
|
|
4039
|
+
this.modals.clear();
|
|
4040
|
+
}
|
|
4041
|
+
};
|
|
4042
|
+
|
|
4043
|
+
// src/toast/toast-capture.ts
|
|
4044
|
+
var DEFAULT_MAX_RECENT = 50;
|
|
4045
|
+
var ToastCapture = class {
|
|
4046
|
+
constructor(config = {}) {
|
|
4047
|
+
this.active = /* @__PURE__ */ new Map();
|
|
4048
|
+
this.recent = [];
|
|
4049
|
+
this.totalCaptured = 0;
|
|
4050
|
+
this.maxRecent = config.maxRecent ?? DEFAULT_MAX_RECENT;
|
|
4051
|
+
}
|
|
4052
|
+
recordToast(toast) {
|
|
4053
|
+
if (this.active.has(toast.id)) {
|
|
4054
|
+
return;
|
|
4055
|
+
}
|
|
4056
|
+
const appearedAt = toast.appearedAt ?? Date.now();
|
|
4057
|
+
const entry = {
|
|
4058
|
+
id: toast.id,
|
|
4059
|
+
message: toast.message,
|
|
4060
|
+
level: toast.level ?? "info",
|
|
4061
|
+
appearedAt,
|
|
4062
|
+
visible: true,
|
|
4063
|
+
durationMs: 0
|
|
4064
|
+
};
|
|
4065
|
+
this.active.set(toast.id, entry);
|
|
4066
|
+
this.totalCaptured += 1;
|
|
4067
|
+
}
|
|
4068
|
+
dismissToast(id) {
|
|
4069
|
+
const entry = this.active.get(id);
|
|
4070
|
+
if (!entry) {
|
|
4071
|
+
return;
|
|
4072
|
+
}
|
|
4073
|
+
this.active.delete(id);
|
|
4074
|
+
const dismissedAt = Date.now();
|
|
4075
|
+
const dismissed = {
|
|
4076
|
+
...entry,
|
|
4077
|
+
dismissedAt,
|
|
4078
|
+
visible: false,
|
|
4079
|
+
durationMs: dismissedAt - entry.appearedAt
|
|
4080
|
+
};
|
|
4081
|
+
this.recent.push(dismissed);
|
|
4082
|
+
if (this.recent.length > this.maxRecent) {
|
|
4083
|
+
this.recent.shift();
|
|
4084
|
+
}
|
|
4085
|
+
}
|
|
4086
|
+
getSnapshotToastContext() {
|
|
4087
|
+
const now = Date.now();
|
|
4088
|
+
const active = Array.from(this.active.values()).map((entry) => ({
|
|
4089
|
+
...entry,
|
|
4090
|
+
durationMs: now - entry.appearedAt
|
|
4091
|
+
}));
|
|
4092
|
+
return {
|
|
4093
|
+
active,
|
|
4094
|
+
recent: [...this.recent],
|
|
4095
|
+
totalCaptured: this.totalCaptured
|
|
4096
|
+
};
|
|
4097
|
+
}
|
|
4098
|
+
clear() {
|
|
4099
|
+
this.active.clear();
|
|
4100
|
+
this.recent = [];
|
|
4101
|
+
this.totalCaptured = 0;
|
|
4102
|
+
}
|
|
4103
|
+
};
|
|
4104
|
+
|
|
4105
|
+
// src/undo/undo-tracker.ts
|
|
4106
|
+
var DEFAULT_MAX_HISTORY = 50;
|
|
4107
|
+
function buildSummary(ctx) {
|
|
4108
|
+
if (!ctx.canUndo && !ctx.canRedo) {
|
|
4109
|
+
return "no undo available";
|
|
4110
|
+
}
|
|
4111
|
+
const parts = [];
|
|
4112
|
+
if (ctx.canUndo) {
|
|
4113
|
+
parts.push(`undo: '${ctx.undoDescription ?? "available"}'`);
|
|
4114
|
+
}
|
|
4115
|
+
if (ctx.canRedo) {
|
|
4116
|
+
parts.push(`redo: '${ctx.redoDescription ?? "available"}'`);
|
|
4117
|
+
}
|
|
4118
|
+
return parts.join(" / ");
|
|
4119
|
+
}
|
|
4120
|
+
var UndoTracker = class {
|
|
4121
|
+
constructor(config = {}) {
|
|
4122
|
+
this.actions = [];
|
|
4123
|
+
this.declaredState = null;
|
|
4124
|
+
this.maxHistory = config.maxHistory ?? DEFAULT_MAX_HISTORY;
|
|
4125
|
+
}
|
|
4126
|
+
recordAction(record) {
|
|
4127
|
+
this.actions.push(record);
|
|
4128
|
+
if (this.actions.length > this.maxHistory) {
|
|
4129
|
+
this.actions = this.actions.slice(-this.maxHistory);
|
|
4130
|
+
}
|
|
4131
|
+
}
|
|
4132
|
+
setDeclaredState(state) {
|
|
4133
|
+
this.declaredState = state;
|
|
4134
|
+
}
|
|
4135
|
+
getActionHistory() {
|
|
4136
|
+
return this.actions.slice();
|
|
4137
|
+
}
|
|
4138
|
+
getSnapshotUndoContext() {
|
|
4139
|
+
const declared = this.declaredState;
|
|
4140
|
+
if (declared) {
|
|
4141
|
+
const summary2 = buildSummary({
|
|
4142
|
+
canUndo: declared.canUndo,
|
|
4143
|
+
canRedo: declared.canRedo,
|
|
4144
|
+
undoDescription: declared.undoDescription,
|
|
4145
|
+
redoDescription: declared.redoDescription
|
|
4146
|
+
});
|
|
4147
|
+
return {
|
|
4148
|
+
canUndo: declared.canUndo,
|
|
4149
|
+
canRedo: declared.canRedo,
|
|
4150
|
+
undoDescription: declared.undoDescription,
|
|
4151
|
+
redoDescription: declared.redoDescription,
|
|
4152
|
+
undoDepth: declared.undoDepth,
|
|
4153
|
+
redoDepth: declared.redoDepth,
|
|
4154
|
+
summary: summary2
|
|
4155
|
+
};
|
|
4156
|
+
}
|
|
4157
|
+
const canUndo = this.actions.some((a) => a.reversible !== false);
|
|
4158
|
+
const canRedo = false;
|
|
4159
|
+
const lastReversible = [...this.actions].reverse().find((a) => a.reversible !== false);
|
|
4160
|
+
const undoDescription = lastReversible?.description;
|
|
4161
|
+
const summary = buildSummary({ canUndo, canRedo, undoDescription });
|
|
4162
|
+
return {
|
|
4163
|
+
canUndo,
|
|
4164
|
+
canRedo,
|
|
4165
|
+
undoDescription,
|
|
4166
|
+
summary
|
|
4167
|
+
};
|
|
4168
|
+
}
|
|
4169
|
+
executeUndo() {
|
|
4170
|
+
return { ok: false, reason: "Not implemented in native" };
|
|
4171
|
+
}
|
|
4172
|
+
executeRedo() {
|
|
4173
|
+
return { ok: false, reason: "Not implemented in native" };
|
|
4174
|
+
}
|
|
4175
|
+
clear() {
|
|
4176
|
+
this.actions = [];
|
|
4177
|
+
this.declaredState = null;
|
|
4178
|
+
}
|
|
4179
|
+
};
|
|
3567
4180
|
var UIBridgeNativeContext = createContext(null);
|
|
3568
4181
|
function UIBridgeNativeProvider({
|
|
3569
4182
|
children,
|
|
@@ -3580,6 +4193,9 @@ function UIBridgeNativeProvider({
|
|
|
3580
4193
|
}) {
|
|
3581
4194
|
const registryRef = useRef(null);
|
|
3582
4195
|
const executorRef = useRef(null);
|
|
4196
|
+
const modalDetectorRef = useRef(null);
|
|
4197
|
+
const toastCaptureRef = useRef(null);
|
|
4198
|
+
const undoTrackerRef = useRef(null);
|
|
3583
4199
|
const [serverRunning, setServerRunning] = useState(false);
|
|
3584
4200
|
if (!registryRef.current) {
|
|
3585
4201
|
registryRef.current = new NativeUIBridgeRegistry({
|
|
@@ -3593,6 +4209,12 @@ function UIBridgeNativeProvider({
|
|
|
3593
4209
|
executorRef.current = createNativeActionExecutor(registry);
|
|
3594
4210
|
}
|
|
3595
4211
|
const executor = executorRef.current;
|
|
4212
|
+
if (!modalDetectorRef.current) modalDetectorRef.current = new ModalDetector();
|
|
4213
|
+
if (!toastCaptureRef.current) toastCaptureRef.current = new ToastCapture();
|
|
4214
|
+
if (!undoTrackerRef.current) undoTrackerRef.current = new UndoTracker();
|
|
4215
|
+
const modalDetector = modalDetectorRef.current;
|
|
4216
|
+
const toastCapture = toastCaptureRef.current;
|
|
4217
|
+
const undoTracker = undoTrackerRef.current;
|
|
3596
4218
|
const serverRef = useRef(null);
|
|
3597
4219
|
const eventBridgeRef = useRef(null);
|
|
3598
4220
|
const announcerRef = useRef(null);
|
|
@@ -3608,7 +4230,8 @@ function UIBridgeNativeProvider({
|
|
|
3608
4230
|
const server = createNativeServer(registry, executor, {
|
|
3609
4231
|
serverPort: config.serverPort || 8087,
|
|
3610
4232
|
cors: true,
|
|
3611
|
-
appInfo: config.appInfo
|
|
4233
|
+
appInfo: config.appInfo,
|
|
4234
|
+
testHooks: features.testHooks === true
|
|
3612
4235
|
});
|
|
3613
4236
|
server.setAdapter(serverAdapter);
|
|
3614
4237
|
if (navigationProvider) {
|
|
@@ -3662,7 +4285,9 @@ function UIBridgeNativeProvider({
|
|
|
3662
4285
|
}
|
|
3663
4286
|
}, [
|
|
3664
4287
|
features.server,
|
|
4288
|
+
features.testHooks,
|
|
3665
4289
|
config.serverPort,
|
|
4290
|
+
config.appInfo,
|
|
3666
4291
|
registry,
|
|
3667
4292
|
executor,
|
|
3668
4293
|
serverAdapter,
|
|
@@ -3696,7 +4321,8 @@ function UIBridgeNativeProvider({
|
|
|
3696
4321
|
const bareServer = createNativeServer(registry, executor, {
|
|
3697
4322
|
serverPort: config.serverPort || 8087,
|
|
3698
4323
|
cors: true,
|
|
3699
|
-
appInfo: config.appInfo
|
|
4324
|
+
appInfo: config.appInfo,
|
|
4325
|
+
testHooks: features.testHooks === true
|
|
3700
4326
|
});
|
|
3701
4327
|
if (navigationProvider) bareServer.setNavigationProvider(navigationProvider);
|
|
3702
4328
|
if (screenshotProvider) bareServer.setScreenshotProvider(screenshotProvider);
|
|
@@ -3781,6 +4407,22 @@ function UIBridgeNativeProvider({
|
|
|
3781
4407
|
lastRoute = current;
|
|
3782
4408
|
});
|
|
3783
4409
|
}, [routeProvider, registry]);
|
|
4410
|
+
useEffect(() => {
|
|
4411
|
+
registry.setEnrichers({ modalDetector, toastCapture, undoTracker });
|
|
4412
|
+
}, [registry, modalDetector, toastCapture, undoTracker]);
|
|
4413
|
+
useEffect(() => {
|
|
4414
|
+
const unsubscribe = executor.onActionExecuted((event) => {
|
|
4415
|
+
if (!event.success) return;
|
|
4416
|
+
undoTracker.recordAction({
|
|
4417
|
+
id: event.requestId ?? `action-${event.timestamp}`,
|
|
4418
|
+
type: event.action,
|
|
4419
|
+
targetId: event.elementId,
|
|
4420
|
+
timestamp: event.timestamp,
|
|
4421
|
+
reversible: true
|
|
4422
|
+
});
|
|
4423
|
+
});
|
|
4424
|
+
return unsubscribe;
|
|
4425
|
+
}, [executor, undoTracker]);
|
|
3784
4426
|
const on = useCallback(
|
|
3785
4427
|
(type, listener) => registry.on(type, listener),
|
|
3786
4428
|
[registry]
|
|
@@ -3795,6 +4437,9 @@ function UIBridgeNativeProvider({
|
|
|
3795
4437
|
config,
|
|
3796
4438
|
registry,
|
|
3797
4439
|
executor,
|
|
4440
|
+
modalDetector,
|
|
4441
|
+
toastCapture,
|
|
4442
|
+
undoTracker,
|
|
3798
4443
|
getElements,
|
|
3799
4444
|
getComponents,
|
|
3800
4445
|
createSnapshot,
|
|
@@ -3811,6 +4456,9 @@ function UIBridgeNativeProvider({
|
|
|
3811
4456
|
config,
|
|
3812
4457
|
registry,
|
|
3813
4458
|
executor,
|
|
4459
|
+
modalDetector,
|
|
4460
|
+
toastCapture,
|
|
4461
|
+
undoTracker,
|
|
3814
4462
|
getElements,
|
|
3815
4463
|
getComponents,
|
|
3816
4464
|
createSnapshot,
|
|
@@ -3905,7 +4553,7 @@ function useUIElement(options) {
|
|
|
3905
4553
|
registeredRef.current = false;
|
|
3906
4554
|
registeredIdRef.current = null;
|
|
3907
4555
|
setRegistered(false);
|
|
3908
|
-
}, [bridge,
|
|
4556
|
+
}, [bridge, id]);
|
|
3909
4557
|
const onLayout = useCallback(
|
|
3910
4558
|
(event) => {
|
|
3911
4559
|
const { x, y, width, height } = event.nativeEvent.layout;
|
|
@@ -4171,7 +4819,7 @@ function useUIComponent(options) {
|
|
|
4171
4819
|
const registeredComponent = useMemo(() => {
|
|
4172
4820
|
if (!bridge) return null;
|
|
4173
4821
|
return bridge.registry.getComponent(id) || null;
|
|
4174
|
-
}, [bridge, id
|
|
4822
|
+
}, [bridge, id]);
|
|
4175
4823
|
return {
|
|
4176
4824
|
registered: registeredRef.current,
|
|
4177
4825
|
executeAction,
|
|
@@ -4291,6 +4939,37 @@ function useUIBridgeRequired() {
|
|
|
4291
4939
|
}
|
|
4292
4940
|
return result;
|
|
4293
4941
|
}
|
|
4942
|
+
function useUIBridgeModal(opts) {
|
|
4943
|
+
const { modalDetector } = useUIBridgeNative();
|
|
4944
|
+
const { id, title, type, blocking, dismissible } = opts;
|
|
4945
|
+
useEffect(() => {
|
|
4946
|
+
modalDetector.pushModal({ id, title, type, blocking, dismissible });
|
|
4947
|
+
return () => {
|
|
4948
|
+
modalDetector.dismissModal(id);
|
|
4949
|
+
};
|
|
4950
|
+
}, [modalDetector, id, title, type, blocking, dismissible]);
|
|
4951
|
+
}
|
|
4952
|
+
function useUIBridgeToast(opts) {
|
|
4953
|
+
const { toastCapture } = useUIBridgeNative();
|
|
4954
|
+
const { id, message, level } = opts;
|
|
4955
|
+
useEffect(() => {
|
|
4956
|
+
toastCapture.recordToast({ id, message, level });
|
|
4957
|
+
return () => {
|
|
4958
|
+
toastCapture.dismissToast(id);
|
|
4959
|
+
};
|
|
4960
|
+
}, [toastCapture, id, message, level]);
|
|
4961
|
+
}
|
|
4962
|
+
function useToastRecorder() {
|
|
4963
|
+
const { toastCapture } = useUIBridgeNative();
|
|
4964
|
+
const recordToast = useCallback(
|
|
4965
|
+
(toast) => {
|
|
4966
|
+
toastCapture.recordToast(toast);
|
|
4967
|
+
},
|
|
4968
|
+
[toastCapture]
|
|
4969
|
+
);
|
|
4970
|
+
const dismissToast = useCallback((id) => toastCapture.dismissToast(id), [toastCapture]);
|
|
4971
|
+
return { recordToast, dismissToast };
|
|
4972
|
+
}
|
|
4294
4973
|
|
|
4295
4974
|
// src/server/types.ts
|
|
4296
4975
|
var UI_BRIDGE_NATIVE_ROUTES = {
|
|
@@ -4420,6 +5099,25 @@ var UI_BRIDGE_NATIVE_ROUTES = {
|
|
|
4420
5099
|
path: "/ui-bridge/design/style-guide",
|
|
4421
5100
|
description: "Clear loaded style guide"
|
|
4422
5101
|
},
|
|
5102
|
+
// AI helpers
|
|
5103
|
+
AI_FILL_FORM: {
|
|
5104
|
+
method: "POST",
|
|
5105
|
+
path: "/ui-bridge/ai/fill-form",
|
|
5106
|
+
description: "Fill multiple input elements in a single call"
|
|
5107
|
+
},
|
|
5108
|
+
// Test hooks — drive ModalDetector state from outside the React tree
|
|
5109
|
+
// (gated by `features.testHooks`). Mirrors `pushModal` / `dismissModal`
|
|
5110
|
+
// calls that components normally make in-process.
|
|
5111
|
+
PUSH_MODAL: {
|
|
5112
|
+
method: "POST",
|
|
5113
|
+
path: "/ui-bridge/control/modal/push",
|
|
5114
|
+
description: "Push a modal onto the modal stack (testHooks)"
|
|
5115
|
+
},
|
|
5116
|
+
DISMISS_MODAL: {
|
|
5117
|
+
method: "POST",
|
|
5118
|
+
path: "/ui-bridge/control/modal/dismiss/:id",
|
|
5119
|
+
description: "Dismiss a modal by id (testHooks)"
|
|
5120
|
+
},
|
|
4423
5121
|
// Health
|
|
4424
5122
|
HEALTH: {
|
|
4425
5123
|
method: "GET",
|
|
@@ -4929,8 +5627,8 @@ function UIBridgeInspector({
|
|
|
4929
5627
|
const bridge = useUIBridgeNativeOptional();
|
|
4930
5628
|
const [showInspector, setShowInspector] = useState(visible);
|
|
4931
5629
|
const [selectedElement, setSelectedElement] = useState(null);
|
|
4932
|
-
const elements = useMemo(() => bridge ? bridge.getElements() : [], [bridge
|
|
4933
|
-
const components = useMemo(() => bridge ? bridge.getComponents() : [], [bridge
|
|
5630
|
+
const elements = useMemo(() => bridge ? bridge.getElements() : [], [bridge]);
|
|
5631
|
+
const components = useMemo(() => bridge ? bridge.getComponents() : [], [bridge]);
|
|
4934
5632
|
const handleToggle = useCallback(() => {
|
|
4935
5633
|
setShowInspector((prev) => !prev);
|
|
4936
5634
|
}, []);
|
|
@@ -5192,6 +5890,6 @@ var styles = StyleSheet.create({
|
|
|
5192
5890
|
}
|
|
5193
5891
|
});
|
|
5194
5892
|
|
|
5195
|
-
export { CloudRelayClient, DefaultNativeActionExecutor, DeviceAnnouncer, NativeUIBridgeRegistry, NativeUIBridgeServer, UIBridgeInspector, UIBridgeNativeProvider, UI_BRIDGE_NATIVE_ROUTES, WebSocketConnection, WebSocketEventBridge, buildTreePath, buildUpgradeResponse, captureNativeResponsiveSnapshot, captureNativeStateVariations, computeAcceptKey, createDesignHandlers, createNativeActionExecutor, createNativeElementIdentifier, createNativeServer, createServerHandlers, extractHandlerNames, findAllByPattern, findElementByIdentifier, getGlobalRegistry, getNativeElementDesignData, mapNativeStyleToExtended, matchesIdentifier, parseTreePath, resetGlobalRegistry, setGlobalRegistry, useUIBridge, useUIBridgeNative, useUIBridgeNativeOptional, useUIBridgeNativeRequired, useUIBridgeRequired, useUIComponent, useUIComponentAction, useUIElement, useUIElementWithProps };
|
|
5893
|
+
export { CloudRelayClient, DefaultNativeActionExecutor, DeviceAnnouncer, ModalDetector, NativeUIBridgeRegistry, NativeUIBridgeServer, ToastCapture, UIBridgeInspector, UIBridgeNativeProvider, UI_BRIDGE_NATIVE_ROUTES, UndoTracker, WebSocketConnection, WebSocketEventBridge, buildTreePath, buildUpgradeResponse, captureNativeResponsiveSnapshot, captureNativeStateVariations, computeAcceptKey, createDesignHandlers, createNativeActionExecutor, createNativeElementIdentifier, createNativeServer, createServerHandlers, extractHandlerNames, findAllByPattern, findElementByIdentifier, getGlobalRegistry, getNativeElementDesignData, mapNativeStyleToExtended, matchesIdentifier, parseTreePath, resetGlobalRegistry, setGlobalRegistry, useToastRecorder, useUIBridge, useUIBridgeModal, useUIBridgeNative, useUIBridgeNativeOptional, useUIBridgeNativeRequired, useUIBridgeRequired, useUIBridgeToast, useUIComponent, useUIComponentAction, useUIElement, useUIElementWithProps };
|
|
5196
5894
|
//# sourceMappingURL=index.mjs.map
|
|
5197
5895
|
//# sourceMappingURL=index.mjs.map
|