@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
@@ -53,9 +53,34 @@ var NativeUIBridgeRegistry = class {
53
53
  this.components = /* @__PURE__ */ new Map();
54
54
  this.workflows = /* @__PURE__ */ new Map();
55
55
  this.eventListeners = /* @__PURE__ */ new Map();
56
+ this.enrichers = {};
57
+ this.snapshotExtras = /* @__PURE__ */ new Map();
56
58
  this.config = config;
57
59
  }
58
60
  // ============================================================================
61
+ // Snapshot Enricher Slots
62
+ // ============================================================================
63
+ /**
64
+ * Register/replace canonical enrichers (modal/toast/undo). HMR-safe — calling
65
+ * with a partial set merges into existing slots instead of clobbering them.
66
+ */
67
+ setEnrichers(e) {
68
+ this.enrichers = { ...this.enrichers, ...e };
69
+ }
70
+ /**
71
+ * Register a custom snapshot enricher. The returned object will be
72
+ * `Object.assign`ed onto the snapshot, so use unique top-level keys to avoid
73
+ * clobbering canonical fields. Returns a disposer.
74
+ */
75
+ registerSnapshotEnricher(name, fn) {
76
+ this.snapshotExtras.set(name, fn);
77
+ return () => this.unregisterSnapshotEnricher(name);
78
+ }
79
+ /** Remove a custom snapshot enricher by name */
80
+ unregisterSnapshotEnricher(name) {
81
+ this.snapshotExtras.delete(name);
82
+ }
83
+ // ============================================================================
59
84
  // Element Management
60
85
  // ============================================================================
61
86
  /**
@@ -418,7 +443,7 @@ var NativeUIBridgeRegistry = class {
418
443
  const currentRoute = routeInfo.currentRoute;
419
444
  elements = elements.filter((e) => e.registrationRoute === currentRoute);
420
445
  }
421
- return {
446
+ const snapshot = {
422
447
  timestamp: Date.now(),
423
448
  elements: elements.map((e) => {
424
449
  const handlers = extractHandlerNames(e.props);
@@ -450,6 +475,49 @@ var NativeUIBridgeRegistry = class {
450
475
  currentRoute: routeInfo?.currentRoute ?? null,
451
476
  segments: routeInfo?.segments
452
477
  };
478
+ if (this.enrichers.modalDetector) {
479
+ try {
480
+ snapshot.modalStack = this.enrichers.modalDetector.getSnapshotModalContext();
481
+ } catch (error3) {
482
+ if (this.config.verbose) {
483
+ console.warn(`[ui-bridge-native] modalDetector enricher threw:`, error3);
484
+ }
485
+ }
486
+ }
487
+ if (this.enrichers.toastCapture) {
488
+ try {
489
+ snapshot.toasts = this.enrichers.toastCapture.getSnapshotToastContext();
490
+ } catch (error3) {
491
+ if (this.config.verbose) {
492
+ console.warn(`[ui-bridge-native] toastCapture enricher threw:`, error3);
493
+ }
494
+ }
495
+ }
496
+ if (this.enrichers.undoTracker) {
497
+ try {
498
+ snapshot.undoRedo = this.enrichers.undoTracker.getSnapshotUndoContext();
499
+ } catch (error3) {
500
+ if (this.config.verbose) {
501
+ console.warn(`[ui-bridge-native] undoTracker enricher threw:`, error3);
502
+ }
503
+ }
504
+ }
505
+ if (this.snapshotExtras.size > 0) {
506
+ const ctx = { elements, currentRoute: routeInfo?.currentRoute ?? null };
507
+ for (const [name, fn] of this.snapshotExtras) {
508
+ try {
509
+ const extra = fn(ctx);
510
+ if (extra && typeof extra === "object") {
511
+ Object.assign(snapshot, extra);
512
+ }
513
+ } catch (error3) {
514
+ if (this.config.verbose) {
515
+ console.warn(`[ui-bridge-native] snapshot enricher "${name}" threw:`, error3);
516
+ }
517
+ }
518
+ }
519
+ }
520
+ return snapshot;
453
521
  }
454
522
  /**
455
523
  * Get registry statistics
@@ -543,6 +611,23 @@ function sleep(ms) {
543
611
  var DefaultNativeActionExecutor = class {
544
612
  constructor(registry) {
545
613
  this.registry = registry;
614
+ this.actionListeners = /* @__PURE__ */ new Set();
615
+ }
616
+ onActionExecuted(listener) {
617
+ this.actionListeners.add(listener);
618
+ return () => {
619
+ this.actionListeners.delete(listener);
620
+ };
621
+ }
622
+ /** Notify listeners out-of-band; isolate each invocation so one bad listener can't poison the rest. */
623
+ emitActionEvent(event) {
624
+ for (const listener of this.actionListeners) {
625
+ try {
626
+ listener(event);
627
+ } catch (error3) {
628
+ console.warn(`[ui-bridge-native] Action listener error:`, error3);
629
+ }
630
+ }
546
631
  }
