@dxos/app-framework 0.14.0 → 0.16.0

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 (129) hide show
  1. package/CHANGELOG.md +73 -0
  2. package/dist/lib/App.mjs +1 -2
  3. package/dist/lib/Hooks.mjs +1 -2
  4. package/dist/lib/chunk-App.mjs +11 -0
  5. package/dist/lib/chunk-App.mjs.map +1 -0
  6. package/dist/lib/chunk-Cli.mjs +8 -0
  7. package/dist/lib/chunk-Cli.mjs.map +1 -0
  8. package/dist/lib/chunk-Devtools.mjs +112 -0
  9. package/dist/lib/chunk-Devtools.mjs.map +1 -0
  10. package/dist/lib/chunk-Hooks2.mjs +35 -0
  11. package/dist/lib/chunk-Hooks2.mjs.map +1 -0
  12. package/dist/lib/chunk-PluginManagerProvider.mjs +12 -0
  13. package/dist/lib/chunk-PluginManagerProvider.mjs.map +1 -0
  14. package/dist/lib/chunk-ProcessManagerPlugin.mjs +3 -3
  15. package/dist/lib/chunk-ProcessManagerPlugin.mjs.map +1 -1
  16. package/dist/lib/chunk-Role.mjs +19 -0
  17. package/dist/lib/chunk-Role.mjs.map +1 -0
  18. package/dist/lib/chunk-SharedPackages.mjs +11 -0
  19. package/dist/lib/chunk-SharedPackages.mjs.map +1 -0
  20. package/dist/lib/chunk-Surface.mjs.map +1 -1
  21. package/dist/lib/chunk-Surface2.mjs +37 -0
  22. package/dist/lib/chunk-Surface2.mjs.map +1 -0
  23. package/dist/lib/chunk-SurfaceManager.mjs +3 -3
  24. package/dist/lib/chunk-SurfaceManager.mjs.map +1 -1
  25. package/dist/lib/chunk-activation-event.mjs +86 -0
  26. package/dist/lib/chunk-activation-event.mjs.map +1 -0
  27. package/dist/lib/chunk-activation-events.mjs +66 -0
  28. package/dist/lib/chunk-activation-events.mjs.map +1 -0
  29. package/dist/lib/chunk-capabilities.mjs +202 -0
  30. package/dist/lib/chunk-capabilities.mjs.map +1 -0
  31. package/dist/lib/chunk-capability-manager.mjs +203 -0
  32. package/dist/lib/chunk-capability-manager.mjs.map +1 -0
  33. package/dist/lib/chunk-capability2.mjs +2 -2
  34. package/dist/lib/chunk-capability2.mjs.map +1 -1
  35. package/dist/lib/chunk-history-tracker.mjs +1 -1
  36. package/dist/lib/chunk-hooks.mjs +5 -5
  37. package/dist/lib/chunk-hooks.mjs.map +1 -1
  38. package/dist/lib/chunk-plugin-asset-cache.mjs +28 -0
  39. package/dist/lib/chunk-plugin-asset-cache.mjs.map +1 -0
  40. package/dist/lib/chunk-plugin-manager.mjs +68 -35
  41. package/dist/lib/chunk-plugin-manager.mjs.map +1 -1
  42. package/dist/lib/chunk-plugin-manager2.mjs +22 -0
  43. package/dist/lib/chunk-plugin-manager2.mjs.map +1 -0
  44. package/dist/lib/chunk-plugin-manifest.mjs +126 -0
  45. package/dist/lib/chunk-plugin-manifest.mjs.map +1 -0
  46. package/dist/lib/chunk-plugin.mjs +354 -0
  47. package/dist/lib/chunk-plugin.mjs.map +1 -0
  48. package/dist/lib/chunk-process-manager-capability.mjs +11 -10
  49. package/dist/lib/chunk-process-manager-capability.mjs.map +1 -1
  50. package/dist/lib/chunk-registry.mjs.map +1 -1
  51. package/dist/lib/chunk-undo-mapping.mjs +47 -0
  52. package/dist/lib/{plugin-process-manager/history/undo-mapping.mjs.map → chunk-undo-mapping.mjs.map} +1 -1
  53. package/dist/lib/chunk-undo-registry.mjs +22 -0
  54. package/dist/lib/{plugin-process-manager/history/undo-registry.mjs.map → chunk-undo-registry.mjs.map} +1 -1
  55. package/dist/lib/chunk-url-loader.mjs +345 -0
  56. package/dist/lib/chunk-url-loader.mjs.map +1 -0
  57. package/dist/lib/cli.mjs +3 -3
  58. package/dist/lib/cli.mjs.map +1 -1
  59. package/dist/lib/common/Role.mjs +1 -19
  60. package/dist/lib/common/activation-events.mjs +1 -57
  61. package/dist/lib/common/capabilities.mjs +1 -202
  62. package/dist/lib/config.mjs +1 -2
  63. package/dist/lib/core/activation-event.mjs +1 -86
  64. package/dist/lib/core/capability-manager.mjs +1 -203
  65. package/dist/lib/core/capability.mjs +1 -2
  66. package/dist/lib/core/plugin-asset-cache.mjs +1 -28
  67. package/dist/lib/core/plugin-manager.mjs +2 -19
  68. package/dist/lib/core/plugin-manifest.mjs +1 -126
  69. package/dist/lib/core/plugin.mjs +1 -354
  70. package/dist/lib/core/registry.mjs +1 -2
  71. package/dist/lib/core/url-loader.mjs +1 -345
  72. package/dist/lib/index.mjs +19 -19
  73. package/dist/lib/ns/App.mjs +2 -11
  74. package/dist/lib/ns/Cli.mjs +2 -8
  75. package/dist/lib/ns/Devtools.mjs +1 -112
  76. package/dist/lib/ns/Hooks.mjs +2 -35
  77. package/dist/lib/ns/PluginManagerProvider.mjs +2 -12
  78. package/dist/lib/ns/SharedPackages.mjs +2 -11
  79. package/dist/lib/ns/Surface.mjs +2 -35
  80. package/dist/lib/ns/Translations.mjs +1 -2
  81. package/dist/lib/plugin-cli.mjs +2 -4
  82. package/dist/lib/plugin-cli.mjs.map +1 -1
  83. package/dist/lib/plugin-process-manager/ProcessManagerPlugin.mjs +1 -2
  84. package/dist/lib/plugin-process-manager/history/history-tracker.mjs +1 -2
  85. package/dist/lib/plugin-process-manager/history/undo-mapping.mjs +1 -47
  86. package/dist/lib/plugin-process-manager/history/undo-registry.mjs +1 -22
  87. package/dist/lib/testing/react.mjs +1 -1
  88. package/dist/lib/testing/react.mjs.map +1 -1
  89. package/dist/lib/testing.mjs +5 -5
  90. package/dist/lib/testing.mjs.map +1 -1
  91. package/dist/lib/vite-plugin.mjs +3 -3
  92. package/dist/lib/vite-plugin.mjs.map +1 -1
  93. package/dist/types/src/common/activation-events.d.ts +8 -0
  94. package/dist/types/src/common/activation-events.d.ts.map +1 -1
  95. package/dist/types/src/core/plugin-manager/manager-state.d.ts.map +1 -1
  96. package/dist/types/src/core/plugin-manager/module-loader.d.ts +0 -10
  97. package/dist/types/src/core/plugin-manager/module-loader.d.ts.map +1 -1
  98. package/dist/types/src/core/plugin-manager/plugin-manager.d.ts.map +1 -1
  99. package/dist/types/src/plugin-process-manager/process-manager-capability.d.ts.map +1 -1
  100. package/dist/types/src/ui/components/Surface/SurfaceComponent.stories.d.ts.map +1 -1
  101. package/dist/types/src/ui/hooks/useApp.d.ts.map +1 -1
  102. package/dist/types/tsconfig.tsbuildinfo +1 -1
  103. package/package.json +32 -32
  104. package/src/common/activation-events.ts +9 -0
  105. package/src/core/plugin-manager/manager-state.ts +14 -12
  106. package/src/core/plugin-manager/module-loader.ts +43 -7
  107. package/src/core/plugin-manager/plugin-manager.ts +2 -1
  108. package/src/plugin-process-manager/process-manager-capability.test.ts +66 -1
  109. package/src/plugin-process-manager/process-manager-capability.ts +22 -2
  110. package/src/ui/components/Surface/SurfaceComponent.stories.tsx +9 -7
  111. package/src/ui/hooks/useApp.tsx +2 -1
  112. package/src/vite-plugin/boot-loader/loader-app/boot-loader.css +19 -1
  113. package/dist/lib/common/Role.mjs.map +0 -1
  114. package/dist/lib/common/activation-events.mjs.map +0 -1
  115. package/dist/lib/common/capabilities.mjs.map +0 -1
  116. package/dist/lib/core/activation-event.mjs.map +0 -1
  117. package/dist/lib/core/capability-manager.mjs.map +0 -1
  118. package/dist/lib/core/plugin-asset-cache.mjs.map +0 -1
  119. package/dist/lib/core/plugin-manager.mjs.map +0 -1
  120. package/dist/lib/core/plugin-manifest.mjs.map +0 -1
  121. package/dist/lib/core/plugin.mjs.map +0 -1
  122. package/dist/lib/core/url-loader.mjs.map +0 -1
  123. package/dist/lib/ns/App.mjs.map +0 -1
  124. package/dist/lib/ns/Cli.mjs.map +0 -1
  125. package/dist/lib/ns/Devtools.mjs.map +0 -1
  126. package/dist/lib/ns/Hooks.mjs.map +0 -1
  127. package/dist/lib/ns/PluginManagerProvider.mjs.map +0 -1
  128. package/dist/lib/ns/SharedPackages.mjs.map +0 -1
  129. package/dist/lib/ns/Surface.mjs.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"file":"chunk-hooks.mjs","names":[],"sources":["../../src/ui/hooks/useActivationSignal.ts","../../src/ui/components/App/loader.ts","../../src/ui/components/App/App.tsx","../../src/ui/hooks/startup-watchdog.ts","../../src/ui/hooks/useApp.tsx","../../src/ui/hooks/useCapabilities.ts","../../src/ui/hooks/useLoading.tsx","../../src/ui/hooks/useProcessManagerRuntime.ts","../../src/ui/hooks/useSettingsState.ts","../../src/ui/hooks/useSurface.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport { useEffect } from 'react';\n\nimport * as EffectEx from '@dxos/effect/EffectEx';\n\nimport { type ActivationEvent } from '../../core/index.ts';\nimport { useOptionalPluginManager } from '../components/PluginManager/PluginManagerProvider.ts';\n\n/**\n * Fires an activation event on mount — the UI-side half of demand-driven activation. A container\n * that constitutes the demand signal for policy-parked modules (a create-object picker, an\n * assistant chat) calls this so the parked providers load exactly when the surface first appears;\n * re-fires of an already-dispatched event are cheap no-ops.\n */\nexport const useActivationSignal = (event: ActivationEvent.ActivationEvent): void => {\n const manager = useOptionalPluginManager();\n useEffect(() => {\n if (!manager) {\n return;\n }\n EffectEx.runDetached(manager.activate(event));\n }, [manager, event]);\n};\n","//\n// Copyright 2026 DXOS.org\n//\n\ndeclare global {\n interface BootLoader {\n status: (payload: {\n event?: string;\n module?: string;\n humanized: string;\n /**\n * Optional `(index/total)` tick. When present, the loader replaces the\n * current line in place (\"Loading plugins (12/80)\") instead of appending\n * a new entry — keeps the visible log compact during long counted phases.\n */\n range?: { index: number; total: number };\n }) => void;\n /** Register plugin icons for the loader's activation row; draws nothing until `activated(...)`. */\n plugins?: (entries: { id: string; icon?: string }[]) => void;\n /** Add the registered plugin with this id to the activation row. */\n activated?: (id: string) => void;\n progress: (fraction?: number) => void;\n /**\n * Report that startup has outrun its budget without failing it, offering `onAbort` as the way\n * out. Dev-only — see the canonical doc on `BootLoaderApi.stalled`.\n */\n stalled: (onAbort: () => void) => void;\n ready: () => void;\n dismiss: () => void;\n }\n\n interface Window {\n /**\n * Driver injected by `@dxos/app-framework/vite-plugin`'s `bootLoaderPlugin`\n * (a Solid app inlined into `index.html`). Declared here — on a module that\n * is part of the `@dxos/app-framework/ui` export surface — so the React side\n * and host apps (e.g. composer-app) can drive the loader without each\n * re-declaring the type. The canonical definition lives in the plugin's\n * `loader-app/types.ts`; this mirror exists because that source compiles in a\n * separate (Solid) program that doesn't ship its globals to consumers.\n */\n __bootLoader?: BootLoader;\n }\n}\n\n// Guarded: `@dxos/app-framework/ui` has a `node` export and this module is re-exported through the\n// UI barrel, so a bare top-level `window` read would throw when imported in SSR / tests / tooling.\nexport const bootLoader = typeof window !== 'undefined' ? window.__bootLoader : undefined;\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport React, { type PropsWithChildren, Suspense, useEffect, useLayoutEffect, useMemo, useRef } from 'react';\n\nimport { Capabilities } from '../../../common/index.ts';\nimport { topologicalSort } from '../../../helpers.ts';\nimport {\n FIRST_INTERACTIVE_EVENT,\n LoadingState,\n type StartupProgress,\n type UseAppOptions,\n useCapabilities,\n useLoading,\n} from '../../hooks/index.ts';\nimport { bootLoader } from './loader.ts';\n\nexport type AppProps = Pick<UseAppOptions, 'debounce' | 'verboseStatus'> & {\n ready: boolean;\n error: unknown;\n progress?: StartupProgress;\n};\n\nexport const App = ({ ready, error, debounce, progress, verboseStatus = false }: AppProps) => {\n const reactContexts = useCapabilities(Capabilities.ReactContext);\n const reactRoots = useCapabilities(Capabilities.ReactRoot);\n const sortedContexts = useMemo(() => topologicalSort(reactContexts), [reactContexts]);\n const stage = useLoading(ready, debounce);\n const placeholderDismissed = stage >= LoadingState.Done;\n // The shell mounts a tick EARLIER than the dismissal, at the same stage that starts the loader's\n // outro: the loader is still on screen (z-index 10) and fading, so the real UI paints beneath it\n // instead of after it — gating both on `Done` leaves a blank frame between the two.\n const shellMounted = stage >= LoadingState.FadeOut;\n\n // Activation lines are opt-in (`verboseStatus`): they name framework internals, so the default\n // boot log is the host's own phases only. When enabled, one line per plugin (and per framework\n // activation event) rather than per module — a plugin contributes a dozen-odd modules, which\n // scrolled hundreds of lines past the user. The framework leaves both choices to the host.\n const announcedRef = useRef(new Set<string>());\n\n // Relay the startup lifecycle into the boot loader injected by\n // `@dxos/app-framework/vite-plugin` (a Solid app inlined into `index.html`,\n // the only visible loading UI). Plugin activation fills the `[0.5, 1]` half\n // of the ring; the raw `event` / `module` ids ride along so the loader owns\n // how each transition is rendered and traced. No-op once the outro has\n // started (`FadeOut`), so the dismissal runs uncontended.\n useEffect(() => {\n if (stage >= LoadingState.FadeOut) {\n return;\n }\n\n const fraction = progress?.progress ?? 0;\n bootLoader?.progress(0.5 + fraction * 0.5);\n if (progress?.pluginSlug) {\n // Optional call — see the note on `plugins?.(...)` in `useApp`.\n bootLoader?.activated?.(progress.pluginSlug);\n }\n\n // Plugin-level transitions collapse to the plugin's own name; event-level ones (no\n // `pluginName`) are the framework's own phases and are already few.\n const humanized = progress?.pluginName ?? progress?.humanizedName;\n if (!verboseStatus || !humanized) {\n return;\n }\n\n const key = progress?.pluginName ?? `event:${progress?.event ?? humanized}`;\n if (announcedRef.current.has(key)) {\n return;\n }\n announcedRef.current.add(key);\n bootLoader?.status({\n event: progress?.event,\n module: progress?.module,\n humanized: `Activating ${humanized}`,\n });\n }, [\n stage,\n progress?.progress,\n progress?.event,\n progress?.module,\n progress?.humanizedName,\n progress?.pluginName,\n progress?.pluginSlug,\n verboseStatus,\n ]);\n\n // Hand off at fade-out: play the loader's graceful shrink-and-fade outro.\n // `useLayoutEffect` runs before the next paint so the outro begins in the\n // same frame the real shell starts rendering beneath it.\n useLayoutEffect(() => {\n if (stage >= LoadingState.FadeOut) {\n bootLoader?.ready();\n }\n }, [stage]);\n\n // Emit a once-per-app `app-framework:first-interactive` mark the first time\n // the loader is dismissed and the real app shell renders. Closes the gap\n // between `Startup` activated and the first interactive paint.\n //\n // Also the framework-owned handoff-complete signal: `dismiss()` removes the\n // loader immediately on the fast-load path (where `useLoading` skips\n // `FadeOut`), and is a no-op once an outro is already in flight, so it never\n // cuts the animation short.\n useEffect(() => {\n if (!placeholderDismissed) {\n return;\n }\n if (performance.getEntriesByName(FIRST_INTERACTIVE_EVENT).length === 0) {\n performance.mark(FIRST_INTERACTIVE_EVENT);\n if (typeof window !== 'undefined') {\n window.dispatchEvent(new CustomEvent(FIRST_INTERACTIVE_EVENT, { detail: Math.round(performance.now()) }));\n }\n }\n bootLoader?.dismiss();\n }, [placeholderDismissed]);\n\n // Used in tests to exercise the error boundary & reset dialog (see\n // composer-app's `basic.spec.ts`). Thrown into the surrounding `ErrorBoundary`.\n if (location.search === '?throw') {\n throw new Error('Test error');\n }\n\n if (error) {\n // This triggers the error boundary to provide UI feedback for the startup error.\n throw error;\n }\n\n // The boot loader owns the screen until its outro starts; render nothing\n // until then (any DOM here would sit invisibly behind the `z-index: 10`\n // loader anyway).\n // TODO(wittjosiah): Consider using Suspense instead.\n if (!shellMounted) {\n return null;\n }\n\n return (\n // Contexts nest, so one suspending provider necessarily withholds everything below it; the\n // boundary here at least keeps that from unwinding past the app's providers.\n <Suspense fallback={null}>\n <ContextChain contexts={sortedContexts}>\n {reactRoots.map(({ id, root: Component }) => (\n // One boundary per root: roots read capabilities whose providers activate in a later\n // wave, and `useCapability` suspends on that. A shared boundary would let one late root\n // withhold every other root's already-renderable content — the blank shell this fixes.\n <Suspense key={id} fallback={null}>\n <Component />\n </Suspense>\n ))}\n </ContextChain>\n </Suspense>\n );\n};\n\n/**\n * Nests each provider around the next at render time so the element type is a constant: composing\n * the nesting into a component makes it a new type per render, remounting the whole app on any\n * capability change. A contributed context appends, so the chain extends inward and the providers\n * above keep their positions; a removed one still shifts everything below it.\n */\nconst ContextChain = ({ contexts, children }: PropsWithChildren<{ contexts: Capabilities.ReactContext[] }>) => {\n if (contexts.length === 0) {\n return <>{children}</>;\n }\n\n const [{ context: Context }, ...rest] = contexts;\n return (\n <Context>\n <ContextChain contexts={rest}>{children}</ContextChain>\n </Context>\n );\n};\n","//\n// Copyright 2025 DXOS.org\n//\n\n/** Interval between watchdog ticks. Each tick credits at most two intervals of executed time. */\nexport const STARTUP_WATCHDOG_TICK_MS = 1_000;\n\nexport type StartupWatchdogOptions = {\n stallMs: number;\n onStall: (stall: { executedMs: number }) => void;\n};\n\nexport type StartupWatchdog = {\n progress: () => void;\n dispose: () => void;\n};\n\n/**\n * Detects a startup that has stopped making progress.\n *\n * The question it answers is not \"has startup taken longer than `stallMs`?\" but \"has startup\n * done nothing for `stallMs` worth of actual execution?\". Two things make that distinction:\n *\n * 1. **Only executed time counts.** A tick runs every second and credits the time since the\n * previous tick, capped at two seconds. If the process was suspended (a hidden webview on\n * macOS, App Nap, system sleep), no ticks run while it is frozen, and the first tick after it\n * thaws credits at most two seconds no matter how long the freeze lasted. `performance.now`\n * keeps advancing through a suspension, so comparing timestamps alone would count the frozen\n * time as a stall.\n *\n * 2. **Progress resets the window.** Each `progress()` call moves the start of the stall window to\n * the current executed time. A slow boot that keeps activating modules can run for as long as\n * it needs; only one with no activation for `stallMs` of executed time is reported.\n *\n * `onStall` fires at most once. Call `dispose()` when startup finishes or fails so it never fires\n * for a boot that completed.\n */\nexport const createStartupWatchdog = ({ stallMs, onStall }: StartupWatchdogOptions): StartupWatchdog => {\n let executedMs = 0;\n let progressAtMs = 0;\n let lastTickAt = performance.now();\n\n const timer = setInterval(() => {\n const tickAt = performance.now();\n executedMs += Math.min(tickAt - lastTickAt, 2 * STARTUP_WATCHDOG_TICK_MS);\n lastTickAt = tickAt;\n if (executedMs - progressAtMs < stallMs) {\n return;\n }\n clearInterval(timer);\n onStall({ executedMs: Math.round(executedMs) });\n }, STARTUP_WATCHDOG_TICK_MS);\n\n return {\n progress: () => {\n progressAtMs = executedMs;\n },\n dispose: () => clearInterval(timer),\n };\n};\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport { RegistryContext } from '@effect/atom-react/RegistryContext';\nimport * as Effect from 'effect/Effect';\nimport * as Fiber from 'effect/Fiber';\nimport * as PubSub from 'effect/PubSub';\nimport React, { type FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';\n\nimport * as EffectEx from '@dxos/effect/EffectEx';\nimport { BaseError, withContext } from '@dxos/errors';\nimport { invariant } from '@dxos/invariant';\nimport { log } from '@dxos/log';\nimport { ErrorBoundary, ErrorFallback, type FallbackProps } from '@dxos/react-error-boundary';\nimport { useAsyncEffect, useDefaultValue } from '@dxos/react-hooks';\nimport { ContextProtocolProvider } from '@dxos/web-context-react';\n\nimport { ActivationEvents, Capabilities } from '../../common/index.ts';\nimport { PluginManagerContext } from '../../context.ts';\nimport { type ActivationEvent, type Plugin, PluginManager } from '../../core/index.ts';\nimport * as Devtools from '../../Devtools.ts';\nimport { App } from '../components/App/App.tsx';\nimport { bootLoader } from '../components/App/loader.ts';\nimport { PluginManagerProvider } from '../components/PluginManager/PluginManagerProvider.ts';\nimport { SurfaceManager } from '../components/Surface/SurfaceManager.ts';\nimport { SurfaceManagerProvider } from '../components/Surface/SurfaceManagerContext.ts';\nimport { createStartupWatchdog } from './startup-watchdog.ts';\n\nconst ENABLED_KEY = 'org.dxos.app-framework.enabled';\n\nexport type StartupProgress = {\n /** Number of modules that have been activated. */\n activated: number;\n /** Total number of modules registered. */\n total: number;\n /** Fractional progress (0-1). */\n progress: number;\n /**\n * Raw activation event key (e.g. `org.dxos.app-framework.event.startup`).\n * Set on event-level transitions, *and* on module-level transitions where\n * it carries the parent activation event that first triggered the\n * module's load (plumbed through `_loadCapabilitiesForModules` →\n * `_loadModule`). Consumers can use this either as the primary id (when\n * {@link module} is absent) or as an extra \"context\" field alongside\n * {@link module}.\n */\n event?: string;\n /**\n * Raw module id (e.g. `org.dxos.plugin.observability.module.ReactSurface`)\n * when the in-flight activation is module-level. When present,\n * {@link event} may also be set, identifying the parent activation that\n * triggered this module's load.\n */\n module?: string;\n /**\n * Pre-humanized label for the currently surfaced transition (module\n * label if {@link module} is set, otherwise the event label), supplied\n * for consumers that want a sensible default. Hosts that prefer to\n * render their own label can read the raw {@link event}/{@link module}\n * fields and ignore this — the framework leaves the policy choice\n * (which transitions to surface, how to format them, whether to drop\n * sub-modules entirely) to the host's `Placeholder`.\n */\n humanizedName?: string;\n /**\n * Humanized label of the plugin owning {@link module} (e.g. \"Markdown\"), absent on\n * event-level transitions. Lets a host collapse a plugin's many module activations\n * into a single status line without re-parsing module ids.\n */\n pluginName?: string;\n /** Slug of the plugin owning {@link module} (e.g. `markdown`) — the id the boot loader's row keys on. */\n pluginSlug?: string;\n};\n\n/**\n * Event dispatched on `window` once the `Startup` activation event has fully completed.\n */\nexport const STARTUP_ACTIVATED_EVENT = 'app-framework:startup-activated';\n\n/**\n * Event dispatched on `window` when startup misses its deadline, carrying\n * {@link StartupDiagnostics} as `detail`.\n */\nexport const STARTUP_FAILED_EVENT = 'app-framework:startup-failed';\n\n/**\n * Event dispatched on `window` the first time the app shell renders and the boot loader is\n * dismissed, carrying the milliseconds since navigation start as `detail`.\n */\nexport const FIRST_INTERACTIVE_EVENT = 'app-framework:first-interactive';\n\n/** Startup did not complete before its deadline. Carries {@link StartupDiagnostics} as context. */\nexport class StartupTimeoutError extends BaseError.extend('StartupTimeoutError', 'Startup timed out') {}\n\n/** Where startup had got to when the deadline expired. */\nexport type StartupDiagnostics = {\n /** Which failure produced these: the deadline expiring, or a module refusing to activate. */\n startupFailureKind: 'timeout' | 'module-error';\n /** The deadline, in ms; reported for both kinds so the two are comparable. */\n startupTimeoutMs: number;\n /** Comma-separated activation events that completed before the failure. */\n startupEventsFired: string;\n /** Modules that had finished activating. */\n startupActivatedModules: number;\n /** Modules registered in total. */\n startupTotalModules: number;\n /** Comma-separated modules still activating, which is the set the stall is in. */\n startupInFlightModules: string;\n /** Activation event in flight when the failure hit. */\n startupLastEvent?: string;\n /** Comma-separated events queued for replay by a pending reset. */\n startupPendingReset: string;\n};\n\ndeclare global {\n interface WindowEventMap {\n [STARTUP_ACTIVATED_EVENT]: CustomEvent<void>;\n [STARTUP_FAILED_EVENT]: CustomEvent<StartupDiagnostics>;\n [FIRST_INTERACTIVE_EVENT]: CustomEvent<number>;\n }\n}\n\nexport type UseAppOptions = {\n pluginManager?: PluginManager.PluginManager;\n pluginLoader?: PluginManager.ManagerOptions['pluginLoader'];\n onPluginRemove?: PluginManager.ManagerOptions['onRemove'];\n pluginRegistryProvider?: PluginManager.ManagerOptions['pluginRegistryProvider'];\n plugins?: Plugin.Plugin[];\n defaults?: string[];\n /**\n * Additional activation events to fire before startup.\n */\n setupEvents?: ActivationEvent.ActivationEvent[];\n cacheEnabled?: boolean;\n safeMode?: boolean;\n debounce?: number;\n /**\n * Executed milliseconds without an activation event before startup is declared stalled; suspended\n * time does not count, and a boot that keeps activating is never aborted.\n */\n timeout?: number;\n /**\n * Relay per-plugin `Activating …` lines into the boot loader's status log. Off by default —\n * activation names framework internals, which is diagnostic detail rather than something a\n * user booting the app is asking for.\n */\n verboseStatus?: boolean;\n fallback?: FC<FallbackProps>;\n};\n\n/**\n * Expected usage is for this to be the entrypoint of the application.\n * Initializes plugins and renders the root components.\n *\n * @example\n * const plugins = [LayoutPlugin(), MyPlugin()];\n * const defaults = [MyPluginId];\n * const fallback = () => <div>Initializing Plugins...</div>;\n * const App = useApp({ plugins, defaults, fallback });\n * createRoot(document.getElementById('root')!).render(\n * <StrictMode>\n * <App />\n * </StrictMode>,\n * );\n *\n * @param params.pluginLoader A function which loads new plugins.\n * @param params.plugins All plugins available to the application. Plugins whose `meta.profile.tags` includes `'system'` are treated as core (force-enabled, not user-toggleable).\n * @param params.defaults Default plugins are enabled by default but can be disabled by the user.\n * @param params.cacheEnabled Whether to cache enabled plugins in localStorage.\n * @param params.safeMode Whether to enable safe mode, which disables optional plugins.\n * @param params.fallback Fallback component to render if an error occurs during startup.\n */\nexport const useApp = ({\n pluginManager,\n pluginLoader: pluginLoaderProp,\n onPluginRemove,\n pluginRegistryProvider,\n plugins: pluginsProp,\n defaults: defaultsProp,\n setupEvents: setupEventsProp,\n fallback = ErrorFallback,\n cacheEnabled = false,\n safeMode = false,\n debounce = 0,\n timeout = 30_000,\n verboseStatus = false,\n}: UseAppOptions) => {\n const plugins = useDefaultValue(pluginsProp, () => []);\n const defaults = useDefaultValue(defaultsProp, () => []);\n const setupEvents = useDefaultValue(setupEventsProp, () => []);\n\n const pluginLoader = useMemo(\n () =>\n pluginLoaderProp ??\n ((id: string) =>\n Effect.sync(() => {\n const plugin = plugins.find((plugin) => plugin.meta.profile.key === id);\n invariant(plugin, `Plugin not found: ${id}`);\n return { plugin };\n })),\n [pluginLoaderProp, plugins],\n );\n\n const readyRef = useRef(false);\n const [ready, setReady] = useState(false);\n const [error, setError] = useState<unknown>(null);\n const [startupProgress, setStartupProgress] = useState<StartupProgress>({\n activated: 0,\n total: 0,\n progress: 0,\n });\n // TODO(wittjosiah): Migrate to Atom.kvs for isomorphic storage.\n const cached: string[] = useMemo(() => JSON.parse(localStorage.getItem(ENABLED_KEY) ?? '[]'), []);\n const enabled = useMemo(\n () => (safeMode ? [] : cacheEnabled && cached.length > 0 ? cached : defaults),\n [safeMode, cacheEnabled, cached, defaults],\n );\n const isExternalManager = !!pluginManager;\n const manager = useMemo(() => {\n const mgr =\n pluginManager ??\n PluginManager.make({\n pluginLoader,\n plugins,\n enabled,\n onRemove: onPluginRemove,\n pluginRegistryProvider,\n });\n log('useApp: useMemo created/reused manager', { provided: !!pluginManager });\n return mgr;\n }, [pluginManager, pluginLoader, plugins, enabled, onPluginRemove, pluginRegistryProvider]);\n\n useEffect(() => {\n if (!cacheEnabled) {\n return;\n }\n return manager.registry.subscribe(manager.enabled, (value) => {\n localStorage.setItem(ENABLED_KEY, JSON.stringify(value));\n });\n }, [cacheEnabled, manager]);\n\n useEffect(() => {\n Devtools.setup(manager);\n }, [manager]);\n\n // Hand the boot loader the enabled plugins' icons from their own meta. This registers, it does\n // not draw: barely half the enabled set activates during startup (the rest activate lazily on\n // first use), so a row seeded from it sat half-dim for the whole boot. Meta is the icon's only\n // source — the loader is a standalone bundle with no access to the plugin registry.\n useEffect(() => {\n // Optional call, not just an optional facade: an already-loaded page can be running an\n // `index.html` whose inlined loader bundle predates this method (service-worker cache, stale\n // dev compile), and a TypeError here would strand that page on the loader forever.\n bootLoader?.plugins?.(\n plugins\n .filter(({ meta }) => enabled.includes(meta.profile.key))\n .map(({ meta }) => ({\n id: pluginSlugOfKey(meta.profile.key),\n icon: meta.profile.icon?.key,\n })),\n );\n }, [plugins, enabled]);\n\n useAsyncEffect(async () => {\n log('useApp: effect mount');\n\n manager.capabilities.contribute({\n interface: Capabilities.PluginManager,\n implementation: manager,\n module: 'org.dxos.app-framework.plugin-manager',\n });\n\n manager.capabilities.contribute({\n interface: Capabilities.AtomRegistry,\n implementation: manager.registry,\n module: 'org.dxos.app-framework.atom-registry',\n });\n\n const inFlightModules = new Set<string>();\n\n const collectDiagnostics = (startupFailureKind: StartupDiagnostics['startupFailureKind']): StartupDiagnostics => ({\n startupFailureKind,\n startupTimeoutMs: timeout,\n startupEventsFired: manager.getEventsFired().join(','),\n startupActivatedModules: manager.getActive().length,\n startupTotalModules: manager.getModules().length,\n startupInFlightModules: [...inFlightModules].join(','),\n startupLastEvent: progressRef.current.event,\n startupPendingReset: manager.getPendingReset().join(','),\n });\n\n const reportFailure = (diagnostics: StartupDiagnostics) => {\n if (typeof window !== 'undefined') {\n window.dispatchEvent(new CustomEvent(STARTUP_FAILED_EVENT, { detail: diagnostics }));\n }\n };\n\n const watchdog = createStartupWatchdog({\n stallMs: timeout,\n onStall: ({ executedMs }) => {\n const diagnostics = collectDiagnostics('timeout');\n\n log.warn('startup timeout diagnostic', { ...diagnostics, executedMs, activeModules: manager.getActive() });\n\n const abort = () => {\n void EffectEx.runAndForwardErrors(Fiber.interrupt(fiber));\n setError(\n withContext(new StartupTimeoutError({ message: `Startup timed out after ${timeout}ms` }), diagnostics),\n );\n reportFailure(diagnostics);\n };\n\n // In development the deadline is a symptom, not a verdict: a cold OPFS, a rebuild or a paused\n // debugger all overrun it while the run is perfectly healthy, and killing it discards the\n // state worth looking at. Startup continues either way and the user decides — the offer raises\n // exactly the failure this branch used to raise unprompted.\n //\n // The missing-`stalled` case does NOT fall through to failing: the loader is inlined into\n // `index.html` at build time, so a page served before this shipped has the old bundle, and\n // treating that as fatal would resurrect the dialog precisely where dev asked for a button.\n if (import.meta.env?.DEV) {\n if (bootLoader?.stalled) {\n bootLoader.stalled(abort);\n } else {\n log.warn('startup timed out; boot loader cannot offer an abort (stale inlined bundle?)', { timeout });\n }\n return;\n }\n\n abort();\n },\n });\n\n const fiber = Effect.gen(function* () {\n const queue = yield* PubSub.subscribe(manager.activation);\n const listener = yield* Effect.forkDetach(\n PubSub.take(queue).pipe(\n Effect.tap(({ event, state, module, error: error$ }) =>\n Effect.sync(() => {\n watchdog.progress();\n if (module) {\n if (state === 'activating') {\n inFlightModules.add(module);\n } else {\n inFlightModules.delete(module);\n }\n }\n // Event-level Startup activated (no `module` field) fires once,\n // after every module triggered by Startup has finished. Module\n // activations now also carry their parent event id (so the trace\n // can attribute a module to its triggering event), which means\n // each module activated under Startup publishes\n // `{ event: 'startup', state: 'activated', module: <id> }`. Without\n // the `!module` guard the listener would mark the app ready on\n // the *first* such module rather than waiting for the event-level\n // completion — leaving downstream capabilities (operation-invoker,\n // app-graph, …) un-registered when the boot loader dismisses.\n if (event === ActivationEvents.Startup.id && state === 'activated' && !module) {\n watchdog.dispose();\n setReady(true);\n readyRef.current = true;\n // Trigger startup profiler dump if available.\n (globalThis as any).composer?.profiler?.dump();\n // Notify any host observability layer that startup completed.\n // A `CustomEvent` keeps this generic — app-framework doesn't\n // import a provider, and consumers can capture the startup\n // summary without us picking one.\n if (typeof window !== 'undefined') {\n window.dispatchEvent(new CustomEvent(STARTUP_ACTIVATED_EVENT));\n }\n return;\n }\n // `activating` is the start-of-load signal. Surface the raw\n // `module` (or `event`) plus a pre-humanized label so the\n // boot loader can decide how to render each transition — show\n // everything, suppress noisy sub-modules, group by plugin,\n // or apply its own formatting. We intentionally do NOT touch\n // these fields on `activated`: pairing the two would cause\n // back-to-back identical updates to the host's effect, which\n // the boot loader treats as a re-trigger because\n // `progress.progress` moved. Leaving the label alone on\n // completion keeps it accurate (\"now activating X\") until\n // the next module starts.\n if (module && state === 'activating' && !readyRef.current) {\n const pluginSlug = pluginSlugOf(module);\n setStartupProgress((current) => ({\n ...current,\n // `event` here is the activation event that first\n // triggered this module load (plumbed through\n // `_loadCapabilitiesForModules` → `_loadModule`).\n // Falsy/empty falls back to undefined so consumers can\n // tell \"no parent context\" from \"parent context: <X>\".\n event: event || undefined,\n module,\n humanizedName: humanizeModuleId(module),\n pluginName: pluginSlug ? titleCase(pluginSlug) : undefined,\n pluginSlug,\n }));\n }\n // Update the activation count when a module commits. The\n // ring's fraction comes from this; `event`/`module`/\n // `humanizedName` were set by the matching `activating`\n // message above and are left alone so the count can advance\n // without re-firing the host's status callback.\n if (module && state === 'activated' && !readyRef.current) {\n const active = manager.getActive();\n const total = manager.getModules().length;\n setStartupProgress((current) => ({\n ...current,\n activated: active.length,\n total,\n progress: total > 0 ? active.length / total : 0,\n }));\n }\n // Event-level `activating` (no `module`) — fired at the start\n // of `_activateModulesForEvent` and recursively for each\n // before/after event. Surfaces a label during the gap before\n // the first module-level message lands; subsequent module\n // updates immediately overwrite this with a more specific\n // label.\n if (event && !module && state === 'activating' && !readyRef.current) {\n setStartupProgress((current) => ({\n ...current,\n event,\n module: undefined,\n humanizedName: humanizeEventKey(event),\n pluginName: undefined,\n pluginSlug: undefined,\n }));\n }\n if (error$ && !readyRef.current) {\n watchdog.dispose();\n const diagnostics = collectDiagnostics('module-error');\n setError(withContext(error$, diagnostics));\n reportFailure(diagnostics);\n }\n }),\n ),\n Effect.forever,\n ),\n );\n\n yield* Effect.all([\n ...setupEvents.map((event) => manager.activate(event)),\n manager.activate(ActivationEvents.Startup),\n ]);\n\n return yield* Fiber.join(listener);\n }).pipe(Effect.scoped, Effect.runFork);\n\n return () => {\n log('useApp: effect cleanup');\n watchdog.dispose();\n void EffectEx.runAndForwardErrors(Fiber.interrupt(fiber));\n if (!isExternalManager) {\n EffectEx.runDetached(manager.shutdown());\n }\n };\n }, [manager]);\n\n const progressRef = useRef(startupProgress);\n progressRef.current = startupProgress;\n\n const surfaces = useMemo(() => new SurfaceManager(manager.capabilities, manager), [manager]);\n\n return useCallback(\n () => (\n <ErrorBoundary name='app' FallbackComponent={fallback}>\n <PluginManagerProvider value={manager}>\n <ContextProtocolProvider value={manager} context={PluginManagerContext}>\n <RegistryContext.Provider value={manager.registry}>\n <SurfaceManagerProvider value={surfaces}>\n <App\n ready={ready}\n error={error}\n debounce={debounce}\n progress={progressRef.current}\n verboseStatus={verboseStatus}\n />\n </SurfaceManagerProvider>\n </RegistryContext.Provider>\n </ContextProtocolProvider>\n </PluginManagerProvider>\n </ErrorBoundary>\n ),\n [fallback, manager, surfaces, ready, error],\n );\n};\n\n/**\n * Extracts a human-readable label from a module ID.\n *\n * Module IDs follow `org.dxos.plugin.<plugin-slug>.module.<module-name>`,\n * where `<plugin-slug>` is kebab-case and `<module-name>` is either an\n * explicit string (often kebab-case, e.g. `'observability'`, `'namespace'`)\n * or a capability tag in PascalCase from `Capability.getModuleTag(...)`\n * (e.g. `'ReactSurface'`, `'AppGraphBuilder'`). The output is\n * `\"Title Case Plugin: kebab-module\"` so the visible status names both the\n * plugin and the aspect being activated, helping disambiguate the multiple\n * modules a plugin contributes.\n *\n * Examples:\n * - \"org.dxos.plugin.markdown.module.ReactSurface\" → \"Markdown: react-surface\"\n * - \"org.dxos.plugin.observability.module.AppGraphBuilder\" → \"Observability: app-graph-builder\"\n * - \"org.dxos.plugin.observability.module.observability\" → \"Observability\"\n * (the module name matches the plugin slug — collapsed to avoid\n * \"Observability: observability\" noise.)\n */\nconst humanizeModuleId = (moduleId: string): string => {\n const match = moduleId.match(/\\.plugin\\.([^.]+)\\.module\\.(.+)$/);\n if (!match) {\n // Fallback: use the last segment.\n const parts = moduleId.split('.');\n return parts[parts.length - 1];\n }\n const [, pluginSlug, moduleName] = match;\n const pluginLabel = titleCase(pluginSlug);\n // Normalise the module name to kebab-case so PascalCase capability tags\n // (\"ReactSurface\") read consistently with explicit kebab IDs\n // (\"operation-handler\"). The two-step substitution handles consecutive\n // uppercase runs (`URLLoader` → `url-loader`) without splitting them\n // mid-acronym.\n const moduleLabel = moduleName\n .replace(/([a-z0-9])([A-Z])/g, '$1-$2')\n .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')\n .toLowerCase();\n // Capability modules whose name matches the plugin slug (e.g. the\n // observability plugin's `observability` capability module) would render\n // as \"Observability: observability\" — drop the redundant suffix.\n if (moduleLabel === pluginSlug) {\n return pluginLabel;\n }\n return `${pluginLabel}: ${moduleLabel}`;\n};\n\n/**\n * Extracts the owning plugin's slug from a module ID, or `undefined` when the id is not\n * plugin-scoped (framework-internal modules). E.g.\n * \"org.dxos.plugin.markdown.module.ReactSurface\" → \"markdown\".\n */\n/** Plugin slug from a plugin key — \"org.dxos.plugin.markdown\" → \"markdown\". */\nexport const pluginSlugOfKey = (key: string): string => key.split('.').at(-1) ?? key;\n\nexport const pluginSlugOf = (moduleId: string): string | undefined =>\n moduleId.match(/\\.plugin\\.([^.]+)\\.module\\./)?.[1];\n\n/** Kebab slug → Title Case. */\nconst titleCase = (slug: string): string =>\n slug\n .split('-')\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1))\n .join(' ');\n\n/**\n * Extracts a human-readable label from an activation event key.\n * E.g., \"org.dxos.app-framework.event.setup-react-surface\" → \"Setup React Surface\".\n */\nconst humanizeEventKey = (eventKey: string): string => {\n // Strip a leading specifier (composite key form: \"<id>:<specifier>\").\n const id = eventKey.split(':')[0];\n // Match the trailing segment after `.event.`.\n const match = id.match(/\\.event\\.(.+)$/);\n const slug = match ? match[1] : (id.split('.').pop() ?? id);\n return titleCase(slug);\n};\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport { useAtomValue } from '@effect/atom-react/Hooks';\nimport type * as Effect from 'effect/Effect';\nimport * as Atom from 'effect/reactivity/Atom';\nimport { use, useCallback, useLayoutEffect, useRef } from 'react';\n\nimport * as Operation from '@dxos/compute/Operation';\nimport * as OperationHandlerSet from '@dxos/compute/OperationHandlerSet';\n\nimport { Capabilities } from '../../common/index.ts';\nimport { type Capability } from '../../core/index.ts';\nimport { usePluginManager } from '../components/PluginManager/PluginManagerProvider.ts';\n\n/** Stable atom yielding `undefined`, used as the fallback for optional atom-capability lookups. */\nconst emptyAtomValue = Atom.make(() => undefined);\n\n/**\n * Hook to request capabilities from the plugin context.\n * @returns An array of capabilities.\n */\nexport const useCapabilities = <T>(interfaceDef: Capability.InterfaceDef<T>) => {\n const manager = usePluginManager();\n return useAtomValue(manager.capabilities.atom(interfaceDef));\n};\n\n/**\n * Hook to request a capability from the plugin context.\n *\n * Suspends (throws the contribution promise) while nothing has contributed the interface yet, so a\n * reader that renders before its provider activates parks at its nearest Suspense boundary. An\n * invariant here cannot tell \"not yet\" from \"never\": most providers are idle-gated or wait on a\n * runtime event, so the reader legitimately renders first, and hard-failing on that blanks the\n * subtree. Declaring the dependency as the module's `requires` is NOT the alternative — that\n * demotes the reader's module into the provider's (later) wave.\n *\n * @returns The capability.\n */\nexport const useCapability = <T>(interfaceDef: Capability.InterfaceDef<T>) => {\n const manager = usePluginManager();\n const capabilities = useCapabilities(interfaceDef);\n if (capabilities.length === 0) {\n throw manager.capabilities.waitForPromise(interfaceDef);\n }\n return capabilities[0];\n};\n\n/**\n * Hook to request capabilities that a plugin may or may not contribute.\n * @returns An array of capabilities, empty when none is registered.\n */\nexport const useOptionalCapabilities = <T>(interfaceDef: Capability.InterfaceDef<T>): readonly T[] => {\n const manager = usePluginManager();\n return useAtomValue(manager.capabilities.atom(interfaceDef));\n};\n\n/**\n * Hook to request a single capability that a plugin may or may not contribute.\n * @returns The first matching capability, or `undefined` when none is registered.\n */\nexport const useOptionalCapability = <T>(interfaceDef: Capability.InterfaceDef<T>): T | undefined =>\n useOptionalCapabilities(interfaceDef)[0];\n\n/**\n * Hook to get the current value of an atom capability.\n * Automatically subscribes to changes.\n * @example const settings = useAtomCapability(CommentCapabilities.Settings);\n */\nexport const useAtomCapability = <T>(atomCapability: Capability.InterfaceDef<Atom.Atom<T>>): T => {\n const atom = useCapability(atomCapability);\n return useAtomValue(atom);\n};\n\n/**\n * Tolerant variant of {@link useAtomCapability}: returns `undefined` while the atom capability is\n * not registered, rather than throwing. Use it wherever a reader can render before its provider —\n * a provider gated on a genuine runtime event (a status indicator whose state arrives with the\n * client) cannot be pulled onto the startup pass with `requires`.\n */\nexport const useOptionalAtomCapability = <T>(atomCapability: Capability.InterfaceDef<Atom.Atom<T>>): T | undefined => {\n const atom = useOptionalCapability(atomCapability);\n return useAtomValue(atom ?? emptyAtomValue) as T | undefined;\n};\n\n/**\n * Hook to get value and updater for an atom capability.\n * Returns [currentValue, updateFn] similar to useState.\n * @example const [settings, updateSettings] = useAtomCapabilityState(CommentCapabilities.Settings);\n */\nexport const useAtomCapabilityState = <T>(\n atomCapability: Capability.InterfaceDef<Atom.Writable<T>>,\n): [T, (fn: (current: T) => T) => void] => {\n const registry = useCapability(Capabilities.AtomRegistry);\n const atom = useCapability(atomCapability);\n const value = useAtomValue(atom);\n const update = useCallback(\n (fn: (current: T) => T) => {\n registry.set(atom, fn(registry.get(atom)));\n },\n [registry, atom],\n );\n return [value, update];\n};\n\n/**\n * Tolerant variant of {@link useAtomCapabilityState}: returns `[undefined, noop]` when the atom\n * capability is not registered (e.g. the contributing plugin is not installed) rather than\n * throwing. The updater is a no-op while the capability is absent.\n */\nexport const useOptionalAtomCapabilityState = <T>(\n atomCapability: Capability.InterfaceDef<Atom.Writable<T>>,\n): [T | undefined, (fn: (current: T) => T) => void] => {\n const registry = useOptionalCapability(Capabilities.AtomRegistry);\n const atom = useOptionalCapability(atomCapability);\n const value = useAtomValue(atom ?? emptyAtomValue);\n const update = useCallback(\n (fn: (current: T) => T) => {\n if (registry && atom) {\n registry.set(atom, fn(registry.get(atom)));\n }\n },\n [registry, atom],\n );\n return [value, update];\n};\n\n/**\n * Hook to get the operation invoker capability.\n */\nexport const useOperationInvoker = (): Capabilities.OperationInvoker => useCapability(Capabilities.OperationInvoker);\n\n/**\n * Suspensefully resolves an operation's handler as an effect fn: `(input) => Effect<Output>`.\n * With `map`, binds the component's callback arguments to the operation input instead:\n * `(...args) => Effect<Output>`.\n *\n * Resolves against the merged {@link Capabilities.OperationHandlers} set. Handler sets are all\n * registered by startup (only handler BODIES load lazily), so the component suspends only while\n * the matched handler's module loads. Throws {@link NoHandlerError} when no contributed set\n * knows the operation.\n *\n * The returned effect still requires the operation's declared services; run it via\n * {@link useSpaceCallback} or the {@link Capabilities.ProcessManagerRuntime}.\n *\n * @example const moveTask = useOperationHandler(TaskOperation.MoveTask);\n * @example const move = useOperationHandler(TaskOperation.MoveTask, (task: Task) => ({ task: Ref.make(task) }));\n */\nexport const useOperationHandler: {\n <const Def extends Operation.Definition.Any>(operation: Def): Operation.Definition.HandlerType<Def>;\n <const Def extends Operation.Definition.Any, TArgs extends readonly unknown[]>(\n operation: Def,\n map: (...args: TArgs) => Operation.Definition.Input<Def>,\n ): (...args: TArgs) => Effect.Effect<Operation.Definition.Output<Def>, any, Operation.Definition.Services<Def>>;\n} = <const Def extends Operation.Definition.Any, TArgs extends readonly unknown[]>(\n operation: Def,\n map?: (...args: TArgs) => Operation.Definition.Input<Def>,\n): any => {\n const handlers = useCapability(Capabilities.OperationHandlers);\n const withHandler = use(OperationHandlerSet.findHandler(handlers, operation));\n if (!withHandler) {\n throw new Operation.NoHandlerError(operation.meta.key);\n }\n const handler = withHandler.handler;\n // The ref is updated in a layout effect so a discarded concurrent render's mapper (closing\n // over that render's props) never leaks into the committed callback.\n const mapRef = useRef(map);\n useLayoutEffect(() => {\n mapRef.current = map;\n }, [map]);\n const mapped = useCallback((...args: TArgs) => handler(mapRef.current!(...args)), [handler]);\n return map ? mapped : handler;\n};\n\n/**\n * Binds an operation to a UI callback in one step: `map` turns the component's callback\n * arguments into the operation's input. The handler identity is stable across renders (the\n * mapper and options read through refs), so it replaces per-handler `useCallback` boilerplate.\n */\nexport const useOperation = <TArgs extends readonly unknown[], TInput>(\n operation: Operation.Definition<TInput, unknown>,\n map: (...args: TArgs) => TInput,\n options?: Operation.InvokeOptions,\n): ((...args: TArgs) => void) => {\n const { invokePromise } = useOperationInvoker();\n const mapRef = useRef(map);\n mapRef.current = map;\n const optionsRef = useRef(options);\n optionsRef.current = options;\n return useCallback(\n (...args: TArgs) => void invokePromise(operation, mapRef.current(...args), optionsRef.current),\n [invokePromise, operation],\n );\n};\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport { useEffect, useRef, useState } from 'react';\n\nexport enum LoadingState {\n Loading = 0,\n FadeIn = 1,\n FadeOut = 2,\n Done = 3,\n}\n\n/**\n * To avoid \"flashing\" the placeholder, we wait a period of time before starting the loading animation.\n * If loading completes during this time the placehoder is not shown, otherwise is it displayed for a minimum period of time.\n *\n * States:\n * 0: Loading - Wait for a period of time before starting the loading animation.\n * 1: Fade-in - Display a loading animation.\n * 2: Fade-out - Fade out the loading animation.\n * 3: Done - Remove the placeholder.\n */\nexport const useLoading = (ready: boolean, debounce = 0) => {\n const [stage, setStage] = useState<LoadingState>(LoadingState.Loading);\n // Mirror `ready` into a ref so the interval's `setStage` callback can read\n // the latest value without depending on the effect re-running. The pure\n // closure-capture pattern (with `ready` in deps) sticks the FSM at\n // `FadeIn` whenever HMR doesn't propagate the dep change end-to-end —\n // a ref sidesteps that entirely and fires the read on every tick.\n const readyRef = useRef(ready);\n readyRef.current = ready;\n\n useEffect(() => {\n if (!debounce) {\n return;\n }\n\n const i = setInterval(() => {\n setStage((stage) => {\n const isReady = readyRef.current;\n switch (stage) {\n case LoadingState.Loading: {\n if (!isReady) {\n return LoadingState.FadeIn;\n }\n clearInterval(i);\n return LoadingState.Done;\n }\n\n case LoadingState.FadeIn: {\n if (isReady) {\n return LoadingState.FadeOut;\n }\n break;\n }\n\n case LoadingState.FadeOut: {\n clearInterval(i);\n return LoadingState.Done;\n }\n }\n\n return stage;\n });\n }, debounce);\n\n return () => clearInterval(i);\n }, [debounce]);\n\n if (!debounce) {\n return ready ? LoadingState.Done : LoadingState.Loading;\n }\n\n return stage;\n};\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport * as Context from 'effect/Context';\nimport * as Effect from 'effect/Effect';\nimport * as Layer from 'effect/Layer';\nimport { type DependencyList, use, useCallback, useMemo } from 'react';\n\nimport * as Operation from '@dxos/compute/Operation';\nimport * as ServiceResolver from '@dxos/compute/ServiceResolver';\nimport * as EffectEx from '@dxos/effect/EffectEx';\nimport type { SpaceId } from '@dxos/keys';\n\nimport { Capabilities } from '../../common/index.ts';\nimport { useCapability } from './useCapabilities.ts';\n\n/**\n * Resolve the shared {@link Capabilities.ProcessManagerRuntime} from the plugin context.\n */\nexport const useProcessManagerRuntime = (): Capabilities.ProcessManagerRuntime =>\n useCapability(Capabilities.ProcessManagerRuntime);\n\n/**\n * Build a callback that runs an effect on the {@link Capabilities.ProcessManagerRuntime}\n * with space-scoped services resolved via {@link ServiceResolver.provide}.\n *\n * Arguments passed to the returned callback are forwarded to `fn`, and the effect's result comes\n * back as the callback's promise — rejecting with the effect's failure (`E`), with\n * {@link ServiceNotAvailableError} when a tag cannot be resolved for the space, or with a\n * `TypeError` when called while `spaceId` is still `undefined`.\n *\n * The `tags` tuple must list every service the effect requires (beyond the\n * fixed {@link Capabilities.ProcessManagerRuntimeServices}); these services are\n * resolved for the given `spaceId` through the runtime's service resolver.\n *\n * Nested `Operation.invoke` / `Operation.schedule` calls within the effect\n * inherit `spaceId` as a default {@link Operation.InvokeOptions} so that\n * spawned operation processes have a space context (and therefore can resolve\n * space-scoped services like `Database.Service`) without each call site having\n * to thread the id through manually.\n */\nexport const useSpaceCallback = <\n const Tags extends readonly Context.Key<any, any>[],\n TArgs extends readonly unknown[],\n T,\n E,\n>(\n spaceId: SpaceId | undefined,\n tags: Tags,\n fn: (\n ...args: TArgs\n ) => Effect.Effect<T, E, Context.Service.Identifier<Tags[number]> | Capabilities.ProcessManagerRuntimeServices>,\n deps?: DependencyList,\n): ((...args: TArgs) => Promise<T>) => {\n const runtime = useProcessManagerRuntime();\n return useCallback(\n (...args: TArgs) => {\n if (spaceId === undefined) {\n throw new TypeError('Space not provided to useSpaceCallback');\n }\n const layer = Layer.merge(\n ServiceResolver.provide({ space: spaceId }, ...tags),\n Operation.withInvocationOptions({ spaceId }),\n );\n return runtime.runPromise(\n fn(...args).pipe(Effect.provide(layer)) as Effect.Effect<T, E | ServiceResolver.ServiceNotAvailableError, any>,\n );\n },\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [runtime, spaceId, ...(deps ?? [])],\n );\n};\n\n/**\n * Suspensefully resolve a single space-scoped service via the\n * {@link Capabilities.ProcessManagerRuntime}'s service resolver.\n */\nexport const useSpaceService = <T extends Context.Key<any, any>>(\n tag: T,\n spaceId: SpaceId | undefined,\n): Context.Service.Shape<T> | undefined => {\n const runtime = useProcessManagerRuntime();\n const promise = useMemo(() => {\n if (spaceId === undefined) {\n return undefined;\n }\n const layer = ServiceResolver.provide({ space: spaceId }, tag);\n const effect = Effect.flatMap(tag, (service) => Effect.succeed(service)).pipe(Effect.provide(layer));\n return runtime.runPromiseExit(effect as Effect.Effect<Context.Service.Shape<T>, any, any>);\n }, [runtime, spaceId, tag]);\n if (!promise) {\n return undefined;\n }\n return EffectEx.unwrapExit(use(promise));\n};\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport { useAtomValue } from '@effect/atom-react/Hooks';\nimport { RegistryContext } from '@effect/atom-react/RegistryContext';\nimport type * as Atom from 'effect/reactivity/Atom';\nimport { useCallback, useContext } from 'react';\n\n/**\n * Hook to read and update a settings atom.\n * Returns the current value and an update function.\n */\nexport const useSettingsState = <T>(\n atom: Atom.Writable<T>,\n): { settings: T; updateSettings: (fn: (current: T) => T) => void } => {\n const registry = useContext(RegistryContext);\n const settings = useAtomValue(atom);\n\n const updateSettings = useCallback(\n (fn: (current: T) => T) => {\n registry.set(atom, fn(registry.get(atom)));\n },\n [registry, atom],\n );\n\n return { settings, updateSettings };\n};\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport { useContext } from 'react';\n\nimport { raise } from '@dxos/debug';\n\nimport { Surface } from '../components/Surface/index.ts';\n\nexport const useSurface = (): Surface.Context => {\n return useContext(Surface.Context) ?? raise(new Error('Missing SurfaceContext'));\n};\n"],"mappings":"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"}
