diff --git a/.github/workflows/linux-canary.yml b/.github/workflows/linux-canary.yml index 9806443378..2a9783ea03 100644 --- a/.github/workflows/linux-canary.yml +++ b/.github/workflows/linux-canary.yml @@ -170,6 +170,9 @@ jobs: ./scripts/bundle-sidecars.sh - name: Build Linux Tauri app + # Intentionally without --features mesh-llm: release-linux matches this + # (CI mesh builds use Metal). Settings → Compute shows a build-unavailable + # empty state via mesh_feature_enabled instead of a dead-end toggle (#3841). run: cd desktop && pnpm tauri build --ci --bundles deb,appimage --config src-tauri/tauri.canary.conf.json env: CMAKE_POLICY_VERSION_MINIMUM: "3.5" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 07951ef81d..ee3ed5aa90 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -573,6 +573,9 @@ jobs: BUZZ_UPDATER_ENDPOINT: https://github.com/block/buzz/releases/download/buzz-desktop-latest/latest.json - name: Build Linux Tauri app + # No --features mesh-llm (macOS release is the only official package that + # enables Share Compute today; CI llama builds target Metal). The desktop + # Settings → Compute card gates on mesh_feature_enabled (#3841). run: cd desktop && pnpm tauri build --verbose --ci --bundles deb,appimage --config src-tauri/tauri.release.conf.json env: BUZZ_UPDATER_PUBLIC_KEY: ${{ secrets.BUZZ_UPDATER_PUBLIC_KEY || secrets.SPROUT_UPDATER_PUBLIC_KEY }} diff --git a/desktop/src-tauri/src/commands/mesh_llm.rs b/desktop/src-tauri/src/commands/mesh_llm.rs index 998bc6e7d2..336c36168e 100644 --- a/desktop/src-tauri/src/commands/mesh_llm.rs +++ b/desktop/src-tauri/src/commands/mesh_llm.rs @@ -978,6 +978,12 @@ pub async fn mesh_model_catalog() -> CmdResult { .map_err(|error| format!("mesh catalog task failed: {error}")) } +/// Build-time probe: Share Compute / mesh-llm is compiled into this binary. +#[tauri::command] +pub fn mesh_feature_enabled() -> bool { + true +} + #[cfg(all(test, feature = "mesh-llm"))] #[path = "mesh_llm_tests.rs"] mod tests; diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs index ee2a98f5c1..d34e13a83d 100644 --- a/desktop/src-tauri/src/lib.rs +++ b/desktop/src-tauri/src/lib.rs @@ -818,6 +818,7 @@ pub fn run() { mesh_serving_usage, mesh_installed_models, mesh_model_catalog, + mesh_feature_enabled, update_managed_agent, discover_backend_providers, probe_backend_provider, diff --git a/desktop/src-tauri/src/mesh_llm_stubs.rs b/desktop/src-tauri/src/mesh_llm_stubs.rs index e8c13f48ea..113f726252 100644 --- a/desktop/src-tauri/src/mesh_llm_stubs.rs +++ b/desktop/src-tauri/src/mesh_llm_stubs.rs @@ -4,13 +4,21 @@ use crate::app_state::AppState; type CmdResult = Result; +const MESH_FEATURE_DISABLED: &str = "Share Compute is not included in this build (mesh-llm feature off — typical for Linux/Windows release packages; macOS releases enable it)."; + +/// Build-time probe: Share Compute / mesh-llm is compiled into this binary. +#[tauri::command] +pub fn mesh_feature_enabled() -> bool { + false +} + #[tauri::command] pub async fn mesh_start_node( _app: tauri::AppHandle, _state: State<'_, AppState>, _request: serde_json::Value, ) -> CmdResult { - Err("mesh-llm feature not enabled".to_string()) + Err(MESH_FEATURE_DISABLED.to_string()) } #[tauri::command] @@ -18,27 +26,27 @@ pub async fn mesh_stop_node( _app: tauri::AppHandle, _state: State<'_, AppState>, ) -> CmdResult { - Err("mesh-llm feature not enabled".to_string()) + Err(MESH_FEATURE_DISABLED.to_string()) } #[tauri::command] pub async fn mesh_node_status(_state: State<'_, AppState>) -> CmdResult { - Err("mesh-llm feature not enabled".to_string()) + Err(MESH_FEATURE_DISABLED.to_string()) } #[tauri::command] pub async fn mesh_serving_usage(_state: State<'_, AppState>) -> CmdResult { - Err("mesh-llm feature not enabled".to_string()) + Err(MESH_FEATURE_DISABLED.to_string()) } #[tauri::command] pub async fn mesh_installed_models( _state: State<'_, AppState>, ) -> CmdResult> { - Err("mesh-llm feature not enabled".to_string()) + Err(MESH_FEATURE_DISABLED.to_string()) } #[tauri::command] pub async fn mesh_model_catalog() -> CmdResult { - Err("mesh-llm feature not enabled".to_string()) + Err(MESH_FEATURE_DISABLED.to_string()) } diff --git a/desktop/src/features/mesh-compute/hooks/useMeshFeatureEnabled.ts b/desktop/src/features/mesh-compute/hooks/useMeshFeatureEnabled.ts new file mode 100644 index 0000000000..9310dcce0b --- /dev/null +++ b/desktop/src/features/mesh-compute/hooks/useMeshFeatureEnabled.ts @@ -0,0 +1,29 @@ +import * as React from "react"; + +import { meshFeatureEnabled } from "@/shared/api/tauriMesh"; + +/** + * Resolves whether Share Compute was compiled into this desktop binary. + * `null` while the probe is in flight. + */ +export function useMeshFeatureEnabled(): boolean | null { + const [enabled, setEnabled] = React.useState(null); + + React.useEffect(() => { + let cancelled = false; + (async () => { + try { + const value = await meshFeatureEnabled(); + if (!cancelled) setEnabled(value); + } catch { + // Older sidecars without the probe still surface stub errors on status. + if (!cancelled) setEnabled(null); + } + })(); + return () => { + cancelled = true; + }; + }, []); + + return enabled; +} diff --git a/desktop/src/features/mesh-compute/isMeshFeatureDisabledError.test.mjs b/desktop/src/features/mesh-compute/isMeshFeatureDisabledError.test.mjs new file mode 100644 index 0000000000..e3e27222c4 --- /dev/null +++ b/desktop/src/features/mesh-compute/isMeshFeatureDisabledError.test.mjs @@ -0,0 +1,24 @@ +import assert from "node:assert/strict"; +import { describe, it } from "node:test"; + +import { isMeshFeatureDisabledError } from "./isMeshFeatureDisabledError.ts"; + +describe("isMeshFeatureDisabledError", () => { + it("matches the stub error and the clearer packaging copy", () => { + assert.equal( + isMeshFeatureDisabledError("mesh-llm feature not enabled"), + true, + ); + assert.equal( + isMeshFeatureDisabledError( + "Share Compute is not included in this build (mesh-llm feature off — typical for Linux/Windows release packages; macOS releases enable it).", + ), + true, + ); + }); + + it("ignores unrelated failures", () => { + assert.equal(isMeshFeatureDisabledError(null), false); + assert.equal(isMeshFeatureDisabledError("download failed"), false); + }); +}); diff --git a/desktop/src/features/mesh-compute/isMeshFeatureDisabledError.ts b/desktop/src/features/mesh-compute/isMeshFeatureDisabledError.ts new file mode 100644 index 0000000000..d203f6d7ef --- /dev/null +++ b/desktop/src/features/mesh-compute/isMeshFeatureDisabledError.ts @@ -0,0 +1,12 @@ +/** + * Classify Share Compute backend errors so the settings card can show a + * build-unavailable state instead of a dead-end toggle (#3841). + */ +export function isMeshFeatureDisabledError(message: string | null | undefined): boolean { + if (!message) return false; + const lower = message.toLowerCase(); + return ( + lower.includes("mesh-llm feature") || + lower.includes("not included in this build") + ); +} diff --git a/desktop/src/features/mesh-compute/ui/MeshComputeSettingsCard.tsx b/desktop/src/features/mesh-compute/ui/MeshComputeSettingsCard.tsx index fd7550eff0..1fe5ef47b9 100644 --- a/desktop/src/features/mesh-compute/ui/MeshComputeSettingsCard.tsx +++ b/desktop/src/features/mesh-compute/ui/MeshComputeSettingsCard.tsx @@ -23,11 +23,13 @@ import { } from "@/features/settings/ui/SettingsOptionGroup"; import { SettingsSectionHeader } from "@/features/settings/ui/SettingsSectionHeader"; import { classifyModelRef } from "../classifyModelRef"; +import { isMeshFeatureDisabledError } from "../isMeshFeatureDisabledError"; import { downloadPercent, formatDownloadBytes, useMeshDownloadProgress, } from "../hooks/useMeshDownloadProgress"; +import { useMeshFeatureEnabled } from "../hooks/useMeshFeatureEnabled"; import { useMeshNodeStatus } from "../hooks/useMeshNodeStatus"; import { useMeshServingUsage } from "../hooks/useMeshServingUsage"; import { deriveMeshShareToggle } from "../shareToggleState"; @@ -64,6 +66,7 @@ function writeDraft(key: string, value: string): void { * exposing implementation protocols or raw mesh controls. */ export function MeshComputeSettingsCard() { + const featureEnabled = useMeshFeatureEnabled(); const { status, error, refresh } = useMeshNodeStatus(); const [installedModels, setInstalledModels] = React.useState< MeshModelOption[] @@ -84,6 +87,35 @@ export function MeshComputeSettingsCard() { const { progress: downloadProgress, reset: resetDownloadProgress } = useMeshDownloadProgress(); + const buildUnavailable = + featureEnabled === false || isMeshFeatureDisabledError(error); + + if (buildUnavailable) { + return ( +
+ + Share this machine with your relay. When on, other members can run + their agents here. + + } + /> +

