@dimina-kit/devtools 0.4.0-dev.20260630070008 → 0.4.0-dev.20260630123203

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.
@@ -11,6 +11,8 @@ import { ServiceHostPool } from '../services/service-host-pool/pool.js';
11
11
  import { registerMiniappSessionConfigurator, SHARED_MINIAPP_PARTITION, } from '../services/views/miniapp-partition.js';
12
12
  import { createConsoleForwarder } from '../services/console-forward/index.js';
13
13
  import { STORAGE_API_NAMES } from '../services/simulator-storage/index.js';
14
+ import { buildPageScrollScript } from './page-scroll.js';
15
+ import { createAppLifecycleController, } from './app-lifecycle.js';
14
16
  // The compiled `logic.js` ships a RELATIVE `//# sourceMappingURL=logic.js.map`.
15
17
  // `injectLogicBundle` loads it via `executeJavaScript`, which gives the injected
16
18
  // script no base URL of its own — so DevTools resolves that relative map against
@@ -152,6 +154,7 @@ export function installBridgeRouter(ctx) {
152
154
  emitRenderEvent: () => { },
153
155
  connections: ctx.connections,
154
156
  debugTap: createDebugTap({ enabled: resolveDebugTapEnabled() }),
157
+ appLifecycle: createAppLifecycleController(),
155
158
  evictAppDataBridges: (ap) => {
156
159
  if (!ctx.appData)
157
160
  return;
@@ -180,6 +183,7 @@ export function installBridgeRouter(ctx) {
180
183
  ctx.registry.add(() => clearTimeout(warmTimer));
181
184
  ctx.registry.add(() => state.pool?.dispose());
182
185
  }
186
+ installAppLifecycleDriver(ctx, state);
183
187
  installResourceProtocolHandlers(ctx, state);
184
188
  // The currently-selected device (renderer toolbar). Cached here so it can ride
185
189
  // the NATIVE_HOST_ENABLED reply (race-free DeviceShell init) and so the
@@ -490,6 +494,11 @@ async function handleSpawn(state, ctx, event, opts) {
490
494
  : '';
491
495
  const pkgRoot = path.resolve(opts.pkgRoot || workspaceProjectPath || process.cwd());
492
496
  const root = opts.root || 'main';
497
+ // Host-config custom API namespaces (WorkbenchContext is the single owner).
498
+ // Threaded into the service-host spawn URL so its preload can install the
499
+ // namespace globals; the simulator-supplied `opts.apiNamespaces` is derived
500
+ // from the same host config and is not authoritative here.
501
+ const apiNamespaces = ctx.apiNamespaces ?? [];
493
502
  // Resource base resolution. Preferred: the simulator-supplied dev-server
494
503
  // origin, which statically serves the compiled `<appId>/<root>/…` tree (same
495
504
  // source the default dimina-fe `<webview>` reads). Fallback (no dev server —
@@ -550,6 +559,7 @@ async function handleSpawn(state, ctx, event, opts) {
550
559
  root,
551
560
  resourceBaseUrl,
552
561
  hostEnvSnapshot: hostEnv,
562
+ apiNamespaces,
553
563
  });
554
564
  }
555
565
  const appSession = {
@@ -562,6 +572,7 @@ async function handleSpawn(state, ctx, event, opts) {
562
572
  serviceWc: serviceWindow.webContents,
563
573
  simulatorWc,
564
574
  serviceLoaded: false,
575
+ logicInjected: null,
565
576
  resourceBaseUrl,
566
577
  resourceServer,
567
578
  hostEnv,
@@ -583,6 +594,7 @@ async function handleSpawn(state, ctx, event, opts) {
583
594
  renderWc: null,
584
595
  renderLoaded: false,
585
596
  resourceLoadedSent: false,
597
+ renderLoadPending: false,
586
598
  windowConfig: rootWindowConfig,
587
599
  };
588
600
  state.appSessions.set(appSessionId, appSession);
@@ -653,7 +665,7 @@ async function handleSpawn(state, ctx, event, opts) {
653
665
  if (!serviceWindow.webContents.getURL().includes('service.html'))
654
666
  return;
655
667
  serviceWindow.webContents.removeListener('did-finish-load', bootOnServiceLoad);
656
- void bootServiceHost(state, appSession);
668
+ void bootServiceHost(state, appSession, ctx);
657
669
  };
658
670
  appSession.onServiceBoot = bootOnServiceLoad;
659
671
  serviceWindow.webContents.on('did-finish-load', bootOnServiceLoad);
@@ -665,13 +677,14 @@ async function handleSpawn(state, ctx, event, opts) {
665
677
  root,
666
678
  resourceBaseUrl,
667
679
  hostEnvSnapshot: hostEnv,
680
+ apiNamespaces,
668
681
  }));
669
682
  }
