@dxos/plugin-registry 0.8.4-main.8360d9e660 → 0.8.4-main.8baae0fced

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 (262) hide show
  1. package/LICENSE +102 -5
  2. package/dist/lib/neutral/RegistryPlugin.mjs +27 -0
  3. package/dist/lib/neutral/RegistryPlugin.mjs.map +7 -0
  4. package/dist/lib/{browser/cli/index.mjs → neutral/RegistryPlugin.node.mjs} +28 -97
  5. package/dist/lib/neutral/RegistryPlugin.node.mjs.map +7 -0
  6. package/dist/lib/neutral/RegistryPlugin.workerd.mjs +10 -0
  7. package/dist/lib/neutral/RegistryPlugin.workerd.mjs.map +7 -0
  8. package/dist/lib/neutral/chunk-MYLRMFLY.mjs +34 -0
  9. package/dist/lib/neutral/chunk-MYLRMFLY.mjs.map +7 -0
  10. package/dist/lib/neutral/chunk-XK2JMH3H.mjs +33 -0
  11. package/dist/lib/neutral/chunk-XK2JMH3H.mjs.map +7 -0
  12. package/dist/lib/neutral/index.mjs +25 -0
  13. package/dist/lib/neutral/index.mjs.map +7 -0
  14. package/dist/lib/neutral/meta.json +1 -0
  15. package/dist/lib/neutral/open-plugin-registry-II735N76.mjs +15 -0
  16. package/dist/lib/neutral/open-plugin-registry-II735N76.mjs.map +7 -0
  17. package/dist/lib/neutral/plugin.mjs +18 -0
  18. package/dist/lib/neutral/plugin.mjs.map +7 -0
  19. package/dist/lib/neutral/translations.mjs +70 -0
  20. package/dist/lib/neutral/translations.mjs.map +7 -0
  21. package/dist/types/src/RegistryPlugin.d.ts +1 -0
  22. package/dist/types/src/RegistryPlugin.d.ts.map +1 -1
  23. package/dist/types/src/{cli/plugin.d.ts → RegistryPlugin.node.d.ts} +2 -1
  24. package/dist/types/src/RegistryPlugin.node.d.ts.map +1 -0
  25. package/dist/types/src/RegistryPlugin.test.d.ts +2 -0
  26. package/dist/types/src/RegistryPlugin.test.d.ts.map +1 -0
  27. package/dist/types/src/RegistryPlugin.workerd.d.ts +4 -0
  28. package/dist/types/src/RegistryPlugin.workerd.d.ts.map +1 -0
  29. package/dist/types/src/capabilities/app-graph-builder.d.ts.map +1 -0
  30. package/dist/types/src/capabilities/dev-plugin-loader.d.ts +11 -0
  31. package/dist/types/src/capabilities/dev-plugin-loader.d.ts.map +1 -0
  32. package/dist/types/src/capabilities/index.d.ts +15 -3
  33. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  34. package/dist/types/src/capabilities/operation-handler.d.ts +6 -0
  35. package/dist/types/src/capabilities/operation-handler.d.ts.map +1 -0
  36. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -0
  37. package/dist/types/src/capabilities/settings.d.ts +14 -0
  38. package/dist/types/src/capabilities/settings.d.ts.map +1 -0
  39. package/dist/types/src/{cli/commands → commands}/disable.d.ts +2 -2
  40. package/dist/types/src/commands/disable.d.ts.map +1 -0
  41. package/dist/types/src/{cli/commands → commands}/enable.d.ts +2 -2
  42. package/dist/types/src/commands/enable.d.ts.map +1 -0
  43. package/dist/types/src/commands/index.d.ts.map +1 -0
  44. package/dist/types/src/commands/list.d.ts +7 -0
  45. package/dist/types/src/commands/list.d.ts.map +1 -0
  46. package/dist/types/src/commands/util.d.ts.map +1 -0
  47. package/dist/types/src/components/DisableDependentsAlert/DisableDependentsAlert.d.ts +24 -0
  48. package/dist/types/src/components/DisableDependentsAlert/DisableDependentsAlert.d.ts.map +1 -0
  49. package/dist/types/src/components/DisableDependentsAlert/index.d.ts +2 -0
  50. package/dist/types/src/components/DisableDependentsAlert/index.d.ts.map +1 -0
  51. package/dist/types/src/components/PluginDetail/PluginDetail.d.ts +75 -2
  52. package/dist/types/src/components/PluginDetail/PluginDetail.d.ts.map +1 -1
  53. package/dist/types/src/components/PluginDetail/PluginDetail.stories.d.ts +60 -16
  54. package/dist/types/src/components/PluginDetail/PluginDetail.stories.d.ts.map +1 -1
  55. package/dist/types/src/components/PluginFailureBadge/PluginFailureBadge.d.ts +20 -0
  56. package/dist/types/src/components/PluginFailureBadge/PluginFailureBadge.d.ts.map +1 -0
  57. package/dist/types/src/components/PluginFailureBadge/index.d.ts +2 -0
  58. package/dist/types/src/components/PluginFailureBadge/index.d.ts.map +1 -0
  59. package/dist/types/src/components/PluginList/PluginItem.d.ts +29 -2
  60. package/dist/types/src/components/PluginList/PluginItem.d.ts.map +1 -1
  61. package/dist/types/src/components/PluginList/PluginList.d.ts +18 -3
  62. package/dist/types/src/components/PluginList/PluginList.d.ts.map +1 -1
  63. package/dist/types/src/components/PluginList/PluginList.stories.d.ts +58 -16
  64. package/dist/types/src/components/PluginList/PluginList.stories.d.ts.map +1 -1
  65. package/dist/types/src/components/RegistrySettings/RegistrySettings.d.ts +18 -0
  66. package/dist/types/src/components/RegistrySettings/RegistrySettings.d.ts.map +1 -0
  67. package/dist/types/src/components/RegistrySettings/index.d.ts +2 -0
  68. package/dist/types/src/components/RegistrySettings/index.d.ts.map +1 -0
  69. package/dist/types/src/components/index.d.ts +3 -0
  70. package/dist/types/src/components/index.d.ts.map +1 -1
  71. package/dist/types/src/containers/BaseRegistryArticle/BaseRegistryArticle.d.ts +20 -0
  72. package/dist/types/src/containers/BaseRegistryArticle/BaseRegistryArticle.d.ts.map +1 -0
  73. package/dist/types/src/containers/BaseRegistryArticle/index.d.ts +2 -0
  74. package/dist/types/src/containers/BaseRegistryArticle/index.d.ts.map +1 -0
  75. package/dist/types/src/containers/LoadPluginDialog/LoadPluginDialog.d.ts +4 -0
  76. package/dist/types/src/containers/LoadPluginDialog/LoadPluginDialog.d.ts.map +1 -0
  77. package/dist/types/src/containers/LoadPluginDialog/index.d.ts +2 -0
  78. package/dist/types/src/containers/LoadPluginDialog/index.d.ts.map +1 -0
  79. package/dist/types/src/containers/PluginArticle/PluginArticle.d.ts.map +1 -1
  80. package/dist/types/src/containers/PluginArticle/index.d.ts +1 -2
  81. package/dist/types/src/containers/PluginArticle/index.d.ts.map +1 -1
  82. package/dist/types/src/containers/PublicRegistryArticle/PublicRegistryArticle.d.ts +8 -0
  83. package/dist/types/src/containers/PublicRegistryArticle/PublicRegistryArticle.d.ts.map +1 -0
  84. package/dist/types/src/containers/PublicRegistryArticle/index.d.ts +2 -0
  85. package/dist/types/src/containers/PublicRegistryArticle/index.d.ts.map +1 -0
  86. package/dist/types/src/containers/RegistryArticle/RegistryArticle.d.ts +14 -0
  87. package/dist/types/src/containers/RegistryArticle/RegistryArticle.d.ts.map +1 -0
  88. package/dist/types/src/containers/RegistryArticle/index.d.ts +2 -0
  89. package/dist/types/src/containers/RegistryArticle/index.d.ts.map +1 -0
  90. package/dist/types/src/containers/RegistrySettingsContainer/RegistrySettingsContainer.d.ts +13 -0
  91. package/dist/types/src/containers/RegistrySettingsContainer/RegistrySettingsContainer.d.ts.map +1 -0
  92. package/dist/types/src/containers/RegistrySettingsContainer/index.d.ts +2 -0
  93. package/dist/types/src/containers/RegistrySettingsContainer/index.d.ts.map +1 -0
  94. package/dist/types/src/containers/index.d.ts +5 -1
  95. package/dist/types/src/containers/index.d.ts.map +1 -1
  96. package/dist/types/src/hooks/index.d.ts +9 -0
  97. package/dist/types/src/hooks/index.d.ts.map +1 -0
  98. package/dist/types/src/hooks/useAutoTags.d.ts +20 -0
  99. package/dist/types/src/hooks/useAutoTags.d.ts.map +1 -0
  100. package/dist/types/src/hooks/useCatalogEntry.d.ts +11 -0
  101. package/dist/types/src/hooks/useCatalogEntry.d.ts.map +1 -0
  102. package/dist/types/src/hooks/useDisableConfirmation.d.ts +19 -0
  103. package/dist/types/src/hooks/useDisableConfirmation.d.ts.map +1 -0
  104. package/dist/types/src/hooks/useInstalledVersionTag.d.ts +18 -0
  105. package/dist/types/src/hooks/useInstalledVersionTag.d.ts.map +1 -0
  106. package/dist/types/src/hooks/usePluginActions.d.ts +36 -0
  107. package/dist/types/src/hooks/usePluginActions.d.ts.map +1 -0
  108. package/dist/types/src/hooks/useRegistryPlugins.d.ts +15 -0
  109. package/dist/types/src/hooks/useRegistryPlugins.d.ts.map +1 -0
  110. package/dist/types/src/hooks/useUpdateAvailable.d.ts +18 -0
  111. package/dist/types/src/hooks/useUpdateAvailable.d.ts.map +1 -0
  112. package/dist/types/src/hooks/useVersionPicker.d.ts +21 -0
  113. package/dist/types/src/hooks/useVersionPicker.d.ts.map +1 -0
  114. package/dist/types/src/index.d.ts +1 -1
  115. package/dist/types/src/index.d.ts.map +1 -1
  116. package/dist/types/src/meta.d.ts +16 -1
  117. package/dist/types/src/meta.d.ts.map +1 -1
  118. package/dist/types/src/operations/index.d.ts +3 -0
  119. package/dist/types/src/operations/index.d.ts.map +1 -0
  120. package/dist/types/src/operations/open-plugin-registry.d.ts +5 -0
  121. package/dist/types/src/operations/open-plugin-registry.d.ts.map +1 -0
  122. package/dist/types/src/plugin.d.ts +4 -0
  123. package/dist/types/src/plugin.d.ts.map +1 -0
  124. package/dist/types/src/storage.d.ts.map +1 -0
  125. package/dist/types/src/translations.d.ts +57 -16
  126. package/dist/types/src/translations.d.ts.map +1 -1
  127. package/dist/types/src/types.d.ts +30 -1
  128. package/dist/types/src/types.d.ts.map +1 -1
  129. package/dist/types/tsconfig.tsbuildinfo +1 -1
  130. package/package.json +60 -37
  131. package/src/{cli/plugin.ts → RegistryPlugin.node.ts} +3 -1
  132. package/src/RegistryPlugin.test.ts +24 -0
  133. package/src/RegistryPlugin.tsx +13 -5
  134. package/src/RegistryPlugin.workerd.ts +11 -0
  135. package/src/capabilities/app-graph-builder.ts +200 -0
  136. package/src/capabilities/dev-plugin-loader.ts +39 -0
  137. package/src/capabilities/index.ts +11 -3
  138. package/src/capabilities/operation-handler.ts +16 -0
  139. package/src/capabilities/react-surface.tsx +145 -0
  140. package/src/capabilities/settings.ts +34 -0
  141. package/src/components/DisableDependentsAlert/DisableDependentsAlert.tsx +69 -0
  142. package/src/components/DisableDependentsAlert/index.ts +5 -0
  143. package/src/components/PluginDetail/PluginDetail.stories.tsx +5 -4
  144. package/src/components/PluginDetail/PluginDetail.tsx +320 -62
  145. package/src/components/PluginFailureBadge/PluginFailureBadge.tsx +68 -0
  146. package/src/components/PluginFailureBadge/index.ts +5 -0
  147. package/src/components/PluginList/PluginItem.tsx +117 -15
  148. package/src/components/PluginList/PluginList.stories.tsx +17 -14
  149. package/src/components/PluginList/PluginList.tsx +32 -4
  150. package/src/components/RegistrySettings/RegistrySettings.tsx +120 -0
  151. package/src/components/RegistrySettings/index.ts +5 -0
  152. package/src/components/index.ts +3 -0
  153. package/src/containers/BaseRegistryArticle/BaseRegistryArticle.tsx +178 -0
  154. package/src/containers/BaseRegistryArticle/index.ts +5 -0
  155. package/src/containers/LoadPluginDialog/LoadPluginDialog.tsx +87 -0
  156. package/src/containers/LoadPluginDialog/index.ts +5 -0
  157. package/src/containers/PluginArticle/PluginArticle.tsx +102 -13
  158. package/src/containers/PluginArticle/index.ts +1 -3
  159. package/src/containers/PublicRegistryArticle/PublicRegistryArticle.tsx +192 -0
  160. package/src/containers/PublicRegistryArticle/index.ts +5 -0
  161. package/src/containers/RegistryArticle/RegistryArticle.tsx +53 -0
  162. package/src/containers/RegistryArticle/index.ts +5 -0
  163. package/src/containers/RegistrySettingsContainer/RegistrySettingsContainer.tsx +59 -0
  164. package/src/containers/RegistrySettingsContainer/index.ts +5 -0
  165. package/src/containers/index.ts +5 -1
  166. package/src/hooks/index.ts +12 -0
  167. package/src/hooks/useAutoTags.ts +51 -0
  168. package/src/hooks/useCatalogEntry.ts +25 -0
  169. package/src/hooks/useDisableConfirmation.ts +58 -0
  170. package/src/hooks/useInstalledVersionTag.ts +41 -0
  171. package/src/hooks/usePluginActions.ts +174 -0
  172. package/src/hooks/useRegistryPlugins.ts +29 -0
  173. package/src/hooks/useUpdateAvailable.ts +31 -0
  174. package/src/hooks/useVersionPicker.ts +112 -0
  175. package/src/index.ts +1 -1
  176. package/src/meta.ts +26 -1
  177. package/src/operations/index.ts +7 -0
  178. package/src/operations/open-plugin-registry.ts +21 -0
  179. package/src/plugin.ts +11 -0
  180. package/src/translations.ts +62 -15
  181. package/src/types.ts +33 -1
  182. package/dist/lib/browser/PluginArticle-N2ITBUGD.mjs +0 -29
  183. package/dist/lib/browser/PluginArticle-N2ITBUGD.mjs.map +0 -7
  184. package/dist/lib/browser/PluginRegistry-DKO4UEYH.mjs +0 -78
  185. package/dist/lib/browser/PluginRegistry-DKO4UEYH.mjs.map +0 -7
  186. package/dist/lib/browser/app-graph-builder-AHS5BI4Y.mjs +0 -167
  187. package/dist/lib/browser/app-graph-builder-AHS5BI4Y.mjs.map +0 -7
  188. package/dist/lib/browser/chunk-2YWVCU2W.mjs +0 -191
  189. package/dist/lib/browser/chunk-2YWVCU2W.mjs.map +0 -7
  190. package/dist/lib/browser/chunk-KMUDYITM.mjs +0 -21
  191. package/dist/lib/browser/chunk-KMUDYITM.mjs.map +0 -7
  192. package/dist/lib/browser/cli/index.mjs.map +0 -7
  193. package/dist/lib/browser/index.mjs +0 -65
  194. package/dist/lib/browser/index.mjs.map +0 -7
  195. package/dist/lib/browser/meta.json +0 -1
  196. package/dist/lib/browser/operation-resolver-PK4B2BZQ.mjs +0 -25
  197. package/dist/lib/browser/operation-resolver-PK4B2BZQ.mjs.map +0 -7
  198. package/dist/lib/browser/react-surface-ZBP7YXKO.mjs +0 -85
  199. package/dist/lib/browser/react-surface-ZBP7YXKO.mjs.map +0 -7
  200. package/dist/lib/node-esm/PluginArticle-3NEYDKN5.mjs +0 -30
  201. package/dist/lib/node-esm/PluginArticle-3NEYDKN5.mjs.map +0 -7
  202. package/dist/lib/node-esm/PluginRegistry-BYKWC732.mjs +0 -79
  203. package/dist/lib/node-esm/PluginRegistry-BYKWC732.mjs.map +0 -7
  204. package/dist/lib/node-esm/app-graph-builder-DBDL5WWF.mjs +0 -168
  205. package/dist/lib/node-esm/app-graph-builder-DBDL5WWF.mjs.map +0 -7
  206. package/dist/lib/node-esm/chunk-BOCFM775.mjs +0 -192
  207. package/dist/lib/node-esm/chunk-BOCFM775.mjs.map +0 -7
  208. package/dist/lib/node-esm/chunk-ENSNUFQK.mjs +0 -23
  209. package/dist/lib/node-esm/chunk-ENSNUFQK.mjs.map +0 -7
  210. package/dist/lib/node-esm/cli/index.mjs +0 -223
  211. package/dist/lib/node-esm/cli/index.mjs.map +0 -7
  212. package/dist/lib/node-esm/index.mjs +0 -66
  213. package/dist/lib/node-esm/index.mjs.map +0 -7
  214. package/dist/lib/node-esm/meta.json +0 -1
  215. package/dist/lib/node-esm/operation-resolver-CCHN4NBP.mjs +0 -26
  216. package/dist/lib/node-esm/operation-resolver-CCHN4NBP.mjs.map +0 -7
  217. package/dist/lib/node-esm/react-surface-FJHMLGZF.mjs +0 -86
  218. package/dist/lib/node-esm/react-surface-FJHMLGZF.mjs.map +0 -7
  219. package/dist/types/src/capabilities/app-graph-builder/app-graph-builder.d.ts.map +0 -1
  220. package/dist/types/src/capabilities/app-graph-builder/index.d.ts +0 -3
  221. package/dist/types/src/capabilities/app-graph-builder/index.d.ts.map +0 -1
  222. package/dist/types/src/capabilities/operation-resolver/index.d.ts +0 -3
  223. package/dist/types/src/capabilities/operation-resolver/index.d.ts.map +0 -1
  224. package/dist/types/src/capabilities/operation-resolver/operation-resolver.d.ts +0 -5
  225. package/dist/types/src/capabilities/operation-resolver/operation-resolver.d.ts.map +0 -1
  226. package/dist/types/src/capabilities/react-surface/index.d.ts +0 -3
  227. package/dist/types/src/capabilities/react-surface/index.d.ts.map +0 -1
  228. package/dist/types/src/capabilities/react-surface/react-surface.d.ts.map +0 -1
  229. package/dist/types/src/cli/commands/disable.d.ts.map +0 -1
  230. package/dist/types/src/cli/commands/enable.d.ts.map +0 -1
  231. package/dist/types/src/cli/commands/index.d.ts.map +0 -1
  232. package/dist/types/src/cli/commands/list.d.ts +0 -7
  233. package/dist/types/src/cli/commands/list.d.ts.map +0 -1
  234. package/dist/types/src/cli/commands/util.d.ts.map +0 -1
  235. package/dist/types/src/cli/index.d.ts +0 -3
  236. package/dist/types/src/cli/index.d.ts.map +0 -1
  237. package/dist/types/src/cli/plugin.d.ts.map +0 -1
  238. package/dist/types/src/cli/storage.d.ts.map +0 -1
  239. package/dist/types/src/containers/PluginRegistry/PluginRegistry.d.ts +0 -8
  240. package/dist/types/src/containers/PluginRegistry/PluginRegistry.d.ts.map +0 -1
  241. package/dist/types/src/containers/PluginRegistry/index.d.ts +0 -3
  242. package/dist/types/src/containers/PluginRegistry/index.d.ts.map +0 -1
  243. package/src/capabilities/app-graph-builder/app-graph-builder.ts +0 -137
  244. package/src/capabilities/app-graph-builder/index.ts +0 -7
  245. package/src/capabilities/operation-resolver/index.ts +0 -7
  246. package/src/capabilities/operation-resolver/operation-resolver.ts +0 -24
  247. package/src/capabilities/react-surface/index.ts +0 -7
  248. package/src/capabilities/react-surface/react-surface.tsx +0 -88
  249. package/src/cli/index.ts +0 -6
  250. package/src/containers/PluginRegistry/PluginRegistry.tsx +0 -86
  251. package/src/containers/PluginRegistry/index.ts +0 -7
  252. /package/dist/types/src/capabilities/{app-graph-builder/app-graph-builder.d.ts → app-graph-builder.d.ts} +0 -0
  253. /package/dist/types/src/capabilities/{react-surface/react-surface.d.ts → react-surface.d.ts} +0 -0
  254. /package/dist/types/src/{cli/commands → commands}/index.d.ts +0 -0
  255. /package/dist/types/src/{cli/commands → commands}/util.d.ts +0 -0
  256. /package/dist/types/src/{cli/storage.d.ts → storage.d.ts} +0 -0
  257. /package/src/{cli/commands → commands}/disable.ts +0 -0
  258. /package/src/{cli/commands → commands}/enable.ts +0 -0
  259. /package/src/{cli/commands → commands}/index.ts +0 -0
  260. /package/src/{cli/commands → commands}/list.ts +0 -0
  261. /package/src/{cli/commands → commands}/util.ts +0 -0
  262. /package/src/{cli/storage.ts → storage.ts} +0 -0
