@dimina-kit/devtools 0.4.0-dev.20260828161241 → 0.4.0-dev.20260907055341

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 (277) hide show
  1. package/README.md +52 -729
  2. package/dist/main/app/app.d.ts +27 -7
  3. package/dist/main/app/app.js +285 -681
  4. package/dist/main/app/editor-view.d.ts +21 -0
  5. package/dist/main/app/editor-view.js +71 -0
  6. package/dist/main/app/global-mirrors.d.ts +23 -0
  7. package/dist/main/app/global-mirrors.js +37 -0
  8. package/dist/main/app/host-sidebar-default.d.ts +19 -0
  9. package/dist/main/app/host-sidebar-default.js +44 -0
  10. package/dist/main/app/menu-setup.d.ts +13 -0
  11. package/dist/main/app/menu-setup.js +49 -0
  12. package/dist/main/app/open-project-ipc.d.ts +13 -0
  13. package/dist/main/app/open-project-ipc.js +21 -0
  14. package/dist/main/app/project-list-surface.d.ts +31 -0
  15. package/dist/main/app/project-list-surface.js +20 -0
  16. package/dist/main/app/project-window.d.ts +36 -0
  17. package/dist/main/app/project-window.js +93 -0
  18. package/dist/main/app/servers.d.ts +17 -0
  19. package/dist/main/app/servers.js +60 -0
  20. package/dist/main/app/ui-extension-targets.d.ts +42 -0
  21. package/dist/main/app/ui-extension-targets.js +101 -0
  22. package/dist/main/app/window-close-reveal.harness.d.ts +34 -0
  23. package/dist/main/app/window-close-reveal.harness.js +65 -0
  24. package/dist/main/app/window-events.d.ts +15 -0
  25. package/dist/main/app/window-events.js +45 -0
  26. package/dist/main/app/window-runtime-services.d.ts +41 -0
  27. package/dist/main/app/window-runtime-services.js +178 -0
  28. package/dist/main/app/workbench-modules.d.ts +24 -0
  29. package/dist/main/app/workbench-modules.js +51 -0
  30. package/dist/main/app/workbench-window.d.ts +85 -0
  31. package/dist/main/app/workbench-window.js +310 -0
  32. package/dist/main/index.bundle.js +10669 -9096
  33. package/dist/main/ipc/app.d.ts +4 -1
  34. package/dist/main/ipc/app.js +4 -3
  35. package/dist/main/ipc/device-picker.d.ts +16 -0
  36. package/dist/main/ipc/device-picker.js +27 -0
  37. package/dist/main/ipc/index.d.ts +1 -0
  38. package/dist/main/ipc/index.js +1 -0
  39. package/dist/main/ipc/internal-devtools.d.ts +2 -1
  40. package/dist/main/ipc/internal-devtools.js +4 -3
  41. package/dist/main/ipc/popover.d.ts +4 -1
  42. package/dist/main/ipc/popover.js +25 -9
  43. package/dist/main/ipc/project-create.d.ts +2 -1
  44. package/dist/main/ipc/project-create.js +6 -5
  45. package/dist/main/ipc/project-fs.d.ts +3 -1
  46. package/dist/main/ipc/project-fs.js +17 -17
  47. package/dist/main/ipc/projects.d.ts +3 -3
  48. package/dist/main/ipc/projects.js +13 -12
  49. package/dist/main/ipc/session.d.ts +4 -1
  50. package/dist/main/ipc/session.js +16 -11
  51. package/dist/main/ipc/settings.d.ts +4 -1
  52. package/dist/main/ipc/settings.js +12 -19
  53. package/dist/main/ipc/simulator-module.d.ts +5 -0
  54. package/dist/main/ipc/simulator-module.js +9 -5
  55. package/dist/main/ipc/simulator.d.ts +4 -1
  56. package/dist/main/ipc/simulator.js +12 -18
  57. package/dist/main/ipc/tooltip.d.ts +2 -1
  58. package/dist/main/ipc/tooltip.js +8 -7
  59. package/dist/main/ipc/views.d.ts +4 -1
  60. package/dist/main/ipc/views.js +20 -9
  61. package/dist/main/runtime/devtools-backend-before-quit.harness.d.ts +2 -20
  62. package/dist/main/runtime/devtools-backend-before-quit.harness.js +14 -283
  63. package/dist/main/runtime/devtools-backend.js +14 -3
  64. package/dist/main/runtime/devtools-runtime-mock.harness.d.ts +21 -0
  65. package/dist/main/runtime/devtools-runtime-mock.harness.js +313 -0
  66. package/dist/main/services/app-services.d.ts +62 -0
  67. package/dist/main/services/app-services.js +64 -0
  68. package/dist/main/services/automation/connection-target.d.ts +36 -0
  69. package/dist/main/services/automation/connection-target.js +29 -0
  70. package/dist/main/services/automation/console-bridge.d.ts +28 -0
  71. package/dist/main/services/automation/console-bridge.js +91 -0
  72. package/dist/main/services/automation/handlers/app.js +4 -4
  73. package/dist/main/services/automation/handlers/element.js +55 -21
  74. package/dist/main/services/automation/index.d.ts +12 -1
  75. package/dist/main/services/automation/index.js +54 -133
  76. package/dist/main/services/default-adapter.js +1 -1
  77. package/dist/main/services/fs-bridge-protocol.d.ts +61 -0
  78. package/dist/main/services/fs-bridge-protocol.js +34 -0
  79. package/dist/main/services/fs-watch-sse.d.ts +8 -1
  80. package/dist/main/services/fs-watch-sse.js +19 -5
  81. package/dist/main/services/mcp/connection-owner.d.ts +38 -0
  82. package/dist/main/services/mcp/connection-owner.js +79 -0
  83. package/dist/main/services/mcp/index.d.ts +3 -1
  84. package/dist/main/services/mcp/index.js +4 -3
  85. package/dist/main/services/mcp/opened-project.d.ts +88 -0
  86. package/dist/main/services/mcp/opened-project.js +60 -0
  87. package/dist/main/services/mcp/target-manager.d.ts +71 -23
  88. package/dist/main/services/mcp/target-manager.js +269 -142
  89. package/dist/main/services/mcp/target-selection.d.ts +47 -0
  90. package/dist/main/services/mcp/target-selection.js +71 -0
  91. package/dist/main/services/mcp/tools/context-tools.js +20 -5
  92. package/dist/main/services/mcp/tools/project-tools.d.ts +29 -6
  93. package/dist/main/services/mcp/tools/project-tools.js +37 -25
  94. package/dist/main/services/module.d.ts +13 -5
  95. package/dist/main/services/notifications/renderer-notifier.d.ts +28 -4
  96. package/dist/main/services/notifications/renderer-notifier.js +15 -3
  97. package/dist/main/services/projects/local-provider.js +2 -0
  98. package/dist/main/services/projects/project-repository.d.ts +31 -1
  99. package/dist/main/services/projects/project-repository.js +144 -20
  100. package/dist/main/services/projects/types.d.ts +31 -8
  101. package/dist/main/services/safe-area/index.d.ts +19 -5
  102. package/dist/main/services/safe-area/index.js +38 -13
  103. package/dist/main/services/simulator/ui-extensions.d.ts +8 -0
  104. package/dist/main/services/simulator/ui-extensions.js +16 -6
  105. package/dist/main/services/simulator-storage/index.d.ts +12 -1
  106. package/dist/main/services/simulator-storage/index.js +22 -7
  107. package/dist/main/services/simulator-storage/window-scope.d.ts +20 -0
  108. package/dist/main/services/simulator-storage/window-scope.js +20 -0
  109. package/dist/main/services/views/host-slot-port-channel.d.ts +20 -3
  110. package/dist/main/services/views/host-slot-port-channel.js +33 -5
  111. package/dist/main/services/views/managed-web-contents-view.js +21 -1
  112. package/dist/main/services/views/native-simulator-view.js +13 -16
  113. package/dist/main/services/views/overlay-panels-view.d.ts +13 -0
  114. package/dist/main/services/views/overlay-panels-view.js +31 -0
  115. package/dist/main/services/views/view-manager.d.ts +1 -1
  116. package/dist/main/services/views/view-manager.js +3 -0
  117. package/dist/main/services/window-contexts/context-router.d.ts +36 -0
  118. package/dist/main/services/window-contexts/context-router.js +55 -0
  119. package/dist/main/services/workbench-coi-host.d.ts +52 -0
  120. package/dist/main/services/workbench-coi-host.js +227 -0
  121. package/dist/main/services/workbench-coi-server.js +75 -133
  122. package/dist/main/services/workbench-coi-shutdown.d.ts +40 -0
  123. package/dist/main/services/workbench-coi-shutdown.js +69 -0
  124. package/dist/main/services/workbench-coi-static.d.ts +23 -0
  125. package/dist/main/services/workbench-coi-static.js +87 -0
  126. package/dist/main/services/workbench-context.d.ts +27 -3
  127. package/dist/main/services/workbench-context.js +10 -5
  128. package/dist/main/services/workspace/compile-mode-adapter.d.ts +18 -0
  129. package/dist/main/services/workspace/compile-mode-adapter.js +57 -0
  130. package/dist/main/services/workspace/compile-mode-store.d.ts +25 -0
  131. package/dist/main/services/workspace/compile-mode-store.js +71 -0
  132. package/dist/main/services/workspace/workspace-compile-modes.d.ts +59 -0
  133. package/dist/main/services/workspace/workspace-compile-modes.js +93 -0
  134. package/dist/main/services/workspace/workspace-referer.d.ts +17 -0
  135. package/dist/main/services/workspace/workspace-referer.js +26 -0
  136. package/dist/main/services/workspace/workspace-service.d.ts +4 -0
  137. package/dist/main/services/workspace/workspace-service.js +51 -84
  138. package/dist/main/services/workspace/workspace-thumbnails.d.ts +22 -0
  139. package/dist/main/services/workspace/workspace-thumbnails.js +67 -0
  140. package/dist/main/utils/ipc-context-source.d.ts +40 -0
  141. package/dist/main/utils/ipc-context-source.js +23 -0
  142. package/dist/main/utils/ipc-registry.d.ts +42 -36
  143. package/dist/main/utils/ipc-registry.js +117 -50
  144. package/dist/main/utils/sender-policy.d.ts +9 -0
  145. package/dist/main/utils/sender-policy.js +26 -3
  146. package/dist/main/utils/theme.d.ts +3 -2
  147. package/dist/main/utils/theme.js +3 -2
  148. package/dist/main/windows/main-window/create.d.ts +16 -0
  149. package/dist/main/windows/main-window/create.js +19 -11
  150. package/dist/main/windows/main-window/events.d.ts +1 -1
  151. package/dist/main/windows/main-window/events.js +26 -13
  152. package/dist/main/windows/main-window/index.d.ts +1 -1
  153. package/dist/main/windows/main-window/index.js +1 -1
  154. package/dist/native-host/common/common.js +94 -73
  155. package/dist/native-host/render/render.js +4079 -3171
  156. package/dist/native-host/service/service.js +2 -2
  157. package/dist/preload/runtime/native-host.d.ts +4 -0
  158. package/dist/preload/runtime/temp-files.js +9 -2
  159. package/dist/preload/runtime/wait-for-slot-root.d.ts +7 -4
  160. package/dist/preload/runtime/wait-for-slot-root.js +36 -8
  161. package/dist/preload/windows/host-dialog-runtime.cjs +10 -3
  162. package/dist/preload/windows/host-dialog-runtime.cjs.map +2 -2
  163. package/dist/preload/windows/host-sidebar-runtime.cjs +10 -3
  164. package/dist/preload/windows/host-sidebar-runtime.cjs.map +2 -2
  165. package/dist/preload/windows/host-toolbar-runtime.cjs +10 -3
  166. package/dist/preload/windows/host-toolbar-runtime.cjs.map +2 -2
  167. package/dist/preload/windows/main.cjs +181 -4
  168. package/dist/preload/windows/main.cjs.map +4 -4
  169. package/dist/preload/windows/simulator.cjs +10 -0
  170. package/dist/preload/windows/simulator.cjs.map +2 -2
  171. package/dist/preload/windows/simulator.js +10 -0
  172. package/dist/render-host/pageFrame.html +1 -1
  173. package/dist/render-host/preload.cjs +16 -4
  174. package/dist/renderer/assets/button-CB2MWjHY.js +2 -0
  175. package/dist/renderer/assets/constants-CkoGPQTS.js +2 -0
  176. package/dist/renderer/assets/constants-zQZ2-y21.js +2 -0
  177. package/dist/renderer/assets/{createLucideIcon-Bqi8hpdi.js → createLucideIcon-cZ9FlZZw.js} +2 -2
  178. package/dist/renderer/assets/devicePicker-H2eIO4e1.js +2 -0
  179. package/dist/renderer/assets/dialog-pHzEA8Vx.js +46 -0
  180. package/dist/renderer/assets/dist-B5HADirS.js +2 -0
  181. package/dist/renderer/assets/{dist-DKWdE1TK.js → dist-DIFlRMKe.js} +2 -2
  182. package/dist/renderer/assets/hostSidebarDefault-DRWh6OCt.js +2 -0
  183. package/dist/renderer/assets/index-CEL_XFge.js +2 -0
  184. package/dist/renderer/assets/{input-Ct3alvkq.js → input-CAObw95L.js} +2 -2
  185. package/dist/renderer/assets/ipc-channels-overlays-gIPUFnqc.js +2 -0
  186. package/dist/renderer/assets/jsx-runtime-DICEQH_0.css +1 -0
  187. package/dist/renderer/assets/{jsx-runtime-DGLHmmOq.js → jsx-runtime-DIw5wnYl.js} +1 -1
  188. package/dist/renderer/assets/plus-BQ9u5EbL.js +2 -0
  189. package/dist/renderer/assets/popover-Cw7G2Ctp.js +2 -0
  190. package/dist/renderer/assets/presets-DMZOb9ay.js +2 -0
  191. package/dist/renderer/assets/project-api-YTOr_Yvo.js +2 -0
  192. package/dist/renderer/assets/{projectCreateDialog-Bbjz75bP.js → projectCreateDialog-BmLqiYxn.js} +2 -2
  193. package/dist/renderer/assets/search-BMvuYF48.js +2 -0
  194. package/dist/renderer/assets/select-nFP95TxM.js +2 -0
  195. package/dist/renderer/assets/settings-CnpVfSWt.js +2 -0
  196. package/dist/renderer/assets/settings-api-Ccsf-Nsj.js +2 -0
  197. package/dist/renderer/assets/settings-tab-bar-CAzrv32m.js +2 -0
  198. package/dist/renderer/assets/smartphone-Bb2GdIKy.js +2 -0
  199. package/dist/renderer/assets/tabs-CPKN3Mmq.js +2 -0
  200. package/dist/renderer/assets/{tooltip-B_-_YVbw.js → tooltip-D6AxXRk5.js} +2 -2
  201. package/dist/renderer/assets/types-Bh0hA7El.js +2 -0
  202. package/dist/renderer/assets/updateDialog-CwndJ0Bl.js +2 -0
  203. package/dist/renderer/assets/{utils-kfQov6Pa.js → utils-sEB5OZEQ.js} +2 -2
  204. package/dist/renderer/assets/view-api-B-2zPBR5.js +2 -0
  205. package/dist/renderer/assets/view-ids-BfaDVV5P.js +2 -0
  206. package/dist/renderer/assets/workbench-BLJ0wKsg.js +5 -0
  207. package/dist/renderer/assets/{workbenchSettings-CV4LVA9b.js → workbenchSettings-YD0Ctbm_.js} +3 -3
  208. package/dist/renderer/entries/device-picker/index.html +28 -0
  209. package/dist/renderer/entries/host-sidebar-default/index.html +6 -5
  210. package/dist/renderer/entries/main/index.html +16 -17
  211. package/dist/renderer/entries/popover/index.html +16 -10
  212. package/dist/renderer/entries/project-create-dialog/index.html +12 -12
  213. package/dist/renderer/entries/settings/index.html +10 -10
  214. package/dist/renderer/entries/tooltip/index.html +5 -5
  215. package/dist/renderer/entries/update-dialog/index.html +10 -10
  216. package/dist/renderer/entries/workbench/index.html +49 -0
  217. package/dist/renderer/entries/workbench-settings/index.html +12 -11
  218. package/dist/service-host/host-env-update.cjs +23 -0
  219. package/dist/service-host/preload.cjs +24 -49
  220. package/dist/service-host/service-message-queue.cjs +71 -0
  221. package/dist/service-host/sync-impls/system-info.js +31 -9
  222. package/dist/shared/compile-mode-state.d.ts +73 -0
  223. package/dist/shared/compile-mode-state.js +100 -0
  224. package/dist/shared/compile-modes.d.ts +152 -0
  225. package/dist/shared/compile-modes.js +237 -0
  226. package/dist/shared/ipc-channels-overlays.d.ts +8 -2
  227. package/dist/shared/ipc-channels-overlays.js +19 -2
  228. package/dist/shared/ipc-channels.d.ts +5 -10
  229. package/dist/shared/ipc-channels.js +17 -14
  230. package/dist/shared/ipc-schemas.d.ts +76 -24
  231. package/dist/shared/ipc-schemas.js +55 -10
  232. package/dist/shared/project-window-types.d.ts +42 -0
  233. package/dist/shared/project-window-types.js +2 -0
  234. package/dist/shared/types.d.ts +43 -17
  235. package/dist/shared/view-ids.d.ts +1 -0
  236. package/dist/shared/view-ids.js +3 -1
  237. package/dist/simulator/assets/bridge-channels-B3JAmRfl.js +2 -0
  238. package/dist/simulator/assets/device-shell-ATEhHV2_.css +1 -0
  239. package/dist/simulator/assets/device-shell-pxnZZp10.js +431 -0
  240. package/dist/simulator/assets/simulator-Dwp7lnvu.js +10 -0
  241. package/dist/simulator/assets/simulator-mini-app-DFo7fhi5.js +2 -0
  242. package/dist/simulator/assets/simulator-ui-B2ddJB1R.js +2 -0
  243. package/dist/simulator/simulator.html +3 -3
  244. package/dist/simulator/temp-files.d.ts +6 -0
  245. package/dist/simulator/temp-files.js +46 -4
  246. package/dist/simulator/types.d.ts +7 -0
  247. package/dist/vscode-workbench/assets/__vite-browser-external-JIkWewFf.js +1 -0
  248. package/dist/vscode-workbench/assets/{dist-CMfmHflW.js → dist-CXG-u1VB.js} +3 -3
  249. package/dist/vscode-workbench/assets/{iconv-lite-umd-Bx8CgaZq.js → iconv-lite-umd-Dxx9Qyb4.js} +1 -1
  250. package/dist/vscode-workbench/assets/{index-CdG6YWhW.js → index-6VJMsaWj.js} +460 -460
  251. package/dist/vscode-workbench/assets/{jschardet-M0qgd2lN.js → jschardet-Ba3D16vt.js} +1 -1
  252. package/dist/vscode-workbench/index.html +1 -1
  253. package/package.json +11 -8
  254. package/dist/renderer/assets/button-D5Q2wkXq.js +0 -2
  255. package/dist/renderer/assets/constants-D7DfVqMp.js +0 -2
  256. package/dist/renderer/assets/constants-D8zsejFq.js +0 -2
  257. package/dist/renderer/assets/dialog-e1TecTOY.js +0 -46
  258. package/dist/renderer/assets/hostSidebarDefault-B3yLfy8j.js +0 -2
  259. package/dist/renderer/assets/index-CsPD68N_.js +0 -5
  260. package/dist/renderer/assets/ipc-channels-overlays-LsDzGAtV.js +0 -2
  261. package/dist/renderer/assets/jsx-runtime-2vXHouwr.css +0 -1
  262. package/dist/renderer/assets/popover-Dix5cm_b.js +0 -2
  263. package/dist/renderer/assets/project-api-k7Oq9iaq.js +0 -2
  264. package/dist/renderer/assets/select-acGb0AmJ.js +0 -2
  265. package/dist/renderer/assets/settings-CQrTMr43.js +0 -2
  266. package/dist/renderer/assets/settings-api-CrUQ9rYV.js +0 -2
  267. package/dist/renderer/assets/settings-tab-bar-Cw1kVIf2.js +0 -2
  268. package/dist/renderer/assets/types-CsEKtFyr.js +0 -2
  269. package/dist/renderer/assets/updateDialog-NsF8223A.js +0 -2
  270. package/dist/renderer/assets/view-api-CLn3rcQE.js +0 -2
  271. package/dist/simulator/assets/bridge-channels-zhsumfky.js +0 -2
  272. package/dist/simulator/assets/device-shell-CmtM1QxP.js +0 -2
  273. package/dist/simulator/assets/device-shell-iFZo1igL.css +0 -1
  274. package/dist/simulator/assets/simulator-CIobn3Mh.js +0 -10
  275. package/dist/simulator/assets/simulator-mini-app-CRe6n9br.js +0 -2
  276. package/dist/simulator/assets/simulator-ui-C_cP602X.js +0 -2
  277. package/dist/vscode-workbench/assets/__vite-browser-external-Bc1vizTf.js +0 -1
