@dimina-kit/devtools 0.4.0-dev.20260612025610 → 0.4.0-dev.20260612152115

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 (41) hide show
  1. package/README.md +4 -4
  2. package/dist/main/index.bundle.js +63 -12
  3. package/dist/main/ipc/bridge-router.js +35 -8
  4. package/dist/main/ipc/views.js +10 -1
  5. package/dist/main/services/notifications/renderer-notifier.d.ts +11 -0
  6. package/dist/main/services/notifications/renderer-notifier.js +3 -0
  7. package/dist/main/services/views/view-manager.d.ts +16 -4
  8. package/dist/main/services/views/view-manager.js +32 -8
  9. package/dist/main/services/workspace/workspace-service.js +39 -0
  10. package/dist/preload/index.d.ts +2 -2
  11. package/dist/preload/index.js +2 -2
  12. package/dist/preload/windows/host-toolbar-runtime.cjs +11 -1
  13. package/dist/preload/windows/host-toolbar-runtime.cjs.map +2 -2
  14. package/dist/preload/windows/main.cjs +9 -7
  15. package/dist/preload/windows/main.cjs.map +2 -2
  16. package/dist/preload/windows/simulator.cjs.map +1 -1
  17. package/dist/renderer/assets/index-Bi_6SS-L.js +50 -0
  18. package/dist/renderer/assets/{input-6fpCXV-n.js → input-DgqX2UvN.js} +2 -2
  19. package/dist/renderer/assets/{ipc-transport-D22e4dv7.js → ipc-transport-CsFhDJ7q.js} +2 -2
  20. package/dist/renderer/assets/ipc-transport-SnOAk0Jq.css +1 -0
  21. package/dist/renderer/assets/{popover-CdGdKkkc.js → popover-D_2o_52M.js} +2 -2
  22. package/dist/renderer/assets/{select-X0sbjwdP.js → select-BJYsdghn.js} +2 -2
  23. package/dist/renderer/assets/{settings-CS4jYaCw.js → settings-CivkfizP.js} +2 -2
  24. package/dist/renderer/assets/{settings-api-CCWTAao_.js → settings-api-7mXkBy5-.js} +2 -2
  25. package/dist/renderer/assets/{workbenchSettings-Bpzaj3bm.js → workbenchSettings-BwpwxuzM.js} +2 -2
  26. package/dist/renderer/entries/main/index.html +6 -6
  27. package/dist/renderer/entries/popover/index.html +5 -5
  28. package/dist/renderer/entries/settings/index.html +5 -5
  29. package/dist/renderer/entries/workbench-settings/index.html +4 -4
  30. package/dist/shared/ipc-channels.d.ts +11 -0
  31. package/dist/shared/ipc-channels.js +14 -0
  32. package/dist/simulator/assets/bridge-channels-BUQ5AbvJ.js +2 -0
  33. package/dist/simulator/assets/device-shell-BEnc2k-W.js +2 -0
  34. package/dist/simulator/assets/{simulator-DHylZf9Z.js → simulator-cV1PERi6.js} +3 -3
  35. package/dist/simulator/assets/simulator-mini-app-CYtW6bMd.js +2 -0
  36. package/dist/simulator/simulator.html +1 -1
  37. package/package.json +4 -4
  38. package/dist/renderer/assets/index-DLzjz1wv.js +0 -50
  39. package/dist/renderer/assets/ipc-transport-Bs8Sf1B2.css +0 -1
  40. package/dist/simulator/assets/device-shell-fH2cI-3q.js +0 -2
  41. package/dist/simulator/assets/simulator-mini-app-BDNu5n26.js +0 -2
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Dimina DevTools
2
2
 
3
- 基于 Electron 的小程序开发者工具。提供模拟器、Chrome DevTools 面板、WXML/AppData/Storage 面板、编译配置等功能。
3
+ 基于 Electron 的小程序开发者工具。提供模拟器、Chrome DevTools 面板、WXML/AppData/Storage/编译 面板、编译配置等功能。
4
4
 
5
5
  下游 host 通过 `launch(config)` 集成并定制 devtools(零配置直接 `launch()`,配置驱动 `launch({...})`;见下方「两种用法」)。两种用法都经领域中立的 [`@dimina-kit/electron-deck`](../electron-deck) 框架编排——框架接管 Electron 进程生命周期(whenReady / will-quit)、wire/trust 原语,devtools 作为 `RuntimeBackend` 注入完整运行时(见 [`framework-extraction-v2.md`](../electron-deck/docs/framework-extraction-v2.md))。
6
6
 
@@ -147,7 +147,7 @@ src/
147
147
  main.tsx # 主窗口 React 根
148
148
  features/ # 主窗口内的业务区域
149
149
  project-runtime/ # 项目视图 + 工具栏 + 右侧面板切换
150
- right-panel/ # WXML / AppData / Storage 面板
150
+ right-panel/ # WXML / AppData / Storage / 编译 面板
151
151
  popover/ settings/ workbench-settings/
152
152
  shared/
153
153
  components/ # UI 组件(ui / layout / json-viewer / ...)
@@ -177,7 +177,7 @@ src/
177
177
  | ------------------ | -------------------- | ------------------- | ---------------------------------- |
178
178
  | `appName` | `string` | `'Dimina DevTools'` | 窗口标题 |
179
179
  | `adapter` | `CompilationAdapter` | 内置 | 项目编译适配器 |
