diff --git a/desktop/src/features/mesh-compute/meshDetailModel.test.mjs b/desktop/src/features/mesh-compute/meshDetailModel.test.mjs index 8adb35740..ca5f70996 100644 --- a/desktop/src/features/mesh-compute/meshDetailModel.test.mjs +++ b/desktop/src/features/mesh-compute/meshDetailModel.test.mjs @@ -327,3 +327,22 @@ test("the card hint distinguishes giving, taking, and neither", () => { "3 of 4 requests ran on shared compute", ); }); + +test("the cold popover explains the switch instead of restating a zero", () => { + // "0 sharing in this community" is a status line with no status. Someone + // opening this for the first time needs to know what the switch beside it + // does, which is the one thing a zero cannot tell them. + const model = deriveMeshDetailModel({ + view: null, + snapshot: snapshot({ + sharingDeviceCount: 0, + sharedCapacityGb: null, + devices: [], + }), + usage: null, + isSharing: false, + inboundWork: false, + }); + assert.match(model.participationLabel, /Share your spare compute/); + assert.doesNotMatch(model.participationLabel, /^0 /); +}); diff --git a/desktop/src/features/mesh-compute/meshDetailModel.ts b/desktop/src/features/mesh-compute/meshDetailModel.ts index 3b06d4760..37cb5770c 100644 --- a/desktop/src/features/mesh-compute/meshDetailModel.ts +++ b/desktop/src/features/mesh-compute/meshDetailModel.ts @@ -31,6 +31,13 @@ import { describeRequestOrigin } from "./meshActivity"; * while our own dispatch count stays flat, means the work is not ours. See * `inferInboundWork` in `meshActivity.ts`. It is sampled, so it can undercount * — but it never over-claims, which is the direction that matters. + * + * ## The cold state explains, it does not report + * + * With nothing shared anywhere there is no status worth stating, so the subtitle + * says what the switch does rather than restating a zero. Every other state has + * a real number, and an explanation sitting permanently beside those would be + * furniture. */ export type MeshDetailModel = { @@ -133,9 +140,12 @@ export function deriveMeshDetailModel({ ? peers.length === 0 ? "No other devices yet" : `${peers.length} ${peers.length === 1 ? "peer" : "peers"} connected` - : // Not participating: the relay snapshot is all we have, and it is the - // reason to consider joining. - `${snapshot?.sharingDeviceCount ?? 0} sharing in this community`, + : (snapshot?.sharingDeviceCount ?? 0) > 0 + ? // Someone else is sharing: the count is the reason to join. + `${snapshot?.sharingDeviceCount ?? 0} sharing in this community` + : // Cold. There is no status worth reporting, so the line explains what + // the switch beside it actually does instead of restating a zero. + "Share your spare compute with the Buzz community to run models", busyNow: (usage?.inflight ?? 0) > 0, activityLabel: describeActivity({ usage, isSharing, inboundWork }), originLabel: describeRequestOrigin(usage), diff --git a/desktop/src/features/mesh-compute/meshRowModel.test.mjs b/desktop/src/features/mesh-compute/meshRowModel.test.mjs index 870bdac55..caa1fec6f 100644 --- a/desktop/src/features/mesh-compute/meshRowModel.test.mjs +++ b/desktop/src/features/mesh-compute/meshRowModel.test.mjs @@ -215,13 +215,38 @@ test("an agent that uses the mesh leaves a busy sharer showing activity", () => test("an empty mesh offers the action instead of sitting inert", () => { // Nothing to show and nothing happening: without a prompt the row is inert // and unexplained. The action is real -- you can be the first to share -- so - // the badge slot carries the word where a figure would go. + // the trailing slot carries an invitation where a figure would go. const model = derive({ snapshot: snapshot() }); assert.equal(model.tone, "unknown"); - assert.equal(model.badge, "Share"); + assert.equal(model.callToAction, "Share your compute"); + assert.equal(model.badge, null); assert.match(model.tooltip, /share this computer to start the mesh/); }); +test("the cold state drops the Buzz qualifier to make room", () => { + // "Buzz MeshLLM" plus "Share your compute" does not fit 256px. The name is + // the part that can afford to lose a word. + assert.equal(derive({ snapshot: snapshot() }).label, "MeshLLM"); + assert.equal( + derive({ + snapshot: snapshot({ sharingDeviceCount: 1, sharedCapacityGb: 36 }), + }).label, + "Buzz MeshLLM", + ); +}); + +test("only the cold state invites; every other state reports", () => { + // A permanent nudge beside a real figure becomes furniture. + for (const input of [ + { snapshot: null }, + { snapshot: snapshot({ sharingDeviceCount: 2, sharedCapacityGb: 64 }) }, + { toggle: SHARING, view: view() }, + { toggle: CONSUMING }, + ]) { + assert.equal(derive(input).callToAction, null); + } +}); + test("an empty mesh does not throb, and an unfetched one offers nothing", () => { // A community that never uses the mesh would otherwise animate forever; // permanent motion earns nothing but annoyance. And a prompt derived from no diff --git a/desktop/src/features/mesh-compute/meshRowModel.ts b/desktop/src/features/mesh-compute/meshRowModel.ts index 195b4f67c..a587e4d45 100644 --- a/desktop/src/features/mesh-compute/meshRowModel.ts +++ b/desktop/src/features/mesh-compute/meshRowModel.ts @@ -64,6 +64,11 @@ export type MeshRowPulse = "none" | "activity" | "invite"; export type MeshRowModel = { tone: MeshRowTone; + /** + * Row label. Drops the "Buzz" qualifier in the cold state, where a call to + * action shares the same 256px and every character costs. + */ + label: string; /** Accessible state sentence, used as the row tooltip. */ tooltip: string; /** @@ -78,6 +83,15 @@ export type MeshRowModel = { * "Share" — the row would otherwise be inert with nothing explaining why. */ badge: string | null; + /** + * Invitation text, or null. + * + * Only set in the cold state — no capacity anywhere, nothing running. There + * is no number to show and no status to report, so without words the row is + * inert and unexplained. Every other state has a figure or a tone that speaks + * for itself, and a permanent nudge beside them would become furniture. + */ + callToAction: string | null; /** * Why the dot moves, if it does. Never decorative: a still dot is a real * statement that there is nothing to act on and nothing happening. @@ -85,6 +99,9 @@ export type MeshRowModel = { pulse: MeshRowPulse; }; +/** Full name. Used everywhere except the cold state, which needs the room. */ +const LABEL = "Buzz MeshLLM"; + /** Live pool capacity: this machine plus every peer that reports a figure. */ function liveCapacityGb(view: MeshLiveView | null): number { if (!view?.connected) return 0; @@ -126,9 +143,11 @@ export function deriveMeshRowModel({ ) { return { tone: "starting", + label: LABEL, tooltip: pendingAction === "stop" ? "Stopping sharing…" : "Starting to share…", badge: null, + callToAction: null, pulse: "none", }; } @@ -138,8 +157,10 @@ export function deriveMeshRowModel({ if (health && health.status !== "ok") { return { tone: "failed", + label: LABEL, tooltip: "Sharing failed — open for details", badge: null, + callToAction: null, pulse: "none", }; } @@ -153,18 +174,22 @@ export function deriveMeshRowModel({ if (hasMeshAgent === false) { return { tone: "sharing", + label: LABEL, tooltip: "Sharing compute · no agent is using it yet", badge, + callToAction: null, pulse: "invite", }; } return { tone: "sharing", + label: LABEL, tooltip: peerCount > 0 ? `Sharing compute · ${peerCount} ${plural(peerCount, "peer")}` : "Sharing compute · waiting for another device", badge, + callToAction: null, pulse: busyNow ? "activity" : "none", }; } @@ -173,8 +198,10 @@ export function deriveMeshRowModel({ const gb = liveCapacityGb(view); return { tone: "consuming", + label: LABEL, tooltip: "Using shared compute from the mesh", badge: gb > 0 ? formatCapacityGb(gb) : null, + callToAction: null, pulse: busyNow ? "activity" : "none", }; } @@ -187,8 +214,12 @@ export function deriveMeshRowModel({ // would appear for one poll and retract, which reads as a glitch. return { tone: "unknown", + label: LABEL, tooltip: "Checking for shared compute…", badge: null, + // Nothing offered from no data: a prompt that appears for one poll and + // retracts reads as a glitch, not a suggestion. + callToAction: null, pulse: "none", }; } @@ -203,14 +234,18 @@ export function deriveMeshRowModel({ // annoyance. The invite throb stays reserved for capacity that exists. return { tone: "unknown", + // "Buzz" is dropped here so the invitation fits beside it. + label: "MeshLLM", tooltip: "No shared compute yet — share this computer to start the mesh", - badge: "Share", + badge: null, + callToAction: "Share your compute", pulse: "none", }; } const gb = snapshot.sharedCapacityGb; return { tone: "available", + label: LABEL, tooltip: gb === null ? `Shared compute available · ${count} ${plural(count, "device")}` @@ -221,6 +256,7 @@ export function deriveMeshRowModel({ gb === null ? `${count} ${plural(count, "device")}` : formatCapacityGb(gb), + callToAction: null, pulse: "invite", }; } diff --git a/desktop/src/features/mesh-compute/ui/MeshComputePopover.tsx b/desktop/src/features/mesh-compute/ui/MeshComputePopover.tsx index 141775344..c78a75d1e 100644 --- a/desktop/src/features/mesh-compute/ui/MeshComputePopover.tsx +++ b/desktop/src/features/mesh-compute/ui/MeshComputePopover.tsx @@ -83,7 +83,7 @@ export function MeshComputePopover({

- {model.capacityLabel ?? "Mesh capacity"} + {model.capacityLabel ?? "Buzz MeshLLM"}

- Compute settings + Advanced settings ) : null}

