@qontinui/ui-bridge-native 0.1.5 → 0.2.0

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.
Files changed (45) hide show
  1. package/dist/control/index.d.mts +9 -5
  2. package/dist/control/index.d.ts +9 -5
  3. package/dist/control/index.js +75 -12
  4. package/dist/control/index.js.map +1 -1
  5. package/dist/control/index.mjs +75 -12
  6. package/dist/control/index.mjs.map +1 -1
  7. package/dist/core/index.d.mts +3 -3
  8. package/dist/core/index.d.ts +3 -3
  9. package/dist/core/index.js +69 -1
  10. package/dist/core/index.js.map +1 -1
  11. package/dist/core/index.mjs +69 -1
  12. package/dist/core/index.mjs.map +1 -1
  13. package/dist/debug/index.js.map +1 -1
  14. package/dist/debug/index.mjs.map +1 -1
  15. package/dist/design/index.d.mts +1 -1
  16. package/dist/design/index.d.ts +1 -1
  17. package/dist/{http-server-D7uVANBU.d.mts → http-server-DCdR8WlL.d.mts} +3 -3
  18. package/dist/{http-server-B7KudcYJ.d.ts → http-server-Dj3grQW4.d.ts} +3 -3
  19. package/dist/index.d.mts +5 -5
  20. package/dist/index.d.ts +5 -5
  21. package/dist/index.js +498 -16
  22. package/dist/index.js.map +1 -1
  23. package/dist/index.mjs +493 -17
  24. package/dist/index.mjs.map +1 -1
  25. package/dist/react/index.d.mts +5 -5
  26. package/dist/react/index.d.ts +5 -5
  27. package/dist/react/index.js +504 -16
  28. package/dist/react/index.js.map +1 -1
  29. package/dist/react/index.mjs +502 -17
  30. package/dist/react/index.mjs.map +1 -1
  31. package/dist/{registry-DhbGbl9c.d.mts → registry-Cahd8Gn2.d.mts} +16 -1
  32. package/dist/{registry-B8A_EoxS.d.ts → registry-z-3WJsxp.d.ts} +16 -1
  33. package/dist/server/index.d.mts +5 -5
  34. package/dist/server/index.d.ts +5 -5
  35. package/dist/server/index.js +102 -1
  36. package/dist/server/index.js.map +1 -1
  37. package/dist/server/index.mjs +102 -1
  38. package/dist/server/index.mjs.map +1 -1
  39. package/dist/{types-Bcj7ji97.d.mts → types-DAfCeWXc.d.mts} +22 -2
  40. package/dist/{types-BPNuYxDx.d.mts → types-Oz13YLZq.d.mts} +95 -1
  41. package/dist/{types-BPNuYxDx.d.ts → types-Oz13YLZq.d.ts} +95 -1
  42. package/dist/{types-BCYWwyVy.d.ts → types-ajakyp-l.d.ts} +22 -2
  43. package/dist/{useUIBridge-CDZbWxII.d.ts → useUIBridgeToast-D32KXyL1.d.ts} +154 -5
  44. package/dist/{useUIBridge-DfIEq9EQ.d.mts → useUIBridgeToast-GmT1TzUe.d.mts} +154 -5
  45. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -55,9 +55,34 @@ 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();
58
60
  this.config = config;
59
61
  }
60
62
  // ============================================================================
63
+ // Snapshot Enricher Slots
64
+ // ============================================================================
65
+ /**
66
+ * Register/replace canonical enrichers (modal/toast/undo). HMR-safe — calling
67
+ * with a partial set merges into existing slots instead of clobbering them.
68
+ */
69
+ setEnrichers(e) {
70
+ this.enrichers = { ...this.enrichers, ...e };
71
+ }
72
+ /**
73
+ * Register a custom snapshot enricher. The returned object will be
74
+ * `Object.assign`ed onto the snapshot, so use unique top-level keys to avoid
75
+ * clobbering canonical fields. Returns a disposer.
76
+ */
77
+ registerSnapshotEnricher(name, fn) {
78
+ this.snapshotExtras.set(name, fn);
79
+ return () => this.unregisterSnapshotEnricher(name);
80
+ }
81
+ /** Remove a custom snapshot enricher by name */
82
+ unregisterSnapshotEnricher(name) {
83
+ this.snapshotExtras.delete(name);
84
+ }
85
+ // ============================================================================
61
86
  // Element Management
62
87
  // ============================================================================
