mesh-llm: C1 MeshComputeSettingsCard

desktop/src/features/settings/ui/MeshComputeSettingsCard.tsx (new):
- Toggle: 'Share this machine's compute' (master switch).
- Three numeric inputs: max VRAM (MB), max RAM (MB), concurrent peers.
  Empty = no cap, validated to non-negative integers.
- Displays the local iroh endpoint id (canonical Display form) so the
  user knows which device identity is publishing.
- All state persisted through the mesh_set_sharing_prefs Tauri command;
  loads via mesh_get_sharing_prefs + mesh_get_endpoint_id on mount.
- Error and saving states surfaced inline.

Registered as a new 'compute' SettingsSection in SettingsPanels.tsx
between 'agents' and 'channel-templates', with a Cpu icon. Reached
through the existing avatar-menu -> Settings flow (no popover
restructuring needed for the MVP).

desktop typecheck (tsc --noEmit): clean.
desktop biome check: clean.

UX (matches Tyler's [1]):
- avatar bottom-left -> ProfilePopover -> Settings -> Share compute
- one switch + three caps; the offering side decides everything.

Signed-off-by: Tyler Longwell <109685178+tlongwell-block@users.noreply.github.com>
Co-authored-by: Dawn (sprout agent) <c6237ef84fa537c78dcee78efd2d4e59f728859c7f194da42ac51ededfa0be05@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
tlongwell-block
2026-05-19 17:03:20 -04:00
co-authored by Dawn
parent d8ca1a9c3e
commit e6b968a9de
2 changed files with 243 additions and 0 deletions
@@ -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<ComputeSharingPrefs | null>(null);
const [endpoint, setEndpoint] = React.useState<MeshEndpointInfo | null>(null);
const [error, setError] = React.useState<string | null>(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<ComputeSharingPrefs>("mesh_get_sharing_prefs"),
invoke<MeshEndpointInfo>("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 (
<section className="min-w-0" data-testid="settings-compute">
<div className="mb-3">
<h2 className="text-sm font-semibold tracking-tight">
Share compute
</h2>
<p className="text-sm text-muted-foreground">
{error ?? "Loading mesh-LLM preferences…"}
</p>
</div>
</section>
);
}
const updateCap = (field: keyof ResourceCaps, raw: string) => {
persist({
...prefs,
caps: { ...prefs.caps, [field]: parseCap(raw) },
});
};
return (
<section className="min-w-0" data-testid="settings-compute">
<div className="mb-3 min-w-0">
<h2 className="text-sm font-semibold tracking-tight">Share compute</h2>
<p className="text-sm text-muted-foreground">
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.
</p>
</div>
{error ? (
<p className="mb-3 rounded bg-destructive/10 px-2 py-1 text-xs text-destructive">
{error}
</p>
) : null}
<div className="flex flex-col gap-4">
{/* ── Master toggle ────────────────────────────────────────── */}
<div className="flex items-center justify-between gap-4">
<div className="min-w-0">
<label
className="text-sm font-medium"
htmlFor="mesh-compute-enabled"
>
Share this machine's compute
</label>
<p className="text-sm text-muted-foreground">
Publishes a kind:31990 compute-offer event when on; deletes it
when off.
</p>
</div>
<Switch
checked={prefs.enabled}
data-testid="mesh-compute-enabled-toggle"
disabled={saving}
id="mesh-compute-enabled"
onCheckedChange={(checked) =>
persist({ ...prefs, enabled: checked })
}
/>
</div>
{/* ── Caps ─────────────────────────────────────────────────── */}
<fieldset className="flex flex-col gap-3" disabled={!prefs.enabled}>
<legend className="flex items-center gap-2 text-sm font-medium">
<Cpu className="h-4 w-4 text-muted-foreground" />
Limits per request
</legend>
<div className="grid grid-cols-3 gap-3">
<div>
<label
className="text-xs text-muted-foreground"
htmlFor="mesh-vram"
>
Max VRAM (MB)
</label>
<Input
data-testid="mesh-cap-vram"
id="mesh-vram"
inputMode="numeric"
onChange={(e) => updateCap("max_vram_mb", e.target.value)}
placeholder="No limit"
value={formatCap(prefs.caps.max_vram_mb)}
/>
</div>
<div>
<label
className="text-xs text-muted-foreground"
htmlFor="mesh-ram"
>
Max RAM (MB)
</label>
<Input
data-testid="mesh-cap-ram"
id="mesh-ram"
inputMode="numeric"
onChange={(e) => updateCap("max_ram_mb", e.target.value)}
placeholder="No limit"
value={formatCap(prefs.caps.max_ram_mb)}
/>
</div>
<div>
<label
className="text-xs text-muted-foreground"
htmlFor="mesh-concurrency"
>
Concurrent peers
</label>
<Input
data-testid="mesh-cap-concurrency"
id="mesh-concurrency"
inputMode="numeric"
onChange={(e) => updateCap("max_concurrency", e.target.value)}
placeholder="1"
value={formatCap(prefs.caps.max_concurrency)}
/>
</div>
</div>
</fieldset>
{/* ── Identity ────────────────────────────────────────────── */}
{endpoint ? (
<div className="rounded border border-border/60 bg-muted/30 px-3 py-2 text-xs text-muted-foreground">
<span className="font-medium text-foreground">
This device's iroh endpoint:
</span>{" "}
<code className="break-all">{endpoint.endpoint_id}</code>
</div>
) : null}
</div>
</section>
);
}
@@ -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 <PreventSleepSettingsCard />;
case "compute":
return <MeshComputeSettingsCard />;
case "channel-templates":
return <ChannelTemplatesSettingsCard />;
case "appearance":