@dxos/plugin-native 0.8.4-main.3fbcb4aa9b → 0.8.4-main.4668b7de9b

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 (82) 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 +22 -4
  5. package/dist/lib/neutral/NativePlugin.mjs.map +4 -4
  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 +9 -3
  9. package/dist/lib/neutral/capabilities/index.mjs.map +3 -3
  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 +2 -1
  17. package/dist/lib/neutral/meta.json +1 -1
  18. package/dist/lib/neutral/meta.mjs +2 -1
  19. package/dist/lib/neutral/{ollama-CYD5M6XB.mjs → ollama-BHWFTROM.mjs} +3 -1
  20. package/dist/lib/neutral/{ollama-CYD5M6XB.mjs.map → ollama-BHWFTROM.mjs.map} +1 -1
  21. package/dist/lib/neutral/plugin.mjs +2 -1
  22. package/dist/lib/neutral/plugin.mjs.map +1 -1
  23. package/dist/lib/neutral/react-surface-ETNGHN4X.mjs +27 -0
  24. package/dist/lib/neutral/react-surface-ETNGHN4X.mjs.map +7 -0
  25. package/dist/lib/neutral/settings-HF6TQNU4.mjs +28 -0
  26. package/dist/lib/neutral/settings-HF6TQNU4.mjs.map +7 -0
  27. package/dist/lib/neutral/{spotlight-listener-TRCJXLOQ.mjs → spotlight-listener-K7S6LGL3.mjs} +3 -1
  28. package/dist/lib/neutral/{spotlight-listener-TRCJXLOQ.mjs.map → spotlight-listener-K7S6LGL3.mjs.map} +1 -1
  29. package/dist/lib/neutral/translations.mjs +18 -1
  30. package/dist/lib/neutral/translations.mjs.map +2 -2
  31. package/dist/lib/neutral/types/index.mjs +31 -0
  32. package/dist/lib/neutral/types/index.mjs.map +7 -0
  33. package/dist/lib/neutral/updater-JEBXWSSK.mjs +244 -0
  34. package/dist/lib/neutral/updater-JEBXWSSK.mjs.map +7 -0
  35. package/dist/types/src/NativePlugin.d.ts.map +1 -1
  36. package/dist/types/src/capabilities/index.d.ts +3 -1
  37. package/dist/types/src/capabilities/index.d.ts.map +1 -1
  38. package/dist/types/src/capabilities/react-surface.d.ts +5 -0
  39. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -0
  40. package/dist/types/src/capabilities/settings.d.ts +7 -0
  41. package/dist/types/src/capabilities/settings.d.ts.map +1 -0
  42. package/dist/types/src/capabilities/updater.d.ts +2 -1
  43. package/dist/types/src/capabilities/updater.d.ts.map +1 -1
  44. package/dist/types/src/components/NativeSettings/NativeSettings.d.ts +6 -0
  45. package/dist/types/src/components/NativeSettings/NativeSettings.d.ts.map +1 -0
  46. package/dist/types/src/components/NativeSettings/index.d.ts +2 -0
  47. package/dist/types/src/components/NativeSettings/index.d.ts.map +1 -0
  48. package/dist/types/src/components/index.d.ts +3 -0
  49. package/dist/types/src/components/index.d.ts.map +1 -0
  50. package/dist/types/src/meta.d.ts +1 -1
  51. package/dist/types/src/meta.d.ts.map +1 -1
  52. package/dist/types/src/translations.d.ts +15 -0
  53. package/dist/types/src/translations.d.ts.map +1 -1
  54. package/dist/types/src/types/NativeCapabilities.d.ts +5 -0
  55. package/dist/types/src/types/NativeCapabilities.d.ts.map +1 -0
  56. package/dist/types/src/types/Settings.d.ts +5 -0
  57. package/dist/types/src/types/Settings.d.ts.map +1 -0
  58. package/dist/types/src/types/Update.d.ts +30 -0
  59. package/dist/types/src/types/Update.d.ts.map +1 -0
  60. package/dist/types/src/types/index.d.ts +4 -0
  61. package/dist/types/src/types/index.d.ts.map +1 -0
  62. package/dist/types/tsconfig.tsbuildinfo +1 -1
  63. package/package.json +40 -22
  64. package/src/NativePlugin.tsx +11 -3
  65. package/src/capabilities/index.ts +2 -0
  66. package/src/capabilities/react-surface.tsx +29 -0
  67. package/src/capabilities/settings.ts +31 -0
  68. package/src/capabilities/updater.ts +136 -71
  69. package/src/components/NativeSettings/NativeSettings.tsx +132 -0
  70. package/src/components/NativeSettings/index.ts +5 -0
  71. package/src/components/index.ts +7 -0
  72. package/src/meta.ts +23 -7
  73. package/src/translations.ts +15 -0
  74. package/src/types/NativeCapabilities.ts +16 -0
  75. package/src/types/Settings.ts +11 -0
  76. package/src/types/Update.ts +24 -0
  77. package/src/types/index.ts +7 -0
  78. package/src/vite-env.d.ts +10 -0
  79. package/dist/lib/neutral/chunk-254JXXJC.mjs +0 -18
  80. package/dist/lib/neutral/chunk-254JXXJC.mjs.map +0 -7
  81. package/dist/lib/neutral/updater-ZAJJC24D.mjs +0 -141
  82. package/dist/lib/neutral/updater-ZAJJC24D.mjs.map +0 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/plugin-native",