547
632
  /**
548
633
  * Execute an action on an element
@@ -556,11 +641,23 @@ var DefaultNativeActionExecutor = class {
556
641
  registered = findElementByIdentifier(elementId) ?? void 0;
557
642
  }
558
643
  if (!registered) {
644
+ const failTime = Date.now();
645
+ const errorMsg = `Element not found: ${elementId}`;
646
+ this.emitActionEvent({
647
+ elementId,
648
+ action: request.action,
649
+ params: request.params,
650
+ success: false,
651
+ error: errorMsg,
652
+ timestamp: failTime,
653
+ requestId: request.requestId,
654
+ durationMs: failTime - startTime
655
+ });
559
656
  return {
560
657
  success: false,
561
- error: `Element not found: ${elementId}`,
562
- durationMs: Date.now() - startTime,
563
- timestamp: Date.now(),
658
+ error: errorMsg,
659
+ durationMs: failTime - startTime,
660
+ timestamp: failTime,
564
661
  requestId: request.requestId
565
662
  };
566
663
  }
@@ -568,33 +665,67 @@ var DefaultNativeActionExecutor = class {
568
665
  const waitResult = await this.waitForElementInternal(registered.id, request.waitOptions);
569
666
  waitDurationMs = waitResult.waitedMs;
570
667
  if (!waitResult.met) {
668
+ const failTime = Date.now();
669
+ const errorMsg = waitResult.error || "Wait condition not met";
670
+ this.emitActionEvent({
671
+ elementId,
672
+ action: request.action,
673
+ params: request.params,
674
+ success: false,
675
+ error: errorMsg,
676
+ timestamp: failTime,
677
+ requestId: request.requestId,
678
+ durationMs: failTime - startTime
679
+ });
571
680
  return {
572
681
  success: false,
573
- error: waitResult.error || "Wait condition not met",
574
- durationMs: Date.now() - startTime,
575
- timestamp: Date.now(),
682
+ error: errorMsg,
683
+ durationMs: failTime - startTime,
684
+ timestamp: failTime,
576
685
  requestId: request.requestId,
577
686
  waitDurationMs
578
687
  };
579
688
  }
580
689
  }
581
690
  const result = await this.performAction(registered, request.action, request.params);
691
+ const successTime = Date.now();
692
+ this.emitActionEvent({
693
+ elementId,
694
+ action: request.action,
695
+ params: request.params,
696
+ success: true,
697
+ timestamp: successTime,
698
+ requestId: request.requestId,
699
+ durationMs: successTime - startTime
700
+ });
582
701
  return {
583
702
  success: true,
584
703
  elementState: registered.getState(),
585
704
  result,
586
- durationMs: Date.now() - startTime,
587
- timestamp: Date.now(),
705
+ durationMs: successTime - startTime,
706
+ timestamp: successTime,
588
707
  requestId: request.requestId,
589
708
  waitDurationMs
590
709
  };
591
710
  } catch (error3) {
711
+ const failTime = Date.now();
712
+ const errorMsg = error3 instanceof Error ? error3.message : String(error3);
713
+ this.emitActionEvent({
714
+ elementId,
715
+ action: request.action,
716
+ params: request.params,
717
+ success: false,
718
+ error: errorMsg,
719
+ timestamp: failTime,
720
+ requestId: request.requestId,
721
+ durationMs: failTime - startTime
722
+ });
592
723
  return {
593
724
  success: false,
594
- error: error3 instanceof Error ? error3.message : String(error3),
725
+ error: errorMsg,
595
726
  stack: error3 instanceof Error ? error3.stack : void 0,
596
- durationMs: Date.now() - startTime,
597
- timestamp: Date.now(),
727
+ durationMs: failTime - startTime,
728
+ timestamp: failTime,
598
729
  requestId: request.requestId,
599
730
  waitDurationMs
600
731
  };
@@ -687,7 +818,7 @@ var DefaultNativeActionExecutor = class {
687
818
  if (!params?.text) {
688
819
  throw new Error("Type action requires text parameter");
689
820
  }
690
- if (params.clearFirst) {
821
+ if (params.clearFirst || params.clear) {
691
822
  await this.performClear(element, props);
692
823
  }
693
824
  if (params.delay && params.delay > 0) {
@@ -1873,6 +2004,15 @@ function createServerHandlers(registry, executor, config) {
1873
2004
  timestamp: Date.now(),
1874
2005
  ...stats
1875
2006
  };
2007
+ if (config?.appInfo) {
2008
+ response.uiBridge = {
2009
+ version: "0.3.0",
2010
+ ...config.appInfo,
2011
+ capabilities: ["elements", "components", "actions", "design"],
2012
+ elementCount: stats.elements,
2013
+ componentCount: stats.components
2014
+ };
2015
+ }
1876
2016
  return success2(response);
1877
2017
  }
1878
2018
  };
@@ -1911,6 +2051,10 @@ var WS_ROUTES = [
1911
2051
  { pattern: "health", handler: "health", httpMethods: ["GET"] },
1912
2052
  // Elements
1913
2053
  { pattern: "control/elements", handler: "getElements", httpMethods: ["GET"] },
2054
+ // Flat alias — matches the runner UI Bridge's `/ui-bridge/elements` path so
2055
+ // agents can use the same URL across runner + mobile without remembering
2056
+ // the `control/` prefix. Delegates to the same handler.
2057
+ { pattern: "elements", handler: "getElements", httpMethods: ["GET"] },
1914
2058
  { pattern: "control/element/:id", handler: "getElement", httpMethods: ["GET"] },
1915
2059
  { pattern: "control/element/:id/state", handler: "getElementState", httpMethods: ["GET"] },
1916
2060
  { pattern: "control/element/:id/action", handler: "executeAction", httpMethods: ["POST"] },
@@ -1961,6 +2105,87 @@ var WS_ROUTES = [
1961
2105
  // Meta / Introspection
1962
2106
  { pattern: "meta/methods", handler: "getMethods", httpMethods: ["GET"] }
1963
2107
  ];
2108
+ var HANDLER_DESCRIPTIONS = {
2109
+ health: "Liveness probe; returns app metadata + element count",
2110
+ getElements: "Flat list of registered UI elements",
2111
+ getElement: "Single element by id",
2112
+ getElementState: "Dynamic state (value, focused, checked) for one element",
2113
+ executeAction: 'Run an action on an element (see "actions" below for names)',
2114
+ getComponents: "Higher-level components with action schemas",
2115
+ getComponent: "Component detail + per-action paramSchema",
2116
+ executeComponentAction: "Invoke a component action directly (preferred over button clicks)",
2117
+ find: "AI-powered element search by natural-language query",
2118
+ getSnapshot: "Full snapshot: elements + route + state. Supports ?visibleOnly=true and ?currentRouteOnly=true",
2119
+ getWorkflows: "List registered workflows",
2120
+ runWorkflow: "Trigger a workflow by id",
2121
+ pageRefresh: "Reload the current route",
2122
+ pageNavigate: "Imperative router.push(url) \u2014 preferred over tapping tab buttons",
2123
+ pageReplace: "router.replace(url)",
2124
+ pageGoBack: "router.back()",
2125
+ pageGoForward: "router.forward()",
2126
+ getScreenshot: "PNG screenshot of the current screen",
2127
+ getElementStyles: "Computed styles for an element (design review)",
2128
+ getElementStateStyles: "Computed styles per interaction state (hover/focus/disabled)",
2129
+ getDesignSnapshot: "Visual + style snapshot for design review",
2130
+ getResponsiveSnapshots: "Screenshots at multiple viewport widths",
2131
+ runDesignAudit: "Accessibility + visual audit",
2132
+ loadStyleGuide: "Load a design system style guide",
2133
+ getStyleGuide: "Fetch the currently loaded style guide",
2134
+ clearStyleGuide: "Unload the style guide",
2135
+ evaluateQuality: "Score the current UI against a quality context",
2136
+ getQualityContexts: "List available quality evaluation contexts",
2137
+ saveBaseline: "Save a quality baseline snapshot",
2138
+ diffBaseline: "Diff current UI against a saved baseline",
2139
+ getMethods: "List every handler name (legacy introspection)"
2140
+ };
2141
+ var ACTION_REFERENCE = [
2142
+ { name: "press", aliases: ["click"], params: "PressActionParams (optional position)" },
2143
+ { name: "longPress", params: "PressActionParams (optional position)" },
2144
+ { name: "doubleTap", params: "none" },
2145
+ {
2146
+ name: "type",
2147
+ params: "TypeActionParams { text, delay?, clear? | clearFirst? } \u2014 `clear:true` replaces content atomically"
2148
+ },
2149
+ { name: "clear", params: "none \u2014 empties the input" },
2150
+ { name: "focus", params: "none" },
2151
+ { name: "blur", params: "none" },
2152
+ { name: "scroll", params: "ScrollActionParams { offset? {x,y}, to? }" },
2153
+ { name: "swipe", params: "SwipeActionParams { direction, distance }" },
2154
+ { name: "toggle", params: "none \u2014 flips boolean controls" }
2155
+ ];
2156
+ function buildRoutesPayload() {
2157
+ const entries = [];
2158
+ for (const r of WS_ROUTES) {
2159
+ if (!r.httpMethods) continue;
2160
+ for (const m of r.httpMethods) {
2161
+ entries.push({ method: m, path: `/ui-bridge/${r.pattern}` });
2162
+ }
2163
+ }
2164
+ entries.push({ method: "GET", path: "/ui-bridge/_help" });
2165
+ entries.push({ method: "GET", path: "/ui-bridge/_routes" });
2166
+ entries.sort((a, b) => a.path.localeCompare(b.path) || a.method.localeCompare(b.method));
2167
+ return { routes: entries, count: entries.length };
2168
+ }
2169
+ function buildHelpPayload() {
2170
+ return {
2171
+ version: 1,
2172
+ description: "UI Bridge Native HTTP surface. Prefix every path with /ui-bridge/.",
2173
+ routes: WS_ROUTES.map((r) => ({
2174
+ pattern: r.pattern,
2175
+ httpMethods: r.httpMethods ?? [],
2176
+ wsOnly: !r.httpMethods,
2177
+ handler: r.handler,
2178
+ description: HANDLER_DESCRIPTIONS[r.handler] ?? ""
2179
+ })),
2180
+ actions: ACTION_REFERENCE,
2181
+ tips: [
2182
+ "Start with `GET /ui-bridge/control/snapshot?visibleOnly=true` to see what the user actually sees.",
2183
+ "Prefer `control/component/:id/action/:actionId` over button presses \u2014 typed paramSchema, no button-id guessing.",
2184
+ "Use `pageNavigate` for tab switches instead of pressing tab buttons; gesture state often swallows clicks.",
2185
+ "The `type` action supports `clear:true` to replace existing text atomically in one call."
2186
+ ]
2187
+ };
2188
+ }
1964
2189
  var NativeUIBridgeServer = class {
1965
2190
  constructor(registry, executor, config = {}) {
1966
2191
  this.registry = registry;
@@ -1977,7 +2202,9 @@ var NativeUIBridgeServer = class {
1977
2202
  cors: true,
1978
2203
  ...config
1979
2204
  };
1980
- this.handlers = createServerHandlers(registry, executor);
2205
+ this.handlers = createServerHandlers(registry, executor, {
2206
+ appInfo: this.config.appInfo
2207
+ });
1981
2208
  this.handlers.getMethods = async () => {
1982
2209
  const methods = WS_ROUTES.map((r) => ({
1983
2210
  method: r.pattern,
@@ -2781,6 +3008,20 @@ var NativeUIBridgeServer = class {
2781
3008
  };
2782
3009
  }
2783
3010
  const stripped = path.slice(PREFIX.length);
3011
+ if (method === "GET" && stripped === "_help") {
3012
+ return {
3013
+ success: true,
3014
+ data: buildHelpPayload(),
3015
+ timestamp: Date.now()
3016
+ };
3017
+ }
3018
+ if (method === "GET" && stripped === "_routes") {
3019
+ return {
3020
+ success: true,
3021
+ data: buildRoutesPayload(),
3022
+ timestamp: Date.now()
3023
+ };
3024
+ }
2784
3025
  let patternMatchedButWrongVerb = false;
2785
3026
  for (const route of WS_ROUTES) {
2786
3027
  const pathParams = parsePath(route.pattern, stripped);
@@ -3402,6 +3643,186 @@ function parseQueryParams(path) {
3402
3643
  }
3403
3644
  return result;
3404
3645
  }
3646
+
3647
+ // src/modal/modal-detector.ts
3648
+ var ModalDetector = class {
3649
+ constructor() {
3650
+ /** Map preserves O(1) idempotent replace by id; ordering comes from detectedAt. */
3651
+ this.modals = /* @__PURE__ */ new Map();
3652
+ }
3653
+ pushModal(modal) {
3654
+ const existing = this.modals.get(modal.id);
3655
+ const detectedAt = existing ? existing.detectedAt : Date.now();
3656
+ const next = {
3657
+ id: modal.id,
3658
+ title: modal.title,
3659
+ type: modal.type ?? "modal",
3660
+ blocking: modal.blocking ?? true,
3661
+ dismissible: modal.dismissible ?? true,
3662
+ detectedAt
3663
+ };
3664
+ this.modals.set(modal.id, next);
3665
+ }
3666
+ dismissModal(id) {
3667
+ this.modals.delete(id);
3668
+ }
3669
+ getSnapshotModalContext() {
3670
+ const active = this.getActive();
3671
+ const ctx = {
3672
+ modals: active,
3673
+ hasBlockingModal: active.some((m) => m.blocking),
3674
+ count: active.length
3675
+ };
3676
+ if (active.length > 0) {
3677
+ ctx.topModal = active[active.length - 1];
3678
+ }
3679
+ return ctx;
3680
+ }
3681
+ getActive() {
3682
+ return Array.from(this.modals.values()).sort((a, b) => a.detectedAt - b.detectedAt);
3683
+ }
3684
+ clear() {
3685
+ this.modals.clear();
3686
+ }
3687
+ };
3688
+
3689
+ // src/toast/toast-capture.ts
3690
+ var DEFAULT_MAX_RECENT = 50;
3691
+ var ToastCapture = class {
3692
+ constructor(config = {}) {
3693
+ this.active = /* @__PURE__ */ new Map();
3694
+ this.recent = [];
3695
+ this.totalCaptured = 0;
3696
+ this.maxRecent = config.maxRecent ?? DEFAULT_MAX_RECENT;
3697
+ }
3698
+ recordToast(toast) {
3699
+ if (this.active.has(toast.id)) {
3700
+ return;
3701
+ }
3702
+ const appearedAt = toast.appearedAt ?? Date.now();
3703
+ const entry = {
3704
+ id: toast.id,
3705
+ message: toast.message,
3706
+ level: toast.level ?? "info",
3707
+ appearedAt,
3708
+ visible: true,
3709
+ durationMs: 0
3710
+ };
3711
+ this.active.set(toast.id, entry);
3712
+ this.totalCaptured += 1;
3713
+ }
3714
+ dismissToast(id) {
3715
+ const entry = this.active.get(id);
3716
+ if (!entry) {
3717
+ return;
3718
+ }
3719
+ this.active.delete(id);
3720
+ const dismissedAt = Date.now();
3721
+ const dismissed = {
3722
+ ...entry,
3723
+ dismissedAt,
3724
+ visible: false,
3725
+ durationMs: dismissedAt - entry.appearedAt
3726
+ };
3727
+ this.recent.push(dismissed);
3728
+ if (this.recent.length > this.maxRecent) {
3729
+ this.recent.shift();
3730
+ }
3731
+ }
3732
+ getSnapshotToastContext() {
3733
+ const now = Date.now();
3734
+ const active = Array.from(this.active.values()).map((entry) => ({
3735
+ ...entry,
3736
+ durationMs: now - entry.appearedAt
3737
+ }));
3738
+ return {
3739
+ active,
3740
+ recent: [...this.recent],
3741
+ totalCaptured: this.totalCaptured
3742
+ };
3743
+ }
3744
+ clear() {
3745
+ this.active.clear();
3746
+ this.recent = [];
3747
+ this.totalCaptured = 0;
3748
+ }
3749
+ };
3750
+
3751
+ // src/undo/undo-tracker.ts
3752
+ var DEFAULT_MAX_HISTORY = 50;
3753
+ function buildSummary(ctx) {
3754
+ if (!ctx.canUndo && !ctx.canRedo) {
3755
+ return "no undo available";
3756
+ }
3757
+ const parts = [];
3758
+ if (ctx.canUndo) {
3759
+ parts.push(`undo: '${ctx.undoDescription ?? "available"}'`);
3760
+ }
3761
+ if (ctx.canRedo) {
3762
+ parts.push(`redo: '${ctx.redoDescription ?? "available"}'`);
3763
+ }
3764
+ return parts.join(" / ");
3765
+ }
3766
+ var UndoTracker = class {
3767
+ constructor(config = {}) {
3768
+ this.actions = [];
3769
+ this.declaredState = null;
3770
+ this.maxHistory = config.maxHistory ?? DEFAULT_MAX_HISTORY;
3771
+ }
3772
+ recordAction(record) {
3773
+ this.actions.push(record);
3774
+ if (this.actions.length > this.maxHistory) {
3775
+ this.actions = this.actions.slice(-this.maxHistory);
3776
+ }
3777
+ }
3778
+ setDeclaredState(state) {
3779
+ this.declaredState = state;
3780
+ }
3781
+ getActionHistory() {
3782
+ return this.actions.slice();
3783
+ }
3784
+ getSnapshotUndoContext() {
3785
+ const declared = this.declaredState;
3786
+ if (declared) {
3787
+ const summary2 = buildSummary({
3788
+ canUndo: declared.canUndo,
3789
+ canRedo: declared.canRedo,
3790
+ undoDescription: declared.undoDescription,
3791
+ redoDescription: declared.redoDescription
3792
+ });
3793
+ return {
3794
+ canUndo: declared.canUndo,
3795
+ canRedo: declared.canRedo,
3796
+ undoDescription: declared.undoDescription,
3797
+ redoDescription: declared.redoDescription,
3798
+ undoDepth: declared.undoDepth,
3799
+ redoDepth: declared.redoDepth,
3800
+ summary: summary2
3801
+ };
3802
+ }
3803
+ const canUndo = this.actions.some((a) => a.reversible !== false);
3804
+ const canRedo = false;
3805
+ const lastReversible = [...this.actions].reverse().find((a) => a.reversible !== false);
3806
+ const undoDescription = lastReversible?.description;
3807
+ const summary = buildSummary({ canUndo, canRedo, undoDescription });
3808
+ return {
3809
+ canUndo,
3810
+ canRedo,
3811
+ undoDescription,
3812
+ summary
3813
+ };
3814
+ }
3815
+ executeUndo() {
3816
+ return { ok: false, reason: "Not implemented in native" };
3817
+ }
3818
+ executeRedo() {
3819
+ return { ok: false, reason: "Not implemented in native" };
3820
+ }
3821
+ clear() {
3822
+ this.actions = [];
3823
+ this.declaredState = null;
3824
+ }
3825
+ };
3405
3826
  var UIBridgeNativeContext = createContext(null);
