@dxos/plugin-registry 0.8.4-main.ae835ea → 0.8.4-main.bc2380dfbc

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 (219) 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/neutral/RegistryPlugin.node.mjs +153 -0
  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-A6F6X6M4.mjs +31 -0
  9. package/dist/lib/neutral/chunk-A6F6X6M4.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 +23 -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 +67 -0
  20. package/dist/lib/neutral/translations.mjs.map +7 -0
  21. package/dist/types/src/RegistryPlugin.d.ts +3 -1
  22. package/dist/types/src/RegistryPlugin.d.ts.map +1 -1
  23. package/dist/types/src/RegistryPlugin.node.d.ts +4 -0
  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 +4 -2
  30. package/dist/types/src/capabilities/app-graph-builder.d.ts.map +1 -1
  31. package/dist/types/src/capabilities/dev-plugin-loader.d.ts +11 -0
  32. package/dist/types/src/capabilities/dev-plugin-loader.d.ts.map +1 -0
  33. package/dist/types/src/capabilities/index.d.ts +15 -3
  34. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  35. package/dist/types/src/capabilities/operation-handler.d.ts +6 -0
  36. package/dist/types/src/capabilities/operation-handler.d.ts.map +1 -0
  37. package/dist/types/src/capabilities/react-surface.d.ts +3 -2
  38. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -1
  39. package/dist/types/src/capabilities/settings.d.ts +14 -0
  40. package/dist/types/src/capabilities/settings.d.ts.map +1 -0
  41. package/dist/types/src/commands/disable.d.ts +11 -0
  42. package/dist/types/src/commands/disable.d.ts.map +1 -0
  43. package/dist/types/src/commands/enable.d.ts +11 -0
  44. package/dist/types/src/commands/enable.d.ts.map +1 -0
  45. package/dist/types/src/commands/index.d.ts +3 -0
  46. package/dist/types/src/commands/index.d.ts.map +1 -0
  47. package/dist/types/src/commands/list.d.ts +7 -0
  48. package/dist/types/src/commands/list.d.ts.map +1 -0
  49. package/dist/types/src/commands/util.d.ts +11 -0
  50. package/dist/types/src/commands/util.d.ts.map +1 -0
  51. package/dist/types/src/components/DisableDependentsAlert/DisableDependentsAlert.d.ts +24 -0
  52. package/dist/types/src/components/DisableDependentsAlert/DisableDependentsAlert.d.ts.map +1 -0
  53. package/dist/types/src/components/DisableDependentsAlert/index.d.ts +2 -0
  54. package/dist/types/src/components/DisableDependentsAlert/index.d.ts.map +1 -0
  55. package/dist/types/src/components/PluginDetail/PluginDetail.d.ts +72 -0
  56. package/dist/types/src/components/PluginDetail/PluginDetail.d.ts.map +1 -0
  57. package/dist/types/src/components/PluginDetail/PluginDetail.stories.d.ts +74 -0
  58. package/dist/types/src/components/PluginDetail/PluginDetail.stories.d.ts.map +1 -0
  59. package/dist/types/src/components/PluginDetail/index.d.ts +2 -0
  60. package/dist/types/src/components/PluginDetail/index.d.ts.map +1 -0
  61. package/dist/types/src/components/PluginFailureBadge/PluginFailureBadge.d.ts +20 -0
  62. package/dist/types/src/components/PluginFailureBadge/PluginFailureBadge.d.ts.map +1 -0
  63. package/dist/types/src/components/PluginFailureBadge/index.d.ts +2 -0
  64. package/dist/types/src/components/PluginFailureBadge/index.d.ts.map +1 -0
  65. package/dist/types/src/components/PluginList/PluginItem.d.ts +40 -0
  66. package/dist/types/src/components/PluginList/PluginItem.d.ts.map +1 -0
  67. package/dist/types/src/components/PluginList/PluginList.d.ts +23 -0
  68. package/dist/types/src/components/PluginList/PluginList.d.ts.map +1 -0
  69. package/dist/types/src/components/PluginList/PluginList.stories.d.ts +73 -0
  70. package/dist/types/src/components/PluginList/PluginList.stories.d.ts.map +1 -0
  71. package/dist/types/src/components/PluginList/index.d.ts +2 -0
  72. package/dist/types/src/components/PluginList/index.d.ts.map +1 -0
  73. package/dist/types/src/components/RegistrySettings/RegistrySettings.d.ts +18 -0
  74. package/dist/types/src/components/RegistrySettings/RegistrySettings.d.ts.map +1 -0
  75. package/dist/types/src/components/RegistrySettings/index.d.ts +2 -0
  76. package/dist/types/src/components/RegistrySettings/index.d.ts.map +1 -0
  77. package/dist/types/src/components/index.d.ts +3 -1
  78. package/dist/types/src/components/index.d.ts.map +1 -1
  79. package/dist/types/src/containers/BaseRegistryArticle/BaseRegistryArticle.d.ts +20 -0
  80. package/dist/types/src/containers/BaseRegistryArticle/BaseRegistryArticle.d.ts.map +1 -0
  81. package/dist/types/src/containers/BaseRegistryArticle/index.d.ts +2 -0
  82. package/dist/types/src/containers/BaseRegistryArticle/index.d.ts.map +1 -0
  83. package/dist/types/src/containers/LoadPluginDialog/LoadPluginDialog.d.ts +4 -0
  84. package/dist/types/src/containers/LoadPluginDialog/LoadPluginDialog.d.ts.map +1 -0
  85. package/dist/types/src/containers/LoadPluginDialog/index.d.ts +2 -0
  86. package/dist/types/src/containers/LoadPluginDialog/index.d.ts.map +1 -0
  87. package/dist/types/src/containers/PluginArticle/PluginArticle.d.ts +7 -0
  88. package/dist/types/src/containers/PluginArticle/PluginArticle.d.ts.map +1 -0
  89. package/dist/types/src/containers/PluginArticle/index.d.ts +2 -0
  90. package/dist/types/src/containers/PluginArticle/index.d.ts.map +1 -0
  91. package/dist/types/src/containers/PublicRegistryArticle/PublicRegistryArticle.d.ts +8 -0
  92. package/dist/types/src/containers/PublicRegistryArticle/PublicRegistryArticle.d.ts.map +1 -0
  93. package/dist/types/src/containers/PublicRegistryArticle/index.d.ts +2 -0
  94. package/dist/types/src/containers/PublicRegistryArticle/index.d.ts.map +1 -0
  95. package/dist/types/src/containers/RegistryArticle/RegistryArticle.d.ts +14 -0
  96. package/dist/types/src/containers/RegistryArticle/RegistryArticle.d.ts.map +1 -0
  97. package/dist/types/src/containers/RegistryArticle/index.d.ts +2 -0
  98. package/dist/types/src/containers/RegistryArticle/index.d.ts.map +1 -0
  99. package/dist/types/src/containers/RegistrySettingsContainer/RegistrySettingsContainer.d.ts +13 -0
  100. package/dist/types/src/containers/RegistrySettingsContainer/RegistrySettingsContainer.d.ts.map +1 -0
  101. package/dist/types/src/containers/RegistrySettingsContainer/index.d.ts +2 -0
  102. package/dist/types/src/containers/RegistrySettingsContainer/index.d.ts.map +1 -0
  103. package/dist/types/src/containers/index.d.ts +8 -0
  104. package/dist/types/src/containers/index.d.ts.map +1 -0
  105. package/dist/types/src/hooks/index.d.ts +5 -0
  106. package/dist/types/src/hooks/index.d.ts.map +1 -0
  107. package/dist/types/src/hooks/use-auto-tags.d.ts +20 -0
  108. package/dist/types/src/hooks/use-auto-tags.d.ts.map +1 -0
  109. package/dist/types/src/hooks/use-disable-confirmation.d.ts +19 -0
  110. package/dist/types/src/hooks/use-disable-confirmation.d.ts.map +1 -0
  111. package/dist/types/src/hooks/use-registry-plugins.d.ts +15 -0
  112. package/dist/types/src/hooks/use-registry-plugins.d.ts.map +1 -0
  113. package/dist/types/src/hooks/use-update-available.d.ts +18 -0
  114. package/dist/types/src/hooks/use-update-available.d.ts.map +1 -0
  115. package/dist/types/src/index.d.ts +1 -1
  116. package/dist/types/src/index.d.ts.map +1 -1
  117. package/dist/types/src/meta.d.ts +9 -3
  118. package/dist/types/src/meta.d.ts.map +1 -1
  119. package/dist/types/src/operations/index.d.ts +3 -0
  120. package/dist/types/src/operations/index.d.ts.map +1 -0
  121. package/dist/types/src/operations/open-plugin-registry.d.ts +5 -0
  122. package/dist/types/src/operations/open-plugin-registry.d.ts.map +1 -0
  123. package/dist/types/src/plugin.d.ts +4 -0
  124. package/dist/types/src/plugin.d.ts.map +1 -0
  125. package/dist/types/src/storage.d.ts +18 -0
  126. package/dist/types/src/storage.d.ts.map +1 -0
  127. package/dist/types/src/translations.d.ts +54 -16
  128. package/dist/types/src/translations.d.ts.map +1 -1
  129. package/dist/types/src/types.d.ts +30 -1
  130. package/dist/types/src/types.d.ts.map +1 -1
  131. package/dist/types/tsconfig.tsbuildinfo +1 -1
  132. package/package.json +76 -35
  133. package/src/RegistryPlugin.node.ts +19 -0
  134. package/src/RegistryPlugin.test.ts +24 -0
  135. package/src/RegistryPlugin.tsx +19 -25
  136. package/src/RegistryPlugin.workerd.ts +11 -0
  137. package/src/capabilities/app-graph-builder.ts +180 -136
  138. package/src/capabilities/dev-plugin-loader.ts +39 -0
  139. package/src/capabilities/index.ts +10 -4
  140. package/src/capabilities/operation-handler.ts +16 -0
  141. package/src/capabilities/react-surface.tsx +133 -73
  142. package/src/capabilities/settings.ts +34 -0
  143. package/src/commands/disable.ts +47 -0
  144. package/src/commands/enable.ts +44 -0
  145. package/src/commands/index.ts +14 -0
  146. package/src/commands/list.ts +41 -0
  147. package/src/commands/util.ts +24 -0
  148. package/src/components/DisableDependentsAlert/DisableDependentsAlert.tsx +69 -0
  149. package/src/components/DisableDependentsAlert/index.ts +5 -0
  150. package/src/components/PluginDetail/PluginDetail.stories.tsx +44 -0
  151. package/src/components/PluginDetail/PluginDetail.tsx +272 -0
  152. package/src/components/PluginDetail/index.ts +5 -0
  153. package/src/components/PluginFailureBadge/PluginFailureBadge.tsx +68 -0
  154. package/src/components/PluginFailureBadge/index.ts +5 -0
  155. package/src/components/PluginList/PluginItem.tsx +226 -0
  156. package/src/components/PluginList/PluginList.stories.tsx +87 -0
  157. package/src/components/PluginList/PluginList.tsx +52 -0
  158. package/src/components/PluginList/index.ts +5 -0
  159. package/src/components/RegistrySettings/RegistrySettings.tsx +120 -0
  160. package/src/components/RegistrySettings/index.ts +5 -0
  161. package/src/components/index.ts +3 -1
  162. package/src/containers/BaseRegistryArticle/BaseRegistryArticle.tsx +178 -0
  163. package/src/containers/BaseRegistryArticle/index.ts +5 -0
  164. package/src/containers/LoadPluginDialog/LoadPluginDialog.tsx +87 -0
  165. package/src/containers/LoadPluginDialog/index.ts +5 -0
  166. package/src/containers/PluginArticle/PluginArticle.tsx +378 -0
  167. package/src/containers/PluginArticle/index.ts +5 -0
  168. package/src/containers/PublicRegistryArticle/PublicRegistryArticle.tsx +192 -0
  169. package/src/containers/PublicRegistryArticle/index.ts +5 -0
  170. package/src/containers/RegistryArticle/RegistryArticle.tsx +53 -0
  171. package/src/containers/RegistryArticle/index.ts +5 -0
  172. package/src/containers/RegistrySettingsContainer/RegistrySettingsContainer.tsx +59 -0
  173. package/src/containers/RegistrySettingsContainer/index.ts +5 -0
  174. package/src/containers/index.ts +12 -0
  175. package/src/hooks/index.ts +8 -0
  176. package/src/hooks/use-auto-tags.ts +51 -0
  177. package/src/hooks/use-disable-confirmation.ts +58 -0
  178. package/src/hooks/use-registry-plugins.ts +29 -0
  179. package/src/hooks/use-update-available.ts +31 -0
  180. package/src/index.ts +1 -1
  181. package/src/meta.ts +19 -7
  182. package/src/operations/index.ts +7 -0
  183. package/src/operations/open-plugin-registry.ts +21 -0
  184. package/src/plugin.ts +11 -0
  185. package/src/storage.ts +50 -0
  186. package/src/translations.ts +59 -15
  187. package/src/types.ts +33 -1
  188. package/dist/lib/browser/app-graph-builder-AOIN4N2Q.mjs +0 -162
  189. package/dist/lib/browser/app-graph-builder-AOIN4N2Q.mjs.map +0 -7
  190. package/dist/lib/browser/chunk-XPEFEID2.mjs +0 -19
  191. package/dist/lib/browser/chunk-XPEFEID2.mjs.map +0 -7
  192. package/dist/lib/browser/index.mjs +0 -69
  193. package/dist/lib/browser/index.mjs.map +0 -7
  194. package/dist/lib/browser/intent-resolver-LKNUYBTN.mjs +0 -23
  195. package/dist/lib/browser/intent-resolver-LKNUYBTN.mjs.map +0 -7
  196. package/dist/lib/browser/meta.json +0 -1
  197. package/dist/lib/browser/react-surface-KP3NX4UD.mjs +0 -357
  198. package/dist/lib/browser/react-surface-KP3NX4UD.mjs.map +0 -7
  199. package/dist/types/src/capabilities/intent-resolver.d.ts +0 -4
  200. package/dist/types/src/capabilities/intent-resolver.d.ts.map +0 -1
  201. package/dist/types/src/components/PluginDetail.d.ts +0 -9
  202. package/dist/types/src/components/PluginDetail.d.ts.map +0 -1
  203. package/dist/types/src/components/PluginDetail.stories.d.ts +0 -32
  204. package/dist/types/src/components/PluginDetail.stories.d.ts.map +0 -1
  205. package/dist/types/src/components/PluginItem.d.ts +0 -13
  206. package/dist/types/src/components/PluginItem.d.ts.map +0 -1
  207. package/dist/types/src/components/PluginList.d.ts +0 -8
  208. package/dist/types/src/components/PluginList.d.ts.map +0 -1
  209. package/dist/types/src/components/PluginList.stories.d.ts +0 -34
  210. package/dist/types/src/components/PluginList.stories.d.ts.map +0 -1
  211. package/dist/types/src/components/RegistryContainer.d.ts +0 -7
  212. package/dist/types/src/components/RegistryContainer.d.ts.map +0 -1
  213. package/src/capabilities/intent-resolver.ts +0 -27
  214. package/src/components/PluginDetail.stories.tsx +0 -45
  215. package/src/components/PluginDetail.tsx +0 -82
  216. package/src/components/PluginItem.tsx +0 -120
  217. package/src/components/PluginList.stories.tsx +0 -80
  218. package/src/components/PluginList.tsx +0 -24
  219. package/src/components/RegistryContainer.tsx +0 -84