3
- "version": "0.8.4-main.3fbcb4aa9b",
3
+ "version": "0.8.4-main.4668b7de9b",
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,7 +8,7 @@
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",
@@ -18,6 +18,11 @@
18
18
  "types": "./dist/types/src/capabilities/index.d.ts",
19
19
  "default": "./dist/lib/neutral/capabilities/index.mjs"
20
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
+ },
21
26
  "#meta": {
22
27
  "source": "./src/meta.ts",
23
28
  "types": "./dist/types/src/meta.d.ts",
@@ -32,6 +37,11 @@
32
37
  "source": "./src/translations.ts",
33
38
  "types": "./dist/types/src/translations.d.ts",
34
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"
35
45
  }
36
46
  },
37
47
  "exports": {
@@ -40,6 +50,7 @@
40
50
  "types": "./dist/types/src/index.d.ts",
41
51
  "default": "./dist/lib/neutral/index.mjs"
42
52
  },
53
+ "./assets/PLUGIN.mdl": "./PLUGIN.mdl",
43
54
  "./plugin": {
44
55
  "source": "./src/plugin.ts",
45
56
  "types": "./dist/types/src/plugin.d.ts",
@@ -54,34 +65,41 @@
54
65
  "types": "dist/types/src/index.d.ts",
55
66
  "files": [
56
67
  "dist",
57
- "src"
68
+ "src",
69
+ "PLUGIN.mdl"
58
70
  ],
59
71
  "dependencies": {
72
+ "@effect-atom/atom-react": "^0.5.0",
60
73
  "@effect/platform": "0.94.4",
61
- "@tauri-apps/api": "^2.8.0",
62
- "@tauri-apps/plugin-os": "^2.3.1",
63
- "@tauri-apps/plugin-process": "^2.3.0",
64
- "@tauri-apps/plugin-shell": "^2.3.1",
65
- "@tauri-apps/plugin-updater": "^2.9.0",
66
- "effect": "3.20.0",
67
- "@dxos/app-toolkit": "0.8.4-main.3fbcb4aa9b",
68
- "@dxos/ai": "0.8.4-main.3fbcb4aa9b",
69
- "@dxos/operation": "0.8.4-main.3fbcb4aa9b",
70
- "@dxos/plugin-assistant": "0.8.4-main.3fbcb4aa9b",
71
- "@dxos/log": "0.8.4-main.3fbcb4aa9b",
72
- "@dxos/plugin-help": "0.8.4-main.3fbcb4aa9b",
73
- "@dxos/app-framework": "0.8.4-main.3fbcb4aa9b",
74
- "@dxos/plugin-navtree": "0.8.4-main.3fbcb4aa9b",
75
- "@dxos/util": "0.8.4-main.3fbcb4aa9b"
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.4668b7de9b",
81
+ "@dxos/app-framework": "0.8.4-main.4668b7de9b",
82
+ "@dxos/app-toolkit": "0.8.4-main.4668b7de9b",
83
+ "@dxos/log": "0.8.4-main.4668b7de9b",
84
+ "@dxos/effect": "0.8.4-main.4668b7de9b",
85
+ "@dxos/plugin-assistant": "0.8.4-main.4668b7de9b",
86
+ "@dxos/util": "0.8.4-main.4668b7de9b",
87
+ "@dxos/keys": "0.8.4-main.4668b7de9b"
76
88
  },
77
89
  "devDependencies": {
78
- "vite": "^8.0.10",
79
- "@dxos/plugin-testing": "0.8.4-main.3fbcb4aa9b",
80
- "@dxos/react-ui": "0.8.4-main.3fbcb4aa9b"
90
+ "@types/react": "~19.2.7",
91
+ "effect": "3.20.0",
92
+ "react": "~19.2.3",
93
+ "vite": "^8.0.14",
94
+ "@dxos/plugin-testing": "0.8.4-main.4668b7de9b",
95
+ "@dxos/react-ui": "0.8.4-main.4668b7de9b",
96
+ "@dxos/react-ui-form": "0.8.4-main.4668b7de9b"
81
97
  },
82
98
  "peerDependencies": {
83
99
  "effect": "3.20.0",
84
- "@dxos/react-ui": "0.8.4-main.3fbcb4aa9b"
100
+ "react": "~19.2.3",
101
+ "@dxos/react-ui": "0.8.4-main.4668b7de9b",
102
+ "@dxos/react-ui-form": "0.8.4-main.4668b7de9b"
85
103
  },
86
104
  "publishConfig": {
87
105
  "access": "public"
@@ -6,20 +6,25 @@ 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';
9
+ import { NativeSettings, Ollama, ReactSurface, SpotlightListener, Updater } from '#capabilities';
10
10
  import { meta } from '#meta';
11
11
  import { translations } from '#translations';
12
12
 
13
+ // eslint-disable-next-line import/no-relative-packages
14
+ import pluginSpec from '../PLUGIN.mdl?raw';
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
21
  id: 'spotlight-listener',
17
- activatesOn: ActivationEvents.OperationInvokerReady,
22
+ activatesOn: ActivationEvents.ProcessManagerReady,
18
23
  activate: SpotlightListener,
19
24
  }),
