Skip to content

Mesh share-compute UX: sidebar card, topology - #4697

Draft
micspiral wants to merge 21 commits into
mainfrom
micspiral/mesh-share-ux
Draft

Mesh share-compute UX: sidebar card, topology#4697
micspiral wants to merge 21 commits into
mainfrom
micspiral/mesh-share-ux

Conversation

@micspiral

@micspiral micspiral commented Aug 4, 2026

Copy link
Copy Markdown
Collaborator

Doens't change behavior or mesh at this time:

idea is to invite people to share compute, and then consume it with an agent.

It also shows very minimalist indications that you are consuming someones compute or you are sharing with others (and you can click in to see more - a live graph of the mesh).

This also is a trivial toggle: it uses automatic mode (with plenty of buffer) and shows the effective total mesh. you can still do config (and will add more advanced config as well, for those that really want to drive stick).

new minimalist take:

demo video: https://www.youtube.com/watch?v=78tMXLn73EM

Fixes

  • buzz_recommended_model tiered on rated (marketing) GB and never checked fit against usable memory. A 6 GB machine was handed a 4.6 GB model the same catalog marks TooLarge, and the one-tap toggle would start it. Tier now selects, fit vetoes, and a machine that can host nothing recommends None.
  • Usage signals were inverted: remote_attempt_count is outbound (this machine using someone else's GPU), so the card claimed "in use now" when the opposite was true. Now uses inflight for busy and remotely_served_request_count for consuming.
  • The "set up an agent" tip never disappeared, because it read AgentPersona.provider — the raw definition field, which is null when the provider comes from global config. Now reads the resolved ManagedAgent.provider.
  • Dropped the debug console link from advanced settings.

Gates: 67 unit tests, Rust mesh + catalog tests, tsc, biome, clippy, file-size, px-text.

Adds a sidebar card showing mesh participation at a glance: whether this
machine is sharing, consuming, or neither, plus the community's shared
capacity and a per-device topology strip.

New mesh_snapshot command projects the member status notes already on the
relay (capacity, models, node state) through discovery's existing
membership/binding/freshness rules. Read-only; never selects a serve target.

Footer cards extracted to SidebarFooterNotices because AppSidebar.tsx sat at
exactly the 1000-line ceiling with no room to add a card inline.

Checkpoint commit: card renders and toggles correctly against a live mesh.

Signed-off-by: Michael Neale <michael.neale@gmail.com>
Card now leads with pool capacity ("115 GB · 1 device") rather than this
machine's participation, and drops the model name — it belongs in the detail
view, not a 256px card.

Fixes an inverted usage signal: routing_metrics is incremented only by this
node's own OpenAI ingress, so remote/endpoint attempts mean this machine is
CONSUMING, not being consumed. The old activeConsumers check read them as
"someone is using your compute" — exactly backwards. mesh-llm exposes no
inbound counter, so participation copy now uses inflight ("working now") and
request_count ("N requests this session") and never claims work was served
for other members. A test pins that wording.

Headline text block is now a button for the forthcoming mesh detail view.

Signed-off-by: Michael Neale <michael.neale@gmail.com>
Two UX-only fixes, no behavior change:

An unfetched snapshot (null) reported "No mesh capacity yet", conflating
"not checked yet" with "the community has nothing" — a verdict on everyone
else's machines rendered during startup. It now reads "Checking mesh
capacity…" and only claims an empty mesh once a snapshot actually says so.

Sharing copy described the wrong mechanism: a sharing machine contributes
inference capacity, it does not host other members' agent processes. "Turn on
to let members run agents on this computer" becomes "Share compute to run
models", and the settings card description is corrected to match.

Also plumbs two data additions the detail view needs: snapshot.memberCount
(NIP-43 roster size, the denominator for "N of M sharing") and the
routing_metrics.pressure split (locallyServed / remotelyServed /
endpointServed) — completed requests rather than attempts, which is the honest
basis for "this ran on my machine" vs "this ran on someone else's".

Signed-off-by: Michael Neale <michael.neale@gmail.com>
Clicking the card headline opens a popover anchored to it — a glance beside the
thing it explains, not a centred dialog task.

Shows pool capacity, "N of M members sharing" from the NIP-43 roster, and a
radial field with this computer at the centre, participating peers around it,
and members who publish no status note as dashed ghosts.

Two honesty constraints are encoded in the code and pinned by tests:

Spokes mean "in the same mesh as you", nothing more. The relay snapshot
carries no adjacency and the activity counters are node-local rather than
edge-attributed, so nothing is animated along a spoke. Only the centre pulses,
driven by this node's own inflight count.

Ghosts are a count, never capacity. A member who never starts a node discloses
no hardware by design, so the overlay never estimates GB on their behalf.

Consuming is now visible: describeRequestOrigin uses the pressure split to say
"3 of 12 requests on shared compute" — provable, because those are completed
requests this machine dispatched. There is deliberately no "served for others"
line: fronted_request_count turns out to alias request_count, confirming
mesh-llm exposes no inbound counter, so activity copy hints that work is
flowing without claiming who asked for it.

UX only — no routing, admission, or lifecycle change.

Signed-off-by: Michael Neale <michael.neale@gmail.com>
…work

Drops the relay/roster overlay from the topology in favour of a single source:
the runtime's own gossip view. A node is drawn because we are connected to it
right now, not because a status note is still inside its 120s freshness window.
That window is why a departed device could sit on the graph as a solid,
capacity-contributing dot for up to two minutes — a reconciliation problem this
removes rather than referees. Community-roster ghosts go too: a member who never
starts a node discloses no hardware, so they added a denominator and little else.

New mesh_live_view command projects peers[] (label, state, capacity honoring the
peer's own cap, models, rtt). Client-mode peers report vram_gb: 0, normalized to
absent so the UI never prints "0 GB" for a machine that shares none. The relay
snapshot is retained for exactly one job: the headline when no local runtime
exists, where it is the only view available.

Card headline is now "MeshLLM · 115 GB, 2 peers", with the participation hint
below it: "You're sharing", "· serving another member", or the consuming split.

Inbound work turns out to be derivable after all. mesh-llm exposes no counter
for it (fronted_request_count merely aliases request_count), but sharing, with
inflight > 0, while our own dispatch count stays flat between samples, means the
work cannot be ours. inferInboundWork encodes that. It is sampled, so it can
undercount a request that starts and ends between polls — it never over-claims,
which is the direction that matters.

Also corrects the inverted doc comments on MeshServingUsage, which still
described the backwards model that caused the original card bug.

UX only — no routing, admission, or lifecycle change.

Signed-off-by: Michael Neale <michael.neale@gmail.com>
buzz_recommended_model tiered purely on rated (marketing) GB and never
consulted fit against usable AI memory — two different numbers. A 6GB machine
was handed the 4.6GB small pick that the same catalog marks TooLarge, and the
one-tap card, which trusts `recommended`, would start it.

The tier still selects, but fit now vetoes: the large pick falls back to small
when usable memory cannot hold it (a --max-vram cap on a high-rated machine),
and a machine that cannot host either recommends None. None is the honest
answer, not a failure — that machine can still consume shared compute.

Signed-off-by: Michael Neale <michael.neale@gmail.com>
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>
Sharing compute that no agent can use is a dead end, and the sidebar card is
where someone would notice it. The card now offers to create an agent — but
only when the setup is genuinely incomplete: capacity exists (shared by this
machine or the community) and no persona carries the relay-mesh provider.

The CTA's value is that it stays quiet, so most of its tests pin silence. It
says nothing once a mesh agent exists, nothing when there is no capacity
anywhere (that would relocate the dead end rather than fix it), and nothing
while the persona list is still loading, since prompting someone to create an
agent they may already have is worse than a moment's quiet.

Agents that run on shared compute are now decorated in ManagedAgentRow. Where
an agent's inference runs is otherwise invisible without opening its config,
and shared compute is the one provider whose health the owner can act on
directly from the card.

Detection reads the persisted provider id (relay-mesh, the same constant the
backend gates mesh model discovery on) rather than inferring intent from a
model string.

Signed-off-by: Michael Neale <michael.neale@gmail.com>
The call to action never disappeared after setting up a mesh agent because it
asked AgentPersona.provider — the raw definition field. The mesh provider is
normally set at the global layer (global-agent-config.json), so that field is
null for most mesh agents and the check answered "no" for all of them.

Rust already resolves the layering (definition -> global for linked instances,
instance -> global for definition-less ones) in resolve_effective_config, and
runtime.rs builds the ManagedAgent DTO from the resolved value. Detection now
consumes that field and holds no precedence rules of its own, rather than
hand-rolling a second copy of the layering that would drift from Rust's.

The CTA is now a tip with a pulsing dot rather than a bare link, and it still
appears when the community has capacity even if this machine is not sharing —
using someone else's compute is a legitimate reason to set up an agent.

Removes the ManagedAgentRow shared-compute chip. Buzz already spends
per-message and per-row provenance on ownership (MessageAgentOwner: "managed
by X"), which answers a governance question; a second provenance marker for a
config fact competes with it for the same space.

Signed-off-by: Michael Neale <michael.neale@gmail.com>
The console URL pointed at mesh-llm's own web UI, which is not a Buzz surface
and answers none of the questions someone opens advanced settings to ask.

Signed-off-by: Michael Neale <michael.neale@gmail.com>
@micspiral micspiral changed the title Mesh share-compute UX: sidebar card, topology, honest states Mesh share-compute UX: sidebar card, topology Aug 4, 2026
sandro-sq and others added 11 commits August 4, 2026 19:33
Signed-off-by: Alessandro Joabar <sandro@squareup.com>
* origin/main: (31 commits)
  feat: paste composer text without formatting (#4801)
  Revert "chore(release): release Buzz Desktop version 0.5.5" (#4808)
  chore(release): release Buzz Desktop version 0.5.5 (#4800)
  fix: reauthenticate databricks model discovery (#4008)
  Revert "chore(release): release Buzz Desktop version 0.5.5" (#4797)
  feat: Buzz entity links — rich preview cards + in-app navigation for repos, PRs, and issues (#4695)
  fix(desktop): serialize tray channel actions for frontend (#4762)
  chore(release): release Buzz Desktop version 0.5.5 (#4788)
  feat(projects): support multiple repositories (#4671)
  fix(ci): make desktop cache test version agnostic (#4791)
  fix(desktop): widen post-Enter timeouts in empty-edit-delete spec (#4792)
  fix(desktop): wait for terminal frame before splash (#4781)
  fix(desktop): integer-align custom reaction emoji (#4779)
  Polish Huddle voice controls (#4694)
  fix(local-archive): default both archive settings to enabled (#4750)
  fix(mobile): stop oversized read-state retry loop (#4595)
  fix(desktop): close reconnect gaps that previously required CMD+R (#4737)
  Dock Buzz Term within channel workspace (#4724)
  perf(relay): index channel-id lookups and skip trace-only reads (#4647)
  fix(agents): canonicalize stale persona harness pins (#4631)
  ...

Signed-off-by: Alessandro Joabar <sandro@squareup.com>
Sidebar front-page space is premium, so shared compute is now a single menu
line under Agents instead of a ~120px footer card. The row carries one signal
-- a coloured dot -- and everything else moved into the popover it opens.

The dot distinguishes six states, and the pair that matters most is "nothing
here" vs "there is capacity you have not joined": mesh_snapshot reads other
members' relay status notes with no local runtime, so idle capacity is knowable
without starting a node or running a probe. Grey would have conflated the two,
and the second is the reason to look at the row at all.

The switch shows only while not sharing; once sharing, the state is legible
from the dot and Stop lives in the popover, which frees the trailing slot for
live pool capacity. It is never disabled while consuming -- a client runtime
can be replaced by a serve runtime at any time, so offering the switch is
honest.

The popover consolidates what were two overlapping surfaces (card + detail
popover) into one, and replaces the small radial SVG with a canvas-2D radar
field: self at centre, radius from RTT, node size from shared capacity,
per-node breathing seeded by id hash, real adjacency as spokes, and
non-participant members as a dashed outer band with no size and no capacity.
Visual language follows mesh-llm's own console, drawn in 2D rather than porting
its ~2,200 lines of WebGL -- a 288px popover does not justify shader plumbing.

Nothing animates along a spoke: mesh-llm's counters are node-local with no
per-edge attribution, so a travelling packet would be invented. Motion is
confined to node breathing and a centre pulse driven by real inflight work.

Row and popover share one useMeshComputeState hook rather than each calling the
polling hooks, which would double every poll and let the two disagree
mid-flight.

Signed-off-by: Michael Neale <michael.neale@gmail.com>
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>
Shared compute in the community is worth noticing even when this machine is
neither using nor adding to it -- that is the moment to either tap in or
contribute, and a static slate dot said nothing.

Pulse is now a named signal rather than a boolean, because two different facts
want motion and a single flag would make a throb ambiguous:

  - activity -- real work is in flight on this node right now
  - invite   -- there is compute here, and we are outside it

They are deliberately unalike. Activity uses Tailwind's stock ping: quick and
insistent, because something is happening. Invite breathes on a new 2.8s
keyframe in slate and never reaches full opacity: noticeable on a second
glance, never competing with unread badges.

An empty or unfetched community still never throbs -- an invitation to join a
mesh that does not exist would be a lie told with animation.

Signed-off-by: Michael Neale <michael.neale@gmail.com>
Drops two captions that hedged or mislabeled facts we have.

"Join to see live detail" was wrong twice over: in that state the only thing to
join is sharing, and the field already shows what the community has published.
"Last reported by the community" annotated a real fact with doubt -- the notes
are current within 120s, and what we know is shared should be shown as shared.
The live-vs-relay difference is already carried by the geometry: relay nodes get
no synthetic RTT, and the centre is hollow with no spokes because adjacency is
what we cannot assert. A caption repeating that was noise.

The only surviving caption states something the drawing cannot: alone on the
mesh.

The memory meter is per-node and now says so -- "This computer · model uses 10
GB of 40 GB". It sits directly beneath a radar field showing the whole
community, so an unqualified figure read as mesh-wide utilization: a number
nobody has, since members publish their own capacity but nothing about each
other's live allocation. A test pins the scoping rather than the wording.

Signed-off-by: Michael Neale <michael.neale@gmail.com>
Three changes to the sidebar row, all aimed at making it argue for itself.

The share Switch is gone from the row. It was scaled to 70%, unlabelled, and
sat where SidebarMenuBadge normally puts a number -- hard to see and ambiguous
about what it would do. The popover already had the same control with room to
label it, so the row now carries state and capacity only, and every control
lives in one place.

That frees the trailing slot for capacity in every state that has a number,
not just while sharing. When this machine is outside the mesh the figure *is*
the invitation: "92 GB" argues for joining better than any exhortation, and
unlike a nudge there is nothing to dismiss. Unknown capacity degrades to a
device count rather than printing 0 GB -- a count still says there is something
here.

Sharing with no agent set up to use the mesh now throbs. It deliberately does
not get a colour of its own: blue already means *consuming*, i.e. taking from
the mesh, and sharing-with-no-consumer is the opposite, so reusing blue would
make blue meaningless. The dot keeps stating what is true -- green, we really
are sharing -- while the existing invite throb says there is a step left, and
the tooltip names which. A loading agent list is treated as "assume set up",
so a slow query never flashes a nudge and retracts it.

Signed-off-by: Michael Neale <michael.neale@gmail.com>
…mesh is empty

An empty mesh was the one state with nothing to show and nothing happening, so
the row sat inert with nothing explaining why. The action is real -- you can be
the first to share -- so the badge slot now carries the word "Share" where a
figure would otherwise go.

Static rather than throbbing. A community that never uses the mesh would
animate forever, and permanent motion earns nothing but annoyance; the invite
throb stays reserved for capacity that actually exists.

An unfetched snapshot still offers nothing at all. A prompt derived from no data
would appear for one poll and retract, which reads as a glitch rather than a
suggestion -- so "not asked yet" and "asked, nothing there" are now separate
branches instead of one condition.

Row label is "Buzz MeshLLM" rather than "Community mesh".

Signed-off-by: Michael Neale <michael.neale@gmail.com>
With nothing shared anywhere, the row and popover were both status surfaces
with no status: a hollow dot beside "Buzz MeshLLM", and a "Mesh capacity"
heading over "0 sharing in this community". Nothing said what the switch does,
which is the one thing a first-time reader needs and the one thing a zero
cannot tell them.

The row now drops the "Buzz" qualifier -- "Buzz MeshLLM" plus an invitation
does not fit 256px, and the name is the part that can afford a word -- carries a
Share2 glyph where the status dot would go, and offers "Share your compute" in
the trailing slot. The dot returns the moment there is a real state to report,
which is the more honest signal there.

The popover's cold subtitle explains rather than reports: "Share your spare
compute with the Buzz community to run models." Its heading names the thing
instead of heading an absent number.

Only the cold state invites. A permanent nudge beside a real figure becomes
furniture, and a prompt derived from an unfetched snapshot would appear for one
poll and retract, reading as a glitch.

Also renames the popover's footer link from "Compute settings" to "Advanced
settings": it named the destination, where the useful thing to name is what is
there. The picker, custom model refs, and the memory cap are all advanced
concerns; the popover already covers the everyday ones.

Signed-off-by: Michael Neale <michael.neale@gmail.com>
Two regressions from the previous commit.

The dot is the row's reason to exist -- hollow when nothing is known, slate when
there is capacity to join, green sharing, blue consuming -- and the last change
swapped it for a Share2 glyph in the cold state. That threw the signal away in
the one state that still had something to say, and made the row read as a
network widget rather than a status line. The dot is now unconditional in every
state, and Share2 sits beside the invitation it labels.

The same change also invented two names, "MeshLLM" cold and "Buzz MeshLLM"
otherwise, then failed to apply the second to the popover -- so the card kept
saying "Buzz MeshLLM" after the row had dropped it. There is now one constant,
"MeshLLM" (the "Buzz" qualifier adds nothing inside Buzz), and a test asserts
the label is identical across every state so it cannot drift again.

Signed-off-by: Michael Neale <michael.neale@gmail.com>
Advanced settings gains a nested Experimental section listing split-capable
models that exceed this machine, with the consequence stated before any choice:
sharing will not start until enough well-connected computers are sharing the
same model. It waits instead of loading, and that waiting is indistinguishable
from a slow download to anyone who was not told.

## Why "beyond this machine" is the partition, not "multi-machine"

The catalog's runtime field marks a model multi-machine whenever a layer package
exists, which is not the same as needing several computers -- Qwen3-8B-Q4_K_M is
5 GB and split-capable. 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 the tier is the
intersection: split-capable AND beyond local memory. Anything that fits belongs
in the ordinary picker, where it starts immediately.

## Catalog comes from mesh-llm's loader

remote_catalog is public in mesh-llm-host-runtime, so Buzz calls it rather than
fetching and parsing the dataset itself -- one implementation of the format.
Hand-parsing is a trap: the package discriminator is `type` on the wire and
`package_type` in Rust via serde rename, and reading the Rust name off the wire
produced a confident "zero models are split-capable" earlier in this work. A
test pins the wire spelling, and it turned out stronger than intended -- serde
rejects a wrong discriminator outright, so format drift surfaces as a parse
error rather than a silently empty tier.

Reads the on-disk cache first and downloads only when nothing is cached, in
spawn_blocking. No running node required: you choose what to share before you
start sharing it.

## Empty is four distinct sentences

Still loading, could not read the catalog, the catalog holds no packages, and
this machine is large enough that nothing needs splitting. The last is good news
and must not read as a failure. A test asserts all four differ.

Two upstream entries publish "30 layers" where bytes belong, which parses to
nothing -- and is why mesh-llm reports unknown_model_size for the model this
machine serves. Unknown size is never claimed to fit: offering the split path is
recoverable, promising a local start that then fails is not.

Also updates the Share compute description to say why it is worth doing rather
than restating the mechanism.

Signed-off-by: Michael Neale <michael.neale@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants