@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
@@ -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
@@ -545,6 +613,23 @@ function sleep(ms) {
545
613
  var DefaultNativeActionExecutor = class {
546
614
  constructor(registry) {
547
615
  this.registry = registry;
616
+ this.actionListeners = /* @__PURE__ */ new Set();
617
+ }
618
+ onActionExecuted(listener) {
619
+ this.actionListeners.add(listener);
620
+ return () => {
621
+ this.actionListeners.delete(listener);
622
+ };
623
+ }
624
+ /** Notify listeners out-of-band; isolate each invocation so one bad listener can't poison the rest. */
625
+ emitActionEvent(event) {
626
+ for (const listener of this.actionListeners) {
627
+ try {
628
+ listener(event);
629
+ } catch (error3) {
630
+ console.warn(`[ui-bridge-native] Action listener error:`, error3);
631
+ }
632
+ }
548
633
  }
549
634
  /**
550
635
  * Execute an action on an element
@@ -558,11 +643,23 @@ var DefaultNativeActionExecutor = class {
558
643
  registered = findElementByIdentifier(elementId) ?? void 0;
559
644
  }
560
645
  if (!registered) {
646
+ const failTime = Date.now();
647
+ const errorMsg = `Element not found: ${elementId}`;
648
+ this.emitActionEvent({
649
+ elementId,
650
+ action: request.action,
651
+ params: request.params,
652
+ success: false,
653
+ error: errorMsg,
654
+ timestamp: failTime,
655
+ requestId: request.requestId,
656
+ durationMs: failTime - startTime
657
+ });
561
658
  return {
562
659
  success: false,
563
- error: `Element not found: ${elementId}`,
564
- durationMs: Date.now() - startTime,
565
- timestamp: Date.now(),
660
+ error: errorMsg,
661
+ durationMs: failTime - startTime,
662
+ timestamp: failTime,
566
663
  requestId: request.requestId
567
664
  };
568
665
  }
@@ -570,33 +667,67 @@ var DefaultNativeActionExecutor = class {
570
667
  const waitResult = await this.waitForElementInternal(registered.id, request.waitOptions);
571
668
  waitDurationMs = waitResult.waitedMs;
572
669
  if (!waitResult.met) {
670
+ const failTime = Date.now();
671
+ const errorMsg = waitResult.error || "Wait condition not met";
672
+ this.emitActionEvent({
673
+ elementId,
674
+ action: request.action,
675
+ params: request.params,
676
+ success: false,
677
+ error: errorMsg,
678
+ timestamp: failTime,
679
+ requestId: request.requestId,
680
+ durationMs: failTime - startTime
681
+ });
573
682
  return {
574
683
  success: false,
575
- error: waitResult.error || "Wait condition not met",
576
- durationMs: Date.now() - startTime,
577
- timestamp: Date.now(),
684
+ error: errorMsg,
685
+ durationMs: failTime - startTime,
686
+ timestamp: failTime,
578
687
  requestId: request.requestId,
579
688
  waitDurationMs
580
689
  };
581
690
  }
582
691
  }
583
692
  const result = await this.performAction(registered, request.action, request.params);
693
+ const successTime = Date.now();
694
+ this.emitActionEvent({
695
+ elementId,
696
+ action: request.action,
697
+ params: request.params,
698
+ success: true,
699
+ timestamp: successTime,
700
+ requestId: request.requestId,
701
+ durationMs: successTime - startTime
702
+ });
584
703
  return {
585
704
  success: true,
586
705
  elementState: registered.getState(),
587
706
  result,
588
- durationMs: Date.now() - startTime,
589
- timestamp: Date.now(),
707
+ durationMs: successTime - startTime,
708
+ timestamp: successTime,
590
709
  requestId: request.requestId,
591
710
  waitDurationMs
592
711
  };
593
712
  } catch (error3) {
713
+ const failTime = Date.now();
714
+ const errorMsg = error3 instanceof Error ? error3.message : String(error3);
715
+ this.emitActionEvent({
716
+ elementId,
717
+ action: request.action,
718
+ params: request.params,
719
+ success: false,
720
+ error: errorMsg,
721
+ timestamp: failTime,
722
+ requestId: request.requestId,
723
+ durationMs: failTime - startTime
724
+ });
594
725
  return {
595
726
  success: false,
596
- error: error3 instanceof Error ? error3.message : String(error3),
727
+ error: errorMsg,
597
728
  stack: error3 instanceof Error ? error3.stack : void 0,
598
- durationMs: Date.now() - startTime,
599
- timestamp: Date.now(),
729
+ durationMs: failTime - startTime,
730
+ timestamp: failTime,
600
731
  requestId: request.requestId,
601
732
  waitDurationMs
602
733
  };
@@ -689,7 +820,7 @@ var DefaultNativeActionExecutor = class {
689
820
  if (!params?.text) {
690
821
  throw new Error("Type action requires text parameter");
691
822
  }
692
- if (params.clearFirst) {
823
+ if (params.clearFirst || params.clear) {
693
824
  await this.performClear(element, props);
694
825
  }
695
826
  if (params.delay && params.delay > 0) {
@@ -1875,6 +2006,15 @@ function createServerHandlers(registry, executor, config) {
1875
2006
  timestamp: Date.now(),
1876
2007
  ...stats
1877
2008
  };
2009
+ if (config?.appInfo) {
2010
+ response.uiBridge = {
2011
+ version: "0.3.0",
2012
+ ...config.appInfo,
2013
+ capabilities: ["elements", "components", "actions", "design"],
2014
+ elementCount: stats.elements,
2015
+ componentCount: stats.components
2016
+ };
2017
+ }
1878
2018
  return success2(response);
1879
2019
  }
1880
2020
  };
@@ -1913,6 +2053,10 @@ var WS_ROUTES = [
1913
2053
  { pattern: "health", handler: "health", httpMethods: ["GET"] },
1914
2054
  // Elements
1915
2055
  { pattern: "control/elements", handler: "getElements", httpMethods: ["GET"] },
2056
+ // Flat alias — matches the runner UI Bridge's `/ui-bridge/elements` path so
2057
+ // agents can use the same URL across runner + mobile without remembering
2058
+ // the `control/` prefix. Delegates to the same handler.
2059
+ { pattern: "elements", handler: "getElements", httpMethods: ["GET"] },
1916
2060
  { pattern: "control/element/:id", handler: "getElement", httpMethods: ["GET"] },
1917
2061
  { pattern: "control/element/:id/state", handler: "getElementState", httpMethods: ["GET"] },
1918
2062
  { pattern: "control/element/:id/action", handler: "executeAction", httpMethods: ["POST"] },
@@ -1963,6 +2107,87 @@ var WS_ROUTES = [
1963
2107
  // Meta / Introspection
1964
2108
  { pattern: "meta/methods", handler: "getMethods", httpMethods: ["GET"] }
1965
2109
  ];
2110
+ var HANDLER_DESCRIPTIONS = {
2111
+ health: "Liveness probe; returns app metadata + element count",
2112
+ getElements: "Flat list of registered UI elements",
2113
+ getElement: "Single element by id",
2114
+ getElementState: "Dynamic state (value, focused, checked) for one element",
2115
+ executeAction: 'Run an action on an element (see "actions" below for names)',
2116
+ getComponents: "Higher-level components with action schemas",
2117
+ getComponent: "Component detail + per-action paramSchema",
2118
+ executeComponentAction: "Invoke a component action directly (preferred over button clicks)",
2119
+ find: "AI-powered element search by natural-language query",
2120
+ getSnapshot: "Full snapshot: elements + route + state. Supports ?visibleOnly=true and ?currentRouteOnly=true",
2121
+ getWorkflows: "List registered workflows",
2122
+ runWorkflow: "Trigger a workflow by id",
2123
+ pageRefresh: "Reload the current route",
2124
+ pageNavigate: "Imperative router.push(url) \u2014 preferred over tapping tab buttons",
2125
+ pageReplace: "router.replace(url)",
2126
+ pageGoBack: "router.back()",
2127
+ pageGoForward: "router.forward()",
2128
+ getScreenshot: "PNG screenshot of the current screen",
2129
+ getElementStyles: "Computed styles for an element (design review)",
2130
+ getElementStateStyles: "Computed styles per interaction state (hover/focus/disabled)",
2131
+ getDesignSnapshot: "Visual + style snapshot for design review",
2132
+ getResponsiveSnapshots: "Screenshots at multiple viewport widths",
2133
+ runDesignAudit: "Accessibility + visual audit",
2134
+ loadStyleGuide: "Load a design system style guide",
2135
+ getStyleGuide: "Fetch the currently loaded style guide",
2136
+ clearStyleGuide: "Unload the style guide",
2137
+ evaluateQuality: "Score the current UI against a quality context",
2138
+ getQualityContexts: "List available quality evaluation contexts",
2139
+ saveBaseline: "Save a quality baseline snapshot",
2140
+ diffBaseline: "Diff current UI against a saved baseline",
2141
+ getMethods: "List every handler name (legacy introspection)"
2142
+ };
2143
+ var ACTION_REFERENCE = [
2144
+ { name: "press", aliases: ["click"], params: "PressActionParams (optional position)" },
2145
+ { name: "longPress", params: "PressActionParams (optional position)" },
2146
+ { name: "doubleTap", params: "none" },
2147
+ {
2148
+ name: "type",
2149
+ params: "TypeActionParams { text, delay?, clear? | clearFirst? } \u2014 `clear:true` replaces content atomically"
2150
+ },
2151
+ { name: "clear", params: "none \u2014 empties the input" },
2152
+ { name: "focus", params: "none" },
2153
+ { name: "blur", params: "none" },
2154
+ { name: "scroll", params: "ScrollActionParams { offset? {x,y}, to? }" },
2155
+ { name: "swipe", params: "SwipeActionParams { direction, distance }" },
2156
+ { name: "toggle", params: "none \u2014 flips boolean controls" }
2157
+ ];
2158
+ function buildRoutesPayload() {
2159
+ const entries = [];
2160
+ for (const r of WS_ROUTES) {
2161
+ if (!r.httpMethods) continue;
2162
+ for (const m of r.httpMethods) {
2163
+ entries.push({ method: m, path: `/ui-bridge/${r.pattern}` });
2164
+ }
2165
+ }
2166
+ entries.push({ method: "GET", path: "/ui-bridge/_help" });
2167
+ entries.push({ method: "GET", path: "/ui-bridge/_routes" });
2168
+ entries.sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method));
2169
+ return { routes: entries, count: entries.length };
2170
+ }
2171
+ function buildHelpPayload() {
2172
+ return {
2173
+ version: 1,
2174
+ description: "UI Bridge Native HTTP surface. Prefix every path with /ui-bridge/.",
2175
+ routes: WS_ROUTES.map((r) => ({
2176
+ pattern: r.pattern,
2177
+ httpMethods: r.httpMethods ?? [],
2178
+ wsOnly: !r.httpMethods,
2179
+ handler: r.handler,
2180
+ description: HANDLER_DESCRIPTIONS[r.handler] ?? ""
2181
+ })),
2182
+ actions: ACTION_REFERENCE,
2183
+ tips: [
2184
+ "Start with `GET /ui-bridge/control/snapshot?visibleOnly=true` to see what the user actually sees.",
2185
+ "Prefer `control/component/:id/action/:actionId` over button presses \u2014 typed paramSchema, no button-id guessing.",
2186
+ "Use `pageNavigate` for tab switches instead of pressing tab buttons; gesture state often swallows clicks.",
2187
+ "The `type` action supports `clear:true` to replace existing text atomically in one call."
2188
+ ]
2189
+ };
2190
+ }
1966
2191
  var NativeUIBridgeServer = class {
1967
2192
  constructor(registry, executor, config = {}) {
1968
2193
  this.registry = registry;
@@ -1979,7 +2204,9 @@ var NativeUIBridgeServer = class {
1979
2204
  cors: true,
1980
2205
  ...config
1981
2206
  };
1982
- this.handlers = createServerHandlers(registry, executor);
2207
+ this.handlers = createServerHandlers(registry, executor, {
2208
+ appInfo: this.config.appInfo
2209
+ });
1983
2210
  this.handlers.getMethods = async () => {
1984
2211
  const methods = WS_ROUTES.map((r) => ({
1985
2212
  method: r.pattern,
@@ -2783,6 +3010,20 @@ var NativeUIBridgeServer = class {
2783
3010
  };
2784
3011
  }
2785
3012
  const stripped = path.slice(PREFIX.length);
3013
+ if (method === "GET" && stripped === "_help") {
3014
+ return {
3015
+ success: true,
3016
+ data: buildHelpPayload(),
3017
+ timestamp: Date.now()
3018
+ };
3019
+ }
3020
+ if (method === "GET" && stripped === "_routes") {
3021
+ return {
3022
+ success: true,
3023
+ data: buildRoutesPayload(),
3024
+ timestamp: Date.now()
3025
+ };
3026
+ }
2786
3027
  let patternMatchedButWrongVerb = false;
2787
3028
  for (const route of WS_ROUTES) {
2788
3029
  const pathParams = parsePath(route.pattern, stripped);
@@ -3404,6 +3645,186 @@ function parseQueryParams(path) {
3404
3645
  }
3405
3646
  return result;
3406
3647
  }
3648
+
3649
+ // src/modal/modal-detector.ts
3650
+ var ModalDetector = class {
3651
+ constructor() {
3652
+ /** Map preserves O(1) idempotent replace by id; ordering comes from detectedAt. */
3653
+ this.modals = /* @__PURE__ */ new Map();
3654
+ }
3655
+ pushModal(modal) {
3656
+ const existing = this.modals.get(modal.id);
3657
+ const detectedAt = existing ? existing.detectedAt : Date.now();
3658
+ const next = {
3659
+ id: modal.id,
3660
+ title: modal.title,
3661
+ type: modal.type ?? "modal",
3662
+ blocking: modal.blocking ?? true,
3663
+ dismissible: modal.dismissible ?? true,
3664
+ detectedAt
3665
+ };
3666
+ this.modals.set(modal.id, next);
3667
+ }
3668
+ dismissModal(id) {
3669
+ this.modals.delete(id);
3670
+ }
3671
+ getSnapshotModalContext() {
3672
+ const active = this.getActive();
3673
+ const ctx = {
3674
+ modals: active,
3675
+ hasBlockingModal: active.some((m) => m.blocking),
3676
+ count: active.length
3677
+ };
3678
+ if (active.length > 0) {
3679
+ ctx.topModal = active[active.length - 1];
3680
+ }
3681
+ return ctx;
3682
+ }
3683
+ getActive() {
3684
+ return Array.from(this.modals.values()).sort((a, b) => a.detectedAt - b.detectedAt);
3685
+ }
3686
+ clear() {
3687
+ this.modals.clear();
3688
+ }
3689
+ };
3690
+
3691
+ // src/toast/toast-capture.ts
3692
+ var DEFAULT_MAX_RECENT = 50;
3693
+ var ToastCapture = class {
3694
+ constructor(config = {}) {
3695
+ this.active = /* @__PURE__ */ new Map();
3696
+ this.recent = [];
3697
+ this.totalCaptured = 0;
3698
+ this.maxRecent = config.maxRecent ?? DEFAULT_MAX_RECENT;
3699
+ }
3700
+ recordToast(toast) {
3701
+ if (this.active.has(toast.id)) {
3702
+ return;
3703
+ }
3704
+ const appearedAt = toast.appearedAt ?? Date.now();
3705
+ const entry = {
3706
+ id: toast.id,
3707
+ message: toast.message,
3708
+ level: toast.level ?? "info",
3709
+ appearedAt,
3710
+ visible: true,
3711
+ durationMs: 0
3712
+ };
3713
+ this.active.set(toast.id, entry);
3714
+ this.totalCaptured += 1;
3715
+ }
3716
+ dismissToast(id) {
3717
+ const entry = this.active.get(id);
3718
+ if (!entry) {
3719
+ return;
3720
+ }
3721
+ this.active.delete(id);
3722
+ const dismissedAt = Date.now();
3723
+ const dismissed = {
3724
+ ...entry,
3725
+ dismissedAt,
3726
+ visible: false,
3727
+ durationMs: dismissedAt - entry.appearedAt
3728
+ };
3729
+ this.recent.push(dismissed);
3730
+ if (this.recent.length > this.maxRecent) {
3731
+ this.recent.shift();
3732
+ }
3733
+ }
3734
+ getSnapshotToastContext() {
3735
+ const now = Date.now();
3736
+ const active = Array.from(this.active.values()).map((entry) => ({
3737
+ ...entry,
3738
+ durationMs: now - entry.appearedAt
3739
+ }));
3740
+ return {
3741
+ active,
3742
+ recent: [...this.recent],
3743
+ totalCaptured: this.totalCaptured
3744
+ };
3745
+ }
3746
+ clear() {
3747
+ this.active.clear();
3748
+ this.recent = [];
3749
+ this.totalCaptured = 0;
3750
+ }
3751
+ };
3752
+
3753
+ // src/undo/undo-tracker.ts
3754
+ var DEFAULT_MAX_HISTORY = 50;
3755
+ function buildSummary(ctx) {
3756
+ if (!ctx.canUndo && !ctx.canRedo) {
3757
+ return "no undo available";
3758
+ }
3759
+ const parts = [];
3760
+ if (ctx.canUndo) {
3761
+ parts.push(`undo: '${ctx.undoDescription ?? "available"}'`);
3762
+ }
3763
+ if (ctx.canRedo) {
3764
+ parts.push(`redo: '${ctx.redoDescription ?? "available"}'`);
3765
+ }
3766
+ return parts.join(" / ");
3767
+ }
3768
+ var UndoTracker = class {
3769
+ constructor(config = {}) {
3770
+ this.actions = [];
3771
+ this.declaredState = null;
3772
+ this.maxHistory = config.maxHistory ?? DEFAULT_MAX_HISTORY;
3773
+ }
3774
+ recordAction(record) {
3775
+ this.actions.push(record);
3776
+ if (this.actions.length > this.maxHistory) {
3777
+ this.actions = this.actions.slice(-this.maxHistory);
3778
+ }
3779
+ }
3780
+ setDeclaredState(state) {
3781
+ this.declaredState = state;
3782
+ }
3783
+ getActionHistory() {
3784
+ return this.actions.slice();
3785
+ }
3786
+ getSnapshotUndoContext() {
3787
+ const declared = this.declaredState;
3788
+ if (declared) {
3789
+ const summary2 = buildSummary({
3790
+ canUndo: declared.canUndo,
3791
+ canRedo: declared.canRedo,
3792
+ undoDescription: declared.undoDescription,
3793
+ redoDescription: declared.redoDescription
3794
+ });
3795
+ return {
3796
+ canUndo: declared.canUndo,
3797
+ canRedo: declared.canRedo,
3798
+ undoDescription: declared.undoDescription,
3799
+ redoDescription: declared.redoDescription,
3800
+ undoDepth: declared.undoDepth,
3801
+ redoDepth: declared.redoDepth,
3802
+ summary: summary2
3803
+ };
3804
+ }
3805
+ const canUndo = this.actions.some((a) => a.reversible !== false);
3806
+ const canRedo = false;
3807
+ const lastReversible = [...this.actions].reverse().find((a) => a.reversible !== false);
3808
+ const undoDescription = lastReversible?.description;
3809
+ const summary = buildSummary({ canUndo, canRedo, undoDescription });
3810
+ return {
3811
+ canUndo,
3812
+ canRedo,
3813
+ undoDescription,
3814
+ summary
3815
+ };
3816
+ }
3817
+ executeUndo() {
3818
+ return { ok: false, reason: "Not implemented in native" };
3819
+ }
3820
+ executeRedo() {
3821
+ return { ok: false, reason: "Not implemented in native" };
3822
+ }
3823
+ clear() {
3824
+ this.actions = [];
3825
+ this.declaredState = null;
3826
+ }
3827
+ };
3407
3828
  var UIBridgeNativeContext = react.createContext(null);