63
88
  /**
@@ -420,7 +445,7 @@ var NativeUIBridgeRegistry = class {
420
445
  const currentRoute = routeInfo.currentRoute;
421
446
  elements = elements.filter((e) => e.registrationRoute === currentRoute);
422
447
  }
423
- return {
448
+ const snapshot = {
424
449
  timestamp: Date.now(),
425
450
  elements: elements.map((e) => {
426
451
  const handlers = extractHandlerNames(e.props);
@@ -452,6 +477,49 @@ var NativeUIBridgeRegistry = class {
452
477
  currentRoute: routeInfo?.currentRoute ?? null,
453
478
  segments: routeInfo?.segments
454
479
  };
480
+ if (this.enrichers.modalDetector) {
481
+ try {
482
+ snapshot.modalStack = this.enrichers.modalDetector.getSnapshotModalContext();
483
+ } catch (error3) {
484
+ if (this.config.verbose) {
485
+ console.warn(`[ui-bridge-native] modalDetector enricher threw:`, error3);
486
+ }
487
+ }
488
+ }
489
+ if (this.enrichers.toastCapture) {
490
+ try {
491
+ snapshot.toasts = this.enrichers.toastCapture.getSnapshotToastContext();
492
+ } catch (error3) {
493
+ if (this.config.verbose) {
494
+ console.warn(`[ui-bridge-native] toastCapture enricher threw:`, error3);
495
+ }
496
+ }
497
+ }
498
+ if (this.enrichers.undoTracker) {
499
+ try {
500
+ snapshot.undoRedo = this.enrichers.undoTracker.getSnapshotUndoContext();
501
+ } catch (error3) {
502
+ if (this.config.verbose) {
503
+ console.warn(`[ui-bridge-native] undoTracker enricher threw:`, error3);
504
+ }
505
+ }
506
+ }
507
+ if (this.snapshotExtras.size > 0) {
508
+ const ctx = { elements, currentRoute: routeInfo?.currentRoute ?? null };
509
+ for (const [name, fn] of this.snapshotExtras) {
510
+ try {
511
+ const extra = fn(ctx);
512
+ if (extra && typeof extra === "object") {
513
+ Object.assign(snapshot, extra);
514
+ }
515
+ } catch (error3) {
516
+ if (this.config.verbose) {
517
+ console.warn(`[ui-bridge-native] snapshot enricher "${name}" threw:`, error3);
518
+ }
519
+ }
520
+ }
521
+ }
522
+ return snapshot;
455
523
  }
456
524
  /**
457
525
  * Get registry statistics
@@ -597,6 +665,23 @@ function sleep(ms) {
597
665
  var DefaultNativeActionExecutor = class {
598
666
  constructor(registry) {
599
667
  this.registry = registry;
668
+ this.actionListeners = /* @__PURE__ */ new Set();
669
+ }
670
+ onActionExecuted(listener) {
671
+ this.actionListeners.add(listener);
672
+ return () => {
673
+ this.actionListeners.delete(listener);
674
+ };
675
+ }
676
+ /** Notify listeners out-of-band; isolate each invocation so one bad listener can't poison the rest. */
677
+ emitActionEvent(event) {
678
+ for (const listener of this.actionListeners) {
679
+ try {
680
+ listener(event);
681
+ } catch (error3) {
682
+ console.warn(`[ui-bridge-native] Action listener error:`, error3);
683
+ }
684
+ }
600
685
  }
