@dxos/app-framework 0.11.1 → 1.0.0-next-bbabc02363f18d7643ea4160e3a7a7809870fd0d

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 (212) hide show
  1. package/API.md +275 -0
  2. package/ARCHITECTURE.md +190 -0
  3. package/CHANGELOG.md +73 -0
  4. package/README.md +3 -0
  5. package/dist/lib/chunk-SurfaceManager.mjs +56 -7
  6. package/dist/lib/chunk-SurfaceManager.mjs.map +1 -1
  7. package/dist/lib/chunk-capability.mjs +372 -10
  8. package/dist/lib/chunk-capability.mjs.map +1 -1
  9. package/dist/lib/chunk-capability2.mjs +15 -0
  10. package/dist/lib/chunk-capability2.mjs.map +1 -0
  11. package/dist/lib/chunk-devtools.mjs +108 -0
  12. package/dist/lib/chunk-devtools.mjs.map +1 -0
  13. package/dist/lib/chunk-history-tracker.mjs +2 -2
  14. package/dist/lib/chunk-history-tracker.mjs.map +1 -1
  15. package/dist/lib/chunk-plugin-manager.mjs +2294 -953
  16. package/dist/lib/chunk-plugin-manager.mjs.map +1 -1
  17. package/dist/lib/chunk-plugin-manifest.mjs +1 -1
  18. package/dist/lib/chunk-process-manager-capability.mjs +42 -18
  19. package/dist/lib/chunk-process-manager-capability.mjs.map +1 -1
  20. package/dist/lib/chunk-url-loader.mjs +1 -1
  21. package/dist/lib/cli.mjs +4 -3
  22. package/dist/lib/cli.mjs.map +1 -1
  23. package/dist/lib/common/activation-events.mjs +32 -19
  24. package/dist/lib/common/activation-events.mjs.map +1 -1
  25. package/dist/lib/common/capabilities.mjs +21 -27
  26. package/dist/lib/common/capabilities.mjs.map +1 -1
  27. package/dist/lib/core/activation-event.mjs +33 -2
  28. package/dist/lib/core/activation-event.mjs.map +1 -1
  29. package/dist/lib/core/capability.mjs +2 -189
  30. package/dist/lib/core/plugin-manager.mjs +14 -2
  31. package/dist/lib/core/plugin-manager.mjs.map +1 -0
  32. package/dist/lib/core/plugin.mjs +59 -57
  33. package/dist/lib/core/plugin.mjs.map +1 -1
  34. package/dist/lib/index.mjs +52 -40
  35. package/dist/lib/index.mjs.map +1 -1
  36. package/dist/lib/testing.mjs +87 -23
  37. package/dist/lib/testing.mjs.map +1 -1
  38. package/dist/lib/ui.mjs +67 -44
  39. package/dist/lib/ui.mjs.map +1 -1
  40. package/dist/lib/vite-plugin.mjs +31 -22
  41. package/dist/lib/vite-plugin.mjs.map +1 -1
  42. package/dist/plugin/node-esm/index.mjs +88 -55
  43. package/dist/plugin/node-esm/index.mjs.map +4 -4
  44. package/dist/plugin/node-esm/meta.json +1 -1
  45. package/dist/types/src/common/activation-events.d.ts +25 -12
  46. package/dist/types/src/common/activation-events.d.ts.map +1 -1
  47. package/dist/types/src/common/capabilities.d.ts +27 -27
  48. package/dist/types/src/common/capabilities.d.ts.map +1 -1
  49. package/dist/types/src/core/activation-event.d.ts +28 -0
  50. package/dist/types/src/core/activation-event.d.ts.map +1 -1
  51. package/dist/types/src/core/capability-manager.d.ts +27 -1
  52. package/dist/types/src/core/capability-manager.d.ts.map +1 -1
  53. package/dist/types/src/core/capability.d.ts +277 -39
  54. package/dist/types/src/core/capability.d.ts.map +1 -1
  55. package/dist/types/src/core/capability.test.d.ts +2 -0
  56. package/dist/types/src/core/capability.test.d.ts.map +1 -0
  57. package/dist/types/src/core/errors.d.ts +230 -0
  58. package/dist/types/src/core/errors.d.ts.map +1 -0
  59. package/dist/types/src/core/index.d.ts +1 -0
  60. package/dist/types/src/core/index.d.ts.map +1 -1
  61. package/dist/types/src/core/plugin-manager/activation-graph.d.ts +82 -0
  62. package/dist/types/src/core/plugin-manager/activation-graph.d.ts.map +1 -0
  63. package/dist/types/src/core/plugin-manager/activation-graph.test.d.ts +2 -0
  64. package/dist/types/src/core/plugin-manager/activation-graph.test.d.ts.map +1 -0
  65. package/dist/types/src/core/plugin-manager/activation-scheduler.d.ts +98 -0
  66. package/dist/types/src/core/plugin-manager/activation-scheduler.d.ts.map +1 -0
  67. package/dist/types/src/core/plugin-manager/idle.d.ts +11 -0
  68. package/dist/types/src/core/plugin-manager/idle.d.ts.map +1 -0
  69. package/dist/types/src/core/plugin-manager/index.d.ts +2 -0
  70. package/dist/types/src/core/plugin-manager/index.d.ts.map +1 -0
  71. package/dist/types/src/core/plugin-manager/manager-state.d.ts +129 -0
  72. package/dist/types/src/core/plugin-manager/manager-state.d.ts.map +1 -0
  73. package/dist/types/src/core/plugin-manager/manager-types.d.ts +104 -0
  74. package/dist/types/src/core/plugin-manager/manager-types.d.ts.map +1 -0
  75. package/dist/types/src/core/plugin-manager/module-loader.d.ts +74 -0
  76. package/dist/types/src/core/plugin-manager/module-loader.d.ts.map +1 -0
  77. package/dist/types/src/core/plugin-manager/plugin-catalog.d.ts +74 -0
  78. package/dist/types/src/core/plugin-manager/plugin-catalog.d.ts.map +1 -0
  79. package/dist/types/src/core/{plugin-manager.d.ts → plugin-manager/plugin-manager.d.ts} +15 -102
  80. package/dist/types/src/core/plugin-manager/plugin-manager.d.ts.map +1 -0
  81. package/dist/types/src/core/plugin-manager/plugin-manager.test.d.ts.map +1 -0
  82. package/dist/types/src/core/plugin.d.ts +126 -51
  83. package/dist/types/src/core/plugin.d.ts.map +1 -1
  84. package/dist/types/src/core/plugin.test.d.ts +2 -0
  85. package/dist/types/src/core/plugin.test.d.ts.map +1 -0
  86. package/dist/types/src/devtools.d.ts +60 -0
  87. package/dist/types/src/devtools.d.ts.map +1 -0
  88. package/dist/types/src/index.d.ts +1 -0
  89. package/dist/types/src/index.d.ts.map +1 -1
  90. package/dist/types/src/plugin-process-manager/ProcessManagerPlugin.d.ts.map +1 -1
  91. package/dist/types/src/plugin-process-manager/history/capability.d.ts +1 -1
  92. package/dist/types/src/plugin-process-manager/history/types.d.ts +1 -1
  93. package/dist/types/src/plugin-process-manager/history/types.d.ts.map +1 -1
  94. package/dist/types/src/plugin-process-manager/history/undo-mapping.d.ts +1 -1
  95. package/dist/types/src/plugin-process-manager/history/undo-mapping.d.ts.map +1 -1
  96. package/dist/types/src/plugin-process-manager/history/undo-registry.d.ts +1 -1
  97. package/dist/types/src/plugin-process-manager/history/undo-registry.d.ts.map +1 -1
  98. package/dist/types/src/plugin-process-manager/process-manager-capability.d.ts +1 -1
  99. package/dist/types/src/plugin-process-manager/process-manager-capability.d.ts.map +1 -1
  100. package/dist/types/src/plugin-process-manager/process-manager-capability.test.d.ts +2 -0
  101. package/dist/types/src/plugin-process-manager/process-manager-capability.test.d.ts.map +1 -0
  102. package/dist/types/src/plugin-process-manager/testing.d.ts +1 -1
  103. package/dist/types/src/plugin-process-manager/testing.d.ts.map +1 -1
  104. package/dist/types/src/testing/demand-gated.d.ts +26 -0
  105. package/dist/types/src/testing/demand-gated.d.ts.map +1 -0
  106. package/dist/types/src/testing/harness.d.ts +14 -3
  107. package/dist/types/src/testing/harness.d.ts.map +1 -1
  108. package/dist/types/src/testing/index.d.ts +1 -0
  109. package/dist/types/src/testing/index.d.ts.map +1 -1
  110. package/dist/types/src/testing/operationCapture.d.ts +2 -2
  111. package/dist/types/src/testing/operationCapture.d.ts.map +1 -1
  112. package/dist/types/src/testing/withPluginManager.d.ts +8 -3
  113. package/dist/types/src/testing/withPluginManager.d.ts.map +1 -1
  114. package/dist/types/src/testing/withPluginManager.stories.d.ts +1 -1
  115. package/dist/types/src/testing/withSurfaceDebug.d.ts +1 -1
  116. package/dist/types/src/testing/withSurfaceDebug.d.ts.map +1 -1
  117. package/dist/types/src/ui/components/App/App.d.ts.map +1 -1
  118. package/dist/types/src/ui/components/App/App.stories.d.ts +1 -1
  119. package/dist/types/src/ui/components/HomeSection/HomeSection.stories.d.ts +1 -1
  120. package/dist/types/src/ui/components/PluginManager/PluginManagerContext.stories.d.ts +1 -1
  121. package/dist/types/src/ui/components/PluginManager/PluginManagerProvider.d.ts +2 -3
  122. package/dist/types/src/ui/components/PluginManager/PluginManagerProvider.d.ts.map +1 -1
  123. package/dist/types/src/ui/components/Surface/SurfaceComponent.d.ts.map +1 -1
  124. package/dist/types/src/ui/components/Surface/SurfaceComponent.stories.d.ts +1 -1
  125. package/dist/types/src/ui/components/Surface/SurfaceManager.d.ts +13 -0
  126. package/dist/types/src/ui/components/Surface/SurfaceManager.d.ts.map +1 -1
  127. package/dist/types/src/ui/hooks/index.d.ts +1 -0
  128. package/dist/types/src/ui/hooks/index.d.ts.map +1 -1
  129. package/dist/types/src/ui/hooks/useActivationSignal.d.ts +9 -0
  130. package/dist/types/src/ui/hooks/useActivationSignal.d.ts.map +1 -0
  131. package/dist/types/src/ui/hooks/useApp.d.ts +0 -1
  132. package/dist/types/src/ui/hooks/useApp.d.ts.map +1 -1
  133. package/dist/types/src/ui/hooks/useCapabilities.d.ts +19 -5
  134. package/dist/types/src/ui/hooks/useCapabilities.d.ts.map +1 -1
  135. package/dist/types/src/ui/hooks/useProcessManagerRuntime.d.ts.map +1 -1
  136. package/dist/types/src/vite-plugin/load.d.ts +7 -5
  137. package/dist/types/src/vite-plugin/load.d.ts.map +1 -1
  138. package/dist/types/src/vite-plugin/packages.d.ts.map +1 -1
  139. package/dist/types/tsconfig.tsbuildinfo +1 -1
  140. package/docs/activation-sequence.md +138 -0
  141. package/moon.yml +1 -0
  142. package/package.json +30 -33
  143. package/src/cli/cli.ts +6 -6
  144. package/src/common/activation-events.ts +27 -21
  145. package/src/common/capabilities.ts +39 -33
  146. package/src/core/activation-event.ts +31 -0
  147. package/src/core/capability-manager.test.ts +50 -12
  148. package/src/core/capability-manager.ts +107 -6
  149. package/src/core/capability.test.ts +157 -0
  150. package/src/core/capability.ts +488 -80
  151. package/src/core/errors.ts +82 -0
  152. package/src/core/index.ts +1 -0
  153. package/src/core/plugin-manager/activation-graph.test.ts +177 -0
  154. package/src/core/plugin-manager/activation-graph.ts +334 -0
  155. package/src/core/plugin-manager/activation-scheduler.ts +972 -0
  156. package/src/core/plugin-manager/idle.ts +64 -0
  157. package/src/core/plugin-manager/index.ts +7 -0
  158. package/src/core/plugin-manager/manager-state.ts +366 -0
  159. package/src/core/plugin-manager/manager-types.ts +68 -0
  160. package/src/core/plugin-manager/module-loader.ts +552 -0
  161. package/src/core/plugin-manager/plugin-catalog.ts +661 -0
  162. package/src/core/{plugin-manager.test.ts → plugin-manager/plugin-manager.test.ts} +1407 -99
  163. package/src/core/plugin-manager/plugin-manager.ts +591 -0
  164. package/src/core/plugin.test.ts +349 -0
  165. package/src/core/plugin.ts +213 -78
  166. package/src/devtools.ts +209 -0
  167. package/src/globals.d.ts +16 -0
  168. package/src/index.ts +1 -0
  169. package/src/plugin-process-manager/ProcessManagerPlugin.ts +36 -13
  170. package/src/plugin-process-manager/history/capability.ts +6 -8
  171. package/src/plugin-process-manager/history/history-tracker.test.ts +1 -1
  172. package/src/plugin-process-manager/history/types.ts +1 -1
  173. package/src/plugin-process-manager/history/undo-mapping.ts +1 -1
  174. package/src/plugin-process-manager/history/undo-registry.ts +1 -1
  175. package/src/plugin-process-manager/process-manager-capability.test.ts +85 -0
  176. package/src/plugin-process-manager/process-manager-capability.ts +54 -25
  177. package/src/plugin-process-manager/testing.ts +1 -1
  178. package/src/testing/demand-gated.ts +54 -0
  179. package/src/testing/harness.ts +19 -4
  180. package/src/testing/index.ts +1 -0
  181. package/src/testing/operationCapture.ts +3 -3
  182. package/src/testing/react.test.tsx +5 -5
  183. package/src/testing/withPluginManager.stories.tsx +1 -1
  184. package/src/testing/withPluginManager.tsx +70 -21
  185. package/src/testing/withSurfaceDebug.tsx +1 -1
  186. package/src/ui/components/App/App.stories.tsx +3 -3
  187. package/src/ui/components/App/App.tsx +21 -8
  188. package/src/ui/components/PluginManager/PluginManagerContext.stories.tsx +5 -5
  189. package/src/ui/components/PluginManager/PluginManagerProvider.ts +2 -3
  190. package/src/ui/components/Surface/SurfaceComponent.test.tsx +66 -17
  191. package/src/ui/components/Surface/SurfaceComponent.tsx +47 -3
  192. package/src/ui/components/Surface/SurfaceManager.ts +27 -0
  193. package/src/ui/hooks/index.ts +1 -0
  194. package/src/ui/hooks/useActivationSignal.ts +26 -0
  195. package/src/ui/hooks/useApp.test.tsx +7 -4
  196. package/src/ui/hooks/useApp.tsx +2 -8
  197. package/src/ui/hooks/useCapabilities.ts +30 -17
  198. package/src/ui/hooks/useProcessManagerRuntime.ts +2 -1
  199. package/src/vite-plugin/boot-loader/loader-app/entry.tsx +6 -0
  200. package/src/vite-plugin/load.ts +30 -28
  201. package/src/vite-plugin/packages.ts +0 -1
  202. package/tsconfig.json +3 -12
  203. package/vite.config.ts +1 -1
  204. package/dist/lib/core/capability.mjs.map +0 -1
  205. package/dist/types/src/common/annotations.d.ts +0 -1
  206. package/dist/types/src/common/annotations.d.ts.map +0 -1
  207. package/dist/types/src/core/plugin-manager.d.ts.map +0 -1
  208. package/dist/types/src/core/plugin-manager.test.d.ts.map +0 -1
  209. package/src/common/annotations.ts +0 -3
  210. package/src/core/plugin-manager.ts +0 -1719
  211. package/src/vite-plugin/boot-loader/BootLoader.solid-stories.tsx +0 -198
  212. /package/dist/types/src/core/{plugin-manager.test.d.ts → plugin-manager/plugin-manager.test.d.ts} +0 -0
package/API.md ADDED
@@ -0,0 +1,275 @@
1
+ # DXOS App Framework API
2
+
3
+ Companion to [ARCHITECTURE.md](./ARCHITECTURE.md), which covers how the pieces relate. This is the
4
+ API surface.
5
+
6
+ ## Imports
7
+
8
+ ```ts
9
+ import {
10
+ ActivationEvent, // Runtime activation events (make/oneOf/allOf)
11
+ ActivationEvents, // Well-known framework events (Startup, ...)
12
+ Capabilities, // Well-known capability tags (ReactSurface, OperationHandler, ...)
13
+ Capability, // Capability tags, contributions, module authoring
14
+ Plugin, // Plugin definition + builder
15
+ PluginManager, // Manager type (usually obtained, not constructed)
16
+ } from '@dxos/app-framework';
17
+ import { Surface, useApp, useCapabilities, useCapability } from '@dxos/app-framework/ui';
18
+ import { withPluginManager } from '@dxos/app-framework/testing';
19
+ ```
20
+
21
+ ---
22
+
23
+ ## Defining a capability
24
+
25
+ A capability is a typed, NSID-keyed slot. **Arity is chosen at definition** and decides what
26
+ consumers get.
27
+
28
+ ```ts
29
+ // Multi (the default): an open registry many modules contribute to.
30
+ export const Template = Capability.make<Template>()('org.dxos.plugin.projects.capability.template');
31
+
32
+ // Singleton: exactly one provider.
33
+ export const Client = Capability.makeSingleton<ClientApi>()('org.dxos.plugin.client.capability.client');
34
+ ```
35
+
36
+ Both are curried — `make<T>()(nsid)` — so the NSID string literal is captured and brands the
37
+ identifier. NSIDs must be camelCase (`DXN.Name`). The single-call form `make<T>(nsid)` still compiles
38
+ but widens the NSID to `string`, making the compile-time check vacuous; prefer the curried form.
39
+
40
+ | | `makeSingleton` → `Tag<T>` | `make` → `MultiTag<T>` |
41
+ | ------------------ | ------------------------------------ | ----------------------------------------- |
42
+ | `yield* tag` gives | `T` | `Contributions<T>` (live view) |
43
+ | Requiring it | gates activation on its one provider | never gates — the collection may be empty |
44
+ | Two providers | `DuplicateProviderError` | normal |
45
+
46
+ ---
47
+
48
+ ## Authoring a module
49
+
50
+ A module is a body plus a declaration of what it `requires` and `provides`.
51
+
52
+ ```ts
53
+ // Code-split (the common case) — the chunk loads when the module activates.
54
+ export const Coordinator = Capability.lazyModule(
55
+ 'ConnectorCoordinator',
56
+ { requires: [ClientCapabilities.Client], provides: [ConnectorCoordinator] },
57
+ () => import('./connector-coordinator'),
58
+ );
59
+
60
+ // Eager — body defined inline, no separate chunk.
61
+ export const Extractor = Capability.inlineModule(
62
+ 'SummarizeExtractor',
63
+ { provides: [InboxCapabilities.ObjectExtractor] },
64
+ () => Effect.succeed(Capability.contribute(InboxCapabilities.ObjectExtractor, extractor)),
65
+ );
66
+ ```
67
+
68
+ The body is written with `Capability.makeModule` and yields its declared requirements:
69
+
70
+ ```ts
71
+ export default Capability.makeModule(
72
+ Effect.fnUntraced(function* () {
73
+ const client = yield* ClientCapabilities.Client; // declared in `requires`
74
+ const coordinator = new Coordinator(client);
75
+ return Capability.contribute(ConnectorCoordinator, coordinator);
76
+ }),
77
+ );
78
+ ```
79
+
80
+ Yielding a tag that is not in `requires` is a type error (it escapes the `R` channel); failing to
81
+ contribute a declared `provides` is also a type error.
82
+
83
+ **Makers.** Capability owners export a maker so call sites don't repeat `provides`.
84
+ `@dxos/app-toolkit` ships them for the common capabilities:
85
+
86
+ ```ts
87
+ export const ReactSurface = AppCapability.surface(() => import('./react-surface'));
88
+ export const AppGraphBuilder = AppCapability.appGraphBuilder(() => import('./app-graph-builder'), {
89
+ requires: [SomeCapabilities.Thing],
90
+ });
91
+ ```
92
+
93
+ Build your own with `Capability.moduleMaker(name, tag)`.
94
+
95
+ ---
96
+
97
+ ## Composing a plugin
98
+
99
+ ```ts
100
+ export const FooPlugin = Plugin.define(meta).pipe(
101
+ Plugin.addModule(ReactSurface),
102
+ Plugin.addModule(Coordinator),
103
+ Plugin.addModule(AppCapability.translations(translations)),
104
+ Plugin.addModule(AppCapability.schema([Foo.Foo])),
105
+ Plugin.make,
106
+ );
107
+ ```
108
+
109
+ `Plugin.define(meta)` starts a builder; each `addModule` adds one module; `Plugin.make` seals it.
110
+ When the plugin takes options, `addModule` also accepts `(options) => module` and makers accept a
111
+ `props` mapping from plugin options to body props. If every option is optional, calling
112
+ `FooPlugin()` with no argument is allowed.
113
+
114
+ ---
115
+
116
+ ## Consuming capabilities
117
+
118
+ Three access paths, for three situations.
119
+
120
+ **1. Inside a module body** — declared, type-checked, ordering-aware.
121
+
122
+ ```ts
123
+ Effect.fnUntraced(function* () {
124
+ const client = yield* ClientCapabilities.Client; // singleton → T
125
+ const templates = yield* ProjectCapabilities.Template; // multi → Contributions<T>
126
+ });
127
+ ```
128
+
129
+ **2. Outside a module** (operations, callbacks, non-module code) — dynamic, string-keyed.
130
+
131
+ ```ts
132
+ Effect.gen(function* () {
133
+ const handlers = yield* Capability.getAll(Capabilities.OperationHandler);
134
+ const maybe = yield* Capability.getOption(AppCapabilities.ProgressRegistry);
135
+ const surfaces = yield* Capability.atom(Capabilities.ReactSurface); // reactive
136
+ const client = yield* Capability.waitFor(ClientCapabilities.Client); // resolves when contributed
137
+ });
138
+ ```
139
+
140
+ **3. React.**
141
+
142
+ ```tsx
143
+ const surfaces = useCapabilities(Capabilities.ReactSurface);
144
+ const client = useCapability(ClientCapabilities.Client);
145
+ ```
146
+
147
+ > In graph-extension atom callbacks always use `Capability.atom`, never a synchronous
148
+ > `Capability.get` — a sync read inside an atom body caches a defect fallback with no reactive
149
+ > dependency.
150
+
151
+ ---
152
+
153
+ ## Contributing values
154
+
155
+ A module returns its contributions from `activate`. `contribute` carries one value, `contributeAll`
156
+ carries several for the same capability; both produce a single `Contribution`.
157
+
158
+ ```ts
159
+ return [
160
+ Capability.contribute(Capabilities.ReactSurface, surface),
161
+ Capability.contributeAll(Capabilities.LayerSpec, [clientSpec, databaseSpec]),
162
+ ];
163
+ ```
164
+
165
+ A module providing exactly one capability may return the `Contribution` directly, without the array.
166
+ An optional third argument is a `deactivate` hook, run once when the module is deactivated.
167
+
168
+ The return is checked against the module's declared `provides`: omitting a declared capability is a
169
+ type error, and contributing an undeclared one fails at runtime.
170
+
171
+ ## Reading a multi capability
172
+
173
+ A multi capability has no single value, and its membership changes as plugins are enabled and
174
+ disabled. Reading one therefore gives a live `Contributions<T>` view rather than an array:
175
+
176
+ ```ts
177
+ Effect.fnUntraced(function* () {
178
+ const templates = yield* ProjectCapabilities.Template; // Contributions<Template>
179
+
180
+ templates.get(); // readonly Template[] — snapshot as of now
181
+ templates.subscribe((values) => setTemplates(values)); // → unsubscribe
182
+ templates.atom; // Atom<Template[]> for Atom-based composition
183
+ });
184
+ ```
185
+
186
+ `get()` is a point-in-time snapshot: hold onto it and it will not reflect capabilities contributed
187
+ later. Consume the collection reactively — via `subscribe`, `atom`, or the React hooks — unless the
188
+ value is used immediately and discarded.
189
+
190
+ ### `Contribution` vs `Contributions`
191
+
192
+ Two types one letter apart, on either side of the registry:
193
+
194
+ | | `Contribution` | `Contributions<T>` |
195
+ | ----------- | ----------------------------------------- | ------------------------------------------------------------ |
196
+ | Direction | what a module **produces** | what a consumer **reads** |
197
+ | Produced by | `Capability.contribute` / `contributeAll` | `yield*` on a `MultiTag`, or `Capability.contributions(tag)` |
198
+ | Shape | `{ capability, values, deactivate? }` | `{ atom, get(), subscribe() }` |
199
+ | Scope | one module's values for one capability | every module's values for one capability |
200
+
201
+ ---
202
+
203
+ ## Activation
204
+
205
+ A module activates in one of two modes, normalized onto `PluginModule.activation`:
206
+
207
+ - **Dependency mode** (the default) — declared `requires`/`provides`. The manager topologically
208
+ orders modules so providers run before consumers. A module with no `requires` is a startup root.
209
+ - **Event mode** — `activatesOn` a genuine runtime event; `requires` are pulled on demand when it
210
+ fires.
211
+
212
+ ```ts
213
+ Capability.lazyModule(
214
+ 'OnSpaceCreated',
215
+ { activatesOn: SpaceEvents.SpaceCreated, provides: [] },
216
+ () => import('./on-space-created'),
217
+ );
218
+ ```
219
+
220
+ Surviving events are runtime occurrences only — `ActivationEvents.Startup`, `SpacesReady`,
221
+ `SpaceCreated`, `IdentityCreated`, `TypeAdded`, plus parameterized `createStateEvent` /
222
+ `createSettingsEvent`. Ordering-only `Setup*`/`*Ready` events were removed; express ordering as
223
+ `requires`/`provides`.
224
+
225
+ Structural problems isolate rather than abort startup: a dependency cycle, duplicate singleton
226
+ provider, or unsatisfiable requirement puts the owning plugin in an error state
227
+ (`DependencyCycleError`, `DuplicateProviderError`, `MissingProviderError`,
228
+ `ProvidesMismatchError`) and everything else proceeds.
229
+
230
+ ---
231
+
232
+ ## Well-known capabilities
233
+
234
+ `Capabilities` (framework) — `ReactSurface`, `ReactContext`, `ReactRoot`, `OperationHandler`,
235
+ `OperationInvoker`, `UndoMapping`, `Command`, `Layer`, `LayerSpec`, `TraceSink`, `AtomRegistry`,
236
+ `ServiceResolver`, `ProcessManagerRuntime`, `ProcessMonitor`, `PluginManager`.
237
+
238
+ `AppCapabilities` (`@dxos/app-toolkit`) — `Translations`, `Schema`, `Settings`, `AppGraph`,
239
+ `AppGraphBuilder`, `SkillDefinition`, `PluginAsset`, `Toolkit`, `NavigationTargetResolver`,
240
+ `NavigationHandler`, `TextContent`, `CommentConfig`, `AnchorSort`, `AnchorResolver`, `Layout`.
241
+
242
+ ---
243
+
244
+ ## Testing
245
+
246
+ ```ts
247
+ withPluginManager({
248
+ plugins: [ClientPlugin(), FooPlugin()],
249
+ capabilities: [Capability.contribute(SomeCapability, stub)], // takes Contribution[]
250
+ });
251
+ ```
252
+
253
+ `@dxos/app-framework/testing` also exports `harness` (headless activation assertions) and
254
+ `createComposerTestApp`.
255
+
256
+ ---
257
+
258
+ ## Type reference
259
+
260
+ | Type | Meaning |
261
+ | ----------------------------- | -------------------------------------------------------------- |
262
+ | `Capability.Tag<T, S>` | Singleton tag; `yield*` → `T` |
263
+ | `Capability.MultiTag<T, S>` | Multi tag; `yield*` → `Contributions<T>` |
264
+ | `Capability.AnyTag` | `Tag \| MultiTag` |
265
+ | `Capability.Contribution` | One branded contribution (capability + n values) |
266
+ | `Capability.Contributions<T>` | Live view: `{ atom, get(), subscribe() }` |
267
+ | `Capability.Any` | A registry entry: `{ interface, implementation, deactivate? }` |
268
+ | `Capability.Module<Options>` | An authored module, opaque except for its options type |
269
+ | `Plugin.Plugin` | `{ meta, modules }` |
270
+ | `Plugin.PluginModule` | `{ id, activation, activate() }` |
271
+ | `Plugin.ActivationSpec` | `dependency \| event` discriminated union |
272
+
273
+ `Contribution` → `Capability.Any` is a 1-to-n expansion performed by the manager
274
+ (`CapabilityManager.expandContributions`); see
275
+ [ARCHITECTURE.md](./ARCHITECTURE.md#the-two-layer-split).
@@ -0,0 +1,190 @@
1
+ # Architecture
2
+
3
+ How plugins, modules, capabilities and contributions relate.
4
+
5
+ The short version: a **plugin** is a bag of **modules**; a module declares which **capabilities** it
6
+ `requires` and `provides`, and its body returns **contributions**; the manager topologically orders
7
+ activation from those declarations and expands each contribution into the flat entries the
8
+ **capability registry** stores.
9
+
10
+ ## Domain model
11
+
12
+ ```mermaid
13
+ classDiagram
14
+ direction TB
15
+
16
+ class Plugin {
17
+ +Meta meta
18
+ +PluginModule[] modules
19
+ }
20
+
21
+ class PluginModule {
22
+ +string id
23
+ +ActivationSpec activation
24
+ +activate(props) Effect
25
+ }
26
+
27
+ class ActivationSpec {
28
+ +mode dependency | event
29
+ +AnyTag[] requires
30
+ +AnyTag[] provides
31
+ +Events activatesOn
32
+ }
33
+
34
+ class Module {
35
+ +AnyTag[] requires
36
+ +AnyTag[] provides
37
+ +Events activatesOn
38
+ +call(options) Effect
39
+ }
40
+
41
+ class Tag {
42
+ +arity single
43
+ +yield* gives T
44
+ }
45
+
46
+ class MultiTag {
47
+ +arity multi
48
+ +yield* gives Contributions
49
+ }
50
+
51
+ class Contribution {
52
+ +AnyTag capability
53
+ +unknown[] values
54
+ +deactivate()
55
+ }
56
+
57
+ class CapabilityEntry {
58
+ +InterfaceDef interface
59
+ +T implementation
60
+ +deactivate()
61
+ }
62
+
63
+ class Contributions {
64
+ +Atom atom
65
+ +get() T[]
66
+ +subscribe(cb)
67
+ }
68
+
69
+ class CapabilityManager {
70
+ +contribute(entry)
71
+ +get(tag) T
72
+ +getAll(tag) T[]
73
+ +atom(tag) Atom
74
+ +contributions(tag) Contributions
75
+ }
76
+
77
+ class PluginManager {
78
+ +start()
79
+ +activate(event)
80
+ }
81
+
82
+ Plugin "1" *-- "0..n" PluginModule : modules
83
+ PluginModule --> ActivationSpec : activation
84
+ Module ..> PluginModule : Plugin.addModule()
85
+ ActivationSpec ..> Tag : requires / provides
86
+ ActivationSpec ..> MultiTag : requires / provides
87
+ Tag ..> Contribution : Capability.contribute()
88
+ MultiTag ..> Contribution : Capability.contributeAll()
89
+ Contribution ..> CapabilityEntry : expandContributions() 1 to n
90
+ CapabilityManager "1" o-- "0..n" CapabilityEntry : registry keyed by NSID
91
+ CapabilityManager ..> Contributions : live view for a MultiTag
92
+ PluginManager --> PluginModule : orders and activates
93
+ PluginManager --> CapabilityManager : owns
94
+ ```
95
+
96
+ ### The two-layer split
97
+
98
+ `Contribution` and `CapabilityEntry` (exported as `Capability.Any`) sit on either side of the
99
+ registry boundary — the authoring layer and the storage layer:
100
+
101
+ | | `Contribution` (authoring) | `Capability.Any` (registry) |
102
+ | ------------ | ---------------------------------------------------------------------------------------------- | --------------------------------------- |
103
+ | Produced by | `Capability.contribute` / `contributeAll` | `CapabilityManager.expandContributions` |
104
+ | Holds | **n** values for one capability | exactly **one** implementation |
105
+ | Identity | branded by capability identity (NSID + arity) — powers the `EnsureProvides` completeness check | the `InterfaceDef` only |
106
+ | Service type | erased to `AnyTag`, so a body's service type never leaks into `.d.ts` emit | names `T` directly |
107
+
108
+ `expandContributions` is the one-way bridge: a `contributeAll(tag, [a, b, c])` is one `Contribution`
109
+ that becomes three registry entries, with the `deactivate` hook pinned to the first so it runs
110
+ exactly once.
111
+
112
+ ## Activation
113
+
114
+ ```mermaid
115
+ flowchart TB
116
+ subgraph authoring["Authoring (compile time)"]
117
+ A1["Capability.make / makeSingleton<br/>defines a Tag"]
118
+ A2["Capability.lazyModule / inlineModule<br/>or an AppCapability maker<br/>→ Capability.Module"]
119
+ A3["Plugin.define(meta).pipe(<br/>Plugin.addModule(...), Plugin.make)<br/>→ Plugin"]
120
+ A1 --> A2 --> A3
121
+ end
122
+
123
+ subgraph ordering["Ordering (PluginManager.start)"]
124
+ B1["Collect every enabled plugin's modules"]
125
+ B2{"activation.mode"}
126
+ B3["dependency:<br/>edges from requires → provides<br/>Kahn topological waves"]
127
+ B4["event:<br/>parked until activatesOn fires,<br/>requires pulled on demand"]
128
+ B1 --> B2
129
+ B2 -->|dependency| B3
130
+ B2 -->|event| B4
131
+ end
132
+
133
+ subgraph runtime["Activation + registry"]
134
+ C1["Build the module's Context<br/>from its requires"]
135
+ C2["activate(props) → Contribution[]"]
136
+ C3["Validate against declared provides"]
137
+ C4["expandContributions → Capability.Any[]"]
138
+ C5["CapabilityManager.contribute(entry)"]
139
+ C1 --> C2 --> C3 --> C4 --> C5
140
+ end
141
+
142
+ subgraph consume["Consumption"]
143
+ D1["In a module body:<br/>yield* Tag"]
144
+ D2["Dynamically:<br/>Capability.get / getAll / atom"]
145
+ D3["Reactively:<br/>Contributions.atom"]
146
+ end
147
+
148
+ A3 --> B1
149
+ B3 --> C1
150
+ B4 --> C1
151
+ C5 --> D1
152
+ C5 --> D2
153
+ C5 --> D3
154
+ ```
155
+
156
+ Failure modes are structural rather than fatal: a dependency cycle, a duplicate singleton provider,
157
+ or an unsatisfiable `requires` puts the owning plugin into an error state and excludes it from
158
+ further rounds, while everything else proceeds. A consumer whose provider is event-gated **waits**
159
+ rather than erroring, and activates in a later round once the provider contributes.
160
+
161
+ ### Singleton vs multi
162
+
163
+ Arity is a property of the tag and decides what `yield*` gives you:
164
+
165
+ - `Capability.makeSingleton<T>()(nsid)` → `Tag`. `yield* tag` gives `T`. Requiring one gates
166
+ activation on its single provider; two providers is a `DuplicateProviderError`.
167
+ - `Capability.make<T>()(nsid)` → `MultiTag` (the default). `yield* tag` gives a live
168
+ `Contributions<T>` collection. Providers never gate consumers, so consume it reactively — a
169
+ one-shot snapshot silently misses late contributions.
170
+
171
+ Both forms are curried so the NSID string literal is captured and brands the identifier; the NSID
172
+ must be camelCase (`DXN.Name`).
173
+
174
+ ## Where things live
175
+
176
+ | Concept | File |
177
+ | -------------------------------------------------------------------- | -------------------------------------------------------- |
178
+ | Tags, arity, `Contribution`, `contribute`, module makers | `src/core/capability.ts` |
179
+ | `Plugin`, `PluginModule`, `ActivationSpec`, the builder | `src/core/plugin.ts` |
180
+ | Public manager API, unit composition, shutdown, failure policy | `src/core/plugin-manager/plugin-manager.ts` |
181
+ | Shared substrate: atoms, status channel, fibers (`ManagerState`) | `src/core/plugin-manager/manager-state.ts` |
182
+ | Catalog: add/enable/disable, lazy resolution, dependency closure | `src/core/plugin-manager/plugin-catalog.ts` |
183
+ | Lifecycle engine: start/activate/deactivate/reset, rounds, waves | `src/core/plugin-manager/activation-scheduler.ts` |
184
+ | Ordering logic (runnable selection, waves, cycles), on `@dxos/graph` | `src/core/plugin-manager/activation-graph.ts` |
185
+ | Memoized module loads, provides validation, deactivation | `src/core/plugin-manager/module-loader.ts` |
186
+ | The NSID-keyed registry and its reactive views | `src/core/capability-manager.ts` |
187
+ | Tagged errors (cycle, missing/duplicate provider, mismatch) | `src/core/errors.ts` |
188
+ | Per-capability module makers (`surface`, `settings`, …) | `@dxos/app-toolkit` `src/app-framework/AppCapability.ts` |
189
+
190
+ Worked examples: `@dxos/plugin-client` and `@dxos/plugin-markdown`.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,78 @@
1
1
  # @dxos/app-framework
2
2
 
3
+ ## 1.0.0-next-bbabc02363f18d7643ea4160e3a7a7809870fd0d
4
+
5
+ ### Minor Changes
6
+
7
+ - 0280a6a: Omitting `activatesOn` on a plugin module now puts it in the **idle** wave rather than the startup wave. A module that must run at boot has to declare `activatesOn: ActivationEvents.Startup` explicitly.
8
+
9
+ This is a behaviour change for out-of-repo plugin authors: an un-annotated module that previously ran during startup now runs at host idle. Un-annotated modules remain pullable as providers, so one that a startup module `requires` is still activated ahead of its own wave — the change is only visible for modules nothing on the boot path depends on.
10
+
11
+ The `@dxos/app-toolkit` maker families that back the app shell — `settings`, `operationHandler`, `reactContext`, `reactRoot`, `navigationResolver` and `navigationHandler` — now state `Startup` explicitly, so modules built with them are unaffected. `appGraphBuilder` (idle) and `skillDefinition` (assistant start) were already explicit.
12
+
13
+ - 34a8433: Order module activation by capability dependencies instead of hand-wired events.
14
+ A module declares the capabilities it `requires` and `provides` (or a runtime
15
+ `activatesOn` event) and the plugin manager topologically orders activation from
16
+ that graph. Capabilities are yieldable Effect services, so accessing an undeclared
17
+ capability or omitting a declared one is now a type error, and missing providers,
18
+ dependency cycles, and duplicate providers fail fast with tagged errors instead of
19
+ runtime assertions. Plugins compose as a flat chain of `Plugin.addModule` over
20
+ module bodies authored with `Capability.lazyModule` (code-split) or
21
+ `Capability.inlineModule` (eager), or with a per-capability maker from the new
22
+ `AppCapability` namespace (`surface`, `settings`, `appGraphBuilder`, `translations`,
23
+ `schema`, ...) that bakes in the module name and default provides. A module is an
24
+ opaque `Capability.Module<Options>`, parameterized only by its options type, so a
25
+ module export never leaks a foreign capability's type into declaration emit.
26
+
27
+ Every plugin in the repository is migrated to this API. The plugins gain no
28
+ behaviour of their own from the change, but any plugin defined outside the
29
+ repository must be migrated too — the legacy API is removed, not deprecated.
30
+
31
+ Breaking: the legacy event-wiring API is removed — `AppPlugin` and its
32
+ `addXModule` helpers, `firesBeforeActivation`/`firesAfterActivation`, `compatFires`,
33
+ and the ordering-only `Setup*`/`*Ready` activation events (genuine runtime events
34
+ remain). `Capability.provide`/`provideAll` are renamed to
35
+ `Capability.contribute`/`contributeAll`, and the untyped raw builder
36
+ `Capability.contributes` is removed. Multi is now the default capability arity:
37
+ `Capability.make` defines a multi (registry) capability and
38
+ `Capability.makeSingleton` the single-provider case, both curried
39
+ (`make<T>()(nsid)`) so the NSID literal brands the identifier. The
40
+ `withPluginManager` `capabilities` test option now accepts `Contribution[]`.
41
+
42
+ ### Patch Changes
43
+
44
+ - 4a0b78b: Bundler-plugin entrypoints no longer publish a `source` export condition: `@dxos/app-framework/vite-plugin` and `@dxos/ui-theme/plugin`. These run in Node inside a `vite.config.ts` and reach `node:*`, so a `source` condition let an app's `source`-first resolver pull their Node-only sources into a browser bundle.
45
+
46
+ Default resolution is unchanged — both entrypoints already resolved to their built `dist` for ordinary consumers, with the same exports and runtime behaviour. Only resolution under `--conditions=source` changes: it now yields the built output, matching `@dxos/config`'s bundler-plugin entrypoints.
47
+
48
+ - Updated dependencies [85ad256]
49
+ - Updated dependencies [557e243]
50
+ - Updated dependencies [557e243]
51
+ - Updated dependencies [7c426d4]
52
+ - Updated dependencies [fc83abd]
53
+ - Updated dependencies [77d0026]
54
+ - @dxos/protocols@1.0.0-next-bbabc02363f18d7643ea4160e3a7a7809870fd0d
55
+ - @dxos/react-ui@1.0.0-next-bbabc02363f18d7643ea4160e3a7a7809870fd0d
56
+ - @dxos/compute@1.0.0-next-bbabc02363f18d7643ea4160e3a7a7809870fd0d
57
+ - @dxos/react-hooks@1.0.0-next-bbabc02363f18d7643ea4160e3a7a7809870fd0d
58
+ - @dxos/compute-runtime@1.0.0-next-bbabc02363f18d7643ea4160e3a7a7809870fd0d
59
+ - @dxos/edge-client@1.0.0-next-bbabc02363f18d7643ea4160e3a7a7809870fd0d
60
+ - @dxos/operation@1.0.0-next-bbabc02363f18d7643ea4160e3a7a7809870fd0d
61
+ - @dxos/async@1.0.0-next-bbabc02363f18d7643ea4160e3a7a7809870fd0d
62
+ - @dxos/context@1.0.0-next-bbabc02363f18d7643ea4160e3a7a7809870fd0d
63
+ - @dxos/debug@1.0.0-next-bbabc02363f18d7643ea4160e3a7a7809870fd0d
64
+ - @dxos/effect@1.0.0-next-bbabc02363f18d7643ea4160e3a7a7809870fd0d
65
+ - @dxos/errors@1.0.0-next-bbabc02363f18d7643ea4160e3a7a7809870fd0d
66
+ - @dxos/graph@1.0.0-next-bbabc02363f18d7643ea4160e3a7a7809870fd0d
67
+ - @dxos/invariant@1.0.0-next-bbabc02363f18d7643ea4160e3a7a7809870fd0d
68
+ - @dxos/keys@1.0.0-next-bbabc02363f18d7643ea4160e3a7a7809870fd0d
69
+ - @dxos/log@1.0.0-next-bbabc02363f18d7643ea4160e3a7a7809870fd0d
70
+ - @dxos/react-error-boundary@1.0.0-next-bbabc02363f18d7643ea4160e3a7a7809870fd0d
71
+ - @dxos/ui-theme@1.0.0-next-bbabc02363f18d7643ea4160e3a7a7809870fd0d
72
+ - @dxos/util@1.0.0-next-bbabc02363f18d7643ea4160e3a7a7809870fd0d
73
+ - @dxos/web-context@1.0.0-next-bbabc02363f18d7643ea4160e3a7a7809870fd0d
74
+ - @dxos/web-context-react@1.0.0-next-bbabc02363f18d7643ea4160e3a7a7809870fd0d
75
+
3
76
  ## 0.11.1
4
77
 
5
78
  ### Patch Changes
package/README.md CHANGED
@@ -2,6 +2,9 @@
2
2
 
3
3
  A framework for building applications from composible plugins.
4
4
 
5
+ See [API.md](./API.md) for the API surface, and [ARCHITECTURE.md](./ARCHITECTURE.md) for how
6
+ plugins, modules, capabilities and contributions relate and how activation is ordered.
7
+
5
8
  ## Installation
6
9
 
7
10
  ```bash
@@ -1,7 +1,11 @@
1
1
  import { ReactSurface } from "./common/capabilities.mjs";
2
+ import { SurfacesRequested } from "./common/activation-events.mjs";
2
3
  import { Position } from "@dxos/util";
4
+ import * as Effect from "effect/Effect";
3
5
  import { Atom } from "@effect-atom/atom";
6
+ import { EffectEx } from "@dxos/effect";
4
7
  import { log } from "@dxos/log";
8
+ import * as Exit from "effect/Exit";
5
9
  import { Fragment, Profiler, Suspense, createContext, createElement, memo, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
6
10
  import { ErrorBoundary } from "@dxos/react-error-boundary";
7
11
  import { Fragment as Fragment$1, jsx } from "react/jsx-runtime";
@@ -37,9 +41,8 @@ var PluginManagerContext = createContext(void 0);
37
41
  */
38
42
  var usePluginManager = () => useContext(PluginManagerContext) ?? raise(/* @__PURE__ */ new Error("Missing PluginManagerContext"));
39
43
  /**
40
- * Get the plugin manager if present, or `undefined` when rendered outside a {@link PluginManagerProvider}
41
- * (e.g. a standalone component story). Lets components treat plugin capabilities as a progressive
42
- * enhancement rather than a hard requirement.
44
+ * Get the plugin manager, or undefined outside a provider. Surfaces and activation signals fire
45
+ * demand events opportunistically and must not throw when rendered standalone (stories, tests).
43
46
  */
44
47
  var useOptionalPluginManager = () => useContext(PluginManagerContext);
45
48
  /**
@@ -607,6 +610,16 @@ var emptySnapshot = () => EMPTY_SNAPSHOT;
607
610
  //#endregion
608
611
  //#region src/ui/components/Surface/SurfaceComponent.tsx
609
612
  var DEFAULT_PLACEHOLDER = /* @__PURE__ */ jsx(Fragment, {});
613
+ /**
614
+ * Fires the role's surface demand event so modules gated on it load (see the `roles` option of
615
+ * the surface module maker). Safe to call repeatedly: the surface manager claims the first
616
+ * demand per role, which keeps mount cost flat where Surface instances are numerous (every card
617
+ * in a grid mounts one) instead of re-entering the scheduler.
618
+ */
619
+ var requestSurfaces = (surfaceManager, manager, role) => {
620
+ if (!manager || !surfaceManager.requestRole(role)) return;
621
+ EffectEx.runDetached(manager.activate(SurfacesRequested(role)).pipe(Effect.onExit((exit) => Exit.isFailure(exit) ? Effect.sync(() => surfaceManager.releaseRole(role)) : Effect.void)));
622
+ };
610
623
  var WebComponentWrapper = memo(({ id, role, data, limit, definition, ...rest }) => {
611
624
  const containerRef = useRef(null);
612
625
  const elementRef = useRef(null);
@@ -732,7 +745,16 @@ var SurfaceComponent = memo(({ id: _id, type, data: dataProp, limit, placeholder
732
745
  const data = useDefaultValue(dataProp, () => ({}));
733
746
  const surfaceManager = useSurfaceManager();
734
747
  const effectiveRole = type?.role ?? "";
735
- const definitions = matchCandidates(useAtomValue(surfaceManager.candidatesAtom(effectiveRole)), effectiveRole, data);
748
+ const roleCandidates = useAtomValue(surfaceManager.candidatesAtom(effectiveRole));
749
+ const pluginManager = useOptionalPluginManager();
750
+ useEffect(() => {
751
+ requestSurfaces(surfaceManager, pluginManager, effectiveRole);
752
+ }, [
753
+ surfaceManager,
754
+ pluginManager,
755
+ effectiveRole
756
+ ]);
757
+ const definitions = matchCandidates(roleCandidates, effectiveRole, data);
736
758
  const candidates = limit != null ? definitions.slice(0, limit) : definitions;
737
759
  const truncated = limit != null && definitions.length > limit;
738
760
  const churnRef = useRef({
@@ -793,12 +815,19 @@ var matchCandidates = (definitions, role, data) => {
793
815
  */
794
816
  var useIsSurfaceAvailable = () => {
795
817
  const surfaceManager = useSurfaceManager();
818
+ const pluginManager = useOptionalPluginManager();
796
819
  const registry = useContext(RegistryContext);
797
820
  return useCallback((args) => {
798
821
  const effectiveRole = args.type?.role;
799
822
  if (effectiveRole == null) return false;
800
- return matchCandidates(registry.get(surfaceManager.candidatesAtom(effectiveRole)), effectiveRole, args.data).length > 0;
801
- }, [surfaceManager, registry]);
823
+ const candidates = matchCandidates(registry.get(surfaceManager.candidatesAtom(effectiveRole)), effectiveRole, args.data);
824
+ if (candidates.length === 0) requestSurfaces(surfaceManager, pluginManager, effectiveRole);
825
+ return candidates.length > 0;
826
+ }, [
827
+ surfaceManager,
828
+ pluginManager,
829
+ registry
830
+ ]);
802
831
  };
803
832
  //#endregion
804
833
  //#region src/ui/components/Surface/types.ts
@@ -930,6 +959,7 @@ var SurfaceManager = class {
930
959
  return bucket ? Data.array(bucket) : EMPTY_CANDIDATES;
931
960
  }).pipe(Atom.keepAlive));
932
961
  #warnedInvalidIds = /* @__PURE__ */ new Set();
962
+ #requestedRoles = /* @__PURE__ */ new Set();
933
963
  constructor(capabilities) {
934
964
  this.#capabilities = capabilities;
935
965
  }
@@ -937,6 +967,25 @@ var SurfaceManager = class {
937
967
  candidatesAtom(role) {
938
968
  return this.#candidates(role);
939
969
  }
970
+ /**
971
+ * Claims the first surface demand for a role, returning `true` only to that first caller so
972
+ * it can fire the activation event. Subsequent calls — re-renders, repeated availability
973
+ * checks — return `false`.
974
+ */
975
+ requestRole(role) {
976
+ if (role === "" || this.#requestedRoles.has(role)) return false;
977
+ this.#requestedRoles.add(role);
978
+ return true;
979
+ }
980
+ /**
981
+ * Returns a claimed role to the pool. The claim is taken before the dispatch is known to have
982
+ * happened, so a dispatch that fails or is interrupted (shutdown, a failed activation) must give
983
+ * it back — nothing else re-requests the role, and `useIsSurfaceAvailable`'s own retry is
984
+ * claimed away too, leaving every surface in that role permanently empty.
985
+ */
986
+ releaseRole(role) {
987
+ this.#requestedRoles.delete(role);
988
+ }
940
989
  /** Drops definitions with an invalid local id, warning once per id. */
941
990
  #dropInvalid(definitions) {
942
991
  return definitions.filter((definition) => {
@@ -946,7 +995,7 @@ var SurfaceManager = class {
946
995
  log.warn("dropping surface with invalid id; the final segment must be camelCase (no hyphens or underscores)", { id: definition.id }, {
947
996
  "~LogMeta": "~LogMeta",
948
997
  F: __dxlog_file,
949
- L: 88,
998
+ L: 115,
950
999
  S: this
951
1000
  });
952
1001
  }