@dxos/plugin-native 0.8.4-main.7996785055 → 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 (110) 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-C7OU2UF6.mjs → neutral/chunk-X3A57PRH.mjs} +8 -3
  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-QOB3EVAJ.mjs → neutral/ollama-BHWFTROM.mjs} +8 -21
  21. package/dist/lib/{browser/ollama-QOB3EVAJ.mjs.map → neutral/ollama-BHWFTROM.mjs.map} +3 -3
  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/{browser/spotlight-listener-YWC4SLQK.mjs → neutral/spotlight-listener-K7S6LGL3.mjs} +8 -21
  29. package/dist/lib/{browser/spotlight-listener-YWC4SLQK.mjs.map → neutral/spotlight-listener-K7S6LGL3.mjs.map} +3 -3
  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 +1 -0
  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.map +1 -0
  41. package/dist/types/src/capabilities/react-surface.d.ts +5 -0
  42. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -0
  43. package/dist/types/src/capabilities/settings.d.ts +7 -0
  44. package/dist/types/src/capabilities/settings.d.ts.map +1 -0
  45. package/dist/types/src/capabilities/spotlight-listener.d.ts.map +1 -0
  46. package/dist/types/src/capabilities/updater.d.ts +6 -0
  47. package/dist/types/src/capabilities/updater.d.ts.map +1 -0
  48. package/dist/types/src/components/NativeSettings/NativeSettings.d.ts +6 -0
  49. package/dist/types/src/components/NativeSettings/NativeSettings.d.ts.map +1 -0
  50. package/dist/types/src/components/NativeSettings/index.d.ts +2 -0
  51. package/dist/types/src/components/NativeSettings/index.d.ts.map +1 -0
  52. package/dist/types/src/components/index.d.ts +3 -0
  53. package/dist/types/src/components/index.d.ts.map +1 -0
  54. package/dist/types/src/index.d.ts +0 -2
  55. package/dist/types/src/index.d.ts.map +1 -1
  56. package/dist/types/src/meta.d.ts.map +1 -1
  57. package/dist/types/src/plugin.d.ts +3 -0
  58. package/dist/types/src/plugin.d.ts.map +1 -0
  59. package/dist/types/src/translations.d.ts +20 -6
  60. package/dist/types/src/translations.d.ts.map +1 -1
  61. package/dist/types/src/types/NativeCapabilities.d.ts +5 -0
  62. package/dist/types/src/types/NativeCapabilities.d.ts.map +1 -0
  63. package/dist/types/src/types/Settings.d.ts +5 -0
  64. package/dist/types/src/types/Settings.d.ts.map +1 -0
  65. package/dist/types/src/types/Update.d.ts +30 -0
  66. package/dist/types/src/types/Update.d.ts.map +1 -0
  67. package/dist/types/src/types/index.d.ts +4 -0
  68. package/dist/types/src/types/index.d.ts.map +1 -0
  69. package/dist/types/tsconfig.tsbuildinfo +1 -1
  70. package/package.json +71 -24
  71. package/src/NativePlugin.tsx +9 -5
  72. package/src/capabilities/index.ts +7 -3
  73. package/src/capabilities/react-surface.tsx +29 -0
  74. package/src/capabilities/settings.ts +31 -0
  75. package/src/capabilities/updater.ts +194 -0
  76. package/src/components/NativeSettings/NativeSettings.tsx +132 -0
  77. package/src/components/NativeSettings/index.ts +5 -0
  78. package/src/components/index.ts +7 -0
  79. package/src/index.ts +0 -2
  80. package/src/meta.ts +5 -1
  81. package/src/plugin.ts +9 -0
  82. package/src/translations.ts +20 -5
  83. package/src/types/NativeCapabilities.ts +16 -0
  84. package/src/types/Settings.ts +11 -0
  85. package/src/types/Update.ts +24 -0
  86. package/src/types/index.ts +7 -0
  87. package/dist/lib/browser/chunk-C7OU2UF6.mjs.map +0 -7
  88. package/dist/lib/browser/index.mjs +0 -59
  89. package/dist/lib/browser/index.mjs.map +0 -7
  90. package/dist/lib/browser/meta.json +0 -1
  91. package/dist/lib/browser/updater-FDID3Y6C.mjs +0 -199
  92. package/dist/lib/browser/updater-FDID3Y6C.mjs.map +0 -7
  93. package/dist/types/src/capabilities/ollama/index.d.ts +0 -3
  94. package/dist/types/src/capabilities/ollama/index.d.ts.map +0 -1
  95. package/dist/types/src/capabilities/ollama/ollama.d.ts.map +0 -1
  96. package/dist/types/src/capabilities/spotlight-listener/index.d.ts +0 -3
  97. package/dist/types/src/capabilities/spotlight-listener/index.d.ts.map +0 -1
  98. package/dist/types/src/capabilities/spotlight-listener/spotlight-listener.d.ts.map +0 -1
  99. package/dist/types/src/capabilities/updater/index.d.ts +0 -3
  100. package/dist/types/src/capabilities/updater/index.d.ts.map +0 -1
  101. package/dist/types/src/capabilities/updater/updater.d.ts +0 -5
  102. package/dist/types/src/capabilities/updater/updater.d.ts.map +0 -1
  103. package/src/capabilities/ollama/index.ts +0 -7
  104. package/src/capabilities/spotlight-listener/index.ts +0 -7
  105. package/src/capabilities/updater/index.ts +0 -7
  106. package/src/capabilities/updater/updater.ts +0 -128
  107. /package/dist/types/src/capabilities/{ollama/ollama.d.ts → ollama.d.ts} +0 -0
  108. /package/dist/types/src/capabilities/{spotlight-listener/spotlight-listener.d.ts → spotlight-listener.d.ts} +0 -0
  109. /package/src/capabilities/{ollama/ollama.ts → ollama.ts} +0 -0
  110. /package/src/capabilities/{spotlight-listener/spotlight-listener.ts → spotlight-listener.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.7996785055",
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",
@@ -8,50 +8,97 @@
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
+ "./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"
20
62
  }