+ Share Compute is not included in this desktop build. Official Linux + and Windows packages currently ship without the mesh-llm feature; + macOS release builds enable it. Build from source with{" "} + --features mesh-llm to turn it on + locally. +

+
+ ); + } + // Fetch installed models. Called on mount and whenever the running state // changes (a fresh start may have downloaded a new model). Stale-tolerant — // the picklist is a convenience, not load-bearing. diff --git a/desktop/src/shared/api/tauriMesh.ts b/desktop/src/shared/api/tauriMesh.ts index 8a0153123f..8feef20191 100644 --- a/desktop/src/shared/api/tauriMesh.ts +++ b/desktop/src/shared/api/tauriMesh.ts @@ -112,3 +112,12 @@ export type MeshModelCatalog = { export async function meshModelCatalog(): Promise { return await invokeTauri("mesh_model_catalog"); } + +/** + * Whether this desktop binary was compiled with `--features mesh-llm`. + * Linux/Windows release packages currently ship without it (#3841); macOS + * release/canary builds enable it. Prefer this over catching stub errors. + */ +export async function meshFeatureEnabled(): Promise { + return await invokeTauri("mesh_feature_enabled"); +} diff --git a/desktop/src/testing/e2eBridge.ts b/desktop/src/testing/e2eBridge.ts index dcad430bbf..51f7400a81 100644 --- a/desktop/src/testing/e2eBridge.ts +++ b/desktop/src/testing/e2eBridge.ts @@ -10012,6 +10012,8 @@ export function maybeInstallE2eTauriMocks() { } case "mesh_installed_models": return mockMeshState.models; + case "mesh_feature_enabled": + return true; case "mesh_model_catalog": return { gpuName: "Mock Apple GPU", diff --git a/docs/buzz-shared-compute-dev.md b/docs/buzz-shared-compute-dev.md index 3712bb7965..0dd964d930 100644 --- a/docs/buzz-shared-compute-dev.md +++ b/docs/buzz-shared-compute-dev.md @@ -61,6 +61,18 @@ stop printing build progress. Using plain `just dev` is not sufficient: the Compute UI and embedded MeshLLM runtime are behind the `mesh-llm` feature. +### Official packages and the Settings → Compute card + +Release/canary packaging only passes `--features mesh-llm` on macOS today +(see `.github/workflows/release.yml` and the Linux/Windows canary jobs). Official +`.deb` / AppImage / Windows installers therefore compile the stub backend: +Share Compute commands return a build-unavailable error, and Settings → Compute +shows an explanatory empty state instead of a dead-end toggle (`#3841`). + +To exercise Share Compute locally on any OS, use `just mesh=1 dev` (or otherwise +build the desktop with `--features mesh-llm`). The `mesh_feature_enabled` Tauri +command reports whether the running binary includes the feature. + ## 2. Share this machine 1. Open **Settings**.