@dimina-kit/devtools 0.3.2-dev.20260601115343 → 0.3.2-dev.20260610114009

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 (189) hide show
  1. package/README.md +49 -59
  2. package/dist/main/api.d.ts +1 -2
  3. package/dist/main/api.js +5 -1
  4. package/dist/main/app/app.d.ts +15 -5
  5. package/dist/main/app/app.js +249 -122
  6. package/dist/main/app/bootstrap.d.ts +16 -0
  7. package/dist/main/app/bootstrap.js +20 -0
  8. package/dist/main/app/launch.d.ts +8 -4
  9. package/dist/main/app/launch.js +9 -4
  10. package/dist/main/index.bundle.js +6071 -1863
  11. package/dist/main/index.js +9 -1
  12. package/dist/main/ipc/app.d.ts +1 -1
  13. package/dist/main/ipc/bridge-router.d.ts +61 -0
  14. package/dist/main/ipc/bridge-router.js +1333 -0
  15. package/dist/main/ipc/panels.d.ts +1 -1
  16. package/dist/main/ipc/panels.js +6 -2
  17. package/dist/main/ipc/popover.d.ts +1 -1
  18. package/dist/main/ipc/project-fs.d.ts +1 -1
  19. package/dist/main/ipc/projects.d.ts +1 -1
  20. package/dist/main/ipc/session.d.ts +1 -1
  21. package/dist/main/ipc/settings.d.ts +1 -1
  22. package/dist/main/ipc/simulator-module.d.ts +6 -0
  23. package/dist/main/ipc/simulator-module.js +9 -1
  24. package/dist/main/ipc/simulator.d.ts +2 -2
  25. package/dist/main/ipc/simulator.js +29 -5
  26. package/dist/main/ipc/toolbar.d.ts +1 -1
  27. package/dist/main/ipc/views.d.ts +1 -1
  28. package/dist/main/ipc/views.js +40 -2
  29. package/dist/main/runtime/devtools-backend.d.ts +17 -0
  30. package/dist/main/runtime/devtools-backend.js +49 -0
  31. package/dist/main/runtime/miniapp-runtime.d.ts +39 -0
  32. package/dist/main/runtime/miniapp-runtime.js +17 -0
  33. package/dist/main/services/automation/exec.d.ts +27 -2
  34. package/dist/main/services/automation/exec.js +37 -3
  35. package/dist/main/services/automation/handlers/app.js +127 -11
  36. package/dist/main/services/automation/handlers/page.js +36 -19
  37. package/dist/main/services/automation/index.js +40 -13
  38. package/dist/main/services/automation/wait-active-page.d.ts +56 -0
  39. package/dist/main/services/automation/wait-active-page.js +71 -0
  40. package/dist/main/services/console-forward/index.d.ts +36 -0
  41. package/dist/main/services/console-forward/index.js +81 -0
  42. package/dist/main/services/dimina-resource-server.d.ts +7 -0
  43. package/dist/main/services/dimina-resource-server.js +99 -0
  44. package/dist/main/services/elements-forward/index.d.ts +66 -0
  45. package/dist/main/services/elements-forward/index.js +583 -0
  46. package/dist/main/services/layout/index.d.ts +22 -0
  47. package/dist/main/services/layout/index.js +25 -0
  48. package/dist/main/services/mcp/index.d.ts +1 -0
  49. package/dist/main/services/mcp/index.js +3 -0
  50. package/dist/main/services/mcp/server.d.ts +1 -1
  51. package/dist/main/services/mcp/server.js +1 -1
  52. package/dist/main/services/mcp/target-manager.d.ts +35 -0
  53. package/dist/main/services/mcp/target-manager.js +63 -1
  54. package/dist/main/services/mcp/tools/context-tools.js +15 -1
  55. package/dist/main/services/module.d.ts +1 -1
  56. package/dist/main/services/network-forward/index.d.ts +190 -0
  57. package/dist/main/services/network-forward/index.js +765 -0
  58. package/dist/main/services/notifications/renderer-notifier.d.ts +11 -0
  59. package/dist/main/services/notifications/renderer-notifier.js +7 -1
  60. package/dist/main/services/render-inspect/index.d.ts +41 -0
  61. package/dist/main/services/render-inspect/index.js +88 -0
  62. package/dist/main/services/safe-area/index.d.ts +17 -0
  63. package/dist/main/services/safe-area/index.js +59 -0
  64. package/dist/main/services/service-host-pool/pool.d.ts +197 -0
  65. package/dist/main/services/service-host-pool/pool.js +364 -0
  66. package/dist/main/services/simulator/custom-apis.d.ts +34 -0
  67. package/dist/main/services/simulator/custom-apis.js +22 -0
  68. package/dist/main/services/simulator-appdata/index.d.ts +35 -0
  69. package/dist/main/services/simulator-appdata/index.js +71 -0
  70. package/dist/main/services/simulator-current-page/index.d.ts +24 -0
  71. package/dist/main/services/simulator-current-page/index.js +19 -0
  72. package/dist/main/services/simulator-storage/index.d.ts +37 -2
  73. package/dist/main/services/simulator-storage/index.js +209 -32
  74. package/dist/main/services/simulator-storage/service-storage-ops.d.ts +40 -0
  75. package/dist/main/services/simulator-storage/service-storage-ops.js +67 -0
  76. package/dist/main/services/simulator-temp-files/index.d.ts +1 -1
  77. package/dist/main/services/simulator-temp-files/index.js +39 -17
  78. package/dist/main/services/simulator-wxml/index.d.ts +29 -0
  79. package/dist/main/services/simulator-wxml/index.js +28 -0
  80. package/dist/main/services/toolbar/toolbar-store.d.ts +3 -3
  81. package/dist/main/services/toolbar/toolbar-store.js +1 -18
  82. package/dist/main/services/views/devtools-tabs.d.ts +47 -0
  83. package/dist/main/services/views/devtools-tabs.js +95 -0
  84. package/dist/main/services/views/miniapp-partition.d.ts +80 -0
  85. package/dist/main/services/views/miniapp-partition.js +138 -0
  86. package/dist/main/services/views/simulator-session-policy.d.ts +28 -0
  87. package/dist/main/services/views/simulator-session-policy.js +82 -0
  88. package/dist/main/services/views/view-manager.d.ts +125 -0
  89. package/dist/main/services/views/view-manager.js +852 -4
  90. package/dist/main/services/workbench-context.d.ts +67 -7
  91. package/dist/main/services/workbench-context.js +6 -1
  92. package/dist/main/services/workspace/workspace-service.js +7 -13
  93. package/dist/main/utils/ipc-registry.d.ts +1 -1
  94. package/dist/main/utils/ipc-registry.js +21 -4
  95. package/dist/main/utils/paths.d.ts +15 -0
  96. package/dist/main/utils/paths.js +19 -0
  97. package/dist/main/utils/sender-policy.d.ts +9 -3
  98. package/dist/main/utils/sender-policy.js +18 -4
  99. package/dist/main/utils/theme.d.ts +1 -1
  100. package/dist/main/utils/theme.js +1 -1
  101. package/dist/main/windows/main-window/create.d.ts +0 -6
  102. package/dist/main/windows/main-window/create.js +6 -80
  103. package/dist/main/windows/main-window/events.d.ts +1 -1
  104. package/dist/main/windows/main-window/events.js +1 -1
  105. package/dist/main/windows/service-host-window/create.d.ts +67 -0
  106. package/dist/main/windows/service-host-window/create.js +138 -0
  107. package/dist/native-host/common/common.js +368 -0
  108. package/dist/native-host/container/pageFrame.css +1 -0
  109. package/dist/native-host/render/render.js +9334 -0
  110. package/dist/native-host/service/service.js +6 -0
  111. package/dist/preload/index.d.ts +2 -0
  112. package/dist/preload/index.js +6 -0
  113. package/dist/preload/instrumentation/app-data.d.ts +18 -20
  114. package/dist/preload/instrumentation/app-data.js +23 -160
  115. package/dist/preload/instrumentation/wxml-extract.d.ts +1 -1
  116. package/dist/preload/instrumentation/wxml-extract.js +1 -1
  117. package/dist/preload/runtime/bridge.d.ts +3 -8
  118. package/dist/preload/runtime/bridge.js +2 -32
  119. package/dist/preload/runtime/custom-apis.js +20 -17
  120. package/dist/preload/runtime/host-toolbar-advertiser.d.ts +27 -0
  121. package/dist/preload/runtime/host-toolbar-advertiser.js +55 -0
  122. package/dist/preload/runtime/main-api-runner.d.ts +9 -0
  123. package/dist/preload/runtime/main-api-runner.js +85 -0
  124. package/dist/preload/runtime/native-host.d.ts +43 -0
  125. package/dist/preload/runtime/native-host.js +83 -0
  126. package/dist/preload/shared/expose.d.ts +10 -5
  127. package/dist/preload/shared/expose.js +30 -12
  128. package/dist/preload/shared/sid-registry.d.ts +11 -0
  129. package/dist/preload/shared/sid-registry.js +38 -0
  130. package/dist/preload/windows/host-toolbar.cjs +161 -0
  131. package/dist/preload/windows/host-toolbar.cjs.map +7 -0
  132. package/dist/preload/windows/host-toolbar.d.ts +2 -0
  133. package/dist/preload/windows/host-toolbar.js +12 -0
  134. package/dist/preload/windows/main.cjs +1935 -0
  135. package/dist/preload/windows/main.cjs.map +4 -4
  136. package/dist/preload/windows/simulator.cjs +925 -0
  137. package/dist/preload/windows/simulator.cjs.map +7 -0
  138. package/dist/preload/windows/simulator.js +244 -574
  139. package/dist/render-host/pageFrame.html +29 -0
  140. package/dist/render-host/preload.cjs +154 -0
  141. package/dist/render-host/render-inspect.js +295 -0
  142. package/dist/renderer/assets/index-CcJKzatF.js +50 -0
  143. package/dist/renderer/assets/{input-CCYS8Ave.js → input-QGx6FFX1.js} +2 -2
  144. package/dist/renderer/assets/ipc-transport-BT4QYQqu.css +1 -0
  145. package/dist/renderer/assets/{ipc-transport-Bn4KXvhu.js → ipc-transport-DvZvwK-Z.js} +2 -2
  146. package/dist/renderer/assets/{popover-CH4rwpdt.js → popover-BOH1If1J.js} +2 -2
  147. package/dist/renderer/assets/select-C7JQ8G5N.js +2 -0
  148. package/dist/renderer/assets/{settings-CMTB-t0x.js → settings-DpAbTJyF.js} +2 -2
  149. package/dist/renderer/assets/{settings-api-Csdngf9Z.js → settings-api-RD2IZF53.js} +2 -2
  150. package/dist/renderer/assets/{workbenchSettings-CODFCzOg.js → workbenchSettings-CNJX4Qna.js} +2 -2
  151. package/dist/renderer/entries/main/index.html +5 -5
  152. package/dist/renderer/entries/popover/index.html +5 -5
  153. package/dist/renderer/entries/settings/index.html +5 -5
  154. package/dist/renderer/entries/workbench-settings/index.html +4 -4
  155. package/dist/service-host/preload.cjs +254 -0
  156. package/dist/service-host/service.html +10 -0
  157. package/dist/service-host/sourcemap-rewrite.cjs +58 -0
  158. package/dist/service-host/sync-api-patch.js +20 -0
  159. package/dist/service-host/sync-impls/menu-button-geometry.js +8 -0
  160. package/dist/service-host/sync-impls/menu-button.js +16 -0
  161. package/dist/service-host/sync-impls/storage.js +50 -0
  162. package/dist/service-host/sync-impls/system-info.js +54 -0
  163. package/dist/shared/appdata-accumulator.d.ts +95 -0
  164. package/dist/shared/appdata-accumulator.js +213 -0
  165. package/dist/shared/bridge-channels.d.ts +378 -0
  166. package/dist/shared/bridge-channels.js +81 -0
  167. package/dist/shared/ipc-channels.d.ts +84 -1
  168. package/dist/shared/ipc-channels.js +90 -10
  169. package/dist/shared/ipc-schemas.d.ts +59 -2
  170. package/dist/shared/ipc-schemas.js +64 -3
  171. package/dist/shared/open-in-editor.d.ts +71 -0
  172. package/dist/shared/open-in-editor.js +138 -0
  173. package/dist/shared/simulator-api-metadata.d.ts +21 -0
  174. package/dist/shared/simulator-api-metadata.js +23 -0
  175. package/dist/shared/types.d.ts +2 -39
  176. package/dist/simulator/assets/device-shell-DsdcNEkt.css +1 -0
  177. package/dist/simulator/assets/device-shell-fH2cI-3q.js +2 -0
  178. package/dist/simulator/assets/jsx-runtime-P6ZegaOf.js +2 -0
  179. package/dist/simulator/assets/simulator-DHylZf9Z.js +10 -0
  180. package/dist/simulator/assets/simulator-mini-app-BDNu5n26.js +2 -0
  181. package/dist/simulator/device-shell/menu-button-geometry.js +8 -0
  182. package/dist/simulator/simulator.html +2 -112
  183. package/package.json +12 -12
  184. package/dist/main/utils/disposable.d.ts +0 -18
  185. package/dist/main/utils/disposable.js +0 -67
  186. package/dist/renderer/assets/index-BDMxXUfn.js +0 -50
  187. package/dist/renderer/assets/ipc-transport-Cu7tubyg.css +0 -1
  188. package/dist/renderer/assets/select-Ct4-sijm.js +0 -2
  189. package/dist/simulator/assets/simulator-B4e3c6_O.js +0 -9
