@dxos/plugin-native 0.8.4-main.c85a9c8dae → 0.8.4-main.d9fc60f731

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (121) hide show
  1. package/LICENSE +102 -5
  2. package/PLUGIN.mdl +284 -0
  3. package/README.md +1 -1
  4. package/dist/lib/neutral/NativePlugin.mjs +46 -0
  5. package/dist/lib/neutral/NativePlugin.mjs.map +7 -0
  6. package/dist/lib/neutral/NativeSettings-CLYG5TJH.mjs +113 -0
  7. package/dist/lib/neutral/NativeSettings-CLYG5TJH.mjs.map +7 -0
  8. package/dist/lib/neutral/capabilities/index.mjs +17 -0
  9. package/dist/lib/neutral/capabilities/index.mjs.map +7 -0
  10. package/dist/lib/neutral/chunk-75E24KAJ.mjs +34 -0
  11. package/dist/lib/neutral/chunk-75E24KAJ.mjs.map +7 -0
  12. package/dist/lib/neutral/chunk-J5LGTIGS.mjs +10 -0
  13. package/dist/lib/neutral/chunk-J5LGTIGS.mjs.map +7 -0
  14. package/dist/lib/neutral/components/index.mjs +9 -0
  15. package/dist/lib/neutral/components/index.mjs.map +7 -0
  16. package/dist/lib/neutral/index.mjs +8 -0
  17. package/dist/lib/neutral/index.mjs.map +7 -0
  18. package/dist/lib/neutral/meta.json +1 -0
  19. package/dist/lib/neutral/meta.mjs +8 -0
  20. package/dist/lib/neutral/meta.mjs.map +7 -0
  21. package/dist/lib/{browser/ollama-QOB3EVAJ.mjs → neutral/ollama-BHWFTROM.mjs} +8 -21
  22. package/dist/lib/{browser/ollama-QOB3EVAJ.mjs.map → neutral/ollama-BHWFTROM.mjs.map} +3 -3
  23. package/dist/lib/neutral/plugin.mjs +12 -0
  24. package/dist/lib/neutral/plugin.mjs.map +7 -0
  25. package/dist/lib/neutral/react-surface-ETNGHN4X.mjs +27 -0
  26. package/dist/lib/neutral/react-surface-ETNGHN4X.mjs.map +7 -0
  27. package/dist/lib/neutral/settings-HF6TQNU4.mjs +28 -0
  28. package/dist/lib/neutral/settings-HF6TQNU4.mjs.map +7 -0
  29. package/dist/lib/neutral/spotlight-listener-K7S6LGL3.mjs +48 -0
  30. package/dist/lib/neutral/spotlight-listener-K7S6LGL3.mjs.map +7 -0
  31. package/dist/lib/neutral/translations.mjs +35 -0
  32. package/dist/lib/neutral/translations.mjs.map +7 -0
  33. package/dist/lib/neutral/types/index.mjs +31 -0
  34. package/dist/lib/neutral/types/index.mjs.map +7 -0
  35. package/dist/lib/neutral/updater-JEBXWSSK.mjs +244 -0
  36. package/dist/lib/neutral/updater-JEBXWSSK.mjs.map +7 -0
  37. package/dist/types/src/NativePlugin.d.ts +1 -0
  38. package/dist/types/src/NativePlugin.d.ts.map +1 -1
  39. package/dist/types/src/capabilities/index.d.ts +6 -3
  40. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  41. package/dist/types/src/capabilities/ollama.d.ts.map +1 -0
  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 +6 -0
  49. package/dist/types/src/capabilities/updater.d.ts.map +1 -0
  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 -2
  59. package/dist/types/src/index.d.ts.map +1 -1
  60. package/dist/types/src/meta.d.ts +1 -1
  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 +75 -27
  76. package/src/NativePlugin.tsx +19 -9
  77. package/src/capabilities/index.ts +7 -3
  78. package/src/capabilities/react-surface.tsx +29 -0
  79. package/src/capabilities/settings.ts +31 -0
  80. package/src/capabilities/spotlight-listener.ts +58 -0
  81. package/src/capabilities/updater.ts +194 -0
  82. package/src/components/NativeSettings/NativeSettings.tsx +132 -0
  83. package/src/components/NativeSettings/index.ts +5 -0
  84. package/src/components/index.ts +7 -0
  85. package/src/constants.ts +7 -0
  86. package/src/index.ts +0 -2
  87. package/src/meta.ts +24 -7
  88. package/src/plugin.ts +9 -0
  89. package/src/translations.ts +20 -5
  90. package/src/types/NativeCapabilities.ts +16 -0
  91. package/src/types/Settings.ts +11 -0
  92. package/src/types/Update.ts +24 -0
  93. package/src/types/index.ts +7 -0
  94. package/src/vite-env.d.ts +10 -0
  95. package/dist/lib/browser/chunk-6374DBU4.mjs +0 -15
  96. package/dist/lib/browser/chunk-6374DBU4.mjs.map +0 -7
  97. package/dist/lib/browser/index.mjs +0 -59
  98. package/dist/lib/browser/index.mjs.map +0 -7
  99. package/dist/lib/browser/meta.json +0 -1
  100. package/dist/lib/browser/updater-AR27PYM4.mjs +0 -190
  101. package/dist/lib/browser/updater-AR27PYM4.mjs.map +0 -7
  102. package/dist/lib/browser/window-GKYYTCEI.mjs +0 -38
  103. package/dist/lib/browser/window-GKYYTCEI.mjs.map +0 -7
  104. package/dist/types/src/capabilities/ollama/index.d.ts +0 -3
  105. package/dist/types/src/capabilities/ollama/index.d.ts.map +0 -1
  106. package/dist/types/src/capabilities/ollama/ollama.d.ts.map +0 -1
  107. package/dist/types/src/capabilities/updater/index.d.ts +0 -3
  108. package/dist/types/src/capabilities/updater/index.d.ts.map +0 -1
  109. package/dist/types/src/capabilities/updater/updater.d.ts +0 -5
  110. package/dist/types/src/capabilities/updater/updater.d.ts.map +0 -1
  111. package/dist/types/src/capabilities/window/index.d.ts +0 -3
  112. package/dist/types/src/capabilities/window/index.d.ts.map +0 -1
  113. package/dist/types/src/capabilities/window/window.d.ts +0 -11
  114. package/dist/types/src/capabilities/window/window.d.ts.map +0 -1
  115. package/src/capabilities/ollama/index.ts +0 -7
  116. package/src/capabilities/updater/index.ts +0 -7
  117. package/src/capabilities/updater/updater.ts +0 -120
  118. package/src/capabilities/window/index.ts +0 -7
  119. package/src/capabilities/window/window.ts +0 -72
  120. /package/dist/types/src/capabilities/{ollama/ollama.d.ts → ollama.d.ts} +0 -0
  121. /package/src/capabilities/{ollama/ollama.ts → ollama.ts} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/plugin-native",
