@dxos/plugin-native 0.8.4-main.7ace549 → 0.8.4-main.8baae0fced

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 (106) hide show
  1. package/LICENSE +102 -5
  2. package/README.md +1 -1
  3. package/dist/lib/neutral/NativePlugin.mjs +34 -0
  4. package/dist/lib/neutral/NativePlugin.mjs.map +7 -0
  5. package/dist/lib/neutral/NativeSettings-CLYG5TJH.mjs +113 -0
  6. package/dist/lib/neutral/NativeSettings-CLYG5TJH.mjs.map +7 -0
  7. package/dist/lib/neutral/capabilities/index.mjs +17 -0
  8. package/dist/lib/neutral/capabilities/index.mjs.map +7 -0
  9. package/dist/lib/neutral/chunk-J5LGTIGS.mjs +10 -0
  10. package/dist/lib/neutral/chunk-J5LGTIGS.mjs.map +7 -0
  11. package/dist/lib/{browser/chunk-SP3MEGKI.mjs → neutral/chunk-X3A57PRH.mjs} +9 -4
  12. package/dist/lib/neutral/chunk-X3A57PRH.mjs.map +7 -0
  13. package/dist/lib/neutral/components/index.mjs +9 -0
  14. package/dist/lib/neutral/components/index.mjs.map +7 -0
  15. package/dist/lib/neutral/index.mjs +8 -0
  16. package/dist/lib/neutral/index.mjs.map +7 -0
  17. package/dist/lib/neutral/meta.json +1 -0
  18. package/dist/lib/neutral/meta.mjs +8 -0
  19. package/dist/lib/neutral/meta.mjs.map +7 -0
  20. package/dist/lib/{browser/ollama-CQE7ZIWL.mjs → neutral/ollama-BHWFTROM.mjs} +14 -28
  21. package/dist/lib/neutral/ollama-BHWFTROM.mjs.map +7 -0
  22. package/dist/lib/neutral/plugin.mjs +12 -0
  23. package/dist/lib/neutral/plugin.mjs.map +7 -0
  24. package/dist/lib/neutral/react-surface-W46XEBPK.mjs +27 -0
  25. package/dist/lib/neutral/react-surface-W46XEBPK.mjs.map +7 -0
  26. package/dist/lib/neutral/settings-HF6TQNU4.mjs +28 -0
  27. package/dist/lib/neutral/settings-HF6TQNU4.mjs.map +7 -0
  28. package/dist/lib/neutral/spotlight-listener-K7S6LGL3.mjs +48 -0
  29. package/dist/lib/neutral/spotlight-listener-K7S6LGL3.mjs.map +7 -0
  30. package/dist/lib/neutral/translations.mjs +35 -0
  31. package/dist/lib/neutral/translations.mjs.map +7 -0
  32. package/dist/lib/neutral/types/index.mjs +31 -0
  33. package/dist/lib/neutral/types/index.mjs.map +7 -0
  34. package/dist/lib/neutral/updater-JEBXWSSK.mjs +244 -0
  35. package/dist/lib/neutral/updater-JEBXWSSK.mjs.map +7 -0
  36. package/dist/types/src/NativePlugin.d.ts +3 -1
  37. package/dist/types/src/NativePlugin.d.ts.map +1 -1
  38. package/dist/types/src/capabilities/index.d.ts +6 -3
  39. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  40. package/dist/types/src/capabilities/ollama.d.ts +5 -2
  41. package/dist/types/src/capabilities/ollama.d.ts.map +1 -1
  42. package/dist/types/src/capabilities/react-surface.d.ts +5 -0
  43. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -0
  44. package/dist/types/src/capabilities/settings.d.ts +7 -0
  45. package/dist/types/src/capabilities/settings.d.ts.map +1 -0
  46. package/dist/types/src/capabilities/spotlight-listener.d.ts +8 -0
  47. package/dist/types/src/capabilities/spotlight-listener.d.ts.map +1 -0
  48. package/dist/types/src/capabilities/updater.d.ts +4 -2
  49. package/dist/types/src/capabilities/updater.d.ts.map +1 -1
  50. package/dist/types/src/components/NativeSettings/NativeSettings.d.ts +6 -0
  51. package/dist/types/src/components/NativeSettings/NativeSettings.d.ts.map +1 -0
  52. package/dist/types/src/components/NativeSettings/index.d.ts +2 -0
  53. package/dist/types/src/components/NativeSettings/index.d.ts.map +1 -0
  54. package/dist/types/src/components/index.d.ts +3 -0
  55. package/dist/types/src/components/index.d.ts.map +1 -0
  56. package/dist/types/src/constants.d.ts +2 -0
  57. package/dist/types/src/constants.d.ts.map +1 -0
  58. package/dist/types/src/index.d.ts +0 -1
  59. package/dist/types/src/index.d.ts.map +1 -1
  60. package/dist/types/src/meta.d.ts +2 -2
  61. package/dist/types/src/meta.d.ts.map +1 -1
  62. package/dist/types/src/plugin.d.ts +3 -0
  63. package/dist/types/src/plugin.d.ts.map +1 -0
  64. package/dist/types/src/translations.d.ts +20 -6
  65. package/dist/types/src/translations.d.ts.map +1 -1
  66. package/dist/types/src/types/NativeCapabilities.d.ts +5 -0
  67. package/dist/types/src/types/NativeCapabilities.d.ts.map +1 -0
  68. package/dist/types/src/types/Settings.d.ts +5 -0
  69. package/dist/types/src/types/Settings.d.ts.map +1 -0
  70. package/dist/types/src/types/Update.d.ts +30 -0
  71. package/dist/types/src/types/Update.d.ts.map +1 -0
  72. package/dist/types/src/types/index.d.ts +4 -0
  73. package/dist/types/src/types/index.d.ts.map +1 -0
  74. package/dist/types/tsconfig.tsbuildinfo +1 -1
  75. package/package.json +77 -24
  76. package/src/NativePlugin.tsx +22 -20
  77. package/src/capabilities/index.ts +6 -4
  78. package/src/capabilities/ollama.ts +26 -20
  79. package/src/capabilities/react-surface.tsx +29 -0
  80. package/src/capabilities/settings.ts +31 -0
  81. package/src/capabilities/spotlight-listener.ts +58 -0
  82. package/src/capabilities/updater.ts +159 -50
  83. package/src/components/NativeSettings/NativeSettings.tsx +132 -0
  84. package/src/components/NativeSettings/index.ts +5 -0
  85. package/src/components/index.ts +7 -0
  86. package/src/constants.ts +7 -0
  87. package/src/index.ts +0 -1
  88. package/src/meta.ts +8 -4
  89. package/src/plugin.ts +9 -0
  90. package/src/translations.ts +20 -5
  91. package/src/types/NativeCapabilities.ts +16 -0
  92. package/src/types/Settings.ts +11 -0
  93. package/src/types/Update.ts +24 -0
  94. package/src/types/index.ts +7 -0
  95. package/dist/lib/browser/chunk-SP3MEGKI.mjs.map +0 -7
  96. package/dist/lib/browser/index.mjs +0 -56
  97. package/dist/lib/browser/index.mjs.map +0 -7
  98. package/dist/lib/browser/meta.json +0 -1
  99. package/dist/lib/browser/ollama-CQE7ZIWL.mjs.map +0 -7
  100. package/dist/lib/browser/updater-SDBPTCXA.mjs +0 -138
  101. package/dist/lib/browser/updater-SDBPTCXA.mjs.map +0 -7
  102. package/dist/lib/browser/window-6G542FZL.mjs +0 -34
  103. package/dist/lib/browser/window-6G542FZL.mjs.map +0 -7
  104. package/dist/types/src/capabilities/window.d.ts +0 -4
  105. package/dist/types/src/capabilities/window.d.ts.map +0 -1
  106. package/src/capabilities/window.ts +0 -49