@@ -1,9 +1,17 @@
1
- import { WebContentsView, webContents } from 'electron';
1
+ import { ipcMain, shell, WebContentsView, webContents } from 'electron';
2
2
  import path from 'path';
3
- import { mainPreloadPath } from '../../utils/paths.js';
4
- import { applyNavigationHardening } from '../../windows/navigation-hardening.js';
3
+ import { cjsSiblingPreloadPath, hostToolbarPreloadPath, mainPreloadPath } from '../../utils/paths.js';
4
+ import { applyNavigationHardening, handleWindowOpenExternal, } from '../../windows/navigation-hardening.js';
5
+ import { SimulatorCustomApiBridgeChannel } from '../../../shared/ipc-channels.js';
6
+ import { OPEN_IN_EDITOR_SCHEME, decodeOpenInEditorUrl, resourceUrlToProjectRelativePath, } from '../../../shared/open-in-editor.js';
7
+ import { createSafeAreaController } from '../safe-area/index.js';
8
+ import { buildCustomizeTabsScript } from './devtools-tabs.js';
9
+ import { installElementsForward } from '../elements-forward/index.js';
5
10
  import * as layout from '../layout/index.js';
11
+ import { handleCustomApiBridgeRequest, } from '../simulator/custom-apis.js';
6
12
  import { getDefaultTab } from '../workbench-context.js';
