diff --git a/desktop/src/features/mesh-compute/meshFieldNodes.test.mjs b/desktop/src/features/mesh-compute/meshFieldNodes.test.mjs index 35a6f2936..36fad2923 100644 --- a/desktop/src/features/mesh-compute/meshFieldNodes.test.mjs +++ b/desktop/src/features/mesh-compute/meshFieldNodes.test.mjs @@ -84,7 +84,14 @@ test("relay nodes carry no RTT and claim no membership", () => { // 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("the relay view is not hedged with a caption", () => { + // What we know is shared gets shown as shared. Annotating it with "last + // reported" hedged a fact we actually have, and the old "join to see live + // detail" mislabeled sharing as joining. + const field = deriveMeshFieldModel({ view: null, snapshot: snapshot() }); + assert.equal(field.caption, null); }); test("our own note is excluded so we are never drawn twice", () => { @@ -132,5 +139,5 @@ test("a solo sharer is told it is waiting, not that it is alone in error", () => snapshot: null, }); assert.equal(field.source, "live"); - assert.match(field.caption, /waiting for another device/); + assert.equal(field.caption, "Waiting for another device"); }); diff --git a/desktop/src/features/mesh-compute/meshFieldNodes.ts b/desktop/src/features/mesh-compute/meshFieldNodes.ts index bb910992a..efdad61cc 100644 --- a/desktop/src/features/mesh-compute/meshFieldNodes.ts +++ b/desktop/src/features/mesh-compute/meshFieldNodes.ts @@ -13,14 +13,16 @@ import type { MeshFieldNode } from "./ui/MeshRadarField"; * 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: + * The two are not equivalent, and the difference is expressed in the geometry + * rather than in a hedging caption — what we know is shared gets shown either + * way, and the drawing simply does not assert more than it can: * * - **live**: reachable now, RTT known, adjacency to us is real - * - **relay**: last known within 120s, no RTT, adjacency unknown + * - **relay**: published within the last 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. + * and the caller draws no spokes and a hollow centre when we have not joined, + * because a spoke would assert a connection we do not have. */ export type MeshFieldSource = "live" | "relay" | "none"; @@ -38,7 +40,13 @@ export type MeshFieldModel = { * 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. */ + /** + * Short caption, or null. + * + * Only ever states a fact the field cannot draw — currently just "alone on + * the mesh". Never used to hedge the relay view: what we know is shared is + * shown as shared, not annotated with doubt. + */ caption: string | null; }; @@ -68,10 +76,7 @@ export function deriveMeshFieldModel({ selfParticipating: true, selfCapacityGb: view.selfCapacityGb, ghostCount, - caption: - view.peers.length === 0 - ? "Connected · waiting for another device" - : null, + caption: view.peers.length === 0 ? "Waiting for another device" : null, }; } @@ -103,6 +108,6 @@ export function deriveMeshFieldModel({ selfParticipating: false, selfCapacityGb: null, ghostCount, - caption: "Last reported by the community · join to see live detail", + caption: null, }; } diff --git a/desktop/src/features/mesh-compute/meshMemoryModel.test.mjs b/desktop/src/features/mesh-compute/meshMemoryModel.test.mjs index f5d7cadc7..27a6bc08e 100644 --- a/desktop/src/features/mesh-compute/meshMemoryModel.test.mjs +++ b/desktop/src/features/mesh-compute/meshMemoryModel.test.mjs @@ -33,7 +33,7 @@ test("prefers the runtime's reported model footprint", () => { catalog, modelRef: "gemma", }); - assert.equal(model.label, "10 GB of 40 GB AI memory used by model"); + assert.equal(model.label, "This computer · model uses 10 GB of 40 GB"); assert.equal(model.usedSegments, 3); }); @@ -43,7 +43,7 @@ test("falls back to the selected catalog model before runtime status arrives", ( catalog, modelRef: "gemma", }); - assert.equal(model.label, "8 GB of 32 GB AI memory used by model"); + assert.equal(model.label, "This computer · model uses 8 GB of 32 GB"); assert.equal(model.usedSegments, 3); }); @@ -53,6 +53,32 @@ test("unknown model size shows available memory without inventing use", () => { catalog, modelRef: "custom/model", }); - assert.equal(model.label, "32 GB AI memory available"); + assert.equal(model.label, "This computer · 32 GB AI memory"); assert.equal(model.usedSegments, 0); }); + +test("every label names this computer, never the mesh", () => { + // The meter sits directly under a radar field showing the whole community. + // An unqualified "10 of 40 GB" would read as mesh-wide utilization, which is + // a number nobody has: members publish their own capacity, not each other's + // live allocation. + const cases = [ + { + view: { + connected: true, + selfCapacityGb: 40, + selfModelSizeGb: 10, + peers: [], + }, + catalog, + modelRef: "gemma", + }, + { view: null, catalog, modelRef: "gemma" }, + { view: null, catalog, modelRef: "custom/model" }, + { view: null, catalog: null, modelRef: null }, + ]; + for (const input of cases) { + const model = deriveMeshMemoryModel(input); + assert.match(model.label, /this computer/i, model.label); + } +}); diff --git a/desktop/src/features/mesh-compute/meshMemoryModel.ts b/desktop/src/features/mesh-compute/meshMemoryModel.ts index 89ee6e8a8..8bee4c71f 100644 --- a/desktop/src/features/mesh-compute/meshMemoryModel.ts +++ b/desktop/src/features/mesh-compute/meshMemoryModel.ts @@ -11,9 +11,16 @@ export type MeshMemoryModel = { }; /** - * Approximate model footprint against this machine's shareable AI memory. - * MeshLLM reports model package size, not live OS GPU allocation, so the UI - * deliberately says "model uses" rather than claiming real-time utilization. + * Approximate model footprint against **this machine's** shareable AI memory. + * + * Scoped to this node on purpose, and the label says so. It sits directly under + * a radar field showing the whole community, so an unqualified "36 of 115 GB" + * would read as mesh-wide utilization — a number we do not have and could not + * compute, since members publish their own capacity but nothing about each + * other's live allocation. + * + * MeshLLM reports model package size, not live OS GPU allocation, so the label + * says the model "uses" rather than claiming real-time utilization. */ export function deriveMeshMemoryModel({ view, @@ -47,12 +54,14 @@ export function deriveMeshMemoryModel({ usedGb, usedSegments, totalSegments: MEMORY_SEGMENTS, + // "This computer" leads, because the meter sits under a community-wide + // radar field and must not be read as mesh utilization. label: usedGb != null && totalGb != null - ? `${formatGb(usedGb)} of ${formatGb(totalGb)} AI memory used by model` + ? `This computer · model uses ${formatGb(usedGb)} of ${formatGb(totalGb)}` : totalGb != null - ? `${formatGb(totalGb)} AI memory available` - : "Checking available AI memory…", + ? `This computer · ${formatGb(totalGb)} AI memory` + : "Checking this computer's AI memory…", }; }