@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
@@ -12,12 +12,13 @@ import type { DebugTap } from '@dimina-kit/electron-deck/main';
12
12
  */
13
13
  /**
14
14
  * Render-side activity worth re-reading panel data on: a page's DOM mounted
15
- * (`domReady`) or the visible page changed (`activePage`). Panels that pull
16
- * from the active render guest (WXML/element-inspect) subscribe via
17
- * `BridgeRouterHandle.onRenderEvent` so they can refresh without polling.
15
+ * (`domReady`), the visible page changed (`activePage`), or the active page's
16
+ * DOM mutated in place (`domMutated`, from the render-guest MutationObserver).
17
+ * Panels that pull from the active render guest (WXML/element-inspect) subscribe
18
+ * via `BridgeRouterHandle.onRenderEvent` so they can refresh without polling.
18
19
  */
19
20
  export interface RenderEvent {
20
- kind: 'domReady' | 'activePage';
21
+ kind: 'domReady' | 'activePage' | 'domMutated';
21
22
  appId: string;
22
23
  bridgeId: string;
23
24
  /** activePage only: the now-visible page's route (bare pagePath), if known.
@@ -583,6 +583,7 @@ async function handleSpawn(state, ctx, event, opts) {
583
583
  poolEntryId,
584
584
  onServiceClosed: null,
585
585
  onServiceBoot: null,
586
+ registryHandle: null,
586
587
  };
587
588
  const rootPage = {
588
589
  bridgeId,
@@ -617,7 +618,7 @@ async function handleSpawn(state, ctx, event, opts) {
617
618
  state.wcIdToAppSessionId.delete(appSession.serviceWc.id);
618
619
  }
619
620
  });
620
- ctx.registry.add(() => disposeAppSession(state, appSessionId));
621
+ appSession.registryHandle = ctx.registry.add(() => disposeAppSession(state, appSessionId));
621
622
  const onServiceClosed = () => {
622
623
  void disposeAppSession(state, appSessionId, { serviceAlreadyClosed: true });
623
624
  };
@@ -977,6 +978,20 @@ function handleContainerMsg(ap, page, msg, ctx, state) {
977
978
  // Guarded + non-throwing: console capture must never break message routing.
978
979
  ctx.guestConsole?.emit(msg.body);
979
980
  break;
981
+ case 'storageChanged':
982
+ // Native-host SYNC storage liveness: the service-host's `setStorageSync`/etc.
983
+ // write `localStorage` directly (no main round-trip), so they post the change
984
+ // here for the Storage panel to stay live. Trust `ap.appId` (sender-resolved),
985
+ // not any appId in the body. Guarded + non-throwing.
986
+ ctx.onServiceStorageChanged?.(ap.appId, msg.body);
987
+ break;
988
+ case 'wxmlChanged':
989
+ // Native-host WXML liveness: the render-guest MutationObserver posts this when
990
+ // the active page's DOM mutated in place (setData). Surface it as a render
991
+ // event so the WXML panel service re-pulls + pushes — same pipeline as
992
+ // domReady/activePage. Trust `page.bridgeId` (sender-resolved), not the body.
993
+ state.emitRenderEvent({ kind: 'domMutated', appId: ap.appId, bridgeId: page.bridgeId });
994
+ break;
980
995
  default:
981
996
  break;
982
997
  }
@@ -1428,6 +1443,15 @@ async function disposeAppSession(state, appSessionId, opts = {}) {
1428
1443
  if (!ap)
1429
1444
  return;
1430
1445
  state.appSessions.delete(appSessionId);
1446
+ // Remove this session's shutdown-fallback registry entry so the registry does
1447
+ // not accumulate one stale closure per respawn. Ordered AFTER the delete
1448
+ // above: dispose() runs the wrapped fn (disposeAppSession again), which
1449
+ // early-returns now that the session is gone — no recursion, no double-run.
1450
+ // (At whole-app shutdown the registry marks the entry released before calling
1451
+ // the fn, so this dispose() is a no-op then.)
1452
+ const registryHandle = ap.registryHandle;
1453
+ ap.registryHandle = null;
1454
+ void registryHandle?.dispose();
1431
1455
  state.appLifecycle.dispose(appSessionId);
1432
1456
  // Evict AppData bridges FIRST — eviction enumerates `ap.pages`, which the
1433
1457
  // page teardown below progressively empties (and finally clears).
@@ -1,5 +1,5 @@
1
1
  import { ServiceHostChannel, SimulatorChannel, SimulatorCustomApiChannel } from '../../shared/ipc-channels.js';
2
- import { SimulatorAttachNativeSchema, SimulatorCustomApiInvokeSchema, SimulatorSetDeviceInfoSchema, SimulatorSetNativeBoundsSchema, } from '../../shared/ipc-schemas.js';
2
+ import { SimulatorAttachNativeSchema, SimulatorCustomApiInvokeSchema, SimulatorSetDeviceInfoSchema, } from '../../shared/ipc-schemas.js';
3
3
  import { deviceInfoToHostEnv } from '../../shared/bridge-channels.js';
4
4
  import { validate } from '../utils/ipc-schema.js';
5
5
  import { IpcRegistry } from '../utils/ipc-registry.js';
@@ -11,10 +11,6 @@ export function registerSimulatorIpc(ctx) {
11
11
  })
12
12
  .handle(SimulatorChannel.Detach, () => {
13
13
  ctx.views.detachSimulator();
14
- })
15
- .handle(SimulatorChannel.SetNativeBounds, (_, ...args) => {
16
- const [p] = validate(SimulatorChannel.SetNativeBounds, SimulatorSetNativeBoundsSchema, args);
17
- ctx.views.setNativeSimulatorViewBounds(p);
18
14
  })
19
15
  .handle(SimulatorChannel.SetDeviceInfo, (_, ...args) => {
20
16
  const [device] = validate(SimulatorChannel.SetDeviceInfo, SimulatorSetDeviceInfoSchema, args);
@@ -1,6 +1,6 @@
1
1
  import { ipcMain } from 'electron';
2
2
  import { ViewChannel } from '../../shared/ipc-channels.js';
3
- import { ViewBoundsSchema, HostToolbarAdvertiseHeightSchema, } from '../../shared/ipc-schemas.js';
3
+ import { PlacementSnapshotSchema, HostToolbarAdvertiseHeightSchema, } from '../../shared/ipc-schemas.js';
4
4
  import { validate } from '../utils/ipc-schema.js';
5
5
  import { IpcRegistry } from '../utils/ipc-registry.js';
6
6
  /**
@@ -20,24 +20,13 @@ import { IpcRegistry } from '../utils/ipc-registry.js';
20
20
  */
21
21
  export function registerViewsIpc(ctx) {
22
22
  const registry = new IpcRegistry(ctx.senderPolicy)
23
- .handle(ViewChannel.SimulatorDevtoolsBounds, (_event, ...args) => {
24
- const [bounds] = validate(ViewChannel.SimulatorDevtoolsBounds, ViewBoundsSchema, args);
25
- ctx.views.setSimulatorDevtoolsBounds(bounds);
26
- })
27
- // Embedded workbench editor: forward anchor (the MAIN renderer measures
28
- // the 'editor' dock slot rect → workbench WCV bounds). invoke, mirroring
29
- // SimulatorDevtoolsBounds. No-op in the view manager when the workbench view
30
- // was never attached (the host did not opt into the workbench editor).
31
- .handle(ViewChannel.WorkbenchBounds, (_event, ...args) => {
32
- const [bounds] = validate(ViewChannel.WorkbenchBounds, ViewBoundsSchema, args);
33
- ctx.views.setWorkbenchBounds(bounds);
34
- })
35
- // Host-controllable toolbar: forward anchor (the MAIN renderer measures the
36
- // placeholder rect → toolbar WCV bounds). invoke, mirroring
37
- // SimulatorDevtoolsBounds. Sender is the trusted main renderer.
38
- .handle(ViewChannel.HostToolbarBounds, (_event, ...args) => {
39
- const [bounds] = validate(ViewChannel.HostToolbarBounds, ViewBoundsSchema, args);
40
- ctx.views.setHostToolbarBounds(bounds);
23
+ // Window-level placement snapshot: the single source of truth for every
24
+ // managed native view's bounds/visibility/z-order. The renderer's central
25
+ // publisher coalesces one snapshot per frame; the reconciler diffs it
26
+ // against the actual view tree. Supersedes the per-view bounds channels.
27
+ .handle(ViewChannel.PlacementSnapshot, (_event, ...args) => {
28
+ const [snapshot] = validate(ViewChannel.PlacementSnapshot, PlacementSnapshotSchema, args);
29
+ ctx.views.setPlacementSnapshot(snapshot);
41
30
  })
42
31
  // Height replay pull: a freshly-mounted main-renderer placeholder asks for
43
32
  // the last NOTIFIED toolbar height (main retains it — the toolbar's
@@ -22,9 +22,11 @@
22
22
  * - Resolve exactly once; later events are inert and the subscription is torn
23
23
  * down on resolve.
24
24
  */
25
- /** The render-event shape `waitForActivePage` reads (subset of `RenderEvent`). */
25
+ /** The render-event shape `waitForActivePage` reads (subset of `RenderEvent`).
26
+ * Only `activePage` events end the wait; other kinds (`domReady`/`domMutated`)
27
+ * are inert but included so a full `RenderEvent` is assignable here. */
26
28
  export interface ActivePageEventLike {
27
- kind: 'domReady' | 'activePage';
29
+ kind: 'domReady' | 'activePage' | 'domMutated';
28
30
  appId: string;
29
31
  bridgeId: string;
30
32
  pagePath?: string;
@@ -24,6 +24,15 @@ export interface RenderInspector {
24
24
  highlight(wc: WebContents, sid: string): Promise<ElementInspection | null>;
25
25
  /** Clear any highlight overlay in the guest. */
26
26
  unhighlight(wc: WebContents): Promise<void>;
27
+ /**
28
+ * Toggle the guest-side WXML MutationObserver. When on, the injected IIFE
29
+ * watches `document.body` and posts a debounced `wxmlChanged` over
30
+ * `DiminaRenderBridge` on every DOM mutation (setData), which bridge-router
31
+ * turns into a `domMutated` render event so the WXML panel re-pulls. Only
32
+ * enabled while the WXML panel is visible, so an unseen panel never drives a
33
+ * full Vue-tree walk. Injects the IIFE first if needed; no-op on a dead guest.
34
+ */
35
+ setWxmlObserving(wc: WebContents, on: boolean): Promise<void>;
27
36
  }
28
37
  export interface RenderInspectorOptions {
29
38
  /** Override the injected IIFE source (default: read the built bundle). Test seam. */
@@ -259,6 +259,18 @@ export function createRenderInspector(options = {}) {
259
259
  // best-effort
260
260
  }
261
261
  }
262
- return { getWxml, highlight, unhighlight };
262
+ async function setWxmlObserving(wc, on) {
263
+ if (wc.isDestroyed())
264
+ return;
265
+ if (!(await ensureInjected(wc)))
266
+ return;
267
+ try {
268
+ await wc.executeJavaScript(`window.__diminaRenderInspect && window.__diminaRenderInspect.setWxmlObserving(${on ? 'true' : 'false'})`);
269
+ }
270
+ catch {
271
+ // best-effort: a guest mid-teardown just stops observing on its own.
272
+ }
273
+ }
274
+ return { getWxml, highlight, unhighlight, setWxmlObserving };
263
275
  }
264
276
  //# sourceMappingURL=index.js.map
@@ -15,6 +15,7 @@
15
15
  */
16
16
  import { type WebContents } from 'electron';
17
17
  import { type ConnectionRegistry, type Disposable } from '@dimina-kit/electron-deck/main';
18
+ import { type SyncStorageChange } from '../../../shared/ipc-channels.js';
18
19
  import { type SenderPolicy } from '../../utils/ipc-registry.js';
19
20
  import type { BridgeRouterHandle } from '../../ipc/bridge-router.js';
20
21
  import type { RenderInspector } from '../render-inspect/index.js';
@@ -34,6 +35,14 @@ export declare const STORAGE_API_NAMES: Set<string>;
34
35
  export interface SimulatorStorageHandle extends Disposable {
35
36
  /** Present only under native-host; wired to ctx.storageApi for bridge-router. */
36
37
  storageApi: StorageApi | null;
38
+ /**
39
+ * Native-host SYNC-storage liveness: bridge-router calls this on each
40
+ * `storageChanged` container message the service-host posts after a
41
+ * `setStorageSync`/`removeStorageSync`/`clearStorageSync`. Those write the
42
+ * service-host `localStorage` directly (bypassing main), so they'd otherwise
43
+ * leave the panel stale until reload — this pushes the matching `StorageEvent`.
44
+ */
45
+ onSyncStorageChange(appId: string, change: SyncStorageChange): void;
37
46
  }
38
47
  export interface SimulatorStorageOptions {
39
48
  /**
@@ -573,7 +573,32 @@ export function setupSimulatorStorage(host, options) {
573
573
  const storageApi = options.bridge?.isNativeHost()
574
574
  ? { invoke: runtimeInvoke }
575
575
  : null;
576
- return { dispose: () => registry.dispose(), storageApi };
576
+ /**
577
+ * SYNC wx storage APIs run inside the service-host window and write
578
+ * `localStorage` directly, so they never reach `setItem`/`runtimeInvoke` here.
579
+ * The service-host posts the change over `DiminaServiceBridge` and bridge-router
580
+ * routes it here so the panel stays live. Only the ACTIVE app's changes are
581
+ * surfaced (the panel shows one app); the key already carries the `${appId}_`
582
+ * prefix, matching the CDP / async event shape.
583
+ */
584
+ function onSyncStorageChange(appId, change) {
585
+ if (appId !== getActiveAppId())
586
+ return;
587
+ if (host.isDestroyed())
588
+ return;
589
+ switch (change.op) {
590
+ case 'set':
591
+ pushSyntheticEvent({ type: 'added', key: change.key, newValue: change.value });
592
+ break;
593
+ case 'remove':
594
+ pushSyntheticEvent({ type: 'removed', key: change.key });
595
+ break;
596
+ case 'clear':
597
+ host.send(SimulatorStorageChannel.Event, { type: 'cleared' });
598
+ break;
599
+ }
600
+ }
601
+ return { dispose: () => registry.dispose(), storageApi, onSyncStorageChange };
577
602
  }
578
603
  // Element inspection delegates to the simulator preload's __simulatorData
579
604
  // bridge via executeJavaScript. Looked up independently from `attachedWc` so
@@ -4,25 +4,131 @@ import { IpcRegistry } from '../../utils/ipc-registry.js';
4
4
  export function setupSimulatorWxml(host, options) {
5
5
  const { bridge, inspector, getActiveAppId } = options;
6
6
  const registry = new DisposableRegistry();
7
- /** Read the WXML tree of the visible page of the active app, or null. */
8
- async function pull() {
9
- const wc = bridge.getActiveRenderWc(getActiveAppId() ?? undefined);
7
+ // Whether the WXML panel is currently visible (renderer drives this via
8
+ // SetActive). Live tree pushes + the render-guest DOM observer only run while
9
+ // active, so an unseen panel never triggers a full Vue-tree walk.
10
+ let active = false;
11
+ // latest-wins: only the newest pull's result is pushed (a slow walk that
12
+ // resolves after a newer one must not clobber the fresher tree).
13
+ let seq = 0;
14
+ // in-flight coalescing: at most one pull runs at a time; a request that lands
15
+ // mid-pull sets `again` so exactly one follow-up pull runs after it settles.
16
+ let pulling = false;
17
+ let again = false;
18
+ // The render guest currently being observed, so we can stop it on page change
19
+ // / deactivation (single owner).
20
+ let observedWc = null;
21
+ /** The visible page's render WebContents of the active app, or null. */
22
+ function activeWc() {
23
+ return bridge.getActiveRenderWc(getActiveAppId() ?? undefined);
24
+ }
25
+ /** Read the WXML tree of a render guest, or null when there is none. */
26
+ async function pull(wc) {
10
27
  if (!wc)
11
28
  return null;
12
29
  return inspector.getWxml(wc);
13
30
  }
31
+ /**
32
+ * Pull the active tree and push it, gated by `active` and reconciled by
33
+ * latest-wins + in-flight coalescing so a burst of render events (setData)
34
+ * collapses to a single fresh push instead of a pile of overlapping walks.
35
+ */
36
+ function schedulePull() {
37
+ if (!active)
38
+ return;
39
+ const mySeq = ++seq;
40
+ if (pulling) {
41
+ again = true;
42
+ return;
43
+ }
44
+ pulling = true;
45
+ void pull(activeWc())
46
+ .then((tree) => {
47
+ // Re-check `active` at send time: a pull started while visible must NOT
48
+ // push after the panel was hidden / disposed (deactivate bumps `seq`, so
49
+ // `mySeq === seq` also guards, but the explicit `active` check is the
50
+ // clear invariant).
51
+ if (mySeq === seq && active && !host.isDestroyed()) {
52
+ host.send(SimulatorWxmlChannel.Event, tree);
53
+ }
54
+ })
55
+ .finally(() => {
56
+ pulling = false;
57
+ if (again) {
58
+ again = false;
59
+ schedulePull();
60
+ }
61
+ });
62
+ }
63
+ function startObserving(wc) {
64
+ if (!wc)
65
+ return;
66
+ observedWc = wc;
67
+ // The guest IIFE defers observe() until document.body exists, so a wc whose
68
+ // DOM isn't up yet still starts observing once it loads — no retry needed here.
69
+ void inspector.setWxmlObserving(wc, true);
70
+ }
71
+ function stopObserving() {
72
+ const wc = observedWc;
73
+ observedWc = null;
74
+ if (wc)
75
+ void inspector.setWxmlObserving(wc, false);
76
+ }
77
+ /**
78
+ * Point the DOM observer at the CURRENT active render guest. Called on every
79
+ * render event while active, so the observer installs even when SetActive fired
80
+ * before the guest existed (the guest appears on `domReady`) and follows the
81
+ * active page across navigation (`activePage`).
82
+ */
83
+ function reconcileObserving() {
84
+ if (!active)
85
+ return;
86
+ const wc = activeWc();
87
+ if (wc !== observedWc) {
88
+ stopObserving();
89
+ startObserving(wc);
90
+ }
91
+ }
92
+ /** Invalidate any in-flight pull + queued re-pull so nothing pushes post-hide. */
93
+ function cancelPending() {
94
+ seq++;
95
+ again = false;
96
+ }
14
97
  const ipc = new IpcRegistry(options.senderPolicy);
15
- ipc.handle(SimulatorWxmlChannel.GetSnapshot, () => pull());
98
+ ipc.handle(SimulatorWxmlChannel.GetSnapshot, () => pull(activeWc()));
99
+ ipc.handle(SimulatorWxmlChannel.SetActive, (_event, on) => {
100
+ if (on) {
101
+ active = true;
102
+ reconcileObserving();
103
+ schedulePull(); // seed the now-visible panel
104
+ }
105
+ else {
106
+ active = false;
107
+ cancelPending();
108
+ stopObserving();
109
+ }
110
+ });
16
111
  registry.add(ipc);
17
- // Re-read + push when a page's DOM mounts or the visible page changes.
18
- const unsubscribe = bridge.onRenderEvent(() => {
19
- void pull().then((tree) => {
20
- if (host.isDestroyed())
21
- return;
22
- host.send(SimulatorWxmlChannel.Event, tree);
23
- });
112
+ // Re-read + push on render-side activity: a page's DOM mounted (`domReady`),
113
+ // the visible page changed (`activePage`), or the active page's DOM mutated in
114
+ // place (`domMutated`, from the render-guest observer). Gated by `active` and
115
+ // scoped to the ACTIVE app so a background app's / stale bridge's event never
116
+ // drives a spurious full walk of the visible page.
117
+ const unsubscribe = bridge.onRenderEvent((event) => {
118
+ if (!active)
119
+ return;
120
+ const appId = getActiveAppId();
121
+ if (appId && event.appId !== appId)
122
+ return;
123
+ // domReady: the guest may have just appeared → (re)install the observer.
124
+ // activePage: the active guest changed → move the observer.
125
+ // domMutated: same guest → reconcile is a no-op, then pull.
126
+ reconcileObserving();
127
+ schedulePull();
24
128
  });
25
129
  registry.add(unsubscribe);
130
+ // Stop observing + invalidate in-flight pulls on teardown (best-effort).
131
+ registry.add(() => { active = false; cancelPending(); stopObserving(); });
26
132
  return registry;
27
133
  }
28
134
  //# sourceMappingURL=index.js.map
@@ -0,0 +1,11 @@
1
+ import type { Bounds, ViewOp } from '@dimina-kit/electron-deck/layout';
2
+ import type { DevtoolsExtra } from '../../../shared/view-ids.js';
3
+ export interface ViewOpTarget {
4
+ attach(viewId: string): void;
5
+ detach(viewId: string): void;
6
+ setBounds(viewId: string, bounds: Bounds, extra?: DevtoolsExtra): void;
7
+ setVisible(viewId: string, visible: boolean): void;
8
+ reorder(order: string[]): void;
9
+ }
10
+ export declare function applyViewOps(ops: ViewOp<DevtoolsExtra>[], target: ViewOpTarget): void;
11
+ //# sourceMappingURL=apply-view-ops.d.ts.map
@@ -0,0 +1,22 @@
1
+ export function applyViewOps(ops, target) {
2
+ for (const op of ops) {
3
+ switch (op.kind) {
4
+ case 'detach':
5
+ target.detach(op.viewId);
6
+ break;
7
+ case 'setVisible':
8
+ target.setVisible(op.viewId, op.visible);
9
+ break;
10
+ case 'setBounds':
11
+ target.setBounds(op.viewId, op.bounds, op.extra);
12
+ break;
13
+ case 'attach':
14
+ target.attach(op.viewId);
15
+ break;
16
+ case 'reorder':
17
+ target.reorder(op.order);
18
+ break;
19
+ }
20
+ }
21
+ }
22
+ //# sourceMappingURL=apply-view-ops.js.map
@@ -0,0 +1,25 @@
1
+ import type { DesiredView } from '@dimina-kit/electron-deck/layout';
2
+ import { type DevtoolsExtra } from '../../../shared/view-ids.js';
3
+ interface Rect {
4
+ x: number;
5
+ y: number;
6
+ width: number;
7
+ height: number;
8
+ }
9
+ interface SnapshotSink {
10
+ setPlacementSnapshot(snapshot: {
11
+ generation: number;
12
+ epoch: number;
13
+ views: DesiredView<DevtoolsExtra>[];
14
+ }): void;
15
+ }
16
+ export declare function hostToolbarBounds(mgr: SnapshotSink, rect: Rect): void;
17
+ export declare function simulatorDevtoolsBounds(mgr: SnapshotSink, rect: Rect): void;
18
+ export declare function workbenchBounds(mgr: SnapshotSink, rect: Rect): void;
19
+ export declare function simulatorBounds(mgr: SnapshotSink, params: Rect & {
20
+ zoom: number;
21
+ }): void;
22
+ /** Drop a view from this manager's desired table (reconcile detaches it). */
23
+ export declare function removeView(mgr: SnapshotSink, viewId: string): void;
24
+ export {};
25
+ //# sourceMappingURL=placement-test-driver.d.ts.map
@@ -0,0 +1,48 @@
1
+ // Test-only helper: drives the view manager through its real runtime entry
2
+ // point, `setPlacementSnapshot`, the way the renderer's central publisher does
3
+ // — accumulating each view's desired placement (per manager) and publishing the
4
+ // WHOLE table under a monotonic epoch. A single-view update thus keeps the other
5
+ // views' last-declared placement, matching the renderer's level-triggered stream
6
+ // rather than the old per-view setXxxBounds edges. These free functions let the
7
+ // many view-manager tests exercise reconcile behaviour with a drop-in rename
8
+ // (`mgr.setHostToolbarBounds(r)` → `hostToolbarBounds(mgr, r)`).
9
+ import { VIEW_ID, VIEW_LAYER } from '../../../shared/view-ids.js';
10
+ // Monotonic across every push in a test file (vitest isolates module state per
11
+ // file), so a fresh manager (lastEpoch -1) accepts the first push and repeated
12
+ // pushes on one manager never look stale.
13
+ let globalEpoch = 0;
14
+ const desiredByMgr = new WeakMap();
15
+ function toPlacement(r) {
16
+ return r.width > 0 && r.height > 0 ? { visible: true, bounds: r } : { visible: false };
17
+ }
18
+ function pushView(mgr, view, generation = 1) {
19
+ let desired = desiredByMgr.get(mgr);
20
+ if (!desired) {
21
+ desired = new Map();
22
+ desiredByMgr.set(mgr, desired);
23
+ }
24
+ desired.set(view.viewId, view);
25
+ mgr.setPlacementSnapshot({ generation, epoch: globalEpoch++, views: [...desired.values()] });
26
+ }
27
+ export function hostToolbarBounds(mgr, rect) {
28
+ pushView(mgr, { viewId: VIEW_ID.hostToolbar, placement: toPlacement(rect), layer: VIEW_LAYER.hostToolbar });
29
+ }
30
+ export function simulatorDevtoolsBounds(mgr, rect) {
31
+ pushView(mgr, { viewId: VIEW_ID.simulatorDevtools, placement: toPlacement(rect), layer: VIEW_LAYER.base });
32
+ }
33
+ export function workbenchBounds(mgr, rect) {
34
+ pushView(mgr, { viewId: VIEW_ID.workbench, placement: toPlacement(rect), layer: VIEW_LAYER.base });
35
+ }
36
+ export function simulatorBounds(mgr, params) {
37
+ const { zoom, ...rect } = params;
38
+ pushView(mgr, { viewId: VIEW_ID.simulator, placement: toPlacement(rect), layer: VIEW_LAYER.base, extra: { zoom } });
39
+ }
40
+ /** Drop a view from this manager's desired table (reconcile detaches it). */
41
+ export function removeView(mgr, viewId) {
42
+ const desired = desiredByMgr.get(mgr);
43
+ if (!desired)
44
+ return;
45
+ desired.delete(viewId);
46
+ mgr.setPlacementSnapshot({ generation: 1, epoch: globalEpoch++, views: [...desired.values()] });
47
+ }
48
+ //# sourceMappingURL=placement-test-driver.js.map
@@ -3,6 +3,8 @@ import type { NativeDeviceInfo } from '../../../shared/ipc-channels.js';
3
3
  import { type OpenInEditorRequest } from '../../../shared/open-in-editor.js';
4
4
  import { type WorkbenchContext } from '../workbench-context.js';
5
5
  import { type HostToolbarMessageSubscription } from './host-toolbar-port-channel.js';
6
+ import type { PlacementSnapshot } from '@dimina-kit/electron-deck/layout';
7
+ import { type DevtoolsExtra } from '../../../shared/view-ids.js';
6
8
  /**
7
9
  * Context surface used by the ViewManager. We only need a small slice of the
8
10
  * full WorkbenchContext here; typing it this way documents the actual dependency.
@@ -11,10 +13,10 @@ export interface ViewManagerContext {
11
13
  windows: WorkbenchContext['windows'];
12
14
  rendererDir: string;
13
15
  /**
14
- * Per-webContents connection registry (foundation.md §4). The native-host
16
+ * Per-webContents connection registry. The native-host
15
17
  * simulator WebContentsView is acquired here so the registry tracks that
16
18
  * trusted webContents and tears its per-wc resources (the custom-api bridge
17
- * `ipcMain.on`) down deterministically on destroy — see P1 DoD #3. Required:
19
+ * `ipcMain.on`) down deterministically on destroy. Required:
18
20
  * `createWorkbenchContext` always supplies it.
19
21
  */
20
22
  connections: WorkbenchContext['connections'];
@@ -135,22 +137,6 @@ export interface ViewManager {
135
137
  * always on close-project → reopen) would otherwise be lost forever.
136
138
  */
137
139
  getHostToolbarHeight(): number;
138
- /**
139
- * NATIVE-HOST ONLY. Position the simulator content WebContentsView over the
140
- * renderer-measured simulator panel REGION rect (the flex:1 placeholder slot,
141
- * CSS px from the main window content top-left, which maps 1:1 to overlay
142
- * setBounds DIP) and apply the device zoom. The WCV fills the region as a
143
- * plain rectangle; DeviceShell draws + scrolls the phone inside. No-op in the
144
- * default `<webview>` path (`nativeSimulatorView` is null). See
145
- * `computeNativeSimulatorViewParams`.
146
- */
147
- setNativeSimulatorViewBounds(params: {
148
- x: number;
149
- y: number;
150
- width: number;
151
- height: number;
152
- zoom: number;
153
- }): void;
154
140
  /**
155
141
  * NATIVE-HOST ONLY. Re-push the selected device's CSS `env(safe-area-inset-*)`
156
142
  * override to every attached render-host guest (on device change). New guests
@@ -164,17 +150,12 @@ export interface ViewManager {
164
150
  */
165
151
  resize(simWidth: number): void;
166
152
  /**
167
- * Apply a renderer-measured rectangle to the simulator's Chromium
168
- * DevTools overlay view. `{ width: 0, height: 0 }` is treated as "hide" —
169
- * the view is removed from the contentView but its WebContents is kept
170
- * alive so re-showing it doesn't re-pay the DevTools bootstrap.
153
+ * Apply the renderer's window-level placement snapshot the single source of
154
+ * truth for every managed native view's bounds/visibility/z-order. Merged with
155
+ * main-owned settings/popover desired state and reconciled against the actual
156
+ * view tree.
171
157
  */
172
- setSimulatorDevtoolsBounds(bounds: {
173
- x: number;
174
- y: number;
175
- width: number;
176
- height: number;
177
- }): void;
158
+ setPlacementSnapshot(snapshot: PlacementSnapshot<DevtoolsExtra>): void;
178
159
  /**
179
160
  * Lazily create the workbench WebContentsView, load `<url>index.html`, and
180
161
  * add it to the contentView. `url` is the COI server's base URL (trailing
@@ -188,19 +169,6 @@ export interface ViewManager {
188
169
  * `setWorkbenchBounds`, keeping it off the app boot critical path.
189
170
  */
190
171
  setWorkbenchSource(url: string): void;
191
- /**
192
- * Apply a renderer-measured rectangle to the workbench editor view. Mirrors
193
- * `setSimulatorDevtoolsBounds`: `{ width: 0, height: 0 }` is "hide" — the view
194
- * is removed from the contentView (kept under settings/popover when re-added)
195
- * but its WebContents stays alive. The first non-zero rect lazily creates +
196
- * loads the workbench (from the URL set by `setWorkbenchSource`).
197
- */
198
- setWorkbenchBounds(bounds: {
199
- x: number;
200
- y: number;
201
- width: number;
202
- height: number;
203
- }): void;
204
172
  /** Destroy the workbench editor view (teardown). No-op if never attached. */
205
173
  detachWorkbench(): void;
206
174
  /**
@@ -211,20 +179,6 @@ export interface ViewManager {
211
179
  * request was dispatched to the workbench.
212
180
  */
213
181
  openFileInWorkbench(relPath: string, line: number, column: number): boolean;
214
- /**
215
- * Apply a renderer-measured rectangle to the host-controllable toolbar
216
- * WebContentsView (the strip above the devtools header). Forward anchor,
217
- * mirroring `setSimulatorDevtoolsBounds`. `{ width: 0, height: 0 }` is
218
- * treated as "hide" — the view is removed from the contentView but its
219
- * WebContents (and the host's loaded content) stays alive. Lazily creates
220
- * the view on the first non-empty rect.
221
- */
222
- setHostToolbarBounds(bounds: {
223
- x: number;
224
- y: number;
225
- width: number;
226
- height: number;
227
- }): void;
228
182
  /**
229
183
  * Reverse size-advertiser sink: the toolbar WCV's own renderer advertises
230
184
  * its intrinsic content height (block-axis extent); we retain it as the