601
686
  /**
602
687
  * Execute an action on an element
@@ -610,11 +695,23 @@ var DefaultNativeActionExecutor = class {
610
695
  registered = findElementByIdentifier(elementId) ?? void 0;
611
696
  }
612
697
  if (!registered) {
698
+ const failTime = Date.now();
699
+ const errorMsg = `Element not found: ${elementId}`;
700
+ this.emitActionEvent({
701
+ elementId,
702
+ action: request.action,
703
+ params: request.params,
704
+ success: false,
705
+ error: errorMsg,
706
+ timestamp: failTime,
707
+ requestId: request.requestId,
708
+ durationMs: failTime - startTime
709
+ });
613
710
  return {
614
711
  success: false,
615
- error: `Element not found: ${elementId}`,
616
- durationMs: Date.now() - startTime,
617
- timestamp: Date.now(),
712
+ error: errorMsg,
713
+ durationMs: failTime - startTime,
714
+ timestamp: failTime,
618
715
  requestId: request.requestId
619
716
  };
620
717
  }
@@ -622,33 +719,67 @@ var DefaultNativeActionExecutor = class {
622
719
  const waitResult = await this.waitForElementInternal(registered.id, request.waitOptions);
623
720
  waitDurationMs = waitResult.waitedMs;
624
721
  if (!waitResult.met) {
722
+ const failTime = Date.now();
723
+ const errorMsg = waitResult.error || "Wait condition not met";
724
+ this.emitActionEvent({
725
+ elementId,
726
+ action: request.action,
727
+ params: request.params,
728
+ success: false,
729
+ error: errorMsg,
730
+ timestamp: failTime,
731
+ requestId: request.requestId,
732
+ durationMs: failTime - startTime
733
+ });
625
734
  return {
626
735
  success: false,
627
- error: waitResult.error || "Wait condition not met",
628
- durationMs: Date.now() - startTime,
629
- timestamp: Date.now(),
736
+ error: errorMsg,
737
+ durationMs: failTime - startTime,
738
+ timestamp: failTime,
630
739
  requestId: request.requestId,
631
740
  waitDurationMs
632
741
  };
633
742
  }
634
743
  }
635
744
  const result = await this.performAction(registered, request.action, request.params);
745
+ const successTime = Date.now();
746
+ this.emitActionEvent({
747
+ elementId,
748
+ action: request.action,
749
+ params: request.params,
750
+ success: true,
751
+ timestamp: successTime,
752
+ requestId: request.requestId,
753
+ durationMs: successTime - startTime
754
+ });
636
755
  return {
637
756
  success: true,
638
757
  elementState: registered.getState(),
639
758
  result,
640
- durationMs: Date.now() - startTime,
641
- timestamp: Date.now(),
759
+ durationMs: successTime - startTime,
760
+ timestamp: successTime,
642
761
  requestId: request.requestId,
643
762
  waitDurationMs
644
763
  };
645
764
  } catch (error3) {
765
+ const failTime = Date.now();
766
+ const errorMsg = error3 instanceof Error ? error3.message : String(error3);
767
+ this.emitActionEvent({
768
+ elementId,
769
+ action: request.action,
770
+ params: request.params,
771
+ success: false,
772
+ error: errorMsg,
773
+ timestamp: failTime,
774
+ requestId: request.requestId,
775
+ durationMs: failTime - startTime
776
+ });
646
777
  return {
647
778
  success: false,
648
- error: error3 instanceof Error ? error3.message : String(error3),
779
+ error: errorMsg,
649
780
  stack: error3 instanceof Error ? error3.stack : void 0,
650
- durationMs: Date.now() - startTime,
651
- timestamp: Date.now(),
781
+ durationMs: failTime - startTime,
782
+ timestamp: failTime,
652
783
  requestId: request.requestId,
653
784
  waitDurationMs
654
785
  };
@@ -741,7 +872,7 @@ var DefaultNativeActionExecutor = class {
741
872
  if (!params?.text) {
742
873
  throw new Error("Type action requires text parameter");
743
874
  }
744
- if (params.clearFirst) {
875
+ if (params.clearFirst || params.clear) {
745
876
  await this.performClear(element, props);
746
877
  }
747
878
  if (params.delay && params.delay > 0) {
@@ -1974,6 +2105,10 @@ var WS_ROUTES = [
1974
2105
  { pattern: "health", handler: "health", httpMethods: ["GET"] },
1975
2106
  // Elements
1976
2107
  { pattern: "control/elements", handler: "getElements", httpMethods: ["GET"] },
2108
+ // Flat alias — matches the runner UI Bridge's `/ui-bridge/elements` path so
2109
+ // agents can use the same URL across runner + mobile without remembering
2110
+ // the `control/` prefix. Delegates to the same handler.
2111
+ { pattern: "elements", handler: "getElements", httpMethods: ["GET"] },
1977
2112
  { pattern: "control/element/:id", handler: "getElement", httpMethods: ["GET"] },
1978
2113
  { pattern: "control/element/:id/state", handler: "getElementState", httpMethods: ["GET"] },
1979
2114
  { pattern: "control/element/:id/action", handler: "executeAction", httpMethods: ["POST"] },
@@ -2024,6 +2159,87 @@ var WS_ROUTES = [
2024
2159
  // Meta / Introspection
2025
2160
  { pattern: "meta/methods", handler: "getMethods", httpMethods: ["GET"] }
2026
2161
  ];
2162
+ var HANDLER_DESCRIPTIONS = {
2163
+ health: "Liveness probe; returns app metadata + element count",
2164
+ getElements: "Flat list of registered UI elements",
2165
+ getElement: "Single element by id",
2166
+ getElementState: "Dynamic state (value, focused, checked) for one element",
2167
+ executeAction: 'Run an action on an element (see "actions" below for names)',
2168
+ getComponents: "Higher-level components with action schemas",
2169
+ getComponent: "Component detail + per-action paramSchema",
2170
+ executeComponentAction: "Invoke a component action directly (preferred over button clicks)",
2171
+ find: "AI-powered element search by natural-language query",
2172
+ getSnapshot: "Full snapshot: elements + route + state. Supports ?visibleOnly=true and ?currentRouteOnly=true",
2173
+ getWorkflows: "List registered workflows",
2174
+ runWorkflow: "Trigger a workflow by id",
2175
+ pageRefresh: "Reload the current route",
2176
+ pageNavigate: "Imperative router.push(url) \u2014 preferred over tapping tab buttons",
2177
+ pageReplace: "router.replace(url)",
2178
+ pageGoBack: "router.back()",
2179
+ pageGoForward: "router.forward()",
2180
+ getScreenshot: "PNG screenshot of the current screen",
2181
+ getElementStyles: "Computed styles for an element (design review)",
2182
+ getElementStateStyles: "Computed styles per interaction state (hover/focus/disabled)",
2183
+ getDesignSnapshot: "Visual + style snapshot for design review",
2184
+ getResponsiveSnapshots: "Screenshots at multiple viewport widths",
2185
+ runDesignAudit: "Accessibility + visual audit",
2186
+ loadStyleGuide: "Load a design system style guide",
2187
+ getStyleGuide: "Fetch the currently loaded style guide",
2188
+ clearStyleGuide: "Unload the style guide",
2189
+ evaluateQuality: "Score the current UI against a quality context",
2190
+ getQualityContexts: "List available quality evaluation contexts",
2191
+ saveBaseline: "Save a quality baseline snapshot",
2192
+ diffBaseline: "Diff current UI against a saved baseline",
2193
+ getMethods: "List every handler name (legacy introspection)"
2194
+ };
2195
+ var ACTION_REFERENCE = [
2196
+ { name: "press", aliases: ["click"], params: "PressActionParams (optional position)" },
2197
+ { name: "longPress", params: "PressActionParams (optional position)" },
2198
+ { name: "doubleTap", params: "none" },
2199
+ {
2200
+ name: "type",
2201
+ params: "TypeActionParams { text, delay?, clear? | clearFirst? } \u2014 `clear:true` replaces content atomically"
2202
+ },
2203
+ { name: "clear", params: "none \u2014 empties the input" },
2204
+ { name: "focus", params: "none" },
2205
+ { name: "blur", params: "none" },
2206
+ { name: "scroll", params: "ScrollActionParams { offset? {x,y}, to? }" },
2207
+ { name: "swipe", params: "SwipeActionParams { direction, distance }" },
2208
+ { name: "toggle", params: "none \u2014 flips boolean controls" }
2209
+ ];
2210
+ function buildRoutesPayload() {
2211
+ const entries = [];
2212
+ for (const r of WS_ROUTES) {
2213
+ if (!r.httpMethods) continue;
2214
+ for (const m of r.httpMethods) {
2215
+ entries.push({ method: m, path: `/ui-bridge/${r.pattern}` });
2216
+ }
2217
+ }
2218
+ entries.push({ method: "GET", path: "/ui-bridge/_help" });
2219
+ entries.push({ method: "GET", path: "/ui-bridge/_routes" });
2220
+ entries.sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method));
2221
+ return { routes: entries, count: entries.length };
2222
+ }
2223
+ function buildHelpPayload() {
2224
+ return {
2225
+ version: 1,
2226
+ description: "UI Bridge Native HTTP surface. Prefix every path with /ui-bridge/.",
2227
+ routes: WS_ROUTES.map((r) => ({
2228
+ pattern: r.pattern,
2229
+ httpMethods: r.httpMethods ?? [],
2230
+ wsOnly: !r.httpMethods,
2231
+ handler: r.handler,
2232
+ description: HANDLER_DESCRIPTIONS[r.handler] ?? ""
2233
+ })),
2234
+ actions: ACTION_REFERENCE,
2235
+ tips: [
2236
+ "Start with `GET /ui-bridge/control/snapshot?visibleOnly=true` to see what the user actually sees.",
2237
+ "Prefer `control/component/:id/action/:actionId` over button presses \u2014 typed paramSchema, no button-id guessing.",
2238
+ "Use `pageNavigate` for tab switches instead of pressing tab buttons; gesture state often swallows clicks.",
2239
+ "The `type` action supports `clear:true` to replace existing text atomically in one call."
2240
+ ]
2241
+ };
2242
+ }
2027
2243
  var NativeUIBridgeServer = class {
2028
2244
  constructor(registry, executor, config = {}) {
2029
2245
  this.registry = registry;
@@ -2040,7 +2256,9 @@ var NativeUIBridgeServer = class {
2040
2256
  cors: true,
2041
2257
  ...config
2042
2258
  };
2043
- this.handlers = createServerHandlers(registry, executor);
2259
+ this.handlers = createServerHandlers(registry, executor, {
2260
+ appInfo: this.config.appInfo
2261
+ });
2044
2262
  this.handlers.getMethods = async () => {
2045
2263
  const methods = WS_ROUTES.map((r) => ({
2046
2264
  method: r.pattern,
@@ -2844,6 +3062,20 @@ var NativeUIBridgeServer = class {
2844
3062
  };
2845
3063
  }
2846
3064
  const stripped = path.slice(PREFIX.length);
3065
+ if (method === "GET" && stripped === "_help") {
3066
+ return {
3067
+ success: true,
3068
+ data: buildHelpPayload(),
3069
+ timestamp: Date.now()
3070
+ };
3071
+ }
3072
+ if (method === "GET" && stripped === "_routes") {
3073
+ return {
3074
+ success: true,
3075
+ data: buildRoutesPayload(),
3076
+ timestamp: Date.now()
3077
+ };
3078
+ }
2847
3079
  let patternMatchedButWrongVerb = false;
2848
3080
  for (const route of WS_ROUTES) {
2849
3081
  const pathParams = parsePath(route.pattern, stripped);
@@ -3465,6 +3697,186 @@ function parseQueryParams(path) {
3465
3697
  }
3466
3698
  return result;
3467
3699
  }
3700
+
3701
+ // src/modal/modal-detector.ts
3702
+ var ModalDetector = class {
3703
+ constructor() {
3704
+ /** Map preserves O(1) idempotent replace by id; ordering comes from detectedAt. */
3705
+ this.modals = /* @__PURE__ */ new Map();
3706
+ }
3707
+ pushModal(modal) {
3708
+ const existing = this.modals.get(modal.id);
3709
+ const detectedAt = existing ? existing.detectedAt : Date.now();
3710
+ const next = {
3711
+ id: modal.id,
3712
+ title: modal.title,
3713
+ type: modal.type ?? "modal",
3714
+ blocking: modal.blocking ?? true,
3715
+ dismissible: modal.dismissible ?? true,
3716
+ detectedAt
3717
+ };
3718
+ this.modals.set(modal.id, next);
3719
+ }
3720
+ dismissModal(id) {
3721
+ this.modals.delete(id);
3722
+ }
3723
+ getSnapshotModalContext() {
3724
+ const active = this.getActive();
3725
+ const ctx = {
3726
+ modals: active,
3727
+ hasBlockingModal: active.some((m) => m.blocking),
3728
+ count: active.length
3729
+ };
3730
+ if (active.length > 0) {
3731
+ ctx.topModal = active[active.length - 1];
3732
+ }
3733
+ return ctx;
3734
+ }
3735
+ getActive() {
3736
+ return Array.from(this.modals.values()).sort((a, b) => a.detectedAt - b.detectedAt);
3737
+ }
3738
+ clear() {
3739
+ this.modals.clear();
3740
+ }
3741
+ };
3742
+
3743
+ // src/toast/toast-capture.ts
3744
+ var DEFAULT_MAX_RECENT = 50;
3745
+ var ToastCapture = class {
3746
+ constructor(config = {}) {
3747
+ this.active = /* @__PURE__ */ new Map();
3748
+ this.recent = [];
3749
+ this.totalCaptured = 0;
3750
+ this.maxRecent = config.maxRecent ?? DEFAULT_MAX_RECENT;
3751
+ }
3752
+ recordToast(toast) {
3753
+ if (this.active.has(toast.id)) {
3754
+ return;
3755
+ }
3756
+ const appearedAt = toast.appearedAt ?? Date.now();
3757
+ const entry = {
3758
+ id: toast.id,
3759
+ message: toast.message,
3760
+ level: toast.level ?? "info",
3761
+ appearedAt,
3762
+ visible: true,
3763
+ durationMs: 0
3764
+ };
3765
+ this.active.set(toast.id, entry);
3766
+ this.totalCaptured += 1;
3767
+ }
3768
+ dismissToast(id) {
3769
+ const entry = this.active.get(id);
3770
+ if (!entry) {
3771
+ return;
3772
+ }
3773
+ this.active.delete(id);
3774
+ const dismissedAt = Date.now();
3775
+ const dismissed = {
3776
+ ...entry,
3777
+ dismissedAt,
3778
+ visible: false,
3779
+ durationMs: dismissedAt - entry.appearedAt
3780
+ };
3781
+ this.recent.push(dismissed);
3782
+ if (this.recent.length > this.maxRecent) {
3783
+ this.recent.shift();
3784
+ }
3785
+ }
3786
+ getSnapshotToastContext() {
3787
+ const now = Date.now();
3788
+ const active = Array.from(this.active.values()).map((entry) => ({
3789
+ ...entry,
3790
+ durationMs: now - entry.appearedAt
3791
+ }));
3792
+ return {
3793
+ active,
3794
+ recent: [...this.recent],
3795
+ totalCaptured: this.totalCaptured
3796
+ };
3797
+ }
3798
+ clear() {
3799
+ this.active.clear();
3800
+ this.recent = [];
3801
+ this.totalCaptured = 0;
3802
+ }
3803
+ };
3804
+
3805
+ // src/undo/undo-tracker.ts
3806
+ var DEFAULT_MAX_HISTORY = 50;
3807
+ function buildSummary(ctx) {
3808
+ if (!ctx.canUndo && !ctx.canRedo) {
3809
+ return "no undo available";
3810
+ }
3811
+ const parts = [];
3812
+ if (ctx.canUndo) {
3813
+ parts.push(`undo: '${ctx.undoDescription ?? "available"}'`);
3814
+ }
3815
+ if (ctx.canRedo) {
3816
+ parts.push(`redo: '${ctx.redoDescription ?? "available"}'`);
3817
+ }
3818
+ return parts.join(" / ");
3819
+ }
3820
+ var UndoTracker = class {
3821
+ constructor(config = {}) {
3822
+ this.actions = [];
3823
+ this.declaredState = null;
3824
+ this.maxHistory = config.maxHistory ?? DEFAULT_MAX_HISTORY;
3825
+ }
3826
+ recordAction(record) {
3827
+ this.actions.push(record);
3828
+ if (this.actions.length > this.maxHistory) {
3829
+ this.actions = this.actions.slice(-this.maxHistory);
3830
+ }
3831
+ }
3832
+ setDeclaredState(state) {
3833
+ this.declaredState = state;
3834
+ }
3835
+ getActionHistory() {
3836
+ return this.actions.slice();
3837
+ }
3838
+ getSnapshotUndoContext() {
3839
+ const declared = this.declaredState;
3840
+ if (declared) {
3841
+ const summary2 = buildSummary({
3842
+ canUndo: declared.canUndo,
3843
+ canRedo: declared.canRedo,
3844
+ undoDescription: declared.undoDescription,
3845
+ redoDescription: declared.redoDescription
3846
+ });
3847
+ return {
3848
+ canUndo: declared.canUndo,
3849
+ canRedo: declared.canRedo,
3850
+ undoDescription: declared.undoDescription,
3851
+ redoDescription: declared.redoDescription,
3852
+ undoDepth: declared.undoDepth,
3853
+ redoDepth: declared.redoDepth,
3854
+ summary: summary2
3855
+ };
3856
+ }
3857
+ const canUndo = this.actions.some((a) => a.reversible !== false);
3858
+ const canRedo = false;
3859
+ const lastReversible = [...this.actions].reverse().find((a) => a.reversible !== false);
3860
+ const undoDescription = lastReversible?.description;
3861
+ const summary = buildSummary({ canUndo, canRedo, undoDescription });
3862
+ return {
3863
+ canUndo,
3864
+ canRedo,
3865
+ undoDescription,
3866
+ summary
3867
+ };
3868
+ }
3869
+ executeUndo() {
3870
+ return { ok: false, reason: "Not implemented in native" };
3871
+ }
3872
+ executeRedo() {
3873
+ return { ok: false, reason: "Not implemented in native" };
3874
+ }
3875
+ clear() {
3876
+ this.actions = [];
3877
+ this.declaredState = null;
3878
+ }
3879
+ };
3468
3880
  var UIBridgeNativeContext = react.createContext(null);
