@dxos/app-framework 0.10.0 → 0.11.0

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 (294) hide show
  1. package/.storybook/main.mts +1 -1
  2. package/.storybook/preview.mts +2 -2
  3. package/CHANGELOG.md +93 -0
  4. package/TASKS.md +11 -0
  5. package/dist/lib/chunk-SurfaceManager.mjs +960 -0
  6. package/dist/lib/chunk-SurfaceManager.mjs.map +1 -0
  7. package/dist/lib/chunk-capability.mjs +15 -0
  8. package/dist/lib/chunk-capability.mjs.map +1 -0
  9. package/dist/lib/{browser/chunk-FJ4765WW.mjs → chunk-context.mjs} +4 -5
  10. package/dist/lib/chunk-context.mjs.map +1 -0
  11. package/dist/lib/chunk-history-tracker.mjs +160 -0
  12. package/dist/lib/chunk-history-tracker.mjs.map +1 -0
  13. package/dist/lib/chunk-plugin-manager.mjs +1390 -0
  14. package/dist/lib/chunk-plugin-manager.mjs.map +1 -0
  15. package/dist/lib/chunk-plugin-manifest.mjs +125 -0
  16. package/dist/lib/chunk-plugin-manifest.mjs.map +1 -0
  17. package/dist/lib/chunk-process-manager-capability.mjs +126 -0
  18. package/dist/lib/chunk-process-manager-capability.mjs.map +1 -0
  19. package/dist/lib/chunk-rolldown-runtime.mjs +13 -0
  20. package/dist/lib/chunk-url-loader.mjs +347 -0
  21. package/dist/lib/chunk-url-loader.mjs.map +1 -0
  22. package/dist/lib/cli.mjs +78 -0
  23. package/dist/lib/cli.mjs.map +1 -0
  24. package/dist/lib/common/activation-events.mjs +35 -0
  25. package/dist/lib/common/activation-events.mjs.map +1 -0
  26. package/dist/lib/common/capabilities.mjs +184 -0
  27. package/dist/lib/common/capabilities.mjs.map +1 -0
  28. package/dist/lib/config.mjs +2 -0
  29. package/dist/lib/core/activation-event.mjs +55 -0
  30. package/dist/lib/core/activation-event.mjs.map +1 -0
  31. package/dist/lib/core/capability.mjs +189 -0
  32. package/dist/lib/core/capability.mjs.map +1 -0
  33. package/dist/lib/core/plugin-manager.mjs +2 -0
  34. package/dist/lib/core/plugin.mjs +340 -0
  35. package/dist/lib/core/plugin.mjs.map +1 -0
  36. package/dist/lib/core/url-loader.mjs +2 -0
  37. package/dist/lib/index.mjs +134 -0
  38. package/dist/lib/index.mjs.map +1 -0
  39. package/dist/lib/testing/react.mjs +70 -0
  40. package/dist/lib/testing/react.mjs.map +1 -0
  41. package/dist/lib/testing.mjs +399 -0
  42. package/dist/lib/testing.mjs.map +1 -0
  43. package/dist/lib/ui.mjs +636 -0
  44. package/dist/lib/ui.mjs.map +1 -0
  45. package/dist/lib/vite-plugin.mjs +986 -0
  46. package/dist/lib/vite-plugin.mjs.map +1 -0
  47. package/dist/plugin/node-esm/index.mjs +4 -3
  48. package/dist/plugin/node-esm/index.mjs.map +3 -3
  49. package/dist/plugin/node-esm/meta.json +1 -1
  50. package/dist/types/src/common/Role.d.ts +31 -0
  51. package/dist/types/src/common/Role.d.ts.map +1 -0
  52. package/dist/types/src/common/Role.test.d.ts +2 -0
  53. package/dist/types/src/common/Role.test.d.ts.map +1 -0
  54. package/dist/types/src/common/capabilities.d.ts +11 -2
  55. package/dist/types/src/common/capabilities.d.ts.map +1 -1
  56. package/dist/types/src/common/index.d.ts +1 -0
  57. package/dist/types/src/common/index.d.ts.map +1 -1
  58. package/dist/types/src/core/capability-manager.d.ts +1 -1
  59. package/dist/types/src/core/capability-manager.d.ts.map +1 -1
  60. package/dist/types/src/core/capability.d.ts +38 -5
  61. package/dist/types/src/core/capability.d.ts.map +1 -1
  62. package/dist/types/src/core/plugin-manager.d.ts +1 -1
  63. package/dist/types/src/core/plugin-manager.d.ts.map +1 -1
  64. package/dist/types/src/core/plugin.d.ts +14 -1
  65. package/dist/types/src/core/plugin.d.ts.map +1 -1
  66. package/dist/types/src/core/registry.d.ts +1 -1
  67. package/dist/types/src/core/registry.d.ts.map +1 -1
  68. package/dist/types/src/plugin-process-manager/history/capability.d.ts +1 -1
  69. package/dist/types/src/plugin-process-manager/process-manager-capability.d.ts +1 -1
  70. package/dist/types/src/plugin-process-manager/process-manager-capability.d.ts.map +1 -1
  71. package/dist/types/src/testing/StorybookErrorFallback.d.ts +15 -0
  72. package/dist/types/src/testing/StorybookErrorFallback.d.ts.map +1 -0
  73. package/dist/types/src/testing/harness.d.ts +1 -1
  74. package/dist/types/src/testing/harness.d.ts.map +1 -1
  75. package/dist/types/src/testing/index.d.ts +2 -0
  76. package/dist/types/src/testing/index.d.ts.map +1 -1
  77. package/dist/types/src/testing/react.d.ts +4 -4
  78. package/dist/types/src/testing/react.d.ts.map +1 -1
  79. package/dist/types/src/testing/withPluginManager.d.ts.map +1 -1
  80. package/dist/types/src/testing/withPluginManager.stories.d.ts +6 -0
  81. package/dist/types/src/testing/withPluginManager.stories.d.ts.map +1 -1
  82. package/dist/types/src/testing/withSurfaceDebug.d.ts +10 -0
  83. package/dist/types/src/testing/withSurfaceDebug.d.ts.map +1 -0
  84. package/dist/types/src/ui/components/App/App.d.ts +0 -32
  85. package/dist/types/src/ui/components/App/App.d.ts.map +1 -1
  86. package/dist/types/src/ui/components/App/index.d.ts +1 -0
  87. package/dist/types/src/ui/components/App/index.d.ts.map +1 -1
  88. package/dist/types/src/ui/components/App/loader.d.ts +35 -0
  89. package/dist/types/src/ui/components/App/loader.d.ts.map +1 -0
  90. package/dist/types/src/ui/components/HomeSection/HomeSection.d.ts +26 -0
  91. package/dist/types/src/ui/components/HomeSection/HomeSection.d.ts.map +1 -0
  92. package/dist/types/src/ui/components/HomeSection/HomeSection.stories.d.ts +15 -0
  93. package/dist/types/src/ui/components/HomeSection/HomeSection.stories.d.ts.map +1 -0
  94. package/dist/types/src/ui/components/HomeSection/index.d.ts +2 -0
  95. package/dist/types/src/ui/components/HomeSection/index.d.ts.map +1 -0
  96. package/dist/types/src/ui/components/NamePopover/NamePopover.d.ts +14 -0
  97. package/dist/types/src/ui/components/NamePopover/NamePopover.d.ts.map +1 -0
  98. package/dist/types/src/ui/components/NamePopover/index.d.ts +2 -0
  99. package/dist/types/src/ui/components/NamePopover/index.d.ts.map +1 -0
  100. package/dist/types/src/ui/components/Surface/SurfaceComponent.d.ts +21 -9
  101. package/dist/types/src/ui/components/Surface/SurfaceComponent.d.ts.map +1 -1
  102. package/dist/types/src/ui/components/Surface/SurfaceComponent.stories.d.ts +4 -1
  103. package/dist/types/src/ui/components/Surface/SurfaceComponent.stories.d.ts.map +1 -1
  104. package/dist/types/src/ui/components/Surface/SurfaceComponent.test.d.ts +2 -0
  105. package/dist/types/src/ui/components/Surface/SurfaceComponent.test.d.ts.map +1 -0
  106. package/dist/types/src/ui/components/Surface/SurfaceDebug.d.ts +40 -0
  107. package/dist/types/src/ui/components/Surface/SurfaceDebug.d.ts.map +1 -0
  108. package/dist/types/src/ui/components/Surface/SurfaceManager.d.ts +23 -0
  109. package/dist/types/src/ui/components/Surface/SurfaceManager.d.ts.map +1 -0
  110. package/dist/types/src/ui/components/Surface/SurfaceManagerContext.d.ts +11 -0
  111. package/dist/types/src/ui/components/Surface/SurfaceManagerContext.d.ts.map +1 -0
  112. package/dist/types/src/ui/components/Surface/SurfaceMetrics.d.ts +72 -0
  113. package/dist/types/src/ui/components/Surface/SurfaceMetrics.d.ts.map +1 -0
  114. package/dist/types/src/ui/components/Surface/SurfaceProfilerContext.d.ts.map +1 -1
  115. package/dist/types/src/ui/components/Surface/index.d.ts +20 -25
  116. package/dist/types/src/ui/components/Surface/index.d.ts.map +1 -1
  117. package/dist/types/src/ui/components/Surface/types.d.ts +56 -52
  118. package/dist/types/src/ui/components/Surface/types.d.ts.map +1 -1
  119. package/dist/types/src/ui/components/index.d.ts +2 -0
  120. package/dist/types/src/ui/components/index.d.ts.map +1 -1
  121. package/dist/types/src/ui/hooks/useApp.d.ts.map +1 -1
  122. package/dist/types/tsconfig.tsbuildinfo +1 -1
  123. package/moon.yml +25 -18
  124. package/package.json +58 -73
  125. package/src/common/Role.test.ts +41 -0
  126. package/src/common/Role.ts +54 -0
  127. package/src/common/capabilities.ts +17 -2
  128. package/src/common/index.ts +1 -0
  129. package/src/core/capability-manager.test.ts +1 -1
  130. package/src/core/capability-manager.ts +1 -1
  131. package/src/core/capability.ts +45 -6
  132. package/src/core/plugin-manager.test.ts +38 -3
  133. package/src/core/plugin-manager.ts +18 -1
  134. package/src/core/plugin.ts +16 -1
  135. package/src/core/registry.ts +1 -1
  136. package/src/plugin-process-manager/process-manager-capability.ts +25 -2
  137. package/src/testing/StorybookErrorFallback.tsx +40 -0
  138. package/src/testing/harness.ts +1 -1
  139. package/src/testing/index.ts +2 -0
  140. package/src/testing/react.test.tsx +2 -1
  141. package/src/testing/react.tsx +14 -8
  142. package/src/testing/withPluginManager.stories.tsx +16 -1
  143. package/src/testing/withPluginManager.tsx +13 -2
  144. package/src/testing/withSurfaceDebug.tsx +22 -0
  145. package/src/ui/components/App/App.tsx +7 -35
  146. package/src/ui/components/App/index.ts +1 -0
  147. package/src/ui/components/App/loader.ts +39 -0
  148. package/src/ui/components/HomeSection/HomeSection.stories.tsx +43 -0
  149. package/src/ui/components/HomeSection/HomeSection.tsx +69 -0
  150. package/src/ui/components/HomeSection/index.ts +5 -0
  151. package/src/ui/components/NamePopover/NamePopover.tsx +70 -0
  152. package/src/ui/components/NamePopover/index.ts +5 -0
  153. package/src/ui/components/Surface/DESIGN.md +136 -0
  154. package/src/ui/components/Surface/SurfaceComponent.stories.tsx +70 -41
  155. package/src/ui/components/Surface/SurfaceComponent.test.tsx +403 -0
  156. package/src/ui/components/Surface/SurfaceComponent.tsx +209 -189
  157. package/src/ui/components/Surface/SurfaceDebug.tsx +435 -0
  158. package/src/ui/components/Surface/SurfaceManager.ts +95 -0
  159. package/src/ui/components/Surface/SurfaceManagerContext.ts +23 -0
  160. package/src/ui/components/Surface/SurfaceMetrics.ts +202 -0
  161. package/src/ui/components/Surface/SurfaceProfilerContext.tsx +12 -2
  162. package/src/ui/components/Surface/index.ts +24 -22
  163. package/src/ui/components/Surface/types.test.ts +47 -54
  164. package/src/ui/components/Surface/types.ts +66 -91
  165. package/src/ui/components/index.ts +2 -0
  166. package/src/ui/hooks/useApp.tsx +7 -3
  167. package/src/vite-plugin/boot-loader/loader-app/boot-loader.css +1 -1
  168. package/tsconfig.json +3 -0
  169. package/vite.config.ts +26 -0
  170. package/dist/lib/browser/capability-S44TE2H7.mjs +0 -35
  171. package/dist/lib/browser/capability-S44TE2H7.mjs.map +0 -7
  172. package/dist/lib/browser/chunk-3NQLTQCP.mjs +0 -47
  173. package/dist/lib/browser/chunk-3NQLTQCP.mjs.map +0 -7
  174. package/dist/lib/browser/chunk-53HJHMS5.mjs +0 -7
  175. package/dist/lib/browser/chunk-53HJHMS5.mjs.map +0 -7
  176. package/dist/lib/browser/chunk-FJ4765WW.mjs.map +0 -7
  177. package/dist/lib/browser/chunk-J5LGTIGS.mjs +0 -10
  178. package/dist/lib/browser/chunk-J5LGTIGS.mjs.map +0 -7
  179. package/dist/lib/browser/chunk-KRJ4KK2W.mjs +0 -505
  180. package/dist/lib/browser/chunk-KRJ4KK2W.mjs.map +0 -7
  181. package/dist/lib/browser/chunk-LUH4Y5F2.mjs +0 -430
  182. package/dist/lib/browser/chunk-LUH4Y5F2.mjs.map +0 -7
  183. package/dist/lib/browser/chunk-NKVRSMFN.mjs +0 -95
  184. package/dist/lib/browser/chunk-NKVRSMFN.mjs.map +0 -7
  185. package/dist/lib/browser/chunk-OO53M5UK.mjs +0 -260
  186. package/dist/lib/browser/chunk-OO53M5UK.mjs.map +0 -7
  187. package/dist/lib/browser/chunk-OPTS3ZSN.mjs +0 -83
  188. package/dist/lib/browser/chunk-OPTS3ZSN.mjs.map +0 -7
  189. package/dist/lib/browser/chunk-Q2GLJTVV.mjs +0 -12
  190. package/dist/lib/browser/chunk-Q2GLJTVV.mjs.map +0 -7
  191. package/dist/lib/browser/chunk-SYXIYT6T.mjs +0 -143
  192. package/dist/lib/browser/chunk-SYXIYT6T.mjs.map +0 -7
  193. package/dist/lib/browser/chunk-U5MESPM5.mjs +0 -603
  194. package/dist/lib/browser/chunk-U5MESPM5.mjs.map +0 -7
  195. package/dist/lib/browser/chunk-UGYCLOXE.mjs +0 -1471
  196. package/dist/lib/browser/chunk-UGYCLOXE.mjs.map +0 -7
  197. package/dist/lib/browser/chunk-VF37YTXJ.mjs +0 -28
  198. package/dist/lib/browser/chunk-VF37YTXJ.mjs.map +0 -7
  199. package/dist/lib/browser/chunk-YUVQIOTB.mjs +0 -60
  200. package/dist/lib/browser/chunk-YUVQIOTB.mjs.map +0 -7
  201. package/dist/lib/browser/cli/index.mjs +0 -74
  202. package/dist/lib/browser/cli/index.mjs.map +0 -7
  203. package/dist/lib/browser/common/activation-events.mjs +0 -20
  204. package/dist/lib/browser/common/activation-events.mjs.map +0 -7
  205. package/dist/lib/browser/common/capabilities.mjs +0 -56
  206. package/dist/lib/browser/common/capabilities.mjs.map +0 -7
  207. package/dist/lib/browser/config/index.mjs +0 -8
  208. package/dist/lib/browser/config/index.mjs.map +0 -7
  209. package/dist/lib/browser/core/activation-event.mjs +0 -20
  210. package/dist/lib/browser/core/activation-event.mjs.map +0 -7
  211. package/dist/lib/browser/core/capability.mjs +0 -32
  212. package/dist/lib/browser/core/capability.mjs.map +0 -7
  213. package/dist/lib/browser/core/plugin-manager.mjs +0 -19
  214. package/dist/lib/browser/core/plugin-manager.mjs.map +0 -7
  215. package/dist/lib/browser/core/plugin.mjs +0 -51
  216. package/dist/lib/browser/core/plugin.mjs.map +0 -7
  217. package/dist/lib/browser/core/url-loader.mjs +0 -24
  218. package/dist/lib/browser/core/url-loader.mjs.map +0 -7
  219. package/dist/lib/browser/index.mjs +0 -98
  220. package/dist/lib/browser/index.mjs.map +0 -7
  221. package/dist/lib/browser/meta.json +0 -1
  222. package/dist/lib/browser/process-manager-capability-QK4BMKMN.mjs +0 -132
  223. package/dist/lib/browser/process-manager-capability-QK4BMKMN.mjs.map +0 -7
  224. package/dist/lib/browser/testing/index.mjs +0 -359
  225. package/dist/lib/browser/testing/index.mjs.map +0 -7
  226. package/dist/lib/browser/testing/react.mjs +0 -78
  227. package/dist/lib/browser/testing/react.mjs.map +0 -7
  228. package/dist/lib/browser/ui/index.mjs +0 -60
  229. package/dist/lib/browser/ui/index.mjs.map +0 -7
  230. package/dist/lib/node-esm/capability-3YRF77LV.mjs +0 -36
  231. package/dist/lib/node-esm/capability-3YRF77LV.mjs.map +0 -7
  232. package/dist/lib/node-esm/chunk-2UPUZXGU.mjs +0 -9
  233. package/dist/lib/node-esm/chunk-2UPUZXGU.mjs.map +0 -7
  234. package/dist/lib/node-esm/chunk-37Z53PXZ.mjs +0 -10
  235. package/dist/lib/node-esm/chunk-37Z53PXZ.mjs.map +0 -7
  236. package/dist/lib/node-esm/chunk-4SE7SX7N.mjs +0 -261
  237. package/dist/lib/node-esm/chunk-4SE7SX7N.mjs.map +0 -7
  238. package/dist/lib/node-esm/chunk-BMW6P6QA.mjs +0 -48
  239. package/dist/lib/node-esm/chunk-BMW6P6QA.mjs.map +0 -7
  240. package/dist/lib/node-esm/chunk-COHOWGGJ.mjs +0 -144
  241. package/dist/lib/node-esm/chunk-COHOWGGJ.mjs.map +0 -7
  242. package/dist/lib/node-esm/chunk-DRT75WGD.mjs +0 -604
  243. package/dist/lib/node-esm/chunk-DRT75WGD.mjs.map +0 -7
  244. package/dist/lib/node-esm/chunk-FKMVORZN.mjs +0 -431
  245. package/dist/lib/node-esm/chunk-FKMVORZN.mjs.map +0 -7
  246. package/dist/lib/node-esm/chunk-FPW45EZH.mjs +0 -14
  247. package/dist/lib/node-esm/chunk-FPW45EZH.mjs.map +0 -7
  248. package/dist/lib/node-esm/chunk-GLH4LGHZ.mjs +0 -84
  249. package/dist/lib/node-esm/chunk-GLH4LGHZ.mjs.map +0 -7
  250. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs +0 -11
  251. package/dist/lib/node-esm/chunk-HSLMI22Q.mjs.map +0 -7
  252. package/dist/lib/node-esm/chunk-IL7O7IRX.mjs +0 -1472
  253. package/dist/lib/node-esm/chunk-IL7O7IRX.mjs.map +0 -7
  254. package/dist/lib/node-esm/chunk-ND2HETHD.mjs +0 -506
  255. package/dist/lib/node-esm/chunk-ND2HETHD.mjs.map +0 -7
  256. package/dist/lib/node-esm/chunk-TLKYZKPW.mjs +0 -29
  257. package/dist/lib/node-esm/chunk-TLKYZKPW.mjs.map +0 -7
  258. package/dist/lib/node-esm/chunk-VCPGX7WN.mjs +0 -62
  259. package/dist/lib/node-esm/chunk-VCPGX7WN.mjs.map +0 -7
  260. package/dist/lib/node-esm/chunk-XSZMX53L.mjs +0 -96
  261. package/dist/lib/node-esm/chunk-XSZMX53L.mjs.map +0 -7
  262. package/dist/lib/node-esm/cli/index.mjs +0 -75
  263. package/dist/lib/node-esm/cli/index.mjs.map +0 -7
  264. package/dist/lib/node-esm/common/activation-events.mjs +0 -21
  265. package/dist/lib/node-esm/common/activation-events.mjs.map +0 -7
  266. package/dist/lib/node-esm/common/capabilities.mjs +0 -57
  267. package/dist/lib/node-esm/common/capabilities.mjs.map +0 -7
  268. package/dist/lib/node-esm/config/index.mjs +0 -9
  269. package/dist/lib/node-esm/config/index.mjs.map +0 -7
  270. package/dist/lib/node-esm/core/activation-event.mjs +0 -21
  271. package/dist/lib/node-esm/core/activation-event.mjs.map +0 -7
  272. package/dist/lib/node-esm/core/capability.mjs +0 -33
  273. package/dist/lib/node-esm/core/capability.mjs.map +0 -7
  274. package/dist/lib/node-esm/core/plugin-manager.mjs +0 -20
  275. package/dist/lib/node-esm/core/plugin-manager.mjs.map +0 -7
  276. package/dist/lib/node-esm/core/plugin.mjs +0 -52
  277. package/dist/lib/node-esm/core/plugin.mjs.map +0 -7
  278. package/dist/lib/node-esm/core/url-loader.mjs +0 -25
  279. package/dist/lib/node-esm/core/url-loader.mjs.map +0 -7
  280. package/dist/lib/node-esm/index.mjs +0 -99
  281. package/dist/lib/node-esm/index.mjs.map +0 -7
  282. package/dist/lib/node-esm/meta.json +0 -1
  283. package/dist/lib/node-esm/process-manager-capability-5S7WZ2JR.mjs +0 -133
  284. package/dist/lib/node-esm/process-manager-capability-5S7WZ2JR.mjs.map +0 -7
  285. package/dist/lib/node-esm/testing/index.mjs +0 -360
  286. package/dist/lib/node-esm/testing/index.mjs.map +0 -7
  287. package/dist/lib/node-esm/testing/react.mjs +0 -79
  288. package/dist/lib/node-esm/testing/react.mjs.map +0 -7
  289. package/dist/lib/node-esm/ui/index.mjs +0 -61
  290. package/dist/lib/node-esm/ui/index.mjs.map +0 -7
  291. package/dist/types/src/ui/components/Surface/SurfaceInfo.d.ts +0 -11
  292. package/dist/types/src/ui/components/Surface/SurfaceInfo.d.ts.map +0 -1
  293. package/src/ui/components/Surface/SurfaceInfo.tsx +0 -106
  294. package/vitest.config.ts +0 -14
@@ -2,17 +2,19 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { Atom, Registry } from '@effect-atom/atom-react';
5
+ import { Atom, Registry } from '@effect-atom/atom';
6
6
  import * as Array from 'effect/Array';
7
7
  import * as Cause from 'effect/Cause';
8
8
  import * as Deferred from 'effect/Deferred';
9
9
  import * as Duration from 'effect/Duration';
10
10
  import * as Effect from 'effect/Effect';
11
+ import * as Exit from 'effect/Exit';
11
12
  import * as Fiber from 'effect/Fiber';
12
13
  import * as Function from 'effect/Function';
13
14
  import * as HashSet from 'effect/HashSet';
14
15
  import * as PubSub from 'effect/PubSub';
15
16
  import * as Ref from 'effect/Ref';
17
+ import * as Scope from 'effect/Scope';
16
18
 
17
19
  import { EffectEx, Performance } from '@dxos/effect';
18
20
  import { BaseError } from '@dxos/errors';
@@ -298,6 +300,7 @@ class ManagerImpl implements PluginManager {
298
300
  private readonly _loadTimeout: Duration.DurationInput;
299
301
  private readonly _activationTimeout: Duration.DurationInput;
300
302
  private readonly _capabilities = new Map<string, Capability.Any[]>();
303
+ private readonly _moduleScopes = new Map<string, Scope.CloseableScope>();
301
304
  private readonly _moduleMemoMap = new Map<Plugin.PluginModule['id'], Deferred.Deferred<Capability.Any[], Error>>();
302
305
  private readonly _moduleSemaphores = new Map<Plugin.PluginModule['id'], Effect.Semaphore>();
303
306
  // Coalesces concurrent `_resolveLazyPlugin` calls per plugin id. Without
@@ -991,6 +994,10 @@ class ManagerImpl implements PluginManager {
991
994
  this._set(this._eventsFiredAtom, []);
992
995
  this._set(this._pendingResetAtom, []);
993
996
  this._moduleMemoMap.clear();
997
+ for (const scope of this._moduleScopes.values()) {
998
+ yield* Scope.close(scope, Exit.void);
999
+ }
1000
+ this._moduleScopes.clear();
994
1001
  yield* Ref.set(this._activatingEvents, []);
995
1002
  yield* Ref.set(this._activatingModules, []);
996
1003
 
@@ -1534,6 +1541,7 @@ class ManagerImpl implements PluginManager {
1534
1541
  const deferred = yield* Deferred.make<Capability.Any[], Error>();
1535
1542
  this._moduleMemoMap.set(module.id, deferred);
1536
1543
 
1544
+ const scope = yield* Scope.make();
1537
1545
  const loadEffect = Effect.gen(this, function* () {
1538
1546
  log('loading module', { module: module.id, parentEvent });
1539
1547
  performance.mark(`module:${module.id}:start`);
@@ -1542,6 +1550,7 @@ class ManagerImpl implements PluginManager {
1542
1550
  const [duration, capabilities] = yield* module.activate().pipe(
1543
1551
  Effect.provideService(Capability.Service, this.capabilities),
1544
1552
  Effect.provideService(Plugin.Service, this),
1553
+ Scope.extend(scope),
1545
1554
  // Cap activation so a single misbehaving module can't hold the
1546
1555
  // event chain open. On timeout the failure is recorded against
1547
1556
  // the plugin and surfaced as `PluginTimeoutError`.
@@ -1555,6 +1564,7 @@ class ManagerImpl implements PluginManager {
1555
1564
  Effect.timed,
1556
1565
  );
1557
1566
  const normalized = capabilities == null ? [] : Array.isArray(capabilities) ? capabilities : [capabilities];
1567
+ this._moduleScopes.set(module.id, scope);
1558
1568
  const elapsed = Duration.toMillis(duration);
1559
1569
  performance.mark(`module:${module.id}:end`);
1560
1570
  performance.measure(`module:${module.id}`, `module:${module.id}:start`, `module:${module.id}:end`);
@@ -1567,6 +1577,7 @@ class ManagerImpl implements PluginManager {
1567
1577
  });
1568
1578
  return normalized as Capability.Any[];
1569
1579
  }).pipe(
1580
+ Effect.tapErrorCause(() => Scope.close(scope, Exit.void)),
1570
1581
  Effect.withSpan('PluginManager._loadModule'),
1571
1582
  together(
1572
1583
  Effect.sleep(Duration.seconds(10)).pipe(
@@ -1654,6 +1665,12 @@ class ManagerImpl implements PluginManager {
1654
1665
  this._capabilities.delete(id);
1655
1666
  }
1656
1667
 
1668
+ const scope = this._moduleScopes.get(id);
1669
+ if (scope) {
1670
+ yield* Scope.close(scope, Exit.void);
1671
+ this._moduleScopes.delete(id);
1672
+ }
1673
+
1657
1674
  const activeIndex = this._get(this._activeAtom).findIndex((event) => event === id);
1658
1675
  if (activeIndex !== -1) {
1659
1676
  this._update(this._activeAtom, (active) => active.filter((event) => event !== id));
@@ -7,6 +7,7 @@ import * as Effect from 'effect/Effect';
7
7
  import * as Option from 'effect/Option';
8
8
  import * as Pipeable from 'effect/Pipeable';
9
9
  import * as Schema from 'effect/Schema';
10
+ import type * as Scope from 'effect/Scope';
10
11
 
11
12
  import { BaseError } from '@dxos/errors';
12
13
  import { invariant } from '@dxos/invariant';
@@ -79,6 +80,18 @@ export const isPluginModule = (value: unknown): value is PluginModule => {
79
80
  /**
80
81
  * A unit of containment of modular functionality that can be provided to an application.
81
82
  * Activation of a module is async allowing for code to split and loaded lazily.
83
+ *
84
+ * Ordering between modules is expressed through shared activation events, not registration
85
+ * order: a module `activatesOn` an event, fires events *after* it activates via
86
+ * {@link PluginModule.firesAfterActivation} (publishing "I'm ready"), and forces prerequisites
87
+ * to complete *before* it activates via {@link PluginModule.firesBeforeActivation}. To run
88
+ * module B after module A, A declares `firesAfterActivation: [E]` and B declares
89
+ * `activatesOn: E`. See `plugin-client/src/ClientPlugin.ts` for a worked example.
90
+ *
91
+ * @idiom org.dxos.app-framework.moduleActivationOrdering
92
+ * applies: sequencing one plugin module's activation before or after another
93
+ * instead-of: assuming module registration order controls activation order
94
+ * uses: {@link PluginModule.firesAfterActivation}, {@link PluginModule.firesBeforeActivation}
82
95
  */
83
96
  export interface PluginModule {
84
97
  readonly [PluginModuleTypeId]: PluginModuleTypeId;
@@ -128,7 +141,9 @@ export interface PluginModule {
128
141
  * @param props Optional props passed to the module.
129
142
  * @returns The capabilities of the module.
130
143
  */
131
- activate: (props?: any) => Effect.Effect<Capability.ModuleReturn, Error, Capability.Service | Service | never>;
144
+ activate: (
145
+ props?: any,
146
+ ) => Effect.Effect<Capability.ModuleReturn, Error, Capability.Service | Service | Scope.Scope | never>;
132
147
  }
133
148
 
134
149
  export type PluginModuleOptions = Omit<PluginModule, 'id' | typeof PluginModuleTypeId> & { id?: string };
@@ -2,7 +2,7 @@
2
2
  // Copyright 2026 DXOS.org
3
3
  //
4
4
 
5
- import { Atom, type Registry as AtomRegistry } from '@effect-atom/atom-react';
5
+ import { Atom, type Registry as AtomRegistry } from '@effect-atom/atom';
6
6
  import * as Effect from 'effect/Effect';
7
7
 
8
8
  import { EffectEx } from '@dxos/effect';
@@ -8,8 +8,15 @@ import * as Layer from 'effect/Layer';
8
8
  import * as ManagedRuntime from 'effect/ManagedRuntime';
9
9
 
10
10
  import { LayerSpec, OperationHandlerSet, Process, ServiceResolver, Trace } from '@dxos/compute';
11
- import { LayerStack, ProcessManager } from '@dxos/compute-runtime';
11
+ import {
12
+ LayerStack,
13
+ ProcessManager,
14
+ ProcessMonitor,
15
+ RemoteProcessManager,
16
+ RemoteTraceMonitor,
17
+ } from '@dxos/compute-runtime';
12
18
  import { invariant } from '@dxos/invariant';
19
+ import { log } from '@dxos/log';
13
20
  // Explicit import so the emitted `.d.ts` references the package via its public
14
21
  // alias instead of a relative `node_modules` path (TS2883).
15
22
  import { OperationInvoker } from '@dxos/operation';
@@ -50,6 +57,10 @@ export default Capability.makeModule(
50
57
 
51
58
  const layerSpecs = yield* Capability.getAll(Capabilities.LayerSpec);
52
59
  const traceSinkFactories = yield* Capability.getAll(Capabilities.TraceSink);
60
+ // Optional swarm-backed remote trace source (DX-1125); first contribution wins, else empty.
61
+ const remoteTraceMonitors = yield* Capability.getAll(Capabilities.RemoteTraceMonitor);
62
+
63
+ log.info('setup process manager', { traceSinkFactories });
53
64
 
54
65
  // Forward reference to `ProcessManager.ProcessManagerService`. The runtime
55
66
  // that owns the manager depends transitively on `ServiceResolver` (which is
@@ -121,7 +132,19 @@ export default Capability.makeModule(
121
132
  Layer.provide(Layer.mergeAll(processManagerLayer, baseLayer)),
122
133
  );
123
134
 
124
- const runtimeLayer = Layer.mergeAll(baseLayer, processManagerLayer, operationInvokerLayer);
135
+ // App-framework has no EDGE runtime, so the remote process view is empty;
136
+ // the aggregate monitor therefore equals the local process tree.
137
+ const remoteProcessManagerLayer = RemoteProcessManager.layerNoop.pipe(Layer.provide(baseLayer));
138
+ // Remote ephemeral trace (DX-1125): use the first contributed swarm-backed monitor, else no-op.
139
+ const remoteTraceMonitorLayer =
140
+ remoteTraceMonitors.length > 0
141
+ ? Layer.succeed(RemoteTraceMonitor.Service, remoteTraceMonitors[0])
142
+ : RemoteTraceMonitor.layerNoop;
143
+ const processMonitorLayer = ProcessMonitor.layer.pipe(
144
+ Layer.provide(Layer.mergeAll(processManagerLayer, remoteProcessManagerLayer, remoteTraceMonitorLayer, baseLayer)),
145
+ );
146
+
147
+ const runtimeLayer = Layer.mergeAll(baseLayer, processManagerLayer, operationInvokerLayer, processMonitorLayer);
125
148
 
126
149
  const managedRuntime = ManagedRuntime.make(runtimeLayer as Layer.Layer<any, any, never>);
127
150
 
@@ -0,0 +1,40 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import React from 'react';
6
+
7
+ import { ErrorFallback, type FallbackProps } from '@dxos/react-error-boundary';
8
+ import { downloadLogs } from '@dxos/storybook-addon-logger/download';
9
+
10
+ /**
11
+ * Default `ErrorBoundary` fallback for storybook-hosted apps (`withPluginManager`/`useApp`). Adds
12
+ * a "Download logs" action alongside the theme-independent default `ErrorFallback`, so a crashed
13
+ * story still offers a one-click way to grab the story's buffered logs, matching the manager
14
+ * toolbar's button (see `@dxos/storybook-addon-logger`).
15
+ *
16
+ * Deliberately does NOT depend on `@dxos/react-ui`'s themed components: `useApp`'s `ErrorBoundary`
17
+ * can catch failures that happen before/during an app's own theme setup, so its fallback must keep
18
+ * working even when no theme context is available — the same reason `ErrorFallback` itself uses
19
+ * inline styles rather than Tailwind/theme dependencies.
20
+ */
21
+ export const StorybookErrorFallback = (props: FallbackProps) => (
22
+ <div>
23
+ <ErrorFallback {...props} />
24
+ <button
25
+ type='button'
26
+ onClick={downloadLogs}
27
+ style={{
28
+ margin: '0 1rem 1rem',
29
+ padding: '0.25rem 0.75rem',
30
+ fontSize: '0.875rem',
31
+ background: 'transparent',
32
+ border: '1px solid #888888',
33
+ borderRadius: '0.375rem',
34
+ cursor: 'pointer',
35
+ }}
36
+ >
37
+ Download logs
38
+ </button>
39
+ </div>
40
+ );
@@ -2,7 +2,7 @@
2
2
  // Copyright 2026 DXOS.org
3
3
  //
4
4
 
5
- import { type Registry } from '@effect-atom/atom-react';
5
+ import { type Registry } from '@effect-atom/atom';
6
6
  import * as Duration from 'effect/Duration';
7
7
  import * as Effect from 'effect/Effect';
8
8
  import * as PubSub from 'effect/PubSub';
@@ -5,4 +5,6 @@
5
5
  export * from './harness';
6
6
  export * from './operationCapture';
7
7
  export * from './service';
8
+ export * from './StorybookErrorFallback';
8
9
  export * from './withPluginManager';
10
+ export * from './withSurfaceDebug';
@@ -9,6 +9,7 @@ import { describe, test } from 'vitest';
9
9
  import { DXN } from '@dxos/keys';
10
10
 
11
11
  import { ActivationEvents, Capabilities } from '../common';
12
+ import * as Role from '../common/Role';
12
13
  import { Capability, Plugin } from '../core';
13
14
  import { Surface } from '../ui';
14
15
  import { createTestApp } from './harness';
@@ -16,7 +17,7 @@ import { render, renderSurface } from './react';
16
17
 
17
18
  const testMeta = Plugin.makeMeta({ key: DXN.make('org.dxos.plugin.test.reactHarness'), name: 'ReactHarnessTest' });
18
19
 
19
- const GreetingRole = Surface.makeType<{ message: string }>('org.dxos.test.role.greeting');
20
+ const GreetingRole = Role.make<{ message: string }>('org.dxos.test.role.greeting');
20
21
 
21
22
  const TestPlugin = Plugin.define(testMeta).pipe(
22
23
  Plugin.addModule({
@@ -4,16 +4,18 @@
4
4
 
5
5
  import { RegistryContext } from '@effect-atom/atom-react';
6
6
  import { type RenderOptions, type RenderResult, render as rtlRender } from '@testing-library/react';
7
- import React, { type FC, Fragment, type PropsWithChildren, type ReactNode } from 'react';
7
+ import React, { type FC, Fragment, type PropsWithChildren, type ReactNode, useMemo } from 'react';
8
8
 
9
9
  import { ContextProtocolProvider } from '@dxos/web-context-react';
10
10
 
11
11
  import { Capabilities } from '../common';
12
+ import * as Role from '../common/Role';
12
13
  import { PluginManagerContext } from '../context';
13
14
  import { topologicalSort } from '../helpers';
14
15
  import { PluginManagerProvider } from '../ui/components/PluginManager/PluginManagerProvider';
15
16
  import { SurfaceComponent } from '../ui/components/Surface/SurfaceComponent';
16
- import { type RoleToken } from '../ui/components/Surface/types';
17
+ import { SurfaceManager } from '../ui/components/Surface/SurfaceManager';
18
+ import { SurfaceManagerProvider } from '../ui/components/Surface/SurfaceManagerContext';
17
19
  import { type TestHarness } from './harness';
18
20
 
19
21
  export type HarnessRenderOptions = Omit<RenderOptions, 'wrapper'> & {
@@ -35,9 +37,9 @@ export const render = (harness: TestHarness, ui: ReactNode, options?: HarnessRen
35
37
  return rtlRender(<>{ui}</>, { ...rest, wrapper: Wrapper });
36
38
  };
37
39
 
38
- export type RenderSurfaceProps<TToken extends RoleToken<any>> = {
40
+ export type RenderSurfaceProps<TToken extends Role.Role<any>> = {
39
41
  type: TToken;
40
- data?: TToken extends RoleToken<infer D> ? D : never;
42
+ data?: TToken extends Role.Role<infer D> ? D : never;
41
43
  limit?: number;
42
44
  fallback?: FC<{ error: Error; data?: any }>;
43
45
  placeholder?: ReactNode;
@@ -46,7 +48,7 @@ export type RenderSurfaceProps<TToken extends RoleToken<any>> = {
46
48
  /**
47
49
  * Renders a `Surface` with the given role token/data inside the harness provider tree.
48
50
  */
49
- export const renderSurface = <TToken extends RoleToken<any>>(
51
+ export const renderSurface = <TToken extends Role.Role<any>>(
50
52
  harness: TestHarness,
51
53
  props: RenderSurfaceProps<TToken>,
52
54
  options?: HarnessRenderOptions,
@@ -68,13 +70,17 @@ const HarnessProviders = ({ harness, extra, children }: HarnessProvidersProps) =
68
70
  const contributed = harness.getAll(Capabilities.ReactContext);
69
71
  const ContributedContext = composeContexts(contributed);
70
72
  const ExtraContext = composeExtra(extra);
73
+ // Mirror useApp's provider stack (see SurfaceManager) so the unit-test render path matches the app.
74
+ const surfaces = useMemo(() => new SurfaceManager(harness.manager.capabilities), [harness]);
71
75
  return (
72
76
  <PluginManagerProvider value={harness.manager}>
73
77
  <ContextProtocolProvider value={harness.manager} context={PluginManagerContext}>
74
78
  <RegistryContext.Provider value={harness.registry}>
75
- <ContributedContext>
76
- <ExtraContext>{children}</ExtraContext>
77
- </ContributedContext>
79
+ <SurfaceManagerProvider value={surfaces}>
80
+ <ContributedContext>
81
+ <ExtraContext>{children}</ExtraContext>
82
+ </ContributedContext>
83
+ </SurfaceManagerProvider>
78
84
  </RegistryContext.Provider>
79
85
  </ContextProtocolProvider>
80
86
  </PluginManagerProvider>
@@ -5,14 +5,16 @@
5
5
  import { type Meta, type StoryObj } from '@storybook/react-vite';
6
6
  import React from 'react';
7
7
 
8
+ import { ThrowError } from '@dxos/react-ui';
8
9
  import { withTheme } from '@dxos/react-ui/testing';
9
10
 
10
11
  import { Capabilities } from '../common';
12
+ import * as Role from '../common/Role';
11
13
  import { Capability } from '../core';
12
14
  import { Surface } from '../ui';
13
15
  import { withPluginManager } from './withPluginManager';
14
16
 
15
- const MainRole = Surface.makeType<Record<string, never>>('org.dxos.test.role.main');
17
+ const MainRole = Role.make<Record<string, never>>('org.dxos.test.role.main');
16
18
 
17
19
  const DefaultStory = () => {
18
20
  console.log('Render');
@@ -49,3 +51,16 @@ export default meta;
49
51
  type Story = StoryObj<typeof meta>;
50
52
 
51
53
  export const Default: Story = {};
54
+
55
+ /**
56
+ * Demonstrates `withPluginManager`'s default fallback (`StorybookErrorFallback`): when the app
57
+ * throws, the theme-independent `ErrorFallback` renders with a "Download logs" button alongside
58
+ * the usual "Copy" action.
59
+ */
60
+ export const Crashes: Story = {
61
+ render: () => <ThrowError />,
62
+ play: async () => {
63
+ // This story intentionally renders an ErrorBoundary fallback; clear the smoke-test error flag.
64
+ (window as any).__ERROR_BOUNDARY_ERRORS__ = [];
65
+ },
66
+ };
@@ -15,6 +15,7 @@ import { type MaybeProvider, getProviderValue } from '@dxos/util';
15
15
  import { ActivationEvents, Capabilities } from '../common';
16
16
  import { type ActivationEvent, Capability, type CapabilityManager, Plugin, PluginManager } from '../core';
17
17
  import { type UseAppOptions, useApp } from '../ui';
18
+ import { StorybookErrorFallback } from './StorybookErrorFallback';
18
19
 
19
20
  /**
20
21
  * @internal
@@ -50,6 +51,7 @@ export const setupPluginManager = ({
50
51
  };
51
52
 
52
53
  type ManagedPluginManagerState = {
54
+ fallback?: UseAppOptions['fallback'];
53
55
  fireEvents?: (ActivationEvent.ActivationEvent | string)[];
54
56
  pluginManager: PluginManager.PluginManager;
55
57
  setupEvents?: ActivationEvent.ActivationEvent[];
@@ -94,6 +96,7 @@ export const withPluginManager = <Args,>(init: WithPluginManagerInitializer<Args
94
96
  pluginManager,
95
97
  setupEvents: options.setupEvents,
96
98
  fireEvents: options.fireEvents,
99
+ fallback: options.fallback,
97
100
  storyId,
98
101
  });
99
102
 
@@ -112,13 +115,21 @@ export const withPluginManager = <Args,>(init: WithPluginManagerInitializer<Args
112
115
  };
113
116
  };
114
117
 
115
- const WithPluginManagerApp = ({ fireEvents, pluginManager, setupEvents, storyId }: ManagedPluginManagerState) => {
118
+ const WithPluginManagerApp = ({
119
+ fallback,
120
+ fireEvents,
121
+ pluginManager,
122
+ setupEvents,
123
+ storyId,
124
+ }: ManagedPluginManagerState) => {
116
125
  // Fire deprecated events only after the effect-owned manager for this story exists.
117
126
  useAsyncEffect(async () => {
118
127
  await Promise.all(fireEvents?.map((event) => pluginManager.activate(event)) ?? []);
119
128
  }, [fireEvents, pluginManager, storyId]);
120
129
 
121
- const App = useApp({ pluginManager, setupEvents });
130
+ // Default to a fallback that offers "Download logs" so a crashed story is still debuggable;
131
+ // callers can override via `withPluginManager({ fallback })`.
132
+ const App = useApp({ pluginManager, setupEvents, fallback: fallback ?? StorybookErrorFallback });
122
133
  return <App />;
123
134
  };
124
135
 
@@ -0,0 +1,22 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { type Decorator } from '@storybook/react';
6
+ import React from 'react';
7
+
8
+ import { Surface } from '../ui';
9
+
10
+ /**
11
+ * Storybook decorator that enables the surface debug overlays (`<dx-surface>` wrappers + boundary
12
+ * badges) for the wrapped story. The flag is a `window` singleton, so it is (re)applied on every
13
+ * render — enabling it on decorated stories and clearing it otherwise, so the setting never leaks
14
+ * between stories. Prefer this over the `.storybook/preview` flag when the story must also render
15
+ * under the aggregate root Storybook, which does not load a per-package preview.
16
+ */
17
+ export const withSurfaceDebug =
18
+ (enabled = true): Decorator =>
19
+ (Story) => {
20
+ Surface.setDebug(enabled);
21
+ return <Story />;
22
+ };
@@ -7,36 +7,9 @@ import React, { type PropsWithChildren, useEffect, useLayoutEffect } from 'react
7
7
  import { Capabilities } from '../../../common';
8
8
  import { topologicalSort } from '../../../helpers';
9
9
  import { LoadingState, type StartupProgress, type UseAppOptions, useCapabilities, useLoading } from '../../hooks';
10
+ import { bootLoader } from './loader';
10
11
 
11
- declare global {
12
- interface Window {
13
- /**
14
- * Driver injected by `@dxos/app-framework/vite-plugin`'s `bootLoaderPlugin`
15
- * (a Solid app inlined into `index.html`). Declared here — on a module that
16
- * is part of the `@dxos/app-framework/ui` export surface — so the React side
17
- * and host apps (e.g. composer-app) can drive the loader without each
18
- * re-declaring the type. The canonical definition lives in the plugin's
19
- * `loader-app/types.ts`; this mirror exists because that source compiles in a
20
- * separate (Solid) program that doesn't ship its globals to consumers.
21
- */
22
- __bootLoader?: {
23
- status: (payload: {
24
- event?: string;
25
- module?: string;
26
- humanized: string;
27
- /**
28
- * Optional `(index/total)` tick. When present, the loader replaces the
29
- * current line in place ("Loading plugins (12/80)") instead of appending
30
- * a new entry — keeps the visible log compact during long counted phases.
31
- */
32
- range?: { index: number; total: number };
33
- }) => void;
34
- progress: (fraction?: number) => void;
35
- ready: () => void;
36
- dismiss: () => void;
37
- };
38
- }
39
- }
12
+ const FIRST_INTERACTIVE_MARK = 'app-framework:first-interactive';
40
13
 
41
14
  export type AppProps = Pick<UseAppOptions, 'debounce'> & {
42
15
  ready: boolean;
@@ -44,8 +17,6 @@ export type AppProps = Pick<UseAppOptions, 'debounce'> & {
44
17
  progress?: StartupProgress;
45
18
  };
46
19
 
47
- const FIRST_INTERACTIVE_MARK = 'app-framework:first-interactive';
48
-
49
20
  export const App = ({ ready, error, debounce, progress }: AppProps) => {
50
21
  const reactContexts = useCapabilities(Capabilities.ReactContext);
51
22
  const reactRoots = useCapabilities(Capabilities.ReactRoot);
@@ -62,10 +33,11 @@ export const App = ({ ready, error, debounce, progress }: AppProps) => {
62
33
  if (stage >= LoadingState.FadeOut) {
63
34
  return;
64
35
  }
36
+
65
37
  const fraction = progress?.progress ?? 0;
66
- window.__bootLoader?.progress(0.5 + fraction * 0.5);
38
+ bootLoader?.progress(0.5 + fraction * 0.5);
67
39
  if (progress?.humanizedName) {
68
- window.__bootLoader?.status({
40
+ bootLoader?.status({
69
41
  event: progress.event,
70
42
  module: progress.module,
71
43
  humanized: `Activating ${progress.humanizedName}`,
@@ -78,7 +50,7 @@ export const App = ({ ready, error, debounce, progress }: AppProps) => {
78
50
  // same frame the real shell starts rendering beneath it.
79
51
  useLayoutEffect(() => {
80
52
  if (stage >= LoadingState.FadeOut) {
81
- window.__bootLoader?.ready();
53
+ bootLoader?.ready();
82
54
  }
83
55
  }, [stage]);
84
56
 
@@ -97,7 +69,7 @@ export const App = ({ ready, error, debounce, progress }: AppProps) => {
97
69
  if (performance.getEntriesByName(FIRST_INTERACTIVE_MARK).length === 0) {
98
70
  performance.mark(FIRST_INTERACTIVE_MARK);
99
71
  }
100
- window.__bootLoader?.dismiss();
72
+ bootLoader?.dismiss();
101
73
  }, [placeholderDismissed]);
102
74
 
103
75
  // Used in tests to exercise the error boundary & reset dialog (see
@@ -3,3 +3,4 @@
3
3
  //
4
4
 
5
5
  export * from './App';
6
+ export * from './loader';
@@ -0,0 +1,39 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ declare global {
6
+ interface BootLoader {
7
+ status: (payload: {
8
+ event?: string;
9
+ module?: string;
10
+ humanized: string;
11
+ /**
12
+ * Optional `(index/total)` tick. When present, the loader replaces the
13
+ * current line in place ("Loading plugins (12/80)") instead of appending
14
+ * a new entry — keeps the visible log compact during long counted phases.
15
+ */
16
+ range?: { index: number; total: number };
17
+ }) => void;
18
+ progress: (fraction?: number) => void;
19
+ ready: () => void;
20
+ dismiss: () => void;
21
+ }
22
+
23
+ interface Window {
24
+ /**
25
+ * Driver injected by `@dxos/app-framework/vite-plugin`'s `bootLoaderPlugin`
26
+ * (a Solid app inlined into `index.html`). Declared here — on a module that
27
+ * is part of the `@dxos/app-framework/ui` export surface — so the React side
28
+ * and host apps (e.g. composer-app) can drive the loader without each
29
+ * re-declaring the type. The canonical definition lives in the plugin's
30
+ * `loader-app/types.ts`; this mirror exists because that source compiles in a
31
+ * separate (Solid) program that doesn't ship its globals to consumers.
32
+ */
33
+ __bootLoader?: BootLoader;
34
+ }
35
+ }
36
+
37
+ // Guarded: `@dxos/app-framework/ui` has a `node` export and this module is re-exported through the
38
+ // UI barrel, so a bare top-level `window` read would throw when imported in SSR / tests / tooling.
39
+ export const bootLoader = typeof window !== 'undefined' ? window.__bootLoader : undefined;
@@ -0,0 +1,43 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
+ import React from 'react';
7
+
8
+ import { Button } from '@dxos/react-ui';
9
+ import { withLayout, withTheme } from '@dxos/react-ui/testing';
10
+
11
+ import { HomeSection } from './HomeSection';
12
+
13
+ const DefaultStory = () => (
14
+ <HomeSection.Root>
15
+ <HomeSection.Header title='Recent' onClose={() => {}} />
16
+ <div className='rounded-sm bg-group-surface p-4 text-description'>Section content.</div>
17
+ </HomeSection.Root>
18
+ );
19
+
20
+ const WithActionsStory = () => (
21
+ <HomeSection.Root>
22
+ <HomeSection.Header title='Activity' onClose={() => {}}>
23
+ <Button variant='ghost'>All</Button>
24
+ <Button variant='ghost'>30d</Button>
25
+ </HomeSection.Header>
26
+ <div className='rounded-sm bg-group-surface p-4 text-description'>Section content.</div>
27
+ </HomeSection.Root>
28
+ );
29
+
30
+ const meta = {
31
+ title: 'sdk/app-framework/components/HomeSection',
32
+ render: DefaultStory,
33
+ decorators: [withTheme(), withLayout({ layout: 'fullscreen', classNames: 'p-4' })],
34
+ parameters: { layout: 'fullscreen' },
35
+ } satisfies Meta<typeof DefaultStory>;
36
+
37
+ export default meta;
38
+
39
+ type Story = StoryObj<typeof meta>;
40
+
41
+ export const Default: Story = {};
42
+
43
+ export const WithActions: Story = { render: WithActionsStory };
@@ -0,0 +1,69 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import React, { type PropsWithChildren, forwardRef } from 'react';
6
+
7
+ import { SystemIconButton, type ThemedClassName } from '@dxos/react-ui';
8
+ import { mx } from '@dxos/ui-theme';
9
+
10
+ //
11
+ // Root
12
+ //
13
+
14
+ type HomeSectionRootProps = ThemedClassName<PropsWithChildren>;
15
+
16
+ /**
17
+ * Shared container for a Home content section: a centered, max-width column. Home surface
18
+ * contributors (welcome, recent, dashboard, suggestions) render inside one so they share a
19
+ * consistent width and rhythm regardless of which plugin owns them.
20
+ */
21
+ const HomeSectionRoot = forwardRef<HTMLDivElement, HomeSectionRootProps>(({ classNames, children }, forwardedRef) => (
22
+ <div ref={forwardedRef} className={mx('flex flex-col w-full mx-auto max-w-[40rem]', classNames)}>
23
+ {children}
24
+ </div>
25
+ ));
26
+
27
+ HomeSectionRoot.displayName = 'HomeSection.Root';
28
+
29
+ //
30
+ // Header
31
+ //
32
+
33
+ type HomeSectionHeaderProps = ThemedClassName<
34
+ PropsWithChildren<{
35
+ /** Section heading. */
36
+ title?: string;
37
+ /** When provided, renders a close affordance that hides the section. */
38
+ onClose?: () => void;
39
+ }>
40
+ >;
41
+
42
+ /**
43
+ * Header row for a Home section: a muted title, an optional trailing actions slot (`children`),
44
+ * and an optional close affordance. `children` render before the close button so section-specific
45
+ * controls (e.g. a range selector) sit inline with it.
46
+ */
47
+ const HomeSectionHeader = forwardRef<HTMLDivElement, HomeSectionHeaderProps>(
48
+ ({ title, onClose, classNames, children }, forwardedRef) => (
49
+ <div ref={forwardedRef} className={mx('flex items-center gap-2', classNames)}>
50
+ {title && <h2 className='grow truncate text-sm font-medium text-description'>{title}</h2>}
51
+ {!title && <span className='grow' />}
52
+ {children}
53
+ {onClose && <SystemIconButton.Close variant='ghost' iconOnly onClick={onClose} />}
54
+ </div>
55
+ ),
56
+ );
57
+
58
+ HomeSectionHeader.displayName = 'HomeSection.Header';
59
+
60
+ //
61
+ // HomeSection
62
+ //
63
+
64
+ export const HomeSection = {
65
+ Root: HomeSectionRoot,
66
+ Header: HomeSectionHeader,
67
+ };
68
+
69
+ export type { HomeSectionHeaderProps, HomeSectionRootProps };