@@ -0,0 +1,40 @@
1
+ import type { WebContents } from 'electron';
2
+ import type { SenderPolicy } from './ipc-registry.js';
3
+ /**
4
+ * Resolves which window context owns an incoming IPC sender.
5
+ *
6
+ * `register*Ipc` takes one of these instead of a bare context so a single
7
+ * handler body serves both shapes: one window (every accepted sender maps to
8
+ * that window's context) and several windows (each message is answered by the
9
+ * context of the window it actually came from).
10
+ */
11
+ export interface IpcContextSource<T> {
12
+ /**
13
+ * Set only by the adapter over a context that carries no `senderPolicy` —
14
+ * narrow test contexts. It keeps {@link IpcRegistry} in its policy-less mode
15
+ * (accept every sender, skip the main-frame check), so wrapping such a
16
+ * context in a source is not itself a trust change.
17
+ */
18
+ readonly ungated?: boolean;
19
+ /** The context owning `sender`, or null when no context claims it. */
20
+ resolve(sender: WebContents): T | null;
21
+ /** Every context this source can hand out. */
22
+ list(): T[];
23
+ }
24
+ /**
25
+ * What every `register*Ipc` accepts: a router covering all live windows, or a
26
+ * single context standing in for itself.
27
+ */
28
+ export type IpcInput<T> = T | IpcContextSource<T>;
29
+ /** Structural test: a source exposes both members, a context exposes neither. */
30
+ export declare function isIpcContextSource<T>(value: unknown): value is IpcContextSource<T>;
31
+ /**
32
+ * Accept either a source or a single context. A single context gates on its
33
+ * own `senderPolicy` — the same predicate a policy-only registry applies — and
34
+ * owns every sender that predicate accepts. A context without a policy stays
35
+ * ungated so narrow test contexts keep the behaviour a bare registry gave them.
36
+ */
37
+ export declare function toIpcContextSource<T extends {
38
+ senderPolicy?: SenderPolicy;
39
+ }>(input: T | IpcContextSource<T>): IpcContextSource<T>;
40
+ //# sourceMappingURL=ipc-context-source.d.ts.map
@@ -0,0 +1,23 @@
1
+ /** Structural test: a source exposes both members, a context exposes neither. */
2
+ export function isIpcContextSource(value) {
3
+ if (value == null || typeof value !== 'object')
4
+ return false;
5
+ const candidate = value;
6
+ return typeof candidate.resolve === 'function' && typeof candidate.list === 'function';
7
+ }
8
+ /**
9
+ * Accept either a source or a single context. A single context gates on its
10
+ * own `senderPolicy` — the same predicate a policy-only registry applies — and
11
+ * owns every sender that predicate accepts. A context without a policy stays
12
+ * ungated so narrow test contexts keep the behaviour a bare registry gave them.
13
+ */
14
+ export function toIpcContextSource(input) {
15
+ if (isIpcContextSource(input))
16
+ return input;
17
+ const ctx = input;
18
+ const policy = ctx.senderPolicy;
19
+ if (!policy)
20
+ return { ungated: true, resolve: () => ctx, list: () => [ctx] };
21
+ return { resolve: (sender) => (policy(sender) ? ctx : null), list: () => [ctx] };
22
+ }
23
+ //# sourceMappingURL=ipc-context-source.js.map
@@ -1,6 +1,7 @@
1
- import type { IpcMainEvent } from 'electron';
1
+ import type { IpcMainEvent, IpcMainInvokeEvent } from 'electron';
2
2
  import { ipcMain } from 'electron';
