@dimina-kit/devtools 0.4.0-dev.20260701155140 → 0.4.0-dev.20260702164903

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 (63) hide show
  1. package/dist/main/app/max-listeners-diagnostic.js +5 -1
  2. package/dist/main/index.bundle.js +1533 -1237
  3. package/dist/main/ipc/bridge-router.d.ts +28 -0
  4. package/dist/main/ipc/bridge-router.js +175 -50
  5. package/dist/main/ipc/session-listener-bag.d.ts +41 -0
  6. package/dist/main/ipc/session-listener-bag.js +40 -0
  7. package/dist/main/ipc/simulator.js +5 -1
  8. package/dist/main/services/elements-forward/index.js +19 -0
  9. package/dist/main/services/network-forward/index.js +13 -0
  10. package/dist/main/services/views/destroy-child-view.d.ts +9 -0
  11. package/dist/main/services/views/destroy-child-view.js +23 -0
  12. package/dist/main/services/views/host-toolbar-view.d.ts +21 -0
  13. package/dist/main/services/views/host-toolbar-view.js +249 -0
  14. package/dist/main/services/views/inject-when-ready.d.ts +41 -0
  15. package/dist/main/services/views/inject-when-ready.js +62 -0
  16. package/dist/main/services/views/native-simulator-devtools-host.d.ts +26 -0
  17. package/dist/main/services/views/native-simulator-devtools-host.js +484 -0
  18. package/dist/main/services/views/native-simulator-view.d.ts +28 -0
  19. package/dist/main/services/views/native-simulator-view.js +428 -0
  20. package/dist/main/services/views/overlay-panels-view.d.ts +29 -0
  21. package/dist/main/services/views/overlay-panels-view.js +150 -0
  22. package/dist/main/services/views/placement-reconciler.d.ts +73 -0
  23. package/dist/main/services/views/placement-reconciler.js +120 -0
  24. package/dist/main/services/views/resolve-project-editor-target.d.ts +13 -0
  25. package/dist/main/services/views/resolve-project-editor-target.js +33 -0
  26. package/dist/main/services/views/view-manager.d.ts +20 -16
  27. package/dist/main/services/views/view-manager.js +66 -1545
  28. package/dist/main/services/views/workbench-view.d.ts +15 -0
  29. package/dist/main/services/views/workbench-view.js +170 -0
  30. package/dist/main/services/workspace/workspace-service.js +1 -1
  31. package/dist/preload/windows/host-toolbar-runtime.cjs.map +2 -2
  32. package/dist/preload/windows/main.cjs +10 -1
  33. package/dist/preload/windows/main.cjs.map +2 -2
  34. package/dist/preload/windows/simulator.cjs +6 -0
  35. package/dist/preload/windows/simulator.cjs.map +2 -2
  36. package/dist/preload/windows/simulator.js +6 -0
  37. package/dist/renderer/assets/index-B-Dqb7G0.js +49 -0
  38. package/dist/renderer/assets/{input-Djg2NR4U.js → input-Cjk0wSEh.js} +2 -2
  39. package/dist/renderer/assets/ipc-transport-D5dKIti1.css +1 -0
  40. package/dist/renderer/assets/{ipc-transport-Be-AzCub.js → ipc-transport-OZS-KmOW.js} +2 -2
  41. package/dist/renderer/assets/{popover-eDUh4NYe.js → popover-CQVvOe90.js} +2 -2
  42. package/dist/renderer/assets/{select-DesFfkVV.js → select-BQDXi5ih.js} +2 -2
  43. package/dist/renderer/assets/{settings-api-HQ7ZFPWi.js → settings-api-B6x2mhaD.js} +2 -2
  44. package/dist/renderer/assets/{settings-CRmO7zUG.js → settings-tP3Px2KR.js} +2 -2
  45. package/dist/renderer/assets/{workbenchSettings-CZBWsliT.js → workbenchSettings-Bim9ol9R.js} +2 -2
  46. package/dist/renderer/entries/main/index.html +6 -6
  47. package/dist/renderer/entries/popover/index.html +5 -5
  48. package/dist/renderer/entries/settings/index.html +5 -5
  49. package/dist/renderer/entries/workbench-settings/index.html +4 -4
  50. package/dist/shared/bridge-channels.d.ts +17 -0
  51. package/dist/shared/bridge-channels.js +9 -0
  52. package/dist/shared/ipc-channels.d.ts +1 -0
  53. package/dist/shared/ipc-channels.js +6 -0
  54. package/dist/shared/ipc-schemas.d.ts +6 -0
  55. package/dist/shared/ipc-schemas.js +10 -0
  56. package/dist/simulator/assets/{bridge-channels-BUQ5AbvJ.js → bridge-channels-zhsumfky.js} +2 -2
  57. package/dist/simulator/assets/{device-shell-CT3LRnTB.js → device-shell-CUl0ILfn.js} +2 -2
  58. package/dist/simulator/assets/{simulator-BizhGGE3.js → simulator-Dz8iGcgy.js} +5 -5
  59. package/dist/simulator/assets/{simulator-mini-app-BfNuDF3R.js → simulator-mini-app-Bt639XL_.js} +2 -2
  60. package/dist/simulator/simulator.html +2 -1
  61. package/package.json +5 -5
  62. package/dist/renderer/assets/index-jDrNHghH.js +0 -49
  63. package/dist/renderer/assets/ipc-transport-C6-KPMqw.css +0 -1
