mirror of
https://github.com/block/buzz.git
synced 2026-08-18 06:50:31 +02:00
feat(mesh): honest empty state and named startup stages
Two idle situations shared one branch and read identically, which was the
confusion: a community with compute to offer is an invitation, while an empty
one is a call to be first. They now say different things, and neither is an
error — an empty mesh is a normal state.
A machine that cannot host anything gets its own line rather than an
instruction it cannot follow: it can still consume shared compute once somebody
else shares. This pairs with the fit-aware recommendation, which now returns
None for such machines instead of a model that would not load.
An unfetched snapshot is still distinguished from a genuinely empty one, so
"be the first" never renders as a verdict on other people's machines during
startup.
Startup was one opaque "Starting to share…" covering resolve, download, and
load. The download dominates — minutes for a multi-gigabyte model — so it is
now named and measured ("Downloading model · 29%", "5 GB of 17 GB · first run
only"). A download with no known total says "Downloading model…" rather than
fabricating 0%.
Signed-off-by: Michael Neale <michael.neale@gmail.com>
This commit is contained in:
@@ -17,6 +17,8 @@ import {
|
||||
describeMeshCapacity,
|
||||
describeMeshHeadline,
|
||||
describeReadyModels,
|
||||
describeStartupHeadline,
|
||||
describeStartupStage,
|
||||
deriveMeshCardModel,
|
||||
formatCapacityGb,
|
||||
shortModelLabel,
|
||||
@@ -103,6 +105,7 @@ function derive(overrides = {}) {
|
||||
view: null,
|
||||
usage: usage(),
|
||||
inboundWork: false,
|
||||
downloadProgress: null,
|
||||
...overrides,
|
||||
});
|
||||
}
|
||||
@@ -350,3 +353,85 @@ test("an unknown runtime occupant is not silently replaceable", () => {
|
||||
});
|
||||
assert.equal(model.switchDisabled, true);
|
||||
});
|
||||
|
||||
function progress(overrides = {}) {
|
||||
return {
|
||||
label: "gemma",
|
||||
file: "model.gguf",
|
||||
downloadedBytes: null,
|
||||
totalBytes: null,
|
||||
status: "downloading",
|
||||
done: false,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
test("a download is named and measured, not hidden behind 'Starting…'", () => {
|
||||
// Minutes of unlabelled spinner reads as a hang; the download is the longest
|
||||
// and most opaque startup step, so it says so.
|
||||
assert.equal(
|
||||
describeStartupHeadline(
|
||||
progress({ downloadedBytes: 5e9, totalBytes: 17e9 }),
|
||||
),
|
||||
"Downloading model · 29%",
|
||||
);
|
||||
assert.match(
|
||||
describeStartupStage(progress({ downloadedBytes: 5e9, totalBytes: 17e9 })),
|
||||
/5 GB of 17 GB/,
|
||||
);
|
||||
});
|
||||
|
||||
test("a download with no known total says so rather than faking 0%", () => {
|
||||
assert.equal(describeStartupHeadline(progress()), "Downloading model…");
|
||||
assert.equal(
|
||||
describeStartupHeadline(progress({ downloadedBytes: 1e9, totalBytes: 0 })),
|
||||
"Downloading model…",
|
||||
);
|
||||
});
|
||||
|
||||
test("each startup stage is distinguishable", () => {
|
||||
assert.equal(
|
||||
describeStartupHeadline(progress({ status: "preparing" })),
|
||||
"Preparing model…",
|
||||
);
|
||||
assert.equal(describeStartupHeadline(null), "Starting to share…");
|
||||
assert.match(describeStartupStage(null), /Loading the model into memory/);
|
||||
});
|
||||
|
||||
test("starting shows the download stage on the card", () => {
|
||||
const model = derive({
|
||||
pendingAction: "start",
|
||||
downloadProgress: progress({ downloadedBytes: 2e9, totalBytes: 4e9 }),
|
||||
});
|
||||
assert.equal(model.tone, "pending");
|
||||
assert.equal(model.headline, "Downloading model · 50%");
|
||||
});
|
||||
|
||||
test("an empty community is a call to be first, not an error", () => {
|
||||
const model = derive({
|
||||
snapshot: snapshot({ sharingDeviceCount: 0, sharedCapacityGb: null }),
|
||||
});
|
||||
assert.equal(model.tone, "idle");
|
||||
assert.match(model.detail, /Be the first to share/);
|
||||
});
|
||||
|
||||
test("a machine that cannot host is told it can still consume", () => {
|
||||
// Saying "turn it on" to someone who cannot is a dead end; consuming still
|
||||
// works once somebody else shares.
|
||||
const model = derive({
|
||||
snapshot: snapshot({ sharingDeviceCount: 0, sharedCapacityGb: null }),
|
||||
canShare: false,
|
||||
});
|
||||
assert.match(model.detail, /too small to share, but can use shared compute/);
|
||||
assert.equal(model.switchDisabled, true);
|
||||
});
|
||||
|
||||
test("an unfetched snapshot is not treated as an empty community", () => {
|
||||
// null is "not checked yet" — claiming "be the first" before the relay
|
||||
// answers would be a verdict on everyone else's machines.
|
||||
const model = derive({ snapshot: null });
|
||||
assert.ok(
|
||||
!/Be the first/.test(model.detail ?? ""),
|
||||
`must not claim empty before fetching: ${model.detail}`,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -5,6 +5,7 @@ import type {
|
||||
MeshSnapshot,
|
||||
MeshSnapshotDevice,
|
||||
} from "@/shared/api/tauriMesh";
|
||||
import type { MeshDownloadProgress } from "./hooks/useMeshDownloadProgress";
|
||||
import { describeParticipationHint } from "./meshActivity";
|
||||
import type { MeshShareToggleModel } from "./shareToggleState";
|
||||
|
||||
@@ -145,6 +146,62 @@ export function describeReadyModels(
|
||||
return `${models.length} models ready`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Name the startup stage instead of showing one opaque "Starting…".
|
||||
*
|
||||
* A first-time start does three very different things behind one spinner:
|
||||
* resolve the model, download several gigabytes, then load it into memory. The
|
||||
* download dominates — minutes, not seconds — and an unlabelled spinner during
|
||||
* it reads as a hang. So the download is named and measured; everything else
|
||||
* stays honest about being indeterminate.
|
||||
*/
|
||||
export function describeStartupHeadline(
|
||||
progress: MeshDownloadProgress | null,
|
||||
): string {
|
||||
if (progress?.status === "downloading") {
|
||||
const pct = downloadPercent(progress);
|
||||
return pct === null ? "Downloading model…" : `Downloading model · ${pct}%`;
|
||||
}
|
||||
if (progress?.status === "preparing") {
|
||||
return "Preparing model…";
|
||||
}
|
||||
return "Starting to share…";
|
||||
}
|
||||
|
||||
/** Secondary line for the startup stages. */
|
||||
export function describeStartupStage(
|
||||
progress: MeshDownloadProgress | null,
|
||||
): string {
|
||||
if (progress?.status === "downloading") {
|
||||
const total = progress.totalBytes;
|
||||
return total === null
|
||||
? "First run downloads the model once."
|
||||
: `${formatBytes(progress.downloadedBytes ?? 0)} of ${formatBytes(total)} · first run only`;
|
||||
}
|
||||
if (progress?.status === "preparing") {
|
||||
return "Checking what's already downloaded.";
|
||||
}
|
||||
return "Loading the model into memory.";
|
||||
}
|
||||
|
||||
function downloadPercent(progress: MeshDownloadProgress): number | null {
|
||||
const { downloadedBytes, totalBytes } = progress;
|
||||
// A percentage needs both figures and a non-zero denominator; without them a
|
||||
// bare "Downloading…" beats a fabricated 0%.
|
||||
if (downloadedBytes === null || totalBytes === null || totalBytes <= 0) {
|
||||
return null;
|
||||
}
|
||||
return Math.min(100, Math.floor((downloadedBytes / totalBytes) * 100));
|
||||
}
|
||||
|
||||
function formatBytes(bytes: number): string {
|
||||
const gb = bytes / 1e9;
|
||||
if (gb >= 1) {
|
||||
return `${Math.round(gb * 10) / 10} GB`;
|
||||
}
|
||||
return `${Math.round(bytes / 1e6)} MB`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trim a model reference down to something that fits a 256px sidebar.
|
||||
* `unsloth/gemma-4-26B-A4B-it-GGUF:UD-Q4_K_M` → `Gemma 4 26B A4B`.
|
||||
@@ -178,6 +235,7 @@ export function deriveMeshCardModel({
|
||||
view,
|
||||
usage,
|
||||
inboundWork,
|
||||
downloadProgress,
|
||||
}: {
|
||||
snapshot: MeshSnapshot | null;
|
||||
status: MeshNodeStatus | null;
|
||||
@@ -194,6 +252,14 @@ export function deriveMeshCardModel({
|
||||
* count flat). Sampled, so it can undercount; it never over-claims.
|
||||
*/
|
||||
inboundWork: boolean;
|
||||
/**
|
||||
* Live model-download progress, when a download is running.
|
||||
*
|
||||
* Without this, a first-time start shows "Starting to share…" for however
|
||||
* long a multi-gigabyte download takes, which reads as a hang. A download is
|
||||
* the single longest and most opaque step, so it gets named and measured.
|
||||
*/
|
||||
downloadProgress: MeshDownloadProgress | null;
|
||||
}): MeshCardModel {
|
||||
const devices = snapshot?.devices ?? [];
|
||||
const headline = describeMeshHeadline({ view, snapshot });
|
||||
@@ -201,6 +267,7 @@ export function deriveMeshCardModel({
|
||||
// Solo means "connected but nobody else is here" — a live-view fact. The
|
||||
// relay snapshot cannot tell us this: a lone note may just be a stale one.
|
||||
const isSolo = view?.connected === true && view.peers.length === 0;
|
||||
const startingDetail = describeStartupStage(downloadProgress);
|
||||
const hint = describeParticipationHint({
|
||||
isSharing: toggle.isSharing,
|
||||
isConsuming: toggle.isConsuming,
|
||||
@@ -230,8 +297,8 @@ export function deriveMeshCardModel({
|
||||
return {
|
||||
...base,
|
||||
tone: "pending",
|
||||
headline: "Starting to share…",
|
||||
detail: "Loading the model. This can take a moment.",
|
||||
headline: describeStartupHeadline(downloadProgress),
|
||||
detail: startingDetail,
|
||||
showSoloHint: false,
|
||||
};
|
||||
}
|
||||
@@ -275,8 +342,8 @@ export function deriveMeshCardModel({
|
||||
return {
|
||||
...base,
|
||||
tone: "pending",
|
||||
headline: "Starting to share…",
|
||||
detail: "Loading the model. This can take a moment.",
|
||||
headline: describeStartupHeadline(downloadProgress),
|
||||
detail: startingDetail,
|
||||
showSoloHint: false,
|
||||
};
|
||||
}
|
||||
@@ -291,8 +358,30 @@ export function deriveMeshCardModel({
|
||||
};
|
||||
}
|
||||
|
||||
// Idle: the invitation. Lead with what the community already has, because
|
||||
// that is the reason to join — not with a description of the mechanism.
|
||||
// Idle. Two very different situations share this branch, and conflating them
|
||||
// was the old bug: a community with compute to offer is an invitation, while
|
||||
// an empty one is a call to be first. Neither is an error.
|
||||
const communityIsEmpty =
|
||||
snapshot !== null && snapshot.sharingDeviceCount === 0;
|
||||
if (communityIsEmpty) {
|
||||
return {
|
||||
...base,
|
||||
tone: "idle",
|
||||
headline,
|
||||
// No hedging about what *might* be available: nobody is sharing, so the
|
||||
// only true statement is that turning this on creates the capacity.
|
||||
detail: canShare
|
||||
? "Be the first to share compute here."
|
||||
: // A machine that cannot host anything is not broken, and saying
|
||||
// "turn it on" to someone who cannot is a dead end. It can still
|
||||
// consume once somebody else shares.
|
||||
"This computer is too small to share, but can use shared compute.",
|
||||
showSoloHint: false,
|
||||
};
|
||||
}
|
||||
|
||||
// The community has compute. Lead with what it already has, because that is
|
||||
// the reason to join — not with a description of the mechanism.
|
||||
return {
|
||||
...base,
|
||||
tone: "idle",
|
||||
|
||||
@@ -8,6 +8,7 @@ import { meshStartNode, meshStopNode } from "@/shared/api/tauriMesh";
|
||||
import type { MeshModelCatalog } from "@/shared/api/tauriMesh";
|
||||
import { meshModelCatalog } from "@/shared/api/tauriMesh";
|
||||
|
||||
import { useMeshDownloadProgress } from "../hooks/useMeshDownloadProgress";
|
||||
import { useMeshLiveView } from "../hooks/useMeshLiveView";
|
||||
import { useMeshNodeStatus } from "../hooks/useMeshNodeStatus";
|
||||
import { useMeshServingUsage } from "../hooks/useMeshServingUsage";
|
||||
@@ -95,6 +96,8 @@ export function SidebarMeshComputeCard({
|
||||
// Live gossip view: peers we are actually connected to. Only meaningful while
|
||||
// a runtime exists, so it is gated on slot occupancy.
|
||||
const { view } = useMeshLiveView(toggle.isSharing || toggle.isConsuming);
|
||||
const { progress: downloadProgress, reset: resetDownloadProgress } =
|
||||
useMeshDownloadProgress();
|
||||
|
||||
// Inbound work has no counter in mesh-llm, so it is inferred by elimination
|
||||
// across two samples: serving, inflight > 0, and our own dispatch count flat
|
||||
@@ -144,6 +147,7 @@ export function SidebarMeshComputeCard({
|
||||
view,
|
||||
usage,
|
||||
inboundWork,
|
||||
downloadProgress,
|
||||
});
|
||||
|
||||
async function handleToggle(next: boolean) {
|
||||
@@ -169,6 +173,9 @@ export function SidebarMeshComputeCard({
|
||||
setActionError(err instanceof Error ? err.message : String(err));
|
||||
} finally {
|
||||
setPendingAction(null);
|
||||
// Clear a lingering final download event so the next start does not begin
|
||||
// by replaying the last run's progress.
|
||||
resetDownloadProgress();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user