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

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 (49) hide show
  1. package/README.md +86 -4
  2. package/dist/main/api.d.ts +1 -1
  3. package/dist/main/app/app.d.ts +4 -0
  4. package/dist/main/app/app.js +6 -0
  5. package/dist/main/index.bundle.js +147 -14
  6. package/dist/main/ipc/app.d.ts +3 -1
  7. package/dist/main/ipc/app.js +80 -0
  8. package/dist/main/ipc/bridge-router.js +35 -8
  9. package/dist/main/ipc/views.js +10 -1
  10. package/dist/main/services/notifications/renderer-notifier.d.ts +15 -0
  11. package/dist/main/services/notifications/renderer-notifier.js +10 -1
  12. package/dist/main/services/views/view-manager.d.ts +16 -4
  13. package/dist/main/services/views/view-manager.js +32 -8
  14. package/dist/main/services/workbench-context.d.ts +13 -1
  15. package/dist/main/services/workbench-context.js +4 -0
  16. package/dist/main/services/workspace/workspace-service.js +39 -0
  17. package/dist/preload/index.d.ts +2 -2
  18. package/dist/preload/index.js +2 -2
  19. package/dist/preload/windows/host-toolbar-runtime.cjs +11 -1
  20. package/dist/preload/windows/host-toolbar-runtime.cjs.map +2 -2
  21. package/dist/preload/windows/main.cjs +9 -7
  22. package/dist/preload/windows/main.cjs.map +2 -2
  23. package/dist/preload/windows/simulator.cjs.map +1 -1
  24. package/dist/renderer/assets/index-Bx4XqFQ2.js +50 -0
  25. package/dist/renderer/assets/{input-6fpCXV-n.js → input-J1MQ5fra.js} +2 -2
  26. package/dist/renderer/assets/{ipc-transport-D22e4dv7.js → ipc-transport-21qDpt9Y.js} +2 -2
  27. package/dist/renderer/assets/ipc-transport-CVp38I_M.css +1 -0
  28. package/dist/renderer/assets/{popover-CdGdKkkc.js → popover-ChIUUEdj.js} +2 -2
  29. package/dist/renderer/assets/{select-X0sbjwdP.js → select-BgWAbMot.js} +2 -2
  30. package/dist/renderer/assets/{settings-CS4jYaCw.js → settings-CGea5k_R.js} +2 -2
  31. package/dist/renderer/assets/{settings-api-CCWTAao_.js → settings-api-Dit4oL6S.js} +2 -2
  32. package/dist/renderer/assets/{workbenchSettings-Bpzaj3bm.js → workbenchSettings-CFrdVOiP.js} +2 -2
  33. package/dist/renderer/entries/main/index.html +6 -6
  34. package/dist/renderer/entries/popover/index.html +5 -5
  35. package/dist/renderer/entries/settings/index.html +5 -5
  36. package/dist/renderer/entries/workbench-settings/index.html +4 -4
  37. package/dist/shared/ipc-channels.d.ts +17 -0
  38. package/dist/shared/ipc-channels.js +20 -0
  39. package/dist/shared/types.d.ts +61 -0
  40. package/dist/simulator/assets/bridge-channels-BUQ5AbvJ.js +2 -0
  41. package/dist/simulator/assets/device-shell-BEnc2k-W.js +2 -0
  42. package/dist/simulator/assets/{simulator-DHylZf9Z.js → simulator-cV1PERi6.js} +3 -3
  43. package/dist/simulator/assets/simulator-mini-app-CYtW6bMd.js +2 -0
  44. package/dist/simulator/simulator.html +1 -1
  45. package/package.json +4 -4
  46. package/dist/renderer/assets/index-DLzjz1wv.js +0 -50
  47. package/dist/renderer/assets/ipc-transport-Bs8Sf1B2.css +0 -1
  48. package/dist/simulator/assets/device-shell-fH2cI-3q.js +0 -2
  49. 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
 