1
+ {"version":3,"file":"chunk-hooks.mjs","names":["Capabilities.ReactContext","Capabilities.ReactRoot","_Fragment","PluginManager.make","Capabilities.PluginManager","Capabilities.AtomRegistry","ActivationEvents.Startup","Capabilities.AtomRegistry","Capabilities.OperationInvoker","Capabilities.OperationHandlers","Capabilities.ProcessManagerRuntime","Surface.Context"],"sources":["../../src/ui/hooks/useActivationSignal.ts","../../src/ui/components/App/loader.ts","../../src/ui/components/App/App.tsx","../../src/ui/hooks/startup-watchdog.ts","../../src/ui/hooks/useApp.tsx","../../src/ui/hooks/useCapabilities.ts","../../src/ui/hooks/useLoading.tsx","../../src/ui/hooks/useProcessManagerRuntime.ts","../../src/ui/hooks/useSettingsState.ts","../../src/ui/hooks/useSurface.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport { useEffect } from 'react';\n\nimport * as EffectEx from '@dxos/effect/EffectEx';\n\nimport { type ActivationEvent } from '../../core/index.ts';\nimport { useOptionalPluginManager } from '../components/PluginManager/PluginManagerProvider.ts';\n\n/**\n * Fires an activation event on mount — the UI-side half of demand-driven activation. A container\n * that constitutes the demand signal for policy-parked modules (a create-object picker, an\n * assistant chat) calls this so the parked providers load exactly when the surface first appears;\n * re-fires of an already-dispatched event are cheap no-ops.\n */\nexport const useActivationSignal = (event: ActivationEvent.ActivationEvent): void => {\n const manager = useOptionalPluginManager();\n useEffect(() => {\n if (!manager) {\n return;\n }\n EffectEx.runDetached(manager.activate(event));\n }, [manager, event]);\n};\n","//\n// Copyright 2026 DXOS.org\n//\n\ndeclare global {\n interface BootLoader {\n status: (payload: {\n event?: string;\n module?: string;\n humanized: string;\n /**\n * Optional `(index/total)` tick. When present, the loader replaces the\n * current line in place (\"Loading plugins (12/80)\") instead of appending\n * a new entry — keeps the visible log compact during long counted phases.\n */\n range?: { index: number; total: number };\n }) => void;\n /** Register plugin icons for the loader's activation row; draws nothing until `activated(...)`. */\n plugins?: (entries: { id: string; icon?: string }[]) => void;\n /** Add the registered plugin with this id to the activation row. */\n activated?: (id: string) => void;\n progress: (fraction?: number) => void;\n /**\n * Report that startup has outrun its budget without failing it, offering `onAbort` as the way\n * out. Dev-only — see the canonical doc on `BootLoaderApi.stalled`.\n */\n stalled: (onAbort: () => void) => void;\n ready: () => void;\n dismiss: () => void;\n }\n\n interface Window {\n /**\n * Driver injected by `@dxos/app-framework/vite-plugin`'s `bootLoaderPlugin`\n * (a Solid app inlined into `index.html`). Declared here — on a module that\n * is part of the `@dxos/app-framework/ui` export surface — so the React side\n * and host apps (e.g. composer-app) can drive the loader without each\n * re-declaring the type. The canonical definition lives in the plugin's\n * `loader-app/types.ts`; this mirror exists because that source compiles in a\n * separate (Solid) program that doesn't ship its globals to consumers.\n */\n __bootLoader?: BootLoader;\n }\n}\n\n// Guarded: `@dxos/app-framework/ui` has a `node` export and this module is re-exported through the\n// UI barrel, so a bare top-level `window` read would throw when imported in SSR / tests / tooling.\nexport const bootLoader = typeof window !== 'undefined' ? window.__bootLoader : undefined;\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport React, { type PropsWithChildren, Suspense, useEffect, useLayoutEffect, useMemo, useRef } from 'react';\n\nimport { Capabilities } from '../../../common/index.ts';\nimport { topologicalSort } from '../../../helpers.ts';\nimport {\n FIRST_INTERACTIVE_EVENT,\n LoadingState,\n type StartupProgress,\n type UseAppOptions,\n useCapabilities,\n useLoading,\n} from '../../hooks/index.ts';\nimport { bootLoader } from './loader.ts';\n\nexport type AppProps = Pick<UseAppOptions, 'debounce' | 'verboseStatus'> & {\n ready: boolean;\n error: unknown;\n progress?: StartupProgress;\n};\n\nexport const App = ({ ready, error, debounce, progress, verboseStatus = false }: AppProps) => {\n const reactContexts = useCapabilities(Capabilities.ReactContext);\n const reactRoots = useCapabilities(Capabilities.ReactRoot);\n const sortedContexts = useMemo(() => topologicalSort(reactContexts), [reactContexts]);\n const stage = useLoading(ready, debounce);\n const placeholderDismissed = stage >= LoadingState.Done;\n // The shell mounts a tick EARLIER than the dismissal, at the same stage that starts the loader's\n // outro: the loader is still on screen (z-index 10) and fading, so the real UI paints beneath it\n // instead of after it — gating both on `Done` leaves a blank frame between the two.\n const shellMounted = stage >= LoadingState.FadeOut;\n\n // Activation lines are opt-in (`verboseStatus`): they name framework internals, so the default\n // boot log is the host's own phases only. When enabled, one line per plugin (and per framework\n // activation event) rather than per module — a plugin contributes a dozen-odd modules, which\n // scrolled hundreds of lines past the user. The framework leaves both choices to the host.\n const announcedRef = useRef(new Set<string>());\n\n // Relay the startup lifecycle into the boot loader injected by\n // `@dxos/app-framework/vite-plugin` (a Solid app inlined into `index.html`,\n // the only visible loading UI). Plugin activation fills the `[0.5, 1]` half\n // of the ring; the raw `event` / `module` ids ride along so the loader owns\n // how each transition is rendered and traced. No-op once the outro has\n // started (`FadeOut`), so the dismissal runs uncontended.\n useEffect(() => {\n if (stage >= LoadingState.FadeOut) {\n return;\n }\n\n const fraction = progress?.progress ?? 0;\n bootLoader?.progress(0.5 + fraction * 0.5);\n if (progress?.pluginSlug) {\n // Optional call — see the note on `plugins?.(...)` in `useApp`.\n bootLoader?.activated?.(progress.pluginSlug);\n }\n\n // Plugin-level transitions collapse to the plugin's own name; event-level ones (no\n // `pluginName`) are the framework's own phases and are already few.\n const humanized = progress?.pluginName ?? progress?.humanizedName;\n if (!verboseStatus || !humanized) {\n return;\n }\n\n const key = progress?.pluginName ?? `event:${progress?.event ?? humanized}`;\n if (announcedRef.current.has(key)) {\n return;\n }\n announcedRef.current.add(key);\n bootLoader?.status({\n event: progress?.event,\n module: progress?.module,\n humanized: `Activating ${humanized}`,\n });\n }, [\n stage,\n progress?.progress,\n progress?.event,\n progress?.module,\n progress?.humanizedName,\n progress?.pluginName,\n progress?.pluginSlug,\n verboseStatus,\n ]);\n\n // Hand off at fade-out: play the loader's graceful shrink-and-fade outro.\n // `useLayoutEffect` runs before the next paint so the outro begins in the\n // same frame the real shell starts rendering beneath it.\n useLayoutEffect(() => {\n if (stage >= LoadingState.FadeOut) {\n bootLoader?.ready();\n }\n }, [stage]);\n\n // Emit a once-per-app `app-framework:first-interactive` mark the first time\n // the loader is dismissed and the real app shell renders. Closes the gap\n // between `Startup` activated and the first interactive paint.\n //\n // Also the framework-owned handoff-complete signal: `dismiss()` removes the\n // loader immediately on the fast-load path (where `useLoading` skips\n // `FadeOut`), and is a no-op once an outro is already in flight, so it never\n // cuts the animation short.\n useEffect(() => {\n if (!placeholderDismissed) {\n return;\n }\n if (performance.getEntriesByName(FIRST_INTERACTIVE_EVENT).length === 0) {\n performance.mark(FIRST_INTERACTIVE_EVENT);\n if (typeof window !== 'undefined') {\n window.dispatchEvent(new CustomEvent(FIRST_INTERACTIVE_EVENT, { detail: Math.round(performance.now()) }));\n }\n }\n bootLoader?.dismiss();\n }, [placeholderDismissed]);\n\n // Used in tests to exercise the error boundary & reset dialog (see\n // composer-app's `basic.spec.ts`). Thrown into the surrounding `ErrorBoundary`.\n if (location.search === '?throw') {\n throw new Error('Test error');\n }\n\n if (error) {\n // This triggers the error boundary to provide UI feedback for the startup error.\n throw error;\n }\n\n // The boot loader owns the screen until its outro starts; render nothing\n // until then (any DOM here would sit invisibly behind the `z-index: 10`\n // loader anyway).\n // TODO(wittjosiah): Consider using Suspense instead.\n if (!shellMounted) {\n return null;\n }\n\n return (\n // Contexts nest, so one suspending provider necessarily withholds everything below it; the\n // boundary here at least keeps that from unwinding past the app's providers.\n <Suspense fallback={null}>\n <ContextChain contexts={sortedContexts}>\n {reactRoots.map(({ id, root: Component }) => (\n // One boundary per root: roots read capabilities whose providers activate in a later\n // wave, and `useCapability` suspends on that. A shared boundary would let one late root\n // withhold every other root's already-renderable content — the blank shell this fixes.\n <Suspense key={id} fallback={null}>\n <Component />\n </Suspense>\n ))}\n </ContextChain>\n </Suspense>\n );\n};\n\n/**\n * Nests each provider around the next at render time so the element type is a constant: composing\n * the nesting into a component makes it a new type per render, remounting the whole app on any\n * capability change. A contributed context appends, so the chain extends inward and the providers\n * above keep their positions; a removed one still shifts everything below it.\n */\nconst ContextChain = ({ contexts, children }: PropsWithChildren<{ contexts: Capabilities.ReactContext[] }>) => {\n if (contexts.length === 0) {\n return <>{children}</>;\n }\n\n const [{ context: Context }, ...rest] = contexts;\n return (\n <Context>\n <ContextChain contexts={rest}>{children}</ContextChain>\n </Context>\n );\n};\n","//\n// Copyright 2025 DXOS.org\n//\n\n/** Interval between watchdog ticks. Each tick credits at most two intervals of executed time. */\nexport const STARTUP_WATCHDOG_TICK_MS = 1_000;\n\nexport type StartupWatchdogOptions = {\n stallMs: number;\n onStall: (stall: { executedMs: number }) => void;\n};\n\nexport type StartupWatchdog = {\n progress: () => void;\n dispose: () => void;\n};\n\n/**\n * Detects a startup that has stopped making progress.\n *\n * The question it answers is not \"has startup taken longer than `stallMs`?\" but \"has startup\n * done nothing for `stallMs` worth of actual execution?\". Two things make that distinction:\n *\n * 1. **Only executed time counts.** A tick runs every second and credits the time since the\n * previous tick, capped at two seconds. If the process was suspended (a hidden webview on\n * macOS, App Nap, system sleep), no ticks run while it is frozen, and the first tick after it\n * thaws credits at most two seconds no matter how long the freeze lasted. `performance.now`\n * keeps advancing through a suspension, so comparing timestamps alone would count the frozen\n * time as a stall.\n *\n * 2. **Progress resets the window.** Each `progress()` call moves the start of the stall window to\n * the current executed time. A slow boot that keeps activating modules can run for as long as\n * it needs; only one with no activation for `stallMs` of executed time is reported.\n *\n * `onStall` fires at most once. Call `dispose()` when startup finishes or fails so it never fires\n * for a boot that completed.\n */\nexport const createStartupWatchdog = ({ stallMs, onStall }: StartupWatchdogOptions): StartupWatchdog => {\n let executedMs = 0;\n let progressAtMs = 0;\n let lastTickAt = performance.now();\n\n const timer = setInterval(() => {\n const tickAt = performance.now();\n executedMs += Math.min(tickAt - lastTickAt, 2 * STARTUP_WATCHDOG_TICK_MS);\n lastTickAt = tickAt;\n if (executedMs - progressAtMs < stallMs) {\n return;\n }\n clearInterval(timer);\n onStall({ executedMs: Math.round(executedMs) });\n }, STARTUP_WATCHDOG_TICK_MS);\n\n return {\n progress: () => {\n progressAtMs = executedMs;\n },\n dispose: () => clearInterval(timer),\n };\n};\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport { RegistryContext } from '@effect/atom-react/RegistryContext';\nimport * as Effect from 'effect/Effect';\nimport * as Fiber from 'effect/Fiber';\nimport * as PubSub from 'effect/PubSub';\nimport React, { type FC, useCallback, useEffect, useMemo, useRef, useState } from 'react';\n\nimport * as EffectEx from '@dxos/effect/EffectEx';\nimport { BaseError, withContext } from '@dxos/errors';\nimport { invariant } from '@dxos/invariant';\nimport { log } from '@dxos/log';\nimport { ErrorBoundary, ErrorFallback, type FallbackProps } from '@dxos/react-error-boundary';\nimport { useAsyncEffect, useDefaultValue } from '@dxos/react-hooks';\nimport { ContextProtocolProvider } from '@dxos/web-context-react';\n\nimport { ActivationEvents, Capabilities } from '../../common/index.ts';\nimport { PluginManagerContext } from '../../context.ts';\nimport { type ActivationEvent, type Plugin, PluginManager } from '../../core/index.ts';\nimport * as Devtools from '../../Devtools.ts';\nimport { App } from '../components/App/App.tsx';\nimport { bootLoader } from '../components/App/loader.ts';\nimport { PluginManagerProvider } from '../components/PluginManager/PluginManagerProvider.ts';\nimport { SurfaceManager } from '../components/Surface/SurfaceManager.ts';\nimport { SurfaceManagerProvider } from '../components/Surface/SurfaceManagerContext.ts';\nimport { createStartupWatchdog } from './startup-watchdog.ts';\n\nconst ENABLED_KEY = 'org.dxos.app-framework.enabled';\n\nexport type StartupProgress = {\n /** Number of modules that have been activated. */\n activated: number;\n /** Total number of modules registered. */\n total: number;\n /** Fractional progress (0-1). */\n progress: number;\n /**\n * Raw activation event key (e.g. `org.dxos.app-framework.event.startup`).\n * Set on event-level transitions, *and* on module-level transitions where\n * it carries the parent activation event that first triggered the\n * module's load (plumbed through `_loadCapabilitiesForModules` →\n * `_loadModule`). Consumers can use this either as the primary id (when\n * {@link module} is absent) or as an extra \"context\" field alongside\n * {@link module}.\n */\n event?: string;\n /**\n * Raw module id (e.g. `org.dxos.plugin.observability.module.ReactSurface`)\n * when the in-flight activation is module-level. When present,\n * {@link event} may also be set, identifying the parent activation that\n * triggered this module's load.\n */\n module?: string;\n /**\n * Pre-humanized label for the currently surfaced transition (module\n * label if {@link module} is set, otherwise the event label), supplied\n * for consumers that want a sensible default. Hosts that prefer to\n * render their own label can read the raw {@link event}/{@link module}\n * fields and ignore this — the framework leaves the policy choice\n * (which transitions to surface, how to format them, whether to drop\n * sub-modules entirely) to the host's `Placeholder`.\n */\n humanizedName?: string;\n /**\n * Humanized label of the plugin owning {@link module} (e.g. \"Markdown\"), absent on\n * event-level transitions. Lets a host collapse a plugin's many module activations\n * into a single status line without re-parsing module ids.\n */\n pluginName?: string;\n /** Slug of the plugin owning {@link module} (e.g. `markdown`) — the id the boot loader's row keys on. */\n pluginSlug?: string;\n};\n\n/**\n * Event dispatched on `window` once the `Startup` activation event has fully completed.\n */\nexport const STARTUP_ACTIVATED_EVENT = 'app-framework:startup-activated';\n\n/**\n * Event dispatched on `window` when startup misses its deadline, carrying\n * {@link StartupDiagnostics} as `detail`.\n */\nexport const STARTUP_FAILED_EVENT = 'app-framework:startup-failed';\n\n/**\n * Event dispatched on `window` the first time the app shell renders and the boot loader is\n * dismissed, carrying the milliseconds since navigation start as `detail`.\n */\nexport const FIRST_INTERACTIVE_EVENT = 'app-framework:first-interactive';\n\n/** Startup did not complete before its deadline. Carries {@link StartupDiagnostics} as context. */\nexport class StartupTimeoutError extends BaseError.extend('StartupTimeoutError', 'Startup timed out') {}\n\n/** Where startup had got to when the deadline expired. */\nexport type StartupDiagnostics = {\n /** Which failure produced these: the deadline expiring, or a module refusing to activate. */\n startupFailureKind: 'timeout' | 'module-error';\n /** The deadline, in ms; reported for both kinds so the two are comparable. */\n startupTimeoutMs: number;\n /** Comma-separated activation events that completed before the failure. */\n startupEventsFired: string;\n /** Modules that had finished activating. */\n startupActivatedModules: number;\n /** Modules registered in total. */\n startupTotalModules: number;\n /** Comma-separated modules still activating, which is the set the stall is in. */\n startupInFlightModules: string;\n /** Activation event in flight when the failure hit. */\n startupLastEvent?: string;\n /** Comma-separated events queued for replay by a pending reset. */\n startupPendingReset: string;\n};\n\ndeclare global {\n interface WindowEventMap {\n [STARTUP_ACTIVATED_EVENT]: CustomEvent<void>;\n [STARTUP_FAILED_EVENT]: CustomEvent<StartupDiagnostics>;\n [FIRST_INTERACTIVE_EVENT]: CustomEvent<number>;\n }\n}\n\nexport type UseAppOptions = {\n pluginManager?: PluginManager.PluginManager;\n pluginLoader?: PluginManager.ManagerOptions['pluginLoader'];\n onPluginRemove?: PluginManager.ManagerOptions['onRemove'];\n pluginRegistryProvider?: PluginManager.ManagerOptions['pluginRegistryProvider'];\n plugins?: Plugin.Plugin[];\n defaults?: string[];\n /**\n * Additional activation events to fire before startup.\n */\n setupEvents?: ActivationEvent.ActivationEvent[];\n cacheEnabled?: boolean;\n safeMode?: boolean;\n debounce?: number;\n /**\n * Executed milliseconds without an activation event before startup is declared stalled; suspended\n * time does not count, and a boot that keeps activating is never aborted.\n */\n timeout?: number;\n /**\n * Relay per-plugin `Activating …` lines into the boot loader's status log. Off by default —\n * activation names framework internals, which is diagnostic detail rather than something a\n * user booting the app is asking for.\n */\n verboseStatus?: boolean;\n fallback?: FC<FallbackProps>;\n};\n\n/**\n * Expected usage is for this to be the entrypoint of the application.\n * Initializes plugins and renders the root components.\n *\n * @example\n * const plugins = [LayoutPlugin(), MyPlugin()];\n * const defaults = [MyPluginId];\n * const fallback = () => <div>Initializing Plugins...</div>;\n * const App = useApp({ plugins, defaults, fallback });\n * createRoot(document.getElementById('root')!).render(\n * <StrictMode>\n * <App />\n * </StrictMode>,\n * );\n *\n * @param params.pluginLoader A function which loads new plugins.\n * @param params.plugins All plugins available to the application. Plugins whose `meta.profile.tags` includes `'system'` are treated as core (force-enabled, not user-toggleable).\n * @param params.defaults Default plugins are enabled by default but can be disabled by the user.\n * @param params.cacheEnabled Whether to cache enabled plugins in localStorage.\n * @param params.safeMode Whether to enable safe mode, which disables optional plugins.\n * @param params.fallback Fallback component to render if an error occurs during startup.\n */\nexport const useApp = ({\n pluginManager,\n pluginLoader: pluginLoaderProp,\n onPluginRemove,\n pluginRegistryProvider,\n plugins: pluginsProp,\n defaults: defaultsProp,\n setupEvents: setupEventsProp,\n fallback = ErrorFallback,\n cacheEnabled = false,\n safeMode = false,\n debounce = 0,\n timeout = 30_000,\n verboseStatus = false,\n}: UseAppOptions) => {\n const plugins = useDefaultValue(pluginsProp, () => []);\n const defaults = useDefaultValue(defaultsProp, () => []);\n const setupEvents = useDefaultValue(setupEventsProp, () => []);\n\n const pluginLoader = useMemo(\n () =>\n pluginLoaderProp ??\n ((id: string) =>\n Effect.sync(() => {\n const plugin = plugins.find((plugin) => plugin.meta.profile.key === id);\n invariant(plugin, `Plugin not found: ${id}`);\n return { plugin };\n })),\n [pluginLoaderProp, plugins],\n );\n\n const readyRef = useRef(false);\n const [ready, setReady] = useState(false);\n const [error, setError] = useState<unknown>(null);\n const [startupProgress, setStartupProgress] = useState<StartupProgress>({\n activated: 0,\n total: 0,\n progress: 0,\n });\n // TODO(wittjosiah): Migrate to Atom.kvs for isomorphic storage.\n const cached: string[] = useMemo(() => JSON.parse(localStorage.getItem(ENABLED_KEY) ?? '[]'), []);\n const enabled = useMemo(\n () => (safeMode ? [] : cacheEnabled && cached.length > 0 ? cached : defaults),\n [safeMode, cacheEnabled, cached, defaults],\n );\n const isExternalManager = !!pluginManager;\n const manager = useMemo(() => {\n const mgr =\n pluginManager ??\n PluginManager.make({\n pluginLoader,\n plugins,\n enabled,\n onRemove: onPluginRemove,\n pluginRegistryProvider,\n });\n log('useApp: useMemo created/reused manager', { provided: !!pluginManager });\n return mgr;\n }, [pluginManager, pluginLoader, plugins, enabled, onPluginRemove, pluginRegistryProvider]);\n\n useEffect(() => {\n if (!cacheEnabled) {\n return;\n }\n return manager.registry.subscribe(manager.enabled, (value) => {\n localStorage.setItem(ENABLED_KEY, JSON.stringify(value));\n });\n }, [cacheEnabled, manager]);\n\n useEffect(() => {\n Devtools.setup(manager);\n }, [manager]);\n\n // Hand the boot loader the enabled plugins' icons from their own meta. This registers, it does\n // not draw: barely half the enabled set activates during startup (the rest activate lazily on\n // first use), so a row seeded from it sat half-dim for the whole boot. Meta is the icon's only\n // source — the loader is a standalone bundle with no access to the plugin registry.\n useEffect(() => {\n // Optional call, not just an optional facade: an already-loaded page can be running an\n // `index.html` whose inlined loader bundle predates this method (service-worker cache, stale\n // dev compile), and a TypeError here would strand that page on the loader forever.\n bootLoader?.plugins?.(\n plugins\n .filter(({ meta }) => enabled.includes(meta.profile.key))\n .map(({ meta }) => ({\n id: pluginSlugOfKey(meta.profile.key),\n icon: meta.profile.icon?.key,\n })),\n );\n }, [plugins, enabled]);\n\n useAsyncEffect(async () => {\n log('useApp: effect mount');\n\n manager.capabilities.contribute({\n interface: Capabilities.PluginManager,\n implementation: manager,\n module: 'org.dxos.app-framework.plugin-manager',\n });\n\n manager.capabilities.contribute({\n interface: Capabilities.AtomRegistry,\n implementation: manager.registry,\n module: 'org.dxos.app-framework.atom-registry',\n });\n\n const inFlightModules = new Set<string>();\n\n const collectDiagnostics = (startupFailureKind: StartupDiagnostics['startupFailureKind']): StartupDiagnostics => ({\n startupFailureKind,\n startupTimeoutMs: timeout,\n startupEventsFired: manager.getEventsFired().join(','),\n startupActivatedModules: manager.getActive().length,\n startupTotalModules: manager.getModules().length,\n startupInFlightModules: [...inFlightModules].join(','),\n startupLastEvent: progressRef.current.event,\n startupPendingReset: manager.getPendingReset().join(','),\n });\n\n const reportFailure = (diagnostics: StartupDiagnostics) => {\n if (typeof window !== 'undefined') {\n window.dispatchEvent(new CustomEvent(STARTUP_FAILED_EVENT, { detail: diagnostics }));\n }\n };\n\n const watchdog = createStartupWatchdog({\n stallMs: timeout,\n onStall: ({ executedMs }) => {\n const diagnostics = collectDiagnostics('timeout');\n\n log.warn('startup timeout diagnostic', { ...diagnostics, executedMs, activeModules: manager.getActive() });\n\n const abort = () => {\n void EffectEx.runAndForwardErrors(Fiber.interrupt(fiber));\n setError(\n withContext(new StartupTimeoutError({ message: `Startup timed out after ${timeout}ms` }), diagnostics),\n );\n reportFailure(diagnostics);\n };\n\n // In development the deadline is a symptom, not a verdict: a cold OPFS, a rebuild or a paused\n // debugger all overrun it while the run is perfectly healthy, and killing it discards the\n // state worth looking at. Startup continues either way and the user decides — the offer raises\n // exactly the failure this branch used to raise unprompted.\n //\n // The missing-`stalled` case does NOT fall through to failing: the loader is inlined into\n // `index.html` at build time, so a page served before this shipped has the old bundle, and\n // treating that as fatal would resurrect the dialog precisely where dev asked for a button.\n if (import.meta.env?.DEV) {\n if (bootLoader?.stalled) {\n bootLoader.stalled(abort);\n } else {\n log.warn('startup timed out; boot loader cannot offer an abort (stale inlined bundle?)', { timeout });\n }\n return;\n }\n\n abort();\n },\n });\n\n const fiber = Effect.gen(function* () {\n const queue = yield* PubSub.subscribe(manager.activation);\n const listener = yield* Effect.forkDetach(\n PubSub.take(queue).pipe(\n Effect.tap(({ event, state, module, error: error$ }) =>\n Effect.sync(() => {\n watchdog.progress();\n if (module) {\n if (state === 'activating') {\n inFlightModules.add(module);\n } else {\n inFlightModules.delete(module);\n }\n }\n // Event-level Startup activated (no `module` field) fires once,\n // after every module triggered by Startup has finished. Module\n // activations now also carry their parent event id (so the trace\n // can attribute a module to its triggering event), which means\n // each module activated under Startup publishes\n // `{ event: 'startup', state: 'activated', module: <id> }`. Without\n // the `!module` guard the listener would mark the app ready on\n // the *first* such module rather than waiting for the event-level\n // completion — leaving downstream capabilities (operation-invoker,\n // app-graph, …) un-registered when the boot loader dismisses.\n if (event === ActivationEvents.Startup.id && state === 'activated' && !module) {\n watchdog.dispose();\n setReady(true);\n readyRef.current = true;\n // Trigger startup profiler dump if available.\n (globalThis as any).composer?.profiler?.dump();\n // Notify any host observability layer that startup completed.\n // A `CustomEvent` keeps this generic — app-framework doesn't\n // import a provider, and consumers can capture the startup\n // summary without us picking one.\n if (typeof window !== 'undefined') {\n window.dispatchEvent(new CustomEvent(STARTUP_ACTIVATED_EVENT));\n }\n return;\n }\n // `activating` is the start-of-load signal. Surface the raw\n // `module` (or `event`) plus a pre-humanized label so the\n // boot loader can decide how to render each transition — show\n // everything, suppress noisy sub-modules, group by plugin,\n // or apply its own formatting. We intentionally do NOT touch\n // these fields on `activated`: pairing the two would cause\n // back-to-back identical updates to the host's effect, which\n // the boot loader treats as a re-trigger because\n // `progress.progress` moved. Leaving the label alone on\n // completion keeps it accurate (\"now activating X\") until\n // the next module starts.\n if (module && state === 'activating' && !readyRef.current) {\n const pluginSlug = pluginSlugOf(module);\n setStartupProgress((current) => ({\n ...current,\n // `event` here is the activation event that first\n // triggered this module load (plumbed through\n // `_loadCapabilitiesForModules` → `_loadModule`).\n // Falsy/empty falls back to undefined so consumers can\n // tell \"no parent context\" from \"parent context: <X>\".\n event: event || undefined,\n module,\n humanizedName: humanizeModuleId(module),\n pluginName: pluginSlug ? titleCase(pluginSlug) : undefined,\n pluginSlug,\n }));\n }\n // Update the activation count when a module commits. The\n // ring's fraction comes from this; `event`/`module`/\n // `humanizedName` were set by the matching `activating`\n // message above and are left alone so the count can advance\n // without re-firing the host's status callback.\n if (module && state === 'activated' && !readyRef.current) {\n const active = manager.getActive();\n const total = manager.getModules().length;\n setStartupProgress((current) => ({\n ...current,\n activated: active.length,\n total,\n progress: total > 0 ? active.length / total : 0,\n }));\n }\n // Event-level `activating` (no `module`) — fired at the start\n // of `_activateModulesForEvent` and recursively for each\n // before/after event. Surfaces a label during the gap before\n // the first module-level message lands; subsequent module\n // updates immediately overwrite this with a more specific\n // label.\n if (event && !module && state === 'activating' && !readyRef.current) {\n setStartupProgress((current) => ({\n ...current,\n event,\n module: undefined,\n humanizedName: humanizeEventKey(event),\n pluginName: undefined,\n pluginSlug: undefined,\n }));\n }\n if (error$ && !readyRef.current) {\n watchdog.dispose();\n const diagnostics = collectDiagnostics('module-error');\n setError(withContext(error$, diagnostics));\n reportFailure(diagnostics);\n }\n }),\n ),\n // Each yield is a clamped 4ms timer in the browser, so a yielding loop drains a startup backlog one message per tick.\n Effect.forever({ disableYield: true }),\n ),\n );\n\n yield* Effect.all([\n ...setupEvents.map((event) => manager.activate(event)),\n manager.activate(ActivationEvents.Startup),\n ]);\n\n return yield* Fiber.join(listener);\n }).pipe(Effect.scoped, Effect.runFork);\n\n return () => {\n log('useApp: effect cleanup');\n watchdog.dispose();\n void EffectEx.runAndForwardErrors(Fiber.interrupt(fiber));\n if (!isExternalManager) {\n EffectEx.runDetached(manager.shutdown());\n }\n };\n }, [manager]);\n\n const progressRef = useRef(startupProgress);\n progressRef.current = startupProgress;\n\n const surfaces = useMemo(() => new SurfaceManager(manager.capabilities, manager), [manager]);\n\n return useCallback(\n () => (\n <ErrorBoundary name='app' FallbackComponent={fallback}>\n <PluginManagerProvider value={manager}>\n <ContextProtocolProvider value={manager} context={PluginManagerContext}>\n <RegistryContext.Provider value={manager.registry}>\n <SurfaceManagerProvider value={surfaces}>\n <App\n ready={ready}\n error={error}\n debounce={debounce}\n progress={progressRef.current}\n verboseStatus={verboseStatus}\n />\n </SurfaceManagerProvider>\n </RegistryContext.Provider>\n </ContextProtocolProvider>\n </PluginManagerProvider>\n </ErrorBoundary>\n ),\n [fallback, manager, surfaces, ready, error],\n );\n};\n\n/**\n * Extracts a human-readable label from a module ID.\n *\n * Module IDs follow `org.dxos.plugin.<plugin-slug>.module.<module-name>`,\n * where `<plugin-slug>` is kebab-case and `<module-name>` is either an\n * explicit string (often kebab-case, e.g. `'observability'`, `'namespace'`)\n * or a capability tag in PascalCase from `Capability.getModuleTag(...)`\n * (e.g. `'ReactSurface'`, `'AppGraphBuilder'`). The output is\n * `\"Title Case Plugin: kebab-module\"` so the visible status names both the\n * plugin and the aspect being activated, helping disambiguate the multiple\n * modules a plugin contributes.\n *\n * Examples:\n * - \"org.dxos.plugin.markdown.module.ReactSurface\" → \"Markdown: react-surface\"\n * - \"org.dxos.plugin.observability.module.AppGraphBuilder\" → \"Observability: app-graph-builder\"\n * - \"org.dxos.plugin.observability.module.observability\" → \"Observability\"\n * (the module name matches the plugin slug — collapsed to avoid\n * \"Observability: observability\" noise.)\n */\nconst humanizeModuleId = (moduleId: string): string => {\n const match = moduleId.match(/\\.plugin\\.([^.]+)\\.module\\.(.+)$/);\n if (!match) {\n // Fallback: use the last segment.\n const parts = moduleId.split('.');\n return parts[parts.length - 1];\n }\n const [, pluginSlug, moduleName] = match;\n const pluginLabel = titleCase(pluginSlug);\n // Normalise the module name to kebab-case so PascalCase capability tags\n // (\"ReactSurface\") read consistently with explicit kebab IDs\n // (\"operation-handler\"). The two-step substitution handles consecutive\n // uppercase runs (`URLLoader` → `url-loader`) without splitting them\n // mid-acronym.\n const moduleLabel = moduleName\n .replace(/([a-z0-9])([A-Z])/g, '$1-$2')\n .replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')\n .toLowerCase();\n // Capability modules whose name matches the plugin slug (e.g. the\n // observability plugin's `observability` capability module) would render\n // as \"Observability: observability\" — drop the redundant suffix.\n if (moduleLabel === pluginSlug) {\n return pluginLabel;\n }\n return `${pluginLabel}: ${moduleLabel}`;\n};\n\n/**\n * Extracts the owning plugin's slug from a module ID, or `undefined` when the id is not\n * plugin-scoped (framework-internal modules). E.g.\n * \"org.dxos.plugin.markdown.module.ReactSurface\" → \"markdown\".\n */\n/** Plugin slug from a plugin key — \"org.dxos.plugin.markdown\" → \"markdown\". */\nexport const pluginSlugOfKey = (key: string): string => key.split('.').at(-1) ?? key;\n\nexport const pluginSlugOf = (moduleId: string): string | undefined =>\n moduleId.match(/\\.plugin\\.([^.]+)\\.module\\./)?.[1];\n\n/** Kebab slug → Title Case. */\nconst titleCase = (slug: string): string =>\n slug\n .split('-')\n .map((word) => word.charAt(0).toUpperCase() + word.slice(1))\n .join(' ');\n\n/**\n * Extracts a human-readable label from an activation event key.\n * E.g., \"org.dxos.app-framework.event.setup-react-surface\" → \"Setup React Surface\".\n */\nconst humanizeEventKey = (eventKey: string): string => {\n // Strip a leading specifier (composite key form: \"<id>:<specifier>\").\n const id = eventKey.split(':')[0];\n // Match the trailing segment after `.event.`.\n const match = id.match(/\\.event\\.(.+)$/);\n const slug = match ? match[1] : (id.split('.').pop() ?? id);\n return titleCase(slug);\n};\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport { useAtomValue } from '@effect/atom-react/Hooks';\nimport type * as Effect from 'effect/Effect';\nimport * as Atom from 'effect/reactivity/Atom';\nimport { use, useCallback, useLayoutEffect, useRef } from 'react';\n\nimport * as Operation from '@dxos/compute/Operation';\nimport * as OperationHandlerSet from '@dxos/compute/OperationHandlerSet';\n\nimport { Capabilities } from '../../common/index.ts';\nimport { type Capability } from '../../core/index.ts';\nimport { usePluginManager } from '../components/PluginManager/PluginManagerProvider.ts';\n\n/** Stable atom yielding `undefined`, used as the fallback for optional atom-capability lookups. */\nconst emptyAtomValue = Atom.make(() => undefined);\n\n/**\n * Hook to request capabilities from the plugin context.\n * @returns An array of capabilities.\n */\nexport const useCapabilities = <T>(interfaceDef: Capability.InterfaceDef<T>) => {\n const manager = usePluginManager();\n return useAtomValue(manager.capabilities.atom(interfaceDef));\n};\n\n/**\n * Hook to request a capability from the plugin context.\n *\n * Suspends (throws the contribution promise) while nothing has contributed the interface yet, so a\n * reader that renders before its provider activates parks at its nearest Suspense boundary. An\n * invariant here cannot tell \"not yet\" from \"never\": most providers are idle-gated or wait on a\n * runtime event, so the reader legitimately renders first, and hard-failing on that blanks the\n * subtree. Declaring the dependency as the module's `requires` is NOT the alternative — that\n * demotes the reader's module into the provider's (later) wave.\n *\n * @returns The capability.\n */\nexport const useCapability = <T>(interfaceDef: Capability.InterfaceDef<T>) => {\n const manager = usePluginManager();\n const capabilities = useCapabilities(interfaceDef);\n if (capabilities.length === 0) {\n throw manager.capabilities.waitForPromise(interfaceDef);\n }\n return capabilities[0];\n};\n\n/**\n * Hook to request capabilities that a plugin may or may not contribute.\n * @returns An array of capabilities, empty when none is registered.\n */\nexport const useOptionalCapabilities = <T>(interfaceDef: Capability.InterfaceDef<T>): readonly T[] => {\n const manager = usePluginManager();\n return useAtomValue(manager.capabilities.atom(interfaceDef));\n};\n\n/**\n * Hook to request a single capability that a plugin may or may not contribute.\n * @returns The first matching capability, or `undefined` when none is registered.\n */\nexport const useOptionalCapability = <T>(interfaceDef: Capability.InterfaceDef<T>): T | undefined =>\n useOptionalCapabilities(interfaceDef)[0];\n\n/**\n * Hook to get the current value of an atom capability.\n * Automatically subscribes to changes.\n * @example const settings = useAtomCapability(CommentCapabilities.Settings);\n */\nexport const useAtomCapability = <T>(atomCapability: Capability.InterfaceDef<Atom.Atom<T>>): T => {\n const atom = useCapability(atomCapability);\n return useAtomValue(atom);\n};\n\n/**\n * Tolerant variant of {@link useAtomCapability}: returns `undefined` while the atom capability is\n * not registered, rather than throwing. Use it wherever a reader can render before its provider —\n * a provider gated on a genuine runtime event (a status indicator whose state arrives with the\n * client) cannot be pulled onto the startup pass with `requires`.\n */\nexport const useOptionalAtomCapability = <T>(atomCapability: Capability.InterfaceDef<Atom.Atom<T>>): T | undefined => {\n const atom = useOptionalCapability(atomCapability);\n return useAtomValue(atom ?? emptyAtomValue) as T | undefined;\n};\n\n/**\n * Hook to get value and updater for an atom capability.\n * Returns [currentValue, updateFn] similar to useState.\n * @example const [settings, updateSettings] = useAtomCapabilityState(CommentCapabilities.Settings);\n */\nexport const useAtomCapabilityState = <T>(\n atomCapability: Capability.InterfaceDef<Atom.Writable<T>>,\n): [T, (fn: (current: T) => T) => void] => {\n const registry = useCapability(Capabilities.AtomRegistry);\n const atom = useCapability(atomCapability);\n const value = useAtomValue(atom);\n const update = useCallback(\n (fn: (current: T) => T) => {\n registry.set(atom, fn(registry.get(atom)));\n },\n [registry, atom],\n );\n return [value, update];\n};\n\n/**\n * Tolerant variant of {@link useAtomCapabilityState}: returns `[undefined, noop]` when the atom\n * capability is not registered (e.g. the contributing plugin is not installed) rather than\n * throwing. The updater is a no-op while the capability is absent.\n */\nexport const useOptionalAtomCapabilityState = <T>(\n atomCapability: Capability.InterfaceDef<Atom.Writable<T>>,\n): [T | undefined, (fn: (current: T) => T) => void] => {\n const registry = useOptionalCapability(Capabilities.AtomRegistry);\n const atom = useOptionalCapability(atomCapability);\n const value = useAtomValue(atom ?? emptyAtomValue);\n const update = useCallback(\n (fn: (current: T) => T) => {\n if (registry && atom) {\n registry.set(atom, fn(registry.get(atom)));\n }\n },\n [registry, atom],\n );\n return [value, update];\n};\n\n/**\n * Hook to get the operation invoker capability.\n */\nexport const useOperationInvoker = (): Capabilities.OperationInvoker => useCapability(Capabilities.OperationInvoker);\n\n/**\n * Suspensefully resolves an operation's handler as an effect fn: `(input) => Effect<Output>`.\n * With `map`, binds the component's callback arguments to the operation input instead:\n * `(...args) => Effect<Output>`.\n *\n * Resolves against the merged {@link Capabilities.OperationHandlers} set. Handler sets are all\n * registered by startup (only handler BODIES load lazily), so the component suspends only while\n * the matched handler's module loads. Throws {@link NoHandlerError} when no contributed set\n * knows the operation.\n *\n * The returned effect still requires the operation's declared services; run it via\n * {@link useSpaceCallback} or the {@link Capabilities.ProcessManagerRuntime}.\n *\n * @example const moveTask = useOperationHandler(TaskOperation.MoveTask);\n * @example const move = useOperationHandler(TaskOperation.MoveTask, (task: Task) => ({ task: Ref.make(task) }));\n */\nexport const useOperationHandler: {\n <const Def extends Operation.Definition.Any>(operation: Def): Operation.Definition.HandlerType<Def>;\n <const Def extends Operation.Definition.Any, TArgs extends readonly unknown[]>(\n operation: Def,\n map: (...args: TArgs) => Operation.Definition.Input<Def>,\n ): (...args: TArgs) => Effect.Effect<Operation.Definition.Output<Def>, any, Operation.Definition.Services<Def>>;\n} = <const Def extends Operation.Definition.Any, TArgs extends readonly unknown[]>(\n operation: Def,\n map?: (...args: TArgs) => Operation.Definition.Input<Def>,\n): any => {\n const handlers = useCapability(Capabilities.OperationHandlers);\n const withHandler = use(OperationHandlerSet.findHandler(handlers, operation));\n if (!withHandler) {\n throw new Operation.NoHandlerError(operation.meta.key);\n }\n const handler = withHandler.handler;\n // The ref is updated in a layout effect so a discarded concurrent render's mapper (closing\n // over that render's props) never leaks into the committed callback.\n const mapRef = useRef(map);\n useLayoutEffect(() => {\n mapRef.current = map;\n }, [map]);\n const mapped = useCallback((...args: TArgs) => handler(mapRef.current!(...args)), [handler]);\n return map ? mapped : handler;\n};\n\n/**\n * Binds an operation to a UI callback in one step: `map` turns the component's callback\n * arguments into the operation's input. The handler identity is stable across renders (the\n * mapper and options read through refs), so it replaces per-handler `useCallback` boilerplate.\n */\nexport const useOperation = <TArgs extends readonly unknown[], TInput>(\n operation: Operation.Definition<TInput, unknown>,\n map: (...args: TArgs) => TInput,\n options?: Operation.InvokeOptions,\n): ((...args: TArgs) => void) => {\n const { invokePromise } = useOperationInvoker();\n const mapRef = useRef(map);\n mapRef.current = map;\n const optionsRef = useRef(options);\n optionsRef.current = options;\n return useCallback(\n (...args: TArgs) => void invokePromise(operation, mapRef.current(...args), optionsRef.current),\n [invokePromise, operation],\n );\n};\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport { useEffect, useRef, useState } from 'react';\n\nexport enum LoadingState {\n Loading = 0,\n FadeIn = 1,\n FadeOut = 2,\n Done = 3,\n}\n\n/**\n * To avoid \"flashing\" the placeholder, we wait a period of time before starting the loading animation.\n * If loading completes during this time the placehoder is not shown, otherwise is it displayed for a minimum period of time.\n *\n * States:\n * 0: Loading - Wait for a period of time before starting the loading animation.\n * 1: Fade-in - Display a loading animation.\n * 2: Fade-out - Fade out the loading animation.\n * 3: Done - Remove the placeholder.\n */\nexport const useLoading = (ready: boolean, debounce = 0) => {\n const [stage, setStage] = useState<LoadingState>(LoadingState.Loading);\n // Mirror `ready` into a ref so the interval's `setStage` callback can read\n // the latest value without depending on the effect re-running. The pure\n // closure-capture pattern (with `ready` in deps) sticks the FSM at\n // `FadeIn` whenever HMR doesn't propagate the dep change end-to-end —\n // a ref sidesteps that entirely and fires the read on every tick.\n const readyRef = useRef(ready);\n readyRef.current = ready;\n\n useEffect(() => {\n if (!debounce) {\n return;\n }\n\n const i = setInterval(() => {\n setStage((stage) => {\n const isReady = readyRef.current;\n switch (stage) {\n case LoadingState.Loading: {\n if (!isReady) {\n return LoadingState.FadeIn;\n }\n clearInterval(i);\n return LoadingState.Done;\n }\n\n case LoadingState.FadeIn: {\n if (isReady) {\n return LoadingState.FadeOut;\n }\n break;\n }\n\n case LoadingState.FadeOut: {\n clearInterval(i);\n return LoadingState.Done;\n }\n }\n\n return stage;\n });\n }, debounce);\n\n return () => clearInterval(i);\n }, [debounce]);\n\n if (!debounce) {\n return ready ? LoadingState.Done : LoadingState.Loading;\n }\n\n return stage;\n};\n","//\n// Copyright 2026 DXOS.org\n//\n\nimport * as Context from 'effect/Context';\nimport * as Effect from 'effect/Effect';\nimport * as Layer from 'effect/Layer';\nimport { type DependencyList, use, useCallback, useMemo } from 'react';\n\nimport * as Operation from '@dxos/compute/Operation';\nimport * as ServiceResolver from '@dxos/compute/ServiceResolver';\nimport * as EffectEx from '@dxos/effect/EffectEx';\nimport type { SpaceId } from '@dxos/keys';\n\nimport { Capabilities } from '../../common/index.ts';\nimport { useCapability } from './useCapabilities.ts';\n\n/**\n * Resolve the shared {@link Capabilities.ProcessManagerRuntime} from the plugin context.\n */\nexport const useProcessManagerRuntime = (): Capabilities.ProcessManagerRuntime =>\n useCapability(Capabilities.ProcessManagerRuntime);\n\n/**\n * Build a callback that runs an effect on the {@link Capabilities.ProcessManagerRuntime}\n * with space-scoped services resolved via {@link ServiceResolver.provide}.\n *\n * Arguments passed to the returned callback are forwarded to `fn`, and the effect's result comes\n * back as the callback's promise — rejecting with the effect's failure (`E`), with\n * {@link ServiceNotAvailableError} when a tag cannot be resolved for the space, or with a\n * `TypeError` when called while `spaceId` is still `undefined`.\n *\n * The `tags` tuple must list every service the effect requires (beyond the\n * fixed {@link Capabilities.ProcessManagerRuntimeServices}); these services are\n * resolved for the given `spaceId` through the runtime's service resolver.\n *\n * Nested `Operation.invoke` / `Operation.schedule` calls within the effect\n * inherit `spaceId` as a default {@link Operation.InvokeOptions} so that\n * spawned operation processes have a space context (and therefore can resolve\n * space-scoped services like `Database.Service`) without each call site having\n * to thread the id through manually.\n */\nexport const useSpaceCallback = <\n const Tags extends readonly Context.Key<any, any>[],\n TArgs extends readonly unknown[],\n T,\n E,\n>(\n spaceId: SpaceId | undefined,\n tags: Tags,\n fn: (\n ...args: TArgs\n ) => Effect.Effect<T, E, Context.Service.Identifier<Tags[number]> | Capabilities.ProcessManagerRuntimeServices>,\n deps?: DependencyList,\n): ((...args: TArgs) => Promise<T>) => {\n const runtime = useProcessManagerRuntime();\n return useCallback(\n (...args: TArgs) => {\n if (spaceId === undefined) {\n throw new TypeError('Space not provided to useSpaceCallback');\n }\n const layer = Layer.merge(\n ServiceResolver.provide({ space: spaceId }, ...tags),\n Operation.withInvocationOptions({ spaceId }),\n );\n return runtime.runPromise(\n fn(...args).pipe(Effect.provide(layer)) as Effect.Effect<T, E | ServiceResolver.ServiceNotAvailableError, any>,\n );\n },\n // eslint-disable-next-line react-hooks/exhaustive-deps\n [runtime, spaceId, ...(deps ?? [])],\n );\n};\n\n/**\n * Suspensefully resolve a single space-scoped service via the\n * {@link Capabilities.ProcessManagerRuntime}'s service resolver.\n */\nexport const useSpaceService = <T extends Context.Key<any, any>>(\n tag: T,\n spaceId: SpaceId | undefined,\n): Context.Service.Shape<T> | undefined => {\n const runtime = useProcessManagerRuntime();\n const promise = useMemo(() => {\n if (spaceId === undefined) {\n return undefined;\n }\n const layer = ServiceResolver.provide({ space: spaceId }, tag);\n const effect = Effect.flatMap(tag, (service) => Effect.succeed(service)).pipe(Effect.provide(layer));\n return runtime.runPromiseExit(effect as Effect.Effect<Context.Service.Shape<T>, any, any>);\n }, [runtime, spaceId, tag]);\n if (!promise) {\n return undefined;\n }\n return EffectEx.unwrapExit(use(promise));\n};\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport { useAtomValue } from '@effect/atom-react/Hooks';\nimport { RegistryContext } from '@effect/atom-react/RegistryContext';\nimport type * as Atom from 'effect/reactivity/Atom';\nimport { useCallback, useContext } from 'react';\n\n/**\n * Hook to read and update a settings atom.\n * Returns the current value and an update function.\n */\nexport const useSettingsState = <T>(\n atom: Atom.Writable<T>,\n): { settings: T; updateSettings: (fn: (current: T) => T) => void } => {\n const registry = useContext(RegistryContext);\n const settings = useAtomValue(atom);\n\n const updateSettings = useCallback(\n (fn: (current: T) => T) => {\n registry.set(atom, fn(registry.get(atom)));\n },\n [registry, atom],\n );\n\n return { settings, updateSettings };\n};\n","//\n// Copyright 2025 DXOS.org\n//\n\nimport { useContext } from 'react';\n\nimport { raise } from '@dxos/debug';\n\nimport { Surface } from '../components/Surface/index.ts';\n\nexport const useSurface = (): Surface.Context => {\n return useContext(Surface.Context) ?? raise(new Error('Missing SurfaceContext'));\n};\n"],"mappings":"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"}
@@ -0,0 +1,28 @@
1
+ import { t as __exportAll } from "./chunk-rolldown-runtime.mjs";
2
+ import * as Effect from "effect/Effect";
3
+ import { BaseError } from "@dxos/errors";
4
+ //#region src/core/plugin-asset-cache.ts
5
+ var plugin_asset_cache_exports = /* @__PURE__ */ __exportAll({
6
+ PluginAssetCacheError: () => PluginAssetCacheError,
7
+ noop: () => noop
8
+ });
9
+ /**
10
+ * Tagged error for cache operations. Construction sites supply `context.operation`
11
+ * (one of `'cache' | 'evict' | 'resolve' | 'list'`) and `context.pluginId` so
12
+ * downstream handlers can route on the failed call without scanning the message.
13
+ */
14
+ var PluginAssetCacheError = class extends BaseError.extend("PluginAssetCacheError", "Plugin asset cache operation failed") {};
15
+ /**
16
+ * No-op implementation. Used when no platform cache is registered (tests, unsupported environments).
17
+ * Plugins still load — they just have no offline guarantee.
18
+ */
19
+ var noop = () => ({
20
+ cache: () => Effect.void,
21
+ evict: () => Effect.void,
22
+ resolve: (_pluginId, url) => Effect.succeed(url),
23
+ list: () => Effect.succeed([])
24
+ });
25
+ //#endregion
26
+ export { noop as n, plugin_asset_cache_exports as r, PluginAssetCacheError as t };
27
+
28
+ //# sourceMappingURL=chunk-plugin-asset-cache.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chunk-plugin-asset-cache.mjs","names":[],"sources":["../../src/core/plugin-asset-cache.ts"],"sourcesContent":["//\n// Copyright 2026 DXOS.org\n//\n\nimport * as Effect from 'effect/Effect';\n\nimport { BaseError } from '@dxos/errors';\n\n/**\n * Tagged error for cache operations. Construction sites supply `context.operation`\n * (one of `'cache' | 'evict' | 'resolve' | 'list'`) and `context.pluginId` so\n * downstream handlers can route on the failed call without scanning the message.\n */\nexport class PluginAssetCacheError extends BaseError.extend(\n 'PluginAssetCacheError',\n 'Plugin asset cache operation failed',\n) {}\n\n/**\n * Per-platform offline cache for third-party plugin assets.\n *\n * Implementations live alongside their platform glue (composer-app's\n * `asset-cache/{tauri,service-worker}.ts`); the no-op default is for tests\n * and unsupported environments.\n */\nexport interface Cache {\n /**\n * Persist all listed URLs under the namespace of `pluginId`. Idempotent.\n * The order of `urls` is significant — the first entry is treated as the entry module.\n */\n cache(pluginId: string, urls: readonly string[]): Effect.Effect<void, PluginAssetCacheError>;\n\n /**\n * Drop all assets for a plugin (uninstall).\n */\n evict(pluginId: string): Effect.Effect<void, PluginAssetCacheError>;\n\n /**\n * Resolves a plugin asset URL to a platform-specific cached URL when one is available.\n * Returns the original URL when no cached copy exists or the platform serves cached\n * responses transparently (e.g. service worker fetch interception on web).\n */\n resolve(pluginId: string, url: string): Effect.Effect<string, PluginAssetCacheError>;\n\n /**\n * List currently cached plugin ids (diagnostic).\n */\n list(): Effect.Effect<readonly string[], PluginAssetCacheError>;\n}\n\n/**\n * No-op implementation. Used when no platform cache is registered (tests, unsupported environments).\n * Plugins still load — they just have no offline guarantee.\n */\nexport const noop = (): Cache => ({\n cache: () => Effect.void,\n evict: () => Effect.void,\n resolve: (_pluginId, url) => Effect.succeed(url),\n list: () => Effect.succeed([] as readonly string[]),\n});\n"],"mappings":";;;;;;;;;;;;;AAaA,IAAa,wBAAb,cAA2C,UAAU,OACnD,yBACA,qCACF,CAAC,CAAC,CAAC;;;;;AAsCH,IAAa,cAAqB;CAChC,aAAa,OAAO;CACpB,aAAa,OAAO;CACpB,UAAU,WAAW,QAAQ,OAAO,QAAQ,GAAG;CAC/C,YAAY,OAAO,QAAQ,CAAC,CAAsB;AACpD"}
@@ -1,10 +1,10 @@
1
- import { Idle, Startup, eventKey, getEvents, isAllOf, pluginStart } from "./core/activation-event.mjs";
1
+ import { a as eventKey, d as pluginStart, n as Startup, o as getEvents, s as isAllOf, t as Idle } from "./chunk-activation-event.mjs";
2
2
  import { A as ProvidesMismatchError, D as DuplicateProviderError, E as DependencyCycleError, O as MissingProviderError, T as CapabilityNotFoundError, a as Service, r as CurrentModuleId, t as ActivatingModuleIds } from "./chunk-capability.mjs";
