Mesh share-compute UX: sidebar card, topology - #4697
Draft
micspiral wants to merge 21 commits into
Draft
Conversation
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>
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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_modeltiered 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 marksTooLarge, and the one-tap toggle would start it. Tier now selects, fit vetoes, and a machine that can host nothing recommendsNone.remote_attempt_countis outbound (this machine using someone else's GPU), so the card claimed "in use now" when the opposite was true. Now usesinflightfor busy andremotely_served_request_countfor consuming.AgentPersona.provider— the raw definition field, which is null when the provider comes from global config. Now reads the resolvedManagedAgent.provider.Gates: 67 unit tests, Rust mesh + catalog tests, tsc, biome, clippy, file-size, px-text.