3406
3827
  function UIBridgeNativeProvider({
3407
3828
  children,
@@ -3418,6 +3839,9 @@ function UIBridgeNativeProvider({
3418
3839
  }) {
3419
3840
  const registryRef = useRef(null);
3420
3841
  const executorRef = useRef(null);
3842
+ const modalDetectorRef = useRef(null);
3843
+ const toastCaptureRef = useRef(null);
3844
+ const undoTrackerRef = useRef(null);
3421
3845
  const [serverRunning, setServerRunning] = useState(false);
3422
3846
  if (!registryRef.current) {
3423
3847
  registryRef.current = new NativeUIBridgeRegistry({
@@ -3431,6 +3855,12 @@ function UIBridgeNativeProvider({
3431
3855
  executorRef.current = createNativeActionExecutor(registry);
3432
3856
  }
3433
3857
  const executor = executorRef.current;
3858
+ if (!modalDetectorRef.current) modalDetectorRef.current = new ModalDetector();
3859
+ if (!toastCaptureRef.current) toastCaptureRef.current = new ToastCapture();
3860
+ if (!undoTrackerRef.current) undoTrackerRef.current = new UndoTracker();
3861
+ const modalDetector = modalDetectorRef.current;
3862
+ const toastCapture = toastCaptureRef.current;
3863
+ const undoTracker = undoTrackerRef.current;
3434
3864
  const serverRef = useRef(null);
3435
3865
  const eventBridgeRef = useRef(null);
3436
3866
  const announcerRef = useRef(null);
@@ -3445,7 +3875,8 @@ function UIBridgeNativeProvider({
3445
3875
  }
3446
3876
  const server = createNativeServer(registry, executor, {
3447
3877
  serverPort: config.serverPort || 8087,
3448
- cors: true
3878
+ cors: true,
3879
+ appInfo: config.appInfo
3449
3880
  });
3450
3881
  server.setAdapter(serverAdapter);
3451
3882
  if (navigationProvider) {
@@ -3532,7 +3963,8 @@ function UIBridgeNativeProvider({
3532
3963
  try {
3533
3964
  const bareServer = createNativeServer(registry, executor, {
3534
3965
  serverPort: config.serverPort || 8087,
3535
- cors: true
3966
+ cors: true,
3967
+ appInfo: config.appInfo
3536
3968
  });
3537
3969
  if (navigationProvider) bareServer.setNavigationProvider(navigationProvider);
3538
3970
  if (screenshotProvider) bareServer.setScreenshotProvider(screenshotProvider);
@@ -3617,6 +4049,22 @@ function UIBridgeNativeProvider({
3617
4049
  lastRoute = current;
3618
4050
  });
3619
4051
  }, [routeProvider, registry]);
4052
+ useEffect(() => {
4053
+ registry.setEnrichers({ modalDetector, toastCapture, undoTracker });
4054
+ }, [registry, modalDetector, toastCapture, undoTracker]);
4055
+ useEffect(() => {
4056
+ const unsubscribe = executor.onActionExecuted((event) => {
4057
+ if (!event.success) return;
4058
+ undoTracker.recordAction({
4059
+ id: event.requestId ?? `action-${event.timestamp}`,
4060
+ type: event.action,
4061
+ targetId: event.elementId,
4062
+ timestamp: event.timestamp,
4063
+ reversible: true
4064
+ });
4065
+ });
4066
+ return unsubscribe;
4067
+ }, [executor, undoTracker]);
3620
4068
  const on = useCallback(
3621
4069
  (type, listener) => registry.on(type, listener),
3622
4070
  [registry]
@@ -3631,6 +4079,9 @@ function UIBridgeNativeProvider({
3631
4079
  config,
3632
4080
  registry,
3633
4081
  executor,
4082
+ modalDetector,
4083
+ toastCapture,
4084
+ undoTracker,
3634
4085
  getElements,
3635
4086
  getComponents,
3636
4087
  createSnapshot,
@@ -3647,6 +4098,9 @@ function UIBridgeNativeProvider({
3647
4098
  config,
3648
4099
  registry,
3649
4100
  executor,
4101
+ modalDetector,
4102
+ toastCapture,
4103
+ undoTracker,
3650
4104
  getElements,
3651
4105
  getComponents,
3652
4106
  createSnapshot,
@@ -4164,7 +4618,38 @@ function useAutoRegister(options = {}) {
4164
4618
  isEnabled: enabled && !!bridge?.registry
4165
4619
  };
4166
4620
  }
4621
+ function useUIBridgeModal(opts) {
4622
+ const { modalDetector } = useUIBridgeNative();
4623
+ const { id, title, type, blocking, dismissible } = opts;
4624
+ useEffect(() => {
4625
+ modalDetector.pushModal({ id, title, type, blocking, dismissible });
4626
+ return () => {
4627
+ modalDetector.dismissModal(id);
4628
+ };
4629
+ }, [modalDetector, id, title, type, blocking, dismissible]);
4630
+ }
4631
+ function useUIBridgeToast(opts) {
4632
+ const { toastCapture } = useUIBridgeNative();
4633
+ const { id, message, level } = opts;
4634
+ useEffect(() => {
4635
+ toastCapture.recordToast({ id, message, level });
4636
+ return () => {
4637
+ toastCapture.dismissToast(id);
4638
+ };
4639
+ }, [toastCapture, id, message, level]);
4640
+ }
4641
+ function useToastRecorder() {
4642
+ const { toastCapture } = useUIBridgeNative();
4643
+ const recordToast = useCallback(
4644
+ (toast) => {
4645
+ toastCapture.recordToast(toast);
4646
+ },
4647
+ [toastCapture]
4648
+ );
4649
+ const dismissToast = useCallback((id) => toastCapture.dismissToast(id), [toastCapture]);
4650
+ return { recordToast, dismissToast };
4651
+ }
4167
4652
 
4168
- export { UIBridgeNativeProvider, useAutoRegister, useUIBridge, useUIBridgeNative, useUIBridgeNativeOptional, useUIBridgeNativeRequired, useUIBridgeRequired, useUIComponent, useUIComponentAction, useUIElement, useUIElementWithProps };
4653
+ export { UIBridgeNativeProvider, useAutoRegister, useToastRecorder, useUIBridge, useUIBridgeModal, useUIBridgeNative, useUIBridgeNativeOptional, useUIBridgeNativeRequired, useUIBridgeRequired, useUIBridgeToast, useUIComponent, useUIComponentAction, useUIElement, useUIElementWithProps };
4169
4654
  //# sourceMappingURL=index.mjs.map
4170
4655
  //# sourceMappingURL=index.mjs.map