package/package.json CHANGED
@@ -1,51 +1,104 @@
1
1
  {
2
2
  "name": "@dxos/plugin-native",
3
- "version": "0.8.4-main.7ace549",
3
+ "version": "0.8.4-main.8baae0fced",
4
4
  "description": "Composer plugin that runs when on a native device.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
7
- "license": "MIT",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "https://github.com/dxos/dxos"
10
+ },
11
+ "license": "FSL-1.1-Apache-2.0",
8
12
  "author": "DXOS.org",
9
13
  "sideEffects": true,
10
14
  "type": "module",
15
+ "imports": {
16
+ "#capabilities": {
17
+ "source": "./src/capabilities/index.ts",
18
+ "types": "./dist/types/src/capabilities/index.d.ts",
19
+ "default": "./dist/lib/neutral/capabilities/index.mjs"
20
+ },
21
+ "#components": {
22
+ "source": "./src/components/index.ts",
23
+ "types": "./dist/types/src/components/index.d.ts",
24
+ "default": "./dist/lib/neutral/components/index.mjs"
25
+ },
26
+ "#meta": {
27
+ "source": "./src/meta.ts",
28
+ "types": "./dist/types/src/meta.d.ts",
29
+ "default": "./dist/lib/neutral/meta.mjs"
30
+ },
31
+ "#plugin": {
32
+ "source": "./src/NativePlugin.tsx",
33
+ "types": "./dist/types/src/NativePlugin.d.ts",
34
+ "default": "./dist/lib/neutral/NativePlugin.mjs"
35
+ },
36
+ "#translations": {
37
+ "source": "./src/translations.ts",
38
+ "types": "./dist/types/src/translations.d.ts",
39
+ "default": "./dist/lib/neutral/translations.mjs"
40
+ },
41
+ "#types": {
42
+ "source": "./src/types/index.ts",
43
+ "types": "./dist/types/src/types/index.d.ts",
44
+ "default": "./dist/lib/neutral/types/index.mjs"
45
+ }
46
+ },
11
47
  "exports": {
12
48
  ".": {
13
49
  "source": "./src/index.ts",
14
50
  "types": "./dist/types/src/index.d.ts",
15
- "browser": "./dist/lib/browser/index.mjs"
51
+ "default": "./dist/lib/neutral/index.mjs"
52
+ },
53
+ "./plugin": {
54
+ "source": "./src/plugin.ts",
55
+ "types": "./dist/types/src/plugin.d.ts",
56
+ "default": "./dist/lib/neutral/plugin.mjs"
57
+ },
58
+ "./translations": {
59
+ "source": "./src/translations.ts",
60
+ "types": "./dist/types/src/translations.d.ts",
61
+ "default": "./dist/lib/neutral/translations.mjs"
16
62
  }
17
63
  },