@@ -0,0 +1,58 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { useCallback } from 'react';
6
+
7
+ import { type PluginManager } from '@dxos/app-framework';
8
+ import { useOperationInvoker } from '@dxos/app-framework/ui';
9
+ import { LayoutOperation } from '@dxos/app-toolkit';
10
+
11
+ import { DISABLE_DEPENDENTS_DIALOG } from '#meta';
12
+
13
+ /**
14
+ * Returns `requestDisable(pluginId)` that gates `manager.disable` on a
15
+ * confirmation prompt when the target has currently-enabled dependents.
16
+ *
17
+ * `PluginManager.disable` cascades by default; this hook is the UX safeguard
18
+ * around that. The caller supplies a `dispatch` callback that performs the
19
+ * actual disable (wrapping it with observability or any other side effects).
20
+ * When there are no enabled dependents, dispatch runs immediately; otherwise
21
+ * we open the shared {@link DISABLE_DEPENDENTS_DIALOG} surface — its
22
+ * `onConfirm` runs the dispatch and closes the dialog.
23
+ *
24
+ * Display-name resolution for the dialog is delegated to the registered
25
+ * plugin set (`Plugin.Meta.name`). Per-plugin i18n translations only load
26
+ * when a plugin is activated, so `meta.name` is the only label always
27
+ * available regardless of enabled state.
28
+ */
29
+ export const useDisableConfirmation = (manager: PluginManager.PluginManager, dispatch: (id: string) => void) => {
30
+ const { invokePromise } = useOperationInvoker();
31
+
32
+ return useCallback(
33
+ (pluginId: string): void => {
34
+ const enabledDependents = manager.getDependents(pluginId, { transitive: true, enabledOnly: true });
35
+ if (enabledDependents.length === 0) {
36
+ dispatch(pluginId);
37
+ return;
38
+ }
39
+ const resolveName = (id: string): string =>
40
+ manager.getPlugins().find((plugin) => plugin.meta.id === id)?.meta.name ?? id;
41
+ void invokePromise(LayoutOperation.UpdateDialog, {
42
+ subject: DISABLE_DEPENDENTS_DIALOG,
43
+ state: true,
44
+ type: 'alert',
45
+ props: {
46
+ pluginId,
47
+ dependents: enabledDependents,
48
+ onResolvePluginName: resolveName,
49
+ onConfirm: () => {
50
+ dispatch(pluginId);
51
+ void invokePromise(LayoutOperation.UpdateDialog, { state: false });
52
+ },
53
+ },
54
+ });
55
+ },
56
+ [manager, dispatch, invokePromise],
57
+ );
58
+ };
@@ -0,0 +1,41 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { useCallback, useEffect, useState } from 'react';
6
+
7
+ import { type Plugin, UrlLoader } from '@dxos/app-framework';
8
+
9
+ /**
10
+ * Tracks the per-plugin installed version (sourced from `UrlLoader` localStorage)
11
+ * as React state so post-install/update re-renders are deterministic — the value
12
+ * lives in component state rather than being read inline on every render.
13
+ *
14
+ * - Initialised from localStorage at mount.
15
+ * - Auto-resyncs when the plugin manager's plugin list changes (catches external
16
+ * installs / removes triggered by the list view).
17
+ * - `syncInstalledVersion()` is the explicit handle install/update operations
18
+ * call inside their `Effect.ensuring` so the post-mutation render sees both
19
+ * the new version and the cleared `installing`/`updating` flag together.
20
+ */
21
+ export const useInstalledVersionTag = (
22
+ pluginId: string,
23
+ plugins: readonly Plugin.Plugin[],
24
+ ): {
25
+ installedVersionTag: string | undefined;
26
+ syncInstalledVersion: () => void;
27
+ } => {
28
+ const [installedVersionTag, setInstalledVersionTag] = useState<string | undefined>(() =>
29
+ UrlLoader.getInstalledVersion(pluginId),
30
+ );
31
+
32
+ const syncInstalledVersion = useCallback(() => {
33
+ setInstalledVersionTag(UrlLoader.getInstalledVersion(pluginId));
34
+ }, [pluginId]);
35
+
36
+ useEffect(() => {
37
+ syncInstalledVersion();
38
+ }, [plugins, syncInstalledVersion]);
39
+
40
+ return { installedVersionTag, syncInstalledVersion };
41
+ };
@@ -0,0 +1,174 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import * as Effect from 'effect/Effect';
6
+ import { useCallback, useState } from 'react';
7
+
8
+ import { type PluginManager, type Registry, UrlLoader } from '@dxos/app-framework';
9
+ import { useOperationInvoker } from '@dxos/app-framework/ui';
10
+ import { LayoutOperation } from '@dxos/app-toolkit';
11
+ import { useAppGraph } from '@dxos/app-toolkit/ui';
12
+ import { runAndForwardErrors } from '@dxos/effect';
13
+ import { useNode } from '@dxos/plugin-graph';
14
+
15
+ import { getPluginSpecPath } from '#meta';
16
+
17
+ import { useDisableConfirmation } from './useDisableConfirmation';
18
+
19
+ export type PluginActionsProps = {
20
+ manager: PluginManager.PluginManager;
21
+ pluginId: string;
22
+ isInstalled: boolean;
23
+ moduleUrl: string | undefined;
24
+ catalogEntry: Registry.Plugin | undefined;
25
+ pickerVersions: readonly Registry.PluginVersion[];
26
+ selectedVersionTag: string | undefined;
27
+ syncInstalledVersion: () => void;
28
+ };
29
+
30
+ export type PluginActions = {
31
+ installing: boolean;
32
+ updating: boolean;
33
+ handleEnableChange: (enabled: boolean) => void;
34
+ handleUninstall: () => void;
35
+ handleInstall: () => void;
36
+ handleUpdate: () => void;
37
+ handleInstallVersion: () => void;
38
+ handleOpenSpec?: () => void;
39
+ };
40
+
41
+ /**
42
+ * The user-facing actions the article exposes — enable/disable, uninstall,
43
+ * install (latest), update (to latest catalog version), and install-from-picker —
44
+ * plus the `installing`/`updating` flags they drive. Each handler ends with a
45
+ * `syncInstalledVersion()` call inside its `Effect.ensuring` so the cached
46
+ * installed version snaps to the freshly-written localStorage value in the same
47
+ * render that clears the in-flight flag.
48
+ *
49
+ * `handleOpenSpec` is included only when a `spec` child graph node exists for
50
+ * this plugin — contributed by whichever plugin can render MDL (today
51
+ * `plugin-code`). When that contributor isn't enabled, the action is absent
52
+ * and the detail view hides its "Open Specification" chip.
53
+ */
54
+ export const usePluginActions = ({
55
+ manager,
56
+ pluginId,
57
+ isInstalled,
58
+ moduleUrl,
59
+ catalogEntry,
60
+ pickerVersions,
61
+ selectedVersionTag,
62
+ syncInstalledVersion,
63
+ }: PluginActionsProps): PluginActions => {
64
+ const { invokePromise } = useOperationInvoker();
65
+ const { graph } = useAppGraph();
66
+
67
+ const [installing, setInstalling] = useState(false);
68
+ const [updating, setUpdating] = useState(false);
69
+ const requestDisable = useDisableConfirmation(manager, (id) => void runAndForwardErrors(manager.disable(id)));
70
+
71
+ const specPath = getPluginSpecPath(pluginId);
72
+ const hasSpecNode = !!useNode(graph, specPath);
73
+ const handleOpenSpec = useCallback(() => {
74
+ void invokePromise(LayoutOperation.Open, { subject: [specPath] });
75
+ }, [invokePromise, specPath]);
76
+
77
+ const handleEnableChange = useCallback(
78
+ (enabled: boolean) => {
79
+ if (enabled) {
80
+ void runAndForwardErrors(manager.enable(pluginId));
81
+ return;
82
+ }
83
+
84
+ requestDisable(pluginId);
85
+ },
86
+ [manager, pluginId, requestDisable],
87
+ );
88
+
89
+ const handleUninstall = useCallback(() => {
90
+ void runAndForwardErrors(manager.remove(pluginId));
91
+ }, [manager, pluginId]);
92
+
93
+ const handleInstall = useCallback(() => {
94
+ if (!moduleUrl) {
95
+ return;
96
+ }
97
+
98
+ setInstalling(true);
99
+ void Effect.gen(function* () {
100
+ const added = yield* manager.add(moduleUrl);
101
+ yield* manager.enable(added.meta.id);
102
+ if (catalogEntry) {
103
+ UrlLoader.setInstalledVersion(added.meta.id, catalogEntry.version);
104
+ }
105
+ }).pipe(
106
+ Effect.ensuring(
107
+ Effect.sync(() => {
108
+ setInstalling(false);
109
+ syncInstalledVersion();
110
+ }),
111
+ ),
112
+ runAndForwardErrors,
113
+ );
114
+ }, [manager, moduleUrl, catalogEntry, syncInstalledVersion]);
115
+
116
+ const handleUpdate = useCallback(() => {
117
+ if (!moduleUrl || !catalogEntry) {
118
+ return;
119
+ }
120
+
121
+ setUpdating(true);
122
+ void Effect.gen(function* () {
123
+ yield* manager.remove(pluginId);
124
+ const added = yield* manager.add(moduleUrl);
125
+ yield* manager.enable(added.meta.id);
126
+ UrlLoader.setInstalledVersion(added.meta.id, catalogEntry.version);
127
+ }).pipe(
128
+ Effect.ensuring(
129
+ Effect.sync(() => {
130
+ setUpdating(false);
131
+ syncInstalledVersion();
132
+ }),
133
+ ),
134
+ runAndForwardErrors,
135
+ );
136
+ }, [manager, pluginId, moduleUrl, catalogEntry, syncInstalledVersion]);
137
+
138
+ const handleInstallVersion = useCallback(() => {
139
+ const version = pickerVersions.find((candidate) => candidate.tag === selectedVersionTag);
140
+ if (!version || !version.moduleUrl) {
141
+ return;
142
+ }
143
+
144
+ setInstalling(true);
145
+ void Effect.gen(function* () {
146
+ // Remove existing version before installing a different one.
147
+ if (isInstalled) {
148
+ yield* manager.remove(pluginId);
149
+ }
150
+ const added = yield* manager.add(version.moduleUrl);
151
+ yield* manager.enable(added.meta.id);
152
+ UrlLoader.setInstalledVersion(added.meta.id, version.tag);
153
+ }).pipe(
154
+ Effect.ensuring(
155
+ Effect.sync(() => {
156
+ setInstalling(false);
157
+ syncInstalledVersion();
158
+ }),
159
+ ),
160
+ runAndForwardErrors,
161
+ );
162
+ }, [manager, pluginId, isInstalled, selectedVersionTag, pickerVersions, syncInstalledVersion]);
163
+
164
+ return {
165
+ installing,
166
+ updating,
167
+ handleEnableChange,
168
+ handleUninstall,
169
+ handleInstall,
170
+ handleUpdate,
171
+ handleInstallVersion,
172
+ handleOpenSpec: hasSpecNode ? handleOpenSpec : undefined,
173
+ };
174
+ };
@@ -0,0 +1,29 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { useAtomValue } from '@effect-atom/atom-react';
6
+
7
+ import { type Registry } from '@dxos/app-framework';
8
+ import { usePluginManager } from '@dxos/app-framework/ui';
9
+
10
+ /**
11
+ * Reads the registry plugin catalog from `manager.pluginRegistry.plugins`.
12
+ * The catalog is loaded once when the plugin manager is constructed (with a
13
+ * provider) so this hook is purely reactive — subscribers re-render as the
14
+ * `{entries, loading, error}` state settles.
15
+ */
16
+ export const useRegistryPlugins = (): Registry.PluginsState => {
17
+ const manager = usePluginManager();
18
+ return useAtomValue(manager.pluginRegistry.plugins);
19
+ };
20
+
21
+ /**
22
+ * Returns the live `Registry.Manager` from the plugin manager so callers can
23
+ * issue `listVersions` / `getPlugin` calls directly. The catalog state itself
24
+ * is best read through {@link useRegistryPlugins} above.
25
+ */
26
+ export const useRegistryPluginProvider = (): Registry.Manager => {
27
+ const manager = usePluginManager();
28
+ return manager.pluginRegistry;
29
+ };
@@ -0,0 +1,31 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { type Registry, UrlLoader } from '@dxos/app-framework';
6
+
7
+ /**
8
+ * Returns a set of plugin ids for which the registry has a newer version
9
+ * than what was recorded at install time.
10
+ *
11
+ * Only flags plugins where a version was recorded at install time AND it differs
12
+ * from the catalog's current version. Legacy installs (no version stored) are ignored.
13
+ *
14
+ * NOTE: this is intentionally NOT memoised. The installed-version side of the
15
+ * comparison lives in localStorage and is mutated by `manager.add` /
16
+ * `manager.remove` / `UrlLoader.setInstalledVersion` outside React's view, so a
17
+ * memo keyed on `entries` would happily return stale data for the rest of the
18
+ * session after an update completes (the bug that produced "Update button
19
+ * reappears after Updating…"). The loop is O(catalog size) sync reads — cheap
20
+ * enough to do every render.
21
+ */
22
+ export const useUpdateAvailableIds = (entries: readonly Registry.Plugin[]): ReadonlySet<string> => {
23
+ const ids = new Set<string>();
24
+ for (const entry of entries) {
25
+ const installedVersion = UrlLoader.getInstalledVersion(entry.id);
26
+ if (installedVersion !== undefined && installedVersion !== entry.version) {
27
+ ids.add(entry.id);
28
+ }
29
+ }
30
+ return ids;
31
+ };
@@ -0,0 +1,112 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import * as Effect from 'effect/Effect';
6
+ import { type Dispatch, type SetStateAction, useEffect, useMemo, useState } from 'react';
7
+
8
+ import { type Registry, UrlLoader } from '@dxos/app-framework';
9
+ import { runAndForwardErrors } from '@dxos/effect';
10
+
11
+ /**
12
+ * Owns the version picker's state machine: fetches the available versions list
13
+ * from the provider, prepends the installed version when the catalog stub omits
14
+ * it, and keeps the selection clamped to whatever's currently in the list (so
15
+ * an external update doesn't strand the trigger on a tag that's no longer
16
+ * available).
17
+ */
18
+ export const useVersionPicker = ({
19
+ provider,
20
+ pluginId,
21
+ repo,
22
+ moduleUrl,
23
+ installedVersionTag,
24
+ }: {
25
+ provider: Registry.Manager;
26
+ pluginId: string;
27
+ repo: string | undefined;
28
+ moduleUrl: string | undefined;
29
+ installedVersionTag: string | undefined;
30
+ }): {
31
+ pickerVersions: readonly Registry.PluginVersion[];
32
+ selectedVersionTag: string | undefined;
33
+ setSelectedVersionTag: Dispatch<SetStateAction<string | undefined>>;
34
+ } => {
35
+ const [versions, setVersions] = useState<readonly Registry.PluginVersion[]>([]);
36
+ const [selectedVersionTag, setSelectedVersionTag] = useState<string | undefined>();
37
+
38
+ // Load version list once the catalog entry's repo is known. Reset state
39
+ // when `repo` is absent or the fetch fails so a previous plugin's
40
+ // versions don't leak into the picker for the current plugin. The
41
+ // `cancelled` flag guards against a stale in-flight `listVersions`
42
+ // response overwriting the newer state after `repo`/`pluginId` changes.
43
+ useEffect(() => {
44
+ if (!repo) {
45
+ setVersions([]);
46
+ setSelectedVersionTag(undefined);
47
+ return;
48
+ }
49
+ // Clear stale picker state before fetching so the UI doesn't display the
50
+ // previous plugin's versions/selection while `listVersions` is pending.
51
+ setVersions([]);
52
+ setSelectedVersionTag(undefined);
53
+ let cancelled = false;
54
+ void provider.listVersions(repo).pipe(
55
+ Effect.match({
56
+ onSuccess: (vs) => {
57
+ if (cancelled) {
58
+ return;
59
+ }
60
+ setVersions(vs);
61
+ // Default selection: the currently installed version, or the latest.
62
+ const installedVersion = UrlLoader.getInstalledVersion(pluginId);
63
+ setSelectedVersionTag(installedVersion ?? vs[0]?.tag);
64
+ },
65
+ onFailure: () => {
66
+ if (cancelled) {
67
+ return;
68
+ }
69
+ setVersions([]);
70
+ setSelectedVersionTag(undefined);
71
+ },
72
+ }),
73
+ runAndForwardErrors,
74
+ );
75
+ return () => {
76
+ cancelled = true;
77
+ };
78
+ }, [provider, repo, pluginId]);
79
+
80
+ // Make sure the picker always lists the installed version, even if the catalog
81
+ // hasn't surfaced it (the current `listVersions` stub only returns latest).
82
+ const pickerVersions = useMemo<readonly Registry.PluginVersion[]>(() => {
83
+ if (!installedVersionTag) {
84
+ return versions;
85
+ }
86
+ if (versions.some((entry) => entry.tag === installedVersionTag)) {
87
+ return versions;
88
+ }
89
+ const installedEntry: Registry.PluginVersion = {
90
+ tag: installedVersionTag,
91
+ // Picker won't be re-installing this entry unless the user selects + clicks Install,
92
+ // and the catalog moduleUrl is the closest stand-in we have.
93
+ moduleUrl: moduleUrl ?? '',
94
+ };
95
+ return [installedEntry, ...versions];
96
+ }, [versions, installedVersionTag, moduleUrl]);
97
+
98
+ // Keep `selectedVersionTag` in sync when picker contents change (e.g. the user
99
+ // updated the plugin from the list view — installed version moved, picker
100
+ // shrunk, prior selection is no longer in the list).
101
+ useEffect(() => {
102
+ if (pickerVersions.length === 0) {
103
+ return;
104
+ }
105
+ if (selectedVersionTag && pickerVersions.some((entry) => entry.tag === selectedVersionTag)) {
106
+ return;
107
+ }
108
+ setSelectedVersionTag(installedVersionTag ?? pickerVersions[0]?.tag);
109
+ }, [pickerVersions, selectedVersionTag, installedVersionTag]);
110
+
111
+ return { pickerVersions, selectedVersionTag, setSelectedVersionTag };
112
+ };
package/src/index.ts CHANGED
@@ -3,4 +3,4 @@
3
3
  //
