@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/react/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
|
*/
|
|
@@ -545,6 +651,23 @@ function sleep(ms) {
|
|
|
545
651
|
var DefaultNativeActionExecutor = class {
|
|
546
652
|
constructor(registry) {
|
|
547
653
|
this.registry = registry;
|
|
654
|
+
this.actionListeners = /* @__PURE__ */ new Set();
|
|
655
|
+
}
|
|
656
|
+
onActionExecuted(listener) {
|
|
657
|
+
this.actionListeners.add(listener);
|
|
658
|
+
return () => {
|
|
659
|
+
this.actionListeners.delete(listener);
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
/** Notify listeners out-of-band; isolate each invocation so one bad listener can't poison the rest. */
|
|
663
|
+
emitActionEvent(event) {
|
|
664
|
+
for (const listener of this.actionListeners) {
|
|
665
|
+
try {
|
|
666
|
+
listener(event);
|
|
667
|
+
} catch (error3) {
|
|
668
|
+
console.warn(`[ui-bridge-native] Action listener error:`, error3);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
548
671
|
}
|
|
549
672
|
/**
|
|
550
673
|
* Execute an action on an element
|
|
@@ -558,11 +681,23 @@ var DefaultNativeActionExecutor = class {
|
|
|
558
681
|
registered = findElementByIdentifier(elementId) ?? void 0;
|
|
559
682
|
}
|
|
560
683
|
if (!registered) {
|
|
684
|
+
const failTime = Date.now();
|
|
685
|
+
const errorMsg = `Element not found: ${elementId}`;
|
|
686
|
+
this.emitActionEvent({
|
|
687
|
+
elementId,
|
|
688
|
+
action: request.action,
|
|
689
|
+
params: request.params,
|
|
690
|
+
success: false,
|
|
691
|
+
error: errorMsg,
|
|
692
|
+
timestamp: failTime,
|
|
693
|
+
requestId: request.requestId,
|
|
694
|
+
durationMs: failTime - startTime
|
|
695
|
+
});
|
|
561
696
|
return {
|
|
562
697
|
success: false,
|
|
563
|
-
error:
|
|
564
|
-
durationMs:
|
|
565
|
-
timestamp:
|
|
698
|
+
error: errorMsg,
|
|
699
|
+
durationMs: failTime - startTime,
|
|
700
|
+
timestamp: failTime,
|
|
566
701
|
requestId: request.requestId
|
|
567
702
|
};
|
|
568
703
|
}
|
|
@@ -570,33 +705,67 @@ var DefaultNativeActionExecutor = class {
|
|
|
570
705
|
const waitResult = await this.waitForElementInternal(registered.id, request.waitOptions);
|
|
571
706
|
waitDurationMs = waitResult.waitedMs;
|
|
572
707
|
if (!waitResult.met) {
|
|
708
|
+
const failTime = Date.now();
|
|
709
|
+
const errorMsg = waitResult.error || "Wait condition not met";
|
|
710
|
+
this.emitActionEvent({
|
|
711
|
+
elementId,
|
|
712
|
+
action: request.action,
|
|
713
|
+
params: request.params,
|
|
714
|
+
success: false,
|
|
715
|
+
error: errorMsg,
|
|
716
|
+
timestamp: failTime,
|
|
717
|
+
requestId: request.requestId,
|
|
718
|
+
durationMs: failTime - startTime
|
|
719
|
+
});
|
|
573
720
|
return {
|
|
574
721
|
success: false,
|
|
575
|
-
error:
|
|
576
|
-
durationMs:
|
|
577
|
-
timestamp:
|
|
722
|
+
error: errorMsg,
|
|
723
|
+
durationMs: failTime - startTime,
|
|
724
|
+
timestamp: failTime,
|
|
578
725
|
requestId: request.requestId,
|
|
579
726
|
waitDurationMs
|
|
580
727
|
};
|
|
581
728
|
}
|
|
582
729
|
}
|
|
583
730
|
const result = await this.performAction(registered, request.action, request.params);
|
|
731
|
+
const successTime = Date.now();
|
|
732
|
+
this.emitActionEvent({
|
|
733
|
+
elementId,
|
|
734
|
+
action: request.action,
|
|
735
|
+
params: request.params,
|
|
736
|
+
success: true,
|
|
737
|
+
timestamp: successTime,
|
|
738
|
+
requestId: request.requestId,
|
|
739
|
+
durationMs: successTime - startTime
|
|
740
|
+
});
|
|
584
741
|
return {
|
|
585
742
|
success: true,
|
|
586
743
|
elementState: registered.getState(),
|
|
587
744
|
result,
|
|
588
|
-
durationMs:
|
|
589
|
-
timestamp:
|
|
745
|
+
durationMs: successTime - startTime,
|
|
746
|
+
timestamp: successTime,
|
|
590
747
|
requestId: request.requestId,
|
|
591
748
|
waitDurationMs
|
|
592
749
|
};
|
|
593
750
|
} catch (error3) {
|
|
751
|
+
const failTime = Date.now();
|
|
752
|
+
const errorMsg = error3 instanceof Error ? error3.message : String(error3);
|
|
753
|
+
this.emitActionEvent({
|
|
754
|
+
elementId,
|
|
755
|
+
action: request.action,
|
|
756
|
+
params: request.params,
|
|
757
|
+
success: false,
|
|
758
|
+
error: errorMsg,
|
|
759
|
+
timestamp: failTime,
|
|
760
|
+
requestId: request.requestId,
|
|
761
|
+
durationMs: failTime - startTime
|
|
762
|
+
});
|
|
594
763
|
return {
|
|
595
764
|
success: false,
|
|
596
|
-
error:
|
|
765
|
+
error: errorMsg,
|
|
597
766
|
stack: error3 instanceof Error ? error3.stack : void 0,
|
|
598
|
-
durationMs:
|
|
599
|
-
timestamp:
|
|
767
|
+
durationMs: failTime - startTime,
|
|
768
|
+
timestamp: failTime,
|
|
600
769
|
requestId: request.requestId,
|
|
601
770
|
waitDurationMs
|
|
602
771
|
};
|
|
@@ -1755,6 +1924,20 @@ function createServerHandlers(registry, executor, config) {
|
|
|
1755
1924
|
return success2(response);
|
|
1756
1925
|
},
|
|
1757
1926
|
// Discovery
|
|
1927
|
+
//
|
|
1928
|
+
// Native parity with the runner's `POST /ui-bridge/ai/find`. The runner
|
|
1929
|
+
// matches a free-text `query` against label > text > value > aria-label >
|
|
1930
|
+
// placeholder > name. We replicate that here against the native registry's
|
|
1931
|
+
// analogues:
|
|
1932
|
+
// 1. `label` (the registered display label)
|
|
1933
|
+
// 2. identifier fields: `testId`, `accessibilityLabel`, `treePath`, `uiId`
|
|
1934
|
+
// 3. registered handler names (e.g. `onPress`, `onChangeText`)
|
|
1935
|
+
//
|
|
1936
|
+
// Match is case-insensitive substring. Empty/missing query falls back to
|
|
1937
|
+
// the structured filters (types/visibleOnly/etc.) so callers that don't
|
|
1938
|
+
// pass `query` still get the legacy behaviour. Once we've narrowed by
|
|
1939
|
+
// query the response is shaped through the executor's NativeFindResponse
|
|
1940
|
+
// envelope so the wire format stays stable.
|
|
1758
1941
|
find: async (ctx) => {
|
|
1759
1942
|
const body = ctx.body;
|
|
1760
1943
|
const response = await executor.find({
|
|
@@ -1762,9 +1945,49 @@ function createServerHandlers(registry, executor, config) {
|
|
|
1762
1945
|
testIdPattern: body?.testIdPattern,
|
|
1763
1946
|
accessibilityLabelPattern: body?.accessibilityLabelPattern,
|
|
1764
1947
|
visibleOnly: body?.visibleOnly,
|
|
1765
|
-
limit
|
|
1948
|
+
// When `query` is supplied we apply our own limit *after* ranking so
|
|
1949
|
+
// the highest-priority matches survive the cap; defer slicing to us
|
|
1950
|
+
// by widening here.
|
|
1951
|
+
limit: body?.query && body.query.length > 0 ? void 0 : body?.limit
|
|
1952
|
+
});
|
|
1953
|
+
const rawQuery = typeof body?.query === "string" ? body.query.trim() : "";
|
|
1954
|
+
if (rawQuery.length === 0) {
|
|
1955
|
+
return success2(response);
|
|
1956
|
+
}
|
|
1957
|
+
const needle = rawQuery.toLowerCase();
|
|
1958
|
+
const containsNeedle = (value) => typeof value === "string" && value.toLowerCase().includes(needle);
|
|
1959
|
+
const scored = [];
|
|
1960
|
+
for (const el of response.elements) {
|
|
1961
|
+
let priorityRank = Number.POSITIVE_INFINITY;
|
|
1962
|
+
if (containsNeedle(el.label)) {
|
|
1963
|
+
priorityRank = Math.min(priorityRank, 0);
|
|
1964
|
+
}
|
|
1965
|
+
const ident = el.identifier;
|
|
1966
|
+
if (ident) {
|
|
1967
|
+
if (containsNeedle(ident.testId)) priorityRank = Math.min(priorityRank, 1);
|
|
1968
|
+
if (containsNeedle(ident.accessibilityLabel)) priorityRank = Math.min(priorityRank, 1);
|
|
1969
|
+
if (containsNeedle(ident.treePath)) priorityRank = Math.min(priorityRank, 1);
|
|
1970
|
+
if (containsNeedle(ident.uiId)) priorityRank = Math.min(priorityRank, 1);
|
|
1971
|
+
}
|
|
1972
|
+
if (priorityRank === Number.POSITIVE_INFINITY) {
|
|
1973
|
+
const registered = registry.getElement(el.id);
|
|
1974
|
+
const handlers = extractHandlerNames(registered?.props);
|
|
1975
|
+
if (handlers.some((h) => h.toLowerCase().includes(needle))) {
|
|
1976
|
+
priorityRank = 2;
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
if (priorityRank !== Number.POSITIVE_INFINITY) {
|
|
1980
|
+
scored.push({ priorityRank, element: el });
|
|
1981
|
+
}
|
|
1982
|
+
}
|
|
1983
|
+
scored.sort((a, b) => a.priorityRank - b.priorityRank);
|
|
1984
|
+
const limit = body?.limit && body.limit > 0 ? body.limit : void 0;
|
|
1985
|
+
const filtered = limit ? scored.slice(0, limit) : scored;
|
|
1986
|
+
return success2({
|
|
1987
|
+
...response,
|
|
1988
|
+
elements: filtered.map((s) => s.element),
|
|
1989
|
+
total: filtered.length
|
|
1766
1990
|
});
|
|
1767
|
-
return success2(response);
|
|
1768
1991
|
},
|
|
1769
1992
|
getSnapshot: async (ctx) => {
|
|
1770
1993
|
const visibleOnly = ctx.query?.visibleOnly === "true" || ctx.body?.visibleOnly === true;
|
|
@@ -1865,15 +2088,162 @@ function createServerHandlers(registry, executor, config) {
|
|
|
1865
2088
|
getMethods: async () => {
|
|
1866
2089
|
return error2("Methods introspection not configured", "NOT_SUPPORTED");
|
|
1867
2090
|
},
|
|
2091
|
+
// AI helpers
|
|
2092
|
+
//
|
|
2093
|
+
// Mobile-side parity with the runner's `POST /ui-bridge/ai/fill-form`.
|
|
2094
|
+
// Each entry in `fields` is dispatched through the existing executor as a
|
|
2095
|
+
// `type` action with `clear: true` so the input value is replaced
|
|
2096
|
+
// atomically (the closest native equivalent to the web bridge's
|
|
2097
|
+
// "setValue" semantics — there is no `setValue` action on native).
|
|
2098
|
+
//
|
|
2099
|
+
// Failures do not short-circuit: every field is attempted, and the
|
|
2100
|
+
// response carries per-field success/error plus aggregate counts.
|
|
2101
|
+
fillForm: async (ctx) => {
|
|
2102
|
+
const body = ctx.body;
|
|
2103
|
+
const rawFields = body?.fields;
|
|
2104
|
+
if (!Array.isArray(rawFields)) {
|
|
2105
|
+
return error2('Request body must include "fields" array', "INVALID_REQUEST");
|
|
2106
|
+
}
|
|
2107
|
+
const results = [];
|
|
2108
|
+
for (let i = 0; i < rawFields.length; i++) {
|
|
2109
|
+
const field = rawFields[i];
|
|
2110
|
+
if (!field || typeof field !== "object") {
|
|
2111
|
+
results.push({
|
|
2112
|
+
elementId: "",
|
|
2113
|
+
success: false,
|
|
2114
|
+
error: `fields[${i}] must be an object with "elementId" and "value"`
|
|
2115
|
+
});
|
|
2116
|
+
continue;
|
|
2117
|
+
}
|
|
2118
|
+
const { elementId, value } = field;
|
|
2119
|
+
if (typeof elementId !== "string" || elementId.length === 0) {
|
|
2120
|
+
results.push({
|
|
2121
|
+
elementId: typeof elementId === "string" ? elementId : "",
|
|
2122
|
+
success: false,
|
|
2123
|
+
error: `fields[${i}] is missing a non-empty "elementId"`
|
|
2124
|
+
});
|
|
2125
|
+
continue;
|
|
2126
|
+
}
|
|
2127
|
+
if (typeof value !== "string") {
|
|
2128
|
+
results.push({
|
|
2129
|
+
elementId,
|
|
2130
|
+
success: false,
|
|
2131
|
+
error: `fields[${i}] is missing a string "value"`
|
|
2132
|
+
});
|
|
2133
|
+
continue;
|
|
2134
|
+
}
|
|
2135
|
+
try {
|
|
2136
|
+
const response = await executor.executeAction(elementId, {
|
|
2137
|
+
action: "type",
|
|
2138
|
+
params: { text: value, clear: true }
|
|
2139
|
+
});
|
|
2140
|
+
if (response.success) {
|
|
2141
|
+
results.push({ elementId, success: true });
|
|
2142
|
+
} else {
|
|
2143
|
+
results.push({
|
|
2144
|
+
elementId,
|
|
2145
|
+
success: false,
|
|
2146
|
+
error: response.error ?? "Action failed"
|
|
2147
|
+
});
|
|
2148
|
+
}
|
|
2149
|
+
} catch (err) {
|
|
2150
|
+
results.push({
|
|
2151
|
+
elementId,
|
|
2152
|
+
success: false,
|
|
2153
|
+
error: err instanceof Error ? err.message : String(err)
|
|
2154
|
+
});
|
|
2155
|
+
}
|
|
2156
|
+
}
|
|
2157
|
+
const succeededCount = results.filter((r) => r.success).length;
|
|
2158
|
+
const failedCount = results.length - succeededCount;
|
|
2159
|
+
return success2({ results, succeededCount, failedCount });
|
|
2160
|
+
},
|
|
2161
|
+
// Test hooks — modal stack manipulation
|
|
2162
|
+
//
|
|
2163
|
+
// These two endpoints exist so external automation runners can drive
|
|
2164
|
+
// ModalDetector state via HTTP, mirroring the in-tree `pushModal` /
|
|
2165
|
+
// `dismissModal` calls that components normally make. They're gated by
|
|
2166
|
+
// `features.testHooks` at the route layer (see http-server.ts); the
|
|
2167
|
+
// handlers themselves still defend against a missing modalDetector
|
|
2168
|
+
// because the bare server path used by CloudRelayClient may run before
|
|
2169
|
+
// enrichers are wired.
|
|
2170
|
+
pushModal: async (ctx) => {
|
|
2171
|
+
const detector = registry.getEnrichers().modalDetector;
|
|
2172
|
+
if (!detector) {
|
|
2173
|
+
return error2(
|
|
2174
|
+
"modal detector not registered",
|
|
2175
|
+
"ENRICHER_UNAVAILABLE"
|
|
2176
|
+
);
|
|
2177
|
+
}
|
|
2178
|
+
const body = ctx.body;
|
|
2179
|
+
const id = body?.id;
|
|
2180
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
2181
|
+
return error2(
|
|
2182
|
+
'pushModal requires a non-empty "id"',
|
|
2183
|
+
"INVALID_REQUEST"
|
|
2184
|
+
);
|
|
2185
|
+
}
|
|
2186
|
+
const allowedTypes = ["modal", "sheet", "drawer", "popover", "alertdialog", "dialog"];
|
|
2187
|
+
const requestedType = body?.type;
|
|
2188
|
+
if (requestedType !== void 0 && (typeof requestedType !== "string" || !allowedTypes.includes(requestedType))) {
|
|
2189
|
+
return error2(
|
|
2190
|
+
`pushModal "type" must be one of: ${allowedTypes.join(", ")}`,
|
|
2191
|
+
"INVALID_REQUEST"
|
|
2192
|
+
);
|
|
2193
|
+
}
|
|
2194
|
+
detector.pushModal({
|
|
2195
|
+
id,
|
|
2196
|
+
title: body?.title,
|
|
2197
|
+
// requestedType is narrowed by the allowedTypes guard above; the cast
|
|
2198
|
+
// converts the validated string to the NativeModalInfo['type'] union.
|
|
2199
|
+
type: requestedType,
|
|
2200
|
+
blocking: body?.blocking,
|
|
2201
|
+
dismissible: body?.dismissible
|
|
2202
|
+
});
|
|
2203
|
+
const stored = detector.getActive().find((m) => m.id === id);
|
|
2204
|
+
return success2({
|
|
2205
|
+
pushed: true,
|
|
2206
|
+
id,
|
|
2207
|
+
title: stored?.title,
|
|
2208
|
+
type: stored?.type,
|
|
2209
|
+
blocking: stored?.blocking,
|
|
2210
|
+
dismissible: stored?.dismissible,
|
|
2211
|
+
pushedAt: stored?.detectedAt ?? Date.now()
|
|
2212
|
+
});
|
|
2213
|
+
},
|
|
2214
|
+
dismissModal: async (ctx) => {
|
|
2215
|
+
const detector = registry.getEnrichers().modalDetector;
|
|
2216
|
+
if (!detector) {
|
|
2217
|
+
return error2(
|
|
2218
|
+
"modal detector not registered",
|
|
2219
|
+
"ENRICHER_UNAVAILABLE"
|
|
2220
|
+
);
|
|
2221
|
+
}
|
|
2222
|
+
const { id } = ctx.params;
|
|
2223
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
2224
|
+
return error2(
|
|
2225
|
+
"dismissModal requires a modal id in the path",
|
|
2226
|
+
"INVALID_REQUEST"
|
|
2227
|
+
);
|
|
2228
|
+
}
|
|
2229
|
+
const wasPresent = detector.getActive().some((m) => m.id === id);
|
|
2230
|
+
if (!wasPresent) {
|
|
2231
|
+
return error2(`modal not found: ${id}`, "MODAL_NOT_FOUND");
|
|
2232
|
+
}
|
|
2233
|
+
detector.dismissModal(id);
|
|
2234
|
+
return success2({ dismissed: id });
|
|
2235
|
+
},
|
|
1868
2236
|
// Design Review
|
|
1869
2237
|
...designHandlers,
|
|
1870
2238
|
// Health
|
|
1871
2239
|
health: async () => {
|
|
1872
2240
|
const stats = registry.getStats();
|
|
2241
|
+
const registration = registry.getRegistrationCoverage();
|
|
1873
2242
|
const response = {
|
|
1874
2243
|
status: "healthy",
|
|
1875
2244
|
timestamp: Date.now(),
|
|
1876
|
-
...stats
|
|
2245
|
+
...stats,
|
|
2246
|
+
registration
|
|
1877
2247
|
};
|
|
1878
2248
|
if (config?.appInfo) {
|
|
1879
2249
|
response.uiBridge = {
|
|
@@ -1974,7 +2344,24 @@ var WS_ROUTES = [
|
|
|
1974
2344
|
{ pattern: "design/evaluate/baseline", handler: "saveBaseline", httpMethods: ["POST"] },
|
|
1975
2345
|
{ pattern: "design/evaluate/diff", handler: "diffBaseline", httpMethods: ["POST"] },
|
|
1976
2346
|
// Meta / Introspection
|
|
1977
|
-
{ pattern: "meta/methods", handler: "getMethods", httpMethods: ["GET"] }
|
|
2347
|
+
{ pattern: "meta/methods", handler: "getMethods", httpMethods: ["GET"] },
|
|
2348
|
+
// AI helpers — mobile parity with the runner's `/ui-bridge/ai/*` family.
|
|
2349
|
+
{ pattern: "ai/fill-form", handler: "fillForm", httpMethods: ["POST"] },
|
|
2350
|
+
// Test hooks — gated behind `features.testHooks`. Let external runners
|
|
2351
|
+
// drive the ModalDetector over HTTP so tests can flip `snapshot.modalStack`
|
|
2352
|
+
// without mounting React components.
|
|
2353
|
+
{
|
|
2354
|
+
pattern: "control/modal/push",
|
|
2355
|
+
handler: "pushModal",
|
|
2356
|
+
httpMethods: ["POST"],
|
|
2357
|
+
requiresTestHooks: true
|
|
2358
|
+
},
|
|
2359
|
+
{
|
|
2360
|
+
pattern: "control/modal/dismiss/:id",
|
|
2361
|
+
handler: "dismissModal",
|
|
2362
|
+
httpMethods: ["POST"],
|
|
2363
|
+
requiresTestHooks: true
|
|
2364
|
+
}
|
|
1978
2365
|
];
|
|
1979
2366
|
var HANDLER_DESCRIPTIONS = {
|
|
1980
2367
|
health: "Liveness probe; returns app metadata + element count",
|
|
@@ -2007,7 +2394,10 @@ var HANDLER_DESCRIPTIONS = {
|
|
|
2007
2394
|
getQualityContexts: "List available quality evaluation contexts",
|
|
2008
2395
|
saveBaseline: "Save a quality baseline snapshot",
|
|
2009
2396
|
diffBaseline: "Diff current UI against a saved baseline",
|
|
2010
|
-
getMethods: "List every handler name (legacy introspection)"
|
|
2397
|
+
getMethods: "List every handler name (legacy introspection)",
|
|
2398
|
+
fillForm: "Fill multiple inputs in one call: body { fields: [{elementId, value}] } \u2014 each field dispatches `type` + `clear:true`",
|
|
2399
|
+
pushModal: "TEST HOOK \u2014 push a modal onto the snapshot.modalStack via the registry ModalDetector",
|
|
2400
|
+
dismissModal: "TEST HOOK \u2014 dismiss a modal by id from the snapshot.modalStack via the registry ModalDetector"
|
|
2011
2401
|
};
|
|
2012
2402
|
var ACTION_REFERENCE = [
|
|
2013
2403
|
{ name: "press", aliases: ["click"], params: "PressActionParams (optional position)" },
|
|
@@ -2024,10 +2414,11 @@ var ACTION_REFERENCE = [
|
|
|
2024
2414
|
{ name: "swipe", params: "SwipeActionParams { direction, distance }" },
|
|
2025
2415
|
{ name: "toggle", params: "none \u2014 flips boolean controls" }
|
|
2026
2416
|
];
|
|
2027
|
-
function buildRoutesPayload() {
|
|
2417
|
+
function buildRoutesPayload(testHooks) {
|
|
2028
2418
|
const entries = [];
|
|
2029
2419
|
for (const r of WS_ROUTES) {
|
|
2030
2420
|
if (!r.httpMethods) continue;
|
|
2421
|
+
if (r.requiresTestHooks && !testHooks) continue;
|
|
2031
2422
|
for (const m of r.httpMethods) {
|
|
2032
2423
|
entries.push({ method: m, path: `/ui-bridge/${r.pattern}` });
|
|
2033
2424
|
}
|
|
@@ -2037,11 +2428,11 @@ function buildRoutesPayload() {
|
|
|
2037
2428
|
entries.sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method));
|
|
2038
2429
|
return { routes: entries, count: entries.length };
|
|
2039
2430
|
}
|
|
2040
|
-
function buildHelpPayload() {
|
|
2431
|
+
function buildHelpPayload(testHooks) {
|
|
2041
2432
|
return {
|
|
2042
2433
|
version: 1,
|
|
2043
2434
|
description: "UI Bridge Native HTTP surface. Prefix every path with /ui-bridge/.",
|
|
2044
|
-
routes: WS_ROUTES.map((r) => ({
|
|
2435
|
+
routes: WS_ROUTES.filter((r) => !r.requiresTestHooks || testHooks).map((r) => ({
|
|
2045
2436
|
pattern: r.pattern,
|
|
2046
2437
|
httpMethods: r.httpMethods ?? [],
|
|
2047
2438
|
wsOnly: !r.httpMethods,
|
|
@@ -2077,7 +2468,8 @@ var NativeUIBridgeServer = class {
|
|
|
2077
2468
|
appInfo: this.config.appInfo
|
|
2078
2469
|
});
|
|
2079
2470
|
this.handlers.getMethods = async () => {
|
|
2080
|
-
const
|
|
2471
|
+
const testHooks = this.config.testHooks === true;
|
|
2472
|
+
const methods = WS_ROUTES.filter((r) => !r.requiresTestHooks || testHooks).map((r) => ({
|
|
2081
2473
|
method: r.pattern,
|
|
2082
2474
|
httpMethods: r.httpMethods ? [...r.httpMethods] : void 0
|
|
2083
2475
|
}));
|
|
@@ -2120,7 +2512,11 @@ var NativeUIBridgeServer = class {
|
|
|
2120
2512
|
timestamp: Date.now()
|
|
2121
2513
|
};
|
|
2122
2514
|
} catch (e) {
|
|
2123
|
-
return {
|
|
2515
|
+
return {
|
|
2516
|
+
success: false,
|
|
2517
|
+
error: `Navigation failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2518
|
+
timestamp: Date.now()
|
|
2519
|
+
};
|
|
2124
2520
|
}
|
|
2125
2521
|
};
|
|
2126
2522
|
this.handlers.pageReplace = async (ctx) => {
|
|
@@ -2146,7 +2542,7 @@ var NativeUIBridgeServer = class {
|
|
|
2146
2542
|
} catch (e) {
|
|
2147
2543
|
return {
|
|
2148
2544
|
success: false,
|
|
2149
|
-
error: `Replace navigation failed: ${e.message}`,
|
|
2545
|
+
error: `Replace navigation failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2150
2546
|
timestamp: Date.now()
|
|
2151
2547
|
};
|
|
2152
2548
|
}
|
|
@@ -2165,7 +2561,7 @@ var NativeUIBridgeServer = class {
|
|
|
2165
2561
|
} catch (e) {
|
|
2166
2562
|
return {
|
|
2167
2563
|
success: false,
|
|
2168
|
-
error: `Back navigation failed: ${e.message}`,
|
|
2564
|
+
error: `Back navigation failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2169
2565
|
timestamp: Date.now()
|
|
2170
2566
|
};
|
|
2171
2567
|
}
|
|
@@ -2186,7 +2582,11 @@ var NativeUIBridgeServer = class {
|
|
|
2186
2582
|
timestamp: Date.now()
|
|
2187
2583
|
};
|
|
2188
2584
|
} catch (e) {
|
|
2189
|
-
return {
|
|
2585
|
+
return {
|
|
2586
|
+
success: false,
|
|
2587
|
+
error: `Refresh failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2588
|
+
timestamp: Date.now()
|
|
2589
|
+
};
|
|
2190
2590
|
}
|
|
2191
2591
|
};
|
|
2192
2592
|
}
|
|
@@ -2244,7 +2644,7 @@ var NativeUIBridgeServer = class {
|
|
|
2244
2644
|
} catch (e) {
|
|
2245
2645
|
return {
|
|
2246
2646
|
success: false,
|
|
2247
|
-
error: `Screenshot capture failed: ${e.message}`,
|
|
2647
|
+
error: `Screenshot capture failed: ${e instanceof Error ? e.message : String(e)}`,
|
|
2248
2648
|
timestamp: Date.now()
|
|
2249
2649
|
};
|
|
2250
2650
|
}
|
|
@@ -2799,9 +3199,11 @@ var NativeUIBridgeServer = class {
|
|
|
2799
3199
|
* "control/component/my-comp/action/submit" → handlers.executeComponentAction
|
|
2800
3200
|
*/
|
|
2801
3201
|
async routeMethodToHandler(method, params) {
|
|
3202
|
+
const testHooks = this.config.testHooks === true;
|
|
2802
3203
|
for (const route of WS_ROUTES) {
|
|
2803
3204
|
const pathParams = parsePath(route.pattern, method);
|
|
2804
3205
|
if (!pathParams) continue;
|
|
3206
|
+
if (route.requiresTestHooks && !testHooks) continue;
|
|
2805
3207
|
const ctx = {
|
|
2806
3208
|
params: pathParams,
|
|
2807
3209
|
query: params.query && typeof params.query === "object" ? params.query : {},
|
|
@@ -2879,17 +3281,18 @@ var NativeUIBridgeServer = class {
|
|
|
2879
3281
|
};
|
|
2880
3282
|
}
|
|
2881
3283
|
const stripped = path.slice(PREFIX.length);
|
|
3284
|
+
const testHooks = this.config.testHooks === true;
|
|
2882
3285
|
if (method === "GET" && stripped === "_help") {
|
|
2883
3286
|
return {
|
|
2884
3287
|
success: true,
|
|
2885
|
-
data: buildHelpPayload(),
|
|
3288
|
+
data: buildHelpPayload(testHooks),
|
|
2886
3289
|
timestamp: Date.now()
|
|
2887
3290
|
};
|
|
2888
3291
|
}
|
|
2889
3292
|
if (method === "GET" && stripped === "_routes") {
|
|
2890
3293
|
return {
|
|
2891
3294
|
success: true,
|
|
2892
|
-
data: buildRoutesPayload(),
|
|
3295
|
+
data: buildRoutesPayload(testHooks),
|
|
2893
3296
|
timestamp: Date.now()
|
|
2894
3297
|
};
|
|
2895
3298
|
}
|
|
@@ -2900,6 +3303,9 @@ var NativeUIBridgeServer = class {
|
|
|
2900
3303
|
if (!route.httpMethods) {
|
|
2901
3304
|
continue;
|
|
2902
3305
|
}
|
|
3306
|
+
if (route.requiresTestHooks && !testHooks) {
|
|
3307
|
+
continue;
|
|
3308
|
+
}
|
|
2903
3309
|
if (!route.httpMethods.includes(method)) {
|
|
2904
3310
|
patternMatchedButWrongVerb = true;
|
|
2905
3311
|
continue;
|
|
@@ -3193,6 +3599,27 @@ var WebSocketEventBridge = class {
|
|
|
3193
3599
|
}, this.heartbeatIntervalMs);
|
|
3194
3600
|
}
|
|
3195
3601
|
};
|
|
3602
|
+
|
|
3603
|
+
// src/transport/logger.ts
|
|
3604
|
+
function createTransportLogger() {
|
|
3605
|
+
return {
|
|
3606
|
+
silent: false,
|
|
3607
|
+
log(message, ...args) {
|
|
3608
|
+
if (!this.silent) {
|
|
3609
|
+
console.log(message, ...args);
|
|
3610
|
+
}
|
|
3611
|
+
},
|
|
3612
|
+
warn(message, ...args) {
|
|
3613
|
+
console.warn(message, ...args);
|
|
3614
|
+
},
|
|
3615
|
+
error(message, ...args) {
|
|
3616
|
+
console.error(message, ...args);
|
|
3617
|
+
}
|
|
3618
|
+
};
|
|
3619
|
+
}
|
|
3620
|
+
var transportLogger = createTransportLogger();
|
|
3621
|
+
|
|
3622
|
+
// src/transport/DeviceAnnouncer.ts
|
|
3196
3623
|
var DEFAULT_PORT = 8087;
|
|
3197
3624
|
var RECONNECT_INITIAL_DELAY_MS = 2e3;
|
|
3198
3625
|
var RECONNECT_MAX_DELAY_MS = 6e4;
|
|
@@ -3227,7 +3654,7 @@ var DeviceAnnouncer = class {
|
|
|
3227
3654
|
*/
|
|
3228
3655
|
async startMdnsAdvertise(ZeroconfCtor) {
|
|
3229
3656
|
if (!ZeroconfCtor) {
|
|
3230
|
-
|
|
3657
|
+
transportLogger.log(
|
|
3231
3658
|
"[DeviceAnnouncer] mDNS advertisement skipped (no Zeroconf constructor provided). Pass one to enable mDNS."
|
|
3232
3659
|
);
|
|
3233
3660
|
return;
|
|
@@ -3243,9 +3670,9 @@ var DeviceAnnouncer = class {
|
|
|
3243
3670
|
pairing_token: this.pairingToken
|
|
3244
3671
|
});
|
|
3245
3672
|
this.state = { ...this.state, mdnsActive: true };
|
|
3246
|
-
|
|
3673
|
+
transportLogger.log(`[DeviceAnnouncer] mDNS: advertising "${serviceName}" on port ${port}`);
|
|
3247
3674
|
} catch (err) {
|
|
3248
|
-
|
|
3675
|
+
transportLogger.warn("[DeviceAnnouncer] mDNS start failed:", err);
|
|
3249
3676
|
}
|
|
3250
3677
|
}
|
|
3251
3678
|
// ── Cloud relay ───────────────────────────────────────────────────────────
|
|
@@ -3261,7 +3688,7 @@ var DeviceAnnouncer = class {
|
|
|
3261
3688
|
}
|
|
3262
3689
|
if (this.stopped) return;
|
|
3263
3690
|
const url = `${this.config.cloudRelayUrl}?token=${encodeURIComponent(this.config.cloudToken)}`;
|
|
3264
|
-
|
|
3691
|
+
transportLogger.log("[DeviceAnnouncer] Cloud relay: connecting to", url);
|
|
3265
3692
|
const ws = new WebSocket(url);
|
|
3266
3693
|
this.cloudWs = ws;
|
|
3267
3694
|
ws.onopen = () => {
|
|
@@ -3271,7 +3698,7 @@ var DeviceAnnouncer = class {
|
|
|
3271
3698
|
}
|
|
3272
3699
|
this.reconnectDelay = RECONNECT_INITIAL_DELAY_MS;
|
|
3273
3700
|
this.state = { ...this.state, cloudConnected: true };
|
|
3274
|
-
|
|
3701
|
+
transportLogger.log("[DeviceAnnouncer] Cloud relay: connected");
|
|
3275
3702
|
ws.send(
|
|
3276
3703
|
JSON.stringify({
|
|
3277
3704
|
type: "device_register",
|
|
@@ -3286,7 +3713,7 @@ var DeviceAnnouncer = class {
|
|
|
3286
3713
|
ws.onmessage = (event) => {
|
|
3287
3714
|
try {
|
|
3288
3715
|
const msg = JSON.parse(event.data);
|
|
3289
|
-
|
|
3716
|
+
transportLogger.log("[DeviceAnnouncer] Cloud relay message:", msg.type);
|
|
3290
3717
|
} catch {
|
|
3291
3718
|
}
|
|
3292
3719
|
};
|
|
@@ -3297,7 +3724,7 @@ var DeviceAnnouncer = class {
|
|
|
3297
3724
|
}
|
|
3298
3725
|
};
|
|
3299
3726
|
ws.onerror = (err) => {
|
|
3300
|
-
|
|
3727
|
+
transportLogger.warn("[DeviceAnnouncer] Cloud relay error:", err);
|
|
3301
3728
|
};
|
|
3302
3729
|
}
|
|
3303
3730
|
// ── Lifecycle ─────────────────────────────────────────────────────────────
|
|
@@ -3332,7 +3759,7 @@ var DeviceAnnouncer = class {
|
|
|
3332
3759
|
scheduleReconnect() {
|
|
3333
3760
|
if (this.stopped) return;
|
|
3334
3761
|
const delay = this.reconnectDelay;
|
|
3335
|
-
|
|
3762
|
+
transportLogger.log(`[DeviceAnnouncer] Cloud relay: reconnecting in ${delay}ms`);
|
|
3336
3763
|
this.reconnectTimer = setTimeout(() => {
|
|
3337
3764
|
this.reconnectTimer = null;
|
|
3338
3765
|
if (!this.stopped) {
|
|
@@ -3394,7 +3821,7 @@ var CloudRelayClient = class {
|
|
|
3394
3821
|
connect() {
|
|
3395
3822
|
if (this.stopped) return;
|
|
3396
3823
|
const url = `${this.config.relayUrl}?token=${encodeURIComponent(this.config.authToken)}`;
|
|
3397
|
-
|
|
3824
|
+
transportLogger.log("[CloudRelayClient] Connecting to", url);
|
|
3398
3825
|
const ws = new WebSocket(url);
|
|
3399
3826
|
this.ws = ws;
|
|
3400
3827
|
ws.onopen = () => {
|
|
@@ -3404,7 +3831,7 @@ var CloudRelayClient = class {
|
|
|
3404
3831
|
}
|
|
3405
3832
|
this._isConnected = true;
|
|
3406
3833
|
this.reconnectDelay = INITIAL_RECONNECT_DELAY_MS;
|
|
3407
|
-
|
|
3834
|
+
transportLogger.log("[CloudRelayClient] Connected to relay");
|
|
3408
3835
|
ws.send(
|
|
3409
3836
|
JSON.stringify({
|
|
3410
3837
|
type: "device_register",
|
|
@@ -3422,7 +3849,7 @@ var CloudRelayClient = class {
|
|
|
3422
3849
|
}
|
|
3423
3850
|
};
|
|
3424
3851
|
ws.onerror = (err) => {
|
|
3425
|
-
|
|
3852
|
+
transportLogger.warn("[CloudRelayClient] WebSocket error:", err);
|
|
3426
3853
|
};
|
|
3427
3854
|
}
|
|
3428
3855
|
async onMessage(raw) {
|
|
@@ -3430,12 +3857,12 @@ var CloudRelayClient = class {
|
|
|
3430
3857
|
try {
|
|
3431
3858
|
msg = JSON.parse(raw);
|
|
3432
3859
|
} catch {
|
|
3433
|
-
|
|
3860
|
+
transportLogger.warn("[CloudRelayClient] Received non-JSON message");
|
|
3434
3861
|
return;
|
|
3435
3862
|
}
|
|
3436
3863
|
if (msg["type"] !== "tunnel_request") return;
|
|
3437
3864
|
if (typeof msg["id"] !== "string" || typeof msg["method"] !== "string" || typeof msg["path"] !== "string") {
|
|
3438
|
-
|
|
3865
|
+
transportLogger.warn("[CloudRelayClient] Malformed tunnel_request \u2014 missing required fields");
|
|
3439
3866
|
return;
|
|
3440
3867
|
}
|
|
3441
3868
|
const req = {
|
|
@@ -3476,7 +3903,7 @@ var CloudRelayClient = class {
|
|
|
3476
3903
|
body: bodyValue
|
|
3477
3904
|
};
|
|
3478
3905
|
} catch (err) {
|
|
3479
|
-
|
|
3906
|
+
transportLogger.error("[CloudRelayClient] Error handling tunneled request:", err);
|
|
3480
3907
|
return {
|
|
3481
3908
|
id: req.id,
|
|
3482
3909
|
status: 500,
|
|
@@ -3491,7 +3918,7 @@ var CloudRelayClient = class {
|
|
|
3491
3918
|
scheduleReconnect() {
|
|
3492
3919
|
if (this.stopped) return;
|
|
3493
3920
|
const delay = this.reconnectDelay;
|
|
3494
|
-
|
|
3921
|
+
transportLogger.log(`[CloudRelayClient] Reconnecting in ${delay}ms`);
|
|
3495
3922
|
this.reconnectTimer = setTimeout(() => {
|
|
3496
3923
|
this.reconnectTimer = null;
|
|
3497
3924
|
if (!this.stopped) {
|
|
@@ -3514,6 +3941,192 @@ function parseQueryParams(path) {
|
|
|
3514
3941
|
}
|
|
3515
3942
|
return result;
|
|
3516
3943
|
}
|
|
3944
|
+
|
|
3945
|
+
// src/modal/modal-detector.ts
|
|
3946
|
+
var ModalDetector = class {
|
|
3947
|
+
constructor() {
|
|
3948
|
+
/** Map preserves O(1) idempotent replace by id; ordering comes from detectedAt. */
|
|
3949
|
+
this.modals = /* @__PURE__ */ new Map();
|
|
3950
|
+
}
|
|
3951
|
+
pushModal(modal) {
|
|
3952
|
+
const existing = this.modals.get(modal.id);
|
|
3953
|
+
const detectedAt = existing ? existing.detectedAt : Date.now();
|
|
3954
|
+
const next = {
|
|
3955
|
+
id: modal.id,
|
|
3956
|
+
title: modal.title,
|
|
3957
|
+
type: modal.type ?? "modal",
|
|
3958
|
+
blocking: modal.blocking ?? true,
|
|
3959
|
+
dismissible: modal.dismissible ?? true,
|
|
3960
|
+
detectedAt
|
|
3961
|
+
};
|
|
3962
|
+
this.modals.set(modal.id, next);
|
|
3963
|
+
}
|
|
3964
|
+
/**
|
|
3965
|
+
* Remove a modal from the stack. Returns `true` if a modal with that id
|
|
3966
|
+
* was actually deleted, `false` if nothing matched (idempotent so repeated
|
|
3967
|
+
* calls don't throw, but callers — e.g. the HTTP test-hook endpoint — can
|
|
3968
|
+
* surface the miss to clients).
|
|
3969
|
+
*/
|
|
3970
|
+
dismissModal(id) {
|
|
3971
|
+
return this.modals.delete(id);
|
|
3972
|
+
}
|
|
3973
|
+
getSnapshotModalContext() {
|
|
3974
|
+
const active = this.getActive();
|
|
3975
|
+
const ctx = {
|
|
3976
|
+
modals: active,
|
|
3977
|
+
hasBlockingModal: active.some((m) => m.blocking),
|
|
3978
|
+
count: active.length
|
|
3979
|
+
};
|
|
3980
|
+
if (active.length > 0) {
|
|
3981
|
+
ctx.topModal = active[active.length - 1];
|
|
3982
|
+
}
|
|
3983
|
+
return ctx;
|
|
3984
|
+
}
|
|
3985
|
+
getActive() {
|
|
3986
|
+
return Array.from(this.modals.values()).sort((a, b) => a.detectedAt - b.detectedAt);
|
|
3987
|
+
}
|
|
3988
|
+
clear() {
|
|
3989
|
+
this.modals.clear();
|
|
3990
|
+
}
|
|
3991
|
+
};
|
|
3992
|
+
|
|
3993
|
+
// src/toast/toast-capture.ts
|
|
3994
|
+
var DEFAULT_MAX_RECENT = 50;
|
|
3995
|
+
var ToastCapture = class {
|
|
3996
|
+
constructor(config = {}) {
|
|
3997
|
+
this.active = /* @__PURE__ */ new Map();
|
|
3998
|
+
this.recent = [];
|
|
3999
|
+
this.totalCaptured = 0;
|
|
4000
|
+
this.maxRecent = config.maxRecent ?? DEFAULT_MAX_RECENT;
|
|
4001
|
+
}
|
|
4002
|
+
recordToast(toast) {
|
|
4003
|
+
if (this.active.has(toast.id)) {
|
|
4004
|
+
return;
|
|
4005
|
+
}
|
|
4006
|
+
const appearedAt = toast.appearedAt ?? Date.now();
|
|
4007
|
+
const entry = {
|
|
4008
|
+
id: toast.id,
|
|
4009
|
+
message: toast.message,
|
|
4010
|
+
level: toast.level ?? "info",
|
|
4011
|
+
appearedAt,
|
|
4012
|
+
visible: true,
|
|
4013
|
+
durationMs: 0
|
|
4014
|
+
};
|
|
4015
|
+
this.active.set(toast.id, entry);
|
|
4016
|
+
this.totalCaptured += 1;
|
|
4017
|
+
}
|
|
4018
|
+
dismissToast(id) {
|
|
4019
|
+
const entry = this.active.get(id);
|
|
4020
|
+
if (!entry) {
|
|
4021
|
+
return;
|
|
4022
|
+
}
|
|
4023
|
+
this.active.delete(id);
|
|
4024
|
+
const dismissedAt = Date.now();
|
|
4025
|
+
const dismissed = {
|
|
4026
|
+
...entry,
|
|
4027
|
+
dismissedAt,
|
|
4028
|
+
visible: false,
|
|
4029
|
+
durationMs: dismissedAt - entry.appearedAt
|
|
4030
|
+
};
|
|
4031
|
+
this.recent.push(dismissed);
|
|
4032
|
+
if (this.recent.length > this.maxRecent) {
|
|
4033
|
+
this.recent.shift();
|
|
4034
|
+
}
|
|
4035
|
+
}
|
|
4036
|
+
getSnapshotToastContext() {
|
|
4037
|
+
const now = Date.now();
|
|
4038
|
+
const active = Array.from(this.active.values()).map((entry) => ({
|
|
4039
|
+
...entry,
|
|
4040
|
+
durationMs: now - entry.appearedAt
|
|
4041
|
+
}));
|
|
4042
|
+
return {
|
|
4043
|
+
active,
|
|
4044
|
+
recent: [...this.recent],
|
|
4045
|
+
totalCaptured: this.totalCaptured
|
|
4046
|
+
};
|
|
4047
|
+
}
|
|
4048
|
+
clear() {
|
|
4049
|
+
this.active.clear();
|
|
4050
|
+
this.recent = [];
|
|
4051
|
+
this.totalCaptured = 0;
|
|
4052
|
+
}
|
|
4053
|
+
};
|
|
4054
|
+
|
|
4055
|
+
// src/undo/undo-tracker.ts
|
|
4056
|
+
var DEFAULT_MAX_HISTORY = 50;
|
|
4057
|
+
function buildSummary(ctx) {
|
|
4058
|
+
if (!ctx.canUndo && !ctx.canRedo) {
|
|
4059
|
+
return "no undo available";
|
|
4060
|
+
}
|
|
4061
|
+
const parts = [];
|
|
4062
|
+
if (ctx.canUndo) {
|
|
4063
|
+
parts.push(`undo: '${ctx.undoDescription ?? "available"}'`);
|
|
4064
|
+
}
|
|
4065
|
+
if (ctx.canRedo) {
|
|
4066
|
+
parts.push(`redo: '${ctx.redoDescription ?? "available"}'`);
|
|
4067
|
+
}
|
|
4068
|
+
return parts.join(" / ");
|
|
4069
|
+
}
|
|
4070
|
+
var UndoTracker = class {
|
|
4071
|
+
constructor(config = {}) {
|
|
4072
|
+
this.actions = [];
|
|
4073
|
+
this.declaredState = null;
|
|
4074
|
+
this.maxHistory = config.maxHistory ?? DEFAULT_MAX_HISTORY;
|
|
4075
|
+
}
|
|
4076
|
+
recordAction(record) {
|
|
4077
|
+
this.actions.push(record);
|
|
4078
|
+
if (this.actions.length > this.maxHistory) {
|
|
4079
|
+
this.actions = this.actions.slice(-this.maxHistory);
|
|
4080
|
+
}
|
|
4081
|
+
}
|
|
4082
|
+
setDeclaredState(state) {
|
|
4083
|
+
this.declaredState = state;
|
|
4084
|
+
}
|
|
4085
|
+
getActionHistory() {
|
|
4086
|
+
return this.actions.slice();
|
|
4087
|
+
}
|
|
4088
|
+
getSnapshotUndoContext() {
|
|
4089
|
+
const declared = this.declaredState;
|
|
4090
|
+
if (declared) {
|
|
4091
|
+
const summary2 = buildSummary({
|
|
4092
|
+
canUndo: declared.canUndo,
|
|
4093
|
+
canRedo: declared.canRedo,
|
|
4094
|
+
undoDescription: declared.undoDescription,
|
|
4095
|
+
redoDescription: declared.redoDescription
|
|
4096
|
+
});
|
|
4097
|
+
return {
|
|
4098
|
+
canUndo: declared.canUndo,
|
|
4099
|
+
canRedo: declared.canRedo,
|
|
4100
|
+
undoDescription: declared.undoDescription,
|
|
4101
|
+
redoDescription: declared.redoDescription,
|
|
4102
|
+
undoDepth: declared.undoDepth,
|
|
4103
|
+
redoDepth: declared.redoDepth,
|
|
4104
|
+
summary: summary2
|
|
4105
|
+
};
|
|
4106
|
+
}
|
|
4107
|
+
const canUndo = this.actions.some((a) => a.reversible !== false);
|
|
4108
|
+
const canRedo = false;
|
|
4109
|
+
const lastReversible = [...this.actions].reverse().find((a) => a.reversible !== false);
|
|
4110
|
+
const undoDescription = lastReversible?.description;
|
|
4111
|
+
const summary = buildSummary({ canUndo, canRedo, undoDescription });
|
|
4112
|
+
return {
|
|
4113
|
+
canUndo,
|
|
4114
|
+
canRedo,
|
|
4115
|
+
undoDescription,
|
|
4116
|
+
summary
|
|
4117
|
+
};
|
|
4118
|
+
}
|
|
4119
|
+
executeUndo() {
|
|
4120
|
+
return { ok: false, reason: "Not implemented in native" };
|
|
4121
|
+
}
|
|
4122
|
+
executeRedo() {
|
|
4123
|
+
return { ok: false, reason: "Not implemented in native" };
|
|
4124
|
+
}
|
|
4125
|
+
clear() {
|
|
4126
|
+
this.actions = [];
|
|
4127
|
+
this.declaredState = null;
|
|
4128
|
+
}
|
|
4129
|
+
};
|
|
3517
4130
|
var UIBridgeNativeContext = react.createContext(null);
|
|
3518
4131
|
function UIBridgeNativeProvider({
|
|
3519
4132
|
children,
|
|
@@ -3530,6 +4143,9 @@ function UIBridgeNativeProvider({
|
|
|
3530
4143
|
}) {
|
|
3531
4144
|
const registryRef = react.useRef(null);
|
|
3532
4145
|
const executorRef = react.useRef(null);
|
|
4146
|
+
const modalDetectorRef = react.useRef(null);
|
|
4147
|
+
const toastCaptureRef = react.useRef(null);
|
|
4148
|
+
const undoTrackerRef = react.useRef(null);
|
|
3533
4149
|
const [serverRunning, setServerRunning] = react.useState(false);
|
|
3534
4150
|
if (!registryRef.current) {
|
|
3535
4151
|
registryRef.current = new NativeUIBridgeRegistry({
|
|
@@ -3543,6 +4159,12 @@ function UIBridgeNativeProvider({
|
|
|
3543
4159
|
executorRef.current = createNativeActionExecutor(registry);
|
|
3544
4160
|
}
|
|
3545
4161
|
const executor = executorRef.current;
|
|
4162
|
+
if (!modalDetectorRef.current) modalDetectorRef.current = new ModalDetector();
|
|
4163
|
+
if (!toastCaptureRef.current) toastCaptureRef.current = new ToastCapture();
|
|
4164
|
+
if (!undoTrackerRef.current) undoTrackerRef.current = new UndoTracker();
|
|
4165
|
+
const modalDetector = modalDetectorRef.current;
|
|
4166
|
+
const toastCapture = toastCaptureRef.current;
|
|
4167
|
+
const undoTracker = undoTrackerRef.current;
|
|
3546
4168
|
const serverRef = react.useRef(null);
|
|
3547
4169
|
const eventBridgeRef = react.useRef(null);
|
|
3548
4170
|
const announcerRef = react.useRef(null);
|
|
@@ -3558,7 +4180,8 @@ function UIBridgeNativeProvider({
|
|
|
3558
4180
|
const server = createNativeServer(registry, executor, {
|
|
3559
4181
|
serverPort: config.serverPort || 8087,
|
|
3560
4182
|
cors: true,
|
|
3561
|
-
appInfo: config.appInfo
|
|
4183
|
+
appInfo: config.appInfo,
|
|
4184
|
+
testHooks: features.testHooks === true
|
|
3562
4185
|
});
|
|
3563
4186
|
server.setAdapter(serverAdapter);
|
|
3564
4187
|
if (navigationProvider) {
|
|
@@ -3612,7 +4235,9 @@ function UIBridgeNativeProvider({
|
|
|
3612
4235
|
}
|
|
3613
4236
|
}, [
|
|
3614
4237
|
features.server,
|
|
4238
|
+
features.testHooks,
|
|
3615
4239
|
config.serverPort,
|
|
4240
|
+
config.appInfo,
|
|
3616
4241
|
registry,
|
|
3617
4242
|
executor,
|
|
3618
4243
|
serverAdapter,
|
|
@@ -3646,7 +4271,8 @@ function UIBridgeNativeProvider({
|
|
|
3646
4271
|
const bareServer = createNativeServer(registry, executor, {
|
|
3647
4272
|
serverPort: config.serverPort || 8087,
|
|
3648
4273
|
cors: true,
|
|
3649
|
-
appInfo: config.appInfo
|
|
4274
|
+
appInfo: config.appInfo,
|
|
4275
|
+
testHooks: features.testHooks === true
|
|
3650
4276
|
});
|
|
3651
4277
|
if (navigationProvider) bareServer.setNavigationProvider(navigationProvider);
|
|
3652
4278
|
if (screenshotProvider) bareServer.setScreenshotProvider(screenshotProvider);
|
|
@@ -3731,6 +4357,22 @@ function UIBridgeNativeProvider({
|
|
|
3731
4357
|
lastRoute = current;
|
|
3732
4358
|
});
|
|
3733
4359
|
}, [routeProvider, registry]);
|
|
4360
|
+
react.useEffect(() => {
|
|
4361
|
+
registry.setEnrichers({ modalDetector, toastCapture, undoTracker });
|
|
4362
|
+
}, [registry, modalDetector, toastCapture, undoTracker]);
|
|
4363
|
+
react.useEffect(() => {
|
|
4364
|
+
const unsubscribe = executor.onActionExecuted((event) => {
|
|
4365
|
+
if (!event.success) return;
|
|
4366
|
+
undoTracker.recordAction({
|
|
4367
|
+
id: event.requestId ?? `action-${event.timestamp}`,
|
|
4368
|
+
type: event.action,
|
|
4369
|
+
targetId: event.elementId,
|
|
4370
|
+
timestamp: event.timestamp,
|
|
4371
|
+
reversible: true
|
|
4372
|
+
});
|
|
4373
|
+
});
|
|
4374
|
+
return unsubscribe;
|
|
4375
|
+
}, [executor, undoTracker]);
|
|
3734
4376
|
const on = react.useCallback(
|
|
3735
4377
|
(type, listener) => registry.on(type, listener),
|
|
3736
4378
|
[registry]
|
|
@@ -3745,6 +4387,9 @@ function UIBridgeNativeProvider({
|
|
|
3745
4387
|
config,
|
|
3746
4388
|
registry,
|
|
3747
4389
|
executor,
|
|
4390
|
+
modalDetector,
|
|
4391
|
+
toastCapture,
|
|
4392
|
+
undoTracker,
|
|
3748
4393
|
getElements,
|
|
3749
4394
|
getComponents,
|
|
3750
4395
|
createSnapshot,
|
|
@@ -3761,6 +4406,9 @@ function UIBridgeNativeProvider({
|
|
|
3761
4406
|
config,
|
|
3762
4407
|
registry,
|
|
3763
4408
|
executor,
|
|
4409
|
+
modalDetector,
|
|
4410
|
+
toastCapture,
|
|
4411
|
+
undoTracker,
|
|
3764
4412
|
getElements,
|
|
3765
4413
|
getComponents,
|
|
3766
4414
|
createSnapshot,
|
|
@@ -3855,7 +4503,7 @@ function useUIElement(options) {
|
|
|
3855
4503
|
registeredRef.current = false;
|
|
3856
4504
|
registeredIdRef.current = null;
|
|
3857
4505
|
setRegistered(false);
|
|
3858
|
-
}, [bridge,
|
|
4506
|
+
}, [bridge, id]);
|
|
3859
4507
|
const onLayout = react.useCallback(
|
|
3860
4508
|
(event) => {
|
|
3861
4509
|
const { x, y, width, height } = event.nativeEvent.layout;
|
|
@@ -4121,7 +4769,7 @@ function useUIComponent(options) {
|
|
|
4121
4769
|
const registeredComponent = react.useMemo(() => {
|
|
4122
4770
|
if (!bridge) return null;
|
|
4123
4771
|
return bridge.registry.getComponent(id) || null;
|
|
4124
|
-
}, [bridge, id
|
|
4772
|
+
}, [bridge, id]);
|
|
4125
4773
|
return {
|
|
4126
4774
|
registered: registeredRef.current,
|
|
4127
4775
|
executeAction,
|
|
@@ -4265,11 +4913,12 @@ function useAutoRegister(options = {}) {
|
|
|
4265
4913
|
[bridge, onUnregister]
|
|
4266
4914
|
);
|
|
4267
4915
|
react.useEffect(() => {
|
|
4916
|
+
const registeredIds = registeredIdsRef.current;
|
|
4268
4917
|
return () => {
|
|
4269
|
-
|
|
4918
|
+
registeredIds.forEach((id) => {
|
|
4270
4919
|
bridge?.registry.unregisterElement(id);
|
|
4271
4920
|
});
|
|
4272
|
-
|
|
4921
|
+
registeredIds.clear();
|
|
4273
4922
|
};
|
|
4274
4923
|
}, [bridge]);
|
|
4275
4924
|
return {
|
|
@@ -4278,14 +4927,48 @@ function useAutoRegister(options = {}) {
|
|
|
4278
4927
|
isEnabled: enabled && !!bridge?.registry
|
|
4279
4928
|
};
|
|
4280
4929
|
}
|
|
4930
|
+
function useUIBridgeModal(opts) {
|
|
4931
|
+
const { modalDetector } = useUIBridgeNative();
|
|
4932
|
+
const { id, title, type, blocking, dismissible } = opts;
|
|
4933
|
+
react.useEffect(() => {
|
|
4934
|
+
modalDetector.pushModal({ id, title, type, blocking, dismissible });
|
|
4935
|
+
return () => {
|
|
4936
|
+
modalDetector.dismissModal(id);
|
|
4937
|
+
};
|
|
4938
|
+
}, [modalDetector, id, title, type, blocking, dismissible]);
|
|
4939
|
+
}
|
|
4940
|
+
function useUIBridgeToast(opts) {
|
|
4941
|
+
const { toastCapture } = useUIBridgeNative();
|
|
4942
|
+
const { id, message, level } = opts;
|
|
4943
|
+
react.useEffect(() => {
|
|
4944
|
+
toastCapture.recordToast({ id, message, level });
|
|
4945
|
+
return () => {
|
|
4946
|
+
toastCapture.dismissToast(id);
|
|
4947
|
+
};
|
|
4948
|
+
}, [toastCapture, id, message, level]);
|
|
4949
|
+
}
|
|
4950
|
+
function useToastRecorder() {
|
|
4951
|
+
const { toastCapture } = useUIBridgeNative();
|
|
4952
|
+
const recordToast = react.useCallback(
|
|
4953
|
+
(toast) => {
|
|
4954
|
+
toastCapture.recordToast(toast);
|
|
4955
|
+
},
|
|
4956
|
+
[toastCapture]
|
|
4957
|
+
);
|
|
4958
|
+
const dismissToast = react.useCallback((id) => toastCapture.dismissToast(id), [toastCapture]);
|
|
4959
|
+
return { recordToast, dismissToast };
|
|
4960
|
+
}
|
|
4281
4961
|
|
|
4282
4962
|
exports.UIBridgeNativeProvider = UIBridgeNativeProvider;
|
|
4283
4963
|
exports.useAutoRegister = useAutoRegister;
|
|
4964
|
+
exports.useToastRecorder = useToastRecorder;
|
|
4284
4965
|
exports.useUIBridge = useUIBridge;
|
|
4966
|
+
exports.useUIBridgeModal = useUIBridgeModal;
|
|
4285
4967
|
exports.useUIBridgeNative = useUIBridgeNative;
|
|
4286
4968
|
exports.useUIBridgeNativeOptional = useUIBridgeNativeOptional;
|
|
4287
4969
|
exports.useUIBridgeNativeRequired = useUIBridgeNativeRequired;
|
|
4288
4970
|
exports.useUIBridgeRequired = useUIBridgeRequired;
|
|
4971
|
+
exports.useUIBridgeToast = useUIBridgeToast;
|
|
4289
4972
|
exports.useUIComponent = useUIComponent;
|
|
4290
4973
|
exports.useUIComponentAction = useUIComponentAction;
|
|
4291
4974
|
exports.useUIElement = useUIElement;
|