18
64
  "types": "dist/types/src/index.d.ts",
19
- "typesVersions": {
20
- "*": {}
21
- },
22
65
  "files": [
23
66
  "dist",
24
67
  "src"
25
68
  ],
26
69
  "dependencies": {
27
- "@effect/platform": "^0.92.1",
28
- "@tauri-apps/api": "^2.8.0",
29
- "@tauri-apps/plugin-os": "^2.3.1",
30
- "@tauri-apps/plugin-process": "^2.3.0",
31
- "@tauri-apps/plugin-shell": "^2.3.1",
32
- "@tauri-apps/plugin-updater": "^2.9.0",
33
- "effect": "3.18.3",
34
- "@dxos/ai": "0.8.4-main.7ace549",
35
- "@dxos/app-framework": "0.8.4-main.7ace549",
36
- "@dxos/log": "0.8.4-main.7ace549",
37
- "@dxos/plugin-assistant": "0.8.4-main.7ace549",
38
- "@dxos/plugin-help": "0.8.4-main.7ace549",
39
- "@dxos/util": "0.8.4-main.7ace549",
40
- "@dxos/plugin-navtree": "0.8.4-main.7ace549"
70
+ "@effect-atom/atom-react": "^0.5.0",
71
+ "@effect/platform": "0.94.4",
72
+ "@tauri-apps/api": "^2.11.0",
73
+ "@tauri-apps/plugin-os": "^2.3.2",
74
+ "@tauri-apps/plugin-process": "^2.3.1",
75
+ "@tauri-apps/plugin-shell": "^2.3.5",
76
+ "@tauri-apps/plugin-updater": "^2.10.1",
77
+ "date-fns": "^3.6.0",
78
+ "@dxos/ai": "0.8.4-main.8baae0fced",
79
+ "@dxos/app-framework": "0.8.4-main.8baae0fced",
80
+ "@dxos/app-toolkit": "0.8.4-main.8baae0fced",
81
+ "@dxos/effect": "0.8.4-main.8baae0fced",
82
+ "@dxos/log": "0.8.4-main.8baae0fced",
83
+ "@dxos/operation": "0.8.4-main.8baae0fced",
84
+ "@dxos/plugin-navtree": "0.8.4-main.8baae0fced",
85
+ "@dxos/util": "0.8.4-main.8baae0fced",
86
+ "@dxos/plugin-assistant": "0.8.4-main.8baae0fced"
41
87
  },
42
88
  "devDependencies": {
43
- "vite": "7.1.9",
44
- "@dxos/react-ui": "0.8.4-main.7ace549"
89
+ "@types/react": "~19.2.7",
90
+ "effect": "3.20.0",
91
+ "react": "~19.2.3",
92
+ "vite": "^8.0.13",
93
+ "@dxos/plugin-testing": "0.8.4-main.8baae0fced",
94
+ "@dxos/react-ui": "0.8.4-main.8baae0fced",
95
+ "@dxos/react-ui-form": "0.8.4-main.8baae0fced"
45
96
  },