3469
3881
  function UIBridgeNativeProvider({
3470
3882
  children,
@@ -3481,6 +3893,9 @@ function UIBridgeNativeProvider({
3481
3893
  }) {
3482
3894
  const registryRef = react.useRef(null);
3483
3895
  const executorRef = react.useRef(null);
3896
+ const modalDetectorRef = react.useRef(null);
3897
+ const toastCaptureRef = react.useRef(null);
3898
+ const undoTrackerRef = react.useRef(null);
3484
3899
  const [serverRunning, setServerRunning] = react.useState(false);
3485
3900
  if (!registryRef.current) {
3486
3901
  registryRef.current = new NativeUIBridgeRegistry({
@@ -3494,6 +3909,12 @@ function UIBridgeNativeProvider({
3494
3909
  executorRef.current = createNativeActionExecutor(registry);
3495
3910
  }
3496
3911
  const executor = executorRef.current;
3912
+ if (!modalDetectorRef.current) modalDetectorRef.current = new ModalDetector();
3913
+ if (!toastCaptureRef.current) toastCaptureRef.current = new ToastCapture();
3914
+ if (!undoTrackerRef.current) undoTrackerRef.current = new UndoTracker();
3915
+ const modalDetector = modalDetectorRef.current;
3916
+ const toastCapture = toastCaptureRef.current;
3917
+ const undoTracker = undoTrackerRef.current;
3497
3918
  const serverRef = react.useRef(null);
3498
3919
  const eventBridgeRef = react.useRef(null);
3499
3920
  const announcerRef = react.useRef(null);
@@ -3508,7 +3929,8 @@ function UIBridgeNativeProvider({
3508
3929
  }
3509
3930
  const server = createNativeServer(registry, executor, {
3510
3931
  serverPort: config.serverPort || 8087,
3511
- cors: true
3932
+ cors: true,
3933
+ appInfo: config.appInfo
3512
3934
  });
3513
3935
  server.setAdapter(serverAdapter);
3514
3936
  if (navigationProvider) {
@@ -3595,7 +4017,8 @@ function UIBridgeNativeProvider({
3595
4017
  try {
3596
4018
  const bareServer = createNativeServer(registry, executor, {
3597
4019
  serverPort: config.serverPort || 8087,
3598
- cors: true
4020
+ cors: true,
4021
+ appInfo: config.appInfo
3599
4022
  });
3600
4023
  if (navigationProvider) bareServer.setNavigationProvider(navigationProvider);
3601
4024
  if (screenshotProvider) bareServer.setScreenshotProvider(screenshotProvider);
@@ -3680,6 +4103,22 @@ function UIBridgeNativeProvider({
3680
4103
  lastRoute = current;
3681
4104
  });
3682
4105
  }, [routeProvider, registry]);
4106
+ react.useEffect(() => {
4107
+ registry.setEnrichers({ modalDetector, toastCapture, undoTracker });
4108
+ }, [registry, modalDetector, toastCapture, undoTracker]);
4109
+ react.useEffect(() => {
4110
+ const unsubscribe = executor.onActionExecuted((event) => {
4111
+ if (!event.success) return;
4112
+ undoTracker.recordAction({
4113
+ id: event.requestId ?? `action-${event.timestamp}`,
4114
+ type: event.action,
4115
+ targetId: event.elementId,
4116
+ timestamp: event.timestamp,
4117
+ reversible: true
4118
+ });
4119
+ });
4120
+ return unsubscribe;
4121
+ }, [executor, undoTracker]);
3683
4122
  const on = react.useCallback(
3684
4123
  (type, listener) => registry.on(type, listener),
3685
4124
  [registry]
@@ -3694,6 +4133,9 @@ function UIBridgeNativeProvider({
3694
4133
  config,
3695
4134
  registry,
3696
4135
  executor,
4136
+ modalDetector,
4137
+ toastCapture,
4138
+ undoTracker,
3697
4139
  getElements,
3698
4140
  getComponents,
3699
4141
  createSnapshot,
@@ -3710,6 +4152,9 @@ function UIBridgeNativeProvider({
3710
4152
  config,
3711
4153
  registry,
3712
4154
  executor,
4155
+ modalDetector,
4156
+ toastCapture,
4157
+ undoTracker,
3713
4158
  getElements,
3714
4159
  getComponents,
3715
4160
  createSnapshot,
@@ -4190,6 +4635,37 @@ function useUIBridgeRequired() {
4190
4635
  }
4191
4636
  return result;
4192
4637
  }
4638
+ function useUIBridgeModal(opts) {
4639
+ const { modalDetector } = useUIBridgeNative();
4640
+ const { id, title, type, blocking, dismissible } = opts;
4641
+ react.useEffect(() => {
4642
+ modalDetector.pushModal({ id, title, type, blocking, dismissible });
4643
+ return () => {
4644
+ modalDetector.dismissModal(id);
4645
+ };
4646
+ }, [modalDetector, id, title, type, blocking, dismissible]);
4647
+ }
4648
+ function useUIBridgeToast(opts) {
4649
+ const { toastCapture } = useUIBridgeNative();
4650
+ const { id, message, level } = opts;
4651
+ react.useEffect(() => {
4652
+ toastCapture.recordToast({ id, message, level });
4653
+ return () => {
4654
+ toastCapture.dismissToast(id);
4655
+ };
4656
+ }, [toastCapture, id, message, level]);
4657
+ }
4658
+ function useToastRecorder() {
4659
+ const { toastCapture } = useUIBridgeNative();
4660
+ const recordToast = react.useCallback(
4661
+ (toast) => {
4662
+ toastCapture.recordToast(toast);
4663
+ },
4664
+ [toastCapture]
4665
+ );
4666
+ const dismissToast = react.useCallback((id) => toastCapture.dismissToast(id), [toastCapture]);
4667
+ return { recordToast, dismissToast };
4668
+ }
4193
4669
 
4194
4670
  // src/server/types.ts
4195
4671
  var UI_BRIDGE_NATIVE_ROUTES = {
@@ -5094,11 +5570,14 @@ var styles = reactNative.StyleSheet.create({
5094
5570
  exports.CloudRelayClient = CloudRelayClient;
5095
5571
  exports.DefaultNativeActionExecutor = DefaultNativeActionExecutor;
5096
5572
  exports.DeviceAnnouncer = DeviceAnnouncer;
5573
+ exports.ModalDetector = ModalDetector;
5097
5574
  exports.NativeUIBridgeRegistry = NativeUIBridgeRegistry;
5098
5575
  exports.NativeUIBridgeServer = NativeUIBridgeServer;
5576
+ exports.ToastCapture = ToastCapture;
5099
5577
  exports.UIBridgeInspector = UIBridgeInspector;
5100
5578
  exports.UIBridgeNativeProvider = UIBridgeNativeProvider;
5101
5579
  exports.UI_BRIDGE_NATIVE_ROUTES = UI_BRIDGE_NATIVE_ROUTES;
5580
+ exports.UndoTracker = UndoTracker;
5102
5581
  exports.WebSocketConnection = WebSocketConnection;
5103
5582
  exports.WebSocketEventBridge = WebSocketEventBridge;
5104
5583
  exports.buildTreePath = buildTreePath;
@@ -5121,11 +5600,14 @@ exports.matchesIdentifier = matchesIdentifier;
5121
5600
  exports.parseTreePath = parseTreePath;
5122
5601
  exports.resetGlobalRegistry = resetGlobalRegistry;
5123
5602
  exports.setGlobalRegistry = setGlobalRegistry;
5603
+ exports.useToastRecorder = useToastRecorder;
5124
5604
  exports.useUIBridge = useUIBridge;
5605
+ exports.useUIBridgeModal = useUIBridgeModal;
5125
5606
  exports.useUIBridgeNative = useUIBridgeNative;
5126
5607
  exports.useUIBridgeNativeOptional = useUIBridgeNativeOptional;
5127
5608
  exports.useUIBridgeNativeRequired = useUIBridgeNativeRequired;
5128
5609
  exports.useUIBridgeRequired = useUIBridgeRequired;
5610
+ exports.useUIBridgeToast = useUIBridgeToast;
5129
5611
  exports.useUIComponent = useUIComponent;
5130
5612
  exports.useUIComponentAction = useUIComponentAction;
5131
5613
  exports.useUIElement = useUIElement;