fix(mesh): CTA reads resolved provider; drop the row chip

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>
This commit is contained in:
Michael Neale
2026-08-04 19:38:07 +10:00
parent 67186f83d5
commit 7e24ee7379
4 changed files with 73 additions and 127 deletions
@@ -5,14 +5,12 @@ import {
ChevronDown,
ChevronRight,
RefreshCw,
Share2,
} from "lucide-react";
import { useAppNavigation } from "@/app/navigation/useAppNavigation";
import { PresenceDot } from "@/features/presence/ui/PresenceBadge";
import { Badge } from "@/shared/ui/badge";
import { AgentStatusBadge } from "@/features/agents/ui/AgentStatusBadge";
import { usesMeshCompute } from "@/features/mesh-compute/meshAgents";
import { useAgentWorking } from "@/features/agents/agentWorkingSignal";
import { useOpenAgentActivity } from "@/features/agents/useOpenAgentActivity";
import { formatElapsed } from "@/features/agents/ui/agentSessionUtils";
@@ -409,23 +407,8 @@ function RuntimeBlock({
<p className="truncate font-mono text-xs text-foreground">
{agent.agentCommand}
</p>
{runtimeSource || agent.model || usesMeshCompute(agent) ? (
{runtimeSource || agent.model ? (
<div className="flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground">
{/*
Where an agent's inference runs is otherwise invisible without
opening its config, and "on shared compute" is the one provider
whose health the user can actually act on from the sidebar card.
*/}
{usesMeshCompute(agent) ? (
<span
className="inline-flex items-center gap-1 text-emerald-600 dark:text-emerald-400"
data-testid="agent-uses-mesh"
title="Runs on Buzz shared compute"
>
<Share2 aria-hidden className="h-3 w-3" />
Shared compute
</span>
) : null}
{runtimeSource ? <span>{runtimeSource}</span> : null}
{agent.model ? <span>{agent.model}</span> : null}
</div>
@@ -4,68 +4,45 @@ import test from "node:test";
/**
* Contract tests for mesh agent detection and the card's call to action.
*
* The CTA's value is that it stays quiet. A nudge that renders every time the
* card does becomes furniture, so most of these tests pin *silence*.
* Two things matter here. First, detection must read the **resolved** provider
* off `ManagedAgent`: the mesh provider is usually set at the global layer, so
* a persona's own `provider` field is null for most mesh agents — asking the
* persona was the original bug. Second, the CTA's value is that it stays
* quiet, so most of these tests pin *silence*.
*/
import {
deriveMeshCallToAction,
summarizeMeshAgents,
usesMeshCompute,
} from "./meshAgents.ts";
import { deriveMeshCallToAction, usesMeshCompute } from "./meshAgents.ts";
function persona(overrides = {}) {
return {
id: "p1",
displayName: "Hop",
provider: "relay-mesh",
...overrides,
};
function agent(overrides = {}) {
return { pubkey: "a1", provider: "relay-mesh", ...overrides };
}
test("mesh agents are identified by provider, not by model string", () => {
const summary = summarizeMeshAgents([
persona({ id: "a", displayName: "Zoe" }),
persona({ id: "b", displayName: "Ada" }),
persona({ id: "c", displayName: "Other", provider: "anthropic" }),
// A model that merely mentions a mesh model is not a mesh agent: intent
// lives in the persisted provider, not in a free-text model field.
persona({ id: "d", displayName: "Lookalike", provider: null }),
]);
assert.equal(summary.count, 2);
assert.equal(summary.hasAny, true);
assert.deepEqual(summary.names, ["Ada", "Zoe"], "names sort for stable copy");
});
test("provider matching tolerates whitespace and absence", () => {
assert.equal(usesMeshCompute(persona({ provider: " relay-mesh " })), true);
assert.equal(usesMeshCompute(persona({ provider: "anthropic" })), false);
assert.equal(usesMeshCompute(persona({ provider: null })), false);
test("detection reads the resolved provider, whitespace tolerant", () => {
assert.equal(usesMeshCompute(agent()), true);
assert.equal(usesMeshCompute(agent({ provider: " relay-mesh " })), true);
assert.equal(usesMeshCompute(agent({ provider: "anthropic" })), false);
// Null is the common case for an agent inheriting a NON-mesh global default,
// and for an orphaned instance. Neither is a mesh agent.
assert.equal(usesMeshCompute(agent({ provider: null })), false);
assert.equal(usesMeshCompute(null), false);
assert.equal(usesMeshCompute(undefined), false);
});
test("an empty persona list is not an error", () => {
const summary = summarizeMeshAgents([]);
assert.equal(summary.hasAny, false);
assert.deepEqual(summary.names, []);
assert.equal(summarizeMeshAgents(undefined).hasAny, false);
});
test("the CTA appears only when compute has no consumer", () => {
// Sharing, but nothing can use it — the dead end worth naming.
test("the CTA appears when there is compute and nothing using it", () => {
// Sharing from this machine.
assert.deepEqual(
deriveMeshCallToAction({
personas: [persona({ provider: "anthropic" })],
agents: [agent({ provider: "anthropic" })],
isSharing: true,
meshHasCapacity: false,
}),
{ kind: "createAgent", label: "Set up an agent to use it" },
);
// Not sharing, but the community has capacity this person could use.
// Not sharing, but the community has capacity this person could use — a
// legitimate reason to set up an agent even without contributing.
assert.equal(
deriveMeshCallToAction({
personas: [],
agents: [],
isSharing: false,
meshHasCapacity: true,
}).kind,
@@ -73,10 +50,10 @@ test("the CTA appears only when compute has no consumer", () => {
);
});
test("the CTA stays silent once a mesh agent exists", () => {
test("the CTA disappears the moment a mesh agent exists", () => {
assert.deepEqual(
deriveMeshCallToAction({
personas: [persona()],
agents: [agent({ provider: "anthropic" }), agent({ pubkey: "a2" })],
isSharing: true,
meshHasCapacity: true,
}),
@@ -85,11 +62,9 @@ test("the CTA stays silent once a mesh agent exists", () => {
});
test("no capacity anywhere means the CTA would just move the dead end", () => {
// Telling someone to build an agent for a mesh with nothing in it does not
// help them; it relocates the problem.
assert.deepEqual(
deriveMeshCallToAction({
personas: [],
agents: [],
isSharing: false,
meshHasCapacity: false,
}),
@@ -97,12 +72,12 @@ test("no capacity anywhere means the CTA would just move the dead end", () => {
);
});
test("an unloaded persona list never prompts a possible duplicate", () => {
// `undefined` is "not fetched yet". Prompting someone to create an agent they
// may already have is worse than staying quiet for a moment.
test("an unloaded agent list never prompts a possible duplicate", () => {
// `undefined` is "not fetched yet". Prompting someone to create an agent
// they may already have is worse than staying quiet for a moment.
assert.deepEqual(
deriveMeshCallToAction({
personas: undefined,
agents: undefined,
isSharing: true,
meshHasCapacity: true,
}),
+27 -49
View File
@@ -1,88 +1,66 @@
import type { AgentPersona } from "@/shared/api/types";
import type { ManagedAgent } from "@/shared/api/types";
/**
* Which agents are set up to run on Buzz shared compute.
*
* A persona points at the mesh by carrying `provider === "relay-mesh"` — the
* same id the backend gates mesh model discovery on
* (`managed_agents::RELAY_MESH_PROVIDER_ID`), so this asks the persistent
* record rather than inferring intent from a model string.
* ## Why this reads `ManagedAgent`, not `AgentPersona`
*
* The point is a call to action: sharing compute that no agent can use is a
* dead end, and the card is where someone would notice. So the card needs to
* know not just *whether* mesh agents exist but whether the whole setup is
* inert — capacity with no consumer, or a consumer with no capacity.
* The mesh provider is usually set at the **global** layer
* (`global-agent-config.json`), not per agent — a persona's own `provider`
* field is null in that common case, so asking a persona "are you a mesh
* agent?" answers "no" for every agent on a machine whose global default is
* `relay-mesh`. That was the original bug here.
*
* Rust already resolves the layering (definition → global for linked
* instances, instance → global for definition-less ones) in
* `effective_config::resolve_effective_config`, and `runtime.rs` builds the
* `ManagedAgent` DTO from that resolved value. So this file consumes the
* resolved field and deliberately holds **no precedence rules of its own** —
* a second copy of that layering would drift from the one in Rust.
*/
/** The provider id a persona carries when it runs on shared compute. */
/** The provider id an agent carries when it runs on shared compute. */
export const RELAY_MESH_PROVIDER_ID = "relay-mesh";
export type MeshAgentSummary = {
/** Agents configured to run on shared compute. */
count: number;
/** Display names, for naming one specifically. Sorted for stable copy. */
names: string[];
/** True when at least one such agent exists. */
hasAny: boolean;
};
export function summarizeMeshAgents(
personas: AgentPersona[] | undefined,
): MeshAgentSummary {
const matching = (personas ?? []).filter(
(persona) => persona.provider?.trim() === RELAY_MESH_PROVIDER_ID,
);
const names = matching
.map((persona) => persona.displayName)
.sort((a, b) => a.localeCompare(b));
return { count: matching.length, names, hasAny: matching.length > 0 };
}
/**
* Whether a given persona runs on shared compute.
* Whether an agent runs on shared compute.
*
* Used to decorate an agent wherever it appears, so "this one is running on the
* mesh" is legible without opening its config.
* Takes the resolved `provider` off a `ManagedAgent`; never re-derives it.
*/
export function usesMeshCompute(
persona: Pick<AgentPersona, "provider"> | null | undefined,
agent: Pick<ManagedAgent, "provider"> | null | undefined,
): boolean {
return persona?.provider?.trim() === RELAY_MESH_PROVIDER_ID;
return agent?.provider?.trim() === RELAY_MESH_PROVIDER_ID;
}
/**
* The card's call to action, or null when none is warranted.
* The card's call to action, or `none` when none is warranted.
*
* Deliberately silent in the common case. A nudge that appears every time the
* card renders becomes furniture, so this only speaks when the setup is
* genuinely incomplete in a way the person can act on:
*
* - sharing (or capacity exists) but no agent can use it → offer to make one
* - an agent exists but nothing is shared anywhere → offer to share
*
* Returns null while the picture is still unknown (`personas === undefined`),
* because prompting someone to create an agent they may already have is worse
* than staying quiet for a moment.
* genuinely incomplete in a way the person can act on: there is compute to use
* and nothing set up to use it.
*/
export type MeshCallToAction =
| { kind: "createAgent"; label: string }
| { kind: "none" };
export function deriveMeshCallToAction({
personas,
agents,
isSharing,
meshHasCapacity,
}: {
personas: AgentPersona[] | undefined;
/** Resolved agent list. `undefined` while still loading. */
agents: ManagedAgent[] | undefined;
isSharing: boolean;
/** Anyone in the community is sharing, including this machine. */
meshHasCapacity: boolean;
}): MeshCallToAction {
// Unknown yet — say nothing rather than nudge toward a duplicate.
if (personas === undefined) {
if (agents === undefined) {
return { kind: "none" };
}
if (summarizeMeshAgents(personas).hasAny) {
if (agents.some(usesMeshCompute)) {
return { kind: "none" };
}
// Compute with no consumer is the dead end worth naming. Only prompt when
@@ -8,7 +8,7 @@ import { meshStartNode, meshStopNode } from "@/shared/api/tauriMesh";
import type { MeshModelCatalog } from "@/shared/api/tauriMesh";
import { meshModelCatalog } from "@/shared/api/tauriMesh";
import { usePersonasQuery } from "@/features/agents/hooks";
import { useManagedAgentsQuery } from "@/features/agents/hooks";
import { requestOpenCreateAgent } from "@/features/agents/openCreateAgentEvent";
import { deriveMeshCallToAction } from "../meshAgents";
import { useMeshDownloadProgress } from "../hooks/useMeshDownloadProgress";
@@ -103,11 +103,12 @@ export function SidebarMeshComputeCard({
useMeshDownloadProgress();
// Sharing compute no agent can use is a dead end, and this card is where
// someone would notice. The CTA stays silent unless the setup is actually
// incomplete — see deriveMeshCallToAction.
const { data: personas } = usePersonasQuery();
// someone would notice. Reads the resolved `provider` off ManagedAgent, not
// a persona's raw field: the mesh provider is usually set globally, so the
// persona value is null for most mesh agents. See meshAgents.ts.
const { data: managedAgents } = useManagedAgentsQuery();
const callToAction = deriveMeshCallToAction({
personas,
agents: managedAgents,
isSharing: toggle.isSharing,
meshHasCapacity: (snapshot?.sharingDeviceCount ?? 0) > 0,
});
@@ -275,13 +276,22 @@ export function SidebarMeshComputeCard({
/>
{callToAction.kind === "createAgent" ? (
// A tip, not a warning: there is compute here and nothing set up to
// use it. The dot pulses to catch the eye once, then the tip simply
// disappears the moment a mesh agent exists.
<button
className="mt-2 text-2xs font-medium text-foreground/80 underline-offset-2 transition-colors hover:text-foreground hover:underline focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
className="mt-2 flex w-full items-center gap-1.5 rounded-md border border-border/60 bg-background/60 px-2 py-1.5 text-left text-2xs font-medium text-foreground/80 transition-colors hover:border-border hover:text-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring"
data-testid="mesh-card-create-agent"
onClick={() => requestOpenCreateAgent()}
type="button"
>
{callToAction.label}
<span className="relative flex h-1.5 w-1.5 shrink-0">
{shouldReduceMotion ? null : (
<span className="absolute h-1.5 w-1.5 animate-ping rounded-full bg-amber-500 dark:bg-amber-400" />
)}
<span className="h-1.5 w-1.5 rounded-full bg-amber-500 dark:bg-amber-400" />
</span>
<span className="min-w-0 truncate">{callToAction.label}</span>
</button>
) : null}