@@ -0,0 +1,428 @@
1
+ import { ipcMain, nativeTheme, shell, WebContentsView, webContents } from 'electron';
2
+ import { cjsSiblingPreloadPath } from '../../utils/paths.js';
3
+ import { simDeskBg } from '../../utils/theme.js';
4
+ import { handleWindowOpenExternal } from '../../windows/navigation-hardening.js';
5
+ import { SimulatorCustomApiBridgeChannel } from '../../../shared/ipc-channels.js';
6
+ import * as layout from '../layout/index.js';
7
+ import { handleCustomApiBridgeRequest, } from '../simulator/custom-apis.js';
8
+ import { configureMiniappSession, miniappPartition } from './miniapp-partition.js';
9
+ import { parseRoute } from '../../../shared/simulator-route.js';
10
+ import { SIMULATOR_EVENTS } from '../../../shared/bridge-channels.js';
11
+ import { VIEW_ID } from '../../../shared/view-ids.js';
12
+ export function createNativeSimulatorView(ctx, reconciler, deps) {
13
+ const { safeArea, devtoolsHost, overlayPanels } = deps;
14
+ let nativeSimulatorView = null;
15
+ let nativeSimulatorViewAdded = false;
16
+ let nativeSimulatorProjectPath = null;
17
+ let settleNativeSimulatorReady = null;
18
+ // Whether the CURRENT simulator view's shell finished its first boot (first
19
+ // render guest did-finish-load) — the precondition for a soft reload: only a
20
+ // fully-booted shell has the RELAUNCH listener installed, so sending earlier
21
+ // would silently drop the event and strand the renderer (it was told
22
+ // "accepted" but nothing reloads). Reset on teardown/re-attach — readiness
23
+ // never carries over to a rebuilt view.
24
+ let nativeSimulatorShellReady = false;
25
+ // The `ipcMain.on` listener that services the `__diminaCustomApis` bridge for
26
+ // the current native simulator webContents (see `attachNativeCustomApiBridge`).
27
+ // Tracked so we can remove it before tearing down / re-attaching the view
28
+ // (otherwise listeners leak across relaunch cycles).
29
+ let nativeCustomApiBridgeHandler = null;
30
+ // The current device zoom as a factor (zoomPercent/100), last reported by the
31
+ // renderer via setNativeSimulatorViewBounds. Stored so nested render-host
32
+ // `<webview>` guests attached AFTER a zoom change still pick up the correct
33
+ // scale in `did-attach-webview`. Defaults to 1 (100%).
34
+ let currentZoomFactor = 1;
35
+ let simulatorWebContentsId = null;
36
+ // simulator zoom rides in `extra`: setBounds also drives the WCV zoomFactor and
37
+ // propagates it to nested render-host guests.
38
+ function applyNativeSimulatorBounds(view, bounds, zoom) {
39
+ const p = layout.computeNativeSimulatorViewParams(bounds, zoom);
40
+ currentZoomFactor = p.zoomFactor;
41
+ view.setBounds(p.bounds);
42
+ const simWc = view.webContents;
43
+ if (!simWc.isDestroyed())
44
+ simWc.setZoomFactor(p.zoomFactor);
45
+ try {
46
+ for (const wc of webContents.getAllWebContents()) {
47
+ if (wc.isDestroyed())
48
+ continue;
49
+ if (wc.hostWebContents === simWc)
50
+ wc.setZoomFactor(p.zoomFactor);
51
+ }
52
+ }
53
+ catch { /* hostWebContents unavailable; guests get zoom on attach */ }
54
+ }
55
+ /**
56
+ * The single teardown path for the live native-host simulator WCV, shared by
57
+ * relaunch (attachNativeSimulator replacing the view) and project close
58
+ * (detachSimulator). Detaches the view from the window, then synchronously
59
+ * clears its bridge sessions (render guests + service host + mappings) BEFORE
60
+ * the WCV's own async close(), so the next project never re-resolves,
61
+ * re-renders, or screenshots the outgoing guest. The sync prefix clears the
62
+ * maps immediately; the async tail (pool / resource-server release) is observed
63
+ * so a rejection is logged rather than swallowed. The simulatorWc 'destroyed'
64
+ * hook in bridge-router stays as an idempotent fallback. No-op when no view is
65
+ * live; idempotent (disposeSessionsForSimulator early-returns once a session is
66
+ * gone), so the eager teardown here and the 'destroyed' fallback never
67
+ * double-dispose. `label` only tags the diagnostic on the async-tail failure.
68
+ */
69
+ function tearDownNativeSimulatorView(label) {
70
+ if (!nativeSimulatorView)
71
+ return;
72
+ if (nativeSimulatorViewAdded && !ctx.windows.mainWindow.isDestroyed()) {
73
+ try {
74
+ ctx.windows.mainWindow.contentView.removeChildView(nativeSimulatorView);
75
+ }
76
+ catch { /* already removed */ }
77
+ }
78
+ try {
79
+ if (!nativeSimulatorView.webContents.isDestroyed()) {
80
+ ctx.bridge?.disposeSessionsForSimulator?.(nativeSimulatorView.webContents.id)
81
+ ?.catch((err) => console.warn(`[view-manager] dispose sessions (${label}) failed:`, err));
82
+ nativeSimulatorView.webContents.close();
83
+ }
84
+ }
85
+ catch { /* ignore */ }
86
+ nativeSimulatorView = null;
87
+ nativeSimulatorViewAdded = false;
88
+ nativeSimulatorShellReady = false;
89
+ // The instance is being replaced/destroyed — forget its reconciled mount
90
+ // state so the NEXT rebuilt view is treated as a fresh attach. Without this,
91
+ // the level-triggered reconciler still records `simulator` as `attached`
92
+ // (the manual removeChildView above bypasses it), so the next reconcile
93
+ // classifies the freshly-built WebContentsView as already-attached, never
94
+ // emits the `attach` op, and the new view is never addChildView'd — a sticky
95
+ // 100%-invisible simulator after every recompile. Mirrors the hostToolbar
96
+ // instance-replacement handling in ensureHostToolbarView.
97
+ reconciler.forgetActual(VIEW_ID.simulator);
98
+ // The view is gone: gateReadiness now hides it, so reconcile detaches it.
99
+ reconciler.reconcileNow();
100
+ }
101
+ /**
102
+ * Remove the custom-apis bridge `ipcMain.on` listener, if any. Idempotent.
103
+ */
104
+ function detachNativeCustomApiBridge() {
105
+ if (nativeCustomApiBridgeHandler) {
106
+ ipcMain.removeListener(SimulatorCustomApiBridgeChannel.Request, nativeCustomApiBridgeHandler);
107
+ nativeCustomApiBridgeHandler = null;
108
+ }
109
+ }
110
+ /**
111
+ * NATIVE-HOST ONLY. The native simulator is a top-level WebContentsView with
112
+ * NO embedder renderer, so the simulator-side `__diminaCustomApis` bridge
113
+ * (`src/preload/runtime/custom-apis.ts`) cannot reach the host via
114
+ * `ipcRenderer.sendToHost` — that only delivers to a `<webview>`'s embedder,
115
+ * which is how the default path's `useCustomApiProxy` answered it. A
116
+ * top-level WebContentsView has no embedder, and `sendToHost` does NOT loop
117
+ * back as `ipc-message-host` on itself, so that channel never fires here.
118
+ *
119
+ * Under native-host the bridge instead talks to `ipcMain` directly (the same
120
+ * way `installNativeHostBridge` issues SPAWN/PAGE_OPEN): the preload sends
121
+ * `SimulatorCustomApiBridgeChannel.Request` via `ipcRenderer.send`, this
122
+ * `ipcMain.on` listener answers it. We do NOT route through `IpcRegistry` /
123
+ * the sender-policy white-list (the simulator is deliberately kept off it);
124
+ * instead we accept the message ONLY when `event.sender` is THIS precise
125
+ * simWc — the same trust model bridge-router uses for the simulator's own
126
+ * SPAWN messages. The result is dispatched through the shared
127
+ * `ctx.simulatorApis` registry and the id-correlated `Response` is posted
128
+ * back via `simWc.send`, which the preload's `ipcRenderer.on(Response)`
129
+ * listener settles.
130
+ */
131
+ function attachNativeCustomApiBridge(simWc) {
132
+ // Re-attach defensively: a stale listener from a prior simWc must never
133
+ // linger. (attachNativeSimulator already tears the old view down first.)
134
+ detachNativeCustomApiBridge();
135
+ const apis = ctx.simulatorApis;
136
+ if (!apis)
137
+ return;
138
+ const simWcId = simWc.id;
139
+ const handler = (event, req) => {
140
+ // Trust gate: only the exact native simulator webContents may drive this.
141
+ if (event.sender.id !== simWcId)
142
+ return;
143
+ const r = req;
144
+ if (!r || typeof r.id !== 'number')
145
+ return;
146
+ void handleCustomApiBridgeRequest(apis, r).then((response) => {
147
+ if (simWc.isDestroyed())
148
+ return;
149
+ simWc.send(SimulatorCustomApiBridgeChannel.Response, response);
150
+ }).catch(() => { });
151
+ };
152
+ nativeCustomApiBridgeHandler = handler;
153
+ ipcMain.on(SimulatorCustomApiBridgeChannel.Request, handler);
154
+ // Consolidate this per-webContents teardown onto the connection layer:
155
+ // acquiring the simWc connection makes the
156
+ // registry track this trusted webContents, and `own()`-ing the detach ties
157
+ // the ipcMain listener's lifetime to the connection so it's torn down
158
+ // deterministically when the wc is destroyed (or the connection is reset),
159
+ // instead of a bespoke `once('destroyed')`. The detach stays idempotent, so
160
+ // the defensive re-attach path calling it directly remains safe.
161
+ ctx.connections.acquire(simWc).own(detachNativeCustomApiBridge);
162
+ }
163
+ function attachNativeSimulator(simulatorUrl, _simWidth) {
164
+ if (!ctx.preloadPath) {
165
+ console.error('[workbench] attachNativeSimulator — preloadPath unset; cannot mount native simulator');
166
+ return Promise.resolve();
167
+ }
168
+ // Unblock a superseded IPC invocation. Its renderer effect cleanup marks
169
+ // that generation cancelled, so this cannot schedule a stale capture.
170
+ settleNativeSimulatorReady?.();
171
+ settleNativeSimulatorReady = null;
172
+ // Tear down any previous native simulator view (relaunch / re-open) through
173
+ // the shared single teardown path. detachNativeCustomApiBridge stays here
174
+ // (the bridge dispatcher is re-installed per attach below).
175
+ if (nativeSimulatorView) {
176
+ detachNativeCustomApiBridge();
177
+ tearDownNativeSimulatorView('relaunch');
178
+ }
179
+ nativeSimulatorProjectPath = null;
180
+ const ready = new Promise((resolve) => {
181
+ settleNativeSimulatorReady = resolve;
182
+ });
183
+ // Derive THIS project's session partition from the simulator URL's appId so
184
+ // its cookies/localStorage/cache are isolated from every other project.
185
+ // Same project → same partition (storage survives a relaunch);
186
+ // unknown appId → the shared fallback. Configure the partition's session
187
+ // (protocol handlers + CORS/referer policy) before any project content loads
188
+ // on it — idempotent per partition.
189
+ const route = parseRoute(simulatorUrl);
190
+ // Include the project path so two projects that declare the same appId at
191
+ // different paths get isolated partitions. The service-host window for THIS
192
+ // project derives its partition from the same (appId, projectPath) pair so
193
+ // render guests + service host still share one session.
194
+ const partition = miniappPartition(route?.appId, ctx.workspace?.getProjectPath());
195
+ configureMiniappSession(partition);
196
+ // The simulator preload is a CJS bundle; webPreferences.preload obeys the
197
+ // `.js` + "type":"module" ESM rule (require would be undefined), so hand the
198
+ // top-level WebContentsView the `.cjs` sibling. contextIsolation:false +
199
+ // sandbox:false + webviewTag:true mirror what the default `<webview>` guest
200
+ // runs with, and the per-project `persist:miniapp-<key>` partition shares
201
+ // storage + the session-registered preload/CORS rules with the rest of THIS
202
+ // project's simulator (render guests + service host), never other projects'.
203
+ const view = new WebContentsView({
204
+ webPreferences: {
205
+ nodeIntegration: false,
206
+ contextIsolation: false,
207
+ sandbox: false,
208
+ webviewTag: true,
209
+ preload: cjsSiblingPreloadPath(ctx.preloadPath),
210
+ partition,
211
+ },
212
+ });
213
+ nativeSimulatorView = view;
214
+ nativeSimulatorProjectPath = ctx.workspace?.getProjectPath() || null;
215
+ // Paint the WCV surface the themed desk color (simDeskBg(): dark #121212 /
216
+ // light #e8e8e8) so a height-resize that grows the region never flashes a
217
+ // mismatched strip before DeviceShell's desk repaints — the WCV, the desk,
218
+ // and the renderer placeholder behind it are all the same color.
219
+ view.setBackgroundColor(simDeskBg());
220
+ const simWc = view.webContents;
221
+ // Keep the WCV surface in sync with the active color scheme. The
222
+ // process-wide installThemeBackgroundSync() re-syncs BrowserWindows on a
223
+ // theme switch, but this top-level WebContentsView is not a window, so its
224
+ // creation-time backgroundColor would otherwise freeze. Mirror simDeskBg()
225
+ // here on every nativeTheme `updated`; the listener is owned by the wc's
226
+ // connection so it detaches when the simulator view is torn down.
227
+ const syncDeskBg = () => {
228
+ try {
229
+ if (!simWc.isDestroyed())
230
+ view.setBackgroundColor(simDeskBg());
231
+ }
232
+ catch { /* view/wc gone */ }
233
+ };
234
+ nativeTheme.on('updated', syncDeskBg);
235
+ ctx.connections.acquire(simWc).own(() => nativeTheme.removeListener('updated', syncDeskBg));
236
+ // Service the simulator-side `__diminaCustomApis` bridge: this top-level
237
+ // WebContentsView has no embedder renderer to proxy through, so dispatch its
238
+ // `sendToHost` requests straight to `ctx.simulatorApis` from main.
239
+ attachNativeCustomApiBridge(simWc);
240
+ // DeviceShell mounts per-page render-host `<webview>`s INSIDE this view.
241
+ // Pin them onto the SAME per-project partition as their host WCV (so render
242
+ // and the rest of this project share one localStorage/cookie jar) and run
243
+ // them with contextIsolation/sandbox off so the render runtime + its preload
244
+ // share the page realm. (A top-level WebContentsView can host these guests; a
245
+ // `<webview>` guest cannot — that's the whole point of Option A.)
246
+ // Page type (`isTab`) of each attaching guest, captured from its render-host
247
+ // URL in will-attach (where `params.src` carries the full URL) and consumed
248
+ // FIFO in the matching did-attach — `guestWc.getURL()` is still empty there.
249
+ // Per-attach scope: a fresh simWc + handlers are built on every (re)attach.
250
+ const pendingGuestIsTab = [];
251
+ simWc.on('will-attach-webview', (_event, webPreferences, params) => {
252
+ ;
253
+ webPreferences.partition = partition;
254
+ params.partition = partition;
255
+ webPreferences.contextIsolation = false;
256
+ webPreferences.sandbox = false;
257
+ let isTab = false;
258
+ try {
259
+ isTab = new URL(params.src).searchParams.get('isTab') === '1';
260
+ }
261
+ catch { /* keep false */ }
262
+ pendingGuestIsTab.push(isTab);
263
+ });
264
+ simWc.on('did-attach-webview', (_event, guestWc) => {
265
+ // Scale the nested render-host page with the device zoom. The host WCV is
266
+ // sized to the SCALED bezel rect and runs at `currentZoomFactor`, so the
267
+ // guest must run at the same factor to lay the page out at the logical
268
+ // device width and paint at the right scale. At 100% (default) this is a
269
+ // no-op identity (factor 1). Newly-attached guests pick up the latest
270
+ // zoom here; live zoom changes re-apply via setNativeSimulatorViewBounds.
271
+ try {
272
+ guestWc.setZoomFactor(currentZoomFactor);
273
+ }
274
+ catch { /* guest not ready; setNativeSimulatorViewBounds re-applies */ }
275
+ // Simulate this device's CSS env(safe-area-inset-*) on the fresh guest
276
+ // before it paints, so notch-aware page layout resolves correctly. The
277
+ // bottom inset is page-type-dependent (see services/safe-area): a tab
278
+ // page's content sits above the shell tabBar (bottom 0); a non-tab page
279
+ // is full-bleed (real bottom inset). The page type was captured from the
280
+ // render-host URL in will-attach (FIFO).
281
+ const isTabGuest = pendingGuestIsTab.shift() ?? false;
282
+ safeArea.applyToGuest(guestWc, ctx.bridge?.getDevice() ?? null, isTabGuest);
283
+ guestWc.setWindowOpenHandler(({ url }) => handleWindowOpenExternal(url));
284
+ guestWc.on('will-navigate', (e, url) => {
285
+ try {
286
+ const u = new URL(url);
287
+ if (u.protocol === 'about:')
288
+ return;
289
+ if ((u.protocol === 'http:' || u.protocol === 'https:')
290
+ && (u.hostname === 'localhost' || u.hostname === '127.0.0.1')) {
291
+ return;
292
+ }
293
+ if (u.protocol === 'file:')
294
+ return;
295
+ e.preventDefault();
296
+ if (u.protocol === 'http:' || u.protocol === 'https:') {
297
+ void shell.openExternal(url);
298
+ }
299
+ }
300
+ catch {
301
+ e.preventDefault();
302
+ }
303
+ });
304
+ // The outer DeviceShell loading is insufficient: slow projects attach a
305
+ // render guest later. Resolve the renderer's attach IPC only after that
306
+ // first mini-app page has completed its own document load.
307
+ guestWc.once('did-finish-load', () => {
308
+ if (nativeSimulatorView !== view || simWc.isDestroyed())
309
+ return;
310
+ // A loaded render guest means the shell booted end-to-end — from here
311
+ // on a soft reload (RELAUNCH into this live shell) is deliverable.
312
+ nativeSimulatorShellReady = true;
313
+ settleNativeSimulatorReady?.();
314
+ settleNativeSimulatorReady = null;
315
+ });
316
+ // A render-host guest only attaches after a spawn, so the service window
317
+ // exists by now — (re)point the right-panel DevTools at it. Belt-and-braces
318
+ // with the `onRenderEvent` path in case its emit lost the attach race.
319
+ devtoolsHost.followServiceHost();
320
+ });
321
+ // The simulator loads http://localhost:<port>/simulator.html. Harden popups
322
+ // + in-place navigation: allow the dev server origin (and about:blank /
323
+ // file:// render hosts), route everything else to the OS browser.
324
+ simWc.setWindowOpenHandler(({ url }) => handleWindowOpenExternal(url));
325
+ simWc.on('will-navigate', (e, url) => {
326
+ try {
327
+ const u = new URL(url);
328
+ if (u.protocol === 'about:')
329
+ return;
330
+ if (u.protocol === 'file:')
331
+ return;
332
+ if ((u.protocol === 'http:' || u.protocol === 'https:')
333
+ && (u.hostname === 'localhost' || u.hostname === '127.0.0.1')) {
334
+ return;
335
+ }
336
+ e.preventDefault();
337
+ if (u.protocol === 'http:' || u.protocol === 'https:') {
338
+ void shell.openExternal(url);
339
+ }
340
+ }
341
+ catch {
342
+ e.preventDefault();
343
+ }
344
+ });
345
+ void simWc.loadURL(simulatorUrl).catch((err) => {
346
+ console.error('[workbench] attachNativeSimulator — loadURL failed', err);
347
+ });
348
+ // This WebContentsView's webContents IS the simulator: the native-host
349
+ // preload runs here and issues SPAWN, so bridge-router resolves
350
+ // ap.simulatorWc = event.sender = this wc, and SIMULATOR_EVENTS flow back.
351
+ simulatorWebContentsId = simWc.id;
352
+ // Forward this WCV's network requests (wx.request/download/upload run here,
353
+ // not in the service host) into the service-host console so they show in the
354
+ // embedded DevTools the right panel hosts. Attaches the CDP debugger; no-op
355
+ // if the forwarder is unwired (partial test ctx) or the debugger is claimed.
356
+ ctx.networkForward?.attachSimulator(simWc);
357
+ // Model A: bounds come ONLY from the renderer's window-level placement
358
+ // snapshot. The view now exists, so re-reconcile: gateReadiness admits the
359
+ // simulator (gated hidden while its WCV was absent) and the latest desired
360
+ // placement attaches + sizes it. Until the next snapshot it stays unadded.
361
+ reconciler.reconcileNow();
362
+ // Native-host page code (console.log / wx.request / Sources) runs in the
363
+ // hidden SERVICE HOST window, not this DeviceShell host nor the render-host
364
+ // guests. Keep the right-panel DevTools host view stable and point it at the
365
+ // service host so its Console/Network(fetch)/Sources reflect the logic layer.
366
+ // (The UI/view layer's Elements equivalent is the native WXML panel +
367
+ // render-guest highlight chain, which targets the active render guest.)
368
+ devtoolsHost.attach();
369
+ return ready;
370
+ }
371
+ function softReloadNativeSimulator(simulatorUrl) {
372
+ if (!nativeSimulatorView || nativeSimulatorView.webContents.isDestroyed())
373
+ return false;
374
+ if (!nativeSimulatorShellReady)
375
+ return false;
376
+ const payload = { url: simulatorUrl };
377
+ nativeSimulatorView.webContents.send(SIMULATOR_EVENTS.RELAUNCH, payload);
378
+ return true;
379
+ }
380
+ function detachSimulator() {
381
+ settleNativeSimulatorReady?.();
382
+ settleNativeSimulatorReady = null;
383
+ devtoolsHost.stopFollowing();
384
+ detachNativeCustomApiBridge();
385
+ // Stop Elements forwarding (detaches only the debugger sessions IT attached;
386
+ // safe-area's sessions are untouched). Idempotent — the host 'destroyed'
387
+ // handler may have already run.
388
+ devtoolsHost.stopElementsForwarding();
389
+ // Stop forwarding the simulator WCV's network (its debugger is detached as
390
+ // the view is closed below, but drop our session deterministically first).
391
+ // Also drop the DevTools front-end host — its wc is destroyed below; a stale
392
+ // ref would make the forwarder dispatch into a dead wc instead of falling
393
+ // back to the console.
394
+ ctx.networkForward?.detachSimulator();
395
+ ctx.networkForward?.setDevtoolsHost(null);
396
+ // Native-host simulator content view (no-op in the default path) — same
397
+ // single teardown path as relaunch. detachNativeCustomApiBridge was already
398
+ // called above.
399
+ tearDownNativeSimulatorView('close');
400
+ overlayPanels.hidePopover();
401
+ // Drop the settings view too — the previous detachAllViews() did.
402
+ overlayPanels.destroySettings();
403
+ // Destroy the right-panel DevTools front-end host — its wc is closed here.
404
+ devtoolsHost.destroyHostView();
405
+ simulatorWebContentsId = null;
406
+ nativeSimulatorProjectPath = null;
407
+ }
408
+ reconciler.registerView(VIEW_ID.simulator, {
409
+ getView: () => nativeSimulatorView,
410
+ setAdded: (added) => { nativeSimulatorViewAdded = added; },
411
+ gateHidden: () => !nativeSimulatorView,
412
+ applyBounds: (view, bounds, extra) => applyNativeSimulatorBounds(view, bounds, extra?.zoom ?? 1),
413
+ });
414
+ return {
415
+ attachNativeSimulator,
416
+ softReloadNativeSimulator,
417
+ detachSimulator,
418
+ getSimulatorWebContentsId: () => simulatorWebContentsId,
419
+ getSimulatorWebContents: () => {
420
+ if (simulatorWebContentsId == null)
421
+ return null;
422
+ const wc = webContents.fromId(simulatorWebContentsId);
423
+ return wc && !wc.isDestroyed() ? wc : null;
424
+ },
425
+ getSimulatorProjectPath: () => nativeSimulatorProjectPath,
426
+ };
427
+ }
428
+ //# sourceMappingURL=native-simulator-view.js.map
@@ -0,0 +1,29 @@
1
+ import type { WebContents } from 'electron';
2
+ import type { PlacementReconciler } from './placement-reconciler.js';
3
+ import type { ViewManagerContext } from './view-manager.js';
4
+ /**
5
+ * The two main-owned overlay panels: the settings sheet (right-side panel over
6
+ * a transparent backdrop) and the transient popover. Their bounds are
7
+ * main-computed and published into the reconciler's `overlayDesired` (top-tier
8
+ * layers, so a reorder keeps them above every base overlay).
9
+ */
10
+ export interface OverlayPanelsView {
11
+ showSettings(): Promise<void>;
12
+ hideSettings(): void;
13
+ showPopover(data: unknown): void;
14
+ hidePopover(): void;
15
+ /** Re-apply whichever of settings/popover is currently present (window resize / toolbar height change). */
16
+ reapplyPresentOverlays(): void;
17
+ /** Re-apply the settings overlay only (the resize entry point re-applies settings, not popover). */
18
+ applySettingsBoundsIfPresent(): void;
19
+ /** Destroy the cached settings view (aggregate simulator detach). */
20
+ destroySettings(): void;
21
+ getSettingsWebContents(): WebContents | null;
22
+ getSettingsWebContentsId(): number | null;
23
+ getPopoverWebContentsId(): number | null;
24
+ }
25
+ export declare function createOverlayPanelsView(ctx: ViewManagerContext, reconciler: PlacementReconciler, deps: {
26
+ /** The host-toolbar strip height that offsets the overlay top edge. */
27
+ getHostToolbarHeight(): number;
28
+ }): OverlayPanelsView;
29
+ //# sourceMappingURL=overlay-panels-view.d.ts.map
@@ -0,0 +1,150 @@
1
+ import { WebContentsView } from 'electron';
2
+ import path from 'path';
3
+ import { mainPreloadPath } from '../../utils/paths.js';
4
+ import { applyNavigationHardening } from '../../windows/navigation-hardening.js';
5
+ import * as layout from '../layout/index.js';
6
+ import { HEADER_H } from '../../../shared/constants.js';
7
+ import { VIEW_ID, VIEW_LAYER } from '../../../shared/view-ids.js';
8
+ import { destroyChildView } from './destroy-child-view.js';
9
+ export function createOverlayPanelsView(ctx, reconciler, deps) {
10
+ let settingsView = null;
11
+ let popoverView = null;
12
+ function overlayHeaderHeight() {
13
+ return HEADER_H + deps.getHostToolbarHeight();
14
+ }
15
+ // settings/popover bounds are main-computed; publish them into overlayDesired
16
+ // and let the reconciler place them (they are top-tier layers, so a reorder
17
+ // keeps them above every base overlay — the old raiseTopOverlays is gone).
18
+ function applySettingsBounds() {
19
+ if (!settingsView || ctx.windows.mainWindow.isDestroyed())
20
+ return;
21
+ const [w = 0, h = 0] = ctx.windows.mainWindow.getContentSize();
22
+ reconciler.setOverlayDesired(VIEW_ID.settings, {
23
+ viewId: VIEW_ID.settings,
24
+ placement: {
25
+ visible: true,
26
+ bounds: layout.computeSettingsBounds(w, h, overlayHeaderHeight()),
27
+ },
28
+ layer: VIEW_LAYER.settings,
29
+ });
30
+ reconciler.reconcileNow();
31
+ }
32
+ function applyPopoverBounds() {
33
+ if (!popoverView || ctx.windows.mainWindow.isDestroyed())
34
+ return;
35
+ const [w = 0, h = 0] = ctx.windows.mainWindow.getContentSize();
36
+ reconciler.setOverlayDesired(VIEW_ID.popover, {
37
+ viewId: VIEW_ID.popover,
38
+ placement: {
39
+ visible: true,
40
+ bounds: layout.computePopoverBounds(w, h, overlayHeaderHeight()),
41
+ },
42
+ layer: VIEW_LAYER.popover,
43
+ });
44
+ reconciler.reconcileNow();
45
+ }
46
+ async function showSettings() {
47
+ if (!settingsView) {
48
+ settingsView = new WebContentsView({
49
+ webPreferences: {
50
+ nodeIntegration: false,
51
+ contextIsolation: true,
52
+ sandbox: false,
53
+ preload: mainPreloadPath,
54
+ },
55
+ });
56
+ // Overlay loads mainPreloadPath, so the same navigation rules as the
57
+ // main window apply — see navigation-hardening.ts.
58
+ applyNavigationHardening(settingsView.webContents, ctx.rendererDir);
59
+ // Transparent backing: the settings view now spans the whole content area
60
+ // (computeSettingsBounds) and its renderer paints a transparent backdrop +
61
+ // an opaque right-side panel, so the underlying editor/simulator show
62
+ // through and a backdrop click closes the overlay (mirrors the popover).
63
+ settingsView.setBackgroundColor('#00000000');
64
+ await settingsView.webContents.loadFile(path.join(ctx.rendererDir, 'entries/settings/index.html'));
65
+ }
66
+ applySettingsBounds();
67
+ }
68
+ function hideSettings() {
69
+ reconciler.deleteOverlayDesired(VIEW_ID.settings);
70
+ reconciler.reconcileNow();
71
+ }
72
+ function showPopover(data) {
73
+ hidePopover();
74
+ const popover = new WebContentsView({
75
+ webPreferences: {
76
+ nodeIntegration: false,
77
+ contextIsolation: true,
78
+ sandbox: false,
79
+ preload: mainPreloadPath,
80
+ },
81
+ });
82
+ // Popover overlay loads mainPreloadPath — same navigation rules apply.
83
+ applyNavigationHardening(popover.webContents, ctx.rendererDir);
84
+ popoverView = popover;
85
+ popover.setBackgroundColor('#00000000');
86
+ applyPopoverBounds();
87
+ popover.webContents.once('did-finish-load', () => {
88
+ ctx.notify.popoverInit(popover, data);
89
+ });
90
+ popover.webContents.loadFile(path.join(ctx.rendererDir, 'entries/popover/index.html'));
91
+ }
92
+ function hidePopover() {
93
+ if (!popoverView)
94
+ return;
95
+ reconciler.deleteOverlayDesired(VIEW_ID.popover);
96
+ // Destroy the WCV first (removeChildView + close), THEN reconcile: the detach
97
+ // op then finds the view already gone and does not double-removeChildView.
98
+ destroyChildView(ctx.windows.mainWindow, popoverView);
99
+ popoverView = null;
100
+ reconciler.reconcileNow();
101
+ ctx.notify.popoverClosed();
102
+ }
103
+ function reapplyPresentOverlays() {
104
+ if (reconciler.hasOverlayDesired(VIEW_ID.settings))
105
+ applySettingsBounds();
106
+ if (reconciler.hasOverlayDesired(VIEW_ID.popover))
107
+ applyPopoverBounds();
108
+ }
109
+ function applySettingsBoundsIfPresent() {
110
+ if (reconciler.hasOverlayDesired(VIEW_ID.settings))
111
+ applySettingsBounds();
112
+ }
113
+ function destroySettings() {
114
+ destroyChildView(ctx.windows.mainWindow, settingsView);
115
+ settingsView = null;
116
+ }
117
+ reconciler.registerView(VIEW_ID.settings, { getView: () => settingsView });
118
+ reconciler.registerView(VIEW_ID.popover, { getView: () => popoverView });
119
+ return {
120
+ showSettings,
121
+ hideSettings,
122
+ showPopover,
123
+ hidePopover,
124
+ reapplyPresentOverlays,
125
+ applySettingsBoundsIfPresent,
126
+ destroySettings,
127
+ getSettingsWebContents: () => {
128
+ if (!settingsView)
129
+ return null;
130
+ if (settingsView.webContents.isDestroyed())
131
+ return null;
132
+ return settingsView.webContents;
133
+ },
134
+ getSettingsWebContentsId: () => {
135
+ if (!settingsView)
136
+ return null;
137
+ if (settingsView.webContents.isDestroyed())
138
+ return null;
139
+ return settingsView.webContents.id;
140
+ },
141
+ getPopoverWebContentsId: () => {
142
+ if (!popoverView)
143
+ return null;
144
+ if (popoverView.webContents.isDestroyed())
145
+ return null;
146
+ return popoverView.webContents.id;
147
+ },
148
+ };
149
+ }
150
+ //# sourceMappingURL=overlay-panels-view.js.map