@dimina-kit/devtools 0.4.0-dev.20260616102751 → 0.4.0-dev.20260624040247

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 (78) hide show
  1. package/README.md +30 -25
  2. package/dist/main/app/app.js +14 -2
  3. package/dist/main/index.bundle.js +559 -111
  4. package/dist/main/ipc/bridge-router.js +57 -9
  5. package/dist/main/ipc/projects.js +1 -1
  6. package/dist/main/ipc/simulator.js +1 -1
  7. package/dist/main/runtime/devtools-backend.d.ts +1 -1
  8. package/dist/main/runtime/devtools-backend.js +1 -1
  9. package/dist/main/runtime/miniapp-runtime.d.ts +5 -5
  10. package/dist/main/services/console-forward/index.js +8 -1
  11. package/dist/main/services/elements-forward/index.js +36 -12
  12. package/dist/main/services/layout/index.d.ts +12 -0
  13. package/dist/main/services/layout/index.js +14 -2
  14. package/dist/main/services/network-forward/index.d.ts +5 -6
  15. package/dist/main/services/network-forward/index.js +9 -10
  16. package/dist/main/services/notifications/renderer-notifier.d.ts +3 -4
  17. package/dist/main/services/projects/index.js +1 -1
  18. package/dist/main/services/projects/types.d.ts +1 -1
  19. package/dist/main/services/projects/types.js +1 -1
  20. package/dist/main/services/service-console/console-api.d.ts +77 -0
  21. package/dist/main/services/service-console/console-api.js +103 -0
  22. package/dist/main/services/service-console/index.d.ts +47 -0
  23. package/dist/main/services/service-console/index.js +139 -0
  24. package/dist/main/services/service-host-pool/pool.d.ts +2 -3
  25. package/dist/main/services/simulator-storage/index.d.ts +2 -4
  26. package/dist/main/services/simulator-storage/index.js +2 -4
  27. package/dist/main/services/simulator-temp-files/index.js +2 -2
  28. package/dist/main/services/update/update-manager.d.ts +1 -1
  29. package/dist/main/services/update/update-manager.js +1 -1
  30. package/dist/main/services/views/host-toolbar-session-runtime.d.ts +4 -4
  31. package/dist/main/services/views/host-toolbar-session-runtime.js +4 -4
  32. package/dist/main/services/views/view-manager.d.ts +24 -1
  33. package/dist/main/services/views/view-manager.js +120 -41
  34. package/dist/main/services/workbench-context.d.ts +7 -6
  35. package/dist/main/services/workspace/workspace-service.js +11 -0
  36. package/dist/main/windows/service-host-window/create.d.ts +1 -0
  37. package/dist/main/windows/service-host-window/create.js +1 -0
  38. package/dist/preload/runtime/host-toolbar-port.d.ts +1 -1
  39. package/dist/preload/runtime/host-toolbar-port.js +1 -1
  40. package/dist/preload/runtime/host-toolbar-runtime.d.ts +8 -7
  41. package/dist/preload/runtime/host-toolbar-runtime.js +8 -7
  42. package/dist/preload/windows/host-toolbar-runtime.cjs.map +2 -2
  43. package/dist/preload/windows/main.cjs.map +2 -2
  44. package/dist/preload/windows/simulator.cjs +1 -1
  45. package/dist/preload/windows/simulator.cjs.map +1 -1
  46. package/dist/preload/windows/simulator.js +1 -1
  47. package/dist/renderer/assets/index-PtsdWlGu.js +50 -0
  48. package/dist/renderer/assets/{input-B8NDCYv3.js → input-DiCxYlqY.js} +2 -2
  49. package/dist/renderer/assets/{ipc-transport-DhrajiC5.js → ipc-transport-DSBmaWpJ.js} +1 -1
  50. package/dist/renderer/assets/ipc-transport-DZxf2YoM.css +1 -0
  51. package/dist/renderer/assets/{popover-DbZOg03s.js → popover-CyL_uffj.js} +2 -2
  52. package/dist/renderer/assets/{select-pW3Ysc9T.js → select-BSwvDMtA.js} +2 -2
  53. package/dist/renderer/assets/settings-Ch8XbFxf.js +2 -0
  54. package/dist/renderer/assets/{settings-api-BSKzKiWd.js → settings-api-e9ILe2JO.js} +2 -2
  55. package/dist/renderer/assets/{workbenchSettings-BGxjl25x.js → workbenchSettings-DZsHf1g_.js} +2 -2
  56. package/dist/renderer/entries/main/index.html +6 -6
  57. package/dist/renderer/entries/popover/index.html +5 -5
  58. package/dist/renderer/entries/settings/index.html +11 -5
  59. package/dist/renderer/entries/workbench-settings/index.html +4 -4
  60. package/dist/service-host/preload.cjs +13 -15
  61. package/dist/service-host/sourcemap-rewrite.cjs +21 -12
  62. package/dist/shared/appdata-accumulator.d.ts +2 -2
  63. package/dist/shared/appdata-accumulator.js +3 -3
  64. package/dist/shared/constants.d.ts +1 -1
  65. package/dist/shared/constants.js +1 -1
  66. package/dist/shared/ipc-channels.d.ts +3 -3
  67. package/dist/shared/ipc-channels.js +3 -3
  68. package/dist/shared/open-in-editor.d.ts +20 -1
  69. package/dist/shared/open-in-editor.js +246 -3
  70. package/dist/shared/vpath.d.ts +2 -2
  71. package/dist/shared/vpath.js +2 -2
  72. package/dist/simulator/assets/{device-shell-BWX7Yopg.js → device-shell-TtcSXwhb.js} +2 -2
  73. package/dist/simulator/assets/{simulator-sf-D0mhw.js → simulator-CtyXt_4V.js} +3 -3
  74. package/dist/simulator/simulator.html +1 -1
  75. package/package.json +8 -6
  76. package/dist/renderer/assets/index-Z4Rh52G8.js +0 -50
  77. package/dist/renderer/assets/ipc-transport-9agi76dX.css +0 -1
  78. package/dist/renderer/assets/settings-CZJOHt8b.js +0 -2