20
25
  Plugin.addModule({
21
26
  id: 'updater',
22
- activatesOn: ActivationEvents.OperationInvokerReady,
27
+ activatesOn: ActivationEvents.ProcessManagerReady,
23
28
  activate: Updater,
24
29
  }),
25
30
  Plugin.addModule({
@@ -27,6 +32,9 @@ export const NativePlugin = Plugin.define(meta).pipe(
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
  );
32
40
 
@@ -4,6 +4,8 @@
4
4
 
5
5
  import { Capability } from '@dxos/app-framework';
6
6
 
7
+ export const NativeSettings = Capability.lazy('NativeSettings', () => import('./settings'));
7
8
  export const Ollama = Capability.lazy('Ollama', () => import('./ollama'));
9
+ export const ReactSurface = Capability.lazy('ReactSurface', () => import('./react-surface'));
8
10
  export const SpotlightListener = Capability.lazy('SpotlightListener', () => import('./spotlight-listener'));
9
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
+ );
@@ -2,6 +2,7 @@
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
8
  import * as Updater from '@tauri-apps/plugin-updater';
@@ -16,114 +17,178 @@ import { LayoutOperation } from '@dxos/app-toolkit';
16
17
  import { log } from '@dxos/log';
17
18
 
18
19
  import { meta } from '#meta';
20
+ import { NativeCapabilities, type Update } from '#types';
19
21
 
20
22
  import { TAURI_LOCALHOST_PORT } from '../constants';
21
23
 
22
24
  const SUPPORTS_OTA = ['linux', 'macos', 'windows'];
23
25
 
24
- /**
25
- * Safely check for updates with error logging.
26
- */
27
- const check = async (): Promise<Updater.Update | 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
28
  try {
29
- return await Updater.check();
29
+ return { ok: true, update: await Updater.check() };
30
30
  } catch (error) {
31
31
  log.error('failed to check for updates', { error });
32
- return null;
32
+ return { ok: false, error: formatError(error) };
33
33
  }
34
34
  };
35
35
 