4
4
 
5
5
  export * from './meta';
6
- export * from './RegistryPlugin';
6
+ export * from './storage';
package/src/meta.ts CHANGED
@@ -3,19 +3,44 @@
3
3
  //
4
4
 
5
5
  import { type Plugin } from '@dxos/app-framework';
6
+ import { pinnedWorkspaceId } from '@dxos/app-toolkit';
6
7
  import { trim } from '@dxos/util';
7
8
 
8
- export const REGISTRY_ID = '!dxos:plugin-registry';
9
+ export const REGISTRY_ID = pinnedWorkspaceId('dxos:plugin-registry');
9
10
  export const REGISTRY_KEY = 'plugin-registry';
10
11
 
12
+ // TODO(wittjosiah): Should this be a special separator or use the standard path separator?
13
+ const CATEGORY_SEPARATOR = '>';
14
+
15
+ /** Build a registry category node ID. */
16
+ export const registryCategoryId = (category: string): string => `${REGISTRY_KEY}${CATEGORY_SEPARATOR}${category}`;
17
+
11
18
  /** Qualified graph path to a specific plugin node. */
12
19
  export const getPluginPath = (pluginId: string): string => `root/${REGISTRY_ID}/${pluginId}`;
13
20
 
21
+ /**
22
+ * Qualified graph path to the bundled MDL spec child node for a plugin.
23
+ *
24
+ * The child is contributed by whichever plugin can render MDL (today:
25
+ * `plugin-code`). plugin-registry only knows the path convention so it can:
26
+ * - dispatch `LayoutOperation.Open` to open the spec viewer, and
27
+ * - probe the app graph for the child's existence to gate the "View
28
+ * specification" button. If no plugin contributes a renderer for the
29
+ * child, the node is absent and the button stays hidden.
30
+ */
31
+ export const getPluginSpecPath = (pluginId: string): string => `${getPluginPath(pluginId)}/spec`;
32
+
14
33
  export const meta: Plugin.Meta = {
15
34
  id: 'org.dxos.plugin.registry',
16
35
  name: 'Plugins',
36
+ author: 'DXOS',
17
37
  description: trim`
18
38
  Plugin management system for discovering, installing, and configuring workspace extensions.
19
39
  Browse available plugins and customize your workspace capabilities.
20
40
  `,
41
+ icon: 'ph--squares-four--regular',
42
+ tags: ['system'],
21
43
  };
44
+
45
+ /** Cascade-disable confirmation dialog surface id. */
46
+ export const DISABLE_DEPENDENTS_DIALOG = `${meta.id}.disable-dependents-dialog`;
@@ -0,0 +1,7 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { OperationHandlerSet } from '@dxos/compute';
6
+
7
+ export const RegistryOperationHandlerSet = OperationHandlerSet.lazy(() => import('./open-plugin-registry'));
@@ -0,0 +1,21 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Effect from 'effect/Effect';
6
+
7
+ import { LayoutOperation, SettingsOperation, getSpacePath } from '@dxos/app-toolkit';
8
+ import { Operation } from '@dxos/compute';
9
+
10
+ import { REGISTRY_ID } from '#meta';
11
+
12
+ const handler: Operation.WithHandler<typeof SettingsOperation.OpenPluginRegistry> =
13
+ SettingsOperation.OpenPluginRegistry.pipe(
14
+ Operation.withHandler(
15
+ Effect.fnUntraced(function* () {
16
+ yield* Operation.invoke(LayoutOperation.SwitchWorkspace, { subject: getSpacePath(REGISTRY_ID) });
17
+ }),
18
+ ),
19
+ );
20
+
21
+ export default handler;
package/src/plugin.ts ADDED
@@ -0,0 +1,11 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { Plugin } from '@dxos/app-framework';
6
+
7
+ import { meta } from './meta';
8
+
9
+ export const RegistryPlugin = Plugin.lazy(meta, () => import('#plugin'));
10
+
11
+ export { RegistryOperationHandlerSet } from './operations';
@@ -4,26 +4,73 @@
4
4
 