@@ -1,13 +1,15 @@
1
1
  import { ipcMain, nativeTheme, shell, WebContentsView, webContents } from 'electron';
2
+ import fs from 'node:fs';
2
3
  import path from 'path';
3
4
  import { cjsSiblingPreloadPath, mainPreloadPath } from '../../utils/paths.js';
4
5
  import { simDeskBg } from '../../utils/theme.js';
5
6
  import { applyNavigationHardening, handleWindowOpenExternal, } from '../../windows/navigation-hardening.js';
6
7
  import { SimulatorCustomApiBridgeChannel } from '../../../shared/ipc-channels.js';
7
- import { OPEN_IN_EDITOR_SCHEME, decodeOpenInEditorUrl, resourceUrlToProjectRelativePath, } from '../../../shared/open-in-editor.js';
8
+ import { buildDevtoolsProjectSourceLinksScript, decodeOpenInEditorUrl, projectSourceContextFromServiceHostUrl, resourceUrlToProjectRelativePath, } from '../../../shared/open-in-editor.js';
8
9
  import { createSafeAreaController } from '../safe-area/index.js';
9
10
  import { buildCustomizeTabsScript } from './devtools-tabs.js';
10
11
  import { installElementsForward } from '../elements-forward/index.js';
12
+ import { installServiceConsoleForward } from '../service-console/index.js';
11
13
  import * as layout from '../layout/index.js';
12
14
  import { handleCustomApiBridgeRequest, } from '../simulator/custom-apis.js';
13
15
  import { configureMiniappSession, miniappPartition } from './miniapp-partition.js';
@@ -15,6 +17,35 @@ import { acquireHostToolbarSessionRuntime, releaseHostToolbarSessionRuntime, } f
15
17
  import { createHostToolbarPortChannel, } from './host-toolbar-port-channel.js';
16
18
  import { parseRoute } from '../../../shared/simulator-route.js';
17
19
  import { HEADER_H, HOST_TOOLBAR_RUNTIME_MARKER } from '../../../shared/constants.js';