@@ -42,6 +42,12 @@ launch({
42
42
  rendererDir,
43
43
  apiNamespaces: ['my'],
44
44
  brandingProvider: () => ({ appName: '我的开发工具' }),
45
+ headerAvatarProvider: () => {
46
+ const user = getCurrentUser()
47
+ return user
48
+ ? { displayName: user.name, displayInitial: user.name, avatarUrl: user.avatarUrl }
49
+ : null
50
+ },
45
51
  icon: '/path/to/icon.png',
46
52
  menuBuilder: (mainWindow, menuCtx) => {
47
53
  // menuCtx 是 MenuContext(只读 menu 相关状态)
@@ -147,7 +153,7 @@ src/
147
153
  main.tsx # 主窗口 React 根
148
154
  features/ # 主窗口内的业务区域
149
155
  project-runtime/ # 项目视图 + 工具栏 + 右侧面板切换
150
- right-panel/ # WXML / AppData / Storage 面板
156
+ right-panel/ # WXML / AppData / Storage / 编译 面板
151
157
  popover/ settings/ workbench-settings/
152
158
  shared/
153
159
  components/ # UI 组件(ui / layout / json-viewer / ...)
@@ -177,10 +183,14 @@ src/
177
183
  | ------------------ | -------------------- | ------------------- | ---------------------------------- |
178
184
  | `appName` | `string` | `'Dimina DevTools'` | 窗口标题 |
179
185
  | `adapter` | `CompilationAdapter` | 内置 | 项目编译适配器 |
180
- | `panels` | `BuiltinPanelId[]` | — | **已废弃,运行时忽略**:界面恒显示全部四个内置面板;保留字段仅为兼容仍传它的 host |
186
+ | `panels` | `BuiltinPanelId[]` | — | **已废弃,运行时忽略**:界面恒显示全部内置面板(WXML / AppData / Storage / Console / 编译);保留字段仅为兼容仍传它的 host |
181
187
  | `preloadPath` | `string` | 内置 | 统一的 host 级 preload 入口;native-host simulator(WCV)自动跑其 `.cjs` sibling(`cjsSiblingPreloadPath`) |
182
188
  | `apiNamespaces` | `string[]` | `[]` | 自定义 API 命名空间(如 `['qd']`) |
183
189
  | `brandingProvider` | `() => { appName }` | — | 品牌信息 provider |
190
+ | `headerAvatarProvider` | `() => HeaderAvatarInfo \| null \| Promise<HeaderAvatarInfo \| null>` | — | 内置 header 头像 slot 的宿主数据源;返回 `null` / `undefined` 时隐藏 |
191
+ | `headerAvatarActionHandler` | `() => void \| Promise<void>` | — | 内置 header 头像 slot 的点击处理;未提供时点击无业务副作用 |
192
+ | `headerActionsProvider` | `() => HeaderActionInfo[] \| null \| Promise<HeaderActionInfo[] \| null>` | — | 内置 header 三栏里的少量宿主命令按钮 |
193
+ | `headerActionHandler` | `(id: string) => void \| Promise<void>` | — | 处理 `headerActionsProvider` 返回按钮的点击 |
184
194
  | `headerHeight` | `number` | — | **已废弃,运行时忽略**:头部栏恒为 40px(`HEADER_H`);需要自定义工具栏请用 host toolbar WCV |
185
195
 
186
196
  ### WorkbenchAppConfig(扩展 WorkbenchConfig)
@@ -208,6 +218,78 @@ src/
208
218
  | `instance.registerSimulatorApi(name, handler)` | 注册 simulator 自定义 API,小程序里 `wx.<name>()` 调用(详见下方"Simulator 自定义 API")。返回 `Disposable` |
209
219
  | `instance.ipc` | `IpcRegistry` 实例,`instance.ipc.handle(channel, fn)` 注册自定义 IPC;已绑定 `senderPolicy` 网关 |
210
220
  | `instance.registerTrustedWindow(win)` | 把 host 自有弹窗 `BrowserWindow` 加入受信 sender 集,否则其发起的 `instance.ipc` 调用会被网关拒绝。窗口关闭即移除 |
221
+ | `instance.refreshHeaderAvatar()` | 通知 main renderer 重新调用 `headerAvatarProvider`,用于宿主登录态 / 头像变更后刷新内置 header 头像 |
222
+ | `instance.refreshHeaderActions()` | 通知 main renderer 重新调用 `headerActionsProvider`,用于宿主 action 文案 / disabled 状态变化后刷新 |
223
+
224
+ ### Header Avatar
225
+
226
+ `headerAvatarProvider` 是内置 project header 头像 slot 的唯一数据源。devtools 会通过 `app:getHeaderAvatar` 拉取一个可序列化 DTO 并在固定 header toolbar 内渲染;宿主不需要、也不应该依赖 renderer 内部组件。
227
+
228
+ 该 slot 只承载当前账号 / 身份入口,不是通用业务按钮扩展点。少量必须贴合内置 header 三栏布局的命令按钮使用下方的 `headerActionsProvider`;完整自定义工具栏、复杂状态展示、表单/筛选器、可变高度内容仍应使用 host toolbar WCV(`instance.context.views.hostToolbar.loadURL/loadFile` + `send/onMessage`),由宿主拥有内容、样式和高度。
229
+
230
+ ```typescript
231
+ launch({
232
+ headerAvatarProvider: () => {
233
+ const user = qdmpSession.currentUser()
234
+ return user
235
+ ? {
236
+ displayName: user.nickname,
237
+ displayInitial: user.nickname,
238
+ avatarUrl: user.avatarUrl,
239
+ tooltip: user.nickname,
240
+ }
241
+ : null
242
+ },
243
+ onSetup(instance) {
244
+ qdmpSession.onUserChanged(() => instance.refreshHeaderAvatar())
245
+ },
246
+ })
247
+ ```
248
+
249
+ `HeaderAvatarInfo` 字段:
250
+
251
+ | 字段 | 说明 |
252
+ | --- | --- |
253
+ | `displayName` | 用户名;用于 tooltip / aria 文本和 fallback 首字 |
254
+ | `displayInitial` | 可选 fallback 文本;头像图片缺失或加载失败时取首字显示 |
255
+ | `avatarUrl` | 头像图片 URL 或 data URL |
256
+ | `tooltip` | 可选 tooltip 覆盖;默认用 `displayName` |
257
+
258
+ ### Header Actions
259
+
260
+ `headerActionsProvider` 用于把少量宿主命令渲染到内置 project header 的三栏中。它不是 host toolbar WCV 的替代品:只适合 1-4 个短文本命令,且按钮高度固定、宽度受 header 约束。
261
+
262
+ placement 对应内置 header 的三个区域:
263
+
264
+ | placement | 渲染位置 |
265
+ | --- | --- |
266
+ | `left` | 左区最右侧,位于头像 / 面板显隐 controls 之后 |
267
+ | `center` | 中区最右侧,位于编译模式 / 重新编译 / 编译状态之后 |
268
+ | `right` | 右区最左侧,位于布局 controls / 设置按钮之前 |
269
+
270
+ ```typescript
271
+ launch({
272
+ headerActionsProvider: () => [
273
+ { id: 'open', label: '打开', placement: 'left' },
274
+ { id: 'bindApp', label: '绑定应用', placement: 'left' },
275
+ { id: 'preview', label: '真机预览', placement: 'center' },
276
+ { id: 'upload', label: '上传', placement: 'right' },
277
+ ],
278
+ headerActionHandler: async (id) => {
279
+ await commands.run(id)
280
+ },
281
+ })
282
+ ```
283
+
284
+ `HeaderActionInfo` 字段:
285
+
286
+ | 字段 | 说明 |
287
+ | --- | --- |
288
+ | `id` | 稳定 action id;点击时传给 `headerActionHandler` |
289
+ | `label` | 短按钮文案 |
290
+ | `tooltip` | 可选 tooltip 覆盖;默认用 `label` |
291
+ | `placement` | `left` / `center` / `right`;默认 `right` |
292
+ | `disabled` | 可选禁用态 |
211
293
 
212
294
  ### 内置面板 ID
213
295
 
@@ -483,7 +565,7 @@ launch({
483
565
 
484
566
  ## Host Toolbar(宿主自定义工具栏)
485
567
 
486
- 下游通过 `instance.context.views.hostToolbar` 拥有 devtools 头部下方的工具栏条(一个 WebContentsView):`loadURL` / `loadFile` 加载自己的内容,`setPreloadPath` 注入自己的 preload,`setHeightMode` 钉死或自动跟随内容高度(自动模式要求内容自带 shrink-to-fit 的 `[data-host-toolbar-root]` 包裹元素;`{ fixed }` 校验入参——非有限数或负数同步抛 `TypeError` 且不污染既有模式)。
568
+ 下游通过 `instance.context.views.hostToolbar` 拥有 devtools 头部下方的工具栏条(一个 WebContentsView):`loadURL` / `loadFile` 加载自己的内容,`setPreloadPath` 注入自己的 preload,`setHeightMode` 钉死或自动跟随内容高度(自动模式要求内容自带 shrink-to-fit 的 `[data-host-toolbar-root]` 包裹元素;`{ fixed }` 校验入参——非有限数或负数同步抛 `TypeError` 且不污染既有模式)。主进程保留最后一次下发的高度(`views.getHostToolbarHeight()`),项目视图的占位条挂载时会主动拉取并回放——广播器对已上报的高度去重不再重发,没有这一步,冷启动在项目列表期间的上报、以及关闭项目再打开后的高度都会永久丢失(工具栏条塌缩为 0)。
487
569
 
488
570
  ### 双向消息:onMessage / send
489
571
 
@@ -16,5 +16,5 @@ export { IpcRegistry } from './utils/ipc-registry.js';
16
16
  export type { SenderPolicy } from './utils/ipc-registry.js';
17
17
  export { UpdateManager, createGitHubReleaseChecker } from './services/update/index.js';
18
18
  export type { UpdateManagerOptions, GitHubReleaseCheckerOptions, PickAssetContext, VersionScheme, } from './services/update/index.js';
19
- export type { WorkbenchConfig, WorkbenchAppConfig, CompilationAdapter, ProjectSession, BuiltinPanelId, BuiltinModuleId, UpdateChecker, UpdateInfo, } from '../shared/types.js';
19
+ export type { WorkbenchConfig, WorkbenchAppConfig, CompilationAdapter, ProjectSession, HeaderAvatarInfo, HeaderAvatarProvider, HeaderActionInfo, HeaderActionPlacement, HeaderActionsProvider, BuiltinPanelId, BuiltinModuleId, UpdateChecker, UpdateInfo, } from '../shared/types.js';
20
20
  //# sourceMappingURL=api.d.ts.map
@@ -15,6 +15,10 @@ export interface WorkbenchAppInstance {
15
15
  registerTrustedWindow: (win: BrowserWindow) => Disposable;
16
16
  /** Registers a simulator custom API into this context's registry. */
17
17
  registerSimulatorApi: (name: string, handler: SimulatorApiHandler) => Disposable;
18
+ /** Ask the main renderer to re-read the header avatar provider. */
19
+ refreshHeaderAvatar: () => void;
20
+ /** Ask the main renderer to re-read the header actions provider. */
21
+ refreshHeaderActions: () => void;
18
22
  automationServer?: AutomationServer;
19
23
  updateManager?: UpdateManager;
20
24
  dispose: () => Promise<void>;
@@ -149,6 +149,10 @@ function createContext(config, mainWindow, rendererDir) {
149
149
  appName: config.appName,
150
150
  apiNamespaces: config.apiNamespaces,
151
151
  brandingProvider: config.brandingProvider,
152
+ headerAvatarProvider: config.headerAvatarProvider,
153
+ headerAvatarActionHandler: config.headerAvatarActionHandler,
154
+ headerActionsProvider: config.headerActionsProvider,
155
+ headerActionHandler: config.headerActionHandler,
152
156
  // The host-supplied ProjectsProvider / template types in `shared/types`
153
157
  // are structurally compatible with the main-process equivalents —
154
158
  // these casts are safe; we re-narrow at the workspace-service /
@@ -352,6 +356,8 @@ export async function createDevtoolsRuntime(config = {}) {
352
356
  // teardown, so a single dispose leaves no dead entry behind.
353
357
  registerTrustedWindow: (win) => context.registry.add(registerTrustedWindow(context, win)),
354
358
  registerSimulatorApi: (name, handler) => context.registry.add(toDisposable(context.simulatorApis.register(name, handler))),
359
+ refreshHeaderAvatar: () => context.notify.headerAvatarChanged(),
360
+ refreshHeaderActions: () => context.notify.headerActionsChanged(),
355
361
  dispose: () => disposeContext(context),
356
362
  };
357
363
  if (config.onSetup) {
@@ -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",
@@ -453,7 +467,13 @@ var WindowChannel = {
453
467
  NavigateBack: "window:navigateBack"
454
468
  };
455
469
  var AppChannel = {
456
- GetBranding: "app:getBranding"
470
+ GetBranding: "app:getBranding",
471
+ GetHeaderAvatar: "app:getHeaderAvatar",
472
+ InvokeHeaderAvatar: "app:invokeHeaderAvatar",
473
+ HeaderAvatarChanged: "app:headerAvatarChanged",
474
+ GetHeaderActions: "app:getHeaderActions",
475
+ InvokeHeaderAction: "app:invokeHeaderAction",
476
+ HeaderActionsChanged: "app:headerActionsChanged"
457
477
  };
458
478
  var AutomationChannel = {
459
479
  GetPort: "automation:port"
@@ -966,6 +986,9 @@ function createRendererNotifier(ctx) {
966
986
  projectStatus(payload) {
967
987
  sendToMain(ProjectChannel.Status, payload);
968
988
  },
989
+ compileLog(payload) {
990
+ sendToMain(ProjectChannel.CompileLog, payload);
991
+ },
969
992
  windowNavigateBack() {
970
993
  sendToMain(WindowChannel.NavigateBack);
971
994
  },
@@ -978,6 +1001,12 @@ function createRendererNotifier(ctx) {
978
1001
  hostToolbarHeightChanged(height) {
979
1002
  sendToMain(ViewChannel.HostToolbarHeightChanged, height);
980
1003
  },
1004
+ headerAvatarChanged() {
1005
+ sendToMain(AppChannel.HeaderAvatarChanged);
1006
+ },
1007
+ headerActionsChanged() {
1008
+ sendToMain(AppChannel.HeaderActionsChanged);
1009
+ },
981
1010
  editorOpenFile(payload) {
982
1011
  sendToMain(EditorChannel.OpenFile, payload);
983
1012
  },
@@ -1841,6 +1870,7 @@ function createViewManager(ctx) {
1841
1870
  let hostToolbarViewAdded = false;
1842
1871
  let hostToolbarRuntimeAcquired = false;
1843
1872
  let hostToolbarHeightMode = "auto";
1873
+ let hostToolbarLastHeight = 0;
1844
1874
  const hostToolbarPort = createHostToolbarPortChannel({
1845
1875
  isCurrent: (wc) => liveHostToolbarWebContents() === wc
1846
1876
  });
@@ -1943,9 +1973,13 @@ function createViewManager(ctx) {
1943
1973
  }
1944
1974
  view.setBounds(bounds);
1945
1975
  }
1976
+ function notifyHostToolbarHeight(height) {
1977
+ hostToolbarLastHeight = height;
1978
+ ctx.notify.hostToolbarHeightChanged(height);
1979
+ }
1946
1980
  function setHostToolbarHeight(extent) {
1947
1981
  if (hostToolbarHeightMode !== "auto") return;
1948
- ctx.notify.hostToolbarHeightChanged(extent);
1982
+ notifyHostToolbarHeight(extent);
1949
1983
  }
1950
1984
  function hideHostToolbar() {
1951
1985
  if (hostToolbarView && hostToolbarViewAdded && !ctx.windows.mainWindow.isDestroyed()) {
@@ -1955,7 +1989,7 @@ function createViewManager(ctx) {
1955
1989
  }
1956
1990
  }
1957
1991
  hostToolbarViewAdded = false;
1958
- ctx.notify.hostToolbarHeightChanged(0);
1992
+ notifyHostToolbarHeight(0);
1959
1993
  }
1960
1994
  const hostToolbar = {
1961
1995
  async loadURL(url) {
@@ -1985,7 +2019,7 @@ function createViewManager(ctx) {
1985
2019
  }
1986
2020
  hostToolbarHeightMode = mode;
1987
2021
  if (mode !== "auto") {
1988
- ctx.notify.hostToolbarHeightChanged(mode.fixed);
2022
+ notifyHostToolbarHeight(mode.fixed);
1989
2023
  }
1990
2024
  },
1991
2025
  onMessage(channel, handler) {
@@ -2546,6 +2580,7 @@ function createViewManager(ctx) {
2546
2580
  return popoverView.webContents.id;
2547
2581
  },
2548
2582
  getHostToolbarWebContentsId: () => liveHostToolbarWebContents()?.id ?? null,
2583
+ getHostToolbarHeight: () => hostToolbarLastHeight,
2549
2584
  setNativeSimulatorViewBounds,
2550
2585
  resize,
2551
2586
  setSimulatorDevtoolsBounds,
@@ -2823,6 +2858,7 @@ function createWorkspaceService(ctx) {
2823
2858
  let currentSession = null;
2824
2859
  let currentProjectPath = "";
2825
2860
  let lastClosedProjectPath = "";
2861
+ let logGeneration = 0;
2826
2862
  function sendStatus(status2, message, hotReload) {
2827
2863
  ctx.notify.projectStatus(hotReload ? { status: status2, message, hotReload: true } : { status: status2, message });
2828
2864
  }
@@ -2871,6 +2907,7 @@ function createWorkspaceService(ctx) {
2871
2907
  validateProjectDir: async (dirPath) => provider.validateProjectDir ? provider.validateProjectDir(dirPath) : null,
2872
2908
  async openProject(projectPath) {
2873
2909
  clearSimulatorServicewechatReferer();
2910
+ logGeneration++;
2874
2911
  await disposeSession();
2875
2912
  currentProjectPath = "";
2876
2913
  lastClosedProjectPath = "";
@@ -2881,6 +2918,7 @@ function createWorkspaceService(ctx) {
2881
2918
  }
2882
2919
  sendStatus("compiling", "\u6B63\u5728\u7F16\u8BD1...");
2883
2920
  const { compile } = loadWorkbenchSettings();
2921
+ const sessionGeneration = ++logGeneration;
2884
2922
  let session5;
2885
2923
  try {
2886
2924
  session5 = await ctx.adapter.openProject({
@@ -2888,7 +2926,21 @@ function createWorkspaceService(ctx) {
2888
2926
  sourcemap: true,
2889
2927
  watch: compile.watch,
2890
2928
  onRebuild: () => sendStatus("ready", "\u7F16\u8BD1\u5B8C\u6210\uFF0C\u5DF2\u70ED\u66F4\u65B0", true),
2891
- onBuildError: (err) => sendStatus("error", String(err))
2929
+ onBuildError: (err) => sendStatus("error", String(err)),
2930
+ // Per-line dmcc log (already filtered in devkit). Stamp the
2931
+ // wall-clock capture time here and push verbatim on the dedicated
2932
+ // compile-log channel — never through projectStatus, whose
2933
+ // one-event-per-payload contract feeds compileEvents. Stale lines
2934
+ // (a closed/replaced session's worker flushing its buffers) are
2935
+ // dropped via the generation check.
2936
+ onLog: (entry) => {
2937
+ if (sessionGeneration !== logGeneration) return;
2938
+ ctx.notify.compileLog({
2939
+ stream: entry.stream,
2940
+ text: entry.text,
2941
+ at: Date.now()
2942
+ });
2943
+ }
2892
2944
  });
2893
2945
  } catch (err) {
2894
2946
  clearSimulatorServicewechatReferer();
@@ -2922,6 +2974,7 @@ function createWorkspaceService(ctx) {
2922
2974
  };
2923
2975
  },
2924
2976
  async closeProject() {
2977
+ logGeneration++;
2925
2978
  clearSimulatorServicewechatReferer();
2926
2979
  await disposeSession();
2927
2980
  if (currentProjectPath !== "") lastClosedProjectPath = currentProjectPath;
@@ -3066,7 +3119,11 @@ function createWorkbenchContext(opts) {
3066
3119
  rendererDir: opts.rendererDir,
3067
3120
  apiNamespaces: opts.apiNamespaces ?? [],
3068
3121
  appName: opts.appName ?? "Dimina DevTools",
3069
- brandingProvider: opts.brandingProvider
3122
+ brandingProvider: opts.brandingProvider,
3123
+ headerAvatarProvider: opts.headerAvatarProvider,
3124
+ headerAvatarActionHandler: opts.headerAvatarActionHandler,
3125
+ headerActionsProvider: opts.headerActionsProvider,
3126
+ headerActionHandler: opts.headerActionHandler
3070
3127
  };
3071
3128
  ctx.registry = new DisposableRegistry3();
3072
3129
  ctx.connections = createConnectionRegistry();
@@ -3166,10 +3223,70 @@ function installAppMenu(ctx) {
3166
3223
  }
3167
3224
 
3168
3225
  // src/main/ipc/app.ts
3226
+ function readString(value) {
3227
+ if (typeof value !== "string") return void 0;
3228
+ const trimmed = value.trim();
3229
+ return trimmed.length > 0 ? trimmed : void 0;
3230
+ }
3231
+ function normalizeHeaderAvatar(value) {
3232
+ if (!value || typeof value !== "object") return null;
3233
+ const raw = value;
3234
+ const avatar = {};
3235
+ const displayName = readString(raw.displayName);
3236
+ const displayInitial = readString(raw.displayInitial);
3237
+ const avatarUrl = readString(raw.avatarUrl);
3238
+ const tooltip = readString(raw.tooltip);
3239
+ if (displayName) avatar.displayName = displayName;
3240
+ if (displayInitial) avatar.displayInitial = displayInitial;
3241
+ if (avatarUrl) avatar.avatarUrl = avatarUrl;
3242
+ if (tooltip) avatar.tooltip = tooltip;
3243
+ return avatar.displayName || avatar.displayInitial || avatar.avatarUrl ? avatar : null;
3244
+ }
3245
+ function readPlacement(value) {
3246
+ return value === "left" || value === "center" || value === "right" ? value : void 0;
3247
+ }
3248
+ function normalizeHeaderAction(value) {
3249
+ if (!value || typeof value !== "object") return null;
3250
+ const raw = value;
3251
+ const id = readString(raw.id);
3252
+ const label = readString(raw.label);
3253
+ if (!id || !label) return null;
3254
+ const action = { id, label };
3255
+ const tooltip = readString(raw.tooltip);
3256
+ const placement = readPlacement(raw.placement);
3257
+ if (tooltip) action.tooltip = tooltip;
3258
+ if (placement) action.placement = placement;
3259
+ if (typeof raw.disabled === "boolean") action.disabled = raw.disabled;
3260
+ return action;
3261
+ }
3262
+ function normalizeHeaderActions(value) {
3263
+ if (!Array.isArray(value)) return [];
3264
+ const actions = [];
3265
+ const seen = /* @__PURE__ */ new Set();
3266
+ for (const item of value) {
3267
+ const action = normalizeHeaderAction(item);
3268
+ if (!action || seen.has(action.id)) continue;
3269
+ seen.add(action.id);
3270
+ actions.push(action);
3271
+ }
3272
+ return actions.slice(0, 8);
3273
+ }
3169
3274
  function registerAppIpc(ctx) {
3170
3275
  return new IpcRegistry(ctx.senderPolicy).handle(AppChannel.GetBranding, async () => {
3171
3276
  if (ctx.brandingProvider) return ctx.brandingProvider();
3172
3277
  return { appName: ctx.appName };
3278
+ }).handle(AppChannel.GetHeaderAvatar, async () => {
3279
+ if (!ctx.headerAvatarProvider) return null;
3280
+ return normalizeHeaderAvatar(await ctx.headerAvatarProvider());
3281
+ }).handle(AppChannel.InvokeHeaderAvatar, async () => {
3282
+ await ctx.headerAvatarActionHandler?.();
3283
+ }).handle(AppChannel.GetHeaderActions, async () => {
3284
+ if (!ctx.headerActionsProvider) return [];
3285
+ return normalizeHeaderActions(await ctx.headerActionsProvider());
3286
+ }).handle(AppChannel.InvokeHeaderAction, async (_event, id) => {
3287
+ const actionId = readString(id);
3288
+ if (!actionId) return;
3289
+ await ctx.headerActionHandler?.(actionId);
3173
3290
  });
3174
3291
  }
3175
3292
 
@@ -4756,12 +4873,16 @@ function summarizeBridgeMsg(payload) {
4756
4873
  var API_CALL_TIMEOUT_MS = 5e3;
4757
4874
  function resolveCurrentApp(state, ctx, appId) {
4758
4875
  if (appId) {
4759
- for (const ap of state.appSessions.values()) if (ap.appId === appId) return ap;
4876
+ let match;
4877
+ for (const ap of state.appSessions.values()) if (ap.appId === appId) match = ap;
4878
+ if (match) return match;
4760
4879
  }
4761
4880
  const appInfo = ctx.workspace?.getSession?.()?.appInfo;
4762
4881
  const activeAppId = appInfo?.appId;
4763
4882
  if (activeAppId) {
4764
- for (const ap of state.appSessions.values()) if (ap.appId === activeAppId) return ap;
4883
+ let match;
4884
+ for (const ap of state.appSessions.values()) if (ap.appId === activeAppId) match = ap;
4885
+ if (match) return match;
4765
4886
  }
4766
4887
  let last;
4767
4888
  for (const ap of state.appSessions.values()) last = ap;
@@ -4778,7 +4899,11 @@ function installBridgeRouter(ctx) {
4778
4899
  emitRenderEvent: () => {
4779
4900
  },
4780
4901
  connections: ctx.connections,
4781
- debugTap: createDebugTap({ enabled: resolveDebugTapEnabled() })
4902
+ debugTap: createDebugTap({ enabled: resolveDebugTapEnabled() }),
4903
+ evictAppDataBridges: (ap) => {
4904
+ if (!ctx.appData) return;
4905
+ for (const page of ap.pages.values()) ctx.appData.evictBridge(ap.appId, page.bridgeId);
4906
+ }
4782
4907
  };
4783
4908
  const prewarmPoolSize = resolvePrewarmPoolSize();
4784
4909
  if (prewarmPoolSize > 0) {
@@ -4950,9 +5075,6 @@ function installBridgeRouter(ctx) {
4950
5075
  console.warn(`[bridge-router] DISPOSE rejected: sender belongs to ${senderApp.appSessionId}, target ${target.appSessionId}`);
4951
5076
  return;
4952
5077
  }
4953
- if (ctx.appData) {
4954
- for (const page of target.pages.values()) ctx.appData.evictBridge(target.appId, page.bridgeId);
4955
- }
4956
5078
  void disposeAppSession(state, target.appSessionId);
4957
5079
  };
4958
5080
  ipcMain3.on(BRIDGE_CHANNELS.DISPOSE, onDispose);
@@ -5135,6 +5257,10 @@ async function handleSpawn(state, ctx, event, opts) {
5135
5257
  };
5136
5258
  appSession.onServiceClosed = onServiceClosed;
5137
5259
  serviceWindow.once("closed", onServiceClosed);
5260
+ const onSimulatorDestroyed = () => {
5261
+ void disposeAppSession(state, appSessionId);
5262
+ };
5263
+ simulatorWc.once("destroyed", onSimulatorDestroyed);
5138
5264
  if (usedPool) {
5139
5265
  const bootOnServiceLoad = () => {
5140
5266
  if (serviceWindow.isDestroyed()) return;
@@ -5639,6 +5765,7 @@ async function disposeAppSession(state, appSessionId, opts = {}) {
5639
5765
  const ap = state.appSessions.get(appSessionId);
5640
5766
  if (!ap) return;
5641
5767
  state.appSessions.delete(appSessionId);
5768
+ state.evictAppDataBridges(ap);
5642
5769
  for (const [requestId, pending] of state.pendingApiCalls) {
5643
5770
  if (pending.appSessionId !== appSessionId) continue;
5644
5771
  clearTimeout(pending.timer);
@@ -5804,7 +5931,7 @@ function registerViewsIpc(ctx) {
5804
5931
  args
5805
5932
  );
5806
5933
  ctx.views.setHostToolbarBounds(bounds);
5807
- });
5934
+ }).handle(ViewChannel.HostToolbarGetHeight, () => ctx.views.getHostToolbarHeight());
5808
5935
  const onAdvertiseHeight = (event, ...args) => {
5809
5936
  if (event.sender.id !== ctx.views.getHostToolbarWebContentsId()) return;
5810
5937
  let extent;
@@ -8768,6 +8895,10 @@ function createContext(config, mainWindow, rendererDir2) {
8768
8895
  appName: config.appName,
8769
8896
  apiNamespaces: config.apiNamespaces,
8770
8897
  brandingProvider: config.brandingProvider,
8898
+ headerAvatarProvider: config.headerAvatarProvider,
8899
+ headerAvatarActionHandler: config.headerAvatarActionHandler,
8900
+ headerActionsProvider: config.headerActionsProvider,
8901
+ headerActionHandler: config.headerActionHandler,
8771
8902
  // The host-supplied ProjectsProvider / template types in `shared/types`
8772
8903
  // are structurally compatible with the main-process equivalents —
8773
8904
  // these casts are safe; we re-narrow at the workspace-service /
@@ -8904,6 +9035,8 @@ async function createDevtoolsRuntime(config = {}) {
8904
9035
  // teardown, so a single dispose leaves no dead entry behind.
8905
9036
  registerTrustedWindow: (win) => context.registry.add(registerTrustedWindow(context, win)),
8906
9037
  registerSimulatorApi: (name, handler) => context.registry.add(toDisposable8(context.simulatorApis.register(name, handler))),
9038
+ refreshHeaderAvatar: () => context.notify.headerAvatarChanged(),
9039
+ refreshHeaderActions: () => context.notify.headerActionsChanged(),
8907
9040
  dispose: () => disposeContext(context)
8908
9041
  };
8909
9042
  if (config.onSetup) {
@@ -1,4 +1,6 @@
1
1
  import type { WorkbenchContext } from '../services/workbench-context.js';
2
2
  import type { Disposable } from '@dimina-kit/electron-deck/main';
3
- export declare function registerAppIpc(ctx: Pick<WorkbenchContext, 'brandingProvider' | 'appName' | 'senderPolicy'>): Disposable;
3
+ type AppIpcContext = Pick<WorkbenchContext, 'brandingProvider' | 'appName' | 'headerAvatarProvider' | 'headerAvatarActionHandler' | 'headerActionsProvider' | 'headerActionHandler' | 'senderPolicy'>;
4
+ export declare function registerAppIpc(ctx: AppIpcContext): Disposable;
5
+ export {};
4
6
  //# sourceMappingURL=app.d.ts.map
@@ -1,11 +1,91 @@
1
1
  import { AppChannel } from '../../shared/ipc-channels.js';
2
2
  import { IpcRegistry } from '../utils/ipc-registry.js';
3
+ function readString(value) {
4
+ if (typeof value !== 'string')
5
+ return undefined;
6
+ const trimmed = value.trim();
7
+ return trimmed.length > 0 ? trimmed : undefined;
8
+ }
9
+ function normalizeHeaderAvatar(value) {
10
+ if (!value || typeof value !== 'object')
11
+ return null;
12
+ const raw = value;
13
+ const avatar = {};
14
+ const displayName = readString(raw.displayName);
15
+ const displayInitial = readString(raw.displayInitial);
16
+ const avatarUrl = readString(raw.avatarUrl);
17
+ const tooltip = readString(raw.tooltip);
18
+ if (displayName)
19
+ avatar.displayName = displayName;
20
+ if (displayInitial)
21
+ avatar.displayInitial = displayInitial;
22
+ if (avatarUrl)
23
+ avatar.avatarUrl = avatarUrl;
24
+ if (tooltip)
25
+ avatar.tooltip = tooltip;
26
+ return avatar.displayName || avatar.displayInitial || avatar.avatarUrl ? avatar : null;
27
+ }
28
+ function readPlacement(value) {
29
+ return value === 'left' || value === 'center' || value === 'right' ? value : undefined;
30
+ }
31
+ function normalizeHeaderAction(value) {
32
+ if (!value || typeof value !== 'object')
33
+ return null;
34
+ const raw = value;
35
+ const id = readString(raw.id);
36
+ const label = readString(raw.label);
37
+ if (!id || !label)
38
+ return null;
39
+ const action = { id, label };
40
+ const tooltip = readString(raw.tooltip);
41
+ const placement = readPlacement(raw.placement);
42
+ if (tooltip)
43
+ action.tooltip = tooltip;
44
+ if (placement)
45
+ action.placement = placement;
46
+ if (typeof raw.disabled === 'boolean')
47
+ action.disabled = raw.disabled;
48
+ return action;
49
+ }
50
+ function normalizeHeaderActions(value) {
51
+ if (!Array.isArray(value))
52
+ return [];
53
+ const actions = [];
54
+ const seen = new Set();
55
+ for (const item of value) {
56
+ const action = normalizeHeaderAction(item);
57
+ if (!action || seen.has(action.id))
58
+ continue;
59
+ seen.add(action.id);
60
+ actions.push(action);
61
+ }
62
+ return actions.slice(0, 8);
63
+ }
3
64
  export function registerAppIpc(ctx) {
4
65
  return new IpcRegistry(ctx.senderPolicy)
5
66
  .handle(AppChannel.GetBranding, async () => {
6
67
  if (ctx.brandingProvider)
7
68
  return ctx.brandingProvider();
8
69
  return { appName: ctx.appName };
70
+ })
71
+ .handle(AppChannel.GetHeaderAvatar, async () => {
72
+ if (!ctx.headerAvatarProvider)
73
+ return null;
74
+ return normalizeHeaderAvatar(await ctx.headerAvatarProvider());
75
+ })
76
+ .handle(AppChannel.InvokeHeaderAvatar, async () => {
77
+ await ctx.headerAvatarActionHandler?.();
78
+ })
79
+ .handle(AppChannel.GetHeaderActions, async () => {
80
+ if (!ctx.headerActionsProvider)
81
+ return [];
82
+ return normalizeHeaderActions(await ctx.headerActionsProvider());
83
+ })
84
+ .handle(AppChannel.InvokeHeaderAction, async (_event, id) => {
85
+ const actionId = readString(id);
86
+ if (!actionId)
87
+ return;
88
+ await ctx.headerActionHandler?.(actionId);
9
89
  });
10
90
  }
11
91
  //# sourceMappingURL=app.js.map