3
- import { expandContributions, isContribution, make as make$1, normalizeActivateResult } from "./core/capability-manager.mjs";
4
- import { LazyPluginError, PluginDependencyError, Service as Service$1, isLazy, resolveLazy } from "./core/plugin.mjs";
3
+ import { a as normalizeActivateResult, i as make$1, n as expandContributions, r as isContribution } from "./chunk-capability-manager.mjs";
4
+ import { S as resolveLazy, c as Service$1, m as isLazy, r as PluginDependencyError, t as LazyPluginError } from "./chunk-plugin.mjs";
5
5
  import { t as Manager } from "./chunk-registry.mjs";
6
- import { ReactSurface } from "./common/capabilities.mjs";
7
- import { Idle as Idle$1 } from "./common/activation-events.mjs";
6
+ import { h as ReactSurface } from "./chunk-capabilities.mjs";
7
+ import { n as Idle$1 } from "./chunk-activation-events.mjs";
8
8
  import * as Effect from "effect/Effect";
9
9
  import * as Exit from "effect/Exit";
10
10
  import * as EffectEx from "@dxos/effect/EffectEx";
@@ -24,6 +24,7 @@ import * as Duration from "effect/Duration";
24
24
  import * as Performance from "@dxos/effect/Performance";
25
25
  import * as GraphModel from "@dxos/graph/GraphModel";
