From 67186f83d567b890a301dba7bae88f0db6c9ac07 Mon Sep 17 00:00:00 2001 From: Michael Neale Date: Tue, 4 Aug 2026 18:23:24 +1000 Subject: [PATCH] feat(mesh): agent call-to-action and shared-compute decorator MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../features/agents/ui/ManagedAgentRow.tsx | 19 ++- .../features/mesh-compute/meshAgents.test.mjs | 111 ++++++++++++++++++ .../src/features/mesh-compute/meshAgents.ts | 95 +++++++++++++++ .../ui/SidebarMeshComputeCard.tsx | 24 ++++ 4 files changed, 248 insertions(+), 1 deletion(-) create mode 100644 desktop/src/features/mesh-compute/meshAgents.test.mjs create mode 100644 desktop/src/features/mesh-compute/meshAgents.ts diff --git a/desktop/src/features/agents/ui/ManagedAgentRow.tsx b/desktop/src/features/agents/ui/ManagedAgentRow.tsx index 606d2b7883..cb0ae79ae8 100644 --- a/desktop/src/features/agents/ui/ManagedAgentRow.tsx +++ b/desktop/src/features/agents/ui/ManagedAgentRow.tsx @@ -5,12 +5,14 @@ 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"; @@ -407,8 +409,23 @@ function RuntimeBlock({

{agent.agentCommand}

- {runtimeSource || agent.model ? ( + {runtimeSource || agent.model || usesMeshCompute(agent) ? (
+ {/* + 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 new file mode 100644 index 0000000000..245896e476 --- /dev/null +++ b/desktop/src/features/mesh-compute/meshAgents.test.mjs @@ -0,0 +1,111 @@ +import assert from "node:assert/strict"; +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*. + */ + +import { + deriveMeshCallToAction, + summarizeMeshAgents, + usesMeshCompute, +} from "./meshAgents.ts"; + +function persona(overrides = {}) { + return { + id: "p1", + displayName: "Hop", + 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); + 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. + assert.deepEqual( + deriveMeshCallToAction({ + personas: [persona({ 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. + assert.equal( + deriveMeshCallToAction({ + personas: [], + isSharing: false, + meshHasCapacity: true, + }).kind, + "createAgent", + ); +}); + +test("the CTA stays silent once a mesh agent exists", () => { + assert.deepEqual( + deriveMeshCallToAction({ + personas: [persona()], + isSharing: true, + meshHasCapacity: true, + }), + { kind: "none" }, + ); +}); + +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: [], + isSharing: false, + meshHasCapacity: false, + }), + { kind: "none" }, + ); +}); + +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. + assert.deepEqual( + deriveMeshCallToAction({ + personas: undefined, + isSharing: true, + meshHasCapacity: true, + }), + { kind: "none" }, + ); +}); diff --git a/desktop/src/features/mesh-compute/meshAgents.ts b/desktop/src/features/mesh-compute/meshAgents.ts new file mode 100644 index 0000000000..40e133f0e6 --- /dev/null +++ b/desktop/src/features/mesh-compute/meshAgents.ts @@ -0,0 +1,95 @@ +import type { AgentPersona } 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. + * + * 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 provider id a persona 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. + * + * Used to decorate an agent wherever it appears, so "this one is running on the + * mesh" is legible without opening its config. + */ +export function usesMeshCompute( + persona: Pick | null | undefined, +): boolean { + return persona?.provider?.trim() === RELAY_MESH_PROVIDER_ID; +} + +/** + * The card's call to action, or null 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. + */ +export type MeshCallToAction = + | { kind: "createAgent"; label: string } + | { kind: "none" }; + +export function deriveMeshCallToAction({ + personas, + isSharing, + meshHasCapacity, +}: { + personas: AgentPersona[] | 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) { + return { kind: "none" }; + } + if (summarizeMeshAgents(personas).hasAny) { + return { kind: "none" }; + } + // Compute with no consumer is the dead end worth naming. Only prompt when + // there is actually capacity to use — telling someone to build an agent for + // a mesh with nothing in it just moves the dead end. + if (isSharing || meshHasCapacity) { + return { kind: "createAgent", label: "Set up an agent to use it" }; + } + return { kind: "none" }; +} diff --git a/desktop/src/features/mesh-compute/ui/SidebarMeshComputeCard.tsx b/desktop/src/features/mesh-compute/ui/SidebarMeshComputeCard.tsx index 37e7671c00..4f19aa2c32 100644 --- a/desktop/src/features/mesh-compute/ui/SidebarMeshComputeCard.tsx +++ b/desktop/src/features/mesh-compute/ui/SidebarMeshComputeCard.tsx @@ -8,6 +8,9 @@ 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 { requestOpenCreateAgent } from "@/features/agents/openCreateAgentEvent"; +import { deriveMeshCallToAction } from "../meshAgents"; import { useMeshDownloadProgress } from "../hooks/useMeshDownloadProgress"; import { useMeshLiveView } from "../hooks/useMeshLiveView"; import { useMeshNodeStatus } from "../hooks/useMeshNodeStatus"; @@ -99,6 +102,16 @@ export function SidebarMeshComputeCard({ const { progress: downloadProgress, reset: resetDownloadProgress } = 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(); + const callToAction = deriveMeshCallToAction({ + personas, + isSharing: toggle.isSharing, + meshHasCapacity: (snapshot?.sharingDeviceCount ?? 0) > 0, + }); + // Inbound work has no counter in mesh-llm, so it is inferred by elimination // across two samples: serving, inflight > 0, and our own dispatch count flat // means the work is not ours. Keeping the previous sample in a ref (not @@ -261,6 +274,17 @@ export function SidebarMeshComputeCard({ showSoloHint={model.showSoloHint} /> + {callToAction.kind === "createAgent" ? ( + + ) : null} + {errorText ? (