diff --git a/desktop/src/features/mesh-compute/ui/SidebarMeshComputeRow.tsx b/desktop/src/features/mesh-compute/ui/SidebarMeshComputeRow.tsx index 86fef77f5..ff2a42f79 100644 --- a/desktop/src/features/mesh-compute/ui/SidebarMeshComputeRow.tsx +++ b/desktop/src/features/mesh-compute/ui/SidebarMeshComputeRow.tsx @@ -1,3 +1,5 @@ +import { Share2 } from "lucide-react"; + import { SidebarMenuButton, SidebarMenuItem } from "@/shared/ui/sidebar"; import { SidebarMenuLabel } from "@/shared/ui/sidebar-menu-label"; import { cn } from "@/shared/lib/cn"; @@ -81,31 +83,47 @@ export function SidebarMeshComputeRow({ aria-hidden className="flex h-4 w-4 shrink-0 items-center justify-center" > - - {row.pulse === "none" ? null : ( - - )} - - + ) : ( + + {row.pulse === "none" ? null : ( + + )} + + + )} - Buzz MeshLLM + {row.label} {/* - Capacity, in every state that has a number — not just while sharing. + The trailing slot: an invitation when cold, capacity otherwise. + + Capacity shows in every state that has a number — not just while + sharing. This slot used to hold a 70%-scaled unlabelled Switch, which was hard to see and ambiguous about what it would do. The control moved wholesale to @@ -114,7 +132,14 @@ export function SidebarMeshComputeRow({ joining better than any exhortation, and unlike a nudge there is nothing to dismiss. Pointer-events off so the whole row stays one click target. */} - {row.badge ? ( + {row.callToAction ? ( + + {row.callToAction} + + ) : row.badge ? (