5
5
  import { type Resource } from '@dxos/react-ui';
6
6
 
7
- import { meta } from './meta';
7
+ import { meta } from '#meta';
8
8
 
9
9
  export const translations = [
10
10
  {
11
11
  'en-US': {
12
12
  [meta.id]: {
13
- 'plugin name': 'Registry',
14
- 'details label': 'Details',
15
- 'home page label': 'Website',
16
- 'source label': 'Source',
17
- 'plugin registry label': 'Plugin Registry',
18
- 'all plugins label': 'All',
19
- 'installed plugins label': 'Enabled',
20
- 'recommended plugins label': 'Recommended',
21
- 'labs plugins label': 'Labs',
22
- 'community plugins label': 'Community',
23
- 'load by url label': 'Load by URL (soon)',
24
- 'coming soon label': 'Coming soon!',
25
- 'open plugin registry label': 'Open plugin registry',
26
- 'settings label': 'Settings',
13
+ 'plugin.name': 'Registry',
14
+ 'details.label': 'Details',
15
+ 'home-page.label': 'Website',
16
+ 'source.label': 'Source',
17
+ 'resources.label': 'Resources',
18
+ 'plugin-registry.label': 'Plugins',
19
+ 'plugin-settings.label': 'Plugin Settings',
20
+ 'bundled-plugins.label': 'Bundled',
21
+ 'installed-plugins.label': 'Enabled',
22
+ 'recommended-plugins.label': 'Recommended',
23
+ 'labs-plugins.label': 'Labs',
24
+ 'registry-plugins.label': 'Registry',
25
+ 'load-by-url.label': 'Load from URL',
26
+ 'load-by-url-dialog.title': 'Load Plugin from URL',
27
+ 'plugin-url.label': 'Plugin URL',
28
+ 'load-plugin.label': 'Load Plugin',
29
+ 'loading.label': 'Loading...',
30
+ 'filter.label': 'Filter plugins',
31
+ 'filter.placeholder': 'Filter…',
32
+ 'registry.loading.label': 'Loading registry plugins…',
33
+ 'registry.error.label': 'Failed to load registry plugins: {{message}}',
34
+ 'registry.empty.label': 'No registry plugins available.',
35
+ 'dev-plugin.section.title': 'Dev plugin',
36
+ 'dev-plugin.description':
37
+ 'Load a plugin from a local Vite dev server (the `composerPlugin` defaults to port 3967). Composer reattaches it on every reload, surviving HMR — useful when iterating on a community plugin against a live registry version.',
38
+ 'dev-plugin.url.label': 'Manifest URL',
39
+ 'dev-plugin.url.description':
40
+ 'Defaults to the local dev server. Edit if your dev server runs on a different port.',
41
+ 'dev-plugin.toggle.label': 'Dev plugin',
42
+ 'dev-plugin.toggle.description':
43
+ 'Stays on across reloads. If the dev server is offline at boot, a warning is logged and the next reload retries.',
44
+ 'dev-plugin.enable.label': 'Enable',
45
+ 'dev-plugin.disable.label': 'Disable',
46
+ 'dev-plugin.busy.label': 'Loading…',
47
+ 'dev-plugin.not-loaded.message':
48
+ 'Dev plugin is enabled but not currently loaded. Check that the dev server is running, then reload.',
49
+ 'coming-soon.label': 'Coming soon!',
50
+ 'open-plugin-registry.label': 'Open plugin registry',
51
+ 'install.label': 'Install',
52
+ 'installing.label': 'Installing…',
53
+ 'uninstall.label': 'Uninstall',
54
+ 'update.label': 'Update',
55
+ 'updating.label': 'Updating…',
56
+ 'versions.label': 'Versions',
57
+ 'installed.label': 'installed',
58
+ 'install-version.label': 'Install version',
59
+ 'failure-badge.label': 'Plugin failure details',
60
+ 'failure-title.label': 'Plugin {{phase}} failed: {{reason}}',
61
+ 'failure-phase-load.label': 'load',
62
+ 'failure-phase-activation.label': 'activation',
63
+ 'failure-reason-timeout.label': 'timed out',
64
+ 'failure-reason-error.label': 'error',
65
+ 'dependencies.label': 'Dependencies',
66
+ 'dependents.label': 'Required by',
67
+ 'spec.label': 'Specification',
68
+ 'open-spec.label': 'Open Specification',
69
+ 'disable-dependents-dialog.title': 'Disable dependent plugins?',
70
+ 'disable-dependents-dialog.description':
71
+ 'Disabling {{plugin}} will also disable the following plugins that depend on it:',
72
+ 'disable-dependents-dialog.confirm.label': 'Disable',
73
+ 'cancel.label': 'Cancel',
27
74
  },
28
75
  },
29
76
  },