26
26
  import * as Cause from "effect/Cause";
27
+ import * as Clock from "effect/Clock";
27
28
  import * as Scope from "effect/Scope";
28
29
  //#region src/core/plugin-manager/activation-graph.ts
29
30
  var makeModel = (modules) => {
@@ -271,7 +272,11 @@ var yieldToHost = Effect.suspend(() => {
271
272
  * as a typed requirement (`R = Plugin.Service`) rather than holding a manager reference — the
272
273
  * manager satisfies it once at its public boundary.
273
274
  */
275
+ var SLOW_MODULE_WARNING = Duration.seconds(10);
276
+ var SLOW_MODULE_CHECK_INTERVAL = Duration.seconds(1);
274
277
  var ModuleLoader = class {
278
+ #inFlight = /* @__PURE__ */ new Set();
279
+ #watchingSlow = false;
275
280
  #memo = /* @__PURE__ */ new Map();
276
281
  #semaphores = /* @__PURE__ */ new Map();
277
282
  #scopes = /* @__PURE__ */ new Map();
@@ -370,7 +375,7 @@ var ModuleLoader = class {
370
375
  log("deactivating", { id }, {
371
376
  "~LogMeta": "~LogMeta",
372
377
  F: __dxlog_file$4,
373
- L: 209,
378
+ L: 219,
374
379
  S: this
375
380
  });
376
381
  this.#memo.delete(id);
@@ -388,7 +393,7 @@ var ModuleLoader = class {
388
393
  log("deactivated", { id }, {
389
394
  "~LogMeta": "~LogMeta",
390
395
  F: __dxlog_file$4,
391
- L: 228,
396
+ L: 238,
392
397
  S: this
393
398
  });
394
399
  return true;
@@ -464,7 +469,7 @@ var ModuleLoader = class {
464
469
  }, {
465
470
  "~LogMeta": "~LogMeta",
466
471
  F: __dxlog_file$4,
467
- L: 321,
472
+ L: 331,
468
473
  S: this
469
474
  }))), Effect.forkDetach);
470
475
  });