180
- | `panels` | `BuiltinPanelId[]` | — | **已废弃,运行时忽略**:界面恒显示全部四个内置面板;保留字段仅为兼容仍传它的 host |
180
+ | `panels` | `BuiltinPanelId[]` | — | **已废弃,运行时忽略**:界面恒显示全部内置面板(WXML / AppData / Storage / Console / 编译);保留字段仅为兼容仍传它的 host |
181
181
  | `preloadPath` | `string` | 内置 | 统一的 host 级 preload 入口;native-host simulator(WCV)自动跑其 `.cjs` sibling(`cjsSiblingPreloadPath`) |
182
182
  | `apiNamespaces` | `string[]` | `[]` | 自定义 API 命名空间(如 `['qd']`) |
183
183
  | `brandingProvider` | `() => { appName }` | — | 品牌信息 provider |
@@ -483,7 +483,7 @@ launch({
483
483
 
484
484
  ## Host Toolbar(宿主自定义工具栏)
485
485
 
486
- 下游通过 `instance.context.views.hostToolbar` 拥有 devtools 头部下方的工具栏条(一个 WebContentsView):`loadURL` / `loadFile` 加载自己的内容,`setPreloadPath` 注入自己的 preload,`setHeightMode` 钉死或自动跟随内容高度(自动模式要求内容自带 shrink-to-fit 的 `[data-host-toolbar-root]` 包裹元素;`{ fixed }` 校验入参——非有限数或负数同步抛 `TypeError` 且不污染既有模式)。
486
+ 下游通过 `instance.context.views.hostToolbar` 拥有 devtools 头部下方的工具栏条(一个 WebContentsView):`loadURL` / `loadFile` 加载自己的内容,`setPreloadPath` 注入自己的 preload,`setHeightMode` 钉死或自动跟随内容高度(自动模式要求内容自带 shrink-to-fit 的 `[data-host-toolbar-root]` 包裹元素;`{ fixed }` 校验入参——非有限数或负数同步抛 `TypeError` 且不污染既有模式)。主进程保留最后一次下发的高度(`views.getHostToolbarHeight()`),项目视图的占位条挂载时会主动拉取并回放——广播器对已上报的高度去重不再重发,没有这一步,冷启动在项目列表期间的上报、以及关闭项目再打开后的高度都会永久丢失(工具栏条塌缩为 0)。
487
487
 
488
488
  ### 双向消息:onMessage / send
489
489
 
@@ -374,6 +374,10 @@ var ProjectChannel = {
374
374
  GetCompileConfig: "project:getCompileConfig",
375
375
  SaveCompileConfig: "project:saveCompileConfig",
376
376
  Status: "project:status",
377
+ // Main → renderer push of per-line dmcc compile logs (devkit `onLog`).
378
+ // Dedicated channel: `project:status` keeps its one-event-per-payload
379
+ // contract (compileEvents), this one carries the line stream (compileLogs).
380
+ CompileLog: "project:compileLog",
377
381
  CaptureThumbnail: "project:captureThumbnail",
378
382
  GetThumbnail: "project:getThumbnail"
379
383
  };
@@ -440,7 +444,17 @@ var ViewChannel = {
440
444
  * main → main-window renderer: push the reserved host-toolbar height so the
441
445
  * renderer placeholder div resizes (closing the dynamic-height loop).
442
446
  */
443
- HostToolbarHeightChanged: "view:host-toolbar:height-changed"
447
+ HostToolbarHeightChanged: "view:host-toolbar:height-changed",
448
+ /**
449
+ * main ← main-window renderer (invoke): pull the last NOTIFIED toolbar
450
+ * height retained in main. Mount-time replay companion to
451
+ * `HostToolbarHeightChanged`: the push listener mounts with the project
452
+ * view and the toolbar's size-advertiser deduplicates (never re-reports),
453
+ * so a height pushed while no project view was mounted would otherwise be
454
+ * lost forever (cold start on the project list races it; close-project →
455
+ * reopen hits it deterministically). No payload; resolves a number.
456
+ */
457
+ HostToolbarGetHeight: "view:host-toolbar:get-height"
444
458
  };
445
459
  var PopoverChannel = {
446
460
  Show: "popover:show",
@@ -966,6 +980,9 @@ function createRendererNotifier(ctx) {
966
980
  projectStatus(payload) {
967
981
  sendToMain(ProjectChannel.Status, payload);
968
982
  },
983
+ compileLog(payload) {
984
+ sendToMain(ProjectChannel.CompileLog, payload);
985
+ },
969
986
  windowNavigateBack() {
970
987
  sendToMain(WindowChannel.NavigateBack);
971
988
  },
@@ -1841,6 +1858,7 @@ function createViewManager(ctx) {
1841
1858
  let hostToolbarViewAdded = false;
1842
1859
  let hostToolbarRuntimeAcquired = false;
1843
1860
  let hostToolbarHeightMode = "auto";
1861
+ let hostToolbarLastHeight = 0;
1844
1862
  const hostToolbarPort = createHostToolbarPortChannel({
1845
1863
  isCurrent: (wc) => liveHostToolbarWebContents() === wc
1846
1864
  });
@@ -1943,9 +1961,13 @@ function createViewManager(ctx) {
1943
1961
  }
1944
1962
  view.setBounds(bounds);
1945
1963
  }
1964
+ function notifyHostToolbarHeight(height) {
1965
+ hostToolbarLastHeight = height;
1966
+ ctx.notify.hostToolbarHeightChanged(height);
1967
+ }
1946
1968
  function setHostToolbarHeight(extent) {
1947
1969
  if (hostToolbarHeightMode !== "auto") return;
1948
- ctx.notify.hostToolbarHeightChanged(extent);
1970
+ notifyHostToolbarHeight(extent);
1949
1971
  }
1950
1972
  function hideHostToolbar() {
1951
1973
  if (hostToolbarView && hostToolbarViewAdded && !ctx.windows.mainWindow.isDestroyed()) {
@@ -1955,7 +1977,7 @@ function createViewManager(ctx) {
1955
1977
  }
1956
1978
  }
1957
1979
  hostToolbarViewAdded = false;
1958
- ctx.notify.hostToolbarHeightChanged(0);
1980
+ notifyHostToolbarHeight(0);
1959
1981
  }
1960
1982
  const hostToolbar = {
1961
1983
  async loadURL(url) {
@@ -1985,7 +2007,7 @@ function createViewManager(ctx) {
1985
2007
  }
1986
2008
  hostToolbarHeightMode = mode;
1987
2009
  if (mode !== "auto") {
1988
- ctx.notify.hostToolbarHeightChanged(mode.fixed);
2010
+ notifyHostToolbarHeight(mode.fixed);
1989
2011
  }
1990
2012
  },
1991
2013
  onMessage(channel, handler) {
@@ -2546,6 +2568,7 @@ function createViewManager(ctx) {
2546
2568
  return popoverView.webContents.id;
2547
2569
  },
2548
2570
  getHostToolbarWebContentsId: () => liveHostToolbarWebContents()?.id ?? null,
2571
+ getHostToolbarHeight: () => hostToolbarLastHeight,
2549
2572
  setNativeSimulatorViewBounds,
2550
2573
  resize,
2551
2574
  setSimulatorDevtoolsBounds,
@@ -2823,6 +2846,7 @@ function createWorkspaceService(ctx) {
2823
2846
  let currentSession = null;
2824
2847
  let currentProjectPath = "";
2825
2848
  let lastClosedProjectPath = "";
2849
+ let logGeneration = 0;
2826
2850
  function sendStatus(status2, message, hotReload) {
2827
2851
  ctx.notify.projectStatus(hotReload ? { status: status2, message, hotReload: true } : { status: status2, message });
2828
2852
  }
@@ -2871,6 +2895,7 @@ function createWorkspaceService(ctx) {
2871
2895
  validateProjectDir: async (dirPath) => provider.validateProjectDir ? provider.validateProjectDir(dirPath) : null,
2872
2896
  async openProject(projectPath) {
2873
2897
  clearSimulatorServicewechatReferer();
2898
+ logGeneration++;
2874
2899
  await disposeSession();
2875
2900
  currentProjectPath = "";
2876
2901
  lastClosedProjectPath = "";
@@ -2881,6 +2906,7 @@ function createWorkspaceService(ctx) {
2881
2906
  }
2882
2907
  sendStatus("compiling", "\u6B63\u5728\u7F16\u8BD1...");
2883
2908
  const { compile } = loadWorkbenchSettings();
2909
+ const sessionGeneration = ++logGeneration;
2884
2910
  let session5;
2885
2911
  try {
2886
2912
  session5 = await ctx.adapter.openProject({
@@ -2888,7 +2914,21 @@ function createWorkspaceService(ctx) {
2888
2914
  sourcemap: true,
2889
2915
  watch: compile.watch,
2890
2916
  onRebuild: () => sendStatus("ready", "\u7F16\u8BD1\u5B8C\u6210\uFF0C\u5DF2\u70ED\u66F4\u65B0", true),
2891
- onBuildError: (err) => sendStatus("error", String(err))
2917
+ onBuildError: (err) => sendStatus("error", String(err)),
2918
+ // Per-line dmcc log (already filtered in devkit). Stamp the
2919
+ // wall-clock capture time here and push verbatim on the dedicated
2920
+ // compile-log channel — never through projectStatus, whose
2921
+ // one-event-per-payload contract feeds compileEvents. Stale lines
2922
+ // (a closed/replaced session's worker flushing its buffers) are
2923
+ // dropped via the generation check.
2924
+ onLog: (entry) => {
2925
+ if (sessionGeneration !== logGeneration) return;
2926
+ ctx.notify.compileLog({
2927
+ stream: entry.stream,
2928
+ text: entry.text,
2929
+ at: Date.now()
2930
+ });
2931
+ }
2892
2932
  });
2893
2933
  } catch (err) {
2894
2934
  clearSimulatorServicewechatReferer();
@@ -2922,6 +2962,7 @@ function createWorkspaceService(ctx) {
2922
2962
  };
2923
2963
  },
2924
2964
  async closeProject() {
2965
+ logGeneration++;
2925
2966
  clearSimulatorServicewechatReferer();
2926
2967
  await disposeSession();
2927
2968
  if (currentProjectPath !== "") lastClosedProjectPath = currentProjectPath;
@@ -4756,12 +4797,16 @@ function summarizeBridgeMsg(payload) {
4756
4797
  var API_CALL_TIMEOUT_MS = 5e3;
4757
4798
  function resolveCurrentApp(state, ctx, appId) {
4758
4799
  if (appId) {
4759
- for (const ap of state.appSessions.values()) if (ap.appId === appId) return ap;
4800
+ let match;
4801
+ for (const ap of state.appSessions.values()) if (ap.appId === appId) match = ap;
4802
+ if (match) return match;
4760
4803
  }
4761
4804
  const appInfo = ctx.workspace?.getSession?.()?.appInfo;
4762
4805
  const activeAppId = appInfo?.appId;
4763
4806
  if (activeAppId) {
4764
- for (const ap of state.appSessions.values()) if (ap.appId === activeAppId) return ap;
4807
+ let match;
4808
+ for (const ap of state.appSessions.values()) if (ap.appId === activeAppId) match = ap;
4809
+ if (match) return match;
4765
4810
  }
4766
4811
  let last;
4767
4812
  for (const ap of state.appSessions.values()) last = ap;
@@ -4778,7 +4823,11 @@ function installBridgeRouter(ctx) {
4778
4823
  emitRenderEvent: () => {
4779
4824
  },
4780
4825
  connections: ctx.connections,
4781
- debugTap: createDebugTap({ enabled: resolveDebugTapEnabled() })
4826
+ debugTap: createDebugTap({ enabled: resolveDebugTapEnabled() }),
4827
+ evictAppDataBridges: (ap) => {
4828
+ if (!ctx.appData) return;
4829
+ for (const page of ap.pages.values()) ctx.appData.evictBridge(ap.appId, page.bridgeId);
4830
+ }
4782
4831
  };
4783
4832
  const prewarmPoolSize = resolvePrewarmPoolSize();
4784
4833
  if (prewarmPoolSize > 0) {
@@ -4950,9 +4999,6 @@ function installBridgeRouter(ctx) {
4950
4999
  console.warn(`[bridge-router] DISPOSE rejected: sender belongs to ${senderApp.appSessionId}, target ${target.appSessionId}`);
4951
5000
  return;
4952
5001
  }
4953
- if (ctx.appData) {
4954
- for (const page of target.pages.values()) ctx.appData.evictBridge(target.appId, page.bridgeId);
4955
- }
4956
5002
  void disposeAppSession(state, target.appSessionId);
4957
5003
  };
4958
5004
  ipcMain3.on(BRIDGE_CHANNELS.DISPOSE, onDispose);
@@ -5135,6 +5181,10 @@ async function handleSpawn(state, ctx, event, opts) {
5135
5181
  };
5136
5182
  appSession.onServiceClosed = onServiceClosed;
5137
5183
  serviceWindow.once("closed", onServiceClosed);
5184
+ const onSimulatorDestroyed = () => {
5185
+ void disposeAppSession(state, appSessionId);
5186
+ };
5187
+ simulatorWc.once("destroyed", onSimulatorDestroyed);
5138
5188
  if (usedPool) {
5139
5189
  const bootOnServiceLoad = () => {
5140
5190
  if (serviceWindow.isDestroyed()) return;
@@ -5639,6 +5689,7 @@ async function disposeAppSession(state, appSessionId, opts = {}) {
5639
5689
  const ap = state.appSessions.get(appSessionId);
5640
5690
  if (!ap) return;
5641
5691
  state.appSessions.delete(appSessionId);
5692
+ state.evictAppDataBridges(ap);
5642
5693
  for (const [requestId, pending] of state.pendingApiCalls) {
5643
5694
  if (pending.appSessionId !== appSessionId) continue;
5644
5695
  clearTimeout(pending.timer);
@@ -5804,7 +5855,7 @@ function registerViewsIpc(ctx) {
5804
5855
  args
5805
5856
  );
5806
5857
  ctx.views.setHostToolbarBounds(bounds);
5807
- });
5858
+ }).handle(ViewChannel.HostToolbarGetHeight, () => ctx.views.getHostToolbarHeight());
5808
5859
  const onAdvertiseHeight = (event, ...args) => {
5809
5860
  if (event.sender.id !== ctx.views.getHostToolbarWebContentsId()) return;
5810
5861
  let extent;
@@ -58,17 +58,26 @@ const API_CALL_TIMEOUT_MS = 5_000;
58
58
  * rather than assuming a single session.
59
59
  */
60
60
  function resolveCurrentApp(state, ctx, appId) {
61
+ // Same-appId matches prefer the MOST RECENT spawn (Maps preserve insertion
62
+ // order): after a respawn/reopen the newest session is the live one — the
63
+ // first match could be a just-superseded session mid-teardown.
61
64
  if (appId) {
65
+ let match;
62
66
  for (const ap of state.appSessions.values())
63
67
  if (ap.appId === appId)
64
- return ap;
68
+ match = ap;
69
+ if (match)
70
+ return match;
65
71
  }
66
72
  const appInfo = ctx.workspace?.getSession?.()?.appInfo;
67
73
  const activeAppId = appInfo?.appId;
68
74
  if (activeAppId) {
75
+ let match;
69
76
  for (const ap of state.appSessions.values())
70
77
  if (ap.appId === activeAppId)
71
- return ap;
78
+ match = ap;
79
+ if (match)
80
+ return match;
72
81
  }
73
82
  // Maps preserve insertion order; the last entry is the most recent spawn.
74
83
  let last;
@@ -87,6 +96,12 @@ export function installBridgeRouter(ctx) {
87
96
  emitRenderEvent: () => { },
88
97
  connections: ctx.connections,
89
98
  debugTap: createDebugTap({ enabled: resolveDebugTapEnabled() }),
99
+ evictAppDataBridges: (ap) => {
100
+ if (!ctx.appData)
101
+ return;
102
+ for (const page of ap.pages.values())
103
+ ctx.appData.evictBridge(ap.appId, page.bridgeId);
104
+ },
90
105
  };
91
106
  // Opt-in (default OFF) pre-warm pool for service-host windows. When enabled,
92
107
  // handleSpawn acquires a warm window instead of constructing one per spawn.
@@ -298,12 +313,8 @@ export function installBridgeRouter(ctx) {
298
313
  console.warn(`[bridge-router] DISPOSE rejected: sender belongs to ${senderApp.appSessionId}, target ${target.appSessionId}`);
299
314
  return;
300
315
  }
301
- // Native-host AppData: clear the app's accumulated bridges so a re-opened
302
- // project doesn't show ghost tabs from the prior session.
303
- if (ctx.appData) {
304
- for (const page of target.pages.values())
305
- ctx.appData.evictBridge(target.appId, page.bridgeId);
306
- }
316
+ // AppData bridge eviction happens inside disposeAppSession (single
317
+ // chokepoint shared with the simulator-WCV 'destroyed' path).
307
318
  void disposeAppSession(state, target.appSessionId);
308
319
  };
309
320
  ipcMain.on(C.DISPOSE, onDispose);
@@ -517,6 +528,19 @@ async function handleSpawn(state, ctx, event, opts) {
517
528
  };
518
529
  appSession.onServiceClosed = onServiceClosed;
519
530
  serviceWindow.once('closed', onServiceClosed);
531
+ // The simulator WCV owns the app's UI lifetime. When it is destroyed —
532
+ // project close (`views.disposeAll`/detach) or a DeviceShell respawn
533
+ // (`attachNativeSimulator`, e.g. watcher hot reload) — the guest never gets
534
+ // to send its graceful `C.DISPOSE`, so without this hook the app session and
535
+ // its hidden service-host window leak, and `resolveCurrentApp` keeps
536
+ // resolving the STALE session for the same appId: `getActiveRenderWc` then
537
+ // dereferences dead pages and every panel pull (WXML/elements) returns null
538
+ // for the whole next session. Disposing here is idempotent with the graceful
539
+ // path (`disposeAppSession` early-returns once the session is gone).
540
+ const onSimulatorDestroyed = () => {
541
+ void disposeAppSession(state, appSessionId);
542
+ };
543
+ simulatorWc.once('destroyed', onSimulatorDestroyed);
520
544
  if (usedPool) {
521
545
  // A pooled/fallback window passes through about:blank (warm load or the
522
546
  // fallback's initial load). Boot only once the REAL service.html navigation
@@ -1144,6 +1168,9 @@ async function disposeAppSession(state, appSessionId, opts = {}) {
1144
1168
  if (!ap)
1145
1169
  return;
1146
1170
  state.appSessions.delete(appSessionId);
1171
+ // Evict AppData bridges FIRST — eviction enumerates `ap.pages`, which the
1172
+ // page teardown below progressively empties (and finally clears).
1173
+ state.evictAppDataBridges(ap);
1147
1174
  // Drain any pending API calls owned by this app session. One-shot calls
1148
1175
  // normally self-clean on response/timeout, but persistent (`keep: true`)
1149
1176
  // subscriptions (e.g. audioListen) live with their timer cleared until
@@ -30,7 +30,16 @@ export function registerViewsIpc(ctx) {
30
30
  .handle(ViewChannel.HostToolbarBounds, (_event, ...args) => {
31
31
  const [bounds] = validate(ViewChannel.HostToolbarBounds, ViewBoundsSchema, args);
32
32
  ctx.views.setHostToolbarBounds(bounds);
33
- });
33
+ })
34
+ // Height replay pull: a freshly-mounted main-renderer placeholder asks for
35
+ // the last NOTIFIED toolbar height (main retains it — the toolbar's
36
+ // size-advertiser deduplicates and never re-pushes, so a push that fired
37
+ // while no project view was mounted is otherwise lost: cold start races
38
+ // it, close-project → reopen hits it always). Rides the SAME
39
+ // senderPolicy-gated registry as HostToolbarBounds: the toolbar WCV's
40
+ // arbitrary host content must not reach this — only the trusted main
41
+ // renderer pulls. Live delegation, not a registration-time snapshot.
42
+ .handle(ViewChannel.HostToolbarGetHeight, () => ctx.views.getHostToolbarHeight());
34
43
  // Reverse size-advertiser: the toolbar WCV's OWN renderer sends this, and the
35
44
  // host loads ARBITRARY content into that WCV. We DELIBERATELY do NOT add the
36
45
  // toolbar wc to the global sender policy — that would trust it for ALL ~72
@@ -12,6 +12,15 @@ export interface ProjectStatusPayload {
12
12
  /** True when the status update is emitted by the file-watcher rebuild loop. */
13
13
  hotReload?: boolean;
14
14
  }
15
+ /**
16
+ * Payload for the `project:compileLog` push — one filtered dmcc log line.
17
+ * `at` is stamped in the main process when the line is captured.
18
+ */
19
+ export interface CompileLogPayload {
20
+ at: number;
21
+ stream: 'stdout' | 'stderr';
22
+ text: string;
23
+ }
15
24
  /**
16
25
  * Payload for the `settings:init` event sent into the embedded settings overlay
17
26
  * right after it is shown.
@@ -43,6 +52,8 @@ export interface WorkbenchSettingsInitPayload {
43
52
  export interface RendererNotifier {
44
53
  /** Broadcast project compile status transitions to the main renderer. */
45
54
  projectStatus(payload: ProjectStatusPayload): void;
55
+ /** Push one per-line dmcc compile-log entry to the main renderer. */
56
+ compileLog(payload: CompileLogPayload): void;
46
57
  /** Ask the main renderer to navigate back to its landing screen. */
47
58
  windowNavigateBack(): void;
48
59
  /** Tell the main renderer the compile popover has been closed. */
@@ -24,6 +24,9 @@ export function createRendererNotifier(ctx) {
24
24
  projectStatus(payload) {
25
25
  sendToMain(ProjectChannel.Status, payload);
26
26
  },
27
+ compileLog(payload) {
28
+ sendToMain(ProjectChannel.CompileLog, payload);
29
+ },
27
30
  windowNavigateBack() {
28
31
  sendToMain(WindowChannel.NavigateBack);
29
32
  },
@@ -112,6 +112,16 @@ export interface ViewManager {
112
112
  * must trust its id — see `createWorkbenchSenderPolicy`.
113
113
  */
114
114
  getHostToolbarWebContentsId(): number | null;
115
+ /**
116
+ * Return the last host-toolbar height NOTIFIED to the main-window renderer
117
+ * (an advertiser report in `'auto'` mode, a `setHeightMode({ fixed })` pin,
118
+ * or 0 after `hostToolbar.hide()`); 0 before any notify. The renderer pulls
119
+ * this on project-view mount to REPLAY a height whose push it missed — the
120
+ * toolbar's size-advertiser deduplicates and never re-reports, so a notify
121
+ * fired while no project view is mounted (cold start on the project list;
122
+ * always on close-project → reopen) would otherwise be lost forever.
123
+ */
124
+ getHostToolbarHeight(): number;
115
125
  /**
116
126
  * NATIVE-HOST ONLY. Position the simulator content WebContentsView over the
117
127
  * renderer-measured simulator panel REGION rect (the flex:1 placeholder slot,
@@ -168,10 +178,12 @@ export interface ViewManager {
168
178
  }): void;
169
179
  /**
170
180
  * Reverse size-advertiser sink: the toolbar WCV's own renderer advertises
171
- * its intrinsic content height (block-axis extent); we store it and push it
172
- * to the main-window renderer so the placeholder div resizes (closing the
173
- * dynamic-height loop). Ignored while a `{ fixed }` height mode is pinned
174
- * via `hostToolbar.setHeightMode` (the session-resident advertiser always
181
+ * its intrinsic content height (block-axis extent); we retain it as the
182
+ * last-notified height (`getHostToolbarHeight`) and push it to the
183
+ * main-window renderer so the placeholder div resizes (closing the
184
+ * dynamic-height loop). Ignored ENTIRELY while a `{ fixed }` height mode is
185
+ * pinned via `hostToolbar.setHeightMode` — dropped reports neither notify
186
+ * nor touch the retained value (the session-resident advertiser always
175
187
  * runs, so its reports must not fight a host-pinned height).
176
188
  */
177
189
  setHostToolbarHeight(extent: number): void;
@@ -95,6 +95,13 @@ export function createViewManager(ctx) {
95
95
  // Placeholder height authority: 'auto' = advertiser reports forward to the
96
96
  // renderer; { fixed } = host-pinned, advertiser reports are dropped.
97
97
  let hostToolbarHeightMode = 'auto';
98
+ // Last toolbar height NOTIFIED to the main-window renderer — the replay
99
+ // source behind `getHostToolbarHeight()`. Updated ONLY inside
100
+ // `notifyHostToolbarHeight` so the retained value can never diverge from
101
+ // what the renderer was told (an advertiser report dropped by a `{ fixed }`
102
+ // pin must not pollute it, and a setHeightMode validation reject leaves it
103
+ // untouched).
104
+ let hostToolbarLastHeight = 0;
98
105
  // Gated narrow channel to the toolbar PAGE (per-load MessagePort handshake;
99
106
  // see host-toolbar-port-channel.ts). Control-level registry — created with
100
107
  // the manager so onMessage() works before any toolbar view exists.
@@ -248,18 +255,30 @@ export function createViewManager(ctx) {
248
255
  }
249
256
  view.setBounds(bounds);
250
257
  }
258
+ // Single funnel for the height notify: retain-then-push, so the retained
259
+ // value is exactly the last value the renderer was told. Every height
260
+ // notify site MUST go through here — the renderer pulls the retained value
261
+ // on project-view mount to replay a push it missed (the toolbar's
262
+ // size-advertiser deduplicates and never re-reports).
263
+ function notifyHostToolbarHeight(height) {
264
+ hostToolbarLastHeight = height;
265
+ ctx.notify.hostToolbarHeightChanged(height);
266
+ }
251
267
  function setHostToolbarHeight(extent) {
252
268
  // While the host pins a fixed height, drop advertiser reports entirely —
253
269
  // the session-resident advertiser is always installed, so forwarding its
254
270
  // reports would make the strip oscillate between the pinned and measured
255
- // heights on every content resize.
271
+ // heights on every content resize. Dropped reports must not touch the
272
+ // retained value either: retention records what was NOTIFIED, not what
273
+ // was reported.
256
274
  if (hostToolbarHeightMode !== 'auto')
257
275
  return;
258
276
  // Push the reserved height back to the main-window renderer so its
259
- // placeholder div resizes (closing the dynamic-height loop). The height is
260
- // not retained in main the renderer placeholder is the single source of
261
- // truth, and the forward anchor re-reports bounds from it.
262
- ctx.notify.hostToolbarHeightChanged(extent);
277
+ // placeholder div resizes (closing the dynamic-height loop). The notified
278
+ // height IS retained in main (`getHostToolbarHeight`) so a renderer that
279
+ // mounts later can pull/replay it; the renderer placeholder remains the
280
+ // geometry authority — the forward anchor re-reports bounds from it.
281
+ notifyHostToolbarHeight(extent);
263
282
  }
264
283
  function hideHostToolbar() {
265
284
  if (hostToolbarView && hostToolbarViewAdded && !ctx.windows.mainWindow.isDestroyed()) {
@@ -273,7 +292,9 @@ export function createViewManager(ctx) {
273
292
  // non-zero reserved height and re-publishes bounds on the next window
274
293
  // resize, silently re-adding the view we just hid (unstable hide). Zeroing
275
294
  // the height flips the anchor to `present:false` so it stops re-publishing.
276
- ctx.notify.hostToolbarHeightChanged(0);
295
+ // Through the funnel so the retained value follows to 0 — a renderer
296
+ // mounting after the hide must replay 0, not the stale pre-hide height.
297
+ notifyHostToolbarHeight(0);
277
298
  }
278
299
  const hostToolbar = {
279
300
  async loadURL(url) {
@@ -317,11 +338,13 @@ export function createViewManager(ctx) {
317
338
  if (mode !== 'auto') {
318
339
  // Pin immediately: a preload-less/static toolbar never advertises, so
319
340
  // waiting for the next report would leave the strip at height 0.
320
- ctx.notify.hostToolbarHeightChanged(mode.fixed);
341
+ notifyHostToolbarHeight(mode.fixed);
321
342
  }
322
343
  // Switching back to 'auto' deliberately does NOT synthesize a notify —
323
344
  // replaying a stale cached height would flash the old size; the NEXT
324
- // advertiser report drives the placeholder again.
345
+ // advertiser report drives the placeholder again. The RETAINED value
346
+ // survives the switch though: a freshly-mounting renderer still needs
347
+ // the pinned height until that next report lands.
325
348
  },
326
349
  onMessage(channel, handler) {
327
350
  return hostToolbarPort.onMessage(channel, handler);
@@ -1146,6 +1169,7 @@ export function createViewManager(ctx) {
1146
1169
  return popoverView.webContents.id;
1147
1170
  },
1148
1171
  getHostToolbarWebContentsId: () => liveHostToolbarWebContents()?.id ?? null,
1172
+ getHostToolbarHeight: () => hostToolbarLastHeight,
1149
1173
  setNativeSimulatorViewBounds,
1150
1174
  resize,
1151
1175
  setSimulatorDevtoolsBounds,
@@ -18,6 +18,16 @@ export function createWorkspaceService(ctx) {
18
18
  // accept an in-flight write that targets the just-closed project. Reset at
19
19
  // the start of every `openProject` so it never accumulates a stale root.
20
20
  let lastClosedProjectPath = '';
21
+ // Session generation for the onLog closure handed to the adapter: a closed
22
+ // (or switched-away-from) session's compile worker dies asynchronously, so
23
+ // buffered log lines can still arrive through the OLD closure after
24
+ // closeProject/openProject. Each openProject claims a new generation and
25
+ // closeProject invalidates the current one; a stale closure sees the
26
+ // mismatch and drops the line instead of polluting the next project's
27
+ // compile panel. (Deliberately NOT a `currentSession !== null` check: the
28
+ // first compile's log lines arrive while the adapter promise is still
29
+ // pending, before currentSession is assigned.)
30
+ let logGeneration = 0;
21
31
  function sendStatus(status, message, hotReload) {
22
32
  ctx.notify.projectStatus(hotReload ? { status, message, hotReload: true } : { status, message });
23
33
  }
@@ -71,6 +81,14 @@ export function createWorkspaceService(ctx) {
71
81
  : null,
72
82
  async openProject(projectPath) {
73
83
  clearSimulatorServicewechatReferer();
84
+ // Invalidate the outgoing session's onLog BEFORE teardown starts (same
85
+ // order as closeProject below): the dying compile worker flushes
86
+ // buffered lines DURING disposeSession(), and with the old generation
87
+ // still current they would pass the staleness guard and pollute the
88
+ // incoming project's compile-log timeline. The new session claims its
89
+ // own fresh generation further down (after dispose), so its onLog
90
+ // forwards normally.
91
+ logGeneration++;
74
92
  await disposeSession();
75
93
  currentProjectPath = '';
76
94
  // A fresh open starts a clean sandbox window — drop any previously
@@ -92,6 +110,9 @@ export function createWorkspaceService(ctx) {
92
110
  }
93
111
  sendStatus('compiling', '正在编译...');
94
112
  const { compile } = loadWorkbenchSettings();
113
+ // This open owns the log channel until the next open/close bumps the
114
+ // generation — the onLog closure below checks it per line.
115
+ const sessionGeneration = ++logGeneration;
95
116
  let session;
96
117
  try {
97
118
  session = await ctx.adapter.openProject({
@@ -100,6 +121,21 @@ export function createWorkspaceService(ctx) {
100
121
  watch: compile.watch,
101
122
  onRebuild: () => sendStatus('ready', '编译完成,已热更新', true),
102
123
  onBuildError: (err) => sendStatus('error', String(err)),
124
+ // Per-line dmcc log (already filtered in devkit). Stamp the
125
+ // wall-clock capture time here and push verbatim on the dedicated
126
+ // compile-log channel — never through projectStatus, whose
127
+ // one-event-per-payload contract feeds compileEvents. Stale lines
128
+ // (a closed/replaced session's worker flushing its buffers) are
129
+ // dropped via the generation check.
130
+ onLog: (entry) => {
131
+ if (sessionGeneration !== logGeneration)
132
+ return;
133
+ ctx.notify.compileLog({
134
+ stream: entry.stream,
135
+ text: entry.text,
136
+ at: Date.now(),
137
+ });
138
+ },
103
139
  });
104
140
  }
105
141
  catch (err) {
@@ -141,6 +177,9 @@ export function createWorkspaceService(ctx) {
141
177
  };
142
178
  },
143
179
  async closeProject() {
180
+ // Invalidate the active session's onLog BEFORE teardown starts — lines
181
+ // flushed by the dying compile worker must not reach the panel.
182
+ logGeneration++;
144
183
  clearSimulatorServicewechatReferer();
145
184
  await disposeSession();
146
185
  // Record the root being torn down BEFORE clearing it, so a teardown/
@@ -13,7 +13,7 @@ export { installConsoleInstrumentation } from './instrumentation/console.js';
13
13
  export { createAppDataSource } from './instrumentation/app-data.js';
14
14
  export type { AppDataSnapshot } from './instrumentation/app-data.js';
15
15
  /**
16
- * @deprecated Will be removed in the next minor. Under the native-host (sole)
16
+ * @deprecated Will be removed in 0.5.0. Under the native-host (sole)
17
17
  * runtime the simulator top frame is a top-level WebContentsView — the page DOM
18
18
  * lives in child render-host `<webview>` guests, so this top-frame DOM observer
19
19
  * only ever publishes `null`. Panel WXML flows over the main-process
@@ -21,7 +21,7 @@ export type { AppDataSnapshot } from './instrumentation/app-data.js';
21
21
  */
22
22
  export { createWxmlSource } from './instrumentation/wxml.js';
23
23
  /**
24
- * @deprecated Will be removed in the next minor. The host's
24
+ * @deprecated Will be removed in 0.5.0. The host's
25
25
  * `miniapp-snapshot:push/pull` IPC has no receiving end under the native-host
26
26
  * (sole) runtime: the simulator is a top-level WebContentsView without an
27
27
  * embedder (its `sendToHost` fires into the void) and the renderer-side puller
@@ -12,7 +12,7 @@
12
12
  export { installConsoleInstrumentation } from './instrumentation/console.js';
13
13
  export { createAppDataSource } from './instrumentation/app-data.js';
14
14
  /**
15
- * @deprecated Will be removed in the next minor. Under the native-host (sole)
15
+ * @deprecated Will be removed in 0.5.0. Under the native-host (sole)
16
16
  * runtime the simulator top frame is a top-level WebContentsView — the page DOM
17
17
  * lives in child render-host `<webview>` guests, so this top-frame DOM observer
18
18
  * only ever publishes `null`. Panel WXML flows over the main-process
@@ -20,7 +20,7 @@ export { createAppDataSource } from './instrumentation/app-data.js';
20
20
  */
21
21
  export { createWxmlSource } from './instrumentation/wxml.js';
22
22
  /**
23
- * @deprecated Will be removed in the next minor. The host's
23
+ * @deprecated Will be removed in 0.5.0. The host's
24
24
  * `miniapp-snapshot:push/pull` IPC has no receiving end under the native-host
25
25
  * (sole) runtime: the simulator is a top-level WebContentsView without an
26
26
  * embedder (its `sendToHost` fires into the void) and the renderer-side puller
@@ -134,7 +134,17 @@ var ViewChannel = {
134
134
  * main → main-window renderer: push the reserved host-toolbar height so the
135
135
  * renderer placeholder div resizes (closing the dynamic-height loop).
136
136
  */
137
- HostToolbarHeightChanged: "view:host-toolbar:height-changed"
137
+ HostToolbarHeightChanged: "view:host-toolbar:height-changed",
138
+ /**
139
+ * main ← main-window renderer (invoke): pull the last NOTIFIED toolbar
140
+ * height retained in main. Mount-time replay companion to
141
+ * `HostToolbarHeightChanged`: the push listener mounts with the project
142
+ * view and the toolbar's size-advertiser deduplicates (never re-reports),
143
+ * so a height pushed while no project view was mounted would otherwise be
144
+ * lost forever (cold start on the project list races it; close-project →
145
+ * reopen hits it deterministically). No payload; resolves a number.
146
+ */
147
+ HostToolbarGetHeight: "view:host-toolbar:get-height"
138
148
  };
139
149
 
140
150
  // src/preload/runtime/host-toolbar-advertiser.ts