@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.
- package/LICENSE +102 -5
- package/PLUGIN.mdl +284 -0
- package/README.md +1 -1
- package/dist/lib/neutral/NativePlugin.mjs +22 -4
- package/dist/lib/neutral/NativePlugin.mjs.map +4 -4
- package/dist/lib/neutral/NativeSettings-CLYG5TJH.mjs +113 -0
- package/dist/lib/neutral/NativeSettings-CLYG5TJH.mjs.map +7 -0
- package/dist/lib/neutral/capabilities/index.mjs +9 -3
- package/dist/lib/neutral/capabilities/index.mjs.map +3 -3
- package/dist/lib/neutral/chunk-75E24KAJ.mjs +34 -0
- package/dist/lib/neutral/chunk-75E24KAJ.mjs.map +7 -0
- package/dist/lib/neutral/chunk-J5LGTIGS.mjs +10 -0
- package/dist/lib/neutral/chunk-J5LGTIGS.mjs.map +7 -0
- package/dist/lib/neutral/components/index.mjs +9 -0
- package/dist/lib/neutral/components/index.mjs.map +7 -0
- package/dist/lib/neutral/index.mjs +2 -1
- package/dist/lib/neutral/meta.json +1 -1
- package/dist/lib/neutral/meta.mjs +2 -1
- package/dist/lib/neutral/{ollama-CYD5M6XB.mjs → ollama-BHWFTROM.mjs} +3 -1
- package/dist/lib/neutral/{ollama-CYD5M6XB.mjs.map → ollama-BHWFTROM.mjs.map} +1 -1
- package/dist/lib/neutral/plugin.mjs +2 -1
- package/dist/lib/neutral/plugin.mjs.map +1 -1
- package/dist/lib/neutral/react-surface-ETNGHN4X.mjs +27 -0
- package/dist/lib/neutral/react-surface-ETNGHN4X.mjs.map +7 -0
- package/dist/lib/neutral/settings-HF6TQNU4.mjs +28 -0
- package/dist/lib/neutral/settings-HF6TQNU4.mjs.map +7 -0
- package/dist/lib/neutral/{spotlight-listener-TRCJXLOQ.mjs → spotlight-listener-K7S6LGL3.mjs} +3 -1
- package/dist/lib/neutral/{spotlight-listener-TRCJXLOQ.mjs.map → spotlight-listener-K7S6LGL3.mjs.map} +1 -1
- package/dist/lib/neutral/translations.mjs +18 -1
- package/dist/lib/neutral/translations.mjs.map +2 -2
- package/dist/lib/neutral/types/index.mjs +31 -0
- package/dist/lib/neutral/types/index.mjs.map +7 -0
- package/dist/lib/neutral/updater-JEBXWSSK.mjs +244 -0
- package/dist/lib/neutral/updater-JEBXWSSK.mjs.map +7 -0
- package/dist/types/src/NativePlugin.d.ts.map +1 -1
- package/dist/types/src/capabilities/index.d.ts +3 -1
- package/dist/types/src/capabilities/index.d.ts.map +1 -1
- package/dist/types/src/capabilities/react-surface.d.ts +5 -0
- package/dist/types/src/capabilities/react-surface.d.ts.map +1 -0
- package/dist/types/src/capabilities/settings.d.ts +7 -0
- package/dist/types/src/capabilities/settings.d.ts.map +1 -0
- package/dist/types/src/capabilities/updater.d.ts +2 -1
- package/dist/types/src/capabilities/updater.d.ts.map +1 -1
- package/dist/types/src/components/NativeSettings/NativeSettings.d.ts +6 -0
- package/dist/types/src/components/NativeSettings/NativeSettings.d.ts.map +1 -0
- package/dist/types/src/components/NativeSettings/index.d.ts +2 -0
- package/dist/types/src/components/NativeSettings/index.d.ts.map +1 -0
- package/dist/types/src/components/index.d.ts +3 -0
- package/dist/types/src/components/index.d.ts.map +1 -0
- package/dist/types/src/meta.d.ts +1 -1
- package/dist/types/src/meta.d.ts.map +1 -1
- package/dist/types/src/translations.d.ts +15 -0
- package/dist/types/src/translations.d.ts.map +1 -1
- package/dist/types/src/types/NativeCapabilities.d.ts +5 -0
- package/dist/types/src/types/NativeCapabilities.d.ts.map +1 -0
- package/dist/types/src/types/Settings.d.ts +5 -0
- package/dist/types/src/types/Settings.d.ts.map +1 -0
- package/dist/types/src/types/Update.d.ts +30 -0
- package/dist/types/src/types/Update.d.ts.map +1 -0
- package/dist/types/src/types/index.d.ts +4 -0
- package/dist/types/src/types/index.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +40 -22
- package/src/NativePlugin.tsx +11 -3
- package/src/capabilities/index.ts +2 -0
- package/src/capabilities/react-surface.tsx +29 -0
- package/src/capabilities/settings.ts +31 -0
- package/src/capabilities/updater.ts +136 -71
- package/src/components/NativeSettings/NativeSettings.tsx +132 -0
- package/src/components/NativeSettings/index.ts +5 -0
- package/src/components/index.ts +7 -0
- package/src/meta.ts +23 -7
- package/src/translations.ts +15 -0
- package/src/types/NativeCapabilities.ts +16 -0
- package/src/types/Settings.ts +11 -0
- package/src/types/Update.ts +24 -0
- package/src/types/index.ts +7 -0
- package/src/vite-env.d.ts +10 -0
- package/dist/lib/neutral/chunk-254JXXJC.mjs +0 -18
- package/dist/lib/neutral/chunk-254JXXJC.mjs.map +0 -7
- package/dist/lib/neutral/updater-ZAJJC24D.mjs +0 -141
- 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.
|
|
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": "
|
|
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.
|
|
62
|
-
"@tauri-apps/plugin-os": "^2.3.
|
|
63
|
-
"@tauri-apps/plugin-process": "^2.3.
|
|
64
|
-
"@tauri-apps/plugin-shell": "^2.3.
|
|
65
|
-
"@tauri-apps/plugin-updater": "^2.
|
|
66
|
-
"
|
|
67
|
-
"@dxos/
|
|
68
|
-
"@dxos/
|
|
69
|
-
"@dxos/
|
|
70
|
-
"@dxos/
|
|
71
|
-
"@dxos/
|
|
72
|
-
"@dxos/plugin-
|
|
73
|
-
"@dxos/
|
|
74
|
-
"@dxos/
|
|
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
|
-
"
|
|
79
|
-
"
|
|
80
|
-
"
|
|
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
|
-
"
|
|
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"
|
package/src/NativePlugin.tsx
CHANGED
|
@@ -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.
|
|
22
|
+
activatesOn: ActivationEvents.ProcessManagerReady,
|
|
18
23
|
activate: SpotlightListener,
|
|
19
24
|
}),
|
|
20
25
|
Plugin.addModule({
|
|
21
26
|
id: 'updater',
|
|
22
|
-
activatesOn: ActivationEvents.
|
|
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
|
-
|
|
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
|
|
32
|
+
return { ok: false, error: formatError(error) };
|
|
33
33
|
}
|
|
34
34
|
};
|
|
35
35
|
|
|
36
|
-
/**
|
|
37
|
-
|
|
38
|
-
*/
|
|
39
|
-
const downloadAndInstall = async (
|
|
36
|
+
/** Safe wrapper around update.downloadAndInstall(). */
|
|
37
|
+
const safeDownloadAndInstall = async (
|
|
40
38
|
update: Updater.Update,
|
|
41
|
-
|
|
42
|
-
): Promise<
|
|
39
|
+
onEvent: (event: Updater.DownloadEvent) => void,
|
|
40
|
+
): Promise<{ ok: true } | { ok: false; error: string }> => {
|
|
43
41
|
try {
|
|
44
|
-
await update.downloadAndInstall(
|
|
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
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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*
|
|
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
|
|
126
|
-
|
|
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
|
+
};
|
package/src/meta.ts
CHANGED
|
@@ -2,15 +2,31 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { Plugin } from '@dxos/app-framework';
|
|
6
|
+
import { DXN } from '@dxos/keys';
|
|
6
7
|
import { trim } from '@dxos/util';
|
|
7
8
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
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
|
-
|
|
13
|
-
|
|
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
|
+
});
|