@@ -477,7 +482,7 @@ var ModuleLoader = class {
477
482
  }, {
478
483
  "~LogMeta": "~LogMeta",
479
484
  F: __dxlog_file$4,
480
- L: 334,
485
+ L: 344,
481
486
  S: this
482
487
  });
483
488
  performance.mark(`module:${module.id}:start`);
@@ -521,18 +526,13 @@ var ModuleLoader = class {
521
526
  }, {
522
527
  "~LogMeta": "~LogMeta",
523
528
  F: __dxlog_file$4,
524
- L: 380,
529
+ L: 390,
525
530
  S: this
526
531
  });
527
532
  const expanded = expandContributions(normalized);
528
533
  yield* this.#fireOwnStartForSurface(expanded, pluginId);
529
534
  return expanded;
530
- }).pipe(Effect.tapCause(() => Scope.close(scope, Exit.void)), Effect.withSpan("ModuleLoader.load", { attributes: { "dx.module.id": module.id } }), together(Effect.sleep(Duration.seconds(10)).pipe(Effect.andThen(Effect.sync(() => log.warn(`module is taking a long time to activate`, { module: module.id }, {
531
- "~LogMeta": "~LogMeta",
532
- F: __dxlog_file$4,
533
- L: 395,
534
- S: this
535
- }))))), Performance.addTrackEntry({
535
+ }).pipe(Effect.tapCause(() => Scope.close(scope, Exit.void)), Effect.withSpan("ModuleLoader.load", { attributes: { "dx.module.id": module.id } }), (activation) => Effect.flatMap(this.#watchSlow(module.id), (settled) => activation.pipe(Effect.ensuring(settled))), Performance.addTrackEntry({
536
536
  name: module.id,
537
537
  devtools: {
538
538
  dataType: "track-entry",
@@ -565,7 +565,7 @@ var ModuleLoader = class {
565
565
  }, {
566
566
  "~LogMeta": "~LogMeta",
567
567
  F: __dxlog_file$4,
568
- L: 444,
568
+ L: 449,
569
569
  S: this
570
570
  });
571
571
  yield* Effect.forEach(inflight, (provider) => Effect.gen({ self: this }, function* () {
@@ -582,7 +582,7 @@ var ModuleLoader = class {
582
582
  }, {
583
583
  "~LogMeta": "~LogMeta",
584
584
  F: __dxlog_file$4,
585
- L: 467,
585
+ L: 472,
586
586
  S: this
587
587
  }))));
588
588
  });
