Files
buzz/desktop/src/features/settings/ui/MeshComputeSettingsCard.tsx
tlongwell-block c5ec624ebc mesh-llm: B6 kind:31990 publisher + settings-card publish-on-save
desktop/src-tauri/src/commands/mesh_llm.rs: new mesh_publish_offer
command. Reads persisted prefs and the local iroh endpoint id; on
enabled=true, builds a kind:31990 event with the JSON-serialised
MeshLlmOffer envelope and a 'd' tag matching prefs.d_tag, then signs
+ POSTs via the existing submit_event pipeline (NIP-98 to /events).

On enabled=false, publishes the *same address* with empty content —
NIP-33's 'delete by replace' idiom — so consumers know the offer has
been withdrawn. PublishOfferResult.published_offer reports which path
was taken.

desktop/src/features/settings/ui/MeshComputeSettingsCard.tsx: persist()
now follows save-prefs with a relay capability probe and (when the
relay advertises iroh_relay_url) a publish call. If the relay doesn't
support mesh-LLM, prefs are still saved locally and the UI surfaces a
specific 'this relay does not advertise iroh_relay_url' message rather
than a confusing 'publish failed'.

The user-facing flow is now: open settings -> Share compute -> toggle
on -> a kind:31990 event hits the relay, NIP-43-fanned-out to other
members. Toggling off publishes the empty-content replacement.

Tests: 208 sprout-relay, 174 sprout-core, 11 desktop mesh_llm — all
unchanged-and-pass. desktop typecheck + biome clean.

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>
2026-05-19 17:06:52 -04:00

251 lines
8.4 KiB
TypeScript

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 {
// Save first so a failed publish leaves the prefs in a sane state.
await invoke("mesh_set_sharing_prefs", { prefs: next });
setPrefs(next);
// Probe the connected relay for its iroh_relay_url. If it doesn't
// advertise mesh-LLM at all, the offer can't be published — but the
// local prefs are still saved (the user might re-connect to a
// mesh-capable relay later).
const relayWsUrl = await invoke<string>("get_relay_ws_url");
const irohUrl = await invoke<string | null>("mesh_relay_iroh_url", {
relayWsUrl,
});
if (irohUrl) {
await invoke("mesh_publish_offer", { irohRelayUrl: irohUrl });
} else if (next.enabled) {
setError(
"Saved locally, but this relay does not advertise iroh_relay_url — your offer will not be visible to other members until the relay is configured for mesh-LLM.",
);
}
} 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>
);
}