46
97
  "peerDependencies": {
47
- "effect": "^3.13.3",
48
- "@dxos/react-ui": "0.8.4-main.7ace549"
98
+ "effect": "3.20.0",
99
+ "react": "~19.2.3",
100
+ "@dxos/react-ui": "0.8.4-main.8baae0fced",
101
+ "@dxos/react-ui-form": "0.8.4-main.8baae0fced"
49
102
  },
50
103
  "publishConfig": {
51
104
  "access": "public"
@@ -2,32 +2,34 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { Capabilities, Events, contributes, defineModule, definePlugin } from '@dxos/app-framework';
5
+ import { ActivationEvents, Plugin } from '@dxos/app-framework';
6
+ import { AppPlugin } from '@dxos/app-toolkit';
6
7
  import { AssistantEvents } from '@dxos/plugin-assistant';
7
8
 
8
- import { Ollama, Updater, Window } from './capabilities';
9
- import { meta } from './meta';
10
- import { translations } from './translations';
9
+ import { NativeSettings, Ollama, ReactSurface, SpotlightListener, Updater } from '#capabilities';
10
+ import { meta } from '#meta';
11
+ import { translations } from '#translations';
11
12
 
12
- export const NativePlugin = definePlugin(meta, () => [
13
- defineModule({
14
- id: `${meta.id}/module/translations`,
15
- activatesOn: Events.SetupTranslations,
16
- activate: () => contributes(Capabilities.Translations, translations),
13
+ export const NativePlugin = Plugin.define(meta).pipe(
14
+ AppPlugin.addSettingsModule({ activate: NativeSettings }),
15
+ AppPlugin.addSurfaceModule({ activate: ReactSurface }),
16
+ AppPlugin.addTranslationsModule({ translations }),
17
+ Plugin.addModule({
18
+ id: 'spotlight-listener',
19
+ activatesOn: ActivationEvents.ProcessManagerReady,
20
+ activate: SpotlightListener,
17
21
  }),
18
- defineModule({
19
- id: `${meta.id}/module/updater`,
20
- activatesOn: Events.DispatcherReady,
22
+ Plugin.addModule({
23
+ id: 'updater',
24
+ activatesOn: ActivationEvents.ProcessManagerReady,
21
25
  activate: Updater,
22
26
  }),
23
- defineModule({
24
- id: `${meta.id}/module/window`,
25
- activatesOn: Events.DispatcherReady,
26
- activate: Window,
27
- }),
28
- defineModule({
29
- id: `${meta.id}/module/ollama`,
27
+ Plugin.addModule({
28
+ id: 'ollama',
30
29
  activatesOn: AssistantEvents.SetupAiServiceProviders,
31
30
  activate: Ollama,
32
31
  }),
33
- ]);
32
+ Plugin.make,
33
+ );
34
+
35
+ export default NativePlugin;
@@ -2,8 +2,10 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { lazy } from '@dxos/app-framework';
5
+ import { Capability } from '@dxos/app-framework';
6
6
 
7
- export const Ollama = lazy(() => import('./ollama'));
8
- export const Updater = lazy(() => import('./updater'));
9
- export const Window = lazy(() => import('./window'));
7
+ export const NativeSettings = Capability.lazy('NativeSettings', () => import('./settings'));
8
+ export const Ollama = Capability.lazy('Ollama', () => import('./ollama'));
9
+ export const ReactSurface = Capability.lazy('ReactSurface', () => import('./react-surface'));
10
+ export const SpotlightListener = Capability.lazy('SpotlightListener', () => import('./spotlight-listener'));
11
+ export const Updater = Capability.lazy('Updater', () => import('./updater'));
@@ -10,29 +10,33 @@ import * as Effect from 'effect/Effect';
10
10
  import * as Layer from 'effect/Layer';
11
11
  import * as ManagedRuntime from 'effect/ManagedRuntime';
12
12
 
13
- import * as AiServiceRouter from '@dxos/ai/AiServiceRouter';
14
- import { Capabilities, type Capability, type PluginContext, contributes } from '@dxos/app-framework';
13
+ import { type AiModelResolver } from '@dxos/ai';
14
+ import { OllamaResolver } from '@dxos/ai/resolvers';
15
+ import { Capability } from '@dxos/app-framework';
16
+ import { AppCapabilities } from '@dxos/app-toolkit';
15
17
  import { log } from '@dxos/log';
16
18
 
17
- // Running ollama on non-standard port.
19
+ // NOTE: Running ollama on non-standard port (config Tauri).
18
20
  const OLLAMA_HOST = 'http://localhost:21434';
19
21
 
20
- export default (_context: PluginContext): Capability<any> => {
21
- const runtime = ManagedRuntime.make(OllamaSidecar.layerLive);
22
+ export type OllamaCapabilities = Capability.Capability<typeof AppCapabilities.AiModelResolver>;
22
23
 
23
- // Layer for the sidecar but the lifecycle is managed by the runtime.
24
- const sidecarLayer = Layer.effectContext(
25
- runtime.runtimeEffect.pipe(Effect.map((rt) => rt.context.pipe(Context.pick(OllamaSidecar)))),
26
- );
24
+ export default Capability.makeModule<[], OllamaCapabilities>(() =>
25
+ Effect.sync(() => {
26
+ const runtime = ManagedRuntime.make(OllamaSidecar.layerLive);
27
27
 
28
- return contributes(
29
- Capabilities.AiModelResolver,
30
- OllamaSidecarModelResolver.pipe(Layer.provide(sidecarLayer)),
31
- async () => {
32
- await runtime.dispose();
33
- },
34
- );
35
- };
28
+ // Layer for the sidecar but the lifecycle is managed by the runtime.
29
+ const sidecarLayer = Layer.effectContext(
30
+ runtime.runtimeEffect.pipe(Effect.map((rt) => rt.context.pipe(Context.pick(OllamaSidecar)))),
31
+ );
32
+
33
+ return Capability.contributes(
34
+ AppCapabilities.AiModelResolver,
35
+ OllamaSidecarModelResolver.pipe(Layer.provide(sidecarLayer)),
36
+ () => Effect.tryPromise(() => runtime.dispose()),
37
+ );
38
+ }),
39
+ );
36
40
 
37
41
  class OllamaSidecar extends Context.Tag('@dxos/plugin-native/OllamaSidecar')<
38
42
  OllamaSidecar,
@@ -51,7 +55,9 @@ class OllamaSidecar extends Context.Tag('@dxos/plugin-native/OllamaSidecar')<
51
55
  });
52
56
 
53
57
  // NOTE: Keeping those as console.log for consistent formatting.
58
+ // eslint-disable-next-line no-console
54
59
  command.stdout.on('data', (data) => console.log('[ollama]', data.toString()));
60
+ // eslint-disable-next-line no-console
55
61
  command.stderr.on('data', (data) => console.error('[ollama]', data.toString()));
56
62
  command.on('close', (code) => log.info('Ollama process exited', { code }));
57
63
  command.on('error', (error) => log.error('Ollama error', { error }));
@@ -70,12 +76,12 @@ class OllamaSidecar extends Context.Tag('@dxos/plugin-native/OllamaSidecar')<
70
76
  );
71
77
  }
72
78
 
73
- const OllamaSidecarModelResolver: Layer.Layer<AiServiceRouter.AiModelResolver, never, OllamaSidecar> =
79
+ const OllamaSidecarModelResolver: Layer.Layer<AiModelResolver.AiModelResolver, never, OllamaSidecar> =
74
80
  Layer.unwrapEffect(
75
81
  Effect.gen(function* () {
76
82
  const { endpoint } = yield* OllamaSidecar;
77
- return AiServiceRouter.OllamaResolver({
78
- host: endpoint,
83
+ return OllamaResolver.make({
84
+ endpoint,
79
85
  transformClient: HttpClient.withTracerPropagation(false),
80
86
  });
81
87
  }),
@@ -0,0 +1,29 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Effect from 'effect/Effect';
6
+ import React from 'react';
7
+
8
+ import { Capabilities, Capability } from '@dxos/app-framework';
9
+ import { Surface, useSettingsState } from '@dxos/app-framework/ui';
10
+ import { AppSurface } from '@dxos/app-toolkit/ui';
11
+
12
+ import { NativeSettings } from '#components';
13
+ import { meta } from '#meta';
14
+ import { type Settings } from '#types';
15
+
16
+ export default Capability.makeModule(() =>
17
+ Effect.succeed(
18
+ Capability.contributes(Capabilities.ReactSurface, [
19
+ Surface.create({
20
+ id: 'plugin-settings',
21
+ filter: AppSurface.settings(AppSurface.Article, meta.id),
22
+ component: ({ data: { subject } }) => {
23
+ const { settings, updateSettings } = useSettingsState<Settings.Settings>(subject.atom);
24
+ return <NativeSettings settings={settings} onSettingsChange={updateSettings} />;
25
+ },
26
+ }),
27
+ ]),
28
+ ),
29
+ );
@@ -0,0 +1,31 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Effect from 'effect/Effect';
6
+
7
+ import { Capability } from '@dxos/app-framework';
8
+ import { AppCapabilities } from '@dxos/app-toolkit';
9
+ import { createKvsStore } from '@dxos/effect';
10
+
11
+ import { meta } from '#meta';
12
+ import { NativeCapabilities, Settings } from '#types';
13
+
14
+ export default Capability.makeModule(() =>
15
+ Effect.sync(() => {
16
+ const settingsAtom = createKvsStore({
17
+ key: meta.id,
18
+ schema: Settings.Settings,
19
+ defaultValue: () => ({}),
20
+ });
21
+
22
+ return [
23
+ Capability.contributes(NativeCapabilities.Settings, settingsAtom),
24
+ Capability.contributes(AppCapabilities.Settings, {
25
+ prefix: meta.id,
26
+ schema: Settings.Settings,
27
+ atom: settingsAtom,
28
+ }),
29
+ ];
30
+ }),
31
+ );
@@ -0,0 +1,58 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import * as Effect from 'effect/Effect';
6
+
7
+ import { Capabilities, Capability } from '@dxos/app-framework';
8
+ import { LayoutOperation } from '@dxos/app-toolkit';
9
+ import { log } from '@dxos/log';
10
+
11
+ // TODO(wittjosiah): Formalize with a stricter schema if we evolve this protocol.
12
+ type SpotlightInvokePayload = {
13
+ operation: string;
14
+ payload?: Record<string, any>;
15
+ };
16
+
17
+ /**
18
+ * Listens for spotlight:invoke events from the popover window and dispatches the corresponding operation.
19
+ */
20
+ export default Capability.makeModule(
21
+ Effect.fnUntraced(function* () {
22
+ const { invokePromise } = yield* Capability.get(Capabilities.OperationInvoker);
23
+
24
+ const unlisten = yield* Effect.promise(async () => {
25
+ const { listen } = await import('@tauri-apps/api/event');
26
+ const { getCurrentWindow } = await import('@tauri-apps/api/window');
27
+
28
+ return listen<SpotlightInvokePayload>('spotlight:invoke', async (event) => {
29
+ const { operation, payload } = event.payload;
30
+ log.info('Received spotlight invoke event', { operation, payload });
31
+ try {
32
+ switch (operation) {
33
+ case 'open':
34
+ await invokePromise(LayoutOperation.Open, payload as any);
35
+ break;
36
+ case 'switch-workspace':
37
+ await invokePromise(LayoutOperation.SwitchWorkspace, payload as any);
38
+ break;
39
+ default:
40
+ log.warn('Unknown spotlight operation', { operation });
41
+ }
42
+
43
+ const mainWindow = getCurrentWindow();
44
+ await mainWindow.show();
45
+ await mainWindow.setFocus();
46
+ } catch (err) {
47
+ log.catch(err);
48
+ }
49
+ });
50
+ });
51
+
52
+ return Capability.contributes(Capabilities.Null, null, () =>
53
+ Effect.sync(() => {
54
+ unlisten();
55
+ }),
56
+ );
57
+ }),
58
+ );
@@ -2,84 +2,193 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
+ import { Atom } from '@effect-atom/atom-react';
5
6
  import { type } from '@tauri-apps/plugin-os';
6
7
  import { relaunch } from '@tauri-apps/plugin-process';
7
- import { type DownloadEvent, check } from '@tauri-apps/plugin-updater';
8
+ import * as Updater from '@tauri-apps/plugin-updater';
8
9
  import * as Duration from 'effect/Duration';
9
10
  import * as Effect from 'effect/Effect';
10
11
  import * as Fiber from 'effect/Fiber';
11
- import * as Function from 'effect/Function';
12
12
  import * as Match from 'effect/Match';
13
13
  import * as Schedule from 'effect/Schedule';
14
14
 
15
- import { Capabilities, LayoutAction, type PluginContext, contributes, createIntent } from '@dxos/app-framework';
15
+ import { Capabilities, Capability } from '@dxos/app-framework';
16
+ import { LayoutOperation } from '@dxos/app-toolkit';
16
17
  import { log } from '@dxos/log';
17
18
 
18
- import { meta } from '../meta';
19
+ import { meta } from '#meta';
20
+ import { NativeCapabilities, type Update } from '#types';
21
+
22
+ import { TAURI_LOCALHOST_PORT } from '../constants';
19
23
 
20
24
  const SUPPORTS_OTA = ['linux', 'macos', 'windows'];
21
25
 
22
- export default (context: PluginContext) => {
23
- // Skip updates if not supported or in dev mode.
24
- const platform = type();
25
- if (!SUPPORTS_OTA.includes(platform) || window.location.hostname === 'localhost') {
26
- return contributes(Capabilities.Null, null);
26
+ /** Safe wrapper around Updater.check(). Distinguishes "no update" (ok with null update) from a thrown error. */
27
+ const safeCheck = async (): Promise<{ ok: true; update: Updater.Update | null } | { ok: false; error: string }> => {
28
+ try {
29
+ return { ok: true, update: await Updater.check() };
30
+ } catch (error) {
31
+ log.error('failed to check for updates', { error });
32
+ return { ok: false, error: formatError(error) };
33
+ }
34
+ };
35
+
36
+ /** Safe wrapper around update.downloadAndInstall(). */
37
+ const safeDownloadAndInstall = async (
38
+ update: Updater.Update,
39
+ onEvent: (event: Updater.DownloadEvent) => void,
40
+ ): Promise<{ ok: true } | { ok: false; error: string }> => {
41
+ try {
42
+ await update.downloadAndInstall(onEvent);
43
+ return { ok: true };
44
+ } catch (error) {
45
+ log.error('failed to download and install update', { error });
46
+ return { ok: false, error: formatError(error) };
27
47
  }
48
+ };
49
+
50
+ /** Extract a user-readable error string from whatever the Tauri updater threw. */
51
+ const formatError = (error: unknown): string => {
52
+ if (typeof error === 'string') {
53
+ return error;
54
+ }
55
+ if (error instanceof Error) {
56
+ return error.message;
57
+ }
58
+ return String(error);
59
+ };
60
+
61
+ export default Capability.makeModule(
62
+ Effect.fnUntraced(function* () {
63
+ const platform = type();
64
+ const isDevServer = window.location.port !== TAURI_LOCALHOST_PORT;
65
+ const enabled = SUPPORTS_OTA.includes(platform) && !isDevServer;
66
+
67
+ const registry = yield* Capability.get(Capabilities.AtomRegistry);
68
+ const { invoke } = yield* Capability.get(Capabilities.OperationInvoker);
28
69
 
29
- const { dispatch } = context.getCapability(Capabilities.IntentDispatcher);
70
+ const statusAtom = Atom.make<Update.Status>(enabled ? { kind: 'idle' } : { kind: 'unsupported' }).pipe(
71
+ Atom.keepAlive,
72
+ );
30
73
 
31
- // https://tauri.app/plugin/updater/#checking-for-updates
32
- const action = Effect.gen(function* () {
33
- log.info('checking for updates');
34
- const update = yield* Effect.tryPromise(() => check());
35
- log.info('checked for updates', { version: update?.version, currentVersion: update?.currentVersion });
36
- if (update) {
37
- log.info('update available', update);
74
+ // Updater.Update is a class with instance methods (downloadAndInstall) and can't live in an
75
+ // atom value; cache it here between check and install.
76
+ let pendingUpdate: Updater.Update | null = null;
77
+
78
+ // Core: check for an update and update the status atom. Returns the update if available.
79
+ const doCheck = async (): Promise<Updater.Update | null> => {
80
+ registry.set(statusAtom, { kind: 'checking' });
81
+ log.info('checking for updates');
82
+ const result = await safeCheck();
83
+ if (!result.ok) {
84
+ pendingUpdate = null;
85
+ registry.set(statusAtom, { kind: 'failed', error: result.error });
86
+ return null;
87
+ }
88
+ const update = result.update;
89
+ if (!update?.available) {
90
+ pendingUpdate = null;
91
+ registry.set(statusAtom, { kind: 'up-to-date', checkedAt: Date.now() });
92
+ return null;
93
+ }
94
+ log.info('update available', { version: update.version });
95
+ pendingUpdate = update;
96
+ registry.set(statusAtom, { kind: 'available', version: update.version });
97
+ return update;
98
+ };
99
+
100
+ // Core: download + install the cached pending update, streaming progress to the status atom.
101
+ const doInstall = async (): Promise<boolean> => {
102
+ const update = pendingUpdate;
103
+ if (!update) {
104
+ return false;
105
+ }
38
106
  let downloaded = 0;
39
107
  let contentLength = 0;
40
-
41
- const handleDownload = Match.type<DownloadEvent>().pipe(
108
+ registry.set(statusAtom, { kind: 'downloading', downloaded: 0, contentLength: 0 });
109
+ const onEvent = Match.type<Updater.DownloadEvent>().pipe(
42
110
  Match.when({ event: 'Started' }, (event) => {
43
111
  contentLength = event.data.contentLength ?? 0;
44
- log.info('download started', { contentLength });
112
+ registry.set(statusAtom, { kind: 'downloading', downloaded, contentLength });
45
113
  }),
46
114
  Match.when({ event: 'Progress' }, (event) => {
47
115
  downloaded += event.data.chunkLength;
48
- log.verbose('download progress', { downloaded, contentLength });
116
+ registry.set(statusAtom, { kind: 'downloading', downloaded, contentLength });
49
117
  }),
50
118
  Match.when({ event: 'Finished' }, () => {
51
119
  log.info('download completed');
52
120
  }),
53
121
  Match.exhaustive,
54
122
  );
123
+ const result = await safeDownloadAndInstall(update, onEvent);
124
+ if (result.ok) {
125
+ registry.set(statusAtom, { kind: 'ready' });
126
+ return true;
127
+ }
128
+ registry.set(statusAtom, { kind: 'failed', error: result.error });
129
+ return false;
130
+ };
55
131
 
56
- yield* Effect.tryPromise(() => update.downloadAndInstall(handleDownload));
57
-
58
- yield* dispatch(
59
- createIntent(LayoutAction.AddToast, {
60
- part: 'toast',
61
- subject: {
62
- id: `${meta.id}/update-ready`,
63
- title: ['update ready label', { ns: meta.id }],
64
- description: ['update ready description', { ns: meta.id }],
65
- duration: Infinity,
66
- actionLabel: ['update label', { ns: meta.id }],
67
- actionAlt: ['update alt', { ns: meta.id }],
68
- onAction: () => relaunch(),
69
- },
70
- }),
71
- );
132
+ const manager: Update.Manager = {
133
+ status: statusAtom,
134
+ check: async () => {
135
+ if (!enabled) {
136
+ return;
137
+ }
138
+ await doCheck();
139
+ },
140
+ install: async () => {
141
+ if (!enabled) {
142
+ return;
143
+ }
144
+ await doInstall();
145
+ },
146
+ relaunch: async () => {
147
+ await relaunch();
148
+ },
149
+ };
150
+
151
+ const managerContribution = Capability.contributes(NativeCapabilities.UpdateManager, manager);
152
+
153
+ if (!enabled) {
154
+ log.info('updater disabled', { platform, port: window.location.port });
155
+ return [managerContribution];
72
156
  }
73
- });
74
-
75
- const fiber = Function.pipe(
76
- // prettier-ignore
77
- action,
78
- Effect.repeat(Schedule.fixed(Duration.hours(1))),
79
- Effect.runFork,
80
- );
81
-
82
- return contributes(Capabilities.Null, null, () => {
83
- Effect.runSync(Fiber.interrupt(fiber));
84
- });
85
- };
157
+
158
+ // Background flow: periodic check + auto-install + toast when ready.
159
+ // The toast is the entry point for users who weren't watching the settings panel.
160
+ const backgroundAction = Effect.gen(function* () {
161
+ const update = yield* Effect.promise(() => doCheck());
162
+ if (!update) {
163
+ return true;
164
+ }
165
+ const ok = yield* Effect.promise(() => doInstall());
166
+ if (!ok) {
167
+ return true;
168
+ }
169
+ yield* invoke(LayoutOperation.AddToast, {
170
+ id: `${meta.id}.update-ready`,
171
+ title: ['update-ready.label', { ns: meta.id }],
172
+ description: ['update-ready.description', { ns: meta.id }],
173
+ duration: Infinity,
174
+ actionLabel: ['update.label', { ns: meta.id }],
175
+ actionAlt: ['update.alt', { ns: meta.id }],
176
+ onAction: () => relaunch(),
177
+ });
178
+ return false;
179
+ });
180
+
181
+ const schedule = Schedule.fixed(Duration.hours(1)).pipe(
182
+ Schedule.whileInput((keepChecking: boolean) => keepChecking),
183
+ );
184
+ const fiber = yield* backgroundAction.pipe(Effect.repeat(schedule), Effect.forkDaemon);
185
+ log.info('updater module initialized, update check scheduled');
186
+
187
+ return [
188
+ managerContribution,
189
+ // Return the interruption effect directly; Fiber.interrupt is async and would throw
190
+ // AsyncFiberException if wrapped in Effect.runSync.
191
+ Capability.contributes(Capabilities.Null, null, () => Fiber.interrupt(fiber)),
192
+ ];
193
+ }),
194
+ );