3
3
  import { type Disposable, type IpcSenderPolicy } from '@dimina-kit/electron-deck/main';
4
+ import type { IpcContextSource } from './ipc-context-source.js';
4
5
  type HandleFn = Parameters<typeof ipcMain.handle>[1];
5
6
  type ListenerFn = Parameters<typeof ipcMain.on>[1];
6
7
  /**
@@ -10,57 +11,62 @@ type ListenerFn = Parameters<typeof ipcMain.on>[1];
10
11
  * the channel, false otherwise.
11
12
  */
12
13
  export type SenderPolicy = IpcSenderPolicy;
13
- /**
14
- * Defense-in-depth frame check (sits alongside the sender white-list). A trusted
15
- * webContents could embed a sub-frame of arbitrary origin; only its top (main)
16
- * frame should reach gated IPC, so a sub-frame can't spoof the trusted sender.
17
- *
18
- * Verified on Electron 41 that `event.senderFrame` is reliably present on
19
- * invoke / send / sendSync — including the editor's `beforeunload` sendSync
20
- * write — and equals `sender.mainFrame` for top-frame traffic, so this never
21
- * mis-rejects legitimate callers.
22
- *
23
- * Fail-closed on a null frame for REAL events: a sub-frame can send a message
24
- * and immediately navigate/destroy itself, so by delivery time `senderFrame`
25
- * resolves to null — allowing that would let the navigate-after-send trick
26
- * bypass the boundary. A real Electron event always exposes the `senderFrame`
27
- * property and `sender.mainFrame`, so we can tell a real event (frame-modeled,
28
- * possibly null) from a frame-unaware unit-test stub (neither present) and only
29
- * skip the check for the latter (the sender-id white-list still gates tests).
30
- */
31
14
  /**
32
15
  * Tiny fluent helper that wraps every `ipcMain.handle` / `ipcMain.on` with a
33
16
  * matching removeHandler/removeListener registered into an internal registry.
34
17
  *
35
- * Each `register*Ipc(ctx)` returns one of these as a Disposable so the
18
+ * Each `register*Ipc(...)` returns one of these as a Disposable so the
36
19
  * workbench-level registry can dispose all built-in handlers in one shot.
37
20
  *
38
- * If a {@link SenderPolicy} is provided, every incoming invocation is gated:
39
- * - `handle`: rejected senders cause the invoke promise to reject with
40
- * `Error('IPC sender rejected for channel <channel>')`.
41
- * - `on`: rejected senders are silently dropped (the original listener is
42
- * never called).
43
- * In both cases a single `console.warn` is emitted with the channel name
44
- * and a short sender summary. Without a policy the wrapper is a no-op,
45
- * preserving backwards compatibility for unit tests and callers that
46
- * opted out.
21
+ * The constructor takes either a {@link SenderPolicy} (trust only: is this
22
+ * sender allowed?) or an {@link IpcContextSource} (trust plus ownership: which
23
+ * window context does this sender belong to?). With either, every incoming
24
+ * invocation is gated:
25
+ * - `handle` / `handleRouted`: rejected senders cause the invoke promise to
26
+ * reject with `Error('IPC sender rejected for channel <channel>')`.
27
+ * - `on` / `onRouted`: rejected senders are silently dropped (the original
28
+ * listener is never called).
29
+ * - `handleSync` / `handleSyncRouted`: rejected senders get a structured
30
+ * `{ ok: false, code: 'EREJECTED', … }` returnValue, because sendSync blocks
31
+ * the renderer until one is set.
32
+ * In every case a single `console.warn` is emitted with the channel name and a
33
+ * short sender summary. With no policy — or a source that declares itself
34
+ * `ungated` — the wrapper is a pass-through, preserving backwards
35
+ * compatibility for unit tests and callers that opted out.
36
+ *
37
+ * The `*Routed` variants hand the owning context to the handler as its first
38
+ * argument. `TCtx` is `never` unless the registry was built from a source, so
39
+ * they are unusable (and unreachable) on a policy-only registry.
47
40
  */
