@dxos/plugin-registry 0.8.4-main.422d1c7879 → 0.8.4-main.4f23b4e393

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 (193) hide show
  1. package/dist/lib/neutral/RegistryPlugin.mjs +27 -0
  2. package/dist/lib/neutral/RegistryPlugin.mjs.map +7 -0
  3. package/dist/lib/{browser/cli/index.mjs → neutral/RegistryPlugin.node.mjs} +29 -95
  4. package/dist/lib/neutral/RegistryPlugin.node.mjs.map +7 -0
  5. package/dist/lib/neutral/chunk-XK2JMH3H.mjs +33 -0
  6. package/dist/lib/neutral/chunk-XK2JMH3H.mjs.map +7 -0
  7. package/dist/lib/neutral/chunk-YV7A523U.mjs +29 -0
  8. package/dist/lib/neutral/chunk-YV7A523U.mjs.map +7 -0
  9. package/dist/lib/neutral/index.mjs +21 -0
  10. package/dist/lib/neutral/index.mjs.map +7 -0
  11. package/dist/lib/neutral/meta.json +1 -0
  12. package/dist/lib/neutral/open-plugin-registry-II735N76.mjs +15 -0
  13. package/dist/lib/neutral/open-plugin-registry-II735N76.mjs.map +7 -0
  14. package/dist/lib/neutral/plugin.mjs +18 -0
  15. package/dist/lib/neutral/plugin.mjs.map +7 -0
  16. package/dist/lib/neutral/translations.mjs +61 -0
  17. package/dist/lib/neutral/translations.mjs.map +7 -0
  18. package/dist/types/src/RegistryPlugin.d.ts +1 -0
  19. package/dist/types/src/RegistryPlugin.d.ts.map +1 -1
  20. package/dist/types/src/{cli/plugin.d.ts → RegistryPlugin.node.d.ts} +2 -1
  21. package/dist/types/src/RegistryPlugin.node.d.ts.map +1 -0
  22. package/dist/types/src/RegistryPlugin.test.d.ts +2 -0
  23. package/dist/types/src/RegistryPlugin.test.d.ts.map +1 -0
  24. package/dist/types/src/capabilities/app-graph-builder.d.ts.map +1 -1
  25. package/dist/types/src/capabilities/dev-plugin-loader.d.ts +11 -0
  26. package/dist/types/src/capabilities/dev-plugin-loader.d.ts.map +1 -0
  27. package/dist/types/src/capabilities/index.d.ts +11 -2
  28. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  29. package/dist/types/src/capabilities/operation-handler.d.ts +1 -1
  30. package/dist/types/src/capabilities/operation-handler.d.ts.map +1 -1
  31. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -1
  32. package/dist/types/src/capabilities/settings.d.ts +14 -0
  33. package/dist/types/src/capabilities/settings.d.ts.map +1 -0
  34. package/dist/types/src/{cli/commands → commands}/disable.d.ts +2 -2
  35. package/dist/types/src/commands/disable.d.ts.map +1 -0
  36. package/dist/types/src/{cli/commands → commands}/enable.d.ts +2 -2
  37. package/dist/types/src/commands/enable.d.ts.map +1 -0
  38. package/dist/types/src/commands/index.d.ts.map +1 -0
  39. package/dist/types/src/commands/list.d.ts +7 -0
  40. package/dist/types/src/commands/list.d.ts.map +1 -0
  41. package/dist/types/src/commands/util.d.ts.map +1 -0
  42. package/dist/types/src/components/PluginDetail/PluginDetail.d.ts +26 -2
  43. package/dist/types/src/components/PluginDetail/PluginDetail.d.ts.map +1 -1
  44. package/dist/types/src/components/PluginDetail/PluginDetail.stories.d.ts +49 -23
  45. package/dist/types/src/components/PluginDetail/PluginDetail.stories.d.ts.map +1 -1
  46. package/dist/types/src/components/PluginFailureBadge/PluginFailureBadge.d.ts +20 -0
  47. package/dist/types/src/components/PluginFailureBadge/PluginFailureBadge.d.ts.map +1 -0
  48. package/dist/types/src/components/PluginFailureBadge/index.d.ts +2 -0
  49. package/dist/types/src/components/PluginFailureBadge/index.d.ts.map +1 -0
  50. package/dist/types/src/components/PluginList/PluginItem.d.ts +17 -3
  51. package/dist/types/src/components/PluginList/PluginItem.d.ts.map +1 -1
  52. package/dist/types/src/components/PluginList/PluginList.d.ts +14 -4
  53. package/dist/types/src/components/PluginList/PluginList.d.ts.map +1 -1
  54. package/dist/types/src/components/PluginList/PluginList.stories.d.ts +49 -23
  55. package/dist/types/src/components/PluginList/PluginList.stories.d.ts.map +1 -1
  56. package/dist/types/src/components/RegistrySettings/RegistrySettings.d.ts +18 -0
  57. package/dist/types/src/components/RegistrySettings/RegistrySettings.d.ts.map +1 -0
  58. package/dist/types/src/components/RegistrySettings/index.d.ts +2 -0
  59. package/dist/types/src/components/RegistrySettings/index.d.ts.map +1 -0
  60. package/dist/types/src/components/index.d.ts +2 -0
  61. package/dist/types/src/components/index.d.ts.map +1 -1
  62. package/dist/types/src/containers/BaseRegistryArticle/BaseRegistryArticle.d.ts +20 -0
  63. package/dist/types/src/containers/BaseRegistryArticle/BaseRegistryArticle.d.ts.map +1 -0
  64. package/dist/types/src/containers/BaseRegistryArticle/index.d.ts +2 -0
  65. package/dist/types/src/containers/BaseRegistryArticle/index.d.ts.map +1 -0
  66. package/dist/types/src/containers/LoadPluginDialog/LoadPluginDialog.d.ts.map +1 -1
  67. package/dist/types/src/containers/PluginArticle/PluginArticle.d.ts.map +1 -1
  68. package/dist/types/src/containers/PublicRegistryArticle/PublicRegistryArticle.d.ts +8 -0
  69. package/dist/types/src/containers/PublicRegistryArticle/PublicRegistryArticle.d.ts.map +1 -0
  70. package/dist/types/src/containers/PublicRegistryArticle/index.d.ts +2 -0
  71. package/dist/types/src/containers/PublicRegistryArticle/index.d.ts.map +1 -0
  72. package/dist/types/src/containers/RegistryArticle/RegistryArticle.d.ts +14 -0
  73. package/dist/types/src/containers/RegistryArticle/RegistryArticle.d.ts.map +1 -0
  74. package/dist/types/src/containers/RegistryArticle/index.d.ts +2 -0
  75. package/dist/types/src/containers/RegistryArticle/index.d.ts.map +1 -0
  76. package/dist/types/src/containers/RegistrySettingsContainer/RegistrySettingsContainer.d.ts +13 -0
  77. package/dist/types/src/containers/RegistrySettingsContainer/RegistrySettingsContainer.d.ts.map +1 -0
  78. package/dist/types/src/containers/RegistrySettingsContainer/index.d.ts +2 -0
  79. package/dist/types/src/containers/RegistrySettingsContainer/index.d.ts.map +1 -0
  80. package/dist/types/src/containers/index.d.ts +3 -2
  81. package/dist/types/src/containers/index.d.ts.map +1 -1
  82. package/dist/types/src/hooks/index.d.ts +2 -1
  83. package/dist/types/src/hooks/index.d.ts.map +1 -1
  84. package/dist/types/src/hooks/use-auto-tags.d.ts +4 -4
  85. package/dist/types/src/hooks/use-auto-tags.d.ts.map +1 -1
  86. package/dist/types/src/hooks/use-registry-plugins.d.ts +15 -0
  87. package/dist/types/src/hooks/use-registry-plugins.d.ts.map +1 -0
  88. package/dist/types/src/hooks/use-update-available.d.ts +18 -0
  89. package/dist/types/src/hooks/use-update-available.d.ts.map +1 -0
  90. package/dist/types/src/index.d.ts +1 -1
  91. package/dist/types/src/index.d.ts.map +1 -1
  92. package/dist/types/src/meta.d.ts.map +1 -1
  93. package/dist/types/src/operations/index.d.ts +1 -1
  94. package/dist/types/src/operations/index.d.ts.map +1 -1
  95. package/dist/types/src/operations/open-plugin-registry.d.ts +1 -1
  96. package/dist/types/src/operations/open-plugin-registry.d.ts.map +1 -1
  97. package/dist/types/src/plugin.d.ts +4 -0
  98. package/dist/types/src/plugin.d.ts.map +1 -0
  99. package/dist/types/src/{cli/storage.d.ts → storage.d.ts} +1 -1
  100. package/dist/types/src/storage.d.ts.map +1 -0
  101. package/dist/types/src/translations.d.ts +48 -23
  102. package/dist/types/src/translations.d.ts.map +1 -1
  103. package/dist/types/src/types.d.ts +28 -10
  104. package/dist/types/src/types.d.ts.map +1 -1
  105. package/dist/types/tsconfig.tsbuildinfo +1 -1
  106. package/package.json +46 -36
  107. package/src/{cli/plugin.ts → RegistryPlugin.node.ts} +2 -0
  108. package/src/RegistryPlugin.test.ts +24 -0
  109. package/src/RegistryPlugin.tsx +9 -7
  110. package/src/capabilities/app-graph-builder.ts +25 -17
  111. package/src/capabilities/dev-plugin-loader.ts +39 -0
  112. package/src/capabilities/index.ts +3 -2
  113. package/src/capabilities/operation-handler.ts +1 -1
  114. package/src/capabilities/react-surface.tsx +63 -47
  115. package/src/capabilities/settings.ts +34 -0
  116. package/src/components/PluginDetail/PluginDetail.stories.tsx +3 -2
  117. package/src/components/PluginDetail/PluginDetail.tsx +105 -15
  118. package/src/components/PluginFailureBadge/PluginFailureBadge.tsx +68 -0
  119. package/src/components/PluginFailureBadge/index.ts +5 -0
  120. package/src/components/PluginList/PluginItem.tsx +46 -7
  121. package/src/components/PluginList/PluginList.stories.tsx +1 -1
  122. package/src/components/PluginList/PluginList.tsx +28 -5
  123. package/src/components/RegistrySettings/RegistrySettings.tsx +120 -0
  124. package/src/components/RegistrySettings/index.ts +5 -0
  125. package/src/components/index.ts +2 -0
  126. package/src/containers/BaseRegistryArticle/BaseRegistryArticle.tsx +163 -0
  127. package/src/containers/BaseRegistryArticle/index.ts +5 -0
  128. package/src/containers/LoadPluginDialog/LoadPluginDialog.tsx +4 -3
  129. package/src/containers/PluginArticle/PluginArticle.tsx +296 -32
  130. package/src/containers/PublicRegistryArticle/PublicRegistryArticle.tsx +192 -0
  131. package/src/containers/PublicRegistryArticle/index.ts +5 -0
  132. package/src/containers/RegistryArticle/RegistryArticle.tsx +53 -0
  133. package/src/containers/RegistryArticle/index.ts +5 -0
  134. package/src/containers/RegistrySettingsContainer/RegistrySettingsContainer.tsx +59 -0
  135. package/src/containers/RegistrySettingsContainer/index.ts +5 -0
  136. package/src/containers/index.ts +3 -2
  137. package/src/hooks/index.ts +2 -1
  138. package/src/hooks/use-auto-tags.ts +7 -8
  139. package/src/hooks/use-registry-plugins.ts +29 -0
  140. package/src/hooks/use-update-available.ts +31 -0
  141. package/src/index.ts +1 -1
  142. package/src/meta.ts +2 -0
  143. package/src/operations/index.ts +1 -1
  144. package/src/operations/open-plugin-registry.ts +1 -1
  145. package/src/plugin.ts +11 -0
  146. package/src/translations.ts +33 -3
  147. package/src/types.ts +21 -14
  148. package/dist/lib/browser/cli/index.mjs.map +0 -7
  149. package/dist/lib/browser/index.mjs +0 -78
  150. package/dist/lib/browser/index.mjs.map +0 -7
  151. package/dist/lib/browser/meta.json +0 -1
  152. package/dist/lib/node-esm/cli/index.mjs +0 -221
  153. package/dist/lib/node-esm/cli/index.mjs.map +0 -7
  154. package/dist/lib/node-esm/index.mjs +0 -80
  155. package/dist/lib/node-esm/index.mjs.map +0 -7
  156. package/dist/lib/node-esm/meta.json +0 -1
  157. package/dist/types/src/capabilities/state.d.ts +0 -7
  158. package/dist/types/src/capabilities/state.d.ts.map +0 -1
  159. package/dist/types/src/cli/commands/disable.d.ts.map +0 -1
  160. package/dist/types/src/cli/commands/enable.d.ts.map +0 -1
  161. package/dist/types/src/cli/commands/index.d.ts.map +0 -1
  162. package/dist/types/src/cli/commands/list.d.ts +0 -7
  163. package/dist/types/src/cli/commands/list.d.ts.map +0 -1
  164. package/dist/types/src/cli/commands/util.d.ts.map +0 -1
  165. package/dist/types/src/cli/index.d.ts +0 -3
  166. package/dist/types/src/cli/index.d.ts.map +0 -1
  167. package/dist/types/src/cli/plugin.d.ts.map +0 -1
  168. package/dist/types/src/cli/storage.d.ts.map +0 -1
  169. package/dist/types/src/containers/CommunityRegistry/CommunityRegistry.d.ts +0 -8
  170. package/dist/types/src/containers/CommunityRegistry/CommunityRegistry.d.ts.map +0 -1
  171. package/dist/types/src/containers/CommunityRegistry/index.d.ts +0 -2
  172. package/dist/types/src/containers/CommunityRegistry/index.d.ts.map +0 -1
  173. package/dist/types/src/containers/PluginRegistry/PluginRegistry.d.ts +0 -14
  174. package/dist/types/src/containers/PluginRegistry/PluginRegistry.d.ts.map +0 -1
  175. package/dist/types/src/containers/PluginRegistry/index.d.ts +0 -2
  176. package/dist/types/src/containers/PluginRegistry/index.d.ts.map +0 -1
  177. package/dist/types/src/hooks/use-community-plugins.d.ts +0 -16
  178. package/dist/types/src/hooks/use-community-plugins.d.ts.map +0 -1
  179. package/src/capabilities/state.ts +0 -40
  180. package/src/cli/index.ts +0 -6
  181. package/src/containers/CommunityRegistry/CommunityRegistry.tsx +0 -181
  182. package/src/containers/CommunityRegistry/index.ts +0 -5
  183. package/src/containers/PluginRegistry/PluginRegistry.tsx +0 -98
  184. package/src/containers/PluginRegistry/index.ts +0 -5
  185. package/src/hooks/use-community-plugins.ts +0 -35
  186. /package/dist/types/src/{cli/commands → commands}/index.d.ts +0 -0
  187. /package/dist/types/src/{cli/commands → commands}/util.d.ts +0 -0
  188. /package/src/{cli/commands → commands}/disable.ts +0 -0
  189. /package/src/{cli/commands → commands}/enable.ts +0 -0
  190. /package/src/{cli/commands → commands}/index.ts +0 -0
  191. /package/src/{cli/commands → commands}/list.ts +0 -0
  192. /package/src/{cli/commands → commands}/util.ts +0 -0
  193. /package/src/{cli/storage.ts → storage.ts} +0 -0