3408
3829
  function UIBridgeNativeProvider({
3409
3830
  children,
@@ -3420,6 +3841,9 @@ function UIBridgeNativeProvider({
3420
3841
  }) {
3421
3842
  const registryRef = react.useRef(null);
3422
3843
  const executorRef = react.useRef(null);
3844
+ const modalDetectorRef = react.useRef(null);
3845
+ const toastCaptureRef = react.useRef(null);
3846
+ const undoTrackerRef = react.useRef(null);
3423
3847
  const [serverRunning, setServerRunning] = react.useState(false);
3424
3848
  if (!registryRef.current) {
3425
3849
  registryRef.current = new NativeUIBridgeRegistry({
@@ -3433,6 +3857,12 @@ function UIBridgeNativeProvider({
3433
3857
  executorRef.current = createNativeActionExecutor(registry);
3434
3858
  }
3435
3859
  const executor = executorRef.current;
3860
+ if (!modalDetectorRef.current) modalDetectorRef.current = new ModalDetector();
3861
+ if (!toastCaptureRef.current) toastCaptureRef.current = new ToastCapture();
3862
+ if (!undoTrackerRef.current) undoTrackerRef.current = new UndoTracker();
3863
+ const modalDetector = modalDetectorRef.current;
3864
+ const toastCapture = toastCaptureRef.current;
3865
+ const undoTracker = undoTrackerRef.current;
3436
3866
  const serverRef = react.useRef(null);
3437
3867
  const eventBridgeRef = react.useRef(null);
3438
3868
  const announcerRef = react.useRef(null);
@@ -3447,7 +3877,8 @@ function UIBridgeNativeProvider({
3447
3877
  }
3448
3878
  const server = createNativeServer(registry, executor, {
3449
3879
  serverPort: config.serverPort || 8087,
3450
- cors: true
3880
+ cors: true,
3881
+ appInfo: config.appInfo
3451
3882
  });
3452
3883
  server.setAdapter(serverAdapter);
3453
3884
  if (navigationProvider) {
@@ -3534,7 +3965,8 @@ function UIBridgeNativeProvider({
3534
3965
  try {
3535
3966
  const bareServer = createNativeServer(registry, executor, {
3536
3967
  serverPort: config.serverPort || 8087,
3537
- cors: true
3968
+ cors: true,
3969
+ appInfo: config.appInfo
3538
3970
  });
3539
3971
  if (navigationProvider) bareServer.setNavigationProvider(navigationProvider);
3540
3972
  if (screenshotProvider) bareServer.setScreenshotProvider(screenshotProvider);
@@ -3619,6 +4051,22 @@ function UIBridgeNativeProvider({
3619
4051
  lastRoute = current;
3620
4052
  });
3621
4053
  }, [routeProvider, registry]);
4054
+ react.useEffect(() => {
4055
+ registry.setEnrichers({ modalDetector, toastCapture, undoTracker });
4056
+ }, [registry, modalDetector, toastCapture, undoTracker]);
4057
+ react.useEffect(() => {
4058
+ const unsubscribe = executor.onActionExecuted((event) => {
4059
+ if (!event.success) return;
4060
+ undoTracker.recordAction({
4061
+ id: event.requestId ?? `action-${event.timestamp}`,
4062
+ type: event.action,
4063
+ targetId: event.elementId,
4064
+ timestamp: event.timestamp,
4065
+ reversible: true
4066
+ });
4067
+ });
4068
+ return unsubscribe;
4069
+ }, [executor, undoTracker]);
3622
4070
  const on = react.useCallback(
3623
4071
  (type, listener) => registry.on(type, listener),
3624
4072
  [registry]
@@ -3633,6 +4081,9 @@ function UIBridgeNativeProvider({
3633
4081
  config,
3634
4082
  registry,
3635
4083
  executor,
4084
+ modalDetector,
4085
+ toastCapture,
4086
+ undoTracker,
3636
4087
  getElements,
3637
4088
  getComponents,
3638
4089
  createSnapshot,
@@ -3649,6 +4100,9 @@ function UIBridgeNativeProvider({
3649
4100
  config,
3650
4101
  registry,
3651
4102
  executor,
4103
+ modalDetector,
4104
+ toastCapture,
4105
+ undoTracker,
3652
4106
  getElements,
3653
4107
  getComponents,
3654
4108
  createSnapshot,
@@ -4166,14 +4620,48 @@ function useAutoRegister(options = {}) {
4166
4620
  isEnabled: enabled && !!bridge?.registry
4167
4621
  };
4168
4622
  }
4623
+ function useUIBridgeModal(opts) {
4624
+ const { modalDetector } = useUIBridgeNative();
4625
+ const { id, title, type, blocking, dismissible } = opts;
4626
+ react.useEffect(() => {
4627
+ modalDetector.pushModal({ id, title, type, blocking, dismissible });
4628
+ return () => {
4629
+ modalDetector.dismissModal(id);
4630
+ };
4631
+ }, [modalDetector, id, title, type, blocking, dismissible]);
4632
+ }
4633
+ function useUIBridgeToast(opts) {
4634
+ const { toastCapture } = useUIBridgeNative();
4635
+ const { id, message, level } = opts;
4636
+ react.useEffect(() => {
4637
+ toastCapture.recordToast({ id, message, level });
4638
+ return () => {
4639
+ toastCapture.dismissToast(id);
4640
+ };
4641
+ }, [toastCapture, id, message, level]);
4642
+ }
4643
+ function useToastRecorder() {
4644
+ const { toastCapture } = useUIBridgeNative();
4645
+ const recordToast = react.useCallback(
4646
+ (toast) => {
4647
+ toastCapture.recordToast(toast);
4648
+ },
4649
+ [toastCapture]
4650
+ );
4651
+ const dismissToast = react.useCallback((id) => toastCapture.dismissToast(id), [toastCapture]);
4652
+ return { recordToast, dismissToast };
4653
+ }
4169
4654
 
4170
4655
  exports.UIBridgeNativeProvider = UIBridgeNativeProvider;
4171
4656
  exports.useAutoRegister = useAutoRegister;
4657
+ exports.useToastRecorder = useToastRecorder;
4172
4658
  exports.useUIBridge = useUIBridge;
4659
+ exports.useUIBridgeModal = useUIBridgeModal;
4173
4660
  exports.useUIBridgeNative = useUIBridgeNative;
4174
4661
  exports.useUIBridgeNativeOptional = useUIBridgeNativeOptional;
4175
4662
  exports.useUIBridgeNativeRequired = useUIBridgeNativeRequired;
4176
4663
  exports.useUIBridgeRequired = useUIBridgeRequired;
4664
+ exports.useUIBridgeToast = useUIBridgeToast;
4177
4665
  exports.useUIComponent = useUIComponent;
4178
4666
  exports.useUIComponentAction = useUIComponentAction;
4179
4667
  exports.useUIElement = useUIElement;