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

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 (50) hide show
  1. package/dist/main/app/app.js +11 -0
  2. package/dist/main/app/max-listeners-diagnostic.d.ts +32 -0
  3. package/dist/main/app/max-listeners-diagnostic.js +59 -0
  4. package/dist/main/index.bundle.js +423 -220
  5. package/dist/main/ipc/bridge-router.d.ts +5 -4
  6. package/dist/main/ipc/bridge-router.js +25 -1
  7. package/dist/main/ipc/simulator.js +1 -5
  8. package/dist/main/ipc/views.js +8 -19
  9. package/dist/main/services/automation/wait-active-page.d.ts +4 -2
  10. package/dist/main/services/render-inspect/index.d.ts +9 -0
  11. package/dist/main/services/render-inspect/index.js +13 -1
  12. package/dist/main/services/simulator-storage/index.d.ts +9 -0
  13. package/dist/main/services/simulator-storage/index.js +26 -1
  14. package/dist/main/services/simulator-wxml/index.js +117 -11
  15. package/dist/main/services/views/apply-view-ops.d.ts +11 -0
  16. package/dist/main/services/views/apply-view-ops.js +22 -0
  17. package/dist/main/services/views/placement-test-driver.d.ts +25 -0
  18. package/dist/main/services/views/placement-test-driver.js +48 -0
  19. package/dist/main/services/views/view-manager.d.ts +9 -55
  20. package/dist/main/services/views/view-manager.js +264 -233
  21. package/dist/main/services/workbench-context.d.ts +10 -0
  22. package/dist/preload/windows/host-toolbar-runtime.cjs +6 -13
  23. package/dist/preload/windows/host-toolbar-runtime.cjs.map +2 -2
  24. package/dist/preload/windows/simulator.cjs +0 -3
  25. package/dist/preload/windows/simulator.cjs.map +2 -2
  26. package/dist/preload/windows/simulator.js +0 -3
  27. package/dist/render-host/render-inspect.js +35 -1
  28. package/dist/renderer/assets/index-jDrNHghH.js +49 -0
  29. package/dist/renderer/assets/{input-DR7bfyQE.js → input-Djg2NR4U.js} +2 -2
  30. package/dist/renderer/assets/{ipc-transport-3Arg8jPC.js → ipc-transport-Be-AzCub.js} +2 -2
  31. package/dist/renderer/assets/ipc-transport-C6-KPMqw.css +1 -0
  32. package/dist/renderer/assets/{popover-CPZX3MrT.js → popover-eDUh4NYe.js} +2 -2
  33. package/dist/renderer/assets/{select-BxyPlVyJ.js → select-DesFfkVV.js} +2 -2
  34. package/dist/renderer/assets/{settings-CG_OR03F.js → settings-CRmO7zUG.js} +2 -2
  35. package/dist/renderer/assets/{settings-api-CtPPJF-F.js → settings-api-HQ7ZFPWi.js} +2 -2
  36. package/dist/renderer/assets/{workbenchSettings-ClqCBGOa.js → workbenchSettings-CZBWsliT.js} +2 -2
  37. package/dist/renderer/entries/main/index.html +6 -6
  38. package/dist/renderer/entries/popover/index.html +5 -5
  39. package/dist/renderer/entries/settings/index.html +5 -5
  40. package/dist/renderer/entries/workbench-settings/index.html +4 -4
  41. package/dist/service-host/sync-api-patch.js +33 -3
  42. package/dist/shared/ipc-channels.d.ts +28 -14
  43. package/dist/shared/ipc-channels.js +10 -17
  44. package/dist/shared/ipc-schemas.d.ts +21 -19
  45. package/dist/shared/ipc-schemas.js +32 -26
  46. package/dist/shared/view-ids.d.ts +19 -0
  47. package/dist/shared/view-ids.js +25 -0
  48. package/package.json +5 -5
  49. package/dist/renderer/assets/index-HE9Xr9bQ.js +0 -49
  50. package/dist/renderer/assets/ipc-transport-Dt0jR7Ir.css +0 -1
@@ -17,6 +17,9 @@ import { acquireHostToolbarSessionRuntime, releaseHostToolbarSessionRuntime, } f
17
17
  import { createHostToolbarPortChannel, } from './host-toolbar-port-channel.js';
18
18
  import { parseRoute } from '../../../shared/simulator-route.js';
19
19
  import { HEADER_H, HOST_TOOLBAR_RUNTIME_MARKER } from '../../../shared/constants.js';
