diff --git a/desktop/src/features/settings/ui/MeshComputeSettingsCard.tsx b/desktop/src/features/settings/ui/MeshComputeSettingsCard.tsx new file mode 100644 index 000000000..911cdc0d2 --- /dev/null +++ b/desktop/src/features/settings/ui/MeshComputeSettingsCard.tsx @@ -0,0 +1,233 @@ +import * as React from "react"; +import { invoke } from "@tauri-apps/api/core"; +import { Cpu } from "lucide-react"; + +import { Switch } from "@/shared/ui/switch"; +import { Input } from "@/shared/ui/input"; + +// --------------------------------------------------------------------------- +// Types matching the Rust mesh_llm::ComputeSharingPrefs / ResourceCaps shape. +// --------------------------------------------------------------------------- + +interface ResourceCaps { + max_vram_mb: number | null; + max_ram_mb: number | null; + max_concurrency: number | null; +} + +interface ModelOffer { + id: string; + label?: string | null; + context_tokens?: number | null; +} + +interface ComputeSharingPrefs { + enabled: boolean; + caps: ResourceCaps; + models: ModelOffer[]; + d_tag: string; +} + +interface MeshEndpointInfo { + endpoint_id: string; +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/// Parse a string into Some(n) when it represents a positive integer, or +/// None to clear the cap. Empty string also clears. +function parseCap(raw: string): number | null { + if (raw.trim() === "") return null; + const n = Number.parseInt(raw, 10); + if (Number.isFinite(n) && n >= 0) return n; + return null; +} + +function formatCap(value: number | null): string { + return value == null ? "" : String(value); +} + +// --------------------------------------------------------------------------- +// Card +// --------------------------------------------------------------------------- + +export function MeshComputeSettingsCard() { + const [prefs, setPrefs] = React.useState(null); + const [endpoint, setEndpoint] = React.useState(null); + const [error, setError] = React.useState(null); + const [saving, setSaving] = React.useState(false); + + // Load the persisted prefs + the iroh endpoint identity on mount. + React.useEffect(() => { + let cancelled = false; + (async () => { + try { + const [p, e] = await Promise.all([ + invoke("mesh_get_sharing_prefs"), + invoke("mesh_get_endpoint_id"), + ]); + if (!cancelled) { + setPrefs(p); + setEndpoint(e); + } + } catch (e) { + if (!cancelled) setError(String(e)); + } + })(); + return () => { + cancelled = true; + }; + }, []); + + const persist = React.useCallback(async (next: ComputeSharingPrefs) => { + setSaving(true); + setError(null); + try { + await invoke("mesh_set_sharing_prefs", { prefs: next }); + setPrefs(next); + } catch (e) { + setError(String(e)); + } finally { + setSaving(false); + } + }, []); + + if (!prefs) { + return ( +
+
+

+ Share compute +

+

+ {error ?? "Loading mesh-LLM preferences…"} +

+
+
+ ); + } + + const updateCap = (field: keyof ResourceCaps, raw: string) => { + persist({ + ...prefs, + caps: { ...prefs.caps, [field]: parseCap(raw) }, + }); + }; + + return ( +
+
+

Share compute

+

+ When enabled, other members of this relay can run agents on this + machine using the limits you set below. Your relay membership is the + only gate — there is no signup or external account. +

+
+ + {error ? ( +

+ {error} +

+ ) : null} + +
+ {/* ── Master toggle ────────────────────────────────────────── */} +
+
+ +

+ Publishes a kind:31990 compute-offer event when on; deletes it + when off. +

+
+ + persist({ ...prefs, enabled: checked }) + } + /> +
+ + {/* ── Caps ─────────────────────────────────────────────────── */} +
+ + + Limits per request + + +
+
+ + updateCap("max_vram_mb", e.target.value)} + placeholder="No limit" + value={formatCap(prefs.caps.max_vram_mb)} + /> +
+
+ + updateCap("max_ram_mb", e.target.value)} + placeholder="No limit" + value={formatCap(prefs.caps.max_ram_mb)} + /> +
+
+ + updateCap("max_concurrency", e.target.value)} + placeholder="1" + value={formatCap(prefs.caps.max_concurrency)} + /> +
+
+
+ + {/* ── Identity ────────────────────────────────────────────── */} + {endpoint ? ( +
+ + This device's iroh endpoint: + {" "} + {endpoint.endpoint_id} +
+ ) : null} +
+
+ ); +} diff --git a/desktop/src/features/settings/ui/SettingsPanels.tsx b/desktop/src/features/settings/ui/SettingsPanels.tsx index 661926de5..786352000 100644 --- a/desktop/src/features/settings/ui/SettingsPanels.tsx +++ b/desktop/src/features/settings/ui/SettingsPanels.tsx @@ -3,6 +3,7 @@ import { BellRing, Bot, Check, + Cpu, Download, Keyboard, LayoutTemplate, @@ -31,6 +32,7 @@ import { SYNTAX_THEMES, isLightTheme } from "@/shared/theme/theme-loader"; import { ChannelTemplatesSettingsCard } from "./ChannelTemplatesSettingsCard"; import { DoctorSettingsPanel } from "./DoctorSettingsPanel"; import { KeyboardShortcutsCard } from "./KeyboardShortcutsCard"; +import { MeshComputeSettingsCard } from "./MeshComputeSettingsCard"; import { MobilePairingCard } from "./MobilePairingCard"; import { NotificationSettingsCard } from "./NotificationSettingsCard"; import { PreventSleepSettingsCard } from "./PreventSleepSettingsCard"; @@ -41,6 +43,7 @@ export type SettingsSection = | "profile" | "notifications" | "agents" + | "compute" | "channel-templates" | "appearance" | "shortcuts" @@ -87,6 +90,11 @@ export const settingsSections: SettingsSectionDescriptor[] = [ label: "Agents", icon: Bot, }, + { + value: "compute", + label: "Share compute", + icon: Cpu, + }, { value: "channel-templates", label: "Templates", @@ -282,6 +290,8 @@ export function renderSettingsSection( ); case "agents": return ; + case "compute": + return ; case "channel-templates": return ; case "appearance":