13
+ import { configureMiniappSession, miniappPartition } from './miniapp-partition.js';
14
+ import { parseRoute } from '../../../shared/simulator-route.js';
7
15
  /**
8
16
  * Build a ViewManager bound to the given context. The returned object is the
9
17
  * only component allowed to instantiate or add/remove overlay WebContentsViews.
@@ -15,19 +23,71 @@ export function createViewManager(ctx) {
15
23
  // Resolve once: full WorkbenchContext always provides headerHeight; partial
16
24
  // test contexts may omit it, in which case fall back to the default 40.
17
25
  const headerHeight = ctx.headerHeight ?? 40;
26
+ // CSS env(safe-area-inset-*) simulation for render-host guests (per device).
27
+ // Driven from did-attach-webview below; re-pushed on device change via
28
+ // reapplySafeArea. Torn down in disposeAll.
29
+ const safeArea = createSafeAreaController({ connections: ctx.connections });
18
30
  // ── Private mutable state ───────────────────────────────────────────────
19
31
  let simulatorView = null;
20
32
  let simulatorViewAdded = false;
33
+ // NATIVE-HOST ONLY: the simulator content WebContentsView (the DeviceShell
34
+ // host). In the default path the simulator is a renderer `<webview>` and this
35
+ // stays null. Positioned in the simulator panel region (left of the splitter)
36
+ // while `simulatorView` above hosts its DevTools in the right panel region.
37
+ let nativeSimulatorView = null;
38
+ let nativeSimulatorViewAdded = false;
39
+ // Model A: the renderer's measured inner-screen rect is the SOLE authority for
40
+ // the native simulator WCV bounds (see docs/simulator-render-architecture.md).
41
+ // Cache the last rect so a report that lands before attachNativeSimulator (the
42
+ // project-open ordering) is not lost — attach replays it instead of using a
43
+ // coarse panel-size fallback (which raced and caused the clip/surround flips).
44
+ let lastRendererRect = null;
45
+ // NATIVE-HOST ONLY. The `ipcMain.on` listener that services the
46
+ // `__diminaCustomApis` bridge for the current native simulator webContents
47
+ // (see `attachNativeCustomApiBridge`). Tracked so we can remove it before
48
+ // tearing down / re-attaching the view (otherwise listeners leak across
49
+ // relaunch cycles).
50
+ let nativeCustomApiBridgeHandler = null;
51
+ // NATIVE-HOST ONLY. The current device zoom as a factor (zoomPercent/100),
52
+ // last reported by the renderer via setNativeSimulatorViewBounds. Stored so
53
+ // nested render-host `<webview>` guests attached AFTER a zoom change still
54
+ // pick up the correct scale in `did-attach-webview`. Defaults to 1 (100%).
55
+ let currentZoomFactor = 1;
21
56
  let settingsView = null;
22
57
  let settingsViewAdded = false;
23
58
  let popoverView = null;
24
59
  let lastSimWidth = 375;
25
60
  let simulatorWebContentsId = null;
61
+ // NATIVE-HOST ONLY. The webContents the right-panel Chrome DevTools front-end
62
+ // currently inspects. We point it at the SERVICE HOST (logic layer) — the
63
+ // hidden BrowserWindow where the mini-app's page code runs (`console.log`,
64
+ // `wx.request`, Sources/Network(fetch) all live there). The UI/view layer
65
+ // (Elements/Styles/WXML tree) is served separately by the native WXML panel +
66
+ // render-guest highlight chain (`simulator-storage`/`simulator-wxml`), so a
67
+ // single DevTools front-end is enough. The service window can be swapped on
68
+ // respawn (pre-warm pool recycles it), so this is re-resolved fresh via
69
+ // `ctx.bridge.getServiceWc()` and re-pointed on every render-side event.
70
+ let nativeDevtoolsSourceWc = null;
71
+ let unsubscribeNativeRenderEvents = null;
72
+ // Disposer for the Elements-forward feature (routes the front-end's Elements
73
+ // CDP traffic at the active render guest). Installed in
74
+ // `attachNativeSimulatorDevtoolsHost`, stopped on detach / host destroyed.
75
+ let stopElementsForward = null;
76
+ let nativeDevtoolsRetryTimer = null;
77
+ let nativeDevtoolsRetryToken = 0;
26
78
  // Renderer-driven overlay bounds for the simulator DevTools view. When
27
79
  // non-null this takes precedence over the legacy layout computed from the
28
80
  // window content size. A zero-area rectangle means "hide" — the overlay is
29
81
  // removed from the contentView but its WebContents stays alive.
30
82
  let simulatorBoundsOverride = null;
83
+ // ── Host-controllable toolbar WebContentsView ───────────────────────────
84
+ // A strip above the devtools header that the downstream host loads its own
85
+ // content into and fully controls. Bounds come from a renderer DOM anchor
86
+ // (forward anchor, like the simulator DevTools overlay); its height is
87
+ // dynamic via a reverse size-advertiser the toolbar's own renderer drives.
88
+ let hostToolbarView = null;
89
+ let hostToolbarPreloadOverride = null;
90
+ let hostToolbarViewAdded = false;
31
91
  // ── Internal helpers ────────────────────────────────────────────────────
32
92
  function destroyViewInternal(view) {
33
93
  if (!view)
@@ -84,6 +144,124 @@ export function createViewManager(ctx) {
84
144
  }
85
145
  simulatorView.setBounds(bounds);
86
146
  }
147
+ // ── Host-controllable toolbar WebContentsView ───────────────────────────
148
+ // Lazily create the host-toolbar view. Mirrors `showSettings` for the
149
+ // webPreferences shape and the native simulator for nav hardening +
150
+ // background color (the host may load arbitrary URLs / content). Idempotent.
151
+ function ensureHostToolbarView() {
152
+ if (hostToolbarView && !hostToolbarView.webContents.isDestroyed()) {
153
+ return hostToolbarView;
154
+ }
155
+ // Rebuilding after the host destroyed the underlying webContents: detach the
156
+ // dead view from the contentView and reset the added-flag so the new view
157
+ // gets re-mounted (otherwise the `hostToolbarViewAdded` guard would skip the
158
+ // addChildView and the toolbar would silently disappear).
159
+ if (hostToolbarView && hostToolbarViewAdded) {
160
+ try {
161
+ ctx.windows.mainWindow.contentView.removeChildView(hostToolbarView);
162
+ }
163
+ catch { /* already removed */ }
164
+ hostToolbarViewAdded = false;
165
+ }
166
+ const view = new WebContentsView({
167
+ webPreferences: {
168
+ nodeIntegration: false,
169
+ contextIsolation: true,
170
+ sandbox: false,
171
+ // Default: the reverse size-advertiser preload that measures the host
172
+ // content's intrinsic height and posts it on the advertise channel so
173
+ // main reserves exactly that strip height (dynamic height via
174
+ // ViewAnchor). When the host-shell supplies its own toolbar preload
175
+ // (workbench config.toolbar.preloadPath), use that instead — the host
176
+ // owns the bridge and may install the advertiser itself.
177
+ preload: hostToolbarPreloadOverride ?? hostToolbarPreloadPath,
178
+ },
179
+ });
180
+ hostToolbarView = view;
181
+ // Paint the surface a neutral color so growing the reserved strip never
182
+ // flashes white before the host content paints (mirrors the native
183
+ // simulator's setBackgroundColor anti-flash).
184
+ try {
185
+ view.setBackgroundColor('#121212');
186
+ }
187
+ catch { /* stub may lack it */ }
188
+ // The host may load arbitrary URLs; route popups + cross-origin in-place
189
+ // navigation to the OS browser (mirror the native simulator hardening).
190
+ try {
191
+ view.webContents.setWindowOpenHandler(({ url }) => handleWindowOpenExternal(url));
192
+ }
193
+ catch { /* stub may lack it */ }
194
+ return view;
195
+ }
196
+ function setHostToolbarBounds(bounds) {
197
+ if (ctx.windows.mainWindow.isDestroyed())
198
+ return;
199
+ // Zero-area rect means "hide" — remove the child view but keep its
200
+ // WebContents (and the host's loaded content) alive. Do NOT create the
201
+ // view just to immediately hide it.
202
+ if (isHidden(bounds)) {
203
+ if (hostToolbarView && hostToolbarViewAdded && !hostToolbarView.webContents.isDestroyed()) {
204
+ try {
205
+ ctx.windows.mainWindow.contentView.removeChildView(hostToolbarView);
206
+ }
207
+ catch { /* already removed */ }
208
+ }
209
+ hostToolbarViewAdded = false;
210
+ return;
211
+ }
212
+ const view = ensureHostToolbarView();
213
+ if (!hostToolbarViewAdded) {
214
+ // addChildView appends = topmost z-order, which is correct for a strip
215
+ // that sits above the devtools header.
216
+ ctx.windows.mainWindow.contentView.addChildView(view);
217
+ hostToolbarViewAdded = true;
218
+ }
219
+ view.setBounds(bounds);
220
+ }
221
+ function setHostToolbarHeight(extent) {
222
+ // Push the reserved height back to the main-window renderer so its
223
+ // placeholder div resizes (closing the dynamic-height loop). The height is
224
+ // not retained in main — the renderer placeholder is the single source of
225
+ // truth, and the forward anchor re-reports bounds from it.
226
+ ctx.notify.hostToolbarHeightChanged(extent);
227
+ }
228
+ function hideHostToolbar() {
229
+ if (hostToolbarView && hostToolbarViewAdded && !ctx.windows.mainWindow.isDestroyed()) {
230
+ try {
231
+ ctx.windows.mainWindow.contentView.removeChildView(hostToolbarView);
232
+ }
233
+ catch { /* already removed */ }
234
+ }
235
+ hostToolbarViewAdded = false;
236
+ // Collapse the renderer placeholder to 0 too. Otherwise its anchor keeps a
237
+ // non-zero reserved height and re-publishes bounds on the next window
238
+ // resize, silently re-adding the view we just hid (unstable hide). Zeroing
239
+ // the height flips the anchor to `present:false` so it stops re-publishing.
240
+ ctx.notify.hostToolbarHeightChanged(0);
241
+ }
242
+ const hostToolbar = {
243
+ async loadURL(url) {
244
+ const view = ensureHostToolbarView();
245
+ await view.webContents.loadURL(url);
246
+ },
247
+ async loadFile(filePath) {
248
+ const view = ensureHostToolbarView();
249
+ await view.webContents.loadFile(filePath);
250
+ },
251
+ get webContents() {
252
+ if (!hostToolbarView)
253
+ return null;
254
+ if (hostToolbarView.webContents.isDestroyed())
255
+ return null;
256
+ return hostToolbarView.webContents;
257
+ },
258
+ hide() {
259
+ hideHostToolbar();
260
+ },
261
+ setPreloadPath(path) {
262
+ hostToolbarPreloadOverride = path;
263
+ },
264
+ };
87
265
  function applySettingsBounds() {
88
266
  if (!settingsView || ctx.windows.mainWindow.isDestroyed())
89
267
  return;
@@ -96,6 +274,242 @@ export function createViewManager(ctx) {
96
274
  const [w = 0, h = 0] = ctx.windows.mainWindow.getContentSize();
97
275
  popoverView.setBounds(layout.computePopoverBounds(w, h, headerHeight));
98
276
  }
277
+ function clearNativeDevtoolsRetry() {
278
+ nativeDevtoolsRetryToken++;
279
+ if (nativeDevtoolsRetryTimer) {
280
+ clearTimeout(nativeDevtoolsRetryTimer);
281
+ nativeDevtoolsRetryTimer = null;
282
+ }
283
+ }
284
+ function closeNativeDevtoolsSource() {
285
+ const source = nativeDevtoolsSourceWc;
286
+ nativeDevtoolsSourceWc = null;
287
+ if (!source || source.isDestroyed())
288
+ return;
289
+ try {
290
+ if (source.isDevToolsOpened()) {
291
+ source.closeDevTools();
292
+ }
293
+ }
294
+ catch { /* source may be mid-destroy */ }
295
+ }
296
+ function stopFollowingNativeServiceHost() {
297
+ clearNativeDevtoolsRetry();
298
+ if (unsubscribeNativeRenderEvents) {
299
+ try {
300
+ unsubscribeNativeRenderEvents();
301
+ }
302
+ catch { /* ignore */ }
303
+ unsubscribeNativeRenderEvents = null;
304
+ }
305
+ closeNativeDevtoolsSource();
306
+ }
307
+ // ── Open-in-editor: click a console file link → built-in Monaco ──────────
308
+ // The right-panel console is the embedded Chromium DevTools front-end. Once a
309
+ // sourcemap maps a console frame back to source (restored by the service-host
310
+ // importScripts sourcemap rewrite), we redirect a source-link click to OUR
311
+ // Monaco editor instead of the DevTools Sources panel: install an "open
312
+ // resource handler" in the front-end realm that encodes (url, line, col) into
313
+ // a sentinel URL and asks the front-end to open it; Electron surfaces that as
314
+ // `devtools-open-url` on the inspected (service-host) wc, which we decode,
315
+ // map to a project-relative path, and broadcast to the renderer's Monaco.
316
+ const openInEditorWiredWcIds = new Set();
317
+ function injectOpenResourceHandler(devtoolsWc) {
318
+ // `setOpenResourceHandler` is the official Chromium DevTools hook IDEs use
319
+ // to route source-link clicks to an external editor. We poll for the host
320
+ // (it appears once the front-end finishes bootstrapping, like `UI` above)
321
+ // and register a handler that re-emits an encoded sentinel via
322
+ // `openInNewTab` → Electron `devtools-open-url`. Best-effort: wrapped in
323
+ // try/catch and a bounded poll so a missing API never throws.
324
+ const scheme = JSON.stringify(OPEN_IN_EDITOR_SCHEME);
325
+ devtoolsWc.executeJavaScript(`
326
+ (function() {
327
+ try {
328
+ let tries = 0
329
+ const timer = setInterval(() => {
330
+ tries++
331
+ try {
332
+ const Host = globalThis.Host
333
+ const host = Host && Host.InspectorFrontendHost
334
+ if (host && typeof host.setOpenResourceHandler === 'function'
335
+ && typeof host.openInNewTab === 'function') {
336
+ host.setOpenResourceHandler((url, lineNumber, columnNumber) => {
337
+ try {
338
+ const p = new URLSearchParams()
339
+ p.set('u', String(url))
340
+ if (typeof lineNumber === 'number') p.set('l', String(lineNumber))
341
+ if (typeof columnNumber === 'number') p.set('c', String(columnNumber))
342
+ host.openInNewTab(${scheme} + ':?' + p.toString())
343
+ } catch (_) {}
344
+ })
345
+ clearInterval(timer)
346
+ return
347
+ }
348
+ } catch (_) {}
349
+ if (tries > 80) clearInterval(timer)
350
+ }, 50)
351
+ } catch (_) {}
352
+ })()
353
+ `).catch(() => { });
354
+ }
355
+ // ── DevTools tab customization: keep only Elements/Console/Network ──────────
356
+ // Inject into the same DevTools front-end host wc that the console/network
357
+ // injectors target. Reorders Elements/Console/Network to the front and closes
358
+ // every other panel tab by driving the front-end's UI.ViewManager /
359
+ // InspectorView.tabbedLocation (see ./devtools-tabs.ts). Best-effort: the
360
+ // injected script bounded-polls for the lazily-registered panels, wraps
361
+ // everything in try/catch, and silently no-ops if the API never appears —
362
+ // DevTools default tab behaviour is preserved on any failure. Re-injected on
363
+ // every (re)point so a re-`openDevTools` (service-host pool swap) re-applies.
364
+ function customizeDevtoolsTabs(devtoolsWc) {
365
+ try {
366
+ if (devtoolsWc.isDestroyed())
367
+ return;
368
+ const inject = () => {
369
+ if (devtoolsWc.isDestroyed())
370
+ return;
371
+ try {
372
+ void devtoolsWc.executeJavaScript(buildCustomizeTabsScript()).catch(() => { });
373
+ }
374
+ catch { /* wc torn down mid-call */ }
375
+ };
376
+ if (devtoolsWc.isLoading()) {
377
+ devtoolsWc.once('dom-ready', inject);
378
+ }
379
+ else {
380
+ inject();
381
+ }
382
+ }
383
+ catch { /* wc surface incomplete / torn down; degrade silently */ }
384
+ }
385
+ function wireOpenInEditor(serviceWc, devtoolsWc) {
386
+ // Inject the front-end handler on every (re)attach — the DevTools front-end
387
+ // wc is recreated per simulatorView, so a fresh host needs the handler set.
388
+ if (!devtoolsWc.isDestroyed()) {
389
+ if (devtoolsWc.isLoading()) {
390
+ devtoolsWc.once('dom-ready', () => injectOpenResourceHandler(devtoolsWc));
391
+ }
392
+ else {
393
+ injectOpenResourceHandler(devtoolsWc);
394
+ }
395
+ }
396
+ // Attach the `devtools-open-url` decoder to the inspected service wc once
397
+ // (the listener is keyed off the encoded sentinel scheme, so it only acts on
398
+ // OUR redirected links; any other devtools "open in new tab" falls through).
399
+ if (openInEditorWiredWcIds.has(serviceWc.id))
400
+ return;
401
+ openInEditorWiredWcIds.add(serviceWc.id);
402
+ const onOpenUrl = (_event, url) => {
403
+ const req = decodeOpenInEditorUrl(url);
404
+ if (!req)
405
+ return; // not our sentinel — leave it to Electron's default path
406
+ const rel = resourceUrlToProjectRelativePath(req.url);
407
+ if (!rel)
408
+ return;
409
+ // DevTools reports 0-based line/column; Monaco is 1-based.
410
+ const line = typeof req.line === 'number' ? req.line + 1 : undefined;
411
+ const column = typeof req.column === 'number' ? req.column + 1 : undefined;
412
+ ctx.notify.editorOpenFile({ path: rel, line, column });
413
+ };
414
+ serviceWc.on('devtools-open-url', onOpenUrl);
415
+ // Consolidate teardown onto the connection layer (foundation.md §4 / P2),
416
+ // but as a wc-LIFETIME resource: the open-in-editor wiring inspects this
417
+ // service-host wc and must SURVIVE pool reuse (`reset`) — the dedup set
418
+ // entry + listener stay valid across sessions reusing the same wc, and the
419
+ // early-return dedup above correctly skips re-wiring after a reset. So we
420
+ // register on `'closed'` (fires only on real wc destroy), NOT `own()`
421
+ // (which also fires on `reset` and would leave the wc un-wired after reuse
422
+ // because re-pointing the same wc.id early-returns). acquire() is
423
+ // idempotent — bridge-router owning session-scoped resources on the same
424
+ // serviceWc connection coexists cleanly.
425
+ const conn = ctx.connections.acquire(serviceWc);
426
+ conn.on('closed', () => {
427
+ openInEditorWiredWcIds.delete(serviceWc.id);
428
+ try {
429
+ serviceWc.removeListener('devtools-open-url', onOpenUrl);
430
+ }
431
+ catch { /* wc gone */ }
432
+ });
433
+ }
434
+ // Point the right-panel Chrome DevTools front-end at `next` — the SERVICE HOST
435
+ // webContents (logic layer). Idempotent: if we already inspect this wc, no-op.
436
+ function pointNativeDevtoolsAtServiceWc(next) {
437
+ if (!simulatorView || simulatorView.webContents.isDestroyed())
438
+ return true;
439
+ if (nativeDevtoolsSourceWc?.id === next.id && !nativeDevtoolsSourceWc.isDestroyed()) {
440
+ return true;
441
+ }
442
+ closeNativeDevtoolsSource();
443
+ nativeDevtoolsSourceWc = next;
444
+ try {
445
+ next.setDevToolsWebContents(simulatorView.webContents);
446
+ // DevTools renders into the right-panel host view (simulatorView); with a
447
+ // custom host the `mode` is overridden, and `activate:false` prevents it
448
+ // stealing focus — this re-points whenever the service window is swapped,
449
+ // so a focusing window would yank focus repeatedly (disrupting the user /
450
+ // e2e).
451
+ next.openDevTools({ mode: 'detach', activate: false });
452
+ // Redirect console source-link clicks to the built-in Monaco editor.
453
+ wireOpenInEditor(next, simulatorView.webContents);
454
+ // Keep only Elements/Console/Network tabs (front-most, in that order).
455
+ // Re-applied on every re-point so a service-host pool swap (fresh
456
+ // openDevTools) re-asserts the custom tab bar.
457
+ customizeDevtoolsTabs(simulatorView.webContents);
458
+ return true;
459
+ }
460
+ catch {
461
+ if (nativeDevtoolsSourceWc?.id === next.id) {
462
+ nativeDevtoolsSourceWc = null;
463
+ }
464
+ return false;
465
+ }
466
+ }
467
+ // Resolve the SERVICE HOST webContents for the active (or named) app. This is
468
+ // the hidden service BrowserWindow's wc — a top-level wc that CAN host a
469
+ // Chrome DevTools front-end (unlike a `<webview>` guest's). Re-resolved fresh
470
+ // so a pre-warm-pool swap on respawn is tolerated.
471
+ function pointNativeDevtoolsAtActiveServiceHost(appId) {
472
+ if (!ctx.bridge?.isNativeHost())
473
+ return true;
474
+ if (!simulatorView || simulatorView.webContents.isDestroyed())
475
+ return true;
476
+ const wc = ctx.bridge.getServiceWc(appId);
477
+ if (!wc || wc.isDestroyed())
478
+ return false;
479
+ return pointNativeDevtoolsAtServiceWc(wc);
480
+ }
481
+ function scheduleNativeDevtoolsFollow(appId, attempt = 0) {
482
+ if (attempt >= 20)
483
+ return;
484
+ if (!ctx.bridge?.isNativeHost())
485
+ return;
486
+ const token = nativeDevtoolsRetryToken;
487
+ if (nativeDevtoolsRetryTimer)
488
+ clearTimeout(nativeDevtoolsRetryTimer);
489
+ nativeDevtoolsRetryTimer = setTimeout(() => {
490
+ nativeDevtoolsRetryTimer = null;
491
+ if (token !== nativeDevtoolsRetryToken)
492
+ return;
493
+ if (pointNativeDevtoolsAtActiveServiceHost(appId))
494
+ return;
495
+ scheduleNativeDevtoolsFollow(appId, attempt + 1);
496
+ }, 50);
497
+ }
498
+ function followNativeDevtoolsServiceHost(appId) {
499
+ clearNativeDevtoolsRetry();
500
+ if (pointNativeDevtoolsAtActiveServiceHost(appId))
501
+ return;
502
+ scheduleNativeDevtoolsFollow(appId);
503
+ }
504
+ // Render-side activity (a page DOM mounting / the visible page changing)
505
+ // always follows a spawn or respawn, by which point the service window exists
506
+ // (and may have been swapped by the pool). Re-resolve + re-point the DevTools
507
+ // at the now-current service host on every such event.
508
+ function onNativeRenderEvent(event) {
509
+ if (event.kind !== 'activePage' && event.kind !== 'domReady')
510
+ return;
511
+ followNativeDevtoolsServiceHost(event.appId);
512
+ }
99
513
  // ── ViewManager methods ─────────────────────────────────────────────────
100
514
  function attachSimulator(simWcId, simWidth) {
101
515
  const sim = webContents.fromId(simWcId);
@@ -118,7 +532,9 @@ export function createViewManager(ctx) {
118
532
  }
119
533
  simulatorView = new WebContentsView();
120
534
  sim.setDevToolsWebContents(simulatorView.webContents);
121
- sim.openDevTools();
535
+ // Embedded in the right-panel host view; never bring it to the foreground
536
+ // (would steal focus from the user / disrupt e2e).
537
+ sim.openDevTools({ mode: 'detach', activate: false });
122
538
  // Default DevTools to Console panel (Chrome DevTools defaults to Elements).
123
539
  // The DevTools UI lives inside closed shadow roots, so a light-DOM
124
540
  // querySelector('[role="tab"]') cannot reach the tab bar to click it.
@@ -128,6 +544,8 @@ export function createViewManager(ctx) {
128
544
  // request the `console` view, and also persist the choice via the
129
545
  // `panel-selectedTab` localStorage key so subsequent reloads honor it.
130
546
  const devtoolsWc = simulatorView.webContents;
547
+ // Keep only Elements/Console/Network tabs (front-most, in that order).
548
+ customizeDevtoolsTabs(devtoolsWc);
131
549
  devtoolsWc.once('dom-ready', () => {
132
550
  devtoolsWc.executeJavaScript(`
133
551
  (function() {
@@ -166,7 +584,360 @@ export function createViewManager(ctx) {
166
584
  }
167
585
  }
168
586
  }
587
+ function attachNativeSimulatorDevtoolsHost(simWidth) {
588
+ stopFollowingNativeServiceHost();
589
+ // Destroy old simulatorView to prevent WebContentsView leak
590
+ if (simulatorView) {
591
+ hideSimulator();
592
+ try {
593
+ if (!simulatorView.webContents.isDestroyed()) {
594
+ simulatorView.webContents.close();
595
+ }
596
+ }
597
+ catch { /* ignore */ }
598
+ simulatorView = null;
599
+ }
600
+ simulatorView = new WebContentsView();
601
+ // Default DevTools to Console panel (Chrome DevTools defaults to Elements).
602
+ // The DevTools UI lives inside closed shadow roots, so a light-DOM
603
+ // querySelector('[role="tab"]') cannot reach the tab bar to click it.
604
+ // Instead drive the front-end's own view manager: the bundled DevTools
605
+ // exposes `UI.ViewManager.instance().showView(id)` on `globalThis.UI`
606
+ // once the front-end has finished bootstrapping. We poll for it and
607
+ // request the `console` view, and also persist the choice via the
608
+ // `panel-selectedTab` localStorage key so subsequent reloads honor it.
609
+ const devtoolsWc = simulatorView.webContents;
610
+ // Hand the network forwarder THIS front-end host wc — it injects the
611
+ // simulator WCV's Network.* CDP events into `window.DevToolsAPI.dispatchMessage`
612
+ // here so the native Network tab renders them (falls back to the service-host
613
+ // console line when null / the API never appears). Cleared on detach.
614
+ ctx.networkForward?.setDevtoolsHost(devtoolsWc);
615
+ // Elements forwarding (production, always on for the native simulator): route
616
+ // the front-end's Elements-panel CDP traffic (DOM/CSS/Overlay/DOMSnapshot/
617
+ // DOMDebugger) onto the ACTIVE RENDER GUEST so the panel reflects the page's
618
+ // live DOM tree instead of the service host it natively inspects. Reuses the
619
+ // safe-area-attached debugger session (never detaches one it doesn't own);
620
+ // Emulation.* and every other domain stay on the service-host path. Degrades
621
+ // to the native service-host DOM if the front-end hook is unavailable. The
622
+ // disposer is stopped on detach / host destroyed.
623
+ if (ctx.bridge) {
624
+ try {
625
+ stopElementsForward?.();
626
+ }
627
+ catch { /* prior disposer already gone */ }
628
+ stopElementsForward = installElementsForward({ devtoolsWc, bridge: ctx.bridge, connections: ctx.connections });
629
+ devtoolsWc.once('destroyed', () => {
630
+ try {
631
+ stopElementsForward?.();
632
+ }
633
+ catch { /* already stopped */ }
634
+ stopElementsForward = null;
635
+ });
636
+ }
637
+ devtoolsWc.once('dom-ready', () => {
638
+ devtoolsWc.executeJavaScript(`
639
+ (function() {
640
+ try { localStorage.setItem('panel-selectedTab', '"console"') } catch {}
641
+ let tries = 0
642
+ const timer = setInterval(() => {
643
+ tries++
644
+ try {
645
+ const UI = globalThis.UI
646
+ const vm = UI && UI.ViewManager && typeof UI.ViewManager.instance === 'function'
647
+ ? UI.ViewManager.instance()
648
+ : null
649
+ if (vm && typeof vm.showView === 'function') {
650
+ vm.showView('console')
651
+ clearInterval(timer)
652
+ return
653
+ }
654
+ } catch {}
655
+ if (tries > 80) clearInterval(timer)
656
+ }, 50)
657
+ })()
658
+ `).catch(() => { });
659
+ });
660
+ if (getDefaultTab(ctx) === 'simulator') {
661
+ if (simulatorBoundsOverride) {
662
+ if (!isHidden(simulatorBoundsOverride)) {
663
+ ctx.windows.mainWindow.contentView.addChildView(simulatorView);
664
+ simulatorViewAdded = true;
665
+ simulatorView.setBounds(simulatorBoundsOverride);
666
+ }
667
+ }
668
+ else {
669
+ ctx.windows.mainWindow.contentView.addChildView(simulatorView);
670
+ simulatorViewAdded = true;
671
+ applySimulatorBounds(simWidth);
672
+ }
673
+ }
674
+ if (ctx.bridge?.isNativeHost()) {
675
+ unsubscribeNativeRenderEvents = ctx.bridge.onRenderEvent(onNativeRenderEvent);
676
+ followNativeDevtoolsServiceHost();
677
+ }
678
+ }
679
+ /**
680
+ * Remove the custom-apis bridge `ipcMain.on` listener, if any. Idempotent.
681
+ */
682
+ function detachNativeCustomApiBridge() {
683
+ if (nativeCustomApiBridgeHandler) {
684
+ ipcMain.removeListener(SimulatorCustomApiBridgeChannel.Request, nativeCustomApiBridgeHandler);
685
+ nativeCustomApiBridgeHandler = null;
686
+ }
687
+ }
688
+ /**
689
+ * NATIVE-HOST ONLY. The native simulator is a top-level WebContentsView with
690
+ * NO embedder renderer, so the simulator-side `__diminaCustomApis` bridge
691
+ * (`src/preload/runtime/custom-apis.ts`) cannot reach the host via
692
+ * `ipcRenderer.sendToHost` — that only delivers to a `<webview>`'s embedder,
693
+ * which is how the default path's `useCustomApiProxy` answered it. A
694
+ * top-level WebContentsView has no embedder, and `sendToHost` does NOT loop
695
+ * back as `ipc-message-host` on itself, so that channel never fires here.
696
+ *
697
+ * Under native-host the bridge instead talks to `ipcMain` directly (the same
698
+ * way `installNativeHostBridge` issues SPAWN/PAGE_OPEN): the preload sends
699
+ * `SimulatorCustomApiBridgeChannel.Request` via `ipcRenderer.send`, this
700
+ * `ipcMain.on` listener answers it. We do NOT route through `IpcRegistry` /
701
+ * the sender-policy white-list (the simulator is deliberately kept off it);
702
+ * instead we accept the message ONLY when `event.sender` is THIS precise
703
+ * simWc — the same trust model bridge-router uses for the simulator's own
704
+ * SPAWN messages. The result is dispatched through the shared
705
+ * `ctx.simulatorApis` registry and the id-correlated `Response` is posted
706
+ * back via `simWc.send`, which the preload's `ipcRenderer.on(Response)`
707
+ * listener settles.
708
+ */
709
+ function attachNativeCustomApiBridge(simWc) {
710
+ // Re-attach defensively: a stale listener from a prior simWc must never
711
+ // linger. (attachNativeSimulator already tears the old view down first.)
712
+ detachNativeCustomApiBridge();
713
+ const apis = ctx.simulatorApis;
714
+ if (!apis)
715
+ return;
716
+ const simWcId = simWc.id;
717
+ const handler = (event, req) => {
718
+ // Trust gate: only the exact native simulator webContents may drive this.
719
+ if (event.sender.id !== simWcId)
720
+ return;
721
+ const r = req;
722
+ if (!r || typeof r.id !== 'number')
723
+ return;
724
+ void handleCustomApiBridgeRequest(apis, r).then((response) => {
725
+ if (simWc.isDestroyed())
726
+ return;
727
+ simWc.send(SimulatorCustomApiBridgeChannel.Response, response);
728
+ }).catch(() => { });
729
+ };
730
+ nativeCustomApiBridgeHandler = handler;
731
+ ipcMain.on(SimulatorCustomApiBridgeChannel.Request, handler);
732
+ // Consolidate this per-webContents teardown onto the connection layer
733
+ // (foundation.md §4 / P1 DoD #3): acquiring the simWc connection makes the
734
+ // registry track this trusted webContents, and `own()`-ing the detach ties
735
+ // the ipcMain listener's lifetime to the connection so it's torn down
736
+ // deterministically when the wc is destroyed (or the connection is reset),
737
+ // instead of a bespoke `once('destroyed')`. The detach stays idempotent, so
738
+ // the defensive re-attach path calling it directly remains safe.
739
+ ctx.connections.acquire(simWc).own(detachNativeCustomApiBridge);
740
+ }
741
+ function attachNativeSimulator(simulatorUrl, simWidth) {
742
+ if (!ctx.preloadPath) {
743
+ console.error('[workbench] attachNativeSimulator — preloadPath unset; cannot mount native simulator');
744
+ return;
745
+ }
746
+ lastSimWidth = simWidth;
747
+ // Tear down any previous native simulator view (relaunch / re-open).
748
+ if (nativeSimulatorView) {
749
+ detachNativeCustomApiBridge();
750
+ if (nativeSimulatorViewAdded) {
751
+ try {
752
+ ctx.windows.mainWindow.contentView.removeChildView(nativeSimulatorView);
753
+ }
754
+ catch { /* already removed */ }
755
+ nativeSimulatorViewAdded = false;
756
+ }
757
+ try {
758
+ if (!nativeSimulatorView.webContents.isDestroyed()) {
759
+ nativeSimulatorView.webContents.close();
760
+ }
761
+ }
762
+ catch { /* ignore */ }
763
+ nativeSimulatorView = null;
764
+ }
765
+ // Derive THIS project's session partition from the simulator URL's appId so
766
+ // its cookies/localStorage/cache are isolated from every other project (P0
767
+ // debt). Same project → same partition (storage survives a relaunch);
768
+ // unknown appId → the shared fallback. Configure the partition's session
769
+ // (protocol handlers + CORS/referer policy) before any project content loads
770
+ // on it — idempotent per partition.
771
+ const route = parseRoute(simulatorUrl);
772
+ const partition = miniappPartition(route?.appId);
773
+ configureMiniappSession(partition);
774
+ // The simulator preload is a CJS bundle; webPreferences.preload obeys the
775
+ // `.js` + "type":"module" ESM rule (require would be undefined), so hand the
776
+ // top-level WebContentsView the `.cjs` sibling. contextIsolation:false +
777
+ // sandbox:false + webviewTag:true mirror what the default `<webview>` guest
778
+ // runs with, and the per-project `persist:miniapp-<key>` partition shares
779
+ // storage + the session-registered preload/CORS rules with the rest of THIS
780
+ // project's simulator (render guests + service host), never other projects'.
781
+ const view = new WebContentsView({
782
+ webPreferences: {
783
+ nodeIntegration: false,
784
+ contextIsolation: false,
785
+ sandbox: false,
786
+ webviewTag: true,
787
+ preload: cjsSiblingPreloadPath(ctx.preloadPath),
788
+ partition,
789
+ },
790
+ });
791
+ nativeSimulatorView = view;
792
+ // Paint the WCV surface the simulator-panel background (≈ --color-sim-bg
793
+ // hsl(0 0% 7%)) so a height-resize that grows the region never flashes white
794
+ // in the newly-exposed strip before DeviceShell's desk repaints — the WCV,
795
+ // the desk, and the renderer placeholder behind it are all the same color.
796
+ view.setBackgroundColor('#121212');
797
+ const simWc = view.webContents;
798
+ // Service the simulator-side `__diminaCustomApis` bridge: this top-level
799
+ // WebContentsView has no embedder renderer to proxy through, so dispatch its
800
+ // `sendToHost` requests straight to `ctx.simulatorApis` from main.
801
+ attachNativeCustomApiBridge(simWc);
802
+ // DeviceShell mounts per-page render-host `<webview>`s INSIDE this view.
803
+ // Pin them onto the SAME per-project partition as their host WCV (so render
804
+ // and the rest of this project share one localStorage/cookie jar) and run
805
+ // them with contextIsolation/sandbox off so the render runtime + its preload
806
+ // share the page realm. (A top-level WebContentsView can host these guests; a
807
+ // `<webview>` guest cannot — that's the whole point of Option A.)
808
+ simWc.on('will-attach-webview', (_event, webPreferences, params) => {
809
+ ;
810
+ webPreferences.partition = partition;
811
+ params.partition = partition;
812
+ webPreferences.contextIsolation = false;
813
+ webPreferences.sandbox = false;
814
+ });
815
+ simWc.on('did-attach-webview', (_event, guestWc) => {
816
+ // Scale the nested render-host page with the device zoom. The host WCV is
817
+ // sized to the SCALED bezel rect and runs at `currentZoomFactor`, so the
818
+ // guest must run at the same factor to lay the page out at the logical
819
+ // device width and paint at the right scale. At 100% (default) this is a
820
+ // no-op identity (factor 1). Newly-attached guests pick up the latest
821
+ // zoom here; live zoom changes re-apply via setNativeSimulatorViewBounds.
822
+ try {
823
+ guestWc.setZoomFactor(currentZoomFactor);
824
+ }
825
+ catch { /* guest not ready; setNativeSimulatorViewBounds re-applies */ }
826
+ // Simulate this device's CSS env(safe-area-inset-*) on the fresh guest
827
+ // before it paints, so notch-aware page layout resolves correctly.
828
+ safeArea.applyToGuest(guestWc, ctx.bridge?.getDevice() ?? null);
829
+ guestWc.setWindowOpenHandler(({ url }) => handleWindowOpenExternal(url));
830
+ guestWc.on('will-navigate', (e, url) => {
831
+ try {
832
+ const u = new URL(url);
833
+ if (u.protocol === 'about:')
834
+ return;
835
+ if ((u.protocol === 'http:' || u.protocol === 'https:')
836
+ && (u.hostname === 'localhost' || u.hostname === '127.0.0.1')) {
837
+ return;
838
+ }
839
+ if (u.protocol === 'file:')
840
+ return;
841
+ e.preventDefault();
842
+ if (u.protocol === 'http:' || u.protocol === 'https:') {
843
+ void shell.openExternal(url);
844
+ }
845
+ }
846
+ catch {
847
+ e.preventDefault();
848
+ }
849
+ });
850
+ // A render-host guest only attaches after a spawn, so the service window
851
+ // exists by now — (re)point the right-panel DevTools at it. Belt-and-braces
852
+ // with the `onRenderEvent` path in case its emit lost the attach race.
853
+ followNativeDevtoolsServiceHost();
854
+ });
855
+ // The simulator loads http://localhost:<port>/simulator.html. Harden popups
856
+ // + in-place navigation: allow the dev server origin (and about:blank /
857
+ // file:// render hosts), route everything else to the OS browser.
858
+ simWc.setWindowOpenHandler(({ url }) => handleWindowOpenExternal(url));
859
+ simWc.on('will-navigate', (e, url) => {
860
+ try {
861
+ const u = new URL(url);
862
+ if (u.protocol === 'about:')
863
+ return;
864
+ if (u.protocol === 'file:')
865
+ return;
866
+ if ((u.protocol === 'http:' || u.protocol === 'https:')
867
+ && (u.hostname === 'localhost' || u.hostname === '127.0.0.1')) {
868
+ return;
869
+ }
870
+ e.preventDefault();
871
+ if (u.protocol === 'http:' || u.protocol === 'https:') {
872
+ void shell.openExternal(url);
873
+ }
874
+ }
875
+ catch {
876
+ e.preventDefault();
877
+ }
878
+ });
879
+ void simWc.loadURL(simulatorUrl).catch((err) => {
880
+ console.error('[workbench] attachNativeSimulator — loadURL failed', err);
881
+ });
882
+ // This WebContentsView's webContents IS the simulator: the native-host
883
+ // preload runs here and issues SPAWN, so bridge-router resolves
884
+ // ap.simulatorWc = event.sender = this wc, and SIMULATOR_EVENTS flow back.
885
+ simulatorWebContentsId = simWc.id;
886
+ // Forward this WCV's network requests (wx.request/download/upload run here,
887
+ // not in the service host) into the service-host console so they show in the
888
+ // embedded DevTools the right panel hosts. Attaches the CDP debugger; no-op
889
+ // if the forwarder is unwired (partial test ctx) or the debugger is claimed.
890
+ ctx.networkForward?.attachSimulator(simWc);
891
+ // Model A: bounds come ONLY from the renderer's measured inner-screen rect.
892
+ // If a report already landed before this attach (project-open ordering),
893
+ // replay it now — setNativeSimulatorViewBounds adds + sizes the view. Else
894
+ // the view stays unadded until the next reportBounds. No coarse fallback.
895
+ if (lastRendererRect) {
896
+ setNativeSimulatorViewBounds(lastRendererRect);
897
+ }
898
+ // Native-host page code (console.log / wx.request / Sources) runs in the
899
+ // hidden SERVICE HOST window, not this DeviceShell host nor the render-host
900
+ // guests. Keep the right-panel DevTools host view stable and point it at the
901
+ // service host so its Console/Network(fetch)/Sources reflect the logic layer.
902
+ // (The UI/view layer's Elements equivalent is the native WXML panel +
903
+ // render-guest highlight chain, which targets the active render guest.)
904
+ attachNativeSimulatorDevtoolsHost(simWidth);
905
+ }
169
906
  function detachSimulator() {
907
+ stopFollowingNativeServiceHost();
908
+ detachNativeCustomApiBridge();
909
+ // Stop Elements forwarding (detaches only the debugger sessions IT attached;
910
+ // safe-area's sessions are untouched). Idempotent — the host 'destroyed'
911
+ // handler may have already run.
912
+ try {
913
+ stopElementsForward?.();
914
+ }
915
+ catch { /* already stopped */ }
916
+ stopElementsForward = null;
917
+ // Stop forwarding the simulator WCV's network (its debugger is detached as
918
+ // the view is closed below, but drop our session deterministically first).
919
+ // Also drop the DevTools front-end host — its wc is destroyed below; a stale
920
+ // ref would make the forwarder dispatch into a dead wc instead of falling
921
+ // back to the console.
922
+ ctx.networkForward?.detachSimulator();
923
+ ctx.networkForward?.setDevtoolsHost(null);
924
+ // Native-host simulator content view (no-op in the default path).
925
+ if (nativeSimulatorView) {
926
+ if (nativeSimulatorViewAdded && !ctx.windows.mainWindow.isDestroyed()) {
927
+ try {
928
+ ctx.windows.mainWindow.contentView.removeChildView(nativeSimulatorView);
929
+ }
930
+ catch { /* already removed */ }
931
+ }
932
+ try {
933
+ if (!nativeSimulatorView.webContents.isDestroyed()) {
934
+ nativeSimulatorView.webContents.close();
935
+ }
936
+ }
937
+ catch { /* ignore */ }
938
+ nativeSimulatorView = null;
939
+ nativeSimulatorViewAdded = false;
940
+ }
170
941
  hidePopover();
171
942
  // Drop the settings view too — the previous detachAllViews() did.
172
943
  destroyViewInternal(settingsView);
@@ -179,6 +950,10 @@ export function createViewManager(ctx) {
179
950
  // Drop the renderer-published rect so a stale "hidden" override doesn't
180
951
  // suppress the next view before its renderer republishes.
181
952
  simulatorBoundsOverride = null;
953
+ // Also drop the cached native-simulator rect: attachNativeSimulator replays
954
+ // lastRendererRect on attach, so a leftover rect/offset from a torn-down
955
+ // session must not be replayed onto a fresh re-attach (stale slice/offset).
956
+ lastRendererRect = null;
182
957
  }
183
958
  function showSimulator(simWidth) {
184
959
  lastSimWidth = simWidth;
@@ -260,6 +1035,8 @@ export function createViewManager(ctx) {
260
1035
  }
261
1036
  }
262
1037
  function repositionAll() {
1038
+ // Native simulator: bounds owned by the renderer's reportBounds (Model A);
1039
+ // its window-resize listener re-measures, so no coarse re-apply here.
263
1040
  if (simulatorView && simulatorViewAdded)
264
1041
  applySimulatorBounds(lastSimWidth);
265
1042
  if (settingsView && settingsViewAdded)
@@ -269,9 +1046,67 @@ export function createViewManager(ctx) {
269
1046
  }
270
1047
  function disposeAll() {
271
1048
  detachSimulator();
1049
+ // Host-controllable toolbar view: removed from the contentView + its
1050
+ // WebContents closed (the host's loaded content is torn down on app exit).
1051
+ destroyViewInternal(hostToolbarView);
1052
+ hostToolbarView = null;
1053
+ hostToolbarViewAdded = false;
1054
+ safeArea.dispose();
1055
+ }
1056
+ function setNativeSimulatorViewBounds(params) {
1057
+ // Cache unconditionally so attachNativeSimulator can replay a report that
1058
+ // landed before the view existed (project-open ordering). This rect is the
1059
+ // ONLY authority for the native WCV bounds.
1060
+ lastRendererRect = params;
1061
+ if (ctx.windows.mainWindow.isDestroyed() || !nativeSimulatorView)
1062
+ return;
1063
+ const p = layout.computeNativeSimulatorViewParams(params, params.zoom);
1064
+ currentZoomFactor = p.zoomFactor;
1065
+ // A zero-area rect means "hide" — the renderer reports this when the
1066
+ // simulator panel/cell is toggled off or unmounts (toolbar toggle only
1067
+ // mutates renderer layout state, so without this the WCV would stay
1068
+ // painted over its old region). Mirror `setSimulatorDevtoolsBounds`:
1069
+ // remove the child view from the contentView but keep its WebContents
1070
+ // alive, so re-showing doesn't re-pay the simulator bootstrap.
1071
+ if (isHidden(p.bounds)) {
1072
+ if (nativeSimulatorViewAdded) {
1073
+ try {
1074
+ ctx.windows.mainWindow.contentView.removeChildView(nativeSimulatorView);
1075
+ }
1076
+ catch { /* already removed */ }
1077
+ nativeSimulatorViewAdded = false;
1078
+ }
1079
+ return;
1080
+ }
1081
+ if (!nativeSimulatorViewAdded) {
1082
+ ctx.windows.mainWindow.contentView.addChildView(nativeSimulatorView);
1083
+ nativeSimulatorViewAdded = true;
1084
+ }
1085
+ nativeSimulatorView.setBounds(p.bounds);
1086
+ const simWc = nativeSimulatorView.webContents;
1087
+ if (!simWc.isDestroyed()) {
1088
+ simWc.setZoomFactor(p.zoomFactor);
1089
+ }
1090
+ // Propagate zoom to any already-attached nested render-host guests so the
1091
+ // page rescales live on zoom change (newly-attached guests get it in
1092
+ // did-attach-webview). `webContents.getAllWebContents()` includes guests;
1093
+ // filter to those hosted by this simulator wc.
1094
+ try {
1095
+ for (const wc of webContents.getAllWebContents()) {
1096
+ if (wc.isDestroyed())
1097
+ continue;
1098
+ if (wc.hostWebContents === simWc) {
1099
+ wc.setZoomFactor(p.zoomFactor);
1100
+ }
1101
+ }
1102
+ }
1103
+ catch { /* hostWebContents unavailable; guests get zoom on attach */ }
272
1104
  }
273
1105
  function resize(simWidth) {
274
1106
  lastSimWidth = simWidth;
1107
+ // Native simulator bounds are owned solely by the renderer's reportBounds
1108
+ // (Model A) — its ResizeObserver/window-resize listeners re-measure on this
1109
+ // same resize. No coarse panel-size re-apply here (it raced the precise rect).
275
1110
  if (simulatorViewAdded)
276
1111
  applySimulatorBounds(simWidth);
277
1112
  if (settingsViewAdded)
@@ -290,7 +1125,9 @@ export function createViewManager(ctx) {
290
1125
  }
291
1126
  return {
292
1127
  attachSimulator,
1128
+ attachNativeSimulator,
293
1129
  detachSimulator,
1130
+ reapplySafeArea: (device) => safeArea.reapplyAll(device),
294
1131
  showSimulator,
295
1132
  hideSimulator,
296
1133
  showSettings,
@@ -330,9 +1167,20 @@ export function createViewManager(ctx) {
330
1167
  return null;
331
1168
  return popoverView.webContents.id;
332
1169
  },
1170
+ getHostToolbarWebContentsId: () => {
1171
+ if (!hostToolbarView)
1172
+ return null;
1173
+ if (hostToolbarView.webContents.isDestroyed())
1174
+ return null;
1175
+ return hostToolbarView.webContents.id;
1176
+ },
1177
+ setNativeSimulatorViewBounds,
333
1178
  resize,
334
1179
  setVisible,
335
1180
  setSimulatorDevtoolsBounds,
1181
+ setHostToolbarBounds,
1182
+ setHostToolbarHeight,
1183
+ hostToolbar,
336
1184
  };
337
1185
  }
338
1186
  //# sourceMappingURL=view-manager.js.map