@dxos/app-framework 0.8.4-main.d05539e30a → 0.8.4-main.d9fc60f731

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 (177) hide show
  1. package/dist/lib/browser/{capability-K5XIVCQU.mjs → capability-JCUNWZ66.mjs} +7 -7
  2. package/dist/lib/browser/{chunk-5AHASNDW.mjs → chunk-2ZIYRDAF.mjs} +1 -1
  3. package/dist/lib/browser/{chunk-5AHASNDW.mjs.map → chunk-2ZIYRDAF.mjs.map} +1 -1
  4. package/dist/lib/browser/{chunk-QLML5QFJ.mjs → chunk-7MBI66M7.mjs} +12 -6
  5. package/dist/lib/browser/chunk-7MBI66M7.mjs.map +7 -0
  6. package/dist/lib/browser/{chunk-VJ5PFAWC.mjs → chunk-BMLCIFZW.mjs} +37 -16
  7. package/dist/lib/browser/chunk-BMLCIFZW.mjs.map +7 -0
  8. package/dist/lib/browser/{chunk-SLX73WRZ.mjs → chunk-DC3WRPBV.mjs} +14 -5
  9. package/dist/lib/browser/chunk-DC3WRPBV.mjs.map +7 -0
  10. package/dist/lib/browser/{chunk-BRK6GYNB.mjs → chunk-E5DTDNDY.mjs} +20 -19
  11. package/dist/lib/browser/chunk-E5DTDNDY.mjs.map +7 -0
  12. package/dist/lib/browser/{chunk-KLHQNYJ2.mjs → chunk-KZUDO43J.mjs} +2 -2
  13. package/dist/lib/browser/chunk-Q2GLJTVV.mjs +12 -0
  14. package/dist/lib/browser/chunk-Q2GLJTVV.mjs.map +7 -0
  15. package/dist/lib/browser/{chunk-UVTGHZQF.mjs → chunk-VBG5F5UF.mjs} +51 -88
  16. package/dist/lib/browser/chunk-VBG5F5UF.mjs.map +7 -0
  17. package/dist/lib/browser/cli/index.mjs +5 -5
  18. package/dist/lib/browser/common/activation-events.mjs +3 -3
  19. package/dist/lib/browser/common/capabilities.mjs +4 -4
  20. package/dist/lib/browser/core/plugin-manager.mjs +2 -2
  21. package/dist/lib/browser/core/plugin.mjs +3 -1
  22. package/dist/lib/browser/core/url-loader.mjs +2 -2
  23. package/dist/lib/browser/index.mjs +13 -14
  24. package/dist/lib/browser/index.mjs.map +3 -3
  25. package/dist/lib/browser/meta.json +1 -1
  26. package/dist/lib/browser/{process-manager-capability-JIWLN7SU.mjs → process-manager-capability-4FUA4TRP.mjs} +6 -6
  27. package/dist/lib/browser/testing/index.mjs +11 -10
  28. package/dist/lib/browser/testing/index.mjs.map +3 -3
  29. package/dist/lib/browser/testing/react.mjs +6 -6
  30. package/dist/lib/browser/ui/index.mjs +7 -9
  31. package/dist/lib/node-esm/{capability-RLKFFLTB.mjs → capability-7TCQK7ZP.mjs} +7 -7
  32. package/dist/lib/node-esm/{chunk-SFYCO3PT.mjs → chunk-4BX2AE47.mjs} +37 -16
  33. package/dist/lib/node-esm/chunk-4BX2AE47.mjs.map +7 -0
  34. package/dist/lib/node-esm/chunk-FPW45EZH.mjs +14 -0
  35. package/dist/lib/node-esm/chunk-FPW45EZH.mjs.map +7 -0
  36. package/dist/lib/node-esm/{chunk-LJNUFNDO.mjs → chunk-GEWHMHM7.mjs} +12 -6
  37. package/dist/lib/node-esm/chunk-GEWHMHM7.mjs.map +7 -0
  38. package/dist/lib/node-esm/{chunk-BYHYYJZH.mjs → chunk-KSH4OVMY.mjs} +20 -19
  39. package/dist/lib/node-esm/chunk-KSH4OVMY.mjs.map +7 -0
  40. package/dist/lib/node-esm/{chunk-PW2VYGOS.mjs → chunk-QBUGLR4H.mjs} +1 -1
  41. package/dist/lib/node-esm/{chunk-PW2VYGOS.mjs.map → chunk-QBUGLR4H.mjs.map} +1 -1
  42. package/dist/lib/node-esm/{chunk-42UNAKYO.mjs → chunk-XGBUKRCK.mjs} +2 -2
  43. package/dist/lib/node-esm/{chunk-6S45OMUP.mjs → chunk-XIGPWXCQ.mjs} +14 -5
  44. package/dist/lib/node-esm/chunk-XIGPWXCQ.mjs.map +7 -0
  45. package/dist/lib/node-esm/{chunk-JNT72ZCN.mjs → chunk-Y7JKXNDU.mjs} +51 -88
  46. package/dist/lib/node-esm/chunk-Y7JKXNDU.mjs.map +7 -0
  47. package/dist/lib/node-esm/cli/index.mjs +5 -5
  48. package/dist/lib/node-esm/common/activation-events.mjs +3 -3
  49. package/dist/lib/node-esm/common/capabilities.mjs +4 -4
  50. package/dist/lib/node-esm/core/plugin-manager.mjs +2 -2
  51. package/dist/lib/node-esm/core/plugin.mjs +3 -1
  52. package/dist/lib/node-esm/core/url-loader.mjs +2 -2
  53. package/dist/lib/node-esm/index.mjs +13 -14
  54. package/dist/lib/node-esm/index.mjs.map +3 -3
  55. package/dist/lib/node-esm/meta.json +1 -1
  56. package/dist/lib/node-esm/{process-manager-capability-PHKLO2BL.mjs → process-manager-capability-F23D62RN.mjs} +6 -6
  57. package/dist/lib/node-esm/testing/index.mjs +11 -10
  58. package/dist/lib/node-esm/testing/index.mjs.map +3 -3
  59. package/dist/lib/node-esm/testing/react.mjs +6 -6
  60. package/dist/lib/node-esm/ui/index.mjs +7 -9
  61. package/dist/plugin/node-esm/index.mjs +155 -77
  62. package/dist/plugin/node-esm/index.mjs.map +4 -4
  63. package/dist/plugin/node-esm/meta.json +1 -1
  64. package/dist/types/src/common/capabilities.d.ts +1 -1
  65. package/dist/types/src/common/index.d.ts +0 -1
  66. package/dist/types/src/common/index.d.ts.map +1 -1
  67. package/dist/types/src/common/translations.d.ts +1 -1
  68. package/dist/types/src/core/capability-manager.d.ts +4 -0
  69. package/dist/types/src/core/capability-manager.d.ts.map +1 -1
  70. package/dist/types/src/core/plugin-manager.d.ts.map +1 -1
  71. package/dist/types/src/core/plugin.d.ts +33 -6
  72. package/dist/types/src/core/plugin.d.ts.map +1 -1
  73. package/dist/types/src/plugin-process-manager/history/history-tracker.d.ts +15 -1
  74. package/dist/types/src/plugin-process-manager/history/history-tracker.d.ts.map +1 -1
  75. package/dist/types/src/plugin-process-manager/history/undo-mapping.d.ts +1 -1
  76. package/dist/types/src/plugin-process-manager/history/undo-mapping.d.ts.map +1 -1
  77. package/dist/types/src/plugin-process-manager/meta.d.ts +1 -1
  78. package/dist/types/src/plugin-process-manager/meta.d.ts.map +1 -1
  79. package/dist/types/src/plugin-process-manager/testing.d.ts.map +1 -1
  80. package/dist/types/src/testing/withPluginManager.d.ts.map +1 -1
  81. package/dist/types/src/ui/components/App/App.d.ts +34 -2
  82. package/dist/types/src/ui/components/App/App.d.ts.map +1 -1
  83. package/dist/types/src/ui/components/App/App.stories.d.ts.map +1 -1
  84. package/dist/types/src/ui/components/PluginManager/PluginManagerContext.stories.d.ts.map +1 -1
  85. package/dist/types/src/ui/components/Surface/types.d.ts.map +1 -1
  86. package/dist/types/src/ui/components/index.d.ts +0 -1
  87. package/dist/types/src/ui/components/index.d.ts.map +1 -1
  88. package/dist/types/src/ui/hooks/useApp.d.ts +1 -7
  89. package/dist/types/src/ui/hooks/useApp.d.ts.map +1 -1
  90. package/dist/types/src/ui/hooks/useCapabilities.d.ts.map +1 -1
  91. package/dist/types/src/ui/hooks/useProcessManagerRuntime.d.ts +6 -0
  92. package/dist/types/src/ui/hooks/useProcessManagerRuntime.d.ts.map +1 -1
  93. package/dist/types/src/vite-plugin/boot-loader/loader.d.ts +26 -23
  94. package/dist/types/src/vite-plugin/boot-loader/loader.d.ts.map +1 -1
  95. package/dist/types/src/vite-plugin/manifest.d.ts.map +1 -1
  96. package/dist/types/tsconfig.tsbuildinfo +1 -1
  97. package/package.json +40 -35
  98. package/src/common/capabilities.ts +1 -1
  99. package/src/common/index.ts +0 -1
  100. package/src/common/translations.ts +1 -1
  101. package/src/core/capability-manager.test.ts +3 -0
  102. package/src/core/capability-manager.ts +22 -1
  103. package/src/core/plugin-manager.test.ts +51 -21
  104. package/src/core/plugin-manager.ts +10 -5
  105. package/src/core/plugin.ts +44 -8
  106. package/src/core/url-loader.test.ts +2 -1
  107. package/src/plugin-process-manager/history/history-tracker.test.ts +39 -60
  108. package/src/plugin-process-manager/history/history-tracker.ts +32 -16
  109. package/src/plugin-process-manager/history/undo-mapping.ts +1 -1
  110. package/src/plugin-process-manager/history/undo-registry.test.ts +3 -1
  111. package/src/plugin-process-manager/meta.ts +6 -4
  112. package/src/plugin-process-manager/testing.ts +6 -5
  113. package/src/testing/react.test.tsx +3 -1
  114. package/src/testing/withPluginManager.tsx +4 -3
  115. package/src/ui/components/App/App.stories.tsx +9 -11
  116. package/src/ui/components/App/App.tsx +80 -17
  117. package/src/ui/components/PluginManager/PluginManagerContext.stories.tsx +9 -14
  118. package/src/ui/components/Surface/SurfaceComponent.stories.tsx +1 -1
  119. package/src/ui/components/Surface/SurfaceComponent.tsx +1 -1
  120. package/src/ui/components/Surface/types.test.ts +2 -2
  121. package/src/ui/components/Surface/types.ts +21 -0
  122. package/src/ui/components/index.ts +0 -1
  123. package/src/ui/hooks/useApp.test.tsx +3 -1
  124. package/src/ui/hooks/useApp.tsx +4 -18
  125. package/src/ui/hooks/useCapabilities.ts +1 -0
  126. package/src/ui/hooks/useProcessManagerRuntime.ts +12 -2
  127. package/src/vite-plugin/boot-loader/BootLoader.solid-stories.tsx +198 -0
  128. package/src/vite-plugin/boot-loader/loader-app/Loader.tsx +166 -0
  129. package/src/vite-plugin/boot-loader/loader-app/boot-loader.css +311 -0
  130. package/src/vite-plugin/boot-loader/loader-app/bridge.ts +25 -0
  131. package/src/vite-plugin/boot-loader/loader-app/entry.tsx +21 -0
  132. package/src/vite-plugin/boot-loader/loader-app/mount.tsx +70 -0
  133. package/src/vite-plugin/boot-loader/loader-app/store.test.ts +137 -0
  134. package/src/vite-plugin/boot-loader/loader-app/store.ts +149 -0
  135. package/src/vite-plugin/boot-loader/loader-app/tsconfig.json +11 -0
  136. package/src/vite-plugin/boot-loader/loader-app/types.ts +78 -0
  137. package/src/vite-plugin/boot-loader/loader.ts +133 -63
  138. package/src/vite-plugin/manifest.test.ts +19 -10
  139. package/src/vite-plugin/manifest.ts +9 -3
  140. package/tsconfig.json +6 -7
  141. package/dist/lib/browser/chunk-BRK6GYNB.mjs.map +0 -7
  142. package/dist/lib/browser/chunk-KFDF7KR3.mjs +0 -42
  143. package/dist/lib/browser/chunk-KFDF7KR3.mjs.map +0 -7
  144. package/dist/lib/browser/chunk-QLML5QFJ.mjs.map +0 -7
  145. package/dist/lib/browser/chunk-SLX73WRZ.mjs.map +0 -7
  146. package/dist/lib/browser/chunk-UVTGHZQF.mjs.map +0 -7
  147. package/dist/lib/browser/chunk-VJ5PFAWC.mjs.map +0 -7
  148. package/dist/lib/node-esm/chunk-6S45OMUP.mjs.map +0 -7
  149. package/dist/lib/node-esm/chunk-BYHYYJZH.mjs.map +0 -7
  150. package/dist/lib/node-esm/chunk-JNT72ZCN.mjs.map +0 -7
  151. package/dist/lib/node-esm/chunk-LJNUFNDO.mjs.map +0 -7
  152. package/dist/lib/node-esm/chunk-OUEMWPIW.mjs +0 -43
  153. package/dist/lib/node-esm/chunk-OUEMWPIW.mjs.map +0 -7
  154. package/dist/lib/node-esm/chunk-SFYCO3PT.mjs.map +0 -7
  155. package/dist/types/src/common/operations.d.ts +0 -19
  156. package/dist/types/src/common/operations.d.ts.map +0 -1
  157. package/dist/types/src/ui/components/Placeholder/Placeholder.d.ts +0 -64
  158. package/dist/types/src/ui/components/Placeholder/Placeholder.d.ts.map +0 -1
  159. package/dist/types/src/ui/components/Placeholder/Placeholder.stories.d.ts +0 -19
  160. package/dist/types/src/ui/components/Placeholder/Placeholder.stories.d.ts.map +0 -1
  161. package/dist/types/src/ui/components/Placeholder/index.d.ts +0 -2
  162. package/dist/types/src/ui/components/Placeholder/index.d.ts.map +0 -1
  163. package/dist/types/src/vite-plugin/boot-loader/BootLoader.stories.d.ts +0 -34
  164. package/dist/types/src/vite-plugin/boot-loader/BootLoader.stories.d.ts.map +0 -1
  165. package/src/common/operations.ts +0 -35
  166. package/src/ui/components/Placeholder/Placeholder.stories.tsx +0 -77
  167. package/src/ui/components/Placeholder/Placeholder.tsx +0 -155
  168. package/src/ui/components/Placeholder/index.ts +0 -5
  169. package/src/vite-plugin/boot-loader/BootLoader.stories.tsx +0 -270
  170. package/src/vite-plugin/boot-loader/boot-loader.css +0 -320
  171. package/src/vite-plugin/boot-loader/boot-loader.js +0 -325
  172. /package/dist/lib/browser/{capability-K5XIVCQU.mjs.map → capability-JCUNWZ66.mjs.map} +0 -0
  173. /package/dist/lib/browser/{chunk-KLHQNYJ2.mjs.map → chunk-KZUDO43J.mjs.map} +0 -0
  174. /package/dist/lib/browser/{process-manager-capability-JIWLN7SU.mjs.map → process-manager-capability-4FUA4TRP.mjs.map} +0 -0
  175. /package/dist/lib/node-esm/{capability-RLKFFLTB.mjs.map → capability-7TCQK7ZP.mjs.map} +0 -0
  176. /package/dist/lib/node-esm/{chunk-42UNAKYO.mjs.map → chunk-XGBUKRCK.mjs.map} +0 -0
  177. /package/dist/lib/node-esm/{process-manager-capability-PHKLO2BL.mjs.map → process-manager-capability-F23D62RN.mjs.map} +0 -0
@@ -7,6 +7,8 @@ import * as Effect from 'effect/Effect';
7
7
  import * as Fiber from 'effect/Fiber';
8
8
  import * as Layer from 'effect/Layer';
9
9
  import * as ManagedRuntime from 'effect/ManagedRuntime';
10
+ import * as PubSub from 'effect/PubSub';
11
+ import * as Queue from 'effect/Queue';
10
12
  import * as TestClock from 'effect/TestClock';
11
13
  import { describe, expect } from 'vitest';
12
14
 
@@ -15,7 +17,6 @@ import { OperationInvoker } from '@dxos/operation';
15
17
 
16
18
  const testRuntime = ManagedRuntime.make(Layer.empty) as unknown as ManagedRuntime.ManagedRuntime<any, any>;
17
19
 
18
- import { UndoOperation } from '../../common';
19
20
  import {
20
21
  Compute,
21
22
  HalveCompute,
@@ -66,7 +67,7 @@ describe('HistoryTracker', () => {
66
67
 
67
68
  yield* invoker.invoke(ToString, { value: 42 });
68
69
 
69
- // Wait for event to be processed.
70
+ // Wait for the pending + success events to be processed.
70
71
  yield* collector.waitForEvents(1);
71
72
 
72
73
  // Without an undo mapping, canUndo should remain false.
@@ -123,7 +124,7 @@ describe('HistoryTracker', () => {
123
124
  const tracker = HistoryTracker.make(invoker, undoRegistry);
124
125
  const collector = yield* createEventCollector(invoker);
125
126
 
126
- // Fork compute operation (should emit event).
127
+ // Fork compute operation (emits one success event).
127
128
  const fiber = yield* Effect.fork(invoker.invoke(Compute, { value: 2 }));
128
129
  yield* TestClock.adjust('20 millis');
129
130
  yield* Fiber.join(fiber);
@@ -134,7 +135,7 @@ describe('HistoryTracker', () => {
134
135
  // Wait until the tracker has processed the event.
135
136
  yield* waitUntil(() => tracker.canUndo());
136
137
 
137
- // Undo (should NOT emit event because it uses _invokeCore).
138
+ // Undo (should NOT emit events because it uses _invokeCore).
138
139
  yield* tracker.undo();
139
140
 
140
141
  // Verify no new events were emitted.
@@ -173,7 +174,7 @@ describe('HistoryTracker', () => {
173
174
  yield* TestClock.adjust('30 millis');
174
175
  yield* Fiber.join(fiber2);
175
176
 
176
- // Wait for both events to be collected.
177
+ // Each invoke emits one success event.
177
178
  yield* collector.waitForEvents(2);
178
179
 
179
180
  // First undo should halve 6 (the output of the second compute).
@@ -193,8 +194,10 @@ describe('HistoryTracker', () => {
193
194
  it.effect('undo on empty history returns error', () =>
194
195
  Effect.gen(function* () {
195
196
  const invoker = OperationInvoker.make(() => Effect.succeed([]), testRuntime);
196
- const undoRegistry = UndoRegistry.make(() => []);
197
- const tracker = HistoryTracker.make(invoker, undoRegistry);
197
+ const tracker = HistoryTracker.make(
198
+ invoker,
199
+ UndoRegistry.make(() => []),
200
+ );
198
201
 
199
202
  const result = yield* tracker.undo().pipe(Effect.either);
200
203
  expect(result._tag).toBe('Left');
@@ -204,7 +207,7 @@ describe('HistoryTracker', () => {
204
207
  }),
205
208
  );
206
209
 
207
- it.effect('fires ShowUndo operation when undoable operation is tracked', () =>
210
+ it.effect('publishes an undoable event with the resolved message', () =>
208
211
  Effect.gen(function* () {
209
212
  const testMessage: [string, { ns: string }] = ['test-undo.message', { ns: 'test' }];
210
213
  const undoMapping = UndoMapping.make({
@@ -214,39 +217,28 @@ describe('HistoryTracker', () => {
214
217
  message: testMessage,
215
218
  });
216
219
 
217
- let showUndoWasCalled = false;
218
- let showUndoMessage: unknown = undefined;
219
- const showUndoHandler = Operation.withHandler(UndoOperation.ShowUndo, (input) => {
220
- showUndoWasCalled = true;
221
- showUndoMessage = input.message;
222
- return Effect.succeed(undefined);
223
- });
224
-
225
- const invoker = OperationInvoker.make(
226
- () => Effect.succeed([computeHandler, halveComputeHandler, showUndoHandler]),
227
- testRuntime,
228
- );
220
+ const invoker = OperationInvoker.make(() => Effect.succeed([computeHandler, halveComputeHandler]), testRuntime);
229
221
  const undoRegistry = UndoRegistry.make(() => [undoMapping]);
230
222
  const tracker = HistoryTracker.make(invoker, undoRegistry);
231
223
 
232
- expect(showUndoWasCalled).toBe(false);
224
+ yield* Effect.scoped(
225
+ Effect.gen(function* () {
226
+ const undoables = yield* PubSub.subscribe(tracker.undoable);
233
227
 
234
- // Fork compute operation and advance clock.
235
- const fiber = yield* Effect.fork(invoker.invoke(Compute, { value: 2 }));
236
- yield* TestClock.adjust('20 millis');
237
- yield* Fiber.join(fiber);
238
-
239
- // Wait until the tracker has processed the event.
240
- yield* waitUntil(() => tracker.canUndo());
228
+ // Fork compute operation and advance clock.
229
+ const fiber = yield* Effect.fork(invoker.invoke(Compute, { value: 2 }));
230
+ yield* TestClock.adjust('20 millis');
231
+ yield* Fiber.join(fiber);
241
232
 
242
- // ShowUndo should have been called with the message from the undo mapping.
243
- yield* waitUntil(() => showUndoWasCalled);
244
- expect(showUndoWasCalled).toBe(true);
245
- expect(showUndoMessage).toEqual(testMessage);
233
+ const undoable = yield* Queue.take(undoables);
234
+ expect(undoable.message).toEqual(testMessage);
235
+ }),
236
+ );
237
+ expect(tracker.canUndo()).toBe(true);
246
238
  }),
247
239
  );
248
240
 
249
- it.effect('ShowUndo message can be derived from input and output', () =>
241
+ it.effect('undo message can be derived from input and output', () =>
250
242
  Effect.gen(function* () {
251
243
  // Dynamic message that depends on input/output.
252
244
  const dynamicMessage = (input: { value: number }, output: { value: number }): [string, { ns: string }] => [
@@ -260,36 +252,23 @@ describe('HistoryTracker', () => {
260
252
  message: dynamicMessage,
261
253
  });
262
254
 
263
- let showUndoWasCalled = false;
264
- let showUndoMessage: unknown = undefined;
265
- const showUndoHandler = Operation.withHandler(UndoOperation.ShowUndo, (input) => {
266
- showUndoWasCalled = true;
267
- showUndoMessage = input.message;
268
- return Effect.succeed(undefined);
269
- });
270
-
271
- const invoker = OperationInvoker.make(
272
- () => Effect.succeed([computeHandler, halveComputeHandler, showUndoHandler]),
273
- testRuntime,
274
- );
255
+ const invoker = OperationInvoker.make(() => Effect.succeed([computeHandler, halveComputeHandler]), testRuntime);
275
256
  const undoRegistry = UndoRegistry.make(() => [undoMapping]);
276
257
  const tracker = HistoryTracker.make(invoker, undoRegistry);
277
258
 
278
- expect(showUndoWasCalled).toBe(false);
259
+ yield* Effect.scoped(
260
+ Effect.gen(function* () {
261
+ const undoables = yield* PubSub.subscribe(tracker.undoable);
279
262
 
280
- // Fork compute operation and advance clock (same as passing test).
281
- const fiber = yield* Effect.fork(invoker.invoke(Compute, { value: 2 }));
282
- yield* TestClock.adjust('20 millis');
283
- yield* Fiber.join(fiber);
284
-
285
- // Wait until the tracker has processed the event.
286
- yield* waitUntil(() => tracker.canUndo());
263
+ // Fork compute operation and advance clock (2 * 2 = 4).
264
+ const fiber = yield* Effect.fork(invoker.invoke(Compute, { value: 2 }));
265
+ yield* TestClock.adjust('20 millis');
266
+ yield* Fiber.join(fiber);
287
267
 
288
- // ShowUndo should have been called with the derived message.
289
- yield* waitUntil(() => showUndoWasCalled);
290
- expect(showUndoWasCalled).toBe(true);
291
- // Compute 2 * 2 = 4, so message should be 'computed 2 to 4'.
292
- expect(showUndoMessage).toEqual(['computed-2-to-4', { ns: 'test' }]);
268
+ const undoable = yield* Queue.take(undoables);
269
+ expect(undoable.message).toEqual(['computed-2-to-4', { ns: 'test' }]);
270
+ }),
271
+ );
293
272
  }),
294
273
  );
295
274
 
@@ -320,7 +299,7 @@ describe('HistoryTracker', () => {
320
299
  yield* TestClock.adjust('20 millis');
321
300
  yield* Fiber.join(fiber);
322
301
 
323
- // Wait for event to be processed.
302
+ // Wait for the pending + success events to be processed.
324
303
  yield* collector.waitForEvents(1);
325
304
 
326
305
  // Even though there's an undo mapping, deriveContext returned undefined,
@@ -359,7 +338,7 @@ describe('HistoryTracker', () => {
359
338
  yield* TestClock.adjust('50 millis');
360
339
  yield* Fiber.join(fiber);
361
340
 
362
- // Wait for event to be processed.
341
+ // Wait for the pending + success events to be processed.
363
342
  yield* collector.waitForEvents(1);
364
343
 
365
344
  // Wait until the tracker has processed the event.
@@ -3,13 +3,14 @@
3
3
  //
4
4
 
5
5
  import * as Effect from 'effect/Effect';
6
+ import * as PubSub from 'effect/PubSub';
6
7
  import * as Stream from 'effect/Stream';
7
8
 
8
9
  import { runAndForwardErrors } from '@dxos/effect';
9
10
  import { log } from '@dxos/log';
10
11
  import { OperationInvoker } from '@dxos/operation';
11
12
 
12
- import { UndoOperation } from '../../common';
13
+ import { type Label } from '../../common';
13
14
  import { EmptyHistoryError } from './errors';
14
15
  import type { HistoryEntry } from './types';
15
16
  import { resolveMessage } from './undo-mapping';
@@ -21,6 +22,15 @@ const HISTORY_LIMIT = 100;
21
22
  // Public Interface
22
23
  //
23
24
 
25
+ /**
26
+ * Emitted when a new undoable action is recorded; consumed by the UI to surface an undo affordance
27
+ * (e.g. the deck notification tracker's undo toast).
28
+ */
29
+ export type UndoableEvent = {
30
+ /** Message describing the undoable action. */
31
+ message?: Label;
32
+ };
33
+
24
34
  /**
25
35
  * HistoryTracker interface - tracks operation history and provides undo.
26
36
  */
@@ -28,6 +38,8 @@ export interface HistoryTracker {
28
38
  undo: () => Effect.Effect<void, Error>;
29
39
  undoPromise: () => Promise<{ error?: Error }>;
30
40
  canUndo: () => boolean;
41
+ /** Stream of undoable actions, published as they are recorded. */
42
+ undoable: PubSub.PubSub<UndoableEvent>;
31
43
  }
32
44
 
33
45
  //
@@ -35,27 +47,29 @@ export interface HistoryTracker {
35
47
  //
36
48
 
37
49
  /**
38
- * Creates a HistoryTracker that subscribes to invocation events and provides undo.
50
+ * Creates a HistoryTracker that subscribes to (successful) invocation events, derives undoability from
51
+ * the undo registry, maintains the undo stack, and publishes an {@link UndoableEvent} for each undoable
52
+ * action so the UI can offer an undo affordance. The invoker itself is undo-agnostic.
39
53
  */
40
54
  export const make = (
41
55
  invoker: OperationInvoker.OperationInvokerInternal,
42
56
  undoRegistry: UndoRegistry,
43
57
  ): HistoryTracker => {
44
58
  const history: HistoryEntry[] = [];
59
+ const undoable = Effect.runSync(PubSub.unbounded<UndoableEvent>());
45
60
 
46
- // Subscribe to invocation stream.
47
- const handleInvocation = (event: OperationInvoker.InvocationEvent) => {
61
+ // Record an invocation; returns the undoable event to publish, or undefined if not undoable.
62
+ const recordInvocation = (event: OperationInvoker.InvocationEvent): UndoableEvent | undefined => {
48
63
  const mapping = undoRegistry.lookup(event.operation);
49
64
  if (!mapping) {
50
- // Operation is not undoable, skip.
51
- return;
65
+ // Operation is not undoable.
66
+ return undefined;
52
67
  }
53
68
 
54
69
  const inverseInput = mapping.deriveContext(event.input, event.output);
55
70
  if (inverseInput === undefined) {
56
71
  // Operation is conditionally not undoable (deriveContext returned undefined).
57
- log('operation not undoable', { key: event.operation.meta.key });
58
- return;
72
+ return undefined;
59
73
  }
60
74
 
61
75
  const entry: HistoryEntry = {
@@ -75,19 +89,20 @@ export const make = (
75
89
  history.splice(0, history.length - HISTORY_LIMIT);
76
90
  }
77
91
 
78
- // Show undo toast (resolve message if it's a function).
79
- const resolvedMessage = resolveMessage(mapping.message, event.input, event.output);
80
- Effect.runFork(
81
- invoker.invoke(UndoOperation.ShowUndo, {
82
- message: resolvedMessage,
83
- }),
84
- );
92
+ return { message: resolveMessage(mapping.message, event.input, event.output) };
85
93
  };
86
94
 
87
95
  // Fork a fiber to consume the invocation stream.
88
96
  Effect.runFork(
89
97
  Stream.fromPubSub(invoker.invocations).pipe(
90
- Stream.runForEach((event) => Effect.sync(() => handleInvocation(event))),
98
+ Stream.runForEach((event) =>
99
+ Effect.gen(function* () {
100
+ const undoableEvent = recordInvocation(event);
101
+ if (undoableEvent) {
102
+ yield* PubSub.publish(undoable, undoableEvent);
103
+ }
104
+ }),
105
+ ),
91
106
  ),
92
107
  );
93
108
 
@@ -124,5 +139,6 @@ export const make = (
124
139
  undo,
125
140
  undoPromise,
126
141
  canUndo,
142
+ undoable,
127
143
  };
128
144
  };
@@ -7,7 +7,7 @@ import type { Operation } from '@dxos/compute';
7
7
  /**
8
8
  * Label type for translatable text (canonical definition in @dxos/app-toolkit).
9
9
  */
10
- type Label = string | [string, { ns: string; count?: number; defaultValue?: string }];
10
+ type Label = string | [string, { ns: string | readonly string[]; count?: number; defaultValue?: string }];
11
11
 
12
12
  /**
13
13
  * Extract the input type from an OperationDefinition.
@@ -4,6 +4,8 @@
4
4
 
5
5
  import { describe, test } from 'vitest';
6
6
 
7
+ import { DXN } from '@dxos/keys';
8
+
7
9
  import { Compute, HalveCompute } from '../testing';
8
10
  import * as UndoMapping from './undo-mapping';
9
11
  import * as UndoRegistry from './undo-registry';
@@ -19,7 +21,7 @@ describe('UndoRegistry', () => {
19
21
 
20
22
  const result = registry.lookup(Compute);
21
23
  expect(result).not.toBe(undefined);
22
- expect(result?.inverse.meta.key).toBe('test.halve-compute');
24
+ expect(result?.inverse.meta.key).toBe(DXN.make('org.dxos.test.halveCompute'));
23
25
  });
24
26
 
25
27
  test('returns undefined for unmapped operations', ({ expect }) => {
@@ -2,13 +2,15 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { type Plugin } from '../core';
5
+ import { DXN } from '@dxos/keys';
6
6
 
7
- export const meta: Plugin.Meta = {
8
- id: 'org.dxos.plugin.process-manager',
7
+ import { Plugin } from '../core';
8
+
9
+ export const meta = Plugin.makeMeta({
10
+ key: DXN.make('org.dxos.plugin.processManager'),
9
11
  name: 'Process Manager Plugin',
10
12
  author: 'DXOS',
11
13
  description:
12
14
  'Hosts the process-manager runtime that composes contributed layer specs into a service resolver, exposes the operation invoker built on top of it, and registers undo/history tracking.',
13
15
  tags: ['system'],
14
- };
16
+ });
@@ -10,6 +10,7 @@ import * as Schema from 'effect/Schema';
10
10
  import * as Stream from 'effect/Stream';
11
11
 
12
12
  import { Operation } from '@dxos/compute';
13
+ import { DXN } from '@dxos/keys';
13
14
  import { OperationInvoker } from '@dxos/operation';
14
15
 
15
16
  //
@@ -19,31 +20,31 @@ import { OperationInvoker } from '@dxos/operation';
19
20
  export const Compute = Operation.make({
20
21
  input: Schema.Struct({ value: Schema.Number }),
21
22
  output: Schema.Struct({ value: Schema.Number }),
22
- meta: { key: 'test.compute' },
23
+ meta: { key: DXN.make('org.dxos.test.compute') },
23
24
  });
24
25
 
25
26
  export const HalveCompute = Operation.make({
26
27
  input: Schema.Struct({ value: Schema.Number }),
27
28
  output: Schema.Struct({ value: Schema.Number }),
28
- meta: { key: 'test.halve-compute' },
29
+ meta: { key: DXN.make('org.dxos.test.halveCompute') },
29
30
  });
30
31
 
31
32
  export const ToString = Operation.make({
32
33
  input: Schema.Struct({ value: Schema.Number }),
33
34
  output: Schema.Struct({ string: Schema.String }),
34
- meta: { key: 'test.to-string' },
35
+ meta: { key: DXN.make('org.dxos.test.toString') },
35
36
  });
36
37
 
37
38
  export const Add = Operation.make({
38
39
  input: Schema.Tuple(Schema.Number, Schema.Number),
39
40
  output: Schema.Number,
40
- meta: { key: 'test.add' },
41
+ meta: { key: DXN.make('org.dxos.test.add') },
41
42
  });
42
43
 
43
44
  export const SideEffect = Operation.make({
44
45
  input: Schema.Void,
45
46
  output: Schema.Void,
46
- meta: { key: 'test.side-effect' },
47
+ meta: { key: DXN.make('org.dxos.test.sideEffect') },
47
48
  });
48
49
 
49
50
  //
@@ -6,13 +6,15 @@ import * as Effect from 'effect/Effect';
6
6
  import React from 'react';
7
7
  import { describe, test } from 'vitest';
8
8
 
9
+ import { DXN } from '@dxos/keys';
10
+
9
11
  import { ActivationEvents, Capabilities } from '../common';
10
12
  import { Capability, Plugin } from '../core';
11
13
  import { Surface } from '../ui';
12
14
  import { createTestApp } from './harness';
13
15
  import { render, renderSurface } from './react';
14
16
 
15
- const testMeta = { id: 'org.dxos.plugin.test.react-harness', name: 'ReactHarnessTest' };
17
+ const testMeta = Plugin.makeMeta({ key: DXN.make('org.dxos.plugin.test.reactHarness'), name: 'ReactHarnessTest' });
16
18
 
17
19
  const TestPlugin = Plugin.define(testMeta).pipe(
18
20
  Plugin.addModule({
@@ -8,6 +8,7 @@ import React, { useEffect, useState } from 'react';
8
8
 
9
9
  import { raise } from '@dxos/debug';
10
10
  import { runAndForwardErrors } from '@dxos/effect';
11
+ import { DXN } from '@dxos/keys';
11
12
  import { useAsyncEffect } from '@dxos/react-hooks';
12
13
  import { type MaybeProvider, getProviderValue } from '@dxos/util';
13
14
 
@@ -119,11 +120,11 @@ const WithPluginManagerApp = ({ fireEvents, pluginManager, setupEvents, storyId
119
120
  return <App />;
120
121
  };
121
122
 
122
- const storyMeta = {
123
- id: 'org.dxos.app-framework.story',
123
+ const storyMeta = Plugin.makeMeta({
124
+ key: DXN.make('org.dxos.appFramework.story'),
124
125
  name: 'Story',
125
126
  tags: ['system'],
126
- };
127
+ });
127
128
 
128
129
  // No-op plugin to ensure there exists at least one plugin for the startup event.
129
130
  // This is necessary because `createApp` expects the startup event to complete before the app is ready.
@@ -6,6 +6,7 @@ import { type Meta, type StoryObj } from '@storybook/react-vite';
6
6
  import * as Effect from 'effect/Effect';
7
7
  import React, { useEffect } from 'react';
8
8
 
9
+ import { DXN } from '@dxos/keys';
9
10
  import { withTheme } from '@dxos/react-ui/testing';
10
11
 
11
12
  import { ActivationEvents, Capabilities } from '../../../common';
@@ -13,11 +14,13 @@ import { Capability, Plugin } from '../../../core';
13
14
  import { useApp } from '../../hooks';
14
15
 
15
16
  // Minimal plugin that contributes a ReactRoot.
16
- const TestPlugin = Plugin.define<{ error?: boolean }>({
17
- id: 'org.dxos.plugin.test',
18
- name: 'Test Plugin',
19
- tags: ['system'],
20
- }).pipe(
17
+ const TestPlugin = Plugin.define<{ error?: boolean }>(
18
+ Plugin.makeMeta({
19
+ key: DXN.make('org.dxos.plugin.test'),
20
+ name: 'Test Plugin',
21
+ tags: ['system'],
22
+ }),
23
+ ).pipe(
21
24
  Plugin.addModule(({ error }) => ({
22
25
  id: 'TestMain',
23
26
  activatesOn: ActivationEvents.Startup,
@@ -50,12 +53,7 @@ const TestPlugin = Plugin.define<{ error?: boolean }>({
50
53
  type DefaultStoryProps = { plugins?: Plugin.Plugin[] };
51
54
 
52
55
  const DefaultStory = ({ plugins }: DefaultStoryProps) => {
53
- const App = useApp({
54
- plugins,
55
- placeholder: () => {
56
- return <div className='text-description'>Loading...</div>;
57
- },
58
- });
56
+ const App = useApp({ plugins });
59
57
 
60
58
  return <App />;
61
59
  };
@@ -2,13 +2,43 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import React, { type PropsWithChildren, useEffect } from 'react';
5
+ import React, { type PropsWithChildren, useEffect, useLayoutEffect } from 'react';
6
6
 
7
7
  import { Capabilities } from '../../../common';
8
8
  import { topologicalSort } from '../../../helpers';
9
9
  import { LoadingState, type StartupProgress, type UseAppOptions, useCapabilities, useLoading } from '../../hooks';
10
10
 
11
- export type AppProps = Pick<UseAppOptions, 'placeholder' | 'debounce'> & {
11
+ declare global {
12
+ interface Window {
13
+ /**
14
+ * Driver injected by `@dxos/app-framework/vite-plugin`'s `bootLoaderPlugin`
15
+ * (a Solid app inlined into `index.html`). Declared here — on a module that
16
+ * is part of the `@dxos/app-framework/ui` export surface — so the React side
17
+ * and host apps (e.g. composer-app) can drive the loader without each
18
+ * re-declaring the type. The canonical definition lives in the plugin's
19
+ * `loader-app/types.ts`; this mirror exists because that source compiles in a
20
+ * separate (Solid) program that doesn't ship its globals to consumers.
21
+ */
22
+ __bootLoader?: {
23
+ status: (payload: {
24
+ event?: string;
25
+ module?: string;
26
+ humanized: string;
27
+ /**
28
+ * Optional `(index/total)` tick. When present, the loader replaces the
29
+ * current line in place ("Loading plugins (12/80)") instead of appending
30
+ * a new entry — keeps the visible log compact during long counted phases.
31
+ */
32
+ range?: { index: number; total: number };
33
+ }) => void;
34
+ progress: (fraction?: number) => void;
35
+ ready: () => void;
36
+ dismiss: () => void;
37
+ };
38
+ }
39
+ }
40
+
41
+ export type AppProps = Pick<UseAppOptions, 'debounce'> & {
12
42
  ready: boolean;
13
43
  error: unknown;
14
44
  progress?: StartupProgress;
@@ -16,22 +46,50 @@ export type AppProps = Pick<UseAppOptions, 'placeholder' | 'debounce'> & {
16
46
 
17
47
  const FIRST_INTERACTIVE_MARK = 'app-framework:first-interactive';
18
48
 
19
- export const App = ({ placeholder: Placeholder, ready, error, debounce, progress }: AppProps) => {
49
+ export const App = ({ ready, error, debounce, progress }: AppProps) => {
20
50
  const reactContexts = useCapabilities(Capabilities.ReactContext);
21
51
  const reactRoots = useCapabilities(Capabilities.ReactRoot);
22
52
  const stage = useLoading(ready, debounce);
23
53
  const placeholderDismissed = stage >= LoadingState.Done;
24
54
 
55
+ // Relay the startup lifecycle into the boot loader injected by
56
+ // `@dxos/app-framework/vite-plugin` (a Solid app inlined into `index.html`,
57
+ // the only visible loading UI). Plugin activation fills the `[0.5, 1]` half
58
+ // of the ring; the raw `event` / `module` ids ride along so the loader owns
59
+ // how each transition is rendered and traced. No-op once the outro has
60
+ // started (`FadeOut`), so the dismissal runs uncontended.
61
+ useEffect(() => {
62
+ if (stage >= LoadingState.FadeOut) {
63
+ return;
64
+ }
65
+ const fraction = progress?.progress ?? 0;
66
+ window.__bootLoader?.progress(0.5 + fraction * 0.5);
67
+ if (progress?.humanizedName) {
68
+ window.__bootLoader?.status({
69
+ event: progress.event,
70
+ module: progress.module,
71
+ humanized: `Activating ${progress.humanizedName}`,
72
+ });
73
+ }
74
+ }, [stage, progress?.progress, progress?.event, progress?.module, progress?.humanizedName]);
75
+
76
+ // Hand off at fade-out: play the loader's graceful shrink-and-fade outro.
77
+ // `useLayoutEffect` runs before the next paint so the outro begins in the
78
+ // same frame the real shell starts rendering beneath it.
79
+ useLayoutEffect(() => {
80
+ if (stage >= LoadingState.FadeOut) {
81
+ window.__bootLoader?.ready();
82
+ }
83
+ }, [stage]);
84
+
25
85
  // Emit a once-per-app `app-framework:first-interactive` mark the first time
26
- // the placeholder is dismissed and the real app shell renders. Closes the
27
- // gap between `Startup` activated and the first interactive paint.
86
+ // the loader is dismissed and the real app shell renders. Closes the gap
87
+ // between `Startup` activated and the first interactive paint.
28
88
  //
29
- // Also dismisses the native-DOM boot loader from
30
- // `@dxos/app-framework/vite-plugin` here the framework owns the
31
- // handoff-complete signal, so dismissing in `App` covers both hosts that
32
- // wire in a `Placeholder` (which may have its own dismiss path) and hosts
33
- // that don't (the loader would otherwise sit forever as a `z-index: 10`
34
- // overlay covering the rendered shell).
89
+ // Also the framework-owned handoff-complete signal: `dismiss()` removes the
90
+ // loader immediately on the fast-load path (where `useLoading` skips
91
+ // `FadeOut`), and is a no-op once an outro is already in flight, so it never
92
+ // cuts the animation short.
35
93
  useEffect(() => {
36
94
  if (!placeholderDismissed) {
37
95
  return;
@@ -39,21 +97,26 @@ export const App = ({ placeholder: Placeholder, ready, error, debounce, progress
39
97
  if (performance.getEntriesByName(FIRST_INTERACTIVE_MARK).length === 0) {
40
98
  performance.mark(FIRST_INTERACTIVE_MARK);
41
99
  }
42
- (window as { __bootLoader?: { dismiss?: () => void } }).__bootLoader?.dismiss?.();
100
+ window.__bootLoader?.dismiss();
43
101
  }, [placeholderDismissed]);
44
102
 
103
+ // Used in tests to exercise the error boundary & reset dialog (see
104
+ // composer-app's `basic.spec.ts`). Thrown into the surrounding `ErrorBoundary`.
105
+ if (location.search === '?throw') {
106
+ throw new Error('Test error');
107
+ }
108
+
45
109
  if (error) {
46
110
  // This triggers the error boundary to provide UI feedback for the startup error.
47
111
  throw error;
48
112
  }
49
113
 
114
+ // The boot loader owns the screen until handoff completes; render nothing
115
+ // until then (any DOM here would sit invisibly behind the `z-index: 10`
116
+ // loader anyway).
50
117
  // TODO(wittjosiah): Consider using Suspense instead.
51
118
  if (!placeholderDismissed) {
52
- if (!Placeholder) {
53
- return null;
54
- }
55
-
56
- return <Placeholder stage={stage} progress={progress} />;
119
+ return null;
57
120
  }
58
121
 
59
122
  const ComposedContext = composeContexts(reactContexts);
@@ -7,6 +7,7 @@ import { type Meta, type StoryObj } from '@storybook/react-vite';
7
7
  import * as Effect from 'effect/Effect';
8
8
  import React, { useEffect, useMemo, useState } from 'react';
9
9
 
10
+ import { DXN } from '@dxos/keys';
10
11
  import { withLayout, withTheme } from '@dxos/react-ui/testing';
11
12
  import { useWebComponentContext } from '@dxos/web-context-react';
12
13
 
@@ -117,11 +118,13 @@ const CounterComponent = () => {
117
118
  };
118
119
 
119
120
  // Plugin that provides the Counter capability and renders the UI
120
- const CounterPlugin = Plugin.define({
121
- id: 'org.dxos.plugin.counter',
122
- name: 'Counter Plugin',
123
- tags: ['system'],
124
- }).pipe(
121
+ const CounterPlugin = Plugin.define(
122
+ Plugin.makeMeta({
123
+ key: DXN.make('org.dxos.plugin.counter'),
124
+ name: 'Counter Plugin',
125
+ tags: ['system'],
126
+ }),
127
+ ).pipe(
125
128
  Plugin.addModule({
126
129
  id: 'CounterMain',
127
130
  activatesOn: ActivationEvents.Startup,
@@ -155,17 +158,9 @@ const CounterPlugin = Plugin.define({
155
158
  )();
156
159
 
157
160
  const plugins = [CounterPlugin];
158
- const placeholder = () => (
159
- <div className='flex h-screen items-center justify-center p-4 text-lg text-neutral-500'>
160
- Initializing Application...
161
- </div>
162
- );
163
161
 
164
162
  const DefaultStory = () => {
165
- const App = useApp({
166
- plugins,
167
- placeholder,
168
- });
163
+ const App = useApp({ plugins });
169
164
 
170
165
  return <App />;
171
166
  };