20
+ import { reconcile, createInitialState } from '@dimina-kit/electron-deck/layout';
21
+ import { applyViewOps } from './apply-view-ops.js';
22
+ import { VIEW_ID, VIEW_LAYER } from '../../../shared/view-ids.js';
20
23
  /**
21
24
  * Resolve a DevTools source request against the service-host URL that created
22
25
  * the inspected app. Its pkgRoot is authoritative; the workspace is only a
@@ -46,6 +49,32 @@ export function resolveProjectEditorTarget(serviceHostUrl, activeProjectRoot, re
46
49
  column: typeof req.column === 'number' ? req.column + 1 : undefined,
47
50
  };
48
51
  }
52
+ // `webContents.executeJavaScript()` registers a transient `did-stop-loading`
53
+ // listener on the target wc whenever that wc is still loading (Electron's
54
+ // `waitTillCanExecuteJavaScript` defers the eval until load finishes, then
55
+ // removes the listener). The DevTools front-end host wc takes a burst of injects
56
+ // during its boot window — tab customization, default-panel selection, the 150ms
57
+ // Elements-forward reconcile, and Network-forward probing all call
58
+ // `executeJavaScript()` on it before the front-end has finished loading — so the
59
+ // pending listeners can transiently exceed Node's default EventEmitter ceiling
60
+ // of 10 and print a spurious `MaxListenersExceededWarning: 11 did-stop-loading
61
+ // listeners`. They drain once the wc stops loading; this is a boot-time
62
+ // concurrency spike, not a per-event leak. Raise the ceiling on the DevTools
63
+ // host wc, which we knowingly inject into repeatedly during its boot window, so
64
+ // the benign spike stays quiet. (Confirmed at runtime: the tripped emitter is
65
+ // this host wc's `did-stop-loading`, count 11 — see the `process.on('warning')`
66
+ // diagnostic in app bootstrap.)
67
+ const DEVTOOLS_HOST_EXEC_JS_MAX_LISTENERS = 50;
68
+ function raiseExecuteJavaScriptListenerCeiling(wc) {
69
+ try {
70
+ if (wc.isDestroyed())
71
+ return;
72
+ if (wc.getMaxListeners() >= DEVTOOLS_HOST_EXEC_JS_MAX_LISTENERS)
73
+ return;
74
+ wc.setMaxListeners(DEVTOOLS_HOST_EXEC_JS_MAX_LISTENERS);
75
+ }
76
+ catch { /* torn-down / stub wc — best effort, warning is non-fatal */ }
77
+ }
49
78
  /**
50
79
  * Build a ViewManager bound to the given context. The returned object is the
51
80
  * only component allowed to instantiate or add/remove overlay WebContentsViews.
@@ -69,12 +98,6 @@ export function createViewManager(ctx) {
69
98
  let nativeSimulatorViewAdded = false;
70
99
  let nativeSimulatorProjectPath = null;
71
100
  let settleNativeSimulatorReady = null;
72
- // Model A: the renderer's measured inner-screen rect is the SOLE authority for
73
- // the native simulator WCV bounds (see docs/simulator-render-architecture.md).
74
- // Cache the last rect so a report that lands before attachNativeSimulator (the
75
- // project-open ordering) is not lost — attach replays it instead of using a
76
- // coarse panel-size fallback (which raced and caused the clip/surround flips).
77
- let lastRendererRect = null;
78
101
  // NATIVE-HOST ONLY. The `ipcMain.on` listener that services the
79
102
  // `__diminaCustomApis` bridge for the current native simulator webContents
80
103
  // (see `attachNativeCustomApiBridge`). Tracked so we can remove it before
@@ -87,7 +110,6 @@ export function createViewManager(ctx) {
87
110
  // pick up the correct scale in `did-attach-webview`. Defaults to 1 (100%).
88
111
  let currentZoomFactor = 1;
89
112
  let settingsView = null;
90
- let settingsViewAdded = false;
91
113
  let popoverView = null;
92
114
  let simulatorWebContentsId = null;
93
115
  // NATIVE-HOST ONLY. The webContents the right-panel Chrome DevTools front-end
@@ -111,17 +133,11 @@ export function createViewManager(ctx) {
111
133
  let stopServiceConsole = null;
112
134
  let nativeDevtoolsRetryTimer = null;
113
135
  let nativeDevtoolsRetryToken = 0;
114
- // Renderer-driven overlay bounds for the simulator DevTools view — the SOLE
115
- // mount/geometry authority (no static-layout fallback). A zero-area
116
- // rectangle means "hide" — the overlay is removed from the contentView but
117
- // its WebContents stays alive.
118
- let simulatorBoundsOverride = null;
119
136
  // ── Embedded workbench editor WebContentsView ────────────────────────
120
137
  // The opt-in VS Code workbench hosting the 'editor' dock slot. Lazily created
121
138
  // by `attachWorkbench` from the COI server URL; its bounds ride the renderer
122
139
  // 'editor'-slot anchor (forward anchor, like the simulator DevTools overlay).
123
140
  let workbenchView = null;
124
- let workbenchViewAdded = false;
125
141
  // Whether the devtools-theme → workbench-theme `nativeTheme` listener is live.
126
142
  // Bound lazily on first workbench attach, removed on detach.
127
143
  let workbenchThemeSyncBound = false;
@@ -178,37 +194,177 @@ export function createViewManager(ctx) {
178
194
  }
179
195
  catch { /* ignore */ }
180
196
  }
