fix(mesh): show the community mesh before joining it

The popover said "Turn on sharing to see who's on the mesh." That was not just
awkward copy, it was false: mesh_snapshot reads other members' relay status
notes and needs no local runtime, so who is sharing -- and how much -- is
knowable while our own node is off. The graph was gated on a local runtime for
no reason.

The radar field now renders from whichever source this machine has, and keeps
the two honestly distinct:

  - live gossip: reachable now, RTT known, adjacency to us is real
  - relay notes: last known within 120s, no RTT, adjacency unknown

So relay nodes get no synthetic RTT (they land mid-band rather than faking
proximity), spokes are omitted when we have not joined because a spoke asserts
a connection we do not have, the centre is drawn hollow for the same reason,
and a caption names which view is on screen. The replacement copy only appears
when the community genuinely has nothing, and names the actual next step.

Row label is now "Community mesh" rather than "Compute".

Signed-off-by: Michael Neale <michael.neale@gmail.com>
This commit is contained in:
Michael Neale
2026-08-05 12:35:04 +10:00
parent 36548e0bc3
commit 3376dbc665
5 changed files with 358 additions and 48 deletions
@@ -0,0 +1,136 @@
import assert from "node:assert/strict";
import test from "node:test";
/**
* Contract tests for the radar field's node source.
*
* The load-bearing claim: the community's shared compute is visible **without**
* running a node, because `mesh_snapshot` reads other members' relay status
* notes. An earlier version gated the graph on a local runtime and told the user
* to "turn on sharing to see who's on the mesh", which was false.
*
* The equally load-bearing counter-claim: relay notes are *last known*, not
* live. These tests pin that the two sources stay distinguishable — no RTT and
* no claimed membership from notes.
*/
import { deriveMeshFieldModel } from "./meshFieldNodes.ts";
function device(overrides = {}) {
return {
deviceId: "d1",
label: "studio54",
capacityGb: 36,
models: ["gemma"],
state: "serving",
isSelf: false,
...overrides,
};
}
function snapshot(overrides = {}) {
return {
sharingDeviceCount: 1,
sharedCapacityGb: 36,
models: [],
devices: [device()],
includesSelf: false,
memberCount: 1,
reason: null,
...overrides,
};
}
function peer(overrides = {}) {
return {
id: "p1",
label: "studio54",
state: "serving",
capacityGb: 36,
models: [],
rttMs: 30,
...overrides,
};
}
test("a live runtime wins: only it can vouch for reachability", () => {
const field = deriveMeshFieldModel({
view: { connected: true, selfCapacityGb: 115, peers: [peer()] },
snapshot: snapshot(),
});
assert.equal(field.source, "live");
assert.equal(field.selfParticipating, true);
assert.equal(field.selfCapacityGb, 115);
assert.equal(field.nodes[0].rttMs, 30);
});
test("with no runtime, relay notes still show the pool", () => {
// The whole point: you can see there is a mesh before joining it.
const field = deriveMeshFieldModel({
view: null,
snapshot: snapshot({
sharingDeviceCount: 2,
devices: [device(), device({ deviceId: "d2" })],
}),
});
assert.equal(field.source, "relay");
assert.equal(field.nodes.length, 2);
});
test("relay nodes carry no RTT and claim no membership", () => {
const field = deriveMeshFieldModel({ view: null, snapshot: snapshot() });
// Inventing an RTT would place a node at a distance that means nothing.
assert.equal(field.nodes[0].rttMs, null);
// A hollow centre and no spokes depend on this staying false.
assert.equal(field.selfParticipating, false);
assert.equal(field.selfCapacityGb, null);
assert.match(field.caption, /Last reported/);
});
test("our own note is excluded so we are never drawn twice", () => {
const field = deriveMeshFieldModel({
view: null,
snapshot: snapshot({
devices: [device(), device({ deviceId: "self", isSelf: true })],
}),
});
assert.equal(field.nodes.length, 1);
});
test("a device id is optional; ring slot keeps keys stable", () => {
const field = deriveMeshFieldModel({
view: null,
snapshot: snapshot({
devices: [device({ deviceId: null }), device({ deviceId: null })],
}),
});
assert.notEqual(field.nodes[0].id, field.nodes[1].id);
});
test("ghosts are a count, never capacity", () => {
const field = deriveMeshFieldModel({
view: null,
snapshot: snapshot({ sharingDeviceCount: 1, memberCount: 12 }),
});
assert.equal(field.ghostCount, 11);
// No ghost contributes to a capacity figure anywhere in this model.
assert.equal(field.selfCapacityGb, null);
});
test("an empty community is 'none', not an empty live view", () => {
const field = deriveMeshFieldModel({
view: null,
snapshot: snapshot({ sharingDeviceCount: 0, devices: [], memberCount: 0 }),
});
assert.equal(field.source, "none");
assert.equal(field.caption, null);
});
test("a solo sharer is told it is waiting, not that it is alone in error", () => {
const field = deriveMeshFieldModel({
view: { connected: true, selfCapacityGb: 115, peers: [] },
snapshot: null,
});
assert.equal(field.source, "live");
assert.match(field.caption, /waiting for another device/);
});
@@ -0,0 +1,108 @@
import type { MeshLiveView, MeshSnapshot } from "@/shared/api/tauriMesh";
import type { MeshFieldNode } from "./ui/MeshRadarField";
/**
* Nodes for the radar field, from whichever source this machine actually has.
*
* ## Why two sources
*
* A running node knows its peers through live gossip: present means reachable
* *now*, and each carries an RTT. With no node there is no gossip — but the
* community's shared compute is still knowable, because `mesh_snapshot` reads
* other members' relay status notes and needs no local runtime. So the field
* renders either way, and the earlier "turn on sharing to see who's on the
* mesh" was simply wrong.
*
* The two are not equivalent, and the difference is not cosmetic:
*
* - **live**: reachable now, RTT known, adjacency to us is real
* - **relay**: last known within 120s, no RTT, adjacency unknown
*
* So relay nodes get no RTT (they land mid-band rather than faking proximity),
* the caller draws no spokes when we have not joined, and the caption names
* which view is on screen. Never dress last-known state as live.
*/
export type MeshFieldSource = "live" | "relay" | "none";
export type MeshFieldModel = {
nodes: MeshFieldNode[];
source: MeshFieldSource;
/** True when this machine is part of the mesh being drawn. */
selfParticipating: boolean;
selfCapacityGb: number | null;
/**
* Community members with no published status note.
*
* A count only. A member who never started a node has disclosed no hardware,
* so ghosts never contribute capacity — they are the invitation, not a total.
*/
ghostCount: number;
/** Caption naming the source, or null when there is nothing to qualify. */
caption: string | null;
};
export function deriveMeshFieldModel({
view,
snapshot,
}: {
view: MeshLiveView | null;
snapshot: MeshSnapshot | null;
}): MeshFieldModel {
const ghostCount = Math.max(
0,
(snapshot?.memberCount ?? 0) - (snapshot?.sharingDeviceCount ?? 0),
);
// A local runtime is the better source whenever it exists: it is the only one
// that can vouch for reachability.
if (view?.connected) {
return {
nodes: view.peers.map((peer) => ({
id: peer.id,
state: peer.state,
capacityGb: peer.capacityGb,
rttMs: peer.rttMs,
})),
source: "live",
selfParticipating: true,
selfCapacityGb: view.selfCapacityGb,
ghostCount,
caption:
view.peers.length === 0
? "Connected · waiting for another device"
: null,
};
}
// No runtime. Other members' status notes are all we have — and they are
// enough to show that a pool exists and roughly how big it is.
const devices = (snapshot?.devices ?? []).filter((device) => !device.isSelf);
if (devices.length === 0) {
return {
nodes: [],
source: "none",
selfParticipating: false,
selfCapacityGb: null,
ghostCount,
caption: null,
};
}
return {
nodes: devices.map((device, index) => ({
// Status notes may omit a device id; the ring slot keeps keys stable
// enough for the per-node animation phase.
id: device.deviceId ?? `relay:${index}`,
state: device.state,
capacityGb: device.capacityGb,
// Deliberately null: notes carry no RTT, and inventing one would place a
// node at a distance that means nothing.
rttMs: null,
})),
source: "relay",
selfParticipating: false,
selfCapacityGb: null,
ghostCount,
caption: "Last reported by the community · join to see live detail",
};
}
@@ -11,6 +11,7 @@ import {
describeStartupStage,
} from "../meshCardModel";
import { deriveMeshDetailModel } from "../meshDetailModel";
import { deriveMeshFieldModel } from "../meshFieldNodes";
import { MeshMemoryBulbs } from "./MeshMemoryBulbs";
import { MeshRadarField } from "./MeshRadarField";
@@ -62,12 +63,10 @@ export function MeshComputePopover({
});
const isStarting = row.tone === "starting";
const selectedModel = status?.modelName ?? status?.modelId ?? modelToShare;
// Members with no published status note. A count only: a member who never
// started a node has disclosed no hardware, so they never imply capacity.
const ghostCount = Math.max(
0,
(snapshot?.memberCount ?? 0) - (snapshot?.sharingDeviceCount ?? 0),
);
// The field renders from live gossip when a node is up, and from other
// members' relay status notes when it is not — the community's shared compute
// is knowable either way. See meshFieldNodes.ts.
const field = deriveMeshFieldModel({ view, snapshot });
return (
<Popover>
@@ -108,14 +107,25 @@ export function MeshComputePopover({
/>
</div>
{model.connected ? (
<MeshRadarField
busyNow={model.busyNow}
ghostCount={ghostCount}
peers={view?.peers ?? []}
selfCapacityGb={view?.selfCapacityGb ?? null}
/>
) : null}
{field.source === "none" && field.ghostCount === 0 ? null : (
<div className="flex flex-col gap-1">
<MeshRadarField
busyNow={model.busyNow}
ghostCount={field.ghostCount}
nodes={field.nodes}
selfCapacityGb={field.selfCapacityGb}
selfParticipating={field.selfParticipating}
/>
{field.caption ? (
<p
className="text-2xs leading-snug text-muted-foreground"
data-testid="mesh-field-caption"
>
{field.caption}
</p>
) : null}
</div>
)}
{toggle.isSharing && !isStarting ? (
<MeshMemoryBulbs memory={memory} />
@@ -171,11 +181,12 @@ export function MeshComputePopover({
) : null}
</div>
{model.connected ? null : (
{field.source === "none" ? (
<p className="border-border/60 border-t pt-2 text-2xs leading-snug text-muted-foreground">
Turn on sharing to see who's on the mesh.
No one is sharing compute here yet. Turn on sharing to start the
community mesh.
</p>
)}
) : null}
{callToAction.kind === "createAgent" ? (
// A tip, not a warning: there is compute here and nothing set up to
@@ -1,9 +1,24 @@
import * as React from "react";
import { useReducedMotion } from "motion/react";
import type { MeshPeer } from "@/shared/api/tauriMesh";
import type { MeshDeviceState } from "@/shared/api/tauriMesh";
import { cn } from "@/shared/lib/cn";
/**
* One node on the ring, from either source.
*
* Live gossip peers carry an RTT and are reachable *now*. Relay status notes
* carry no RTT and are only *last known* (valid 120s), so they land mid-band.
* The caller says which it passed via `live`, and the caption names it — the
* field never implies reachability it cannot show.
*/
export type MeshFieldNode = {
id: string;
state: MeshDeviceState;
capacityGb: number | null;
rttMs: number | null;
};
/**
* Canvas-2D radar field for the mesh.
*
@@ -66,7 +81,7 @@ const DARK: Palette = {
ghost: "148, 163, 184",
};
function peerColor(state: MeshPeer["state"], palette: Palette): string {
function peerColor(state: MeshDeviceState, palette: Palette): string {
if (state === "serving") return palette.serving;
if (state === "consuming") return palette.consuming;
return palette.standby;
@@ -87,15 +102,22 @@ function sizeFor(capacityGb: number | null): number {
}
export function MeshRadarField({
peers,
nodes,
selfCapacityGb,
selfParticipating,
ghostCount,
busyNow,
className,
height = 150,
}: {
peers: MeshPeer[];
nodes: MeshFieldNode[];
selfCapacityGb: number | null;
/**
* Whether this machine is in the mesh. When false the centre is drawn hollow:
* we are looking at a pool we have not joined, and a solid centre would claim
* membership we do not have.
*/
selfParticipating: boolean;
/** Community members with no node. Drawn as a dashed band, count only. */
ghostCount: number;
busyNow: boolean;
@@ -121,13 +143,21 @@ export function MeshRadarField({
// Snapshot the frame inputs so the draw loop is not re-created per render.
const frame = React.useRef({
peers,
nodes,
selfCapacityGb,
selfParticipating,
ghostCount,
busyNow,
isDark,
});
frame.current = { peers, selfCapacityGb, ghostCount, busyNow, isDark };
frame.current = {
nodes,
selfCapacityGb,
selfParticipating,
ghostCount,
busyNow,
isDark,
};
React.useEffect(() => {
const canvas = canvasRef.current;
@@ -142,8 +172,9 @@ export function MeshRadarField({
const draw = (now: number) => {
if (disposed) return;
const {
peers: livePeers,
nodes: fieldNodes,
selfCapacityGb: selfGb,
selfParticipating: joined,
ghostCount: ghosts,
busyNow: busy,
isDark: dark,
@@ -201,8 +232,8 @@ export function MeshRadarField({
}
// Peers: angle spread evenly, radius from RTT, size from capacity.
const count = livePeers.length;
livePeers.forEach((peer, index) => {
const count = fieldNodes.length;
fieldNodes.forEach((peer, index) => {
const spread = count === 0 ? 0 : (index / count) * Math.PI * 2;
const angle = spread - Math.PI / 2 + hashUnit(peer.id) * 0.4;
const radius = radiusFor(peer.rttMs, innerMin, innerMax);
@@ -210,15 +241,20 @@ export function MeshRadarField({
const y = cy + Math.sin(angle) * radius;
const rgb = peerColor(peer.state, palette);
// Edge: real adjacency. Opacity carries RTT — closer reads stronger.
const proximity =
peer.rttMs === null ? 0.5 : 1 - Math.min(1, peer.rttMs / 120);
context.strokeStyle = `rgba(${palette.edge}, ${0.12 + proximity * 0.22})`;
context.lineWidth = 1;
context.beginPath();
context.moveTo(cx, cy);
context.lineTo(x, y);
context.stroke();
// A spoke asserts "this machine is connected to that node". True only
// when we have joined; from the outside the nodes are real but our
// adjacency to them is not, so the spokes are simply omitted.
if (joined) {
// Opacity carries RTT — closer reads stronger.
const proximity =
peer.rttMs === null ? 0.5 : 1 - Math.min(1, peer.rttMs / 120);
context.strokeStyle = `rgba(${palette.edge}, ${0.12 + proximity * 0.22})`;
context.lineWidth = 1;
context.beginPath();
context.moveTo(cx, cy);
context.lineTo(x, y);
context.stroke();
}
// Breathing, phase-seeded by id so the field shimmers rather than blinks.
const phase = hashUnit(`${peer.id}:phase`) * Math.PI * 2;
@@ -262,22 +298,39 @@ export function MeshRadarField({
cy,
selfSize * 3.6,
);
selfGlow.addColorStop(0, `rgba(${palette.self}, ${dark ? 0.42 : 0.34})`);
selfGlow.addColorStop(1, `rgba(${palette.self}, 0)`);
selfGlow.addColorStop(
0,
joined
? `rgba(${palette.self}, ${dark ? 0.42 : 0.34})`
: `rgba(${palette.standby}, ${dark ? 0.16 : 0.12})`,
);
selfGlow.addColorStop(
1,
`rgba(${joined ? palette.self : palette.standby}, 0)`,
);
context.fillStyle = selfGlow;
context.beginPath();
context.arc(cx, cy, selfSize * 3.6, 0, Math.PI * 2);
context.fill();
context.fillStyle = `rgba(${palette.self}, 0.98)`;
context.beginPath();
context.arc(cx, cy, selfSize, 0, Math.PI * 2);
context.fill();
context.strokeStyle = dark
? "rgba(255, 255, 255, 0.5)"
: "rgba(255, 255, 255, 0.85)";
context.lineWidth = 1.4;
context.stroke();
if (joined) {
context.fillStyle = `rgba(${palette.self}, 0.98)`;
context.beginPath();
context.arc(cx, cy, selfSize, 0, Math.PI * 2);
context.fill();
context.strokeStyle = dark
? "rgba(255, 255, 255, 0.5)"
: "rgba(255, 255, 255, 0.85)";
context.lineWidth = 1.4;
context.stroke();
} else {
// Hollow centre: this is a pool we are looking at, not one we are in.
context.strokeStyle = `rgba(${palette.standby}, ${dark ? 0.7 : 0.6})`;
context.lineWidth = 1.4;
context.beginPath();
context.arc(cx, cy, selfSize, 0, Math.PI * 2);
context.stroke();
}
// A frozen field needs no frames; reduced motion draws once and stops.
if (!shouldReduceMotion) {
@@ -8,7 +8,7 @@ import type { MeshRowTone } from "../meshRowModel";
import { MeshComputePopover } from "./MeshComputePopover";
/**
* The sidebar Compute row.
* The sidebar Community mesh row.
*
* Replaces the ~120px footer card with one menu line beside Inbox and Agents,
* because front-page sidebar space is premium. The row carries a single signal
@@ -80,7 +80,9 @@ export function SidebarMeshComputeRow({
/>
</span>
</span>
<SidebarMenuLabel className="opacity-80">Compute</SidebarMenuLabel>
<SidebarMenuLabel className="opacity-80">
Community mesh
</SidebarMenuLabel>
</SidebarMenuButton>
</MeshComputePopover>