From 0e60a227348a41140baf56cda12784c7dac7ec93 Mon Sep 17 00:00:00 2001 From: Alessandro Joabar Date: Thu, 27 Aug 2026 12:23:42 -0400 Subject: [PATCH] Simplify shared compute settings experience Signed-off-by: Alessandro Joabar --- .../mesh-compute/classifyModelRef.test.mjs | 59 -- .../features/mesh-compute/classifyModelRef.ts | 43 -- .../mesh-compute/experimentalModel.test.mjs | 119 ---- .../mesh-compute/experimentalModel.ts | 133 ---- .../hooks/useMeshExperimentalCatalog.ts | 62 -- .../ui/CommunityComputeComponents.test.mjs | 4 +- .../ui/CommunityComputeKpiRow.tsx | 38 +- .../ui/MeshComputeCommunityView.tsx | 12 +- .../mesh-compute/ui/MeshComputePage.tsx | 38 +- .../ui/MeshComputeSettingsCard.tsx | 670 ------------------ .../ui/MeshComputeShareBanner.tsx | 182 ++--- .../ui/MeshExperimentalSection.tsx | 131 ---- desktop/tests/e2e/mesh-compute.spec.ts | 125 +--- 13 files changed, 118 insertions(+), 1498 deletions(-) delete mode 100644 desktop/src/features/mesh-compute/classifyModelRef.test.mjs delete mode 100644 desktop/src/features/mesh-compute/classifyModelRef.ts delete mode 100644 desktop/src/features/mesh-compute/experimentalModel.test.mjs delete mode 100644 desktop/src/features/mesh-compute/experimentalModel.ts delete mode 100644 desktop/src/features/mesh-compute/hooks/useMeshExperimentalCatalog.ts delete mode 100644 desktop/src/features/mesh-compute/ui/MeshComputeSettingsCard.tsx delete mode 100644 desktop/src/features/mesh-compute/ui/MeshExperimentalSection.tsx diff --git a/desktop/src/features/mesh-compute/classifyModelRef.test.mjs b/desktop/src/features/mesh-compute/classifyModelRef.test.mjs deleted file mode 100644 index 9876678c8a..0000000000 --- a/desktop/src/features/mesh-compute/classifyModelRef.test.mjs +++ /dev/null @@ -1,59 +0,0 @@ -import assert from "node:assert/strict"; -import test from "node:test"; - -import { classifyModelRef } from "./classifyModelRef.ts"; - -test("empty string → unknown", () => { - assert.deepEqual(classifyModelRef(""), { kind: "unknown" }); - assert.deepEqual(classifyModelRef(" "), { kind: "unknown" }); -}); - -test("hf:// prefix → huggingface", () => { - assert.deepEqual(classifyModelRef("hf://meshllm/qwen3-8b@main"), { - kind: "huggingface", - ref: "hf://meshllm/qwen3-8b@main", - }); -}); - -test("absolute path → local-path", () => { - assert.deepEqual(classifyModelRef("/Users/me/models/qwen.gguf"), { - kind: "local-path", - path: "/Users/me/models/qwen.gguf", - }); -}); - -test("relative path with ./ → local-path", () => { - assert.deepEqual(classifyModelRef("./models/qwen.gguf"), { - kind: "local-path", - path: "./models/qwen.gguf", - }); -}); - -test("home shortcut → local-path", () => { - assert.deepEqual(classifyModelRef("~/models/qwen.gguf"), { - kind: "local-path", - path: "~/models/qwen.gguf", - }); -}); - -test(".gguf extension without path prefix → local-path", () => { - // Bare filename ending in .gguf — user clearly means a file. - assert.deepEqual(classifyModelRef("my-model.gguf"), { - kind: "local-path", - path: "my-model.gguf", - }); -}); - -test("plain name → catalog", () => { - assert.deepEqual(classifyModelRef("Qwen3-8B-Q4_K_M"), { - kind: "catalog", - name: "Qwen3-8B-Q4_K_M", - }); -}); - -test("trims whitespace before classifying", () => { - assert.deepEqual(classifyModelRef(" Qwen3-8B-Q4_K_M "), { - kind: "catalog", - name: "Qwen3-8B-Q4_K_M", - }); -}); diff --git a/desktop/src/features/mesh-compute/classifyModelRef.ts b/desktop/src/features/mesh-compute/classifyModelRef.ts deleted file mode 100644 index 666c56b309..0000000000 --- a/desktop/src/features/mesh-compute/classifyModelRef.ts +++ /dev/null @@ -1,43 +0,0 @@ -/** - * Classification of a free-text model ref entered into the serve card. - * Mirrors mesh's own resolution categories for input validation. - */ -export type ModelRefKind = - | { kind: "catalog"; name: string } - | { kind: "huggingface"; ref: string } - | { kind: "local-path"; path: string } - | { kind: "unknown" }; - -/** - * Classify a model-ref string the way mesh-llm's runtime does: - * - `hf://…` → HuggingFace ref - * - starts with `/` or `./` or `~`, OR ends with `.gguf` → local file - * - otherwise non-empty → catalog name - * - empty/whitespace → unknown - * - * Source: mesh runtime/mod.rs:3390 ("local file, catalog name, or HuggingFace URL"). - * - * This is validation-only — canonical resolution happens server-side via - * `mesh_start_node`. - */ -export function classifyModelRef(raw: string): ModelRefKind { - const trimmed = raw.trim(); - if (trimmed.length === 0) { - return { kind: "unknown" }; - } - if (trimmed.startsWith("hf://")) { - return { kind: "huggingface", ref: trimmed }; - } - // Local path heuristics. Conservative: only mark as path when there are - // unambiguous signals (leading separator, home shortcut, .gguf extension). - const looksLikePath = - trimmed.startsWith("/") || - trimmed.startsWith("./") || - trimmed.startsWith("../") || - trimmed.startsWith("~") || - trimmed.toLowerCase().endsWith(".gguf"); - if (looksLikePath) { - return { kind: "local-path", path: trimmed }; - } - return { kind: "catalog", name: trimmed }; -} diff --git a/desktop/src/features/mesh-compute/experimentalModel.test.mjs b/desktop/src/features/mesh-compute/experimentalModel.test.mjs deleted file mode 100644 index 76cdcd4787..0000000000 --- a/desktop/src/features/mesh-compute/experimentalModel.test.mjs +++ /dev/null @@ -1,119 +0,0 @@ -import assert from "node:assert/strict"; -import { test } from "node:test"; - -import { - describeExperimentalEmpty, - describeSplitEntry, - deriveMeshExperimentalModel, -} from "./experimentalModel.ts"; - -function entry(overrides = {}) { - return { - name: "Model-Q4", - sizeLabel: "20GB", - sizeGb: 20, - layerCount: 64, - packageRepo: "meshllm/Model-layers", - fitsLocally: false, - description: null, - ...overrides, - }; -} - -function catalog(overrides = {}) { - return { entries: [], vramGb: 96, stale: false, ...overrides }; -} - -test("only models beyond this machine are experimental", () => { - // A split-capable model that fits locally is not an experimental choice -- - // Qwen3-8B is 5GB and split-capable, and mesh-llm returns SingleNodeFit for - // it before ever considering a split. Offering it here would tell someone a - // 5GB model needs a cluster. - const model = deriveMeshExperimentalModel( - catalog({ - entries: [ - entry({ name: "Fits", sizeGb: 5, fitsLocally: true }), - entry({ name: "Beyond", sizeGb: 300, fitsLocally: false }), - ], - }), - ); - assert.equal(model.options.length, 1); - assert.equal(model.options[0].entry.name, "Beyond"); -}); - -test("a machine that fits everything is told so, not shown a failure", () => { - // The good outcome must not read like an error. - const model = deriveMeshExperimentalModel( - catalog({ entries: [entry({ fitsLocally: true })] }), - ); - assert.equal(model.options.length, 0); - assert.equal(model.emptyReason, "machineFitsEverything"); - assert.match(describeExperimentalEmpty(model.emptyReason), /on its own/); -}); - -test("empty has distinct causes that never read alike", () => { - // "Could not read the catalog", "the catalog has no packages", and "this - // machine is big enough" are three different facts. - assert.equal(deriveMeshExperimentalModel(undefined).emptyReason, "loading"); - assert.equal( - deriveMeshExperimentalModel(null).emptyReason, - "catalogUnavailable", - ); - assert.equal( - deriveMeshExperimentalModel(catalog()).emptyReason, - "noPackages", - ); - assert.equal( - deriveMeshExperimentalModel(catalog({ stale: true })).emptyReason, - "catalogUnavailable", - ); - - const messages = new Set( - [ - "loading", - "catalogUnavailable", - "noPackages", - "machineFitsEverything", - ].map(describeExperimentalEmpty), - ); - assert.equal(messages.size, 4, "each cause needs its own sentence"); -}); - -test("detail states size and layers, and never invents either", () => { - assert.equal(describeSplitEntry(entry()), "20 GB · 64 layers"); - // Two upstream entries publish "30 layers" where bytes belong. Layers are the - // unit actually distributed, so they stand alone; a size is never synthesized - // from a layer count, because they are not convertible. - assert.equal( - describeSplitEntry( - entry({ sizeGb: null, sizeLabel: "30 layers", layerCount: 30 }), - ), - "30 layers", - ); - // Neither figure published: fall back to the raw label rather than inventing. - assert.equal( - describeSplitEntry( - entry({ sizeGb: null, sizeLabel: "unknown", layerCount: null }), - ), - "unknown", - ); -}); - -test("a large model with no published size still reaches the list", () => { - // Unknown size is reported as not fitting, which is the honest direction: - // offering the split path is recoverable, promising a local start is not. - const model = deriveMeshExperimentalModel( - catalog({ - entries: [ - entry({ - sizeGb: null, - sizeLabel: "61 layers", - layerCount: 61, - fitsLocally: false, - }), - ], - }), - ); - assert.equal(model.options.length, 1); - assert.equal(model.options[0].detail, "61 layers"); -}); diff --git a/desktop/src/features/mesh-compute/experimentalModel.ts b/desktop/src/features/mesh-compute/experimentalModel.ts deleted file mode 100644 index 4b8e34b60f..0000000000 --- a/desktop/src/features/mesh-compute/experimentalModel.ts +++ /dev/null @@ -1,133 +0,0 @@ -import type { - MeshExperimentalCatalog, - MeshSplitEntry, -} from "@/shared/api/tauriMesh"; - -/** - * Pure projection for the experimental (split) tier of Advanced settings. - * - * ## Why "beyond this machine" is the partition - * - * Splittability alone is not the interesting fact. A model can carry a layer - * package and still fit on one machine — `Qwen3-8B-Q4_K_M` is 5 GB and - * split-capable — and mesh-llm agrees about precedence: - * `evaluate_model_target_capacity` returns `SingleNodeFit` first and only falls - * back to `SplitCandidate` when no single node can hold the model. - * - * So offering every split-capable model here would tell someone a 5 GB model - * needs a cluster. The experimental tier is the intersection: split-capable - * **and** beyond local memory. Anything that fits belongs in the ordinary - * picker, where it starts immediately. - * - * ## Why the consequence is stated before selection, not after - * - * A collective model does not load on demand. mesh-llm gathers a cohort over - * gossip and holds a settle barrier — at least two participants, membership - * unchanged for a dwell period — then elects a coordinator deterministically by - * VRAM. Until that forms, a node named for a collective model sits waiting. - * - * Nothing about that looks different from a slow download, so a person who was - * not told would reasonably read it as a hang and file a bug. The disclosure is - * therefore part of the option, not a footnote after the fact. - */ - -export type MeshExperimentalOption = { - entry: MeshSplitEntry; - /** e.g. "134 GB · 80 layers", or "61 layers" when no byte size is published. */ - detail: string; -}; - -export type MeshExperimentalModel = { - /** Split-capable models that exceed this machine — the experimental offer. */ - options: MeshExperimentalOption[]; - /** - * Why the list is empty, or null when it has entries. - * - * An empty list has three quite different causes and they must not read - * alike: the catalog could not be read, it holds no packages at all, or this - * machine is simply large enough that nothing needs splitting. The third is - * good news and must not look like a failure. - */ - emptyReason: - | "loading" - | "catalogUnavailable" - | "noPackages" - | "machineFitsEverything" - | null; -}; - -/** Bytes → GB label, matching the rest of the mesh surfaces. */ -function formatGb(gb: number): string { - return `${gb >= 10 ? Math.round(gb) : Math.round(gb * 10) / 10} GB`; -} - -/** - * One line describing reach: size where known, layers always. - * - * Layers are the unit actually distributed, so they are the honest figure when - * the catalog publishes a layer count in place of bytes (two upstream entries - * do). Never synthesizes a size from a layer count — they are not convertible. - */ -export function describeSplitEntry(entry: MeshSplitEntry): string { - const parts: string[] = []; - if (entry.sizeGb !== null) { - parts.push(formatGb(entry.sizeGb)); - } - if (entry.layerCount !== null) { - parts.push(`${entry.layerCount} layers`); - } - if (parts.length === 0) { - // Neither figure published. Say nothing rather than invent a number. - return entry.sizeLabel ?? ""; - } - return parts.join(" · "); -} - -export function deriveMeshExperimentalModel( - catalog: MeshExperimentalCatalog | null | undefined, -): MeshExperimentalModel { - if (catalog === undefined) { - return { options: [], emptyReason: "loading" }; - } - if (catalog === null) { - return { options: [], emptyReason: "catalogUnavailable" }; - } - if (catalog.entries.length === 0) { - return { - options: [], - emptyReason: catalog.stale ? "catalogUnavailable" : "noPackages", - }; - } - const beyondThisMachine = catalog.entries.filter( - (entry) => !entry.fitsLocally, - ); - if (beyondThisMachine.length === 0) { - // Every split-capable model fits here. Not a failure — the machine is big - // enough that splitting buys nothing. - return { options: [], emptyReason: "machineFitsEverything" }; - } - return { - options: beyondThisMachine.map((entry) => ({ - entry, - detail: describeSplitEntry(entry), - })), - emptyReason: null, - }; -} - -/** Copy for an empty experimental tier, keyed on why. */ -export function describeExperimentalEmpty( - reason: NonNullable, -): string { - switch (reason) { - case "loading": - return "Checking for models that can run across machines…"; - case "catalogUnavailable": - return "Can't reach the model catalog right now."; - case "noPackages": - return "No models in the catalog can run across machines yet."; - case "machineFitsEverything": - // Deliberately not phrased as an absence: this is the good outcome. - return "This computer can run every model in the catalog on its own."; - } -} diff --git a/desktop/src/features/mesh-compute/hooks/useMeshExperimentalCatalog.ts b/desktop/src/features/mesh-compute/hooks/useMeshExperimentalCatalog.ts deleted file mode 100644 index fe1384f9a5..0000000000 --- a/desktop/src/features/mesh-compute/hooks/useMeshExperimentalCatalog.ts +++ /dev/null @@ -1,62 +0,0 @@ -import * as React from "react"; - -import { meshExperimentalCatalog } from "@/shared/api/tauriMesh"; -import type { MeshExperimentalCatalog } from "@/shared/api/tauriMesh"; - -/** - * Load the split-capable model catalog. - * - * Fetched once rather than polled: the community catalog is a published dataset - * that changes when someone packages a model, not live state. mesh-llm caches it - * on disk for 24h, so re-asking on a timer would add nothing. - * - * `undefined` while in flight and `null` on failure are deliberately distinct — - * the experimental tier has to tell "still looking" from "couldn't read the - * catalog" from "the catalog genuinely has no packages", and all three would - * otherwise render as an unexplained empty list. - * - * Requires no running node: you have to choose what to share before you start - * sharing it. - */ -export function useMeshExperimentalCatalog(enabled: boolean): { - catalog: MeshExperimentalCatalog | null | undefined; - error: string | null; -} { - const [catalog, setCatalog] = React.useState< - MeshExperimentalCatalog | null | undefined - >(undefined); - const [error, setError] = React.useState(null); - const loaded = React.useRef(false); - - React.useEffect(() => { - // Only when the section is actually opened: the first call can download the - // catalog, which is not work to do for a screen nobody expanded. - if (!enabled || loaded.current) return; - loaded.current = true; - let cancelled = false; - void (async () => { - try { - const next = await meshExperimentalCatalog(); - if (!cancelled) { - setCatalog(next); - setError(null); - } - } catch (err) { - if (!cancelled) { - // null, not an empty catalog: "could not read" must not masquerade as - // "nothing published". - setCatalog(null); - setError(err instanceof Error ? err.message : String(err)); - // Allow a retry on reopen — a transient network failure should not - // permanently empty the section. - loaded.current = false; - } - } - })(); - return () => { - cancelled = true; - }; - }, [enabled]); - - return { catalog, error }; -} diff --git a/desktop/src/features/mesh-compute/ui/CommunityComputeComponents.test.mjs b/desktop/src/features/mesh-compute/ui/CommunityComputeComponents.test.mjs index 99569262d8..5824d6e970 100644 --- a/desktop/src/features/mesh-compute/ui/CommunityComputeComponents.test.mjs +++ b/desktop/src/features/mesh-compute/ui/CommunityComputeComponents.test.mjs @@ -22,8 +22,8 @@ describe("community compute visual components", () => { ); assert.match(html, /community-compute-kpi-members-sharing/); assert.match(html, /community-compute-kpi-contributed-vram/); - assert.match(html, /community-compute-kpi-vram-allocated/); - assert.match(html, /24 community members/); + assert.doesNotMatch(html, /community-compute-kpi-vram-allocated/); + assert.match(html, /24 people in this community/); assert.match(html, />2,744 GB 0 && "border-border/60 border-t sm:border-l sm:border-t-0", )} - data-testid={`community-compute-kpi-${kpiTestId(item.label)}`} + data-testid={`community-compute-kpi-${item.testId}`} key={item.label} >
@@ -77,7 +71,3 @@ function formatCapacity(value: number | null): string { if (value === null) return "—"; return `${new Intl.NumberFormat(undefined, { maximumFractionDigits: 1 }).format(value)} GB`; } - -function kpiTestId(label: string): string { - return label.toLowerCase().replaceAll(" ", "-"); -} diff --git a/desktop/src/features/mesh-compute/ui/MeshComputeCommunityView.tsx b/desktop/src/features/mesh-compute/ui/MeshComputeCommunityView.tsx index 67692c6797..dfd1ca86b3 100644 --- a/desktop/src/features/mesh-compute/ui/MeshComputeCommunityView.tsx +++ b/desktop/src/features/mesh-compute/ui/MeshComputeCommunityView.tsx @@ -14,9 +14,11 @@ export function MeshComputeCommunityView({ error, onRefresh, snapshot, + isPreparing = false, }: { communityName: string; error: string | null; + isPreparing?: boolean; onRefresh: () => void; snapshot: MeshSnapshot | CommunityComputeSnapshotInput | null; }) { @@ -43,13 +45,15 @@ export function MeshComputeCommunityView({

{model.deployments.length === 1 - ? "Your community’s first compute node" + ? "The mesh starts here" : "Community mesh"}

- {model.deployments.length === 1 - ? "This machine is the foundation of the mesh. More contributors add capacity, model choice, and resilience." - : "See the compute currently available to your community. Larger territories offer more model memory."} + {isPreparing + ? "Your contribution is being prepared and will appear here when it is ready." + : model.deployments.length === 1 + ? "One tile is a beginning. Every contributor makes the mesh more capable and resilient." + : "Each tile is compute a community member is making available."}

- {advancedOpen ? ( -
- - { - const next = e.target.value; - setShareComputeMaxVramGb(next); - }} - placeholder="No limit" - usePersonaInputStyle - value={maxVramGb} - /> - - {/* - Nested one level deeper than the memory cap: capping memory is a - normal advanced adjustment, whereas a collective model changes - whether sharing starts at all. Two clicks to reach it is the - point. - */} - -
- ) : null} - - - - {showSharingControls ? ( - -
-

Sharing

-
- - {servingIndicator.show ? ( -

- {servingIndicator.label} - {servingIndicator.detail ? ( - - {" "} - · {servingIndicator.detail} - - ) : null} -

- ) : null} -
-
-
- ) : null} -
- - - ); -} - -/** - * Renders the lifecycle/health text under the toggle. Maps Max's `state` × - * `health` matrix to honest copy — no "starting…" stuck forever when mesh - * is actually downloading weights or has failed. - */ -/** - * Live model-download progress: name, bytes, percent bar. Rendered above the - * option group while the backend streams mesh-download-progress events — - * the answer to "it just greys out while downloading". - */ -function DownloadProgressBar({ - progress, -}: { - progress: NonNullable["progress"]>; -}) { - const percent = downloadPercent(progress); - const bytes = formatDownloadBytes(progress); - return ( -
-
- - {progress.status === "preparing" ? "Preparing" : "Downloading"}{" "} - {progress.label} - - - {percent != null ? `${percent}%` : bytes || "…"} - -
- {bytes && percent != null ? ( -

- {bytes} -

- ) : null} -
-
-
-
- ); -} - -const FIT_LABEL: Record = { - comfortable: "Fits well", - tight: "Tight fit", - tradeoff: "Trade-off", - too_large: "Too large", -}; - -const FIT_CLASS: Record = { - comfortable: "text-green-600 dark:text-green-400", - tight: "text-amber-600 dark:text-amber-400", - tradeoff: "text-orange-600 dark:text-orange-400", - too_large: "text-destructive", -}; - -/** - * Share Compute's models use the agent configuration picker instead of a - * second, hand-rolled option system. The catalog remains hardware-aware, but - * its recommendations, installed models, and advanced choices now appear in - * the same searchable dropdown used when customizing an agent. - */ -function MeshModelPicker({ - catalog, - disabled, - installedModels, - isCustomModelEditing, - model, - onCustomModelEditingChange, - onModelChange, -}: { - catalog: MeshModelCatalog | null; - disabled: boolean; - installedModels: readonly MeshModelOption[]; - isCustomModelEditing: boolean; - model: string; - onCustomModelEditingChange: (editing: boolean) => void; - onModelChange: (model: string) => void; -}) { - const options = React.useMemo(() => { - const seen = new Set(); - const catalogOptions = (catalog?.entries ?? []).map((entry) => { - seen.add(entry.name); - return { - disabled: entry.fit === "too_large", - label: , - value: entry.name, - }; - }); - const localOptions = installedModels.flatMap((installed) => { - if (seen.has(installed.id)) return []; - return [ - { - label: ( -
- - {installed.name ?? installed.id} - - - Installed - -
- ), - value: installed.id, - }, - ]; - }); - return [ - ...catalogOptions, - ...localOptions, - { label: "Custom model…", value: CUSTOM_MODEL_DROPDOWN_VALUE }, - ]; - }, [catalog?.entries, installedModels]); - const knownModel = options.some((option) => option.value === model.trim()); - const showCustomModelInput = - isCustomModelEditing || (model.trim().length > 0 && !knownModel); - const selectedValue = showCustomModelInput - ? CUSTOM_MODEL_DROPDOWN_VALUE - : model.trim(); - - function handleModelChange(next: string) { - if (next === CUSTOM_MODEL_DROPDOWN_VALUE) { - onCustomModelEditingChange(true); - return; - } - onCustomModelEditingChange(false); - onModelChange(next); - } - - return ( -
- - - {showCustomModelInput ? ( - { - // A stored custom ref starts out inferred rather than explicitly - // selected. Mark it as an active custom edit before applying a - // cleared value so the field stays mounted while it is replaced. - onCustomModelEditingChange(true); - onModelChange(event.target.value); - }} - placeholder="Qwen3-8B-Q4_K_M or hf://meshllm/qwen3-8b@main" - usePersonaInputStyle - value={model} - /> - ) : null} -

- {catalog - ? `Recommended for this machine${catalog.gpuName ? ` (${catalog.gpuName}, ${catalog.vramDisplay} AI memory)` : ""}.` - : "Choose a model or enter a model reference or local file."}{" "} - Buzz downloads remote models when sharing starts. -

-
- ); -} - -function MeshModelOptionLabel({ entry }: { entry: MeshCatalogEntry }) { - return ( -
- {entry.name} - {entry.size} - - {FIT_LABEL[entry.fit]} - - {entry.recommended ? ( - - Recommended - - ) : null} - {entry.installed ? ( - - Installed - - ) : null} - {!entry.curated ? ( - - Advanced - - ) : null} -
- ); -} - -function StatusLine({ - displayModel, - isConsuming, - omitSharingVerb = false, - pendingAction, - status, -}: { - displayModel?: string; - isConsuming: boolean; - omitSharingVerb?: boolean; - pendingAction: "start" | "stop" | null; - status: MeshNodeStatus | null; -}) { - if (pendingAction === "start") { - return

Starting…

; - } - if (pendingAction === "stop") { - return

Stopping…

; - } - // A client-mode runtime owns the single slot: this machine is consuming a - // peer's compute, not sharing. The switch stays off, but remains available - // so the member can replace the client with a serving runtime. - if (isConsuming) { - return ( -

- This machine is currently using another member's shared compute. Turn on - sharing to switch to the selected local model; Buzz may briefly restart. -

- ); - } - if (!status) { - return

Checking status…

; - } - const { state, health, modelId, modelName } = status; - const modelLabel = displayModel ?? modelName ?? modelId ?? ""; - - if (state === "off") { - return null; - } - if (state === "starting") { - const reason = - health.status === "degraded" || health.status === "failed" - ? health.reason - : "Starting…"; - return

{reason}

; - } - if (state === "running") { - if (health.status === "failed") { - return ( -

- Couldn't load: {health.reason} -

- ); - } - if (health.status === "degraded") { - return ( -

- Active{modelLabel ? ` — ${modelLabel}` : ""}. {health.reason} -

- ); - } - return ( -

- {omitSharingVerb ? "" : "Sharing"} - {modelLabel ? `${omitSharingVerb ? "" : " "}${modelLabel}` : ""} with - relay members. -

- ); - } - if (state === "stopping") { - return

Stopping…

; - } - if (state === "failed") { - const reason = - health.status === "failed" || health.status === "degraded" - ? health.reason - : "Couldn't start."; - return

{reason}

; - } - return null; -} diff --git a/desktop/src/features/mesh-compute/ui/MeshComputeShareBanner.tsx b/desktop/src/features/mesh-compute/ui/MeshComputeShareBanner.tsx index b65b17a613..2957d2f554 100644 --- a/desktop/src/features/mesh-compute/ui/MeshComputeShareBanner.tsx +++ b/desktop/src/features/mesh-compute/ui/MeshComputeShareBanner.tsx @@ -1,8 +1,12 @@ -import { Cpu, LoaderCircle, ShieldCheck, Sparkles, Zap } from "lucide-react"; +import { Cpu, LoaderCircle } from "lucide-react"; -import { Switch } from "@/shared/ui/switch"; import { cn } from "@/shared/lib/cn"; +import { Switch } from "@/shared/ui/switch"; import type { useMeshComputeState } from "../hooks/useMeshComputeState"; +import { + downloadPercent, + formatDownloadBytes, +} from "../hooks/useMeshDownloadProgress"; export function MeshComputeShareBanner({ communityName, @@ -12,26 +16,17 @@ export function MeshComputeShareBanner({ mesh: ReturnType; }) { const { - error, - memory, - modelToShare, + downloadProgress, pendingAction, shareSwitchChecked, - status, toggle, setSharing, } = mesh; const busy = pendingAction !== null; - const model = status?.modelName ?? status?.modelId ?? modelToShare; - const capacity = memory.totalGb == null ? null : formatGb(memory.totalGb); - const statusCopy = shareStatusCopy({ - communityName, - modelToShare, - pendingAction, - statusModel: status?.modelName ?? status?.modelId ?? null, - isConsuming: toggle.isConsuming, - isSharing: toggle.isSharing, - }); + const progress = downloadProgress ? downloadPercent(downloadProgress) : null; + const progressDetail = downloadProgress + ? formatDownloadBytes(downloadProgress) + : ""; return (
- Automatic contribution + Share compute

- Let Buzz choose the most useful compatible model and make this - machine available when {communityName} needs it. + Contribute this machine’s spare capacity so {communityName}’s agents + have more compute available. Pause anytime.

-
- - - -
- -
-

- {statusCopy} +

+

+ {statusCopy(mesh, communityName)}

- {error ? ( + {downloadProgress ? ( +
+
+ + {downloadProgress.status === "preparing" + ? "Preparing" + : "Downloading"}{" "} + {downloadProgress.label} + + + {progress !== null + ? `${progress}%` + : progressDetail || "Working…"} + +
+ {progressDetail && progress !== null ? ( +

+ {progressDetail} +

+ ) : null} +
+
+
+
+ ) : null} + {mesh.error ? (

- {error} + {mesh.error}

) : null}
@@ -114,58 +119,23 @@ export function MeshComputeShareBanner({ ); } -function Benefit({ - icon: Icon, - label, - detail, -}: { - icon: typeof Sparkles; - label: string; - detail: string; -}) { - return ( -
- -
-

{label}

-

- {detail} -

-
-
- ); -} - -function shareStatusCopy({ - communityName, - isConsuming, - isSharing, - modelToShare, - pendingAction, - statusModel, -}: { - communityName: string; - isConsuming: boolean; - isSharing: boolean; - modelToShare: string | null; - pendingAction: "start" | "stop" | null; - statusModel: string | null; -}): string { - if (pendingAction === "start") - return `Preparing ${statusModel ?? modelToShare ?? "a model"} for ${communityName}…`; - if (pendingAction === "stop") return "Stopping shared compute…"; - if (isSharing) - return `${statusModel ?? modelToShare ?? "A model"} is available to ${communityName}`; - if (isConsuming) - return "Using another member’s compute; enable this to contribute instead"; - if (modelToShare) - return "Ready to contribute automatically when you turn this on"; - return "Checking this machine and choosing a compatible model…"; -} - -function formatGb(value: number): string { - return `${new Intl.NumberFormat(undefined, { maximumFractionDigits: 1 }).format(value)} GB`; +function statusCopy( + mesh: ReturnType, + communityName: string, +): string { + const model = + mesh.status?.modelName ?? mesh.status?.modelId ?? mesh.modelToShare; + if (mesh.pendingAction === "start") { + return mesh.downloadProgress + ? "Downloading the files needed to contribute. Your tile will appear when this machine is ready." + : "Checking this machine and preparing shared compute. Your tile will appear when it is ready."; + } + if (mesh.pendingAction === "stop") return "Stopping shared compute…"; + if (mesh.toggle.isSharing) + return `This machine is sharing compute with ${communityName}${model ? ` using ${model}` : ""}.`; + if (mesh.toggle.isConsuming) + return "This machine is using shared compute. Turn this on to contribute too."; + if (mesh.modelToShare) + return "Ready to contribute. Buzz will choose compatible settings automatically."; + return "Checking whether this machine can contribute…"; } diff --git a/desktop/src/features/mesh-compute/ui/MeshExperimentalSection.tsx b/desktop/src/features/mesh-compute/ui/MeshExperimentalSection.tsx deleted file mode 100644 index 842839e652..0000000000 --- a/desktop/src/features/mesh-compute/ui/MeshExperimentalSection.tsx +++ /dev/null @@ -1,131 +0,0 @@ -import { ChevronDown, FlaskConical } from "lucide-react"; -import * as React from "react"; - -import { cn } from "@/shared/lib/cn"; - -import { useMeshExperimentalCatalog } from "../hooks/useMeshExperimentalCatalog"; -import { - describeExperimentalEmpty, - deriveMeshExperimentalModel, -} from "../experimentalModel"; - -/** - * The experimental tier of Advanced settings: models served across machines. - * - * ## Why this is a separate, collapsed tier - * - * Every other model in the picker starts when you flip the switch. These do not. - * mesh-llm gathers a cohort over gossip and holds a settle barrier — at least - * two participants, membership unchanged for a dwell period — then elects a - * coordinator deterministically by VRAM. Until that forms, a node named for a - * collective model waits. - * - * That waiting is indistinguishable from a slow download, so someone who was not - * warned would read it as a hang. The disclosure therefore sits above the list, - * before a choice is made, rather than as a toast afterwards. - * - * ## Why it can be empty for good reasons - * - * Three different facts produce an empty list — the catalog could not be read, - * it holds no layer packages, or this machine is large enough that nothing needs - * splitting — and the third is good news. `deriveMeshExperimentalModel` keeps - * them distinct so this never renders an unexplained blank. - */ -export function MeshExperimentalSection({ - disabled, - onModelChange, - selectedModel, -}: { - disabled: boolean; - onModelChange: (model: string) => void; - selectedModel: string; -}) { - const [open, setOpen] = React.useState(false); - // Fetch only once opened: the first call can download the catalog, which is - // not work to do for a section nobody expanded. - const { catalog } = useMeshExperimentalCatalog(open); - const model = React.useMemo( - () => deriveMeshExperimentalModel(catalog), - [catalog], - ); - const selected = selectedModel.trim(); - - return ( -
- - - {open ? ( -
- {/* - Stated before the list, not after a selection. The wait is the whole - character of these models, and it looks exactly like a hang to - anyone who was not told. - */} -

- These models are too large for this computer alone and run across - several machines at once.{" "} - - Sharing will not start until enough well-connected computers are - sharing the same model - {" "} - — it waits instead of loading. For advanced setups. -

- - {model.emptyReason ? ( -

- {describeExperimentalEmpty(model.emptyReason)} -

- ) : ( -
    - {model.options.map((option) => { - const isSelected = selected === option.entry.name; - return ( -
  • - -
  • - ); - })} -
- )} -
- ) : null} -
- ); -} diff --git a/desktop/tests/e2e/mesh-compute.spec.ts b/desktop/tests/e2e/mesh-compute.spec.ts index 9d3d4ce15e..3a9396a3f4 100644 --- a/desktop/tests/e2e/mesh-compute.spec.ts +++ b/desktop/tests/e2e/mesh-compute.spec.ts @@ -26,73 +26,32 @@ type E2eWindow = Window & { }) => void; }; -test("Share compute chooses a model before sharing", async ({ page }) => { - const modelRef = "hf://demo/SmolLM2-135M-Instruct-GGUF:Q4_K_M"; +test("Share compute uses one automatic contribution control", async ({ + page, +}) => { await installMockBridge(page); await page.goto("/"); await openSettings(page, "compute"); - await page.getByTestId("compute-tab-settings").click(); - const card = page.getByTestId("settings-mesh-share-compute"); - const toggle = page.getByTestId("mesh-share-compute-toggle"); - const model = page.getByTestId("mesh-share-compute-model"); + await expect(page.getByTestId("compute-tab-community")).toHaveCount(0); + await expect(page.getByTestId("compute-tab-settings")).toHaveCount(0); + await expect(page.getByTestId("settings-mesh-share-compute")).toHaveCount(0); - await expect(card).not.toContainText("Not sharing right now"); - await expect( - page.getByTestId("mesh-share-compute-options-motion"), - ).toHaveCount(0); - await expect( - page.getByTestId("mesh-share-compute-sharing-status"), - ).toHaveCount(0); - await expect(model).toBeVisible(); + const toggle = page.getByTestId("compute-share-banner-toggle"); await expect(toggle).toBeEnabled(); - await model.click(); - await page.getByRole("option", { name: "Custom model…" }).click(); - await page.getByLabel("Custom model reference").fill(modelRef); - await toggle.click(); - await expect( - page.getByTestId("mesh-share-compute-options-motion"), - ).toBeVisible(); - await expect( - page.getByTestId("mesh-share-compute-sharing-status"), - ).toBeVisible(); - await expect(model).toBeVisible(); - await expect(card).toContainText( - "Buzz downloads remote models when sharing starts", - ); await expect(toggle).toBeChecked(); - await expect( - page.getByTestId("mesh-share-compute-sharing-status"), - ).toContainText("SmolLM2 135M with relay members"); + await expect(page.getByTestId("compute-share-banner-status")).toContainText( + "Your tile will appear when it is ready", + ); await expect .poll(() => page.evaluate(() => (window as E2eWindow).__BUZZ_E2E_COMMANDS__ ?? []), ) .toContain("mesh_start_node"); - await expect - .poll(() => - page.evaluate( - () => (window as E2eWindow).__BUZZ_E2E_COMMAND_PAYLOADS__ ?? [], - ), - ) - .toContainEqual({ - command: "mesh_start_node", - payload: { - request: { mode: "serve", modelId: modelRef }, - }, - }); await toggle.click(); await expect(toggle).not.toBeChecked(); - await expect(card).not.toContainText("Not sharing right now"); - await expect( - page.getByTestId("mesh-share-compute-options-motion"), - ).toHaveCount(0); - await expect( - page.getByTestId("mesh-share-compute-sharing-status"), - ).toHaveCount(0); - await expect(model).toBeVisible(); await expect .poll(() => page.evaluate(() => (window as E2eWindow).__BUZZ_E2E_COMMANDS__ ?? []), @@ -100,67 +59,6 @@ test("Share compute chooses a model before sharing", async ({ page }) => { .toContain("mesh_stop_node"); }); -test("a consuming client can switch to sharing its saved local model", async ({ - page, -}) => { - // Regression: consuming someone else's shared compute starts a client-mode - // node in the single runtime slot, which reports state:"running". The Share - // toggle keyed off state alone and lit up. A later guard overcorrected by - // disabling the switch and copying the remote model over the local sharing - // choice. Keep the switch off, preserve the local model, then replace the - // client with one serve start (never a stop command). - const localModel = "hf://demo/local-small-model:Q4_K_M"; - await page.addInitScript((model) => { - window.localStorage.setItem("buzz.mesh-compute.share.model.v1", model); - }, localModel); - await installMockBridge(page); - await page.goto("/"); - // The mesh seed hook is installed when the mock bridge boots; calling it - // before then silently no-ops (optional chaining) and the seed is lost. - await page.waitForFunction( - () => typeof (window as E2eWindow).__BUZZ_E2E_SET_MESH__ === "function", - ); - await page.evaluate(() => { - (window as E2eWindow).__BUZZ_E2E_SET_MESH__?.({ - nodeState: "running", - nodeMode: "client", - }); - }); - await openSettings(page, "compute"); - await page.getByTestId("compute-tab-settings").click(); - - const card = page.getByTestId("settings-mesh-share-compute"); - const toggle = page.getByTestId("mesh-share-compute-toggle"); - await expect(card).toContainText( - "This machine is currently using another member's shared compute", - ); - await expect(card).toContainText("Buzz may briefly restart"); - await expect(toggle).not.toBeChecked(); - await expect( - page.getByTestId("mesh-share-compute-options-motion"), - ).toHaveCount(0); - await expect(toggle).toBeEnabled(); - const customModel = page.getByLabel("Custom model reference"); - await expect(customModel).toHaveValue(localModel); - await customModel.fill(""); - await expect(customModel).toBeVisible(); - await customModel.fill("hf://demo/replacement-model:Q4_K_M"); - await toggle.click(); - await expect(toggle).toBeChecked(); - - const commands = await page.evaluate(() => ({ - names: (window as E2eWindow).__BUZZ_E2E_COMMANDS__ ?? [], - payloads: (window as E2eWindow).__BUZZ_E2E_COMMAND_PAYLOADS__ ?? [], - })); - expect(commands.names).not.toContain("mesh_stop_node"); - expect(commands.payloads).toContainEqual({ - command: "mesh_start_node", - payload: { - request: { mode: "serve", modelId: "hf://demo/replacement-model:Q4_K_M" }, - }, - }); -}); - test("Shared Compute is only available from Settings", async ({ page }) => { await installMockBridge(page); await page.goto("/"); @@ -215,9 +113,6 @@ test("Community Compute keeps the live community topology focused", async ({ await expect( page.getByTestId("community-compute-kpi-contributed-vram"), ).toContainText("96 GB"); - await expect( - page.getByTestId("community-compute-kpi-vram-allocated"), - ).toContainText("42%"); await expect( page.getByTestId("community-compute-territory-map"), ).toBeVisible();