36
- /**
37
- * Safely download and install update with error logging.
38
- */
39
- const downloadAndInstall = async (
36
+ /** Safe wrapper around update.downloadAndInstall(). */
37
+ const safeDownloadAndInstall = async (
40
38
  update: Updater.Update,
41
- handleDownload: (event: Updater.DownloadEvent) => void,
42
- ): Promise<boolean> => {
39
+ onEvent: (event: Updater.DownloadEvent) => void,
40
+ ): Promise<{ ok: true } | { ok: false; error: string }> => {
43
41
  try {
44
- await update.downloadAndInstall(handleDownload);
45
- return true;
42
+ await update.downloadAndInstall(onEvent);
43
+ return { ok: true };
46
44
  } catch (error) {
47
45
  log.error('failed to download and install update', { error });
48
- return false;
46
+ return { ok: false, error: formatError(error) };
49
47
  }
50
48
  };
51
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
+
52
61
  export default Capability.makeModule(
53
62
  Effect.fnUntraced(function* () {
54
- // Skip updates if not supported or in dev mode.
55
63
  const platform = type();
56
64
  const isDevServer = window.location.port !== TAURI_LOCALHOST_PORT;
57
- if (!SUPPORTS_OTA.includes(platform) || isDevServer) {
58
- log.info('skipping updater', { platform, port: window.location.port });
59
- return;
60
- }
65
+ const enabled = SUPPORTS_OTA.includes(platform) && !isDevServer;
61
66
 
67
+ const registry = yield* Capability.get(Capabilities.AtomRegistry);
62
68
  const { invoke } = yield* Capability.get(Capabilities.OperationInvoker);
63
69
 
64
- // https://tauri.app/plugin/updater/#checking-for-updates
65
- // Returns true to keep checking, false to stop.
66
- const action = Effect.gen(function* () {
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' });
67
81
  log.info('checking for updates');
68
- const update = yield* Effect.promise(() => check());
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;
69
103
  if (!update) {
70
- log.info('no update available');
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' });
71
126
  return true;
72
127
  }
73
- log.info('update check complete', {
74
- available: update.available,
75
- currentVersion: update.currentVersion,
76
- version: update.version,
77
- date: update.date,
78
- body: update.body,
79
- });
80
- if (update.available) {
81
- log.info('update available, starting download', { version: update.version });
82
- let downloaded = 0;
83
- let contentLength = 0;
84
-
85
- const handleDownload = Match.type<Updater.DownloadEvent>().pipe(
86
- Match.when({ event: 'Started' }, (event) => {
87
- contentLength = event.data.contentLength ?? 0;
88
- log.info('download started', { contentLength });
89
- }),
90
- Match.when({ event: 'Progress' }, (event) => {
91
- downloaded += event.data.chunkLength;
92
- log.verbose('download progress', { downloaded, contentLength });
93
- }),
94
- Match.when({ event: 'Finished' }, () => {
95
- log.info('download completed');
96
- }),
97
- Match.exhaustive,
98
- );
99
-
100
- const downloadSuccess = yield* Effect.promise(() => downloadAndInstall(update, handleDownload));
101
-
102
- if (downloadSuccess) {
103
- yield* invoke(LayoutOperation.AddToast, {
104
- id: `${meta.id}.update-ready`,
105
- title: ['update-ready.label', { ns: meta.id }],
106
- description: ['update-ready.description', { ns: meta.id }],
107
- duration: Infinity,
108
- actionLabel: ['update.label', { ns: meta.id }],
109
- actionAlt: ['update.alt', { ns: meta.id }],
110
- onAction: () => relaunch(),
111
- });
112
- return false;
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;
113
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;
114
168
  }
115
- return true;
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;
116
179
  });
117
180
 
118
- // Run immediately on startup, then repeat every hour. Stop once an update is found.
119
181
  const schedule = Schedule.fixed(Duration.hours(1)).pipe(
120
182
  Schedule.whileInput((keepChecking: boolean) => keepChecking),
121
183
  );
122
- const fiber = yield* action.pipe(Effect.repeat(schedule), Effect.forkDaemon);
184
+ const fiber = yield* backgroundAction.pipe(Effect.repeat(schedule), Effect.forkDaemon);
123
185
  log.info('updater module initialized, update check scheduled');
124
186
 
125
- return Capability.contributes(Capabilities.Null, null, () =>
126
- Effect.sync(() => Effect.runSync(Fiber.interrupt(fiber))),
127
- );
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
+ ];
128
193
  }),
129
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/meta.ts CHANGED
@@ -2,15 +2,31 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { type Plugin } from '@dxos/app-framework';
5
+ import { Plugin } from '@dxos/app-framework';
6
+ import { DXN } from '@dxos/keys';
6
7
  import { trim } from '@dxos/util';
7
8
 
8
- export const meta: Plugin.Meta = {
9
- id: 'org.dxos.plugin.native',
10
- name: 'Native',
9
+ // TODO(wittjosiah): Rename plugin (package + id) from `native` to `app` to match the user-facing name.
10
+ export const meta = Plugin.makeMeta({
11
+ key: DXN.make('org.dxos.plugin.native'),
12
+ name: 'App',
13
+ author: 'DXOS',
14
+ spec: 'PLUGIN.mdl',
11
15
  description: trim`
12
- Native platform integration providing desktop-specific features and system-level capabilities.
13
- Access native file dialogs, notifications, and OS integrations.
16
+ Tauri-based native platform integration for DXOS Composer on macOS, Linux, and Windows.
17
+
18
+ Provides Spotlight window integration that forwards spotlight:invoke Tauri events to
19
+ Composer operations, letting the OS-level launcher open spaces and switch workspaces
20
+ without the main window being focused first.
21
+
22
+ Manages over-the-air application updates via the Tauri updater plugin, automatically
23
+ checking for new releases in the background, streaming download progress, and surfacing
24
+ a toast notification with a one-click Relaunch action when an update is ready.
25
+
26
+ Spawns an Ollama sidecar process on localhost for local AI model inference, registering
27
+ it as an AiModelResolver so Assistant features can resolve model requests without any
28
+ external server dependency.
14
29
  `,
30
+ icon: 'ph--app-window--regular',
15
31
  tags: ['system'],
16
- };
32
+ });