21
63
  },
22
64
  "types": "dist/types/src/index.d.ts",
23
- "typesVersions": {
24
- "*": {}
25
- },
26
65
  "files": [
27
66
  "dist",
28
67
  "src"
29
68
  ],
30
69
  "dependencies": {
70
+ "@effect-atom/atom-react": "^0.5.0",
31
71
  "@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.20.0",
38
- "@dxos/app-toolkit": "0.8.4-main.7996785055",
39
- "@dxos/ai": "0.8.4-main.7996785055",
40
- "@dxos/log": "0.8.4-main.7996785055",
41
- "@dxos/plugin-assistant": "0.8.4-main.7996785055",
42
- "@dxos/operation": "0.8.4-main.7996785055",
43
- "@dxos/plugin-help": "0.8.4-main.7996785055",
44
- "@dxos/plugin-navtree": "0.8.4-main.7996785055",
45
- "@dxos/app-framework": "0.8.4-main.7996785055",
46
- "@dxos/util": "0.8.4-main.7996785055"
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"
47
87
  },
48
88
  "devDependencies": {
49
- "vite": "^7.1.11",
50
- "@dxos/react-ui": "0.8.4-main.7996785055"
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"
51
96
  },
52
97
  "peerDependencies": {
53
98
  "effect": "3.20.0",
54
- "@dxos/react-ui": "0.8.4-main.7996785055"
99
+ "react": "~19.2.3",
100
+ "@dxos/react-ui": "0.8.4-main.8baae0fced",
101
+ "@dxos/react-ui-form": "0.8.4-main.8baae0fced"
55
102
  },
56
103
  "publishConfig": {
57
104
  "access": "public"
@@ -6,20 +6,22 @@ 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, SpotlightListener, Updater } 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
12
 