@@ -0,0 +1,378 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { useAtomValue } from '@effect-atom/atom-react';
6
+ import * as Effect from 'effect/Effect';
7
+ import React, { type Dispatch, type SetStateAction, useCallback, useEffect, useMemo, useState } from 'react';
8
+
9
+ import { type Plugin, type PluginManager, type Registry, UrlLoader } from '@dxos/app-framework';
10
+ import { useOperationInvoker, usePluginManager } from '@dxos/app-framework/ui';
11
+ import { LayoutOperation } from '@dxos/app-toolkit';
12
+ import { runAndForwardErrors } from '@dxos/effect';
13
+
14
+ import { PluginDetail } from '#components';
15
+ import { getPluginPath } from '#meta';
16
+
17
+ import { useDisableConfirmation, useRegistryPluginProvider, useRegistryPlugins, useRemotePluginIds } from '../../hooks';
18
+
19
+ // TODO(burdon): Convert to ECHO type.
20
+ export type PluginArticleProps = { subject: Plugin.Plugin };
21
+
22
+ export const PluginArticle = ({ subject: plugin }: PluginArticleProps) => {
23
+ const pluginId = plugin.meta.id;
24
+ const manager = usePluginManager();
25
+ const plugins = useAtomValue(manager.plugins);
26
+ const remotePluginIds = useRemotePluginIds();
27
+ const provider = useRegistryPluginProvider();
28
+ const { invokePromise } = useOperationInvoker();
29
+
30
+ const { catalogEntry, moduleUrl, repo } = useCatalogEntry(pluginId);
31
+ const { installedVersionTag, syncInstalledVersion } = useInstalledVersionTag(pluginId, plugins);
32
+ const { pickerVersions, selectedVersionTag, setSelectedVersionTag } = useVersionPicker({
33
+ provider,
34
+ pluginId,
35
+ repo,
36
+ moduleUrl,
37
+ installedVersionTag,
38
+ });
39
+
40
+ const enabled = manager.getEnabled().includes(pluginId);
41
+ const failed = useAtomValue(manager.failed);
42
+ const failure = useMemo(() => failed.find((entry) => entry.id === pluginId), [failed, pluginId]);
43
+ const isInstalled = useMemo(() => plugins.some((candidate) => candidate.meta.id === pluginId), [plugins, pluginId]);
44
+ const isCore = manager.getCore().includes(pluginId);
45
+ const canUninstall = isInstalled && !isCore && remotePluginIds.has(pluginId);
46
+ const hasUpdate =
47
+ isInstalled && !!catalogEntry && !!installedVersionTag && installedVersionTag !== catalogEntry.version;
48
+
49
+ // Recompute graph slices whenever the plugin list changes, so installs /
50
+ // removals through other surfaces (or this article's own actions) keep the
51
+ // detail view in sync.
52
+ const dependencies = useMemo(
53
+ () => manager.getDependencies(pluginId, { transitive: false }),
54
+ [manager, pluginId, plugins],
55
+ );
56
+ const dependents = useMemo(
57
+ () => manager.getDependents(pluginId, { transitive: false }),
58
+ [manager, pluginId, plugins],
59
+ );
60
+
61
+ // Resolves a plugin id to its display name. A plugin's translations are
62
+ // only loaded when it is activated, so the always-available `meta.name`
63
+ // from the registered plugin set is the right source for chip labels.
64
+ const handleResolvePluginName = useCallback(
65
+ (id: string): string => plugins.find((candidate) => candidate.meta.id === id)?.meta.name ?? id,
66
+ [plugins],
67
+ );
68
+
69
+ const handleNavigateToPlugin = useCallback(
70
+ (targetId: string) => {
71
+ void invokePromise(LayoutOperation.Open, {
72
+ subject: [getPluginPath(targetId)],
73
+ });
74
+ },
75
+ [invokePromise],
76
+ );
77
+
78
+ const actions = usePluginActions({
79
+ manager,
80
+ pluginId,
81
+ isInstalled,
82
+ moduleUrl,
83
+ catalogEntry,
84
+ pickerVersions,
85
+ selectedVersionTag,
86
+ syncInstalledVersion,
87
+ });
88
+
89
+ return (
90
+ <PluginDetail
91
+ plugin={plugin}
92
+ enabled={enabled}
93
+ installing={actions.installing}
94
+ updating={actions.updating}
95
+ hasUpdate={hasUpdate}
96
+ installedVersionTag={installedVersionTag}
97
+ selectedVersionTag={selectedVersionTag}
98
+ versions={pickerVersions}
99
+ dependencies={dependencies}
100
+ dependents={dependents}
101
+ failure={failure}
102
+ onEnabledChange={actions.handleEnableChange}
103
+ onInstall={!isInstalled && moduleUrl ? actions.handleInstall : undefined}
104
+ onInstallVersion={pickerVersions.length > 0 ? actions.handleInstallVersion : undefined}
105
+ onNavigateToPlugin={handleNavigateToPlugin}
106
+ onResolvePluginName={handleResolvePluginName}
107
+ onUninstall={canUninstall ? actions.handleUninstall : undefined}
108
+ onUpdate={hasUpdate ? actions.handleUpdate : undefined}
109
+ onVersionChange={setSelectedVersionTag}
110
+ />
111
+ );
112
+ };
113
+
114
+ //
115
+ // Hooks
116
+ //
117
+ // Each of the hooks below isolates a self-contained slice of the article's
118
+ // state machine. They're declared at module scope (rather than inline in
119
+ // `PluginArticle`) to keep the component readable as a thin wiring layer over
120
+ // `PluginDetail`.
121
+ //
122
+
123
+ /**
124
+ * Resolves the registry catalog entry for a plugin by id, plus the two fields
125
+ * (`moduleUrl`, `repo`) the rest of the article cares about.
126
+ */
127
+ const useCatalogEntry = (
128
+ pluginId: string,
129
+ ): {
130
+ catalogEntry: Registry.Plugin | undefined;
131
+ moduleUrl: string | undefined;
132
+ repo: string | undefined;
133
+ } => {
134
+ const { entries } = useRegistryPlugins();
135
+ const catalogEntry = useMemo(() => entries.find((entry) => entry.id === pluginId), [entries, pluginId]);
136
+ return { catalogEntry, moduleUrl: catalogEntry?.moduleUrl, repo: catalogEntry?.repo };
137
+ };
138
+
139
+ /**
140
+ * Tracks the per-plugin installed version (sourced from `UrlLoader` localStorage)
141
+ * as React state so post-install/update re-renders are deterministic — the value
142
+ * lives in component state rather than being read inline on every render.
143
+ *
144
+ * - Initialised from localStorage at mount.
145
+ * - Auto-resyncs when the plugin manager's plugin list changes (catches external
146
+ * installs / removes triggered by the list view).
147
+ * - `syncInstalledVersion()` is the explicit handle install/update operations
148
+ * call inside their `Effect.ensuring` so the post-mutation render sees both
149
+ * the new version and the cleared `installing`/`updating` flag together.
150
+ */
151
+ const useInstalledVersionTag = (
152
+ pluginId: string,
153
+ plugins: readonly Plugin.Plugin[],
154
+ ): {
155
+ installedVersionTag: string | undefined;
156
+ syncInstalledVersion: () => void;
157
+ } => {
158
+ const [installedVersionTag, setInstalledVersionTag] = useState<string | undefined>(() =>
159
+ UrlLoader.getInstalledVersion(pluginId),
160
+ );
161
+
162
+ const syncInstalledVersion = useCallback(() => {
163
+ setInstalledVersionTag(UrlLoader.getInstalledVersion(pluginId));
164
+ }, [pluginId]);
165
+
166
+ useEffect(() => {
167
+ syncInstalledVersion();
168
+ }, [plugins, syncInstalledVersion]);
169
+
170
+ return { installedVersionTag, syncInstalledVersion };
171
+ };
172
+
173
+ /**
174
+ * Owns the version picker's state machine: fetches the available versions list
175
+ * from the provider, prepends the installed version when the catalog stub omits
176
+ * it, and keeps the selection clamped to whatever's currently in the list (so
177
+ * an external update doesn't strand the trigger on a tag that's no longer
178
+ * available).
179
+ */
180
+ const useVersionPicker = ({
181
+ provider,
182
+ pluginId,
183
+ repo,
184
+ moduleUrl,
185
+ installedVersionTag,
186
+ }: {
187
+ provider: Registry.Manager;
188
+ pluginId: string;
189
+ repo: string | undefined;
190
+ moduleUrl: string | undefined;
191
+ installedVersionTag: string | undefined;
192
+ }): {
193
+ pickerVersions: readonly Registry.PluginVersion[];
194
+ selectedVersionTag: string | undefined;
195
+ setSelectedVersionTag: Dispatch<SetStateAction<string | undefined>>;
196
+ } => {
197
+ const [versions, setVersions] = useState<readonly Registry.PluginVersion[]>([]);
198
+ const [selectedVersionTag, setSelectedVersionTag] = useState<string | undefined>();
199
+
200
+ // Load version list once the catalog entry's repo is known.
201
+ useEffect(() => {
202
+ if (!repo) {
203
+ return;
204
+ }
205
+ void provider.listVersions(repo).pipe(
206
+ Effect.match({
207
+ onSuccess: (vs) => {
208
+ setVersions(vs);
209
+ // Default selection: the currently installed version, or the latest.
210
+ const installedVersion = UrlLoader.getInstalledVersion(pluginId);
211
+ setSelectedVersionTag(installedVersion ?? vs[0]?.tag);
212
+ },
213
+ onFailure: () => {
214
+ // Non-critical; version picker stays empty.
215
+ },
216
+ }),
217
+ runAndForwardErrors,
218
+ );
219
+ }, [provider, repo, pluginId]);
220
+
221
+ // Make sure the picker always lists the installed version, even if the catalog
222
+ // hasn't surfaced it (the current `listVersions` stub only returns latest).
223
+ const pickerVersions = useMemo<readonly Registry.PluginVersion[]>(() => {
224
+ if (!installedVersionTag) {
225
+ return versions;
226
+ }
227
+ if (versions.some((entry) => entry.tag === installedVersionTag)) {
228
+ return versions;
229
+ }
230
+ const installedEntry: Registry.PluginVersion = {
231
+ tag: installedVersionTag,
232
+ // Picker won't be re-installing this entry unless the user selects + clicks Install,
233
+ // and the catalog moduleUrl is the closest stand-in we have.
234
+ moduleUrl: moduleUrl ?? '',
235
+ };
236
+ return [installedEntry, ...versions];
237
+ }, [versions, installedVersionTag, moduleUrl]);
238
+
239
+ // Keep `selectedVersionTag` in sync when picker contents change (e.g. the user
240
+ // updated the plugin from the list view — installed version moved, picker
241
+ // shrunk, prior selection is no longer in the list).
242
+ useEffect(() => {
243
+ if (pickerVersions.length === 0) {
244
+ return;
245
+ }
246
+ if (selectedVersionTag && pickerVersions.some((entry) => entry.tag === selectedVersionTag)) {
247
+ return;
248
+ }
249
+ setSelectedVersionTag(installedVersionTag ?? pickerVersions[0]?.tag);
250
+ }, [pickerVersions, selectedVersionTag, installedVersionTag]);
251
+
252
+ return { pickerVersions, selectedVersionTag, setSelectedVersionTag };
253
+ };
254
+
255
+ /**
256
+ * The user-facing actions the article exposes — enable/disable, uninstall,
257
+ * install (latest), update (to latest catalog version), and install-from-picker —
258
+ * plus the `installing`/`updating` flags they drive. Each handler ends with a
259
+ * `syncInstalledVersion()` call inside its `Effect.ensuring` so the cached
260
+ * installed version snaps to the freshly-written localStorage value in the same
261
+ * render that clears the in-flight flag.
262
+ */
263
+ const usePluginActions = ({
264
+ manager,
265
+ pluginId,
266
+ isInstalled,
267
+ moduleUrl,
268
+ catalogEntry,
269
+ pickerVersions,
270
+ selectedVersionTag,
271
+ syncInstalledVersion,
272
+ }: {
273
+ manager: PluginManager.PluginManager;
274
+ pluginId: string;
275
+ isInstalled: boolean;
276
+ moduleUrl: string | undefined;
277
+ catalogEntry: Registry.Plugin | undefined;
278
+ pickerVersions: readonly Registry.PluginVersion[];
279
+ selectedVersionTag: string | undefined;
280
+ syncInstalledVersion: () => void;
281
+ }) => {
282
+ const [installing, setInstalling] = useState(false);
283
+ const [updating, setUpdating] = useState(false);
284
+ const requestDisable = useDisableConfirmation(manager, (id) => void runAndForwardErrors(manager.disable(id)));
285
+
286
+ const handleEnableChange = useCallback(
287
+ (enabled: boolean) => {
288
+ if (enabled) {
289
+ void runAndForwardErrors(manager.enable(pluginId));
290
+ return;
291
+ }
292
+ requestDisable(pluginId);
293
+ },
294
+ [manager, pluginId, requestDisable],
295
+ );
296
+
297
+ const handleUninstall = useCallback(() => {
298
+ void runAndForwardErrors(manager.remove(pluginId));
299
+ }, [manager, pluginId]);
300
+
301
+ const handleInstall = useCallback(() => {
302
+ if (!moduleUrl) {
303
+ return;
304
+ }
305
+ setInstalling(true);
306
+ void Effect.gen(function* () {
307
+ const added = yield* manager.add(moduleUrl);
308
+ yield* manager.enable(added.meta.id);
309
+ if (catalogEntry) {
310
+ UrlLoader.setInstalledVersion(added.meta.id, catalogEntry.version);
311
+ }
312
+ }).pipe(
313
+ Effect.ensuring(
314
+ Effect.sync(() => {
315
+ setInstalling(false);
316
+ syncInstalledVersion();
317
+ }),
318
+ ),
319
+ runAndForwardErrors,
320
+ );
321
+ }, [manager, moduleUrl, catalogEntry, syncInstalledVersion]);
322
+
323
+ const handleUpdate = useCallback(() => {
324
+ if (!moduleUrl || !catalogEntry) {
325
+ return;
326
+ }
327
+ setUpdating(true);
328
+ void Effect.gen(function* () {
329
+ yield* manager.remove(pluginId);
330
+ const added = yield* manager.add(moduleUrl);
331
+ yield* manager.enable(added.meta.id);
332
+ UrlLoader.setInstalledVersion(added.meta.id, catalogEntry.version);
333
+ }).pipe(
334
+ Effect.ensuring(
335
+ Effect.sync(() => {
336
+ setUpdating(false);
337
+ syncInstalledVersion();
338
+ }),
339
+ ),
340
+ runAndForwardErrors,
341
+ );
342
+ }, [manager, pluginId, moduleUrl, catalogEntry, syncInstalledVersion]);
343
+
344
+ const handleInstallVersion = useCallback(() => {
345
+ const version = pickerVersions.find((candidate) => candidate.tag === selectedVersionTag);
346
+ if (!version || !version.moduleUrl) {
347
+ return;
348
+ }
349
+ setInstalling(true);
350
+ void Effect.gen(function* () {
351
+ // Remove existing version before installing a different one.
352
+ if (isInstalled) {
353
+ yield* manager.remove(pluginId);
354
+ }
355
+ const added = yield* manager.add(version.moduleUrl);
356
+ yield* manager.enable(added.meta.id);
357
+ UrlLoader.setInstalledVersion(added.meta.id, version.tag);
358
+ }).pipe(
359
+ Effect.ensuring(
360
+ Effect.sync(() => {
361
+ setInstalling(false);
362
+ syncInstalledVersion();
363
+ }),
364
+ ),
365
+ runAndForwardErrors,
366
+ );
367
+ }, [manager, pluginId, isInstalled, selectedVersionTag, pickerVersions, syncInstalledVersion]);
368
+
369
+ return {
370
+ installing,
371
+ updating,
372
+ handleEnableChange,
373
+ handleUninstall,
374
+ handleInstall,
375
+ handleUpdate,
376
+ handleInstallVersion,
377
+ };
378
+ };
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export { PluginArticle as default } from './PluginArticle';
@@ -0,0 +1,192 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import { useAtomValue } from '@effect-atom/atom-react';
6
+ import * as Effect from 'effect/Effect';
7
+ import React, { useCallback, useMemo, useState } from 'react';
8
+
9
+ import { type Registry, Plugin, UrlLoader } from '@dxos/app-framework';
10
+ import { useOperationInvoker, usePluginManager } from '@dxos/app-framework/ui';
11
+ import { runAndForwardErrors } from '@dxos/effect';
12
+ import { ObservabilityOperation } from '@dxos/plugin-observability';
13
+ import { useTranslation } from '@dxos/react-ui';
14
+ import { composable } from '@dxos/ui-theme';
15
+
16
+ import { meta } from '#meta';
17
+
18
+ import { useAutoTags, useRegistryPlugins, useUpdateAvailableIds } from '../../hooks';
19
+ import { BaseRegistryArticle } from '../BaseRegistryArticle';
20
+
21
+ const sortEntries = (a: Registry.Plugin, b: Registry.Plugin) => (a.name ?? a.id).localeCompare(b.name ?? b.id);
22
+
23
+ const sortPlugins = (a: Plugin.Plugin, b: Plugin.Plugin) =>
24
+ (a.meta.name ?? a.meta.id).localeCompare(b.meta.name ?? b.meta.id);
25
+
26
+ /**
27
+ * Turns a registry catalog entry into a minimal Plugin object so we can reuse
28
+ * {@link BaseRegistryArticle} for rendering. The synthesized plugin has no
29
+ * modules — it exists only for display until the user installs it.
30
+ */
31
+ const toDisplayPlugin = (plugin: Registry.Plugin): Plugin.Plugin =>
32
+ ({
33
+ [Plugin.PluginTypeId]: Plugin.PluginTypeId,
34
+ meta: {
35
+ id: plugin.id,
36
+ name: plugin.name,
37
+ description: plugin.description,
38
+ homePage: plugin.homePage,
39
+ source: plugin.source,
40
+ screenshots: plugin.screenshots,
41
+ tags: plugin.tags,
42
+ icon: plugin.icon,
43
+ iconHue: plugin.iconHue,
44
+ },
45
+ modules: [],
46
+ }) as Plugin.Plugin;
47
+
48
+ export type PublicRegistryArticleProps = {
49
+ id: string;
50
+ };
51
+
52
+ export const PublicRegistryArticle = composable<HTMLDivElement, PublicRegistryArticleProps>(
53
+ ({ id, ...props }, forwardedRef) => {
54
+ const { t } = useTranslation(meta.id);
55
+ const manager = usePluginManager();
56
+ const { invoke } = useOperationInvoker();
57
+ const { entries, loading, error } = useRegistryPlugins();
58
+ const plugins = useAtomValue(manager.plugins);
59
+ const installedIds = useMemo(() => plugins.map((plugin) => plugin.meta.id), [plugins]);
60
+ const extraTagsById = useAutoTags(entries);
61
+
62
+ // Snapshot of installed plugin ids at mount time. Used to sort installed
63
+ // plugins to the top without having newly-installed rows jump up mid-session.
64
+ const [installedSnapshot] = useState<ReadonlySet<string>>(
65
+ () => new Set(manager.getPlugins().map((plugin) => plugin.meta.id)),
66
+ );
67
+
68
+ const [installingIds, setInstallingIds] = useState<readonly string[]>([]);
69
+ const [updatingIds, setUpdatingIds] = useState<readonly string[]>([]);
70
+ const updateAvailableIds = useUpdateAvailableIds(entries);
71
+
72
+ const sortedEntries = useMemo(() => [...entries].sort(sortEntries), [entries]);
73
+ const moduleUrlById = useMemo(() => {
74
+ const map: Record<string, string> = {};
75
+ for (const entry of sortedEntries) {
76
+ map[entry.id] = entry.moduleUrl;
77
+ }
78
+ return map;
79
+ }, [sortedEntries]);
80
+
81
+ // Maps plugin id → current registry version (i.e. plugin.version from the catalog).
82
+ const versionById = useMemo(() => {
83
+ const map: Record<string, string> = {};
84
+ for (const entry of sortedEntries) {
85
+ map[entry.id] = entry.version;
86
+ }
87
+ return map;
88
+ }, [sortedEntries]);
89
+
90
+ // Single list with installed-at-mount plugins sorted to the top. Sort order
91
+ // is based on the mount-time snapshot so rows don't jump as the user
92
+ // installs plugins during the session.
93
+ const items = useMemo(() => {
94
+ const catalogIds = new Set(sortedEntries.map((entry) => entry.id));
95
+ const remoteIds = new Set(UrlLoader.getRemoteEntries().map((entry) => entry.id));
96
+ const fromCatalog = sortedEntries.map(toDisplayPlugin);
97
+ const fromUrlOnly = plugins.filter((plugin) => remoteIds.has(plugin.meta.id) && !catalogIds.has(plugin.meta.id));
98
+ const installedFirst = [...fromCatalog, ...fromUrlOnly].sort((a, b) => {
99
+ const aInstalled = installedSnapshot.has(a.meta.id);
100
+ const bInstalled = installedSnapshot.has(b.meta.id);
101
+ if (aInstalled !== bInstalled) {
102
+ return aInstalled ? -1 : 1;
103
+ }
104
+ return sortPlugins(a, b);
105
+ });
106
+ return installedFirst;
107
+ }, [sortedEntries, installedSnapshot, plugins]);
108
+
109
+ const handleInstall = useCallback(
110
+ (pluginId: string) => {
111
+ const moduleUrl = moduleUrlById[pluginId];
112
+ const version = versionById[pluginId];
113
+ if (!moduleUrl) {
114
+ return;
115
+ }
116
+ setInstallingIds((prev) => (prev.includes(pluginId) ? prev : [...prev, pluginId]));
117
+ void Effect.gen(function* () {
118
+ const plugin = yield* manager.add(moduleUrl);
119
+ yield* manager.enable(plugin.meta.id);
120
+ // Persist the installed version for future update detection.
121
+ if (version) {
122
+ UrlLoader.setInstalledVersion(plugin.meta.id, version);
123
+ }
124
+ yield* invoke(ObservabilityOperation.SendEvent, {
125
+ name: 'plugins.install',
126
+ properties: { plugin: plugin.meta.id, source: 'registry' },
127
+ });
128
+ }).pipe(
129
+ Effect.ensuring(Effect.sync(() => setInstallingIds((prev) => prev.filter((pid) => pid !== pluginId)))),
130
+ runAndForwardErrors,
131
+ );
132
+ },
133
+ [invoke, manager, moduleUrlById, versionById],
134
+ );
135
+
136
+ const handleUpdate = useCallback(
137
+ (pluginId: string) => {
138
+ const moduleUrl = moduleUrlById[pluginId];
139
+ const version = versionById[pluginId];
140
+ if (!moduleUrl) {
141
+ return;
142
+ }
143
+ setUpdatingIds((prev) => (prev.includes(pluginId) ? prev : [...prev, pluginId]));
144
+ void Effect.gen(function* () {
145
+ // Unload the old version then re-install from the new URL.
146
+ yield* manager.remove(pluginId);
147
+ const plugin = yield* manager.add(moduleUrl);
148
+ yield* manager.enable(plugin.meta.id);
149
+ if (version) {
150
+ UrlLoader.setInstalledVersion(plugin.meta.id, version);
151
+ }
152
+ yield* invoke(ObservabilityOperation.SendEvent, {
153
+ name: 'plugins.update',
154
+ properties: { plugin: plugin.meta.id, source: 'registry' },
155
+ });
156
+ }).pipe(
157
+ Effect.ensuring(Effect.sync(() => setUpdatingIds((prev) => prev.filter((pid) => pid !== pluginId)))),
158
+ runAndForwardErrors,
159
+ );
160
+ },
161
+ [invoke, manager, moduleUrlById, versionById],
162
+ );
163
+
164
+ const empty = error ? (
165
+ <div className='p-4 text-description'>{t('registry.error.label', { message: error.message })}</div>
166
+ ) : loading ? (
167
+ <div className='p-4 text-description'>{t('registry.loading.label')}</div>
168
+ ) : (
169
+ <div className='p-4 text-description'>{t('registry.empty.label')}</div>
170
+ );
171
+
172
+ return (
173
+ <BaseRegistryArticle
174
+ {...props}
175
+ id={id}
176
+ source='registry'
177
+ plugins={items}
178
+ installed={installedIds}
179
+ installing={installingIds}
180
+ updating={updatingIds}
181
+ updateAvailableIds={updateAvailableIds}
182
+ extraTagsById={extraTagsById}
183
+ onInstall={handleInstall}
184
+ onUpdate={handleUpdate}
185
+ empty={empty}
186
+ ref={forwardedRef}
187
+ />
188
+ );
189
+ },
190
+ );
191
+
192
+ PublicRegistryArticle.displayName = 'PublicRegistryArticle';
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ export { PublicRegistryArticle as default, PublicRegistryArticle } from './PublicRegistryArticle';
@@ -0,0 +1,53 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { useAtomValue } from '@effect-atom/atom-react';
6
+ import React, { useMemo } from 'react';
7
+
8
+ import { type Plugin, type PluginManager } from '@dxos/app-framework';
9
+ import { usePluginManager } from '@dxos/app-framework/ui';
10
+ import { composable } from '@dxos/ui-theme';
11
+
12
+ import { BaseRegistryArticle } from '../BaseRegistryArticle';
13
+
14
+ const sortByPluginMeta = (a: Plugin.Plugin, b: Plugin.Plugin) =>
15
+ (a.meta.name ?? a.meta.id).localeCompare(b.meta.name ?? b.meta.id);
16
+
17
+ export type RegistryArticleProps = {
18
+ id: string;
19
+ plugins: Plugin.Plugin[];
20
+ /**
21
+ * Map from plugin id → display-only tags (e.g. `registry`, `local`) computed by the caller.
22
+ */
23
+ extraTagsById?: Record<string, readonly string[]>;
24
+ };
25
+
26
+ export const RegistryArticle = composable<HTMLDivElement, RegistryArticleProps>(
27
+ ({ id, plugins: pluginsProp, extraTagsById, ...props }, forwardedRef) => {
28
+ const manager = usePluginManager();
29
+ const failed = useAtomValue(manager.failed);
30
+ const plugins = useMemo(() => [...pluginsProp].sort(sortByPluginMeta), [pluginsProp]);
31
+ const failuresById = useMemo(
32
+ () =>
33
+ failed.reduce<Record<string, PluginManager.PluginFailure>>((acc, failure) => {
34
+ acc[failure.id] = failure;
35
+ return acc;
36
+ }, {}),
37
+ [failed],
38
+ );
39
+
40
+ return (
41
+ <BaseRegistryArticle
42
+ {...props}
43
+ id={id}
44
+ plugins={plugins}
45
+ extraTagsById={extraTagsById}
46
+ failuresById={failuresById}
47
+ ref={forwardedRef}
48
+ />
49
+ );
50
+ },
51
+ );
52
+
53
+ RegistryArticle.displayName = 'RegistryArticle';
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ export { RegistryArticle as default, RegistryArticle } from './RegistryArticle';