670
683
  else {
671
684
  // Fresh window: its only navigation is service.html (issued inside
672
685
  // createServiceHostWindow), so the first did-finish-load is the spawn load.
673
686
  serviceWindow.webContents.once('did-finish-load', () => {
674
- void bootServiceHost(state, appSession);
687
+ void bootServiceHost(state, appSession, ctx);
675
688
  });
676
689
  }
677
690
  return {
@@ -707,6 +720,7 @@ async function handlePageOpen(state, event, opts) {
707
720
  renderWc: null,
708
721
  renderLoaded: false,
709
722
  resourceLoadedSent: false,
723
+ renderLoadPending: false,
710
724
  windowConfig,
711
725
  };
712
726
  state.pageSessions.set(bridgeId, page);
@@ -761,26 +775,53 @@ function handleNavCallback(state, sender, payload) {
761
775
  sendCallback(ap, payload.callbacks.complete, result);
762
776
  }
763
777
  // ── Service-host boot & per-page resource handshake ──────────────────────────
764
- async function bootServiceHost(state, ap) {
778
+ async function bootServiceHost(state, ap, ctx) {
765
779
  // Liveness guard: never boot a session that was already disposed. With pooling,
766
780
  // the service window is recycled, so a stale did-finish-load listener from an
767
781
  // early-disposed prior owner could otherwise fire here and inject the wrong
768
782
  // app's logic.js into the next spawn (the recycled webContents is shared).
769
783
  if (state.appSessions.get(ap.appSessionId) !== ap)
770
784
  return;
771
- await injectLogicBundle(ap);
785
+ ap.logicInjected = await injectLogicBundle(ap);
786
+ if (!ap.logicInjected) {
787
+ // The compiled logic.js never executed, so `modDefine` registered nothing.
788
+ // Sending `loadResource` now would run `modRequire('app')` against an empty
789
+ // registry and throw the cryptic `module app not found` — masking the real
790
+ // cause (unreachable resource tree / failed compile / unresolved appId).
791
+ // Skip it and surface one actionable diagnostic instead; the render side
792
+ // gates on the same flag in `routeFromRender`.
793
+ reportLogicLoadFailure(ap, ctx);
794
+ return;
795
+ }
772
796
  // serviceLoaded is flipped only when service responds with
773
797
  // `serviceResourceLoaded`; see handleContainerMsg. Setting it here would
774
798
  // race a per-page `resourceLoaded` ahead of the service-side handler.
775
799
  forwardToService(ap, makeLoadResource(ap, ap.pages.get(ap.appSessionId), 'service'));
800
+ // Flush any render `loadResource` that arrived (renderHostReady) while
801
+ // injection was in-flight — now that the bundle is confirmed present.
802
+ for (const page of ap.pages.values()) {
803
+ if (page.renderLoadPending) {
804
+ page.renderLoadPending = false;
805
+ sendRenderLoadResource(ap, page);
806
+ }
807
+ }
808
+ }
809
+ function sendRenderLoadResource(ap, page) {
810
+ if (page.renderWc && !page.renderWc.isDestroyed()) {
811
+ page.renderWc.send(C.TO_RENDER, { msg: makeLoadResource(ap, page, 'render') });
812
+ }
813
+ }
814
+ /** The compiled logic.js URL `injectLogicBundle` fetches (mode-dependent). */
815
+ function logicBundleUrl(ap) {
816
+ return ap.resourceServer
817
+ ? new URL('logic.js', ap.resourceBaseUrl).toString()
818
+ : new URL(`${ap.appId}/${ap.root}/logic.js`, ap.resourceBaseUrl).toString();
776
819
  }
777
820
  async function injectLogicBundle(ap) {
778
821
  // Fetch the compiled service logic over HTTP from the same base the render
779
822
  // host reads (`<base><appId>/<root>/logic.js`). The fallback local server
780
823
  // serves its root, so `<base>logic.js` resolves there too — both are http.
781
- const logicUrl = ap.resourceServer
782
- ? new URL('logic.js', ap.resourceBaseUrl).toString()
783
- : new URL(`${ap.appId}/${ap.root}/logic.js`, ap.resourceBaseUrl).toString();
824
+ const logicUrl = logicBundleUrl(ap);
784
825
  try {
785
826
  const res = await fetch(logicUrl);
786
827
  if (!res.ok)
@@ -790,11 +831,31 @@ async function injectLogicBundle(ap) {
790
831
  // relative map would 404 and break sourcemapped console frames / Sources.
791
832
  const logicContent = rewriteSourceMappingUrl(await res.text(), logicUrl);
792
833
  await ap.serviceWc.executeJavaScript(`${logicContent}\n//# sourceURL=${logicUrl}`, true);
834
+ return true;
793
835
  }
794
836
  catch (error) {
795
837
  console.warn('[bridge-router] unable to inject service logic.js:', error);
838
+ return false;
796
839
  }
797
840
  }
841
+ /**
842
+ * Emit one actionable diagnostic when the compiled logic bundle could not be
843
+ * loaded — the single point where "the app's compiled resource tree is
844
+ * unreachable" is known with certainty. Goes to the main-process log AND the
845
+ * guest console sink so it surfaces in the devtools Console panel where the
846
+ * developer would otherwise see only the misleading `module app not found`.
847
+ */
848
+ function reportLogicLoadFailure(ap, ctx) {
849
+ const hint = ap.appId === 'unknown'
850
+ ? ' appId could not be resolved (it fell back to "unknown") — the mini-program likely failed to compile or its project manifest/app config is missing.'
851
+ : '';
852
+ const message = `[dimina-kit] Failed to load the mini-program logic bundle from ${logicBundleUrl(ap)}. `
853
+ + 'The service runtime has no registered modules, so no page can mount.'
854
+ + hint
855
+ + ` Verify the project compiled successfully and that the resource server serves "${ap.appId}/${ap.root}/".`;
856
+ console.error(message);
857
+ ctx.guestConsole?.emit({ source: 'service', level: 'error', args: [message] });
858
+ }
798
859
  function maybeSendResourceLoaded(ap, page) {
799
860
  if (page.resourceLoadedSent || !ap.serviceLoaded || !page.renderLoaded)
800
861
  return;
@@ -842,9 +903,21 @@ function routeFromService(state, ap, defaultPage, msg, ctx) {
842
903
  }
843
904
  function routeFromRender(state, ap, page, msg, ctx) {
844
905
  if (msg.type === 'renderHostReady') {
845
- if (page.renderWc && !page.renderWc.isDestroyed()) {
846
- page.renderWc.send(C.TO_RENDER, { msg: makeLoadResource(ap, page, 'render') });
906
+ // Logic injection definitively failed: the render bundle shares the same
907
+ // (unreachable) resource tree, so `loadResource` here would only produce a
908
+ // second cryptic `module <pagePath> not found`. bootServiceHost already
909
+ // surfaced the actionable diagnostic — stay silent.
910
+ if (ap.logicInjected === false)
911
+ return;
912
+ // Injection still in-flight: the render guest fires `renderHostReady` on its
913
+ // own DOMContentLoaded, which routinely beats the async fetch+inject. Hold
914
+ // the render `loadResource` and let `bootServiceHost` flush it once the
915
+ // bundle settles, so a failed bundle never reaches the render side.
916
+ if (ap.logicInjected === null) {
917
+ page.renderLoadPending = true;
918
+ return;
847
919
  }
920
+ sendRenderLoadResource(ap, page);
848
921
  return;
849
922
  }
850
923
  if (msg.type === 'renderResourceLoaded') {
@@ -882,9 +955,20 @@ function handleContainerMsg(ap, page, msg, ctx, state) {
882
955
  case 'invokeAPI':
883
956
  void handleSimulatorApi(state, ap, page, msg.body, ctx);
884
957
  break;
885
- case 'serviceHostError':
958
+ case 'serviceHostError': {
886
959
  console.warn('[bridge-router] service host error:', msg.body);
960
+ // Forward the error to every registered `wx.onError` listener. (The dimina
961
+ // service runtime's App lifecycle is only onLaunch/onShow/onHide, so it
962
+ // does not dispatch `App.onError` — `wx.onError` is the supported path.)
963
+ // The service preload's `deliver` try/catch already reports errors thrown
964
+ // during lifecycle/event dispatch here.
965
+ const errBody = msg.body;
966
+ const errArg = errBody?.message ?? msg.body;
967
+ for (const id of state.appLifecycle.listeners(ap.appSessionId, 'onError')) {
968
+ sendCallback(ap, id, errArg);
969
+ }
887
970
  break;
971
+ }
888
972
  case 'consoleLog':
889
973
  // Native-host console capture: the render-host / service-host guest preloads
890
974
  // monkeypatch console.* and post each entry here (one case handles both —
@@ -922,6 +1006,54 @@ const TAB_ACTION_NAMES = new Set([
922
1006
  'showTabBarRedDot',
923
1007
  'hideTabBarRedDot',
924
1008
  ]);
1009
+ /**
1010
+ * Drive app foreground/background from the main window's visibility. Minimizing
1011
+ * or hiding the window backgrounds the mini-program (App.onHide + wx.onAppHide);
1012
+ * restoring or showing it foregrounds it (App.onShow + wx.onAppShow). Focus/blur
1013
+ * is deliberately NOT used — clicking a DevTools panel would falsely fire it.
1014
+ * The initial App.onShow fires once at spawn (service `invokeSomeLifecycle`), so
1015
+ * the driver only handles subsequent transitions.
1016
+ */
1017
+ function installAppLifecycleDriver(ctx, state) {
1018
+ const win = ctx.windows?.mainWindow;
1019
+ // Guard against a headless/stub mainWindow (unit tests) that isn't a real
1020
+ // event-emitting BrowserWindow.
1021
+ if (!win || typeof win.on !== 'function')
1022
+ return;
1023
+ const emit = (serviceEvent, listenerEvent) => {
1024
+ for (const ap of state.appSessions.values()) {
1025
+ // App.onShow / onHide: the service runtime already listens for these.
1026
+ forwardToService(ap, { type: serviceEvent, target: 'service', body: {} });
1027
+ // wx.onAppShow / onAppHide imperative listeners.
1028
+ for (const id of state.appLifecycle.listeners(ap.appSessionId, listenerEvent)) {
1029
+ sendCallback(ap, id, {});
1030
+ }
1031
+ }
1032
+ };
1033
+ const onHide = () => emit('appHide', 'onAppHide');
1034
+ const onShow = () => emit('appShow', 'onAppShow');
1035
+ win.on('hide', onHide);
1036
+ win.on('minimize', onHide);
1037
+ win.on('show', onShow);
1038
+ win.on('restore', onShow);
1039
+ ctx.registry.add(() => {
1040
+ win.off('hide', onHide);
1041
+ win.off('minimize', onHide);
1042
+ win.off('show', onShow);
1043
+ win.off('restore', onShow);
1044
+ });
1045
+ }
1046
+ // wx app-event API name → the lifecycle event whose listeners it (de)registers.
1047
+ const APP_LIFECYCLE_REGISTER = {
1048
+ onAppShow: 'onAppShow',
1049
+ onAppHide: 'onAppHide',
1050
+ onError: 'onError',
1051
+ };
1052
+ const APP_LIFECYCLE_UNREGISTER = {
1053
+ offAppShow: 'onAppShow',
1054
+ offAppHide: 'onAppHide',
1055
+ offError: 'onError',
1056
+ };
925
1057
  async function handleSimulatorApi(state, ap, page, body, ctx) {
926
1058
  const name = String(body.name ?? '');
927
1059
  const params = normalizeParams(body.params);
@@ -974,6 +1106,36 @@ async function handleSimulatorApi(state, ap, page, body, ctx) {
974
1106
  }
975
1107
  return;
976
1108
  }
1109
+ // App-level lifecycle listeners (wx.onAppShow / onAppHide / onError + off*).
1110
+ // The service encodes the listener as a keep callback id in `params.success`;
1111
+ // we store it and re-fire on main-window foreground/background (the driver in
1112
+ // installBridgeRouter) and on serviceHostError. `off*` clears the event's
1113
+ // listeners for the session.
1114
+ const lifecycleRegister = APP_LIFECYCLE_REGISTER[name];
1115
+ if (lifecycleRegister) {
1116
+ state.appLifecycle.register(ap.appSessionId, lifecycleRegister, params.success);
1117
+ return;
1118
+ }
1119
+ const lifecycleUnregister = APP_LIFECYCLE_UNREGISTER[name];
1120
+ if (lifecycleUnregister) {
1121
+ // off(cb) carries the same evtId as the original on(cb) → removes just that
1122
+ // listener; off() with no callback carries no id → clears the whole event.
1123
+ state.appLifecycle.unregister(ap.appSessionId, lifecycleUnregister, params.success);
1124
+ return;
1125
+ }
1126
+ // pageScrollTo acts on the page's render guest (scroll its document), which
1127
+ // only the main process can reach — run the scroll script in the invoking
1128
+ // page's render webContents rather than forwarding to the simulator.
1129
+ if (name === 'pageScrollTo') {
1130
+ const renderWc = page.renderWc;
1131
+ if (renderWc && !renderWc.isDestroyed()) {
1132
+ void renderWc.executeJavaScript(buildPageScrollScript(params)).catch(() => { });
1133
+ }
1134
+ const successResult = { errMsg: 'pageScrollTo:ok' };
1135
+ sendCallback(ap, params.success, successResult);
1136
+ sendCallback(ap, params.complete, successResult);
1137
+ return;
1138
+ }
977
1139
  // Native-host storage unification: route async wx.setStorage/getStorage/etc.
978
1140
  // to the service-host window's file:// store (the same store the *Sync APIs +
979
1141
  // the Storage panel use), instead of forwarding to the simulator guest's
@@ -1266,6 +1428,7 @@ async function disposeAppSession(state, appSessionId, opts = {}) {
1266
1428
  if (!ap)
1267
1429
  return;
1268
1430
  state.appSessions.delete(appSessionId);
1431
+ state.appLifecycle.dispose(appSessionId);
1269
1432
  // Evict AppData bridges FIRST — eviction enumerates `ap.pages`, which the
1270
1433
  // page teardown below progressively empties (and finally clears).
1271
1434
  state.evictAppDataBridges(ap);
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Build the script that `wx.pageScrollTo` runs inside the page's render guest.
3
+ *
4
+ * The mini-program page scrolls the whole render document (the native iOS impl
5
+ * scrolls the WKWebView's scrollView; Android scrolls the page view), so the
6
+ * simulator scrolls `window`. `duration` is best-effort: a positive duration
7
+ * maps to smooth scrolling (the browser owns the easing — the exact ms is not
8
+ * controllable), and `0` jumps instantly.
9
+ */
10
+ export declare function buildPageScrollScript(params: {
11
+ scrollTop?: unknown;
12
+ duration?: unknown;
13
+ }): string;
14
+ //# sourceMappingURL=page-scroll.d.ts.map
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Build the script that `wx.pageScrollTo` runs inside the page's render guest.
3
+ *
4
+ * The mini-program page scrolls the whole render document (the native iOS impl
5
+ * scrolls the WKWebView's scrollView; Android scrolls the page view), so the
6
+ * simulator scrolls `window`. `duration` is best-effort: a positive duration
7
+ * maps to smooth scrolling (the browser owns the easing — the exact ms is not
8
+ * controllable), and `0` jumps instantly.
9
+ */
10
+ export function buildPageScrollScript(params) {
11
+ const rawTop = Number(params.scrollTop);
12
+ const top = Number.isFinite(rawTop) ? rawTop : 0;
13
+ const duration = params.duration === undefined ? 300 : Number(params.duration);
14
+ const behavior = Number.isFinite(duration) && duration > 0 ? 'smooth' : 'auto';
15
+ return `window.scrollTo({ top: ${top}, left: 0, behavior: ${JSON.stringify(behavior)} })`;
16
+ }
17
+ //# sourceMappingURL=page-scroll.js.map
@@ -21,6 +21,13 @@ export interface ServiceHostWindowOptions {
21
21
  root?: string;
22
22
  resourceBaseUrl: string;
23
23
  hostEnvSnapshot?: Record<string, unknown>;
24
+ /**
25
+ * Custom API namespace names (host config `apiNamespaces`, e.g. `['qd']`).
26
+ * Encoded into the spawn URL so the service-host preload can install them as
27
+ * `globalThis.__diminaApiNamespaces` before service.js evaluates — without
28
+ * which page logic referencing `qd.*` throws `ReferenceError: qd is not defined`.
29
+ */
30
+ apiNamespaces?: string[];
24
31
  }
25
32
  /**
26
33
  * Construct an (un-navigated) service-host BrowserWindow. Split out from
@@ -60,6 +60,11 @@ export function buildServiceHostSpawnUrl(opts) {
60
60
  // Without this, sync-impls/system-info.ts falls back to generic defaults.
61
61
  url.searchParams.set('hostEnv', encodeURIComponent(JSON.stringify(opts.hostEnvSnapshot)));
62
62
  }
63
+ if (opts.apiNamespaces && opts.apiNamespaces.length > 0) {
64
+ // CSV of namespace names; the preload reads this back into
65
+ // `globalThis.__diminaApiNamespaces`. URLSearchParams round-trips the comma.
66
+ url.searchParams.set('apiNamespaces', opts.apiNamespaces.join(','));
67
+ }
63
68
  return url.toString();
64
69
  }
65
70
  /**
@@ -327,6 +327,68 @@ function vibrateLong({ success, complete } = {}) {
327
327
  onComplete?.();
328
328
  }
329
329
  var scanCode = notSupportedApi("scanCode");
330
+ function getClipboardData({ success, fail, complete } = {}) {
331
+ const { onSuccess, onFail, onComplete } = bindCallbacks(this, { success, fail, complete });
332
+ return navigator.clipboard.readText().then(
333
+ (data) => {
334
+ onSuccess?.({ data, errMsg: "getClipboardData:ok" });
335
+ onComplete?.();
336
+ },
337
+ (error) => {
338
+ onFail?.({ errMsg: `getClipboardData:fail ${error?.message ?? error}` });
339
+ onComplete?.();
340
+ }
341
+ );
342
+ }
343
+ function setClipboardData({ data, success, fail, complete } = {}) {
344
+ const { onSuccess, onFail, onComplete } = bindCallbacks(this, { success, fail, complete });
345
+ if (typeof data !== "string") {
346
+ onFail?.({ errMsg: "setClipboardData:fail data is required" });
347
+ onComplete?.();
348
+ return Promise.resolve();
349
+ }
350
+ return navigator.clipboard.writeText(data).then(
351
+ () => {
352
+ onSuccess?.({ errMsg: "setClipboardData:ok" });
353
+ onComplete?.();
354
+ },
355
+ (error) => {
356
+ onFail?.({ errMsg: `setClipboardData:fail ${error?.message ?? error}` });
357
+ onComplete?.();
358
+ }
359
+ );
360
+ }
361
+ function mapEffectiveType(effectiveType) {
362
+ switch (effectiveType) {
363
+ case "4g":
364
+ return "4g";
365
+ case "3g":
366
+ return "3g";
367
+ case "2g":
368
+ case "slow-2g":
369
+ return "2g";
370
+ default:
371
+ return "unknown";
372
+ }
373
+ }
374
+ function resolveNetworkType(info) {
375
+ if (!info.onLine) return "none";
376
+ if (info.type === "wifi" || info.type === "ethernet") return "wifi";
377
+ if (info.type === "cellular") return mapEffectiveType(info.effectiveType);
378
+ if (info.effectiveType) return mapEffectiveType(info.effectiveType);
379
+ return "wifi";
380
+ }
381
+ function getNetworkType({ success, complete } = {}) {
382
+ const { onSuccess, onComplete } = bindCallbacks(this, { success, complete });
383
+ const connection = navigator.connection;
384
+ const networkType = resolveNetworkType({
385
+ onLine: navigator.onLine,
386
+ type: connection?.type,
387
+ effectiveType: connection?.effectiveType
388
+ });
389
+ onSuccess?.({ networkType, errMsg: "getNetworkType:ok" });
390
+ onComplete?.();
391
+ }
330
392
 
331
393
  // src/simulator/event-bridge.ts
332
394
  function bindDomEvents(target, eventMap, fire, buildPayload) {
@@ -1754,6 +1816,134 @@ function uploadFileAbort({ uploadId } = {}) {
1754
1816
  }
1755
1817
  }
1756
1818
 
1819
+ // src/simulator/ui-overlay-bus.ts
1820
+ var UiOverlayBus = class {
1821
+ state = { toast: null, dialog: null };
1822
+ listeners = /* @__PURE__ */ new Set();
1823
+ getState() {
1824
+ return this.state;
1825
+ }
1826
+ subscribe(listener) {
1827
+ this.listeners.add(listener);
1828
+ return () => {
1829
+ this.listeners.delete(listener);
1830
+ };
1831
+ }
1832
+ showToast(toast) {
1833
+ this.set({ ...this.state, toast });
1834
+ }
1835
+ hideToast() {
1836
+ this.set({ ...this.state, toast: null });
1837
+ }
1838
+ /**
1839
+ * Clear `toast` only if it is still the active one. The toast auto-dismiss
1840
+ * timer holds a reference to the toast it scheduled; a newer showToast may
1841
+ * have replaced it in the meantime, and a blind `hideToast()` from the stale
1842
+ * timer would wrongly clear the new toast.
1843
+ */
1844
+ dismissToast(toast) {
1845
+ if (this.state.toast === toast) this.hideToast();
1846
+ }
1847
+ showDialog(dialog) {
1848
+ this.set({ ...this.state, dialog });
1849
+ }
1850
+ hideDialog() {
1851
+ this.set({ ...this.state, dialog: null });
1852
+ }
1853
+ set(next) {
1854
+ this.state = next;
1855
+ for (const listener of this.listeners) listener(next);
1856
+ }
1857
+ };
1858
+ var uiOverlayBus = new UiOverlayBus();
1859
+
1860
+ // src/simulator/simulator-api-ui.ts
1861
+ function showToast(opts = {}) {
1862
+ const { onSuccess, onComplete } = bindCallbacks(this, opts);
1863
+ uiOverlayBus.showToast({
1864
+ title: opts.title ?? "",
1865
+ icon: opts.icon ?? "success",
1866
+ image: opts.image,
1867
+ duration: opts.duration ?? 1500,
1868
+ mask: opts.mask ?? false
1869
+ });
1870
+ onSuccess?.({ errMsg: "showToast:ok" });
1871
+ onComplete?.();
1872
+ }
1873
+ function hideToast(opts = {}) {
1874
+ const { onSuccess, onComplete } = bindCallbacks(this, opts);
1875
+ uiOverlayBus.hideToast();
1876
+ onSuccess?.({ errMsg: "hideToast:ok" });
1877
+ onComplete?.();
1878
+ }
1879
+ function showLoading(opts = {}) {
1880
+ const { onSuccess, onComplete } = bindCallbacks(this, opts);
1881
+ uiOverlayBus.showToast({
1882
+ title: opts.title ?? "",
1883
+ icon: "loading",
1884
+ duration: Infinity,
1885
+ mask: opts.mask ?? false
1886
+ });
1887
+ onSuccess?.({ errMsg: "showLoading:ok" });
1888
+ onComplete?.();
1889
+ }
1890
+ function hideLoading(opts = {}) {
1891
+ const { onSuccess, onComplete } = bindCallbacks(this, opts);
1892
+ uiOverlayBus.hideToast();
1893
+ onSuccess?.({ errMsg: "hideLoading:ok" });
1894
+ onComplete?.();
1895
+ }
1896
+ function showModal(opts = {}) {
1897
+ const { onSuccess, onComplete } = bindCallbacks(this, opts);
1898
+ const editable = opts.editable ?? false;
1899
+ let settled = false;
1900
+ uiOverlayBus.showDialog({
1901
+ kind: "modal",
1902
+ title: opts.title ?? "",
1903
+ content: opts.content ?? "",
1904
+ showCancel: opts.showCancel ?? true,
1905
+ cancelText: opts.cancelText ?? "\u53D6\u6D88",
1906
+ cancelColor: opts.cancelColor ?? "#000000",
1907
+ confirmText: opts.confirmText ?? "\u786E\u5B9A",
1908
+ confirmColor: opts.confirmColor ?? "#576B95",
1909
+ editable,
1910
+ placeholderText: opts.placeholderText ?? "",
1911
+ onResult: (confirmed, content) => {
1912
+ if (settled) return;
1913
+ settled = true;
1914
+ uiOverlayBus.hideDialog();
1915
+ const result = {
1916
+ confirm: confirmed,
1917
+ cancel: !confirmed,
1918
+ errMsg: "showModal:ok"
1919
+ };
1920
+ if (editable) result.content = content ?? "";
1921
+ onSuccess?.(result);
1922
+ onComplete?.();
1923
+ }
1924
+ });
1925
+ }
1926
+ function showActionSheet(opts = {}) {
1927
+ const { onSuccess, onFail, onComplete } = bindCallbacks(this, opts);
1928
+ let settled = false;
1929
+ uiOverlayBus.showDialog({
1930
+ kind: "actionSheet",
1931
+ itemList: opts.itemList ?? [],
1932
+ itemColor: opts.itemColor ?? "#000000",
1933
+ onSelect: (index) => {
1934
+ if (settled) return;
1935
+ settled = true;
1936
+ uiOverlayBus.hideDialog();
1937
+ if (index === -1) {
1938
+ onFail?.({ errMsg: "showActionSheet:fail cancel" });
1939
+ } else {
1940
+ onSuccess?.({ tapIndex: index, errMsg: "showActionSheet:ok" });
1941
+ }
1942
+ onComplete?.();
1943
+ }
1944
+ });
1945
+ }
1946
+
1757
1947
  // src/simulator/simulator-api.ts
1758
1948
  function canIUse({ success, complete }) {
1759
1949
  const { onSuccess, onComplete } = bindCallbacks(this, { success, complete });
@@ -1872,6 +2062,13 @@ var simulatorApis = {
1872
2062
  getSystemInfoSync,
1873
2063
  getWindowInfo,
1874
2064
  getSystemSetting,
2065
+ // UI: interaction
2066
+ showToast,
2067
+ hideToast,
2068
+ showLoading,
2069
+ hideLoading,
2070
+ showModal,
2071
+ showActionSheet,
1875
2072
  // Network
1876
2073
  downloadFile,
1877
2074
  uploadFile,
@@ -1899,6 +2096,9 @@ var simulatorApis = {
1899
2096
  vibrateShort,
1900
2097
  vibrateLong,
1901
2098
  scanCode,
2099
+ getClipboardData,
2100
+ setClipboardData,
2101
+ getNetworkType,
1902
2102
  // Media: Image
1903
2103
  chooseImage,
1904
2104
  previewImage,