3
- "version": "0.8.4-main.c85a9c8dae",
3
+ "version": "0.8.4-main.d9fc60f731",
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",
@@ -8,50 +8,98 @@
8
8
  "type": "git",
9
9
  "url": "https://github.com/dxos/dxos"
10
10
  },
11
- "license": "MIT",
11
+ "license": "FSL-1.1-Apache-2.0",
12
12
  "author": "DXOS.org",
13
13
  "sideEffects": true,
14
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
+ },
15
47
  "exports": {
16
48
  ".": {
17
49
  "source": "./src/index.ts",
18
50
  "types": "./dist/types/src/index.d.ts",
19
- "browser": "./dist/lib/browser/index.mjs"
51
+ "default": "./dist/lib/neutral/index.mjs"
52
+ },
53
+ "./assets/PLUGIN.mdl": "./PLUGIN.mdl",
54
+ "./plugin": {
55
+ "source": "./src/plugin.ts",
56
+ "types": "./dist/types/src/plugin.d.ts",
57
+ "default": "./dist/lib/neutral/plugin.mjs"
58
+ },
59
+ "./translations": {
60
+ "source": "./src/translations.ts",
61
+ "types": "./dist/types/src/translations.d.ts",
62
+ "default": "./dist/lib/neutral/translations.mjs"
20
63
  }
21
64
  },
22
65
  "types": "dist/types/src/index.d.ts",
23
- "typesVersions": {
24
- "*": {}
25
- },
26
66
  "files": [
27
67
  "dist",
28
- "src"
68
+ "src",
69
+ "PLUGIN.mdl"
29
70
  ],
30
71
  "dependencies": {
31
- "@effect/platform": "0.94.4",
32
- "@tauri-apps/api": "^2.8.0",
33
- "@tauri-apps/plugin-os": "^2.3.1",
34
- "@tauri-apps/plugin-process": "^2.3.0",
35
- "@tauri-apps/plugin-shell": "^2.3.1",
36
- "@tauri-apps/plugin-updater": "^2.9.0",
37
- "effect": "3.19.16",
38
- "@dxos/ai": "0.8.4-main.c85a9c8dae",
39
- "@dxos/app-toolkit": "0.8.4-main.c85a9c8dae",
40
- "@dxos/app-framework": "0.8.4-main.c85a9c8dae",
41
- "@dxos/log": "0.8.4-main.c85a9c8dae",
42
- "@dxos/plugin-assistant": "0.8.4-main.c85a9c8dae",
43
- "@dxos/operation": "0.8.4-main.c85a9c8dae",
44
- "@dxos/plugin-help": "0.8.4-main.c85a9c8dae",
45
- "@dxos/util": "0.8.4-main.c85a9c8dae",
46
- "@dxos/plugin-navtree": "0.8.4-main.c85a9c8dae"
72
+ "@effect-atom/atom-react": "^0.5.0",
73
+ "@effect/platform": "0.96.1",
74
+ "@tauri-apps/api": "^2.11.0",
75
+ "@tauri-apps/plugin-os": "^2.3.2",
76
+ "@tauri-apps/plugin-process": "^2.3.1",
77
+ "@tauri-apps/plugin-shell": "^2.3.5",
78
+ "@tauri-apps/plugin-updater": "^2.10.1",
79
+ "date-fns": "^3.6.0",
80
+ "@dxos/ai": "0.8.4-main.d9fc60f731",
81
+ "@dxos/app-toolkit": "0.8.4-main.d9fc60f731",
82
+ "@dxos/app-framework": "0.8.4-main.d9fc60f731",
83
+ "@dxos/effect": "0.8.4-main.d9fc60f731",
84
+ "@dxos/log": "0.8.4-main.d9fc60f731",
85
+ "@dxos/keys": "0.8.4-main.d9fc60f731",
86
+ "@dxos/plugin-assistant": "0.8.4-main.d9fc60f731",
87
+ "@dxos/util": "0.8.4-main.d9fc60f731"
47
88
  },
48
89
  "devDependencies": {
49
- "vite": "^7.1.11",
50
- "@dxos/react-ui": "0.8.4-main.c85a9c8dae"
90
+ "@types/react": "~19.2.7",
91
+ "effect": "3.21.2",
92
+ "react": "~19.2.3",
93
+ "vite": "^8.0.14",
94
+ "@dxos/plugin-testing": "0.8.4-main.d9fc60f731",
95
+ "@dxos/react-ui-form": "0.8.4-main.d9fc60f731",
96
+ "@dxos/react-ui": "0.8.4-main.d9fc60f731"
51
97
  },
52
98
  "peerDependencies": {
53
- "effect": "3.19.16",
54
- "@dxos/react-ui": "0.8.4-main.c85a9c8dae"
99
+ "effect": "3.21.2",
100
+ "react": "~19.2.3",
101
+ "@dxos/react-ui": "0.8.4-main.d9fc60f731",
102
+ "@dxos/react-ui-form": "0.8.4-main.d9fc60f731"
55
103
  },
56
104
  "publishConfig": {
57
105
  "access": "public"
@@ -6,26 +6,36 @@ import { ActivationEvents, Plugin } from '@dxos/app-framework';
6
6
  import { AppPlugin } from '@dxos/app-toolkit';
7
7
  import { AssistantEvents } from '@dxos/plugin-assistant';
8
8
 
9
- import { Ollama, Updater, Window } from './capabilities';
10
- import { meta } from './meta';
11
- import { translations } from './translations';
9
+ import { NativeSettings, Ollama, ReactSurface, SpotlightListener, Updater } from '#capabilities';
10
+ import { meta } from '#meta';
11
+ import { translations } from '#translations';
12
+
13
+ // eslint-disable-next-line import/no-relative-packages
14
+ import pluginSpec from '../PLUGIN.mdl?raw';
12
15
 
13
16
  export const NativePlugin = Plugin.define(meta).pipe(
17
+ AppPlugin.addSettingsModule({ activate: NativeSettings }),
18
+ AppPlugin.addSurfaceModule({ activate: ReactSurface }),
14
19
  AppPlugin.addTranslationsModule({ translations }),
15
20
  Plugin.addModule({
16
- id: 'updater',
17
- activatesOn: ActivationEvents.OperationInvokerReady,
18
- activate: Updater,
21
+ id: 'spotlight-listener',
22
+ activatesOn: ActivationEvents.ProcessManagerReady,
23
+ activate: SpotlightListener,
19
24
  }),
20
25
  Plugin.addModule({
21
- id: 'window',
22
- activatesOn: ActivationEvents.OperationInvokerReady,
23
- activate: Window,
26
+ id: 'updater',
27
+ activatesOn: ActivationEvents.ProcessManagerReady,
28
+ activate: Updater,
24
29
  }),
25
30
  Plugin.addModule({
26
31
  id: 'ollama',
27
32
  activatesOn: AssistantEvents.SetupAiServiceProviders,
28
33
  activate: Ollama,
29
34
  }),
35
+ AppPlugin.addPluginAssetModule({
36
+ asset: { pluginId: meta.id, path: 'PLUGIN.mdl', content: pluginSpec, mimeType: 'application/x-mdl' },
37
+ }),
30
38
  Plugin.make,
31
39
  );
40
+
41
+ export default NativePlugin;
@@ -2,6 +2,10 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- export * from './ollama';
6
- export * from './updater';
7
- export * from './window';
5
+ import { Capability } from '@dxos/app-framework';
6
+
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'));
@@ -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: 'pluginSettings',
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
+ );
@@ -0,0 +1,194 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { Atom } from '@effect-atom/atom-react';
6
+ import { type } from '@tauri-apps/plugin-os';
7
+ import { relaunch } from '@tauri-apps/plugin-process';
8
+ import * as Updater from '@tauri-apps/plugin-updater';
9
+ import * as Duration from 'effect/Duration';
10
+ import * as Effect from 'effect/Effect';
11
+ import * as Fiber from 'effect/Fiber';
12
+ import * as Match from 'effect/Match';
13
+ import * as Schedule from 'effect/Schedule';
14
+
15
+ import { Capabilities, Capability } from '@dxos/app-framework';
16
+ import { LayoutOperation } from '@dxos/app-toolkit';
17
+ import { log } from '@dxos/log';
18
+
19
+ import { meta } from '#meta';
20
+ import { NativeCapabilities, type Update } from '#types';
21
+
22
+ import { TAURI_LOCALHOST_PORT } from '../constants';
23
+
24
+ const SUPPORTS_OTA = ['linux', 'macos', 'windows'];
25
+
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) };
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);
69
+
70
+ const statusAtom = Atom.make<Update.Status>(enabled ? { kind: 'idle' } : { kind: 'unsupported' }).pipe(
71
+ Atom.keepAlive,
72
+ );
73
+
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
+ }
106
+ let downloaded = 0;
107
+ let contentLength = 0;
108
+ registry.set(statusAtom, { kind: 'downloading', downloaded: 0, contentLength: 0 });
109
+ const onEvent = Match.type<Updater.DownloadEvent>().pipe(
110
+ Match.when({ event: 'Started' }, (event) => {
111
+ contentLength = event.data.contentLength ?? 0;
112
+ registry.set(statusAtom, { kind: 'downloading', downloaded, contentLength });
113
+ }),
114
+ Match.when({ event: 'Progress' }, (event) => {
115
+ downloaded += event.data.chunkLength;
116
+ registry.set(statusAtom, { kind: 'downloading', downloaded, contentLength });
117
+ }),
118
+ Match.when({ event: 'Finished' }, () => {
119
+ log.info('download completed');
120
+ }),
121
+ Match.exhaustive,
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
+ };
131
+
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];
156
+ }
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
+ );
@@ -0,0 +1,132 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { useAtomValue } from '@effect-atom/atom-react';
6
+ import { formatDistanceToNow } from 'date-fns/formatDistanceToNow';
7
+ import * as Match from 'effect/Match';
8
+ import React, { type ReactNode, useState } from 'react';
9
+
10
+ import { useCapability } from '@dxos/app-framework/ui';
11
+ import { type AppSurface } from '@dxos/app-toolkit/ui';
12
+ import { Button, useTranslation } from '@dxos/react-ui';
13
+ import { Settings as SettingsForm } from '@dxos/react-ui-form';
14
+
15
+ import { meta } from '#meta';
16
+ import { NativeCapabilities, type Settings, type Update } from '#types';
17
+
18
+ export type NativeSettingsProps = AppSurface.SettingsArticleProps<Settings.Settings>;
19
+
20
+ type Pending = null | 'check' | 'install' | 'relaunch';
21
+
22
+ type UpdateActions = {
23
+ onCheck: () => Promise<void>;
24
+ onInstall: () => Promise<void>;
25
+ onRelaunch: () => Promise<void>;
26
+ };
27
+
28
+ type UpdateRow = { description: string; button: ReactNode };
29
+
30
+ export const NativeSettings = (_props: NativeSettingsProps) => {
31
+ const { t } = useTranslation(meta.id);
32
+ const manager = useCapability(NativeCapabilities.UpdateManager);
33
+ const status = useAtomValue(manager.status);
34
+
35
+ // UI-level pending flag. The status atom can flip between `checking` and `up-to-date` faster
36
+ // than the user can perceive, so we also gate the button on the click handler's lifetime to
37
+ // guarantee no duplicate triggers and a visible busy state.
38
+ const [pending, setPending] = useState<Pending>(null);
39
+
40
+ const runAction = (kind: Exclude<Pending, null>, action: () => Promise<void>) => async () => {
41
+ setPending(kind);
42
+ try {
43
+ await action();
44
+ } finally {
45
+ setPending(null);
46
+ }
47
+ };
48
+
49
+ const { description, button } = renderUpdateRow(status, pending, t, {
50
+ onCheck: runAction('check', manager.check),
51
+ onInstall: runAction('install', manager.install),
52
+ onRelaunch: runAction('relaunch', manager.relaunch),
53
+ });
54
+
55
+ return (
56
+ <SettingsForm.Viewport>
57
+ <SettingsForm.Section title={t('settings.title')}>
58
+ <SettingsForm.Item title={t('settings.updates.label')} description={description}>
59
+ {button}
60
+ </SettingsForm.Item>
61
+ </SettingsForm.Section>
62
+ </SettingsForm.Viewport>
63
+ );
64
+ };
65
+
66
+ const renderUpdateRow = (
67
+ status: Update.Status,
68
+ pending: Pending,
69
+ t: (key: string, options?: Record<string, unknown>) => string,
70
+ { onCheck, onInstall, onRelaunch }: UpdateActions,
71
+ ): UpdateRow => {
72
+ const isChecking = pending === 'check' || status.kind === 'checking';
73
+ const isInstalling = pending === 'install' || status.kind === 'downloading';
74
+
75
+ const checkButton = (disabled = false) => (
76
+ <Button disabled={disabled || isChecking || isInstalling || pending === 'relaunch'} onClick={() => void onCheck()}>
77
+ {isChecking ? t('settings.updates.checking.label') : t('settings.updates.check.label')}
78
+ </Button>
79
+ );
80
+
81
+ return Match.value(status).pipe(
82
+ Match.withReturnType<UpdateRow>(),
83
+ Match.when({ kind: 'unsupported' }, () => ({
84
+ description: t('settings.updates.unsupported.message'),
85
+ button: checkButton(true),
86
+ })),
87
+ Match.when({ kind: 'idle' }, () => ({
88
+ description: isChecking ? t('settings.updates.checking.message') : t('settings.updates.idle.message'),
89
+ button: checkButton(),
90
+ })),
91
+ Match.when({ kind: 'checking' }, () => ({
92
+ description: t('settings.updates.checking.message'),
93
+ button: checkButton(),
94
+ })),
95
+ Match.when({ kind: 'up-to-date' }, (s) => ({
96
+ description: isChecking
97
+ ? t('settings.updates.checking.message')
98
+ : t('settings.updates.up-to-date.message', {
99
+ checkedAt: formatDistanceToNow(new Date(s.checkedAt), { addSuffix: true }),
100
+ }),
101
+ button: checkButton(),
102
+ })),
103
+ Match.when({ kind: 'available' }, (s) => ({
104
+ description: t('settings.updates.available.message', { version: s.version }),
105
+ button: (
106
+ <Button variant='primary' disabled={isInstalling} onClick={() => void onInstall()}>
107
+ {isInstalling ? t('settings.updates.downloading.label') : t('settings.updates.update-now.label')}
108
+ </Button>
109
+ ),
110
+ })),
111
+ Match.when({ kind: 'downloading' }, (s) => {
112
+ const percent = s.contentLength > 0 ? Math.round((s.downloaded / s.contentLength) * 100) : 0;
113
+ return {
114
+ description: t('settings.updates.downloading.message', { percent }),
115
+ button: <Button disabled>{t('settings.updates.downloading.label')}</Button>,
116
+ };
117
+ }),
118
+ Match.when({ kind: 'ready' }, () => ({
119
+ description: t('settings.updates.ready.message'),
120
+ button: (
121
+ <Button variant='primary' disabled={pending === 'relaunch'} onClick={() => void onRelaunch()}>
122
+ {t('settings.updates.relaunch.label')}
123
+ </Button>
124
+ ),
125
+ })),
126
+ Match.when({ kind: 'failed' }, (s) => ({
127
+ description: t('settings.updates.failed.message', { error: s.error }),
128
+ button: checkButton(),
129
+ })),
130
+ Match.exhaustive,
131
+ );
132
+ };
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export { NativeSettings as default } from './NativeSettings';
@@ -0,0 +1,7 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { type ComponentType, lazy } from 'react';
6
+
7
+ export const NativeSettings: ComponentType<any> = lazy(() => import('./NativeSettings'));
@@ -0,0 +1,7 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ /// Stable port for the Tauri localhost asset server in production builds.
6
+ /// Must match LOCALHOST_PORT in src-tauri/src/lib.rs.
7
+ export const TAURI_LOCALHOST_PORT = '26777';
package/src/index.ts CHANGED
@@ -2,6 +2,4 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- export * from './capabilities';
6
5
  export * from './meta';
7
- export * from './NativePlugin';