@@ -610,7 +610,7 @@ var ModuleLoader = class {
610
610
  }, {
611
611
  "~LogMeta": "~LogMeta",
612
612
  F: __dxlog_file$4,
613
- L: 500,
613
+ L: 505,
614
614
  S: this
615
615
  });
616
616
  return Effect.void;
@@ -638,7 +638,7 @@ var ModuleLoader = class {
638
638
  }, {
639
639
  "~LogMeta": "~LogMeta",
640
640
  F: __dxlog_file$4,
641
- L: 521,
641
+ L: 526,
642
642
  S: this
643
643
  });
644
644
  const normalizedError = error instanceof Error ? error : new Error(String(error));
@@ -653,6 +653,40 @@ var ModuleLoader = class {
653
653
  return normalizedError;
654
654
  });
655
655
  }
656
+ /** Registers an in-flight activation for the slow-module warning; the returned effect deregisters it. */
657
+ #watchSlow(moduleId) {
658
+ return Effect.gen({ self: this }, function* () {
659
+ const activation = {
660
+ moduleId,
661
+ started: yield* Clock.currentTimeMillis,
662
+ warned: false
663
+ };
664
+ this.#inFlight.add(activation);
665
+ if (!this.#watchingSlow) {
666
+ this.#watchingSlow = true;
667
+ yield* this.#state.fibers.trackForked(yield* Effect.forkDetach(this.#slowWatcher()));
668
+ }
669
+ return Effect.sync(() => void this.#inFlight.delete(activation));
670
+ });
671
+ }
672
+ #slowWatcher() {
673
+ return Effect.gen({ self: this }, function* () {
674
+ for (;;) {
675
+ yield* Effect.sleep(SLOW_MODULE_CHECK_INTERVAL);
676
+ if (this.#inFlight.size === 0) return;
677
+ const now = yield* Clock.currentTimeMillis;
678
+ for (const activation of this.#inFlight) if (!activation.warned && now - activation.started >= Duration.toMillis(SLOW_MODULE_WARNING)) {
679
+ activation.warned = true;
680
+ log.warn("module is taking a long time to activate", { module: activation.moduleId }, {
681
+ "~LogMeta": "~LogMeta",
682
+ F: __dxlog_file$4,
683
+ L: 574,
684
+ S: this
685
+ });
686
+ }
687
+ }
688
+ }).pipe(Effect.ensuring(Effect.sync(() => this.#watchingSlow = false)));
689
+ }
656
690
  };
657
691
  /**
658
692
  * Runs an effect concurrently with another effect.
@@ -1678,18 +1712,16 @@ var ManagerState = class {
1678
1712
  * Tracks fibers forked by the manager's units so shutdown can interrupt in-flight work.
1679
1713
  */
1680
1714
  var FiberTracker = class {
1681
- #fibers = Effect.runSync(Ref.make([]));
1715
+ #fibers = /* @__PURE__ */ new Set();
1682
1716
  track(fiber) {
1683
- return Ref.update(this.#fibers, (fibers) => [...fibers, fiber]);
1717
+ return Effect.sync(() => void this.#fibers.add(fiber));
1684
1718
  }
1685
1719
  untrack(fiber) {
1686
- return Ref.update(this.#fibers, (fibers) => fibers.filter((existing) => existing !== fiber));
1720
+ return Effect.sync(() => void this.#fibers.delete(fiber));
1687
1721
  }
1688
1722
  /** Fire-and-forget: fork the effect and track the fiber until it settles. */
1689
1723
  fork(effect) {
1690
- const fiber = Effect.runFork(effect);
1691
- Effect.runSync(this.track(fiber));
1692
- Effect.runFork(Fiber.await(fiber).pipe(Effect.andThen(() => this.untrack(fiber))));
1724
+ this.#trackUntilSettled(Effect.runFork(effect));
1693
1725
  }
1694
1726
  /**
1695
1727
  * Registers a fiber forked inside an Effect, untracking it when it settles. The counterpart to
@@ -1697,21 +1729,22 @@ var FiberTracker = class {
1697
1729
  * cannot supply.
1698
1730
  */
1699
1731
  trackForked(fiber) {
1700
- return Effect.gen({ self: this }, function* () {
1701
- yield* this.track(fiber);
1702
- yield* Effect.forkDetach(Fiber.await(fiber).pipe(Effect.andThen(() => this.untrack(fiber))));
1703
- });
1732
+ return Effect.sync(() => this.#trackUntilSettled(fiber));
1704
1733
  }
1705
1734
  interruptAll() {
1706
1735
  return Effect.gen({ self: this }, function* () {
1707
- const fibers = yield* Ref.get(this.#fibers);
1736
+ const fibers = [...this.#fibers];
1737
+ this.#fibers.clear();
1708
1738
  yield* Effect.forEach(fibers, (fiber) => Fiber.interrupt(fiber), {
1709
1739
  concurrency: "unbounded",
1710
1740
  discard: true
1711
1741
  });
1712
- yield* Ref.set(this.#fibers, []);
1713
1742
  });
1714
1743
  }
1744
+ #trackUntilSettled(fiber) {
1745
+ this.#fibers.add(fiber);
1746
+ fiber.addObserver(() => this.#fibers.delete(fiber));
1747
+ }
1715
1748
  };
1716
1749
  /**
1717
1750
  * True when `error` (or anything along its `cause` chain) is a {@link PluginTimeoutError}.
@@ -2294,7 +2327,7 @@ var ManagerImpl = class {
2294
2327
  */
2295
2328
  _superviseFailures() {
2296
2329
  if (this._supervisor !== void 0) return;
2297
- this._supervisor = PubSub.subscribe(this._state.activation).pipe(Effect.flatMap((subscription) => PubSub.take(subscription).pipe(Effect.tap((message) => Effect.sync(() => this._autoDisableOnModuleError(message))), Effect.forever)), Effect.scoped, Effect.runFork);
2330
+ this._supervisor = PubSub.subscribe(this._state.activation).pipe(Effect.flatMap((subscription) => PubSub.take(subscription).pipe(Effect.tap((message) => Effect.sync(() => this._autoDisableOnModuleError(message))), Effect.forever({ disableYield: true }))), Effect.scoped, Effect.runFork);
2298
2331
  }
2299
2332
  /**
2300
2333
  * Auto-disables the plugin owning a failed module. Error messages without a registered
@@ -2430,7 +2463,7 @@ var ManagerImpl = class {
2430
2463
  log("shutdown", void 0, {
2431
2464
  "~LogMeta": "~LogMeta",
2432
2465
  F: __dxlog_file,
2433
- L: 591,
2466
+ L: 592,
2434
2467
  S: this
2435
2468
  });
2436
2469
  if (this._supervisor !== void 0) {
@@ -2452,7 +2485,7 @@ var ManagerImpl = class {
2452
2485
  log("shutdown complete", void 0, {
2453
2486
  "~LogMeta": "~LogMeta",
2454
2487
  F: __dxlog_file,
2455
- L: 617,
2488
+ L: 618,
2456
2489
  S: this
2457
2490
  });
2458
2491
  return true;