- {/*
- 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) ? (
-
-
- Shared compute
-
- ) : null}
{runtimeSource ? {runtimeSource} : null}
{agent.model ? {agent.model} : null}
diff --git a/desktop/src/features/mesh-compute/meshAgents.test.mjs b/desktop/src/features/mesh-compute/meshAgents.test.mjs
index 245896e47..fc3fd1ce5 100644
--- a/desktop/src/features/mesh-compute/meshAgents.test.mjs
+++ b/desktop/src/features/mesh-compute/meshAgents.test.mjs
@@ -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,
}),
diff --git a/desktop/src/features/mesh-compute/meshAgents.ts b/desktop/src/features/mesh-compute/meshAgents.ts
index 40e133f0e..a6b9a8480 100644
--- a/desktop/src/features/mesh-compute/meshAgents.ts
+++ b/desktop/src/features/mesh-compute/meshAgents.ts
@@ -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 | null | undefined,
+ agent: Pick | 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
diff --git a/desktop/src/features/mesh-compute/ui/SidebarMeshComputeCard.tsx b/desktop/src/features/mesh-compute/ui/SidebarMeshComputeCard.tsx
index 4f19aa2c3..3e40fffe9 100644
--- a/desktop/src/features/mesh-compute/ui/SidebarMeshComputeCard.tsx
+++ b/desktop/src/features/mesh-compute/ui/SidebarMeshComputeCard.tsx
@@ -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.
) : null}