diff --git a/apps/linux/README.md b/apps/linux/README.md index e69838b8ccb2..fb2829e7357f 100644 --- a/apps/linux/README.md +++ b/apps/linux/README.md @@ -26,7 +26,7 @@ cargo build The app uses `OPENCLAW_DESKTOP_CLI` when set. Otherwise it checks `~/.openclaw/bin/openclaw`, then `openclaw` on `PATH`. -After a first-run CLI install, the app opens the local dashboard once with onboarding mode enabled. Reconnects and later app launches use the normal dashboard URL. +On first run, release builds automatically install the stable CLI channel, while development builds ask for a release channel and preselect Development. After the CLI install, the app opens the local dashboard once with onboarding mode enabled. Reconnects and later app launches use the normal dashboard URL. ## Updates diff --git a/apps/linux/src-tauri/build.rs b/apps/linux/src-tauri/build.rs index d005858999b4..bd4adf8500d5 100644 --- a/apps/linux/src-tauri/build.rs +++ b/apps/linux/src-tauri/build.rs @@ -3,6 +3,7 @@ fn main() { // target's invoke handler, so keep the Linux-only command permission known. const COMMANDS: &[&str] = &[ "bootstrap", + "build_info", "canvas_a2ui_action", "check_for_updates", "connect_discovered_gateway", diff --git a/apps/linux/src-tauri/permissions/autogenerated/build_info.toml b/apps/linux/src-tauri/permissions/autogenerated/build_info.toml new file mode 100644 index 000000000000..be6dbdc2ac4a --- /dev/null +++ b/apps/linux/src-tauri/permissions/autogenerated/build_info.toml @@ -0,0 +1,11 @@ +# Automatically generated - DO NOT EDIT! + +[[permission]] +identifier = "allow-build-info" +description = "Enables the build_info command without any pre-configured scope." +commands.allow = ["build_info"] + +[[permission]] +identifier = "deny-build-info" +description = "Denies the build_info command without any pre-configured scope." +commands.deny = ["build_info"] diff --git a/apps/linux/src-tauri/src/main.rs b/apps/linux/src-tauri/src/main.rs index cc202a98523c..c7d3d47e6d1e 100644 --- a/apps/linux/src-tauri/src/main.rs +++ b/apps/linux/src-tauri/src/main.rs @@ -10,6 +10,7 @@ mod updater; use cli::{CliError, OpenClawCli}; use gateway::{GatewayAction, GatewaySnapshot}; use installer::InstallChannel; +use serde::Serialize; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex}; use std::thread; @@ -19,6 +20,18 @@ use tauri::{AppHandle, Manager, State, Url, WebviewWindow}; const CONNECTED_WATCH_INTERVAL: Duration = Duration::from_secs(15); const RECONNECT_INTERVAL: Duration = Duration::from_secs(3); +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct BuildInfo { + version: String, + release_build: bool, +} + +fn is_release_version(version: &str) -> bool { + // The committed 0.1.0 version identifies branch builds; release builds are stamped by CI. + version != "0.1.0" +} + #[derive(Default)] struct NavigationState { // One lock owns both fields so the intent check and WebView navigation cannot interleave. @@ -418,7 +431,18 @@ fn local_mode(snapshot: &GatewaySnapshot) -> &'static str { #[cfg(test)] mod navigation_tests { - use super::NavigationState; + use super::{is_release_version, NavigationState}; + + #[test] + fn committed_package_version_is_a_development_build() { + assert!(!is_release_version("0.1.0")); + } + + #[test] + fn stamped_package_versions_are_release_builds() { + assert!(is_release_version("2026.7.2")); + assert!(is_release_version("2026.7.2-beta.1")); + } #[test] fn newer_remote_selection_blocks_older_local_navigation() { @@ -502,6 +526,15 @@ fn main_window(app: &AppHandle) -> Result { .ok_or_else(|| "Main window is unavailable.".to_string()) } +#[tauri::command] +fn build_info(app: AppHandle) -> BuildInfo { + let version = app.package_info().version.to_string(); + BuildInfo { + release_build: is_release_version(&version), + version, + } +} + #[tauri::command] async fn bootstrap( app: AppHandle, @@ -567,6 +600,7 @@ fn main() { #[cfg(target_os = "linux")] let builder = builder.invoke_handler(tauri::generate_handler![ bootstrap, + build_info, canvas::canvas_a2ui_action, updater::check_for_updates, discovery::connect_discovered_gateway, @@ -580,6 +614,7 @@ fn main() { #[cfg(not(target_os = "linux"))] let builder = builder.invoke_handler(tauri::generate_handler![ bootstrap, + build_info, updater::check_for_updates, discovery::connect_discovered_gateway, discovery::discover_gateways, diff --git a/apps/linux/src-tauri/tauri.conf.json b/apps/linux/src-tauri/tauri.conf.json index 4e8cce117363..be26280c66e7 100644 --- a/apps/linux/src-tauri/tauri.conf.json +++ b/apps/linux/src-tauri/tauri.conf.json @@ -39,6 +39,7 @@ "windows": ["main"], "permissions": [ "allow-bootstrap", + "allow-build-info", "allow-check-for-updates", "allow-connect-discovered-gateway", "allow-discover-gateways", diff --git a/apps/linux/ui/main.js b/apps/linux/ui/main.js index e2272aaebc27..f6049f35d38c 100644 --- a/apps/linux/ui/main.js +++ b/apps/linux/ui/main.js @@ -189,6 +189,18 @@ async function connect() { try { const snapshot = await invoke("bootstrap"); if (snapshot.phase === "missingCli") { + const buildInfo = await invoke("build_info").catch(() => null); + if (buildInfo?.releaseBuild === false) { + elements.channel.value = "dev"; + render({ + description: + "This companion is a development build, so its Gateway install should usually use the matching release channel.", + eyebrow: "FIRST-RUN SETUP", + showInstall: true, + title: "Choose a release channel", + }); + return; + } render({ activity: "Starting the bundled installer…", description: "OpenClaw is installing its managed CLI and Node runtime.", diff --git a/docs/platforms/linux.md b/docs/platforms/linux.md index 2db04238414b..a1d191b5a2cb 100644 --- a/docs/platforms/linux.md +++ b/docs/platforms/linux.md @@ -16,7 +16,7 @@ because it does not provide `node:sqlite`. The OpenClaw Linux companion is a Tauri desktop app for a local Gateway. It: -- installs the OpenClaw CLI and managed Node runtime when they are missing +- installs the OpenClaw CLI and managed Node runtime when they are missing; release builds install the stable channel automatically, while development builds ask for the channel first - attaches to a healthy Gateway before attempting service changes - delegates install, start, stop, and restart operations to the CLI-managed systemd user service - discovers nearby Bonjour Gateways and opens their Control UI from the resolved service endpoint