@@ -4,12 +4,14 @@
4
4
 
5
5
  import React from 'react';
6
6
 
7
- import { type Plugin } from '@dxos/app-framework';
8
- import { Button, Icon, Input, Link, ScrollArea, useTranslation } from '@dxos/react-ui';
7
+ import { type Registry, type Plugin, type PluginManager } from '@dxos/app-framework';
8
+ import { Button, Icon, Input, Link, ScrollArea, Select, useTranslation } from '@dxos/react-ui';
9
9
  import { composable, composableProps, getStyles, mx } from '@dxos/ui-theme';
10
10
 
11
11
  import { meta } from '#meta';
12
12
 
13
+ import { PluginFailureBadge } from '../PluginFailureBadge';
14
+
13
15
  export type PluginDetailProps = {
14
16
  plugin: Plugin.Plugin;
15
17
  enabled?: boolean;
@@ -25,10 +27,54 @@ export type PluginDetailProps = {
25
27
  */
26
28
  onInstall?: () => void;
27
29
  installing?: boolean;
30
+ /**
31
+ * When provided and the plugin is installed with a newer catalog version available,
32
+ * an Update button is shown in place of the enable Switch.
33
+ */
34
+ onUpdate?: () => void;
35
+ /** True when the catalog has a newer version than the one installed. */
36
+ hasUpdate?: boolean;
37
+ /** True while an in-flight update is running. Forces the Update button disabled state. */
38
+ updating?: boolean;
39
+ /** Available versions of this plugin from the catalog. When non-empty, a version picker is shown. */
40
+ versions?: readonly Registry.PluginVersion[];
41
+ /** Currently selected version tag in the picker. */
42
+ selectedVersionTag?: string;
43
+ /** Called when the user selects a different version in the picker. */
44
+ onVersionChange?: (tag: string) => void;
45
+ /** Called when the user clicks Install on the version picker. */
46
+ onInstallVersion?: () => void;
47
+ /** Currently installed version tag (used to mark the matching select option). */
48
+ installedVersionTag?: string;
49
+ /**
50
+ * Failure record for this plugin, if any. When present a warning badge is
51
+ * rendered next to the plugin name in the header.
52
+ */
53
+ failure?: PluginManager.PluginFailure;
28
54
  };
29
55
 
30
56
  export const PluginDetail = composable<HTMLDivElement, PluginDetailProps>(
31
- ({ plugin, enabled, onEnabledChange, onUninstall, onInstall, installing, ...props }, forwardedRef) => {
57
+ (
58
+ {
59
+ plugin,
60
+ enabled,
61
+ onEnabledChange,
62
+ onUninstall,
63
+ onInstall,
64
+ installing,
65
+ onUpdate,
66
+ hasUpdate,
67
+ updating,
68
+ versions,
69
+ selectedVersionTag,
70
+ onVersionChange,
71
+ onInstallVersion,
72
+ installedVersionTag,
73
+ failure,
74
+ ...props
75
+ },
76
+ forwardedRef,
77
+ ) => {
32
78
  const { t } = useTranslation(meta.id);
33
79
  const {
34
80
  id,
@@ -45,13 +91,16 @@ export const PluginDetail = composable<HTMLDivElement, PluginDetailProps>(
45
91
  return (
46
92
  <ScrollArea.Root {...composableProps(props)} orientation='vertical' ref={forwardedRef}>
47
93
  <ScrollArea.Viewport>
48
- <div role='none' className='dx-document grid grid-cols-[min-content_1fr] gap-4 p-4'>
49
- <div role='none'>
50
- <Icon classNames={mx('p-1 rounded-md', styles.fill, styles.surfaceText)} icon={icon} size={14} />
94
+ <div className='dx-document grid grid-cols-[min-content_1fr] gap-4 p-4'>
95
+ <div>
96
+ <Icon classNames={mx('p-1 rounded-md', styles.fill, styles.foreground)} icon={icon} size={14} />
51
97
  </div>
52
- <div role='none' className='flex flex-col gap-6'>
53
- <div role='none' className='grid grid-cols-[1fr_min-content] gap-x-3 w-full pt-1'>
54
- <h2 className='text-xl'>{name}</h2>
98
+ <div className='flex flex-col gap-6'>
99
+ <div className='grid grid-cols-[1fr_min-content] gap-x-3 w-full pt-1'>
100
+ <div className='flex items-center gap-2'>
101
+ <h2 className='text-xl'>{name}</h2>
102
+ {failure && <PluginFailureBadge failure={failure} size={5} />}
103
+ </div>
55
104
  {onInstall ? (
56
105
  <Button density='fine' variant='primary' disabled={installing} onClick={onInstall}>
57
106
  {installing ? t('installing.label') : t('install.label')}
@@ -63,16 +112,16 @@ export const PluginDetail = composable<HTMLDivElement, PluginDetailProps>(
63
112
  )}
64
113
  <p className='pt-0.5 text-sm text-description'>{id}</p>
65
114
  </div>
66
- <div role='none'>
115
+ <div>
67
116
  <p className='text-description'>{description}</p>
68
117
  </div>
69
118
  {screenshots && screenshots.length > 0 && (
70
- <div role='none' className='flex flex-col gap-2'>
119
+ <div className='flex flex-col gap-2'>
71
120
  <h2>Preview</h2>
72
121
  <img src={screenshots[0]} alt={name} className='aspect-video object-fit' />
73
122
  </div>
74
123
  )}
75
- <div role='none' className='flex flex-col gap-2'>
124
+ <div className='flex flex-col gap-2'>
76
125
  <h2>Resources</h2>
77
126
  <div className='flex gap-2'>
78
127
  {homePage && (
@@ -90,9 +139,50 @@ export const PluginDetail = composable<HTMLDivElement, PluginDetailProps>(
90
139
  )}
91
140
  </div>
92
141
  </div>
93
- {onUninstall && (
94
- <div role='none'>
95
- <Button onClick={onUninstall}>{t('uninstall.label')}</Button>
142
+ {versions && versions.length > 0 && (
143
+ <div className='flex flex-col gap-2'>
144
+ <h2>{t('versions.label')}</h2>
145
+ <div className='flex gap-2 items-center'>
146
+ <Select.Root value={selectedVersionTag} onValueChange={onVersionChange}>
147
+ <Select.TriggerButton classNames='min-w-32' />
148
+ <Select.Portal>
149
+ <Select.Content>
150
+ <Select.Viewport>
151
+ {versions.map((versionEntry) => (
152
+ <Select.Option key={versionEntry.tag} value={versionEntry.tag}>
153
+ {versionEntry.tag}
154
+ {installedVersionTag === versionEntry.tag ? ` (${t('installed.label')})` : ''}
155
+ </Select.Option>
156
+ ))}
157
+ </Select.Viewport>
158
+ </Select.Content>
159
+ </Select.Portal>
160
+ </Select.Root>
161
+ {onInstallVersion && (
162
+ <Button
163
+ density='fine'
164
+ variant='primary'
165
+ disabled={installing || selectedVersionTag === installedVersionTag}
166
+ onClick={onInstallVersion}
167
+ >
168
+ {installing ? t('installing.label') : t('install-version.label')}
169
+ </Button>
170
+ )}
171
+ </div>
172
+ </div>
173
+ )}
174
+ {(onUninstall || (hasUpdate && onUpdate) || updating) && (
175
+ <div className='flex gap-2'>
176
+ {updating ? (
177
+ <Button variant='primary' disabled>
178
+ {t('updating.label')}
179
+ </Button>
180
+ ) : hasUpdate && onUpdate ? (
181
+ <Button variant='primary' onClick={onUpdate}>
182
+ {t('update.label')}
183
+ </Button>
184
+ ) : null}
185
+ {onUninstall && <Button onClick={onUninstall}>{t('uninstall.label')}</Button>}
96
186
  </div>
97
187
  )}
98
188
  </div>
@@ -0,0 +1,68 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import React from 'react';
6
+
7
+ import { type PluginManager } from '@dxos/app-framework';
8
+ import { IconButton, Popover, useTranslation } from '@dxos/react-ui';
9
+
10
+ import { meta } from '#meta';
11
+
12
+ // Mirrors react-ui `Icon`'s `size` prop literal subset; kept inline so we don't
13
+ // have to add `@dxos/ui-types` as a dep just for one type alias.
14
+ type IconSize = 4 | 5 | 6 | 8;
15
+
16
+ export type PluginFailureBadgeProps = {
17
+ failure: PluginManager.PluginFailure;
18
+ /** Optional override for the trigger icon's tailwind classes. */
19
+ iconClassNames?: string;
20
+ /** Visual size of the warning icon (passed through to react-ui `<Icon />`). */
21
+ size?: IconSize;
22
+ };
23
+
24
+ /**
25
+ * Compact warning glyph rendered next to a plugin's name when it has failed
26
+ * to load or activate. Clicking the icon opens a popover that names the phase
27
+ * (load / activation), the reason (timeout / error), and the underlying error
28
+ * message — enough for an operator to tell "remote host is offline" apart
29
+ * from "the plugin crashed".
30
+ */
31
+ export const PluginFailureBadge = ({ failure, iconClassNames, size = 4 }: PluginFailureBadgeProps) => {
32
+ const { t } = useTranslation(meta.id);
33
+
34
+ const phaseLabel = failure.phase === 'load' ? t('failure-phase-load.label') : t('failure-phase-activation.label');
35
+ const reasonLabel =
36
+ failure.reason === 'timeout' ? t('failure-reason-timeout.label') : t('failure-reason-error.label');
37
+
38
+ return (
39
+ <Popover.Root>
40
+ <Popover.Trigger asChild>
41
+ <IconButton
42
+ variant='ghost'
43
+ iconOnly
44
+ noTooltip
45
+ icon='ph--warning--bold'
46
+ size={size}
47
+ iconClassNames={iconClassNames}
48
+ label={t('failure-badge.label')}
49
+ data-testid={`pluginFailureBadge.${failure.id}`}
50
+ // Don't bubble up to row click handlers (e.g. the card's `onClick`
51
+ // that opens the details pane). The popover is the action here.
52
+ onClick={(event) => event.stopPropagation()}
53
+ />
54
+ </Popover.Trigger>
55
+ <Popover.Content>
56
+ <Popover.Viewport>
57
+ <div className='px-3 py-2 min-w-[18rem] max-w-[28rem] flex flex-col gap-1'>
58
+ <p className='font-medium text-sm'>
59
+ {t('failure-title.label', { phase: phaseLabel, reason: reasonLabel })}
60
+ </p>
61
+ <p className='text-description text-sm break-words'>{failure.error.message}</p>
62
+ </div>
63
+ </Popover.Viewport>
64
+ <Popover.Arrow />
65
+ </Popover.Content>
66
+ </Popover.Root>
67
+ );
68
+ };
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ export * from './PluginFailureBadge';
@@ -4,7 +4,7 @@
4
4
 
5
5
  import React, { type MouseEvent, useCallback, useMemo } from 'react';
6
6
 
7
- import { type Plugin } from '@dxos/app-framework';
7
+ import { type Plugin, type PluginManager } from '@dxos/app-framework';
8
8
  import {
9
9
  Button,
10
10
  type ChromaticPalette,
@@ -23,6 +23,8 @@ import { getStyles } from '@dxos/ui-theme';
23
23
  import { meta } from '#meta';
24
24
  import { type RegistryTagType } from '#types';
25
25
 
26
+ import { PluginFailureBadge } from '../PluginFailureBadge';
27
+
26
28
  export type PluginItemProps = {
27
29
  plugin: Plugin.Plugin;
28
30
  /** Ids of plugins currently installed (loaded into the manager). */
@@ -31,7 +33,7 @@ export type PluginItemProps = {
31
33
  installing?: readonly string[];
32
34
  enabled?: readonly string[];
33
35
  /**
34
- * Derived tags (e.g. `community`, `local`) to display alongside the plugin's own meta.tags.
36
+ * Derived tags (e.g. `registry`, `local`) to display alongside the plugin's own meta.tags.
35
37
  * Not persisted to plugin meta; computed per-render by the container.
36
38
  */
37
39
  extraTags?: readonly string[];
@@ -42,8 +44,22 @@ export type PluginItemProps = {
42
44
  * is rendered in place of the enable switch.
43
45
  */
44
46
  onInstall?: (id: string) => void;
47
+ /**
48
+ * When true and plugin is installed, shows an Update button instead of the enable switch.
49
+ */
50
+ hasUpdate?: boolean;
51
+ /** Called when the user clicks the Update button. */
52
+ onUpdate?: (id: string) => void;
53
+ /** Ids of plugins whose update is currently in flight. */
54
+ updating?: readonly string[];
45
55
  hasSettings?: (id: string) => boolean;
46
56
  onSettings?: (id: string) => void;
57
+ /**
58
+ * Failure record for this plugin, if any. When present a warning badge is
59
+ * rendered next to the plugin name; clicking it opens a popover with the
60
+ * phase, reason, and error message.
61
+ */
62
+ failure?: PluginManager.PluginFailure;
47
63
  };
48
64
 
49
65
  export const PluginItem = ({
@@ -55,8 +71,12 @@ export const PluginItem = ({
55
71
  onClick,
56
72
  onChange,
57
73
  onInstall,
74
+ hasUpdate,
75
+ onUpdate,
76
+ updating,
58
77
  hasSettings: hasSettingsProp,
59
78
  onSettings,
79
+ failure,
60
80
  }: PluginItemProps) => {
61
81
  const { t } = useTranslation(meta.id);
62
82
  const { id, name, description, tags, icon = 'ph--circle--regular', iconHue = 'neutral' } = plugin.meta;
@@ -73,7 +93,9 @@ export const PluginItem = ({
73
93
  const isEnabled = enabled.includes(id);
74
94
  const isInstalled = installed ? installed.includes(id) : true;
75
95
  const isInstalling = installing?.includes(id) ?? false;
96
+ const isUpdating = updating?.includes(id) ?? false;
76
97
  const showInstallButton = !!onInstall && !isInstalled;
98
+ const showUpdateButton = !!onUpdate && isInstalled && !!hasUpdate;
77
99
  const inputId = `${id}-input`;
78
100
  const labelId = `${id}-label`;
79
101
  const descriptionId = `${id}-description`;
@@ -95,6 +117,14 @@ export const PluginItem = ({
95
117
  [id, onInstall],
96
118
  );
97
119
 
120
+ const handleUpdate = useCallback(
121
+ (event: MouseEvent) => {
122
+ event.stopPropagation();
123
+ onUpdate?.(id);
124
+ },
125
+ [id, onUpdate],
126
+ );
127
+
98
128
  const hasSettings = hasSettingsProp?.(id) ?? false;
99
129
  const handleSettings = useCallback(() => onSettings?.(id), [id, onSettings]);
100
130
  const styles = getStyles(iconHue);
@@ -111,7 +141,7 @@ export const PluginItem = ({
111
141
  >
112
142
  <div className={mx(gridRows, 'justify-center rounded-l-md', styles.surface)}>
113
143
  <Icon
114
- classNames={mx('row-start-2 cursor-pointer', styles.surfaceText)}
144
+ classNames={mx('row-start-2 cursor-pointer', styles.foreground)}
115
145
  icon={icon}
116
146
  size={14}
117
147
  onClick={handleClick}
@@ -119,8 +149,9 @@ export const PluginItem = ({
119
149
  </div>
120
150
 
121
151
  <div className={mx(gridRows)}>
122
- <div className='flex items-center overflow-hidden cursor-pointer' onClick={handleClick}>
152
+ <div className='flex items-center gap-2 overflow-hidden cursor-pointer' onClick={handleClick}>
123
153
  <span className='text-lg truncate'>{name ?? id}</span>
154
+ {failure && <PluginFailureBadge failure={failure} />}
124
155
  </div>
125
156
 
126
157
  <div>
@@ -140,7 +171,7 @@ export const PluginItem = ({
140
171
  aria-describedby={descriptionId}
141
172
  classNames='cursor-pointer'
142
173
  icon='ph--gear--regular'
143
- label={t('settings.label')}
174
+ label={t('plugin-settings.label')}
144
175
  iconOnly
145
176
  size={4}
146
177
  onClick={handleSettings}
@@ -153,7 +184,15 @@ export const PluginItem = ({
153
184
 
154
185
  <div className='grow' />
155
186
  <div className='pe-1'>
156
- {showInstallButton ? (
187
+ {isUpdating ? (
188
+ <Button aria-describedby={descriptionId} density='fine' variant='primary' disabled>
189
+ {t('updating.label')}
190
+ </Button>
191
+ ) : showUpdateButton ? (
192
+ <Button aria-describedby={descriptionId} density='fine' variant='primary' onClick={handleUpdate}>
193
+ {t('update.label')}
194
+ </Button>
195
+ ) : showInstallButton ? (
157
196
  <Button
158
197
  aria-describedby={descriptionId}
159
198
  density='fine'
@@ -182,6 +221,6 @@ const tagColors: Record<RegistryTagType, ChromaticPalette | NeutralPalette> = {
182
221
  popular: 'green',
183
222
  featured: 'pink',
184
223
  experimental: 'amber',
185
- community: 'indigo',
224
+ registry: 'indigo',
186
225
  local: 'neutral',
187
226
  };
@@ -11,9 +11,9 @@ import { ScrollArea } from '@dxos/react-ui';
11
11
  import { withLayout, withTheme } from '@dxos/react-ui/testing';
12
12
  import { getHashHue } from '@dxos/ui-theme';
13
13
 
14
+ import { translations } from '#translations';
14
15
  import { RegistryTagType } from '#types';
15
16
 
16
- import { translations } from '../../translations';
17
17
  import { PluginList } from './PluginList';
18
18
 
19
19
  random.seed(1);
@@ -4,25 +4,48 @@
4
4
 
5
5
  import React from 'react';
6
6
 
7
- import { type Plugin } from '@dxos/app-framework';
7
+ import { type Plugin, type PluginManager } from '@dxos/app-framework';
8
8
  import { List } from '@dxos/react-ui';
9
9
 
10
10
  import { PluginItem, type PluginItemProps } from './PluginItem';
11
11
 
12
- export type PluginListProps = Omit<PluginItemProps, 'plugin' | 'extraTags'> & {
12
+ export type PluginListProps = Omit<PluginItemProps, 'plugin' | 'extraTags' | 'hasUpdate' | 'failure'> & {
13
13
  plugins?: readonly Plugin.Plugin[];
14
14
  /**
15
- * Map from plugin id → extra tags to display (e.g. `community`, `local`).
15
+ * Map from plugin id → extra tags to display (e.g. `registry`, `local`).
16
16
  * Computed by the container; not persisted to plugin meta.
17
17
  */
18
18
  extraTagsById?: Record<string, readonly string[]>;
19
+ /**
20
+ * Set of plugin ids for which a newer version is available in the catalog.
21
+ * Causes each matching item to render an Update button instead of the enable switch.
22
+ */
23
+ updateAvailableIds?: ReadonlySet<string>;
24
+ /**
25
+ * Map from plugin id → most recent failure record. Used to render a warning
26
+ * badge next to the plugin name. Sourced from `PluginManager.failed`.
27
+ */
28
+ failuresById?: Record<string, PluginManager.PluginFailure>;
19
29
  };
20
30
 
21
- export const PluginList = ({ plugins = [], extraTagsById, ...props }: PluginListProps) => {
31
+ export const PluginList = ({
32
+ plugins = [],
33
+ extraTagsById,
34
+ updateAvailableIds,
35
+ failuresById,
36
+ ...props
37
+ }: PluginListProps) => {
22
38
  return (
23
39
  <List classNames='grid grid-cols-[repeat(auto-fill,minmax(18rem,1fr))] auto-rows-[max-content] gap-4 p-4'>
24
40
  {plugins.map((plugin) => (
25
- <PluginItem key={plugin.meta.id} plugin={plugin} extraTags={extraTagsById?.[plugin.meta.id]} {...props} />
41
+ <PluginItem
42
+ key={plugin.meta.id}
43
+ plugin={plugin}
44
+ extraTags={extraTagsById?.[plugin.meta.id]}
45
+ hasUpdate={updateAvailableIds?.has(plugin.meta.id)}
46
+ failure={failuresById?.[plugin.meta.id]}
47
+ {...props}
48
+ />
26
49
  ))}
27
50
  </List>
28
51
  );
@@ -0,0 +1,120 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ import React, { useCallback, useState } from 'react';
6
+
7
+ import { type AppSurface } from '@dxos/app-toolkit/ui';
8
+ import { log } from '@dxos/log';
9
+ import { Button, Input, Message, useTranslation } from '@dxos/react-ui';
10
+ import { Settings as SettingsForm } from '@dxos/react-ui-form';
11
+
12
+ import { meta } from '#meta';
13
+
14
+ import { type RegistrySettings as RegistrySettingsType } from '../../types';
15
+
16
+ export type RegistrySettingsProps = AppSurface.SettingsArticleProps<
17
+ RegistrySettingsType,
18
+ {
19
+ activeDevPluginIds: readonly string[];
20
+ onEnableDev: (url: string) => Promise<void>;
21
+ onDisableDev: (id: string) => Promise<void>;
22
+ }
23
+ >;
24
+
25
+ /**
26
+ * Settings panel for `@dxos/plugin-registry`. The dev-plugin section lets a
27
+ * plugin author point Composer at a local Vite dev server (defaults to
28
+ * `localhost:3967`) and toggle the dev plugin on/off persistently — boot will
29
+ * re-attach it on the next reload, surviving HMR-triggered reloads. If the
30
+ * dev server is offline at boot, the toggle stays on and a warning is logged
31
+ * (the manager's `failed` atom also surfaces a badge on the plugin list).
32
+ */
33
+ export const RegistrySettings = ({
34
+ settings,
35
+ onSettingsChange,
36
+ activeDevPluginIds,
37
+ onEnableDev,
38
+ onDisableDev,
39
+ }: RegistrySettingsProps) => {
40
+ const { t } = useTranslation(meta.id);
41
+ const [busy, setBusy] = useState(false);
42
+ const enabled = !!settings.devPluginEnabled;
43
+ const url = settings.devPluginUrl ?? '';
44
+ const trimmedUrl = url.trim();
45
+ const loadedDevId = activeDevPluginIds[0];
46
+
47
+ const handleToggle = useCallback(async () => {
48
+ if (busy || !onSettingsChange) {
49
+ return;
50
+ }
51
+ setBusy(true);
52
+ try {
53
+ if (enabled && loadedDevId) {
54
+ await onDisableDev(loadedDevId);
55
+ onSettingsChange((current) => ({ ...current, devPluginEnabled: false }));
56
+ } else if (enabled) {
57
+ // Toggle is on but no dev plugin is currently loaded (boot-time load
58
+ // failed or hasn't finished). Treat the click as "turn it off."
59
+ onSettingsChange((current) => ({ ...current, devPluginEnabled: false }));
60
+ } else {
61
+ if (!trimmedUrl) {
62
+ return;
63
+ }
64
+ // Optimistically persist intent so the next reload retries even if
65
+ // the page closes mid-load.
66
+ onSettingsChange((current) => ({ ...current, devPluginEnabled: true }));
67
+ try {
68
+ await onEnableDev(trimmedUrl);
69
+ } catch (error) {
70
+ // Per product direction: leave the intent on, log the failure. The
71
+ // user explicitly turns it off if they're done iterating.
72
+ log.warn('dev plugin enable failed', { url: trimmedUrl, error });
73
+ }
74
+ }
75
+ } finally {
76
+ setBusy(false);
77
+ }
78
+ }, [busy, enabled, loadedDevId, onSettingsChange, onDisableDev, onEnableDev, trimmedUrl]);
79
+
80
+ // Button label tracks the persisted intent, not the current load state, so
81
+ // a transient "intent on but not loaded" combination still reads as
82
+ // "Disable Dev Plugin" — clicking it turns the intent off cleanly.
83
+ const buttonLabel = busy
84
+ ? t('dev-plugin.busy.label')
85
+ : enabled
86
+ ? t('dev-plugin.disable.label')
87
+ : t('dev-plugin.enable.label');
88
+
89
+ return (
90
+ <SettingsForm.Viewport>
91
+ <SettingsForm.Section title={t('dev-plugin.section.title')}>
92
+ <Message.Root valence='neutral'>
93
+ <Message.Content>{t('dev-plugin.description')}</Message.Content>
94
+ </Message.Root>
95
+ <SettingsForm.Item title={t('dev-plugin.url.label')} description={t('dev-plugin.url.description')}>
96
+ <Input.TextInput
97
+ classNames='ms-2'
98
+ disabled={!onSettingsChange || enabled || busy}
99
+ value={url}
100
+ onChange={(event) => onSettingsChange?.((current) => ({ ...current, devPluginUrl: event.target.value }))}
101
+ />
102
+ </SettingsForm.Item>
103
+ <SettingsForm.Item title={t('dev-plugin.toggle.label')} description={t('dev-plugin.toggle.description')}>
104
+ <Button
105
+ variant={enabled ? undefined : 'primary'}
106
+ disabled={busy || (!enabled && !trimmedUrl)}
107
+ onClick={() => void handleToggle()}
108
+ >
109
+ {buttonLabel}
110
+ </Button>
111
+ </SettingsForm.Item>
112
+ {enabled && !loadedDevId && !busy && (
113
+ <Message.Root valence='warning'>
114
+ <Message.Content>{t('dev-plugin.not-loaded.message')}</Message.Content>
115
+ </Message.Root>
116
+ )}
117
+ </SettingsForm.Section>
118
+ </SettingsForm.Viewport>
119
+ );
120
+ };
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2026 DXOS.org
3
+ //
4
+
5
+ export * from './RegistrySettings';
@@ -3,4 +3,6 @@
3
3
  //
4
4
 
5
5
  export * from './PluginDetail';
6
+ export * from './PluginFailureBadge';
6
7
  export * from './PluginList';
8
+ export * from './RegistrySettings';