20
+ /**
21
+ * Resolve a DevTools source request against the service-host URL that created
22
+ * the inspected app. Its pkgRoot is authoritative; the workspace is only a
23
+ * stale-session consistency guard.
24
+ */
25
+ export function resolveProjectEditorTarget(serviceHostUrl, activeProjectRoot, req, isFile = (absolutePath) => fs.statSync(absolutePath).isFile()) {
26
+ const sourceContext = projectSourceContextFromServiceHostUrl(serviceHostUrl, activeProjectRoot);
27
+ if (!sourceContext)
28
+ return null;
29
+ const rel = resourceUrlToProjectRelativePath(req.url, sourceContext);
30
+ if (!rel)
31
+ return null;
32
+ const absolute = path.resolve(sourceContext.projectRoot, ...rel.split('/'));
33
+ const fromRoot = path.relative(path.resolve(sourceContext.projectRoot), absolute);
34
+ if (!fromRoot || fromRoot.startsWith('..') || path.isAbsolute(fromRoot))
35
+ return null;
36
+ try {
37
+ if (!isFile(absolute))
38
+ return null;
39
+ }
40
+ catch {
41
+ return null;
42
+ }
43
+ return {
44
+ path: rel,
45
+ line: typeof req.line === 'number' ? req.line + 1 : undefined,
46
+ column: typeof req.column === 'number' ? req.column + 1 : undefined,
47
+ };
48
+ }
18
49
  /**
19
50
  * Build a ViewManager bound to the given context. The returned object is the
20
51
  * only component allowed to instantiate or add/remove overlay WebContentsViews.
@@ -36,6 +67,8 @@ export function createViewManager(ctx) {
36
67
  // while `simulatorView` above hosts its DevTools in the right panel region.
37
68
  let nativeSimulatorView = null;
38
69
  let nativeSimulatorViewAdded = false;
70
+ let nativeSimulatorProjectPath = null;
71
+ let settleNativeSimulatorReady = null;
39
72
  // Model A: the renderer's measured inner-screen rect is the SOLE authority for
40
73
  // the native simulator WCV bounds (see docs/simulator-render-architecture.md).
41
74
  // Cache the last rect so a report that lands before attachNativeSimulator (the
@@ -72,6 +105,10 @@ export function createViewManager(ctx) {
72
105
  // CDP traffic at the active render guest). Installed in
73
106
  // `attachNativeSimulatorDevtoolsHost`, stopped on detach / host destroyed.
74
107
  let stopElementsForward = null;
108
+ // Disposer for the service-layer console capture (CDP `consoleAPICalled` on the
109
+ // service host wc → console fan-out). Installed when the DevTools is pointed at
110
+ // a service host, stopped when that source is closed / swapped.
111
+ let stopServiceConsole = null;
75
112
  let nativeDevtoolsRetryTimer = null;
76
113
  let nativeDevtoolsRetryToken = 0;
77
114
  // Renderer-driven overlay bounds for the simulator DevTools view — the SOLE
@@ -152,6 +189,9 @@ export function createViewManager(ctx) {
152
189
  if (!simulatorViewAdded) {
153
190
  ctx.windows.mainWindow.contentView.addChildView(simulatorView);
154
191
  simulatorViewAdded = true;
192
+ // Re-attaching this base overlay moved it to the top of the z-stack; keep
193
+ // any open settings/popover above it.
194
+ raiseTopOverlays();
155
195
  }
156
196
  simulatorView.setBounds(bounds);
157
197
  }
@@ -386,6 +426,11 @@ export function createViewManager(ctx) {
386
426
  function closeNativeDevtoolsSource() {
387
427
  const source = nativeDevtoolsSourceWc;
388
428
  nativeDevtoolsSourceWc = null;
429
+ try {
430
+ stopServiceConsole?.();
431
+ }
432
+ catch { /* already stopped */ }
433
+ stopServiceConsole = null;
389
434
  if (!source || source.isDestroyed())
390
435
  return;
