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