diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index e40a64f..381340d 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -47,7 +47,8 @@ jobs:
working-directory: src-tauri
run: cargo fmt --check
- # Default features only — candle-cuda must never be enabled in CI.
+ # --no-default-features: default enables candle-cuda for the main dev
+ # machine; CI runners have no CUDA toolkit and must build CPU-only.
- name: Clippy
working-directory: src-tauri
- run: cargo clippy --all-targets --locked
+ run: cargo clippy --all-targets --locked --no-default-features
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 1e8fde7..242ea79 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -52,3 +52,6 @@ jobs:
prerelease: false
uploadUpdaterJson: true
updaterJsonPreferNsis: true
+ # Cargo args after `--`: ship the CPU/DirectML build — default
+ # features enable candle-cuda, which runners (and most users) lack.
+ args: '-- --no-default-features'
diff --git a/package.json b/package.json
index 87d6738..c8b2cd2 100644
--- a/package.json
+++ b/package.json
@@ -22,6 +22,8 @@
"@tauri-apps/plugin-fs": "^2.5.0",
"@tauri-apps/plugin-notification": "^2.3.3",
"@tauri-apps/plugin-opener": "^2",
+ "@tauri-apps/plugin-process": "^2.3.1",
+ "@tauri-apps/plugin-updater": "^2.10.1",
"d3-force": "^3.0.0",
"framer-motion": "^12.38.0",
"react": "^19.1.0",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index ec0add6..4192a50 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -26,6 +26,12 @@ importers:
'@tauri-apps/plugin-opener':
specifier: ^2
version: 2.5.3
+ '@tauri-apps/plugin-process':
+ specifier: ^2.3.1
+ version: 2.3.1
+ '@tauri-apps/plugin-updater':
+ specifier: ^2.10.1
+ version: 2.10.1
d3-force:
specifier: ^3.0.0
version: 3.0.0
@@ -662,6 +668,12 @@ packages:
'@tauri-apps/plugin-opener@2.5.3':
resolution: {integrity: sha512-CCcUltXMOfUEArbf3db3kCE7Ggy1ExBEBl51Ko2ODJ6GDYHRp1nSNlQm5uNCFY5k7/ufaK5Ib3Du/Zir19IYQQ==}
+ '@tauri-apps/plugin-process@2.3.1':
+ resolution: {integrity: sha512-nCa4fGVaDL/B9ai03VyPOjfAHRHSBz5v6F/ObsB73r/dA3MHHhZtldaDMIc0V/pnUw9ehzr2iEG+XkSEyC0JJA==}
+
+ '@tauri-apps/plugin-updater@2.10.1':
+ resolution: {integrity: sha512-NFYMg+tWOZPJdzE/PpFj2qfqwAWwNS3kXrb1tm1gnBJ9mYzZ4WDRrwy8udzWoAnfGCHLuePNLY1WVCNHnh3eRA==}
+
'@types/babel__core@7.20.5':
resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==}
@@ -1462,6 +1474,14 @@ snapshots:
dependencies:
'@tauri-apps/api': 2.10.1
+ '@tauri-apps/plugin-process@2.3.1':
+ dependencies:
+ '@tauri-apps/api': 2.10.1
+
+ '@tauri-apps/plugin-updater@2.10.1':
+ dependencies:
+ '@tauri-apps/api': 2.10.1
+
'@types/babel__core@7.20.5':
dependencies:
'@babel/parser': 7.29.2
diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock
index b39cfeb..b9e5323 100644
--- a/src-tauri/Cargo.lock
+++ b/src-tauri/Cargo.lock
@@ -3495,6 +3495,12 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+[[package]]
+name = "minisign-verify"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22f9645cb765ea72b8111f36c522475d2daa0d22c957a9826437e97534bc4e9e"
+
[[package]]
name = "miniz_oxide"
version = "0.8.9"
@@ -3986,6 +3992,18 @@ dependencies = [
"objc2-core-foundation",
]
+[[package]]
+name = "objc2-osa-kit"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f112d1746737b0da274ef79a23aac283376f335f4095a083a267a082f21db0c0"
+dependencies = [
+ "bitflags 2.11.0",
+ "objc2",
+ "objc2-app-kit",
+ "objc2-foundation",
+]
+
[[package]]
name = "objc2-quartz-core"
version = "0.3.2"
@@ -4155,6 +4173,20 @@ dependencies = [
"ureq",
]
+[[package]]
+name = "osakit"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "732c71caeaa72c065bb69d7ea08717bd3f4863a4f451402fc9513e29dbd5261b"
+dependencies = [
+ "objc2",
+ "objc2-foundation",
+ "objc2-osa-kit",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.18",
+]
+
[[package]]
name = "pango"
version = "0.18.3"
@@ -4457,6 +4489,8 @@ dependencies = [
"tauri-plugin-fs",
"tauri-plugin-notification",
"tauri-plugin-opener",
+ "tauri-plugin-process",
+ "tauri-plugin-updater",
"tokenizers",
"tokio",
"ureq",
@@ -5110,15 +5144,20 @@ dependencies = [
"http-body",
"http-body-util",
"hyper",
+ "hyper-rustls",
"hyper-util",
"js-sys",
"log",
"percent-encoding",
"pin-project-lite",
+ "rustls",
+ "rustls-pki-types",
+ "rustls-platform-verifier",
"serde",
"serde_json",
"sync_wrapper",
"tokio",
+ "tokio-rustls",
"tokio-util",
"tower",
"tower-http",
@@ -5234,6 +5273,18 @@ dependencies = [
"zeroize",
]
+[[package]]
+name = "rustls-native-certs"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dab5152771c58876a2146916e53e35057e1a4dfa2b9df0f0305b07f611fdea4d"
+dependencies = [
+ "openssl-probe",
+ "rustls-pki-types",
+ "schannel",
+ "security-framework",
+]
+
[[package]]
name = "rustls-pki-types"
version = "1.14.0"
@@ -5243,6 +5294,33 @@ dependencies = [
"zeroize",
]
+[[package]]
+name = "rustls-platform-verifier"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784"
+dependencies = [
+ "core-foundation 0.10.1",
+ "core-foundation-sys",
+ "jni",
+ "log",
+ "once_cell",
+ "rustls",
+ "rustls-native-certs",
+ "rustls-platform-verifier-android",
+ "rustls-webpki",
+ "security-framework",
+ "security-framework-sys",
+ "webpki-root-certs",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustls-platform-verifier-android"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
+
[[package]]
name = "rustls-webpki"
version = "0.103.10"
@@ -6251,6 +6329,49 @@ dependencies = [
"zbus",
]
+[[package]]
+name = "tauri-plugin-process"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d55511a7bf6cd70c8767b02c97bf8134fa434daf3926cfc1be0a0f94132d165a"
+dependencies = [
+ "tauri",
+ "tauri-plugin",
+]
+
+[[package]]
+name = "tauri-plugin-updater"
+version = "2.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "806d9dac662c2e4594ff03c647a552f2c9bd544e7d0f683ec58f872f952ce4af"
+dependencies = [
+ "base64 0.22.1",
+ "dirs",
+ "flate2",
+ "futures-util",
+ "http",
+ "infer",
+ "log",
+ "minisign-verify",
+ "osakit",
+ "percent-encoding",
+ "reqwest 0.13.2",
+ "rustls",
+ "semver",
+ "serde",
+ "serde_json",
+ "tar",
+ "tauri",
+ "tauri-plugin",
+ "tempfile",
+ "thiserror 2.0.18",
+ "time",
+ "tokio",
+ "url",
+ "windows-sys 0.60.2",
+ "zip 4.6.1",
+]
+
[[package]]
name = "tauri-runtime"
version = "2.10.1"
diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml
index 9836e2f..eeb3967 100644
--- a/src-tauri/Cargo.toml
+++ b/src-tauri/Cargo.toml
@@ -55,6 +55,8 @@ kamadak-exif = "0.5"
notify = "6"
tauri-plugin-notification = "2"
mozjpeg = "0.10.13"
+tauri-plugin-updater = "2"
+tauri-plugin-process = "2"
# ── Dev-mode performance ────────────────────────────────────────────────────
# opt-level=1 on the main crate keeps incremental compile short.
diff --git a/src-tauri/capabilities/default.json b/src-tauri/capabilities/default.json
index a526b2c..93b55f7 100644
--- a/src-tauri/capabilities/default.json
+++ b/src-tauri/capabilities/default.json
@@ -14,6 +14,8 @@
"fs:read-files",
"fs:read-dirs",
"notification:default",
+ "updater:default",
+ "process:allow-restart",
"core:window:allow-minimize",
"core:window:allow-close",
"core:window:allow-toggle-maximize",
diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs
index b0e3ccb..fe6832d 100644
--- a/src-tauri/src/lib.rs
+++ b/src-tauri/src/lib.rs
@@ -16,6 +16,8 @@ use tauri::Manager;
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
+ .plugin(tauri_plugin_updater::Builder::new().build())
+ .plugin(tauri_plugin_process::init())
.plugin(tauri_plugin_opener::init())
.plugin(tauri_plugin_dialog::init())
.plugin(tauri_plugin_fs::init())
diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json
index 5ec02ac..70180f3 100644
--- a/src-tauri/tauri.conf.json
+++ b/src-tauri/tauri.conf.json
@@ -31,9 +31,18 @@
}
}
},
+ "plugins": {
+ "updater": {
+ "pubkey": "dW50cnVzdGVkIGNvbW1lbnQ6IG1pbmlzaWduIHB1YmxpYyBrZXk6IDYyMDVBQzkyOENENjYzOTUKUldTVlk5YU1rcXdGWW9JRklYdHpOVXA1MDNMVEpyell6cVlma0VGS3pYaUVBLzJydy9nNUtJdlUK",
+ "endpoints": [
+ "https://github.com/JezzWTF/phokus/releases/latest/download/latest.json"
+ ]
+ }
+ },
"bundle": {
"active": true,
"targets": ["nsis"],
+ "createUpdaterArtifacts": true,
"publisher": "JezzWTF",
"license": "MIT",
"copyright": "Copyright © 2026 JezzWTF",
diff --git a/src/App.tsx b/src/App.tsx
index 409da27..d1ad17e 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -10,6 +10,7 @@ import { DuplicateFinder } from "./components/DuplicateFinder";
import { Timeline } from "./components/Timeline";
import { TitleBar } from "./components/TitleBar";
import { SettingsModal } from "./components/SettingsModal";
+import { UpdateToast } from "./components/UpdateToast";
import { initializeNotifications } from "./notifications";
export default function App() {
@@ -21,12 +22,19 @@ export default function App() {
const loadMutedFolderIds = useGalleryStore((state) => state.loadMutedFolderIds);
const loadNotificationsPaused = useGalleryStore((state) => state.loadNotificationsPaused);
const subscribeToProgress = useGalleryStore((state) => state.subscribeToProgress);
+ const loadAppVersion = useGalleryStore((state) => state.loadAppVersion);
+ const checkForUpdates = useGalleryStore((state) => state.checkForUpdates);
const activeView = useGalleryStore((state) => state.activeView);
useEffect(() => {
void initializeNotifications();
void loadMutedFolderIds();
void loadNotificationsPaused();
+ void loadAppVersion();
+ // Quiet launch check — dev builds have no signed artifacts to update to.
+ if (import.meta.env.PROD) {
+ void checkForUpdates({ quiet: true });
+ }
loadFolders().then(() => {
void loadBackgroundJobProgress();
void loadCaptionModelStatus();
@@ -79,6 +87,7 @@ export default function App() {
Update available
++ Phokus v{updateVersion} is ready to download and install. +
++ {updateStatus === "installing" ? "Installing update..." : "Downloading update..."} +
+The app will restart when it finishes.
+ > + )} +