391
436
  try {
@@ -416,43 +461,17 @@ export function createViewManager(ctx) {
416
461
  // `devtools-open-url` on the inspected (service-host) wc, which we decode,
417
462
  // map to a project-relative path, and broadcast to the renderer's Monaco.
418
463
  const openInEditorWiredWcIds = new Set();
419
- function injectOpenResourceHandler(devtoolsWc) {
464
+ function injectOpenResourceHandler(serviceWc, devtoolsWc) {
420
465
  // `setOpenResourceHandler` is the official Chromium DevTools hook IDEs use
421
466
  // to route source-link clicks to an external editor. We poll for the host
422
467
  // (it appears once the front-end finishes bootstrapping, like `UI` above)
423
468
  // and register a handler that re-emits an encoded sentinel via
424
469
  // `openInNewTab` → Electron `devtools-open-url`. Best-effort: wrapped in
425
470
  // try/catch and a bounded poll so a missing API never throws.
426
- const scheme = JSON.stringify(OPEN_IN_EDITOR_SCHEME);
427
- devtoolsWc.executeJavaScript(`
428
- (function() {
429
- try {
430
- let tries = 0
431
- const timer = setInterval(() => {
432
- tries++
433
- try {
434
- const Host = globalThis.Host
435
- const host = Host && Host.InspectorFrontendHost
436
- if (host && typeof host.setOpenResourceHandler === 'function'
437
- && typeof host.openInNewTab === 'function') {
438
- host.setOpenResourceHandler((url, lineNumber, columnNumber) => {
439
- try {
440
- const p = new URLSearchParams()
441
- p.set('u', String(url))
442
- if (typeof lineNumber === 'number') p.set('l', String(lineNumber))
443
- if (typeof columnNumber === 'number') p.set('c', String(columnNumber))
444
- host.openInNewTab(${scheme} + ':?' + p.toString())
445
- } catch (_) {}
446
- })
447
- clearInterval(timer)
448
- return
449
- }
450
- } catch (_) {}
451
- if (tries > 80) clearInterval(timer)
452
- }, 50)
453
- } catch (_) {}
454
- })()
455
- `).catch(() => { });
471
+ const sourceContext = projectSourceContextFromServiceHostUrl(serviceWc.getURL(), ctx.workspace?.getProjectPath?.());
472
+ if (!sourceContext)
473
+ return;
474
+ devtoolsWc.executeJavaScript(buildDevtoolsProjectSourceLinksScript(sourceContext)).catch(() => { });
456
475
  }
457
476
  // ── DevTools tab customization: keep only Elements/Console/Network ──────────
458
477
  // Inject into the same DevTools front-end host wc that the console/network
@@ -489,10 +508,10 @@ export function createViewManager(ctx) {
489
508
  // wc is recreated per simulatorView, so a fresh host needs the handler set.
490
509
  if (!devtoolsWc.isDestroyed()) {
491
510
  if (devtoolsWc.isLoading()) {
492
- devtoolsWc.once('dom-ready', () => injectOpenResourceHandler(devtoolsWc));
511
+ devtoolsWc.once('dom-ready', () => injectOpenResourceHandler(serviceWc, devtoolsWc));
493
512
  }
494
513
  else {
495
- injectOpenResourceHandler(devtoolsWc);
514
+ injectOpenResourceHandler(serviceWc, devtoolsWc);
496
515
  }
497
516
  }
498
517
  // Attach the `devtools-open-url` decoder to the inspected service wc once
@@ -505,13 +524,10 @@ export function createViewManager(ctx) {
505
524
  const req = decodeOpenInEditorUrl(url);
506
525
  if (!req)
507
526
  return; // not our sentinel — leave it to Electron's default path
508
- const rel = resourceUrlToProjectRelativePath(req.url);
509
- if (!rel)
527
+ const target = resolveProjectEditorTarget(serviceWc.getURL(), ctx.workspace?.getProjectPath?.(), req);
528
+ if (!target)
510
529
  return;
511
- // DevTools reports 0-based line/column; Monaco is 1-based.
512
- const line = typeof req.line === 'number' ? req.line + 1 : undefined;
513
- const column = typeof req.column === 'number' ? req.column + 1 : undefined;
514
- ctx.notify.editorOpenFile({ path: rel, line, column });
530
+ ctx.notify.editorOpenFile(target);
515
531
  };
516
532
  serviceWc.on('devtools-open-url', onOpenUrl);
517
533
  // Consolidate teardown onto the connection layer (foundation.md §4 / P2),
@@ -557,6 +573,19 @@ export function createViewManager(ctx) {
557
573
  // Re-applied on every re-point so a service-host pool swap (fresh
558
574
  // openDevTools) re-asserts the custom tab bar.
559
575
  customizeDevtoolsTabs(simulatorView.webContents);
576
+ // Capture service-layer console via CDP (NOT a preload monkeypatch, which
577
+ // would clobber native source attribution) and feed it to the console
578
+ // fan-out (automation `App.logAdded`). Bound to THIS service wc; replaced
579
+ // on the next re-point via closeNativeDevtoolsSource.
580
+ try {
581
+ stopServiceConsole?.();
582
+ }
583
+ catch { /* already stopped */ }
584
+ stopServiceConsole = installServiceConsoleForward({
585
+ serviceWc: next,
586
+ connections: ctx.connections,
587
+ emit: (entry) => ctx.consoleForwarder?.emit(entry),
588
+ }).stop;
560
589
  return true;
561
590
  }
562
591
  catch {
@@ -767,8 +796,12 @@ export function createViewManager(ctx) {
767
796
  function attachNativeSimulator(simulatorUrl, _simWidth) {
768
797
  if (!ctx.preloadPath) {
769
798
  console.error('[workbench] attachNativeSimulator — preloadPath unset; cannot mount native simulator');
770
- return;
799
+ return Promise.resolve();
771
800
  }
801
+ // Unblock a superseded IPC invocation. Its renderer effect cleanup marks
802
+ // that generation cancelled, so this cannot schedule a stale capture.
803
+ settleNativeSimulatorReady?.();
804
+ settleNativeSimulatorReady = null;
772
805
  // Tear down any previous native simulator view (relaunch / re-open).
773
806
  if (nativeSimulatorView) {
774
807
  detachNativeCustomApiBridge();
@@ -787,6 +820,10 @@ export function createViewManager(ctx) {
787
820
  catch { /* ignore */ }
788
821
  nativeSimulatorView = null;
789
822
  }
823
+ nativeSimulatorProjectPath = null;
824
+ const ready = new Promise((resolve) => {
825
+ settleNativeSimulatorReady = resolve;
826
+ });
790
827
  // Derive THIS project's session partition from the simulator URL's appId so
791
828
  // its cookies/localStorage/cache are isolated from every other project (P0
792
829
  // debt). Same project → same partition (storage survives a relaunch);
@@ -814,6 +851,7 @@ export function createViewManager(ctx) {
814
851
  },
815
852
  });
816
853
  nativeSimulatorView = view;
854
+ nativeSimulatorProjectPath = ctx.workspace?.getProjectPath() || null;
817
855
  // Paint the WCV surface the themed desk color (simDeskBg(): dark #121212 /
818
856
  // light #e8e8e8) so a height-resize that grows the region never flashes a
819
857
  // mismatched strip before DeviceShell's desk repaints — the WCV, the desk,
@@ -903,6 +941,15 @@ export function createViewManager(ctx) {
903
941
  e.preventDefault();
904
942
  }
905
943
  });
944
+ // The outer DeviceShell loading is insufficient: slow projects attach a
945
+ // render guest later. Resolve the renderer's attach IPC only after that
946
+ // first mini-app page has completed its own document load.
947
+ guestWc.once('did-finish-load', () => {
948
+ if (nativeSimulatorView !== view || simWc.isDestroyed())
949
+ return;
950
+ settleNativeSimulatorReady?.();
951
+ settleNativeSimulatorReady = null;
952
+ });
906
953
  // A render-host guest only attaches after a spawn, so the service window
907
954
  // exists by now — (re)point the right-panel DevTools at it. Belt-and-braces
908
955
  // with the `onRenderEvent` path in case its emit lost the attach race.
@@ -958,8 +1005,11 @@ export function createViewManager(ctx) {
958
1005
  // (The UI/view layer's Elements equivalent is the native WXML panel +
959
1006
  // render-guest highlight chain, which targets the active render guest.)
960
1007
  attachNativeSimulatorDevtoolsHost();
1008
+ return ready;
961
1009
  }
962
1010
  function detachSimulator() {
1011
+ settleNativeSimulatorReady?.();
1012
+ settleNativeSimulatorReady = null;
963
1013
  stopFollowingNativeServiceHost();
964
1014
  detachNativeCustomApiBridge();
965
1015
  // Stop Elements forwarding (detaches only the debugger sessions IT attached;
@@ -1003,6 +1053,7 @@ export function createViewManager(ctx) {
1003
1053
  simulatorView = null;
1004
1054
  simulatorViewAdded = false;
1005
1055
  simulatorWebContentsId = null;
1056
+ nativeSimulatorProjectPath = null;
1006
1057
  // Drop the renderer-published rect so a stale "hidden" override doesn't
1007
1058
  // suppress the next view before its renderer republishes.
1008
1059
  simulatorBoundsOverride = null;
@@ -1038,6 +1089,11 @@ export function createViewManager(ctx) {
1038
1089
  // Overlay loads mainPreloadPath, so the same navigation rules as the
1039
1090
  // main window apply — see navigation-hardening.ts.
1040
1091
  applyNavigationHardening(settingsView.webContents, ctx.rendererDir);
1092
+ // Transparent backing: the settings view now spans the whole content area
1093
+ // (computeSettingsBounds) and its renderer paints a transparent backdrop +
1094
+ // an opaque right-side panel, so the underlying editor/simulator show
1095
+ // through and a backdrop click closes the overlay (mirrors the popover).
1096
+ settingsView.setBackgroundColor('#00000000');
1041
1097
  await settingsView.webContents.loadFile(path.join(ctx.rendererDir, 'entries/settings/index.html'));
1042
1098
  }
1043
1099
  if (!settingsViewAdded) {
@@ -1055,6 +1111,25 @@ export function createViewManager(ctx) {
1055
1111
  settingsViewAdded = false;
1056
1112
  }
1057
1113
  }
1114
+ // Keep the TOP-tier overlays (settings, popover) above the BASE-tier native
1115
+ // overlays (the native simulator WCV + the console/DevTools WCV). Native
1116
+ // overlays are z-ordered by `addChildView` insertion order — the last-added
1117
+ // sits on top — so RE-attaching a base overlay while a top overlay is open
1118
+ // (e.g. the simulator re-shows on a tab switch, or the console bounds
1119
+ // republish re-adds it) would move the base ABOVE the open settings/popover
1120
+ // and occlude it. Whenever a base overlay is (re)added, re-append the open top
1121
+ // overlays so they stay on top. Settings is re-appended before popover so a
1122
+ // simultaneously-open popover ends up topmost. A no-op when neither is open
1123
+ // (pinned by the z-order guard test).
1124
+ function raiseTopOverlays() {
1125
+ if (ctx.windows.mainWindow.isDestroyed())
1126
+ return;
1127
+ const cv = ctx.windows.mainWindow.contentView;
1128
+ if (settingsView && settingsViewAdded)
1129
+ cv.addChildView(settingsView);
1130
+ if (popoverView)
1131
+ cv.addChildView(popoverView);
1132
+ }
1058
1133
  function showPopover(data) {
1059
1134
  hidePopover();
1060
1135
  const popover = new WebContentsView({
@@ -1141,6 +1216,9 @@ export function createViewManager(ctx) {
1141
1216
  if (!nativeSimulatorViewAdded) {
1142
1217
  ctx.windows.mainWindow.contentView.addChildView(nativeSimulatorView);
1143
1218
  nativeSimulatorViewAdded = true;
1219
+ // Re-attaching this base overlay moved it to the top of the z-stack; keep
1220
+ // any open settings/popover above it.
1221
+ raiseTopOverlays();
1144
1222
  }
1145
1223
  nativeSimulatorView.setBounds(p.bounds);
1146
1224
  const simWc = nativeSimulatorView.webContents;
@@ -1186,6 +1264,7 @@ export function createViewManager(ctx) {
1186
1264
  const wc = webContents.fromId(simulatorWebContentsId);
1187
1265
  return wc && !wc.isDestroyed() ? wc : null;
1188
1266
  },
1267
+ getSimulatorProjectPath: () => nativeSimulatorProjectPath,
1189
1268
  getSettingsWebContents: () => {
1190
1269
  if (!settingsView)
1191
1270
  return null;
@@ -137,12 +137,13 @@ export interface WorkbenchContext {
137
137
  */
138
138
  storageApi?: StorageApi;
139
139
  /**
140
- * Native-host console sink. The render-host / service-host guest preloads
141
- * monkeypatch `console.*` and post each entry to main as a `consoleLog`
142
- * container message; bridge-router routes those here. Owned by the
143
- * `ConsoleForwarder` (set in `installBridgeRouter`), whose `emit` fans every
144
- * entry out to subscribers (automation WS) AND mirrors render-layer entries
145
- * into the service host's own console for the embedded DevTools.
140
+ * Native-host console sink. Render-layer entries arrive from
141
+ * render-host/preload.cjs (`console.*` monkeypatch `consoleLog` message);
142
+ * service-layer entries arrive from CDP `Runtime.consoleAPICalled`
143
+ * (services/service-console) so native source attribution is preserved.
144
+ * Owned by the `ConsoleForwarder` (set in `installBridgeRouter`), whose `emit`
145
+ * fans every entry out to subscribers (automation WS) AND mirrors render-layer
146
+ * entries into the service host's own console for the embedded DevTools.
146
147
  */
147
148
  guestConsole?: {
148
149
  emit(entry: unknown): void;
@@ -215,11 +215,22 @@ export function createWorkspaceService(ctx) {
215
215
  getLastClosedProjectPath: () => lastClosedProjectPath,
216
216
  hasActiveSession: () => currentSession !== null,
217
217
  async captureThumbnail(projectPath) {
218
+ if (!currentSession || projectPath !== currentProjectPath)
219
+ return null;
218
220
  const wc = ctx.views.getSimulatorWebContents();
219
221
  if (!wc)
220
222
  return null;
223
+ if (ctx.views.getSimulatorProjectPath() !== projectPath)
224
+ return null;
225
+ const session = currentSession;
221
226
  try {
222
227
  const image = await wc.capturePage();
228
+ if (currentSession !== session
229
+ || currentProjectPath !== projectPath
230
+ || ctx.views.getSimulatorWebContents() !== wc
231
+ || ctx.views.getSimulatorProjectPath() !== projectPath) {
232
+ return null;
233
+ }
223
234
  const dataUrl = `data:image/png;base64,${image.toPNG().toString('base64')}`;
224
235
  if (provider.saveThumbnail) {
225
236
  await provider.saveThumbnail(projectPath, dataUrl);
@@ -14,6 +14,7 @@ export interface ServiceHostWindowOptions {
14
14
  appId: string;
15
15
  pagePath: string;
16
16
  pkgRoot: string;
17
+ root?: string;
17
18
  resourceBaseUrl: string;
18
19
  hostEnvSnapshot?: Record<string, unknown>;
19
20
  }
@@ -52,6 +52,7 @@ export function buildServiceHostSpawnUrl(opts) {
52
52
  url.searchParams.set('appId', opts.appId);
53
53
  url.searchParams.set('pagePath', opts.pagePath);
54
54
  url.searchParams.set('pkgRoot', opts.pkgRoot);
55
+ url.searchParams.set('root', opts.root || 'main');
55
56
  url.searchParams.set('resourceBaseUrl', opts.resourceBaseUrl);
56
57
  if (opts.hostEnvSnapshot) {
57
58
  // Encode the resolved host-env so preload can hydrate
@@ -19,7 +19,7 @@
19
19
  * envelope stays the single waist (same posture as the main side's
20
20
  * host-toolbar-port-channel.ts).
21
21
  *
22
- * Ordering reality this module absorbs (spike .repro/wave3-spike):
22
+ * Ordering reality this module absorbs:
23
23
  * - The page script runs BEFORE the handshake can complete (the port is
24
24
  * posted on did-finish-load). Page `send()`s issued before the port
25
25
  * arrives go into a PENDING QUEUE and flush in order on handshake —
@@ -19,7 +19,7 @@
19
19
  * envelope stays the single waist (same posture as the main side's
20
20
  * host-toolbar-port-channel.ts).
21
21
  *
22
- * Ordering reality this module absorbs (spike .repro/wave3-spike):
22
+ * Ordering reality this module absorbs:
23
23
  * - The page script runs BEFORE the handshake can complete (the port is
24
24
  * posted on did-finish-load). Page `send()`s issued before the port
25
25
  * arrives go into a PENDING QUEUE and flush in order on handshake —
@@ -10,15 +10,16 @@
10
10
  * - the toolbar WCV's creation injects `HOST_TOOLBAR_RUNTIME_MARKER` into its
11
11
  * process argv via `webPreferences.additionalArguments`;
12
12
  * - the marker is PROCESS-level (subframes of the toolbar window carry it
13
- * too — spike .repro/wave3-spike/RESULTS.md item 3), so the guard needs
14
- * BOTH wings: `isMainFrame` AND `argv.includes(marker)`;
13
+ * too), so the guard needs BOTH wings: `isMainFrame` AND
14
+ * `argv.includes(marker)`;
15
15
  * - a renderer that fails the guard returns immediately with ZERO footprint
16
- * (no advertiser, no listeners, no globals — spike item 4).
16
+ * (no advertiser, no listeners, no globals).
17
17
  *
18
- * Why session-resident at all: the advertiser used to ride the toolbar WCV's
19
- * `webPreferences.preload`, so a host calling `setPreloadPath(<its own
20
- * preload>)` REPLACED it and the strip height collapsed to 0. With the runtime
21
- * on the session layer, the host preload and the framework runtime coexist.
18
+ * Why session-resident at all: riding the toolbar WCV's
19
+ * `webPreferences.preload` lets a host calling `setPreloadPath(<its own
20
+ * preload>)` REPLACE the advertiser, collapsing the strip height to 0. With the
21
+ * runtime on the session layer, the host preload and the framework runtime
22
+ * coexist.
22
23
  */
23
24
  /**
24
25
  * Pure guard predicate: should the toolbar runtime activate in a renderer with
@@ -10,15 +10,16 @@
10
10
  * - the toolbar WCV's creation injects `HOST_TOOLBAR_RUNTIME_MARKER` into its
11
11
  * process argv via `webPreferences.additionalArguments`;
12
12
  * - the marker is PROCESS-level (subframes of the toolbar window carry it
13
- * too — spike .repro/wave3-spike/RESULTS.md item 3), so the guard needs
14
- * BOTH wings: `isMainFrame` AND `argv.includes(marker)`;
13
+ * too), so the guard needs BOTH wings: `isMainFrame` AND
14
+ * `argv.includes(marker)`;
15
15
  * - a renderer that fails the guard returns immediately with ZERO footprint
16
- * (no advertiser, no listeners, no globals — spike item 4).
16
+ * (no advertiser, no listeners, no globals).
17
17
  *
18
- * Why session-resident at all: the advertiser used to ride the toolbar WCV's
19
- * `webPreferences.preload`, so a host calling `setPreloadPath(<its own
20
- * preload>)` REPLACED it and the strip height collapsed to 0. With the runtime
21
- * on the session layer, the host preload and the framework runtime coexist.
18
+ * Why session-resident at all: riding the toolbar WCV's
19
+ * `webPreferences.preload` lets a host calling `setPreloadPath(<its own
20
+ * preload>)` REPLACE the advertiser, collapsing the strip height to 0. With the
21
+ * runtime on the session layer, the host preload and the framework runtime
22
+ * coexist.
22
23
  */
23
24
  import { HOST_TOOLBAR_RUNTIME_MARKER } from '../../shared/constants.js';
24
25
  import { installHostToolbarAdvertiserWhenReady } from './host-toolbar-advertiser.js';