181
- // Renderer publishes width/height = 0 to mean "hide overlay" — the
182
- // surrounding React panel is unmounted/collapsed. We keep the cached
183
- // value (so future republishes win over legacy layout) but remove the
184
- // child view from the contentView until a non-empty rect arrives.
185
- function isHidden(b) {
186
- return b.width <= 0 || b.height <= 0;
197
+ // ── Placement reconciler ────────────────────────────────────────────────
198
+ // The main-process view tree is converged toward the renderer's declared
199
+ // placement by a level-triggered reconciler (docs/view-placement-reconciler.md).
200
+ // The renderer publishes a window-level snapshot into `baseDesired`;
201
+ // settings/popover are main-owned and live in `overlayDesired`. Any change
202
+ // merges the two, runs the pure reconcile core, and applies the ordered ops
203
+ // through `viewTarget`. `epochCounter` is a single monotonic tick — main is the
204
+ // only (serial) reconcile caller, so the core's stale guard passes by
205
+ // construction; `rendererGeneration` still drives the reset on renderer restart.
206
+ let placementState = createInitialState();
207
+ let epochCounter = 0;
208
+ let rendererGeneration = 0;
209
+ const baseDesired = new Map();
210
+ const overlayDesired = new Map();
211
+ // simulator / simulator-devtools WCVs are built by the project-open flow, not
212
+ // by an attach op; gate them hidden until they exist so the reconciler never
213
+ // records an attach that addChildView can't perform. The view-creation sites
214
+ // call reconcileNow() to re-open the gate. workbench is gated until it has a
215
+ // source or a live view (it lazy-creates from the URL in target.attach).
216
+ function gateReadiness(v) {
217
+ if (v.viewId === VIEW_ID.simulator && !nativeSimulatorView)
218
+ return { ...v, placement: { visible: false } };
219
+ if (v.viewId === VIEW_ID.simulatorDevtools && !simulatorView)
220
+ return { ...v, placement: { visible: false } };
221
+ if (v.viewId === VIEW_ID.workbench && !workbenchView && !workbenchUrl)
222
+ return { ...v, placement: { visible: false } };
223
+ return v;
187
224
  }
188
- function setSimulatorDevtoolsBounds(bounds) {
189
- simulatorBoundsOverride = bounds;
190
- if (!simulatorView || simulatorView.webContents.isDestroyed())
191
- return;
192
- if (ctx.windows.mainWindow.isDestroyed())
193
- return;
194
- if (isHidden(bounds)) {
195
- if (simulatorViewAdded) {
225
+ function resolveView(viewId) {
226
+ switch (viewId) {
227
+ case VIEW_ID.simulator: return nativeSimulatorView;
228
+ case VIEW_ID.simulatorDevtools: return simulatorView;
229
+ case VIEW_ID.workbench: return workbenchView;
230
+ case VIEW_ID.hostToolbar: return hostToolbarView;
231
+ case VIEW_ID.settings: return settingsView;
232
+ case VIEW_ID.popover: return popoverView;
233
+ default: return null;
234
+ }
235
+ }
236
+ function markAdded(viewId, added) {
237
+ switch (viewId) {
238
+ case VIEW_ID.simulator:
239
+ nativeSimulatorViewAdded = added;
240
+ break;
241
+ case VIEW_ID.simulatorDevtools:
242
+ simulatorViewAdded = added;
243
+ break;
244
+ case VIEW_ID.hostToolbar:
245
+ hostToolbarViewAdded = added;
246
+ break;
247
+ // popover has no added-flag (create == added).
248
+ }
249
+ }
250
+ // simulator zoom rides in `extra`: setBounds also drives the WCV zoomFactor and
251
+ // propagates it to nested render-host guests (from the old
252
+ // setNativeSimulatorViewBounds body).
253
+ function applyNativeSimulatorBounds(view, bounds, zoom) {
254
+ const p = layout.computeNativeSimulatorViewParams(bounds, zoom);
255
+ currentZoomFactor = p.zoomFactor;
256
+ view.setBounds(p.bounds);
257
+ const simWc = view.webContents;
258
+ if (!simWc.isDestroyed())
259
+ simWc.setZoomFactor(p.zoomFactor);
260
+ try {
261
+ for (const wc of webContents.getAllWebContents()) {
262
+ if (wc.isDestroyed())
263
+ continue;
264
+ if (wc.hostWebContents === simWc)
265
+ wc.setZoomFactor(p.zoomFactor);
266
+ }
267
+ }
268
+ catch { /* hostWebContents unavailable; guests get zoom on attach */ }
269
+ }
270
+ const viewTarget = {
271
+ attach(viewId) {
272
+ if (ctx.windows.mainWindow.isDestroyed())
273
+ return;
274
+ if (viewId === VIEW_ID.workbench && !workbenchView && workbenchUrl) {
275
+ // Lazy-load the workbench; attachWorkbench adds it + sets the flag.
276
+ void attachWorkbench(workbenchUrl);
277
+ return;
278
+ }
279
+ if (viewId === VIEW_ID.hostToolbar) {
280
+ const view = ensureHostToolbarView();
281
+ ctx.windows.mainWindow.contentView.addChildView(view);
282
+ hostToolbarViewAdded = true;
283
+ return;
284
+ }
285
+ const view = resolveView(viewId);
286
+ if (!view)
287
+ return;
288
+ ctx.windows.mainWindow.contentView.addChildView(view);
289
+ markAdded(viewId, true);
290
+ },
291
+ detach(viewId) {
292
+ const view = resolveView(viewId);
293
+ if (view && !ctx.windows.mainWindow.isDestroyed()) {
196
294
  try {
197
- ctx.windows.mainWindow.contentView.removeChildView(simulatorView);
295
+ ctx.windows.mainWindow.contentView.removeChildView(view);
198
296
  }
199
297
  catch { /* already removed */ }
200
- simulatorViewAdded = false;
201
298
  }
202
- return;
203
- }
204
- if (!simulatorViewAdded) {
205
- ctx.windows.mainWindow.contentView.addChildView(simulatorView);
206
- simulatorViewAdded = true;
207
- // Re-attaching this base overlay moved it to the top of the z-stack; keep
208
- // any open settings/popover above it.
209
- raiseTopOverlays();
210
- }
211
- simulatorView.setBounds(bounds);
299
+ markAdded(viewId, false);
300
+ },
301
+ setBounds(viewId, bounds, extra) {
302
+ const view = resolveView(viewId);
303
+ if (!view || view.webContents.isDestroyed())
304
+ return;
305
+ if (viewId === VIEW_ID.simulator) {
306
+ applyNativeSimulatorBounds(view, bounds, extra?.zoom ?? 1);
307
+ return;
308
+ }
309
+ view.setBounds(bounds);
310
+ },
311
+ setVisible(viewId, visible) {
312
+ const view = resolveView(viewId);
313
+ if (!view)
314
+ return;
315
+ try {
316
+ view.setVisible(visible);
317
+ }
318
+ catch { /* stub may lack setVisible */ }
319
+ },
320
+ reorder(order) {
321
+ // A single attached view is already in place — nothing to reorder.
322
+ if (order.length <= 1 || ctx.windows.mainWindow.isDestroyed())
323
+ return;
324
+ const cv = ctx.windows.mainWindow.contentView;
325
+ for (const viewId of order) {
326
+ const view = resolveView(viewId);
327
+ if (view) {
328
+ try {
329
+ cv.addChildView(view);
330
+ }
331
+ catch { /* already attached */ }
332
+ }
333
+ }
334
+ },
335
+ };
336
+ // Create the lazily-built views (host-toolbar, workbench) BEFORE reconcile so
337
+ // the setBounds op — which the core emits before attach — lands on a live view
338
+ // rather than a not-yet-created one.
339
+ function ensureLazyViews() {
340
+ const ht = baseDesired.get(VIEW_ID.hostToolbar);
341
+ if (ht?.placement.visible && !liveHostToolbarWebContents())
342
+ ensureHostToolbarView();
343
+ const wb = baseDesired.get(VIEW_ID.workbench);
344
+ if (wb?.placement.visible && !workbenchView && workbenchUrl)
345
+ void attachWorkbench(workbenchUrl);
346
+ }
347
+ function reconcileNow() {
348
+ ensureLazyViews();
349
+ const views = [];
350
+ for (const v of baseDesired.values())
351
+ views.push(gateReadiness(v));
352
+ for (const v of overlayDesired.values())
353
+ views.push(v);
354
+ const result = reconcile(placementState, {
355
+ generation: rendererGeneration,
356
+ epoch: ++epochCounter,
357
+ views,
358
+ });
359
+ placementState = result.state;
360
+ applyViewOps(result.ops, viewTarget);
361
+ }
362
+ function setPlacementSnapshot(snapshot) {
363
+ rendererGeneration = snapshot.generation;
364
+ baseDesired.clear();
365
+ for (const v of snapshot.views)
366
+ baseDesired.set(v.viewId, v);
367
+ reconcileNow();
212
368
  }
213
369
  // ── Embedded workbench editor WebContentsView ────────────────────────
214
370
  /** Current devtools color scheme, mirrored into the workbench's theme. */
@@ -257,9 +413,6 @@ export function createViewManager(ctx) {
257
413
  }
258
414
  catch { /* stub may lack it */ }
259
415
  ctx.windows.mainWindow.contentView.addChildView(view);
260
- workbenchViewAdded = true;
261
- // Keep settings/popover above the freshly-added base overlay.
262
- raiseTopOverlays();
263
416
  // Hand the workbench the current devtools scheme as a URL query so its very
264
417
  // first paint already matches (the runtime setter only exists post-init).
265
418
  const loadUrl = `${url}index.html?theme=${workbenchThemeScheme()}`;
@@ -271,40 +424,6 @@ export function createViewManager(ctx) {
271
424
  function setWorkbenchSource(url) {
272
425
  workbenchUrl = url;
273
426
  }
274
- function setWorkbenchBounds(bounds) {
275
- if (ctx.windows.mainWindow.isDestroyed())
276
- return;
277
- // Zero-area rect means "hide" — remove the child view but keep its
278
- // WebContents (and the workbench's loaded state) alive. Never triggers the
279
- // lazy load: a hidden slot must not pull the workbench onto screen.
280
- if (isHidden(bounds)) {
281
- if (workbenchView && workbenchViewAdded && !workbenchView.webContents.isDestroyed()) {
282
- try {
283
- ctx.windows.mainWindow.contentView.removeChildView(workbenchView);
284
- }
285
- catch { /* already removed */ }
286
- workbenchViewAdded = false;
287
- }
288
- return;
289
- }
290
- // First time the 'editor' slot becomes visible: lazily create + load the
291
- // workbench (off the boot critical path). attachWorkbench assigns
292
- // workbenchView + addChildView synchronously, so bounds apply immediately
293
- // while the bundle loads in the background.
294
- if (!workbenchView && workbenchUrl) {
295
- void attachWorkbench(workbenchUrl);
296
- }
297
- if (!workbenchView || workbenchView.webContents.isDestroyed())
298
- return;
299
- if (!workbenchViewAdded) {
300
- ctx.windows.mainWindow.contentView.addChildView(workbenchView);
301
- workbenchViewAdded = true;
302
- // Re-attaching this base overlay moved it to the top of the z-stack; keep
303
- // any open settings/popover above it.
304
- raiseTopOverlays();
305
- }
306
- workbenchView.setBounds(bounds);
307
- }
308
427
  function detachWorkbench() {
309
428
  if (workbenchThemeSyncBound) {
310
429
  nativeTheme.removeListener('updated', pushWorkbenchTheme);
@@ -312,7 +431,8 @@ export function createViewManager(ctx) {
312
431
  }
313
432
  destroyViewInternal(workbenchView);
314
433
  workbenchView = null;
315
- workbenchViewAdded = false;
434
+ baseDesired.delete(VIEW_ID.workbench);
435
+ reconcileNow();
316
436
  }
317
437
  // Build the `file:///workspace/<rel>` URI string with each path SEGMENT
318
438
  // percent-encoded. A raw `rel` passed to `vscode.Uri.parse` mis-parses a
@@ -412,6 +532,9 @@ export function createViewManager(ctx) {
412
532
  catch { /* already removed */ }
413
533
  hostToolbarViewAdded = false;
414
534
  }
535
+ // The instance is being replaced — forget its reconciled mount state so the
536
+ // rebuilt view is treated as a fresh attach (not skipped as already-attached).
537
+ placementState.actual.delete(VIEW_ID.hostToolbar);
415
538
  // The framework's height-advertiser runtime is SESSION-resident: register
416
539
  // it on session.defaultSession (ref-counted across coexisting managers)
417
540
  // BEFORE the view exists, so the very first load already runs it. The
@@ -457,31 +580,6 @@ export function createViewManager(ctx) {
457
580
  catch { /* stub may lack it */ }
458
581
  return view;
459
582
  }
460
- function setHostToolbarBounds(bounds) {
461
- if (ctx.windows.mainWindow.isDestroyed())
462
- return;
463
- // Zero-area rect means "hide" — remove the child view but keep its
464
- // WebContents (and the host's loaded content) alive. Do NOT create the
465
- // view just to immediately hide it.
466
- if (isHidden(bounds)) {
467
- if (hostToolbarView && hostToolbarViewAdded && liveHostToolbarWebContents()) {
468
- try {
469
- ctx.windows.mainWindow.contentView.removeChildView(hostToolbarView);
470
- }
471
- catch { /* already removed */ }
472
- }
473
- hostToolbarViewAdded = false;
474
- return;
475
- }
476
- const view = ensureHostToolbarView();
477
- if (!hostToolbarViewAdded) {
478
- // addChildView appends = topmost z-order, which is correct for a strip
479
- // that sits above the devtools header.
480
- ctx.windows.mainWindow.contentView.addChildView(view);
481
- hostToolbarViewAdded = true;
482
- }
483
- view.setBounds(bounds);
484
- }
485
583
  // Single funnel for the height notify: retain-then-push, so the retained
486
584
  // value is exactly the last value the renderer was told. Every height
487
585
  // notify site MUST go through here — the renderer pulls the retained value
@@ -490,9 +588,9 @@ export function createViewManager(ctx) {
490
588
  function notifyHostToolbarHeight(height) {
491
589
  hostToolbarLastHeight = height;
492
590
  ctx.notify.hostToolbarHeightChanged(height);
493
- if (settingsViewAdded)
591
+ if (overlayDesired.has(VIEW_ID.settings))
494
592
  applySettingsBounds();
495
- if (popoverView)
593
+ if (overlayDesired.has(VIEW_ID.popover))
496
594
  applyPopoverBounds();
497
595
  }
498
596
  function setHostToolbarHeight(extent) {
@@ -512,13 +610,12 @@ export function createViewManager(ctx) {
512
610
  notifyHostToolbarHeight(extent);
513
611
  }
514
612
  function hideHostToolbar() {
515
- if (hostToolbarView && hostToolbarViewAdded && !ctx.windows.mainWindow.isDestroyed()) {
516
- try {
517
- ctx.windows.mainWindow.contentView.removeChildView(hostToolbarView);
518
- }
519
- catch { /* already removed */ }
520
- }
521
- hostToolbarViewAdded = false;
613
+ baseDesired.set(VIEW_ID.hostToolbar, {
614
+ viewId: VIEW_ID.hostToolbar,
615
+ placement: { visible: false },
616
+ layer: VIEW_LAYER.hostToolbar,
617
+ });
618
+ reconcileNow();
522
619
  // Collapse the renderer placeholder to 0 too. Otherwise its anchor keeps a
523
620
  // non-zero reserved height and re-publishes bounds on the next window
524
621
  // resize, silently re-adding the view we just hid (unstable hide). Zeroing
@@ -590,17 +687,36 @@ export function createViewManager(ctx) {
590
687
  function overlayHeaderHeight() {
591
688
  return HEADER_H + hostToolbarLastHeight;
592
689
  }
690
+ // settings/popover bounds are main-computed; publish them into overlayDesired
691
+ // and let the reconciler place them (they are top-tier layers, so a reorder
692
+ // keeps them above every base overlay — the old raiseTopOverlays is gone).
593
693
  function applySettingsBounds() {
594
694
  if (!settingsView || ctx.windows.mainWindow.isDestroyed())
595
695
  return;
596
696
  const [w = 0, h = 0] = ctx.windows.mainWindow.getContentSize();
597
- settingsView.setBounds(layout.computeSettingsBounds(w, h, overlayHeaderHeight()));
697
+ overlayDesired.set(VIEW_ID.settings, {
698
+ viewId: VIEW_ID.settings,
699
+ placement: {
700
+ visible: true,
701
+ bounds: layout.computeSettingsBounds(w, h, overlayHeaderHeight()),
702
+ },
703
+ layer: VIEW_LAYER.settings,
704
+ });
705
+ reconcileNow();
598
706
  }
599
707
  function applyPopoverBounds() {
600
708
  if (!popoverView || ctx.windows.mainWindow.isDestroyed())
601
709
  return;
602
710
  const [w = 0, h = 0] = ctx.windows.mainWindow.getContentSize();
603
- popoverView.setBounds(layout.computePopoverBounds(w, h, overlayHeaderHeight()));
711
+ overlayDesired.set(VIEW_ID.popover, {
712
+ viewId: VIEW_ID.popover,
713
+ placement: {
714
+ visible: true,
715
+ bounds: layout.computePopoverBounds(w, h, overlayHeaderHeight()),
716
+ },
717
+ layer: VIEW_LAYER.popover,
718
+ });
719
+ reconcileNow();
604
720
  }
605
721
  function clearNativeDevtoolsRetry() {
606
722
  nativeDevtoolsRetryToken++;
@@ -736,7 +852,7 @@ export function createViewManager(ctx) {
736
852
  ctx.notify.editorOpenFile(target);
737
853
  };
738
854
  serviceWc.on('devtools-open-url', onOpenUrl);
739
- // Consolidate teardown onto the connection layer (foundation.md §4 / P2),
855
+ // Consolidate teardown onto the connection layer,
740
856
  // but as a wc-LIFETIME resource: the open-in-editor wiring inspects this
741
857
  // service-host wc and must SURVIVE pool reuse (`reset`) — the dedup set
742
858
  // entry + listener stay valid across sessions reusing the same wc, and the
@@ -871,6 +987,11 @@ export function createViewManager(ctx) {
871
987
  // request the `console` view, and also persist the choice via the
872
988
  // `panel-selectedTab` localStorage key so subsequent reloads honor it.
873
989
  const devtoolsWc = simulatorView.webContents;
990
+ // The boot-time burst of `executeJavaScript()` injects on this host wc (tab
991
+ // customization / console default / Elements+Network forwarding below) each
992
+ // queue a pending `did-stop-loading` waiter while the front-end is still
993
+ // loading (see raiseExecuteJavaScriptListenerCeiling).
994
+ raiseExecuteJavaScriptListenerCeiling(devtoolsWc);
874
995
  // Hand the network forwarder THIS front-end host wc — it injects the
875
996
  // simulator WCV's Network.* CDP events into `window.DevToolsAPI.dispatchMessage`
876
997
  // here so the native Network tab renders them (falls back to the service-host
@@ -936,11 +1057,9 @@ export function createViewManager(ctx) {
936
1057
  // unadded and unsized until the first publish arrives. No static-layout
937
1058
  // fallback — an attach-time computed rect raced the precise anchor rect
938
1059
  // and flashed the overlay at the wrong rectangle.
939
- if (simulatorBoundsOverride && !isHidden(simulatorBoundsOverride)) {
940
- ctx.windows.mainWindow.contentView.addChildView(simulatorView);
941
- simulatorViewAdded = true;
942
- simulatorView.setBounds(simulatorBoundsOverride);
943
- }
1060
+ // The view now exists: re-run the reconciler so a placement published before
1061
+ // this attach (project-open ordering) attaches it once the gate re-opens.
1062
+ reconcileNow();
944
1063
  if (ctx.bridge?.isNativeHost()) {
945
1064
  unsubscribeNativeRenderEvents = ctx.bridge.onRenderEvent(onNativeRenderEvent);
946
1065
  followNativeDevtoolsServiceHost();
@@ -988,6 +1107,8 @@ export function createViewManager(ctx) {
988
1107
  catch { /* ignore */ }
989
1108
  nativeSimulatorView = null;
990
1109
  nativeSimulatorViewAdded = false;
1110
+ // The view is gone: gateReadiness now hides it, so reconcile detaches it.
1111
+ reconcileNow();
991
1112
  }
992
1113
  /**
993
1114
  * NATIVE-HOST ONLY. The native simulator is a top-level WebContentsView with
@@ -1033,8 +1154,8 @@ export function createViewManager(ctx) {
1033
1154
  };
1034
1155
  nativeCustomApiBridgeHandler = handler;
1035
1156
  ipcMain.on(SimulatorCustomApiBridgeChannel.Request, handler);
1036
- // Consolidate this per-webContents teardown onto the connection layer
1037
- // (foundation.md §4 / P1 DoD #3): acquiring the simWc connection makes the
1157
+ // Consolidate this per-webContents teardown onto the connection layer:
1158
+ // acquiring the simWc connection makes the
1038
1159
  // registry track this trusted webContents, and `own()`-ing the detach ties
1039
1160
  // the ipcMain listener's lifetime to the connection so it's torn down
1040
1161
  // deterministically when the wc is destroyed (or the connection is reset),
@@ -1063,8 +1184,8 @@ export function createViewManager(ctx) {
1063
1184
  settleNativeSimulatorReady = resolve;
1064
1185
  });
1065
1186
  // Derive THIS project's session partition from the simulator URL's appId so
1066
- // its cookies/localStorage/cache are isolated from every other project (P0
1067
- // debt). Same project → same partition (storage survives a relaunch);
1187
+ // its cookies/localStorage/cache are isolated from every other project.
1188
+ // Same project → same partition (storage survives a relaunch);
1068
1189
  // unknown appId → the shared fallback. Configure the partition's session
1069
1190
  // (protocol handlers + CORS/referer policy) before any project content loads
1070
1191
  // on it — idempotent per partition.
@@ -1233,13 +1354,11 @@ export function createViewManager(ctx) {
1233
1354
  // embedded DevTools the right panel hosts. Attaches the CDP debugger; no-op
1234
1355
  // if the forwarder is unwired (partial test ctx) or the debugger is claimed.
1235
1356
  ctx.networkForward?.attachSimulator(simWc);
1236
- // Model A: bounds come ONLY from the renderer's measured inner-screen rect.
1237
- // If a report already landed before this attach (project-open ordering),
1238
- // replay it now setNativeSimulatorViewBounds adds + sizes the view. Else
1239
- // the view stays unadded until the next reportBounds. No coarse fallback.
1240
- if (lastRendererRect) {
1241
- setNativeSimulatorViewBounds(lastRendererRect);
1242
- }
1357
+ // Model A: bounds come ONLY from the renderer's window-level placement
1358
+ // snapshot. The view now exists, so re-reconcile: gateReadiness admits the
1359
+ // simulator (gated hidden while its WCV was absent) and the latest desired
1360
+ // placement attaches + sizes it. Until the next snapshot it stays unadded.
1361
+ reconcileNow();
1243
1362
  // Native-host page code (console.log / wx.request / Sources) runs in the
1244
1363
  // hidden SERVICE HOST window, not this DeviceShell host nor the render-host
1245
1364
  // guests. Keep the right-panel DevTools host view stable and point it at the
@@ -1277,19 +1396,11 @@ export function createViewManager(ctx) {
1277
1396
  // Drop the settings view too — the previous detachAllViews() did.
1278
1397
  destroyViewInternal(settingsView);
1279
1398
  settingsView = null;
1280
- settingsViewAdded = false;
1281
1399
  destroyViewInternal(simulatorView);
1282
1400
  simulatorView = null;
1283
1401
  simulatorViewAdded = false;
1284
1402
  simulatorWebContentsId = null;
1285
1403
  nativeSimulatorProjectPath = null;
1286
- // Drop the renderer-published rect so a stale "hidden" override doesn't
1287
- // suppress the next view before its renderer republishes.
1288
- simulatorBoundsOverride = null;
1289
- // Also drop the cached native-simulator rect: attachNativeSimulator replays
1290
- // lastRendererRect on attach, so a leftover rect/offset from a torn-down
1291
- // session must not be replayed onto a fresh re-attach (stale slice/offset).
1292
- lastRendererRect = null;
1293
1404
  }
1294
1405
  // Remove (but do not destroy) the DevTools overlay from the contentView.
1295
1406
  // Internal teardown helper for re-attach; user-facing visibility is the
@@ -1325,39 +1436,11 @@ export function createViewManager(ctx) {
1325
1436
  settingsView.setBackgroundColor('#00000000');
1326
1437
  await settingsView.webContents.loadFile(path.join(ctx.rendererDir, 'entries/settings/index.html'));
1327
1438
  }
1328
- if (!settingsViewAdded) {
1329
- ctx.windows.mainWindow.contentView.addChildView(settingsView);
1330
- settingsViewAdded = true;
1331
- }
1332
1439
  applySettingsBounds();
1333
1440
  }
1334
1441
  function hideSettings() {
1335
- if (settingsView && settingsViewAdded) {
1336
- try {
1337
- ctx.windows.mainWindow.contentView.removeChildView(settingsView);
1338
- }
1339
- catch { /* ignore */ }
1340
- settingsViewAdded = false;
1341
- }
1342
- }
1343
- // Keep the TOP-tier overlays (settings, popover) above the BASE-tier native
1344
- // overlays (the native simulator WCV + the console/DevTools WCV). Native
1345
- // overlays are z-ordered by `addChildView` insertion order — the last-added
1346
- // sits on top — so RE-attaching a base overlay while a top overlay is open
1347
- // (e.g. the simulator re-shows on a tab switch, or the console bounds
1348
- // republish re-adds it) would move the base ABOVE the open settings/popover
1349
- // and occlude it. Whenever a base overlay is (re)added, re-append the open top
1350
- // overlays so they stay on top. Settings is re-appended before popover so a
1351
- // simultaneously-open popover ends up topmost. A no-op when neither is open
1352
- // (pinned by the z-order guard test).
1353
- function raiseTopOverlays() {
1354
- if (ctx.windows.mainWindow.isDestroyed())
1355
- return;
1356
- const cv = ctx.windows.mainWindow.contentView;
1357
- if (settingsView && settingsViewAdded)
1358
- cv.addChildView(settingsView);
1359
- if (popoverView)
1360
- cv.addChildView(popoverView);
1442
+ overlayDesired.delete(VIEW_ID.settings);
1443
+ reconcileNow();
1361
1444
  }
1362
1445
  function showPopover(data) {
1363
1446
  hidePopover();
@@ -1373,7 +1456,6 @@ export function createViewManager(ctx) {
1373
1456
  applyNavigationHardening(popover.webContents, ctx.rendererDir);
1374
1457
  popoverView = popover;
1375
1458
  popover.setBackgroundColor('#00000000');
1376
- ctx.windows.mainWindow.contentView.addChildView(popover);
1377
1459
  applyPopoverBounds();
1378
1460
  popover.webContents.once('did-finish-load', () => {
1379
1461
  ctx.notify.popoverInit(popover, data);
@@ -1381,19 +1463,23 @@ export function createViewManager(ctx) {
1381
1463
  popover.webContents.loadFile(path.join(ctx.rendererDir, 'entries/popover/index.html'));
1382
1464
  }
1383
1465
  function hidePopover() {
1384
- if (popoverView) {
1385
- destroyViewInternal(popoverView);
1386
- popoverView = null;
1387
- ctx.notify.popoverClosed();
1388
- }
1466
+ if (!popoverView)
1467
+ return;
1468
+ overlayDesired.delete(VIEW_ID.popover);
1469
+ // Destroy the WCV first (removeChildView + close), THEN reconcile: the detach
1470
+ // op then finds the view already gone and does not double-removeChildView.
1471
+ destroyViewInternal(popoverView);
1472
+ popoverView = null;
1473
+ reconcileNow();
1474
+ ctx.notify.popoverClosed();
1389
1475
  }
1390
1476
  function repositionAll() {
1391
1477
  // Simulator + DevTools overlay: bounds owned by the renderer's anchor
1392
1478
  // publishes; their ResizeObserver/window-resize listeners re-measure, so
1393
1479
  // no static re-apply here.
1394
- if (settingsView && settingsViewAdded)
1480
+ if (overlayDesired.has(VIEW_ID.settings))
1395
1481
  applySettingsBounds();
1396
- if (popoverView)
1482
+ if (overlayDesired.has(VIEW_ID.popover))
1397
1483
  applyPopoverBounds();
1398
1484
  }
1399
1485
  function disposeAll() {
@@ -1420,63 +1506,11 @@ export function createViewManager(ctx) {
1420
1506
  }
1421
1507
  safeArea.dispose();
1422
1508
  }
1423
- function setNativeSimulatorViewBounds(params) {
1424
- // Cache unconditionally so attachNativeSimulator can replay a report that
1425
- // landed before the view existed (project-open ordering). This rect is the
1426
- // ONLY authority for the native WCV bounds.
1427
- lastRendererRect = params;
1428
- if (ctx.windows.mainWindow.isDestroyed() || !nativeSimulatorView)
1429
- return;
1430
- const p = layout.computeNativeSimulatorViewParams(params, params.zoom);
1431
- currentZoomFactor = p.zoomFactor;
1432
- // A zero-area rect means "hide" — the renderer reports this when the
1433
- // simulator panel/cell is toggled off or unmounts (toolbar toggle only
1434
- // mutates renderer layout state, so without this the WCV would stay
1435
- // painted over its old region). Mirror `setSimulatorDevtoolsBounds`:
1436
- // remove the child view from the contentView but keep its WebContents
1437
- // alive, so re-showing doesn't re-pay the simulator bootstrap.
1438
- if (isHidden(p.bounds)) {
1439
- if (nativeSimulatorViewAdded) {
1440
- try {
1441
- ctx.windows.mainWindow.contentView.removeChildView(nativeSimulatorView);
1442
- }
1443
- catch { /* already removed */ }
1444
- nativeSimulatorViewAdded = false;
1445
- }
1446
- return;
1447
- }
1448
- if (!nativeSimulatorViewAdded) {
1449
- ctx.windows.mainWindow.contentView.addChildView(nativeSimulatorView);
1450
- nativeSimulatorViewAdded = true;
1451
- // Re-attaching this base overlay moved it to the top of the z-stack; keep
1452
- // any open settings/popover above it.
1453
- raiseTopOverlays();
1454
- }
1455
- nativeSimulatorView.setBounds(p.bounds);
1456
- const simWc = nativeSimulatorView.webContents;
1457
- if (!simWc.isDestroyed()) {
1458
- simWc.setZoomFactor(p.zoomFactor);
1459
- }
1460
- // Propagate zoom to any already-attached nested render-host guests so the
1461
- // page rescales live on zoom change (newly-attached guests get it in
1462
- // did-attach-webview). `webContents.getAllWebContents()` includes guests;
1463
- // filter to those hosted by this simulator wc.
1464
- try {
1465
- for (const wc of webContents.getAllWebContents()) {
1466
- if (wc.isDestroyed())
1467
- continue;
1468
- if (wc.hostWebContents === simWc) {
1469
- wc.setZoomFactor(p.zoomFactor);
1470
- }
1471
- }
1472
- }
1473
- catch { /* hostWebContents unavailable; guests get zoom on attach */ }
1474
- }
1475
1509
  function resize(_simWidth) {
1476
1510
  // Simulator + DevTools overlay bounds are owned solely by the renderer's
1477
1511
  // anchor publishes — its ResizeObserver/window-resize listeners re-measure
1478
1512
  // on this same resize. No static re-apply (it raced the precise rect).
1479
- if (settingsViewAdded)
1513
+ if (overlayDesired.has(VIEW_ID.settings))
1480
1514
  applySettingsBounds();
1481
1515
  }
1482
1516
  return {
@@ -1520,15 +1554,12 @@ export function createViewManager(ctx) {
1520
1554
  },
1521
1555
  getHostToolbarWebContentsId: () => liveHostToolbarWebContents()?.id ?? null,
1522
1556
  getHostToolbarHeight: () => hostToolbarLastHeight,
1523
- setNativeSimulatorViewBounds,
1524
1557
  resize,
1525
- setSimulatorDevtoolsBounds,
1558
+ setPlacementSnapshot,
1526
1559
  attachWorkbench,
1527
1560
  setWorkbenchSource,
1528
- setWorkbenchBounds,
1529
1561
  detachWorkbench,
1530
1562
  openFileInWorkbench,
1531
- setHostToolbarBounds,
1532
1563
  setHostToolbarHeight,
1533
1564
  hostToolbar,
1534
1565
  };