48
- export declare class IpcRegistry implements Disposable {
49
- private policy?;
41
+ export declare class IpcRegistry<TCtx = never> implements Disposable {
50
42
  private cleanups;
51
43
  private _disposed;
52
- constructor(policy?: SenderPolicy | undefined);
44
+ private readonly policy;
45
+ private readonly source;
46
+ /** False reproduces the policy-less pass-through exactly: no main-frame
47
+ * check, and a synchronous handler keeps its synchronous return value. */
48
+ private readonly gated;
49
+ constructor(policyOrSource?: SenderPolicy | IpcContextSource<TCtx>);
50
+ /** Trust gate and owner resolution for one incoming message. */
51
+ private admit;
52
+ private warnRejected;
53
+ private addInvokeHandler;
53
54
  handle(channel: string, fn: HandleFn): this;
55
+ /** {@link handle} answered by the context that owns the calling sender. */
56
+ handleRouted(channel: string, fn: (ctx: TCtx, event: IpcMainInvokeEvent, ...args: unknown[]) => unknown): this;
57
+ private addSyncHandler;
54
58
  /**
55
59
  * Register a SYNCHRONOUS `ipcRenderer.sendSync` handler. Unlike {@link on}
56
60
  * (fire-and-forget), the renderer is blocked until we set `event.returnValue`,
57
- * so `fn` MUST be synchronous and return the value to hand back. The sender
58
- * policy gates it like {@link handle}; a rejected sender or a thrown error
59
- * yields a structured `{ ok: false, code, message }` returnValue instead of
60
- * the silent drop `on` uses, so the blocked renderer always gets an answer.
61
+ * so `fn` MUST be synchronous and return the value to hand back.
61
62
  */
62
63
  handleSync(channel: string, fn: (event: IpcMainEvent, ...args: unknown[]) => unknown): this;
64
+ /** {@link handleSync} answered by the context that owns the calling sender. */
65
+ handleSyncRouted(channel: string, fn: (ctx: TCtx, event: IpcMainEvent, ...args: unknown[]) => unknown): this;
66
+ private addListener;
63
67
  on(channel: string, fn: ListenerFn): this;
68
+ /** {@link on} answered by the context that owns the calling sender. */
69
+ onRouted(channel: string, fn: (ctx: TCtx, event: IpcMainEvent, ...args: unknown[]) => unknown): this;
64
70
  dispose(): Promise<void>;
65
71
  }
66
72
  export {};
@@ -1,5 +1,5 @@
1
1
  import { ipcMain } from 'electron';
2
- import { isMainFrameIpcSender, } from '@dimina-kit/electron-deck/main';
2
+ import { addMuxedInvokeHandler, isMainFrameIpcSender, } from '@dimina-kit/electron-deck/main';
3
3
  import { IpcValidationError } from './ipc-schema.js';
4
4
  import { createLogger } from './logger.js';
5
5
  const log = createLogger('ipc');
@@ -48,25 +48,41 @@ function summarizeSender(sender) {
48
48
  * possibly null) from a frame-unaware unit-test stub (neither present) and only
49
49
  * skip the check for the latter (the sender-id white-list still gates tests).
50
50
  */
51
+ /** Internal marker for "this sender is not allowed on this channel". */
52
+ const REJECT = Symbol('ipc-reject');
53
+ // Channel multiplexing (one real `ipcMain.handle` per channel, dispatched to
54
+ // whichever per-window registration claims the sender — needed because the
55
+ // storage, WXML and AppData panels each register their channels once per
56
+ // window) is shared with dimina-electron-runtime's bridge router; see
57
+ // @dimina-kit/electron-deck/main's ipc-mux.ts for the mux itself.
51
58
  /**
52
59
  * Tiny fluent helper that wraps every `ipcMain.handle` / `ipcMain.on` with a
53
60
  * matching removeHandler/removeListener registered into an internal registry.
54
61
  *
55
- * Each `register*Ipc(ctx)` returns one of these as a Disposable so the
62
+ * Each `register*Ipc(...)` returns one of these as a Disposable so the
56
63
  * workbench-level registry can dispose all built-in handlers in one shot.
57
64
  *
58
- * If a {@link SenderPolicy} is provided, every incoming invocation is gated:
59
- * - `handle`: rejected senders cause the invoke promise to reject with
60
- * `Error('IPC sender rejected for channel <channel>')`.
61
- * - `on`: rejected senders are silently dropped (the original listener is
62
- * never called).
63
- * In both cases a single `console.warn` is emitted with the channel name
64
- * and a short sender summary. Without a policy the wrapper is a no-op,
65
- * preserving backwards compatibility for unit tests and callers that
66
- * opted out.
65
+ * The constructor takes either a {@link SenderPolicy} (trust only: is this
66
+ * sender allowed?) or an {@link IpcContextSource} (trust plus ownership: which
67
+ * window context does this sender belong to?). With either, every incoming
68
+ * invocation is gated:
69
+ * - `handle` / `handleRouted`: rejected senders cause the invoke promise to
70
+ * reject with `Error('IPC sender rejected for channel <channel>')`.
71
+ * - `on` / `onRouted`: rejected senders are silently dropped (the original
72
+ * listener is never called).
73
+ * - `handleSync` / `handleSyncRouted`: rejected senders get a structured
74
+ * `{ ok: false, code: 'EREJECTED', … }` returnValue, because sendSync blocks
75
+ * the renderer until one is set.
76
+ * In every case a single `console.warn` is emitted with the channel name and a
77
+ * short sender summary. With no policy — or a source that declares itself
78
+ * `ungated` — the wrapper is a pass-through, preserving backwards
79
+ * compatibility for unit tests and callers that opted out.
80
+ *
81
+ * The `*Routed` variants hand the owning context to the handler as its first
82
+ * argument. `TCtx` is `never` unless the registry was built from a source, so
83
+ * they are unusable (and unreachable) on a policy-only registry.
67
84
  */
68
85
  export class IpcRegistry {
69
- policy;
70
86
  // Every cleanup is a synchronous ipcMain.removeHandler/removeListener call,
71
87
  // so dispose() can (and must) run them all before returning: callers
72
88
  // `dispose()` without awaiting and rely on every channel being unregistered
@@ -74,48 +90,81 @@ export class IpcRegistry {
74
90
  // live until a later microtask.
75
91
  cleanups = [];
76
92
  _disposed = false;
77
- constructor(policy) {
78
- this.policy = policy;
93
+ policy;
94
+ source;
95
+ /** False reproduces the policy-less pass-through exactly: no main-frame
96
+ * check, and a synchronous handler keeps its synchronous return value. */
97
+ gated;
98
+ constructor(policyOrSource) {
99
+ const isPolicy = typeof policyOrSource === 'function';
100
+ this.policy = isPolicy ? policyOrSource : null;
101
+ this.source = policyOrSource != null && !isPolicy ? policyOrSource : null;
102
+ this.gated = this.policy != null || (this.source != null && !this.source.ungated);
79
103
  }
80
- handle(channel, fn) {
81
- const policy = this.policy;
104
+ /** Trust gate and owner resolution for one incoming message. */
105
+ admit(event) {
106
+ const source = this.source;
107
+ if (source) {
108
+ // An ungated source always claims the sender, so the frame check — part
109
+ // of the trust gate, not of ownership — stays off with it.
110
+ if (source.ungated)
111
+ return source.resolve(event.sender);
112
+ if (!isMainFrameIpcSender(event))
113
+ return REJECT;
114
+ const ctx = source.resolve(event.sender);
115
+ return ctx == null ? REJECT : ctx;
116
+ }
117
+ // A bare policy answers trust only, so the accepted branch carries no
118
+ // context; `TCtx` is `never` in that mode, so no handler can observe it.
119
+ if (!this.policy)
120
+ return undefined;
121
+ return this.policy(event.sender) && isMainFrameIpcSender(event)
122
+ ? undefined
123
+ : REJECT;
124
+ }
125
+ warnRejected(channel, sender) {
126
+ console.warn(`[ipc] sender rejected for channel '${channel}' (${summarizeSender(sender)})`);
127
+ }
128
+ addInvokeHandler(channel, invoke) {
82
129
  // The gate is `async` so a rejected sender surfaces as a *rejected
83
130
  // promise* (the invoke-result contract) rather than a synchronous throw —
84
131
  // synchronous throws can escape callers that wrap the result in
85
132
  // `Promise.resolve(...)` instead of `await`-ing it directly.
86
- const guarded = policy
133
+ const guarded = this.gated
87
134
  ? async (event, ...args) => {
88
- const sender = event.sender;
89
- if (!policy(sender) || !isMainFrameIpcSender(event)) {
90
- console.warn(`[ipc] sender rejected for channel '${channel}' (${summarizeSender(sender)})`);
135
+ const ctx = this.admit(event);
136
+ if (ctx === REJECT) {
137
+ this.warnRejected(channel, event.sender);
91
138
  throw new Error(`IPC sender rejected for channel ${channel}`);
92
139
  }
93
- return fn(event, ...args);
140
+ return invoke(ctx, event, args);
94
141
  }
95
- : fn;
96
- ipcMain.handle(channel, guarded);
97
- this.cleanups.push(() => ipcMain.removeHandler(channel));
142
+ : (event, ...args) => invoke(this.admit(event), event, args);
143
+ this.cleanups.push(addMuxedInvokeHandler(ipcMain, channel, {
144
+ claims: (event) => this.admit(event) !== REJECT,
145
+ handle: guarded,
146
+ }));
98
147
  return this;
99
148
  }
100
- /**
101
- * Register a SYNCHRONOUS `ipcRenderer.sendSync` handler. Unlike {@link on}
102
- * (fire-and-forget), the renderer is blocked until we set `event.returnValue`,
103
- * so `fn` MUST be synchronous and return the value to hand back. The sender
104
- * policy gates it like {@link handle}; a rejected sender or a thrown error
105
- * yields a structured `{ ok: false, code, message }` returnValue instead of
106
- * the silent drop `on` uses, so the blocked renderer always gets an answer.
107
- */
108
- handleSync(channel, fn) {
109
- const policy = this.policy;
149
+ handle(channel, fn) {
150
+ const raw = fn;
151
+ return this.addInvokeHandler(channel, (_ctx, event, args) => raw(event, ...args));
152
+ }
153
+ /** {@link handle} answered by the context that owns the calling sender. */
154
+ handleRouted(channel, fn) {
155
+ return this.addInvokeHandler(channel, (ctx, event, args) => fn(ctx, event, ...args));
156
+ }
157
+ addSyncHandler(channel, invoke) {
110
158
  const listener = (event, ...args) => {
111
159
  const syncEvent = event;
112
- // Everything — including the policy check — runs inside the try so EVERY
113
- // path sets `event.returnValue`. sendSync blocks the renderer until it is
160
+ // Everything — including the gate — runs inside the try so EVERY path
161
+ // sets `event.returnValue`. sendSync blocks the renderer until it is
114
162
  // set, so an unset value (e.g. a throwing policy fn) would hang the
115
163
  // renderer forever; the catch guarantees a sentinel instead.
116
164
  try {
117
- if (policy && (!policy(event.sender) || !isMainFrameIpcSender(event))) {
118
- console.warn(`[ipc] sender rejected for channel '${channel}' (${summarizeSender(event.sender)})`);
165
+ const ctx = this.admit(event);
166
+ if (ctx === REJECT) {
167
+ this.warnRejected(channel, event.sender);
119
168
  syncEvent.returnValue = {
120
169
  ok: false,
121
170
  code: 'EREJECTED',
@@ -123,7 +172,7 @@ export class IpcRegistry {
123
172
  };
124
173
  return;
125
174
  }
126
- syncEvent.returnValue = fn(event, ...args);
175
+ syncEvent.returnValue = invoke(ctx, event, args);
127
176
  }
128
177
  catch (err) {
129
178
  reportListenerError(channel, err);
@@ -139,12 +188,22 @@ export class IpcRegistry {
139
188
  this.cleanups.push(() => ipcMain.removeListener(channel, listener));
140
189
  return this;
141
190
  }
142
- on(channel, fn) {
143
- const policy = this.policy;
144
- const raw = fn;
145
- const safeInvoke = (event, args) => {
191
+ /**
192
+ * Register a SYNCHRONOUS `ipcRenderer.sendSync` handler. Unlike {@link on}
193
+ * (fire-and-forget), the renderer is blocked until we set `event.returnValue`,
194
+ * so `fn` MUST be synchronous and return the value to hand back.
195
+ */
196
+ handleSync(channel, fn) {
197
+ return this.addSyncHandler(channel, (_ctx, event, args) => fn(event, ...args));
198
+ }
199
+ /** {@link handleSync} answered by the context that owns the calling sender. */
200
+ handleSyncRouted(channel, fn) {
201
+ return this.addSyncHandler(channel, (ctx, event, args) => fn(ctx, event, ...args));
202
+ }
203
+ addListener(channel, invoke) {
204
+ const safeInvoke = (ctx, event, args) => {
146
205
  try {
147
- const ret = raw(event, ...args);
206
+ const ret = invoke(ctx, event, args);
148
207
  if (isThenable(ret)) {
149
208
  // Async listeners would otherwise leak rejections into Electron's
150
209
  // event loop as `UnhandledPromiseRejection`. Funnel them into the
@@ -156,20 +215,28 @@ export class IpcRegistry {
156
215
  reportListenerError(channel, err);
157
216
  }
158
217
  };
159
- const guarded = policy
218
+ const guarded = this.gated
160
219
  ? (event, ...args) => {
161
- const sender = event.sender;
162
- if (!policy(sender) || !isMainFrameIpcSender(event)) {
163
- console.warn(`[ipc] sender rejected for channel '${channel}' (${summarizeSender(sender)})`);
220
+ const ctx = this.admit(event);
221
+ if (ctx === REJECT) {
222
+ this.warnRejected(channel, event.sender);
164
223
  return;
165
224
  }
166
- safeInvoke(event, args);
225
+ safeInvoke(ctx, event, args);
167
226
  }
168
- : (event, ...args) => safeInvoke(event, args);
227
+ : (event, ...args) => safeInvoke(this.admit(event), event, args);
169
228
  ipcMain.on(channel, guarded);
170
229
  this.cleanups.push(() => ipcMain.removeListener(channel, guarded));
171
230
  return this;
172
231
  }
232
+ on(channel, fn) {
233
+ const raw = fn;
234
+ return this.addListener(channel, (_ctx, event, args) => raw(event, ...args));
235
+ }
236
+ /** {@link on} answered by the context that owns the calling sender. */
237
+ onRouted(channel, fn) {
238
+ return this.addListener(channel, (ctx, event, args) => fn(ctx, event, ...args));
239
+ }
173
240
  dispose() {
174
241
  // Synchronous drain (LIFO, idempotent): every channel is unregistered
175
242
  // before this returns; the promise only carries aggregated errors.
@@ -31,4 +31,13 @@ import type { SenderPolicy } from './ipc-registry.js';
31
31
  * iframe — is rejected.
32
32
  */
33
33
  export declare function createWorkbenchSenderPolicy(ctx: Pick<WorkbenchContext, 'windows' | 'views' | 'trustedWindowSenderIds'>): SenderPolicy;
34
+ /**
35
+ * The senders this context structurally OWNS: its own main-window renderer,
36
+ * its own settings window and the overlay views mounted inside it. Deliberately
37
+ * excludes `trustedWindowSenderIds` — that map is shared by the whole app, so
38
+ * every context would claim the same host window and ownership would stop being
39
+ * unique. `createWindowContextRouter` routes on this check first for exactly
40
+ * that reason.
41
+ */
42
+ export declare function createWorkbenchOwnedSenderCheck(ctx: Pick<WorkbenchContext, 'windows' | 'views'>): SenderPolicy;
34
43
  //# sourceMappingURL=sender-policy.d.ts.map
@@ -29,6 +29,26 @@
29
29
  * iframe — is rejected.
30
30
  */
31
31
  export function createWorkbenchSenderPolicy(ctx) {
32
+ const ownsSender = createWorkbenchOwnedSenderCheck(ctx);
33
+ return (sender) => {
34
+ if (ownsSender(sender))
35
+ return true;
36
+ if (sender.isDestroyed())
37
+ return false;
38
+ // Host-registered trusted windows (registerTrustedWindow). The map is
39
+ // app-wide, so this is the one accepted sender no single context owns.
40
+ return ctx.trustedWindowSenderIds.has(sender.id);
41
+ };
42
+ }
43
+ /**
44
+ * The senders this context structurally OWNS: its own main-window renderer,
45
+ * its own settings window and the overlay views mounted inside it. Deliberately
46
+ * excludes `trustedWindowSenderIds` — that map is shared by the whole app, so
47
+ * every context would claim the same host window and ownership would stop being
48
+ * unique. `createWindowContextRouter` routes on this check first for exactly
49
+ * that reason.
50
+ */
51
+ export function createWorkbenchOwnedSenderCheck(ctx) {
32
52
  return (sender) => {
33
53
  if (sender.isDestroyed())
34
54
  return false;
@@ -38,9 +58,6 @@ export function createWorkbenchSenderPolicy(ctx) {
38
58
  // Standalone settings BrowserWindow renderer
39
59
  if (ctx.windows.isSettingsWindowSender(sender.id))
40
60
  return true;
41
- // Host-registered trusted windows (registerTrustedWindow)
42
- if (ctx.trustedWindowSenderIds.has(sender.id))
43
- return true;
44
61
  // Settings overlay view (mounted inside the main window)
45
62
  const settingsViewId = ctx.views.getSettingsWebContentsId();
46
63
  if (settingsViewId != null && sender.id === settingsViewId)
@@ -64,6 +81,12 @@ export function createWorkbenchSenderPolicy(ctx) {
64
81
  const updateDialogViewId = ctx.views.getUpdateDialogWebContentsId();
65
82
  if (updateDialogViewId != null && sender.id === updateDialogViewId)
66
83
  return true;
84
+ // Device-picker overlay view. Same devtools-owned trust level — the
85
+ // simulator toolbar's device selector, moved out of the main renderer's
86
+ // DOM so the simulator WCV can't occlude it.
87
+ const devicePickerViewId = ctx.views.getDevicePickerWebContentsId();
88
+ if (devicePickerViewId != null && sender.id === devicePickerViewId)
89
+ return true;
67
90
  // The host-toolbar overlay is DELIBERATELY NOT trusted here. The host loads
68
91
  // arbitrary content into it, so granting it the global white-list would open
69
92
  // all ~72 IpcRegistry channels to that content. Its one channel (the reverse
@@ -15,8 +15,9 @@ export declare function themeBg(): string;
15
15
  * phone keeps contrast against it in BOTH schemes.
16
16
  *
17
17
  * Dark: #282828 | Light: #f7f7f9 — both match the renderer's --color-surface-2
18
- * chrome tone (design.css). Dark is devtools-owned rather than a Cornetto
19
- * --qd-muted alias: pixel-verified against Figma node 66:1729 at rgb(40,40,40),
18
+ * chrome tone (@dimina-kit/design, css/tokens.css). Dark is a package-owned
19
+ * value rather than a Cornetto --qd-muted alias: pixel-verified against Figma
20
+ * node 66:1729 at rgb(40,40,40),
20
21
  * which doesn't match any Cornetto dark token (--qd-muted is #383838, off by
21
22
  * 16 units — confirmed too light against the app's own dark-mode screenshots).
22
23
  * Light's #f7f7f9 IS an exact --qd-muted match (verified against node 25:5),
@@ -19,8 +19,9 @@ export function themeBg() {
19
19
  * phone keeps contrast against it in BOTH schemes.
20
20
  *
21
21
  * Dark: #282828 | Light: #f7f7f9 — both match the renderer's --color-surface-2
22
- * chrome tone (design.css). Dark is devtools-owned rather than a Cornetto
23
- * --qd-muted alias: pixel-verified against Figma node 66:1729 at rgb(40,40,40),
22
+ * chrome tone (@dimina-kit/design, css/tokens.css). Dark is a package-owned
23
+ * value rather than a Cornetto --qd-muted alias: pixel-verified against Figma
24
+ * node 66:1729 at rgb(40,40,40),
24
25
  * which doesn't match any Cornetto dark token (--qd-muted is #383838, off by
25
26
  * 16 units — confirmed too light against the app's own dark-mode screenshots).
26
27
  * Light's #f7f7f9 IS an exact --qd-muted match (verified against node 25:5),
@@ -6,6 +6,14 @@ export interface WindowOptions {
6
6
  minWidth?: number;
7
7
  minHeight?: number;
8
8
  indexHtml: string;
9
+ /**
10
+ * Query string appended to the loaded `file://` URL. This is how a window
11
+ * receives its own bootstrap identity (the workbench window reads the project
12
+ * it belongs to from here), so the renderer entry knows what to mount without
13
+ * a round-trip. Survives navigation-hardening: that check matches on the
14
+ * `file://` prefix only and ignores the query (see navigation-hardening.ts).
15
+ */
16
+ query?: Record<string, string>;
9
17
  /**
10
18
  * Auto-show the window on `ready-to-show` in non-test envs. Defaults to
11
19
  * `true`. `false` lets a login-gating host keep the window hidden and call
@@ -13,5 +21,13 @@ export interface WindowOptions {
13
21
  */
14
22
  autoShow?: boolean;
15
23
  }
24
+ /**
25
+ * Shows `win` the way the current env expects: `showInactive()` in test env
26
+ * so e2e windows don't steal focus, `show()` otherwise. Shared by this
27
+ * module's own `ready-to-show` handler and by the workbench window manager's
28
+ * reveal-after-hook-and-paint gate (`workbench-window.ts`), so the two reveal
29
+ * paths can never pick different show methods.
30
+ */
31
+ export declare function revealMainWindow(win: BrowserWindow): void;
16
32
  export declare function createMainWindow(opts: WindowOptions): BrowserWindow;
17
33
  //# sourceMappingURL=create.d.ts.map
@@ -3,6 +3,19 @@ import path from 'path';
3
3
  import { mainPreloadPath } from '../../utils/paths.js';
4
4
  import { themeBg } from '../../utils/theme.js';
5
5
  import { applyNavigationHardening } from '../navigation-hardening.js';
6
+ /**
7
+ * Shows `win` the way the current env expects: `showInactive()` in test env
8
+ * so e2e windows don't steal focus, `show()` otherwise. Shared by this
9
+ * module's own `ready-to-show` handler and by the workbench window manager's
10
+ * reveal-after-hook-and-paint gate (`workbench-window.ts`), so the two reveal
11
+ * paths can never pick different show methods.
12
+ */
13
+ export function revealMainWindow(win) {
14
+ if (process.env.NODE_ENV === 'test')
15
+ win.showInactive();
16
+ else
17
+ win.show();
18
+ }
6
19
  export function createMainWindow(opts) {
7
20
  const mainWindow = new BrowserWindow({
8
21
  width: opts.width ?? 1280,
@@ -25,16 +38,11 @@ export function createMainWindow(opts) {
25
38
  const isTest = process.env.NODE_ENV === 'test';
26
39
  // Visibility is governed by `autoShow` in BOTH envs. A login-gating host
27
40
  // opts out via `autoShow: false` and reveals the window itself once auth
28
- // passes — don't flash an un-authed window. The env only chooses HOW to
29
- // show: test uses showInactive() so e2e windows don't steal focus, prod
30
- // uses show(). The framework must never force-show when the host opted
31
- // out, in test env either (that would fight the host's own reveal handler).
32
- if (opts.autoShow !== false) {
33
- if (isTest)
34
- mainWindow.showInactive();
35
- else
36
- mainWindow.show();
37
- }
41
+ // passes — don't flash an un-authed window. The framework must never
42
+ // force-show when the host opted out, in test env either (that would
43
+ // fight the host's own reveal handler).
44
+ if (opts.autoShow !== false)
45
+ revealMainWindow(mainWindow);
38
46
  // Don't auto-open a detached DevTools for the devtools UI shell itself —
39
47
  // it's noise for normal use (the mini-app's Console lives in the embedded
40
48
  // right-panel DevTools, not here). Opt in via env for debugging the shell.
@@ -46,7 +54,7 @@ export function createMainWindow(opts) {
46
54
  // Restrict the main renderer to file:// URLs under the renderer bundle and
47
55
  // route every popup through the OS browser. See navigation-hardening.ts.
48
56
  applyNavigationHardening(mainWindow.webContents, rendererDir);
49
- mainWindow.loadFile(opts.indexHtml);
57
+ mainWindow.loadFile(opts.indexHtml, opts.query ? { query: opts.query } : undefined);
50
58
  const container = new View();
51
59
  const mainWebView = mainWindow.contentView;
52
60
  container.addChildView(mainWebView);
@@ -1,4 +1,4 @@
1
- import type { BrowserWindow } from 'electron';
1
+ import { BrowserWindow } from 'electron';
2
2
  import type { WorkbenchContext } from '../../services/workbench-context.js';
3
3
  import { type Disposable } from '@dimina-kit/electron-deck/main';
4
4
  export interface MainWindowEventState {
@@ -1,5 +1,29 @@
1
- import { globalShortcut } from 'electron';
1
+ import { BrowserWindow, globalShortcut } from 'electron';
2
2
  import { DisposableRegistry, toDisposable } from '@dimina-kit/electron-deck/main';
3
+ const DEVTOOLS_ACCELERATOR = 'CommandOrControl+Shift+I';
4
+ let devToolsShortcutHolders = 0;
5
+ /**
6
+ * Claims a share of the ONE process-wide DevTools accelerator. `globalShortcut`
7
+ * is a process singleton: a per-window registration is refused for every window
8
+ * after the first, which would give that first window the shortcut for the
9
+ * whole app and kill it outright when that window closed. So the accelerator is
10
+ * registered once, opens whichever window is focused when it is pressed, and is
11
+ * released only when the last holder lets go — a later window can then claim it
12
+ * again. `unregisterAll()` in lifecycle stays as a process-exit safety net.
13
+ */
14
+ function acquireDevToolsShortcut() {
15
+ if (devToolsShortcutHolders === 0) {
16
+ globalShortcut.register(DEVTOOLS_ACCELERATOR, () => {
17
+ BrowserWindow.getFocusedWindow()?.webContents.openDevTools({ mode: 'detach' });
18
+ });
19
+ }
20
+ devToolsShortcutHolders += 1;
21
+ return toDisposable(() => {
22
+ devToolsShortcutHolders -= 1;
23
+ if (devToolsShortcutHolders === 0)
24
+ globalShortcut.unregister(DEVTOOLS_ACCELERATOR);
25
+ });
26
+ }
3
27
  export function wireMainWindowEvents(win, state = {}) {
4
28
  const registry = new DisposableRegistry();
5
29
  const mainWebView = win.contentView.children[0];
@@ -24,18 +48,7 @@ export function wireMainWindowEvents(win, state = {}) {
24
48
  win.removeListener('close', onCloseHandler);
25
49
  }));
26
50
  }
27
- // DevTools shortcut: scoped to this main window so its disposal is tied
28
- // to ctx.registry. `unregisterAll()` in lifecycle remains as a process-exit
29
- // safety net.
30
- const devToolsAccelerator = 'CommandOrControl+Shift+I';
31
- const registered = globalShortcut.register(devToolsAccelerator, () => {
32
- win.webContents.openDevTools({ mode: 'detach' });
33
- });
34
- if (registered) {
35
- registry.add(toDisposable(() => {
36
- globalShortcut.unregister(devToolsAccelerator);
37
- }));
38
- }
51
+ registry.add(acquireDevToolsShortcut());
39
52
  return registry;
40
53
  }
41
54
  //# sourceMappingURL=events.js.map