13
13
  export const NativePlugin = Plugin.define(meta).pipe(
14
+ AppPlugin.addSettingsModule({ activate: NativeSettings }),
15
+ AppPlugin.addSurfaceModule({ activate: ReactSurface }),
14
16
  AppPlugin.addTranslationsModule({ translations }),
15
17
  Plugin.addModule({
16
18
  id: 'spotlight-listener',
17
- activatesOn: ActivationEvents.OperationInvokerReady,
19
+ activatesOn: ActivationEvents.ProcessManagerReady,
18
20
  activate: SpotlightListener,
19
21
  }),
20
22
  Plugin.addModule({
21
23
  id: 'updater',
22
- activatesOn: ActivationEvents.OperationInvokerReady,
24
+ activatesOn: ActivationEvents.ProcessManagerReady,
23
25
  activate: Updater,
24
26
  }),
25
27
  Plugin.addModule({
@@ -29,3 +31,5 @@ export const NativePlugin = Plugin.define(meta).pipe(
29
31
  }),
30
32
  Plugin.make,
31
33
  );
34
+
35
+ export default NativePlugin;
@@ -2,6 +2,10 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- export * from './ollama';
6
- export * from './spotlight-listener';
7
- export * from './updater';
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: '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,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'));
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';
package/src/meta.ts CHANGED
@@ -5,11 +5,15 @@
5
5
  import { type Plugin } from '@dxos/app-framework';
6
6
  import { trim } from '@dxos/util';
7
7
 
8
+ // TODO(wittjosiah): Rename plugin (package + id) from `native` to `app` to match the user-facing name.
8
9
  export const meta: Plugin.Meta = {
9
10
  id: 'org.dxos.plugin.native',
10
- name: 'Native',
11
+ name: 'App',
12
+ author: 'DXOS',
11
13
  description: trim`
12
14
  Native platform integration providing desktop-specific features and system-level capabilities.
13
15
  Access native file dialogs, notifications, and OS integrations.
14
16
  `,
17
+ icon: 'ph--app-window--regular',
18
+ tags: ['system'],
15
19
  };
package/src/plugin.ts ADDED
@@ -0,0 +1,9 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import { Plugin } from '@dxos/app-framework';
6
+
7
+ import { meta } from './meta';
8
+
9
+ export const NativePlugin = Plugin.lazy(meta, () => import('#plugin'));
@@ -4,16 +4,31 @@
4
4
 
5
5
  import { type Resource } from '@dxos/react-ui';
6
6
 
7
- import { meta } from './meta';
7
+ import { meta } from '#meta';
8
8
 
9
9
  export const translations = [
10
10
  {
11
11
  'en-US': {
12
12
  [meta.id]: {
13
- 'update ready label': 'Ready to update',
14
- 'update ready description': 'A new version of Composer is available.',
15
- 'update label': 'Update',
16
- 'update alt': 'Relaunch the app to update',
13
+ 'update-ready.label': 'Ready to update',
14
+ 'update-ready.description': 'A new version of Composer is available.',
15
+ 'update.label': 'Update',
16
+ 'update.alt': 'Relaunch the app to update',
17
+ 'settings.title': 'App',
18
+ 'settings.updates.label': 'Updates',
19
+ 'settings.updates.check.label': 'Check for updates',
20
+ 'settings.updates.checking.label': 'Checking…',
21
+ 'settings.updates.update-now.label': 'Update now',
22
+ 'settings.updates.downloading.label': 'Downloading…',
23
+ 'settings.updates.relaunch.label': 'Restart to install',
24
+ 'settings.updates.idle.message': 'Check now for a newer version of Composer.',
25
+ 'settings.updates.checking.message': 'Checking for updates…',
26
+ 'settings.updates.up-to-date.message': 'You are running the latest version of Composer. Checked {{checkedAt}}.',
27
+ 'settings.updates.available.message': 'Version {{version}} is available.',
28
+ 'settings.updates.downloading.message': 'Downloading… {{percent}}%',
29
+ 'settings.updates.ready.message': 'Update ready. Restart Composer to apply.',
30
+ 'settings.updates.failed.message': 'Update failed: {{error}}',
31
+ 'settings.updates.unsupported.message': 'Updates are not available on this platform.',
17
32
  },
18
33
  },
19
34
  },