@dxos/app-framework 0.8.1-main.ba2dec9 → 0.8.1-staging.31c3ee1

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 (107) hide show
  1. package/dist/lib/browser/{app-graph-builder-IAEV7KKF.mjs → app-graph-builder-576BHZC7.mjs} +3 -3
  2. package/dist/lib/browser/{chunk-PWAAJE2Z.mjs → chunk-6AVTZPMT.mjs} +65 -45
  3. package/dist/lib/browser/chunk-6AVTZPMT.mjs.map +7 -0
  4. package/dist/lib/browser/{chunk-CIP4C47B.mjs → chunk-PPIBZ5N4.mjs} +55 -11
  5. package/dist/lib/browser/chunk-PPIBZ5N4.mjs.map +7 -0
  6. package/dist/lib/browser/{chunk-CB3W6T5E.mjs → chunk-SFPT4Z2C.mjs} +1 -1
  7. package/dist/lib/browser/{chunk-CB3W6T5E.mjs.map → chunk-SFPT4Z2C.mjs.map} +2 -2
  8. package/dist/lib/browser/index.mjs +8 -7
  9. package/dist/lib/browser/index.mjs.map +3 -3
  10. package/dist/lib/browser/{intent-dispatcher-UR566RVO.mjs → intent-dispatcher-3Q67MHZZ.mjs} +2 -2
  11. package/dist/lib/browser/{intent-resolver-LEJ3X6CP.mjs → intent-resolver-O763LCLG.mjs} +3 -3
  12. package/dist/lib/browser/meta.json +1 -1
  13. package/dist/lib/browser/{store-5TN4OLFQ.mjs → store-URSN7DZI.mjs} +2 -2
  14. package/dist/lib/browser/testing/index.mjs +4 -3
  15. package/dist/lib/browser/testing/index.mjs.map +3 -3
  16. package/dist/lib/browser/worker.mjs +1 -1
  17. package/dist/lib/node/{app-graph-builder-3RWOZENP.cjs → app-graph-builder-JZCSKYPY.cjs} +28 -28
  18. package/dist/lib/node/{chunk-SCHC4AZQ.cjs → chunk-JUSEAFDU.cjs} +4 -4
  19. package/dist/lib/node/{chunk-SCHC4AZQ.cjs.map → chunk-JUSEAFDU.cjs.map} +2 -2
  20. package/dist/lib/node/{chunk-XP4TI5DS.cjs → chunk-YIFTVCOR.cjs} +191 -171
  21. package/dist/lib/node/chunk-YIFTVCOR.cjs.map +7 -0
  22. package/dist/lib/node/{chunk-JLQABYHB.cjs → chunk-YNTKVTVX.cjs} +70 -26
  23. package/dist/lib/node/chunk-YNTKVTVX.cjs.map +7 -0
  24. package/dist/lib/node/index.cjs +79 -78
  25. package/dist/lib/node/index.cjs.map +3 -3
  26. package/dist/lib/node/{intent-dispatcher-EDW7NFJ4.cjs → intent-dispatcher-H334XLFD.cjs} +8 -8
  27. package/dist/lib/node/{intent-dispatcher-EDW7NFJ4.cjs.map → intent-dispatcher-H334XLFD.cjs.map} +2 -2
  28. package/dist/lib/node/{intent-resolver-34J5IM27.cjs → intent-resolver-3F4POWAM.cjs} +12 -12
  29. package/dist/lib/node/meta.json +1 -1
  30. package/dist/lib/node/{store-3I3UFH5D.cjs → store-OFDHTDCB.cjs} +7 -7
  31. package/dist/lib/node/testing/index.cjs +10 -9
  32. package/dist/lib/node/testing/index.cjs.map +3 -3
  33. package/dist/lib/node/worker.cjs +37 -37
  34. package/dist/lib/node/worker.cjs.map +1 -1
  35. package/dist/lib/node-esm/{app-graph-builder-RG3DT2OD.mjs → app-graph-builder-VYKLSMSZ.mjs} +3 -3
  36. package/dist/lib/node-esm/{chunk-NIVSBD3D.mjs → chunk-AHKIPS2L.mjs} +65 -45
  37. package/dist/lib/node-esm/chunk-AHKIPS2L.mjs.map +7 -0
  38. package/dist/lib/node-esm/{chunk-BSYAFUZ7.mjs → chunk-PHOUQACM.mjs} +1 -1
  39. package/dist/lib/node-esm/{chunk-BSYAFUZ7.mjs.map → chunk-PHOUQACM.mjs.map} +2 -2
  40. package/dist/lib/node-esm/{chunk-T2URISUU.mjs → chunk-R6A7Z4LU.mjs} +55 -11
  41. package/dist/lib/node-esm/chunk-R6A7Z4LU.mjs.map +7 -0
  42. package/dist/lib/node-esm/index.mjs +8 -7
  43. package/dist/lib/node-esm/index.mjs.map +3 -3
  44. package/dist/lib/node-esm/{intent-dispatcher-7GCMKMSQ.mjs → intent-dispatcher-YDE2ONZA.mjs} +2 -2
  45. package/dist/lib/node-esm/{intent-resolver-7T6UOQOS.mjs → intent-resolver-LAGJ7LXM.mjs} +3 -3
  46. package/dist/lib/node-esm/meta.json +1 -1
  47. package/dist/lib/node-esm/{store-PTKVELJP.mjs → store-EYSUVNCS.mjs} +2 -2
  48. package/dist/lib/node-esm/testing/index.mjs +4 -3
  49. package/dist/lib/node-esm/testing/index.mjs.map +3 -3
  50. package/dist/lib/node-esm/worker.mjs +1 -1
  51. package/dist/types/src/App.d.ts +4 -2
  52. package/dist/types/src/App.d.ts.map +1 -1
  53. package/dist/types/src/common/layout.d.ts +1 -0
  54. package/dist/types/src/common/layout.d.ts.map +1 -1
  55. package/dist/types/src/common/surface.d.ts +4 -7
  56. package/dist/types/src/common/surface.d.ts.map +1 -1
  57. package/dist/types/src/core/capabilities.d.ts +5 -3
  58. package/dist/types/src/core/capabilities.d.ts.map +1 -1
  59. package/dist/types/src/core/plugin.d.ts +1 -1
  60. package/dist/types/src/core/plugin.d.ts.map +1 -1
  61. package/dist/types/src/playground/playground.stories.d.ts.map +1 -1
  62. package/dist/types/src/plugin-intent/actions.d.ts +14 -0
  63. package/dist/types/src/plugin-intent/actions.d.ts.map +1 -1
  64. package/dist/types/src/plugin-intent/intent-dispatcher.d.ts +1 -1
  65. package/dist/types/src/plugin-intent/intent-dispatcher.d.ts.map +1 -1
  66. package/dist/types/src/plugin-settings/actions.d.ts.map +1 -1
  67. package/dist/types/src/react/Surface.d.ts.map +1 -1
  68. package/package.json +20 -19
  69. package/src/App.tsx +65 -8
  70. package/src/common/layout.ts +1 -0
  71. package/src/common/surface.ts +6 -1
  72. package/src/core/capabilities.test.ts +3 -2
  73. package/src/core/capabilities.ts +11 -3
  74. package/src/core/manager.test.ts +30 -30
  75. package/src/core/manager.ts +2 -2
  76. package/src/core/plugin.ts +1 -1
  77. package/src/playground/debug/plugin.ts +1 -1
  78. package/src/playground/generator/generator.ts +1 -1
  79. package/src/playground/generator/plugin.ts +1 -1
  80. package/src/playground/layout/plugin.ts +1 -1
  81. package/src/playground/logger/plugin.ts +1 -1
  82. package/src/playground/playground.stories.tsx +5 -1
  83. package/src/plugin-intent/IntentPlugin.tsx +1 -1
  84. package/src/plugin-intent/actions.ts +11 -0
  85. package/src/plugin-intent/intent-dispatcher.ts +18 -5
  86. package/src/plugin-settings/SettingsPlugin.ts +1 -1
  87. package/src/plugin-settings/actions.ts +2 -0
  88. package/src/react/Surface.tsx +5 -3
  89. package/src/testing/withPluginManager.stories.tsx +2 -2
  90. package/src/testing/withPluginManager.tsx +1 -1
  91. package/dist/lib/browser/chunk-CIP4C47B.mjs.map +0 -7
  92. package/dist/lib/browser/chunk-PWAAJE2Z.mjs.map +0 -7
  93. package/dist/lib/node/chunk-JLQABYHB.cjs.map +0 -7
  94. package/dist/lib/node/chunk-XP4TI5DS.cjs.map +0 -7
  95. package/dist/lib/node-esm/chunk-NIVSBD3D.mjs.map +0 -7
  96. package/dist/lib/node-esm/chunk-T2URISUU.mjs.map +0 -7
  97. /package/dist/lib/browser/{app-graph-builder-IAEV7KKF.mjs.map → app-graph-builder-576BHZC7.mjs.map} +0 -0
  98. /package/dist/lib/browser/{intent-dispatcher-UR566RVO.mjs.map → intent-dispatcher-3Q67MHZZ.mjs.map} +0 -0
  99. /package/dist/lib/browser/{intent-resolver-LEJ3X6CP.mjs.map → intent-resolver-O763LCLG.mjs.map} +0 -0
  100. /package/dist/lib/browser/{store-5TN4OLFQ.mjs.map → store-URSN7DZI.mjs.map} +0 -0
  101. /package/dist/lib/node/{app-graph-builder-3RWOZENP.cjs.map → app-graph-builder-JZCSKYPY.cjs.map} +0 -0
  102. /package/dist/lib/node/{intent-resolver-34J5IM27.cjs.map → intent-resolver-3F4POWAM.cjs.map} +0 -0
  103. /package/dist/lib/node/{store-3I3UFH5D.cjs.map → store-OFDHTDCB.cjs.map} +0 -0
  104. /package/dist/lib/node-esm/{app-graph-builder-RG3DT2OD.mjs.map → app-graph-builder-VYKLSMSZ.mjs.map} +0 -0
  105. /package/dist/lib/node-esm/{intent-dispatcher-7GCMKMSQ.mjs.map → intent-dispatcher-YDE2ONZA.mjs.map} +0 -0
  106. /package/dist/lib/node-esm/{intent-resolver-7T6UOQOS.mjs.map → intent-resolver-LAGJ7LXM.mjs.map} +0 -0
  107. /package/dist/lib/node-esm/{store-PTKVELJP.mjs.map → store-EYSUVNCS.mjs.map} +0 -0
package/src/App.tsx CHANGED
@@ -3,7 +3,7 @@
3
3
  //
4
4
 
5
5
  import { effect } from '@preact/signals-core';
6
- import React, { type PropsWithChildren, type ReactNode } from 'react';
6
+ import React, { useEffect, useState, type FC, type PropsWithChildren } from 'react';
7
7
 
8
8
  import { invariant } from '@dxos/invariant';
9
9
  import { create } from '@dxos/live-object';
@@ -21,7 +21,7 @@ export type CreateAppOptions = {
21
21
  plugins?: Plugin[];
22
22
  core?: string[];
23
23
  defaults?: string[];
24
- placeholder?: ReactNode;
24
+ placeholder?: FC<{ stage: number }>;
25
25
  fallback?: ErrorBoundary['props']['fallback'];
26
26
  cacheEnabled?: boolean;
27
27
  };
@@ -56,7 +56,7 @@ export const createApp = ({
56
56
  plugins = [],
57
57
  core = plugins.map(({ meta }) => meta.id),
58
58
  defaults = [],
59
- placeholder = null,
59
+ placeholder,
60
60
  fallback = DefaultFallback,
61
61
  cacheEnabled = false,
62
62
  }: CreateAppOptions) => {
@@ -110,22 +110,79 @@ export const createApp = ({
110
110
  );
111
111
  };
112
112
 
113
- type AppProps = Required<Pick<CreateAppOptions, 'placeholder'>> & {
113
+ const DELAY_PLACEHOLDER = 2_000;
114
+
115
+ enum LoadingState {
116
+ Loading = 0,
117
+ FadeIn = 1,
118
+ FadeOut = 2,
119
+ Done = 3,
120
+ }
121
+
122
+ /**
123
+ * To avoid "flashing" the placeholder, we wait a period of time before starting the loading animation.
124
+ * If loading completes during this time the placehoder is not shown, otherwise is it displayed for a minimum period of time.
125
+ *
126
+ * States:
127
+ * 0: Loading - Wait for a period of time before starting the loading animation.
128
+ * 1: Fade-in - Display a loading animation.
129
+ * 2: Fade-out - Fade out the loading animation.
130
+ * 3: Done - Remove the placeholder.
131
+ */
132
+ const useLoading = (state: AppProps['state']) => {
133
+ const [stage, setStage] = useState<LoadingState>(LoadingState.Loading);
134
+ useEffect(() => {
135
+ const i = setInterval(() => {
136
+ setStage((tick) => {
137
+ switch (tick) {
138
+ case LoadingState.Loading:
139
+ if (!state.ready) {
140
+ return LoadingState.FadeIn;
141
+ } else {
142
+ clearInterval(i);
143
+ return LoadingState.Done;
144
+ }
145
+ case LoadingState.FadeIn:
146
+ if (state.ready) {
147
+ return LoadingState.FadeOut;
148
+ }
149
+ break;
150
+ case LoadingState.FadeOut:
151
+ clearInterval(i);
152
+ return LoadingState.Done;
153
+ }
154
+
155
+ return tick;
156
+ });
157
+ }, DELAY_PLACEHOLDER);
158
+
159
+ return () => clearInterval(i);
160
+ }, []);
161
+
162
+ return stage;
163
+ };
164
+
165
+ type AppProps = Pick<CreateAppOptions, 'placeholder'> & {
114
166
  state: { ready: boolean; error: unknown };
115
167
  };
116
168
 
117
- const App = ({ placeholder, state }: AppProps) => {
169
+ const App = ({ placeholder: Placeholder, state }: AppProps) => {
118
170
  const reactContexts = useCapabilities(Capabilities.ReactContext);
119
171
  const reactRoots = useCapabilities(Capabilities.ReactRoot);
172
+ const stage = useLoading(state);
120
173
 
121
174
  if (state.error) {
122
- // This trigger the error boundary to provide UI feedback for the startup error.
175
+ // This triggers the error boundary to provide UI feedback for the startup error.
123
176
  throw state.error;
124
177
  }
125
178
 
126
179
  // TODO(wittjosiah): Consider using Suspense instead?
127
- if (!state.ready) {
128
- return <>{placeholder}</>;
180
+ if (stage < LoadingState.Done) {
181
+ if (!Placeholder) {
182
+ return null;
183
+ }
184
+
185
+ return <Placeholder stage={stage} />;
129
186
  }
130
187
 
131
188
  const ComposedContext = composeContexts(reactContexts);
@@ -176,6 +176,7 @@ export namespace LayoutAction {
176
176
  options: S.optional(
177
177
  S.Struct({
178
178
  state: S.optional(S.Literal(true).annotations({ description: 'The items are being added.' })),
179
+ variant: S.optional(S.String.annotations({ description: 'The variant of the item to open.' })),
179
180
  key: S.optional(
180
181
  S.String.annotations({ description: 'If provided, will replace item with a matching key (id prefix).' }),
181
182
  ),
@@ -78,4 +78,9 @@ export type SurfaceDefinition<T extends Record<string, any> = any> = Readonly<{
78
78
  component: SurfaceComponent<GuardedType<SurfaceDefinition<T>['filter']>>;
79
79
  }>;
80
80
 
81
- export const createSurface = <T extends Record<string, any> = any>(definition: SurfaceDefinition<T>) => definition;
81
+ /**
82
+ * Creates a surface definition.
83
+ */
84
+ export const createSurface = <T extends Record<string, any> = any>(
85
+ definition: SurfaceDefinition<T>,
86
+ ): SurfaceDefinition<T> => definition;
@@ -2,6 +2,7 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
+ import { Effect } from 'effect';
5
6
  import { describe, expect, it } from 'vitest';
6
7
 
7
8
  import { updateCounter } from '@dxos/echo-schema/testing';
@@ -12,8 +13,8 @@ import { defineCapability, PluginsContext } from './capabilities';
12
13
  registerSignalsRuntime();
13
14
 
14
15
  const defaultOptions = {
15
- activate: () => Promise.resolve(false),
16
- reset: () => Promise.resolve(false),
16
+ activate: () => Effect.succeed(false),
17
+ reset: () => Effect.succeed(false),
17
18
  };
18
19
 
19
20
  describe('PluginsContext', () => {
@@ -6,7 +6,7 @@
6
6
  //
7
7
 
8
8
  import { effect, untracked } from '@preact/signals-core';
9
- import { type Effect } from 'effect';
9
+ import { Effect } from 'effect';
10
10
 
11
11
  import { Trigger } from '@dxos/async';
12
12
  import { invariant } from '@dxos/invariant';
@@ -56,8 +56,8 @@ export type Capability<T> = {
56
56
  export type AnyCapability = Capability<any>;
57
57
 
58
58
  type PluginsContextOptions = {
59
- activate: (event: ActivationEvent) => MaybePromise<boolean>;
60
- reset: (event: ActivationEvent) => MaybePromise<boolean>;
59
+ activate: (event: ActivationEvent) => Effect.Effect<boolean, Error>;
60
+ reset: (event: ActivationEvent) => Effect.Effect<boolean, Error>;
61
61
  };
62
62
 
63
63
  // NOTE: This is implemented as a class to prevent it from being proxied by PluginManager state.
@@ -219,4 +219,12 @@ export class PluginsContext {
219
219
  unsubscribe();
220
220
  return capability;
221
221
  }
222
+
223
+ async activatePromise(event: ActivationEvent): Promise<boolean> {
224
+ return this.activate(event).pipe(Effect.runPromise);
225
+ }
226
+
227
+ async resetPromise(event: ActivationEvent): Promise<boolean> {
228
+ return this.reset(event).pipe(Effect.runPromise);
229
+ }
222
230
  }
@@ -28,7 +28,7 @@ const Total = defineCapability<{ total: number }>('dxos.org/test/total');
28
28
  const CountEvent = defineEvent('dxos.org/test/count');
29
29
  const FailEvent = defineEvent('dxos.org/test/fail');
30
30
 
31
- const testMeta = { id: 'dxos.org/plugin/test' };
31
+ const testMeta = { id: 'dxos.org/plugin/test', name: 'Test' };
32
32
 
33
33
  describe('PluginManager', () => {
34
34
  let plugins: Plugin[] = [];
@@ -180,46 +180,46 @@ describe('PluginManager', () => {
180
180
  });
181
181
 
182
182
  it('should be able to fire custom activation events', async () => {
183
- const One = definePlugin({ id: 'dxos.org/test/one' }, [
183
+ const Plugin1 = definePlugin({ id: 'dxos.org/test/plugin-1', name: 'Plugin 1' }, [
184
184
  defineModule({
185
- id: 'dxos.org/test/one',
185
+ id: 'dxos.org/test/plugin-1',
186
186
  activatesOn: CountEvent,
187
187
  activate: () => [contributes(Number, { number: 1 })],
188
188
  }),
189
189
  ]);
190
- const Two = definePlugin({ id: 'dxos.org/test/two' }, [
190
+ const Plugin2 = definePlugin({ id: 'dxos.org/test/plugin-2', name: 'Plugin 2' }, [
191
191
  defineModule({
192
- id: 'dxos.org/test/two',
192
+ id: 'dxos.org/test/plugin-2',
193
193
  activatesOn: CountEvent,
194
194
  activate: () => [contributes(Number, { number: 2 })],
195
195
  }),
196
196
  ]);
197
- const Three = definePlugin({ id: 'dxos.org/test/three' }, [
197
+ const Plugin3 = definePlugin({ id: 'dxos.org/test/plugin-3', name: 'Plugin 3' }, [
198
198
  defineModule({
199
- id: 'dxos.org/test/three',
199
+ id: 'dxos.org/test/plugin-3',
200
200
  activatesOn: CountEvent,
201
201
  activate: () => [contributes(Number, { number: 3 })],
202
202
  }),
203
203
  ]);
204
- plugins = [One, Two, Three];
204
+ plugins = [Plugin1, Plugin2, Plugin3];
205
205
 
206
206
  const manager = new PluginManager({ pluginLoader });
207
207
  expect(manager.active).toEqual([]);
208
208
  expect(manager.context.requestCapabilities(Number)).toHaveLength(0);
209
209
 
210
- await manager.add(One.meta.id);
210
+ await manager.add(Plugin1.meta.id);
211
211
  await manager.activate(CountEvent);
212
- expect(manager.active).toEqual([One.meta.id]);
212
+ expect(manager.active).toEqual([Plugin1.meta.id]);
213
213
  expect(manager.context.requestCapabilities(Number)).toHaveLength(1);
214
214
 
215
- await manager.add(Two.meta.id);
215
+ await manager.add(Plugin2.meta.id);
216
216
  await manager.activate(CountEvent);
217
- expect(manager.active).toEqual([One.meta.id, Two.meta.id]);
217
+ expect(manager.active).toEqual([Plugin1.meta.id, Plugin2.meta.id]);
218
218
  expect(manager.context.requestCapabilities(Number)).toHaveLength(2);
219
219
 
220
- await manager.add(Three.meta.id);
220
+ await manager.add(Plugin3.meta.id);
221
221
  await manager.activate(CountEvent);
222
- expect(manager.active).toEqual([One.meta.id, Two.meta.id, Three.meta.id]);
222
+ expect(manager.active).toEqual([Plugin1.meta.id, Plugin2.meta.id, Plugin3.meta.id]);
223
223
  expect(manager.context.requestCapabilities(Number)).toHaveLength(3);
224
224
  });
225
225
 
@@ -283,7 +283,7 @@ describe('PluginManager', () => {
283
283
  state.total = numbers.reduce((acc, n) => acc + n.number, 0);
284
284
  };
285
285
 
286
- const Count = definePlugin({ id: 'dxos.org/test/count' }, [
286
+ const Count = definePlugin({ id: 'dxos.org/test/count', name: 'Count' }, [
287
287
  defineModule({
288
288
  id: 'dxos.org/test/count',
289
289
  activatesOn: Events.Startup,
@@ -297,17 +297,17 @@ describe('PluginManager', () => {
297
297
 
298
298
  const Test = definePlugin(testMeta, [
299
299
  defineModule({
300
- id: 'dxos.org/test/one',
300
+ id: 'dxos.org/test/plugin-1',
301
301
  activatesOn: CountEvent,
302
302
  activate: () => contributes(Number, { number: 1 }),
303
303
  }),
304
304
  defineModule({
305
- id: 'dxos.org/test/two',
305
+ id: 'dxos.org/test/plugin-2',
306
306
  activatesOn: CountEvent,
307
307
  activate: () => contributes(Number, { number: 2 }),
308
308
  }),
309
309
  defineModule({
310
- id: 'dxos.org/test/three',
310
+ id: 'dxos.org/test/plugin-3',
311
311
  activatesOn: CountEvent,
312
312
  activate: () => contributes(Number, { number: 3 }),
313
313
  }),
@@ -390,28 +390,28 @@ describe('PluginManager', () => {
390
390
  });
391
391
 
392
392
  it('should be reactive', async () => {
393
- const One = definePlugin({ id: 'dxos.org/test/one' }, [
393
+ const Plugin1 = definePlugin({ id: 'dxos.org/test/plugin-1', name: 'Plugin 1' }, [
394
394
  defineModule({
395
- id: 'dxos.org/test/one',
395
+ id: 'dxos.org/test/plugin-1',
396
396
  activatesOn: CountEvent,
397
397
  activate: () => [contributes(Number, { number: 1 })],
398
398
  }),
399
399
  ]);
400
- const Two = definePlugin({ id: 'dxos.org/test/two' }, [
400
+ const Plugin2 = definePlugin({ id: 'dxos.org/test/plugin-2', name: 'Plugin 2' }, [
401
401
  defineModule({
402
- id: 'dxos.org/test/two',
402
+ id: 'dxos.org/test/plugin-2',
403
403
  activatesOn: CountEvent,
404
404
  activate: () => [contributes(Number, { number: 2 })],
405
405
  }),
406
406
  ]);
407
- const Three = definePlugin({ id: 'dxos.org/test/three' }, [
407
+ const Plugin3 = definePlugin({ id: 'dxos.org/test/plugin-3', name: 'Plugin 3' }, [
408
408
  defineModule({
409
- id: 'dxos.org/test/three',
409
+ id: 'dxos.org/test/plugin-3',
410
410
  activatesOn: CountEvent,
411
411
  activate: () => [contributes(Number, { number: 3 })],
412
412
  }),
413
413
  ]);
414
- plugins = [One, Two, Three];
414
+ plugins = [Plugin1, Plugin2, Plugin3];
415
415
 
416
416
  const manager = new PluginManager({ pluginLoader });
417
417
  using pluginUpdates = updateCounter(() => {
@@ -439,7 +439,7 @@ describe('PluginManager', () => {
439
439
  expect(eventsFiredUpdates.count).toEqual(0);
440
440
  expect(pendingResetUpdates.count).toEqual(0);
441
441
 
442
- await manager.add(One.meta.id);
442
+ await manager.add(Plugin1.meta.id);
443
443
  expect(pluginUpdates.count).toEqual(1);
444
444
  expect(enabledUpdates.count).toEqual(1);
445
445
  expect(modulesUpdates.count).toEqual(1);
@@ -455,7 +455,7 @@ describe('PluginManager', () => {
455
455
  expect(eventsFiredUpdates.count).toEqual(1);
456
456
  expect(pendingResetUpdates.count).toEqual(0);
457
457
 
458
- await manager.add(Two.meta.id);
458
+ await manager.add(Plugin2.meta.id);
459
459
  expect(pluginUpdates.count).toEqual(2);
460
460
  expect(enabledUpdates.count).toEqual(2);
461
461
  expect(modulesUpdates.count).toEqual(2);
@@ -471,7 +471,7 @@ describe('PluginManager', () => {
471
471
  expect(eventsFiredUpdates.count).toEqual(1);
472
472
  expect(pendingResetUpdates.count).toEqual(2);
473
473
 
474
- await manager.add(Three.meta.id);
474
+ await manager.add(Plugin3.meta.id);
475
475
  expect(pluginUpdates.count).toEqual(3);
476
476
  expect(enabledUpdates.count).toEqual(3);
477
477
  expect(modulesUpdates.count).toEqual(3);
@@ -488,7 +488,7 @@ describe('PluginManager', () => {
488
488
  expect(eventsFiredUpdates.count).toEqual(1);
489
489
  expect(pendingResetUpdates.count).toEqual(4);
490
490
 
491
- await manager.disable(One.meta.id);
491
+ await manager.disable(Plugin1.meta.id);
492
492
  expect(pluginUpdates.count).toEqual(3);
493
493
  expect(enabledUpdates.count).toEqual(4);
494
494
  expect(modulesUpdates.count).toEqual(4);
@@ -496,7 +496,7 @@ describe('PluginManager', () => {
496
496
  expect(eventsFiredUpdates.count).toEqual(1);
497
497
  expect(pendingResetUpdates.count).toEqual(4);
498
498
 
499
- await manager.remove(One.meta.id);
499
+ await manager.remove(Plugin1.meta.id);
500
500
  expect(pluginUpdates.count).toEqual(4);
501
501
  expect(enabledUpdates.count).toEqual(4);
502
502
  expect(modulesUpdates.count).toEqual(4);
@@ -45,8 +45,8 @@ export class PluginManager {
45
45
  readonly activation = new Event<{ event: string; state: 'activating' | 'activated' | 'error'; error?: any }>();
46
46
 
47
47
  readonly context = new PluginsContext({
48
- activate: (event) => this.activate(event),
49
- reset: (id) => this.reset(id),
48
+ activate: (event) => this._activate(event),
49
+ reset: (id) => this._reset(id),
50
50
  });
51
51
 
52
52
  private readonly _state: ReactiveObject<PluginManagerState>;
@@ -79,7 +79,7 @@ export type PluginMeta = {
79
79
  /**
80
80
  * Human-readable name.
81
81
  */
82
- name?: string;
82
+ name: string;
83
83
 
84
84
  /**
85
85
  * Short description of plugin functionality.
@@ -8,7 +8,7 @@ import { defineModule, definePlugin, lazy } from '../../core';
8
8
  const Debug = lazy(() => import('./Debug'));
9
9
 
10
10
  export const DebugPlugin = () =>
11
- definePlugin({ id: 'dxos.org/test/debug' }, [
11
+ definePlugin({ id: 'dxos.org/test/plugin-debug', name: 'Debug' }, [
12
12
  defineModule({
13
13
  id: 'dxos.org/test/debug/main',
14
14
  activatesOn: Events.Startup,
@@ -26,7 +26,7 @@ export const createGeneratorIntent = (id: string) => {
26
26
  export const createNumberPlugin = (id: string) => {
27
27
  const number = Math.floor(Math.random() * 100);
28
28
 
29
- return definePlugin({ id }, [
29
+ return definePlugin({ id, name: `Plugin ${id}` }, [
30
30
  defineModule({
31
31
  id: `${id}/main`,
32
32
  activatesOn: CountEvent,
@@ -9,7 +9,7 @@ const Main = lazy(() => import('./Main'));
9
9
  const Toolbar = lazy(() => import('./Toolbar'));
10
10
 
11
11
  export const GeneratorPlugin = () =>
12
- definePlugin({ id: 'dxos.org/test/generator' }, [
12
+ definePlugin({ id: 'dxos.org/test/generator', name: 'Generator' }, [
13
13
  defineModule({
14
14
  id: 'dxos.org/test/generator/main',
15
15
  activatesOn: Events.Startup,
@@ -8,7 +8,7 @@ import { definePlugin, lazy, defineModule } from '../../core';
8
8
  const Layout = lazy(() => import('./Layout'));
9
9
 
10
10
  export const LayoutPlugin = () =>
11
- definePlugin({ id: 'dxos.org/test/layout' }, [
11
+ definePlugin({ id: 'dxos.org/test/layout', name: 'Layout' }, [
12
12
  defineModule({
13
13
  id: 'dxos.org/test/layout/root',
14
14
  activatesOn: Events.Startup,
@@ -12,7 +12,7 @@ import { createResolver } from '../../plugin-intent';
12
12
  const Toolbar = lazy(() => import('./Toolbar'));
13
13
 
14
14
  export const LoggerPlugin = () =>
15
- definePlugin({ id: 'dxos.org/test/logger' }, [
15
+ definePlugin({ id: 'dxos.org/test/logger', name: 'Logger' }, [
16
16
  defineModule({
17
17
  id: 'dxos.org/test/logger/intents',
18
18
  activatesOn: Events.SetupIntentResolver,
@@ -17,12 +17,16 @@ import { IntentPlugin } from '../plugin-intent';
17
17
 
18
18
  const plugins = [IntentPlugin(), LayoutPlugin(), DebugPlugin(), LoggerPlugin(), GeneratorPlugin()];
19
19
 
20
+ const Placeholder = () => {
21
+ return <div>Loading...</div>;
22
+ };
23
+
20
24
  const Story = createApp({
21
25
  pluginLoader: (id) => createNumberPlugin(id),
22
26
  plugins,
23
27
  core: plugins.map((plugin) => plugin.meta.id),
24
28
  // Having a non-empty placeholder makes it clear if it's taking a while to load.
25
- placeholder: <div>Loading...</div>,
29
+ placeholder: Placeholder,
26
30
  });
27
31
 
28
32
  export const Playground = {};
@@ -7,7 +7,7 @@ import { Events } from '../common';
7
7
  import { defineModule, definePlugin, lazy } from '../core';
8
8
 
9
9
  export const IntentPlugin = () =>
10
- definePlugin({ id: INTENT_PLUGIN }, [
10
+ definePlugin({ id: INTENT_PLUGIN, name: 'Intent' }, [
11
11
  defineModule({
12
12
  id: `${INTENT_PLUGIN}/module/dispatcher`,
13
13
  // TODO(wittjosiah): This will mean that startup needs to be reset when intents are added or removed.
@@ -10,6 +10,17 @@ export const INTENT_PLUGIN = 'dxos.org/plugin/intent';
10
10
  export const INTENT_ACTION = `${INTENT_PLUGIN}/action`;
11
11
 
12
12
  export namespace IntentAction {
13
+ /**
14
+ * Log an intent.
15
+ */
16
+ export class Track extends S.TaggedClass<Track>()(`${INTENT_ACTION}/track`, {
17
+ input: S.Struct({
18
+ intents: S.Array(S.String),
19
+ error: S.optional(S.String),
20
+ }),
21
+ output: S.Void,
22
+ }) {}
23
+
13
24
  /**
14
25
  * Fired after an intent is dispatched if the intent is undoable.
15
26
  */
@@ -83,7 +83,9 @@ export type IntentDispatcherResult<Input, Output> = Pick<IntentEffectResult<Inpu
83
83
  export type IntentEffectDefinition<Input, Output> = (
84
84
  data: Input,
85
85
  undo: boolean,
86
- ) => MaybePromise<IntentEffectResult<Input, Output> | void> | Effect.Effect<IntentEffectResult<Input, Output> | void>;
86
+ ) =>
87
+ | MaybePromise<IntentEffectResult<Input, Output> | void>
88
+ | Effect.Effect<IntentEffectResult<Input, Output> | void, Error>;
87
89
 
88
90
  /**
89
91
  * Intent resolver to match intents to their effects.
@@ -174,8 +176,8 @@ export const createDispatcher = (
174
176
  const handleIntent = (intent: AnyIntent) =>
175
177
  Effect.gen(function* () {
176
178
  const candidates = getResolvers(intent.module)
177
- .filter((r) => r.intent._tag === intent.id)
178
- .filter((r) => !r.filter || r.filter(intent.data))
179
+ .filter((resolver) => resolver.intent._tag === intent.id)
180
+ .filter((resolver) => !resolver.filter || resolver.filter(intent.data))
179
181
  .toSorted(byPosition);
180
182
  if (candidates.length === 0) {
181
183
  yield* Effect.fail(new NoResolversError(intent.id));
@@ -199,17 +201,28 @@ export const createDispatcher = (
199
201
  yield* Ref.update(resultsRef, (results) => [result, ...results]);
200
202
  if (result.intents) {
201
203
  for (const intent of result.intents) {
202
- // Returned intents are dispatched but not yielded into results,
203
- // as such they cannot be undone.
204
+ // Returned intents are dispatched but not yielded into results, as such they cannot be undone.
204
205
  // TODO(wittjosiah): Use higher execution concurrency?
205
206
  yield* dispatch(intent, depth + 1);
206
207
  }
207
208
  }
209
+
208
210
  if (result.error) {
211
+ // yield* dispatch(
212
+ // createIntent(IntentAction.Track, {
213
+ // intents: intentChain.all.map((i) => i.id),
214
+ // error: result.error.message,
215
+ // }),
216
+ // );
209
217
  yield* Effect.fail(result.error);
210
218
  }
211
219
  }
212
220
 
221
+ // Track the intent chain.
222
+ // if (intentChain.all.some((intent) => intent.id !== IntentAction.Track._tag)) {
223
+ // yield* dispatch(createIntent(IntentAction.Track, { intents: intentChain.all.map((i) => i.id) }));
224
+ // }
225
+
213
226
  const results = yield* resultsRef.get;
214
227
  const result = results[0];
215
228
  yield* Ref.update(historyRef, (history) => {
@@ -9,7 +9,7 @@ import { contributes, defineModule, definePlugin, lazy } from '../core';
9
9
 
10
10
  // TODO(wittjosiah): Add options to exclude some modules.
11
11
  export const SettingsPlugin = () =>
12
- definePlugin({ id: SETTINGS_PLUGIN }, [
12
+ definePlugin({ id: SETTINGS_PLUGIN, name: 'Settings' }, [
13
13
  defineModule({
14
14
  id: `${SETTINGS_PLUGIN}/module/store`,
15
15
  activatesOn: Events.Startup,
@@ -6,6 +6,8 @@ import { Schema as S } from 'effect';
6
6
 
7
7
  export const SETTINGS_PLUGIN = 'dxos.org/plugin/settings';
8
8
  export const SETTINGS_ACTION = `${SETTINGS_PLUGIN}/action`;
9
+ // TODO(wittjosiah): This is a hack to prevent the previous deck from being set for pinned items.
10
+ // Ideally this should be worked into the data model in a generic way.
9
11
  export const SETTINGS_ID = '!dxos:settings';
10
12
  export const SETTINGS_KEY = 'settings';
11
13
 
@@ -2,7 +2,7 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import React, { memo, forwardRef, Suspense, useMemo } from 'react';
5
+ import React, { memo, forwardRef, Suspense, useMemo, Fragment } from 'react';
6
6
 
7
7
  import { useDefaultValue } from '@dxos/react-hooks';
8
8
  import { byPosition } from '@dxos/util';
@@ -12,6 +12,8 @@ import { useCapabilities } from './useCapabilities';
12
12
  import { Capabilities, type SurfaceDefinition, type SurfaceProps } from '../common';
13
13
  import { type PluginsContext } from '../core';
14
14
 
15
+ const DEFAULT_PLACEHOLDER = <Fragment />;
16
+
15
17
  /**
16
18
  * @internal
17
19
  */
@@ -43,7 +45,7 @@ export const isSurfaceAvailable = (context: PluginsContext, { role, data }: Pick
43
45
  */
44
46
  export const Surface = memo(
45
47
  forwardRef<HTMLElement, SurfaceProps>(
46
- ({ id: _id, role, data: _data, limit, fallback, placeholder, ...rest }, forwardedRef) => {
48
+ ({ id: _id, role, data: _data, limit, fallback, placeholder = DEFAULT_PLACEHOLDER, ...rest }, forwardedRef) => {
47
49
  // TODO(wittjosiah): This will make all surfaces depend on a single signal.
48
50
  // This isn't ideal because it means that any change to the data will cause all surfaces to re-render.
49
51
  // This effectively means that plugin modules which contribute surfaces need to all be activated at startup.
@@ -59,7 +61,7 @@ export const Surface = memo(
59
61
  <Component ref={forwardedRef} key={id} id={id} role={role} data={data} limit={limit} {...rest} />
60
62
  ));
61
63
 
62
- const suspense = placeholder ? <Suspense fallback={placeholder}>{nodes}</Suspense> : nodes;
64
+ const suspense = <Suspense fallback={placeholder}>{nodes}</Suspense>;
63
65
 
64
66
  return fallback ? (
65
67
  <ErrorBoundary data={data} fallback={fallback}>
@@ -12,7 +12,7 @@ import { Capabilities, createSurface } from '../common';
12
12
  import { contributes } from '../core';
13
13
  import { Surface } from '../react';
14
14
 
15
- const Render = () => {
15
+ const DefaultStory = () => {
16
16
  console.log('Render');
17
17
  return (
18
18
  <div>
@@ -24,7 +24,7 @@ const Render = () => {
24
24
 
25
25
  const meta: Meta = {
26
26
  title: 'sdk/app-framework/withPluginManager',
27
- render: Render,
27
+ render: DefaultStory,
28
28
  decorators: [
29
29
  withTheme,
30
30
  withPluginManager({
@@ -86,6 +86,6 @@ export const withPluginManager = (options: WithPluginManagerOptions = {}): Decor
86
86
  // This is necessary because `createApp` expects the startup event to complete before the app is ready.
87
87
  const STORY_PLUGIN = 'dxos.org/app-framework/story';
88
88
  const StoryPlugin = () =>
89
- definePlugin({ id: STORY_PLUGIN }, [
89
+ definePlugin({ id: STORY_PLUGIN, name: 'Story' }, [
90
90
  defineModule({ id: STORY_PLUGIN, activatesOn